diff --git a/LICENSE.txt b/LICENSE.txt index 1b5f2163e..3d615ecb5 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -13,23 +13,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . -------------------------------------------------------------------------- - - All files under the directories briar-android/src, briar-api/src, - briar-core/src, briar-desktop/src and briar-test/src are licensed - under the Apache License, version 2.0 (the "License"); you may not - use these files except in compliance with the License. - - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied. See the License for the specific language governing - permissions and limitations under the License. - ------------------------------------------------------------------------- GNU GENERAL PUBLIC LICENSE diff --git a/briar-android/AndroidManifest.xml b/briar-android/AndroidManifest.xml index 5da35cdf4..254379442 100644 --- a/briar-android/AndroidManifest.xml +++ b/briar-android/AndroidManifest.xml @@ -134,7 +134,8 @@ + android:parentActivityName=".android.NavDrawerActivity" + android:windowSoftInputMode="adjustResize|stateHidden"> + + + + + + + image/svg+xml + + + + + + + + + diff --git a/briar-android/assets/emoji_activity.png b/briar-android/assets/emoji_activity.png new file mode 100644 index 000000000..908370dd1 Binary files /dev/null and b/briar-android/assets/emoji_activity.png differ diff --git a/briar-android/assets/emoji_animals_nature.png b/briar-android/assets/emoji_animals_nature.png new file mode 100644 index 000000000..7a0661fe1 Binary files /dev/null and b/briar-android/assets/emoji_animals_nature.png differ diff --git a/briar-android/assets/emoji_flags.png b/briar-android/assets/emoji_flags.png new file mode 100644 index 000000000..80f6bbd0d Binary files /dev/null and b/briar-android/assets/emoji_flags.png differ diff --git a/briar-android/assets/emoji_food_drink.png b/briar-android/assets/emoji_food_drink.png new file mode 100644 index 000000000..33d7cd0a6 Binary files /dev/null and b/briar-android/assets/emoji_food_drink.png differ diff --git a/briar-android/assets/emoji_objects.png b/briar-android/assets/emoji_objects.png new file mode 100644 index 000000000..f2b6dfbf9 Binary files /dev/null and b/briar-android/assets/emoji_objects.png differ diff --git a/briar-android/assets/emoji_smiley_people.png b/briar-android/assets/emoji_smiley_people.png new file mode 100644 index 000000000..9325d703a Binary files /dev/null and b/briar-android/assets/emoji_smiley_people.png differ diff --git a/briar-android/assets/emoji_symbols.png b/briar-android/assets/emoji_symbols.png new file mode 100644 index 000000000..e88275b2f Binary files /dev/null and b/briar-android/assets/emoji_symbols.png differ diff --git a/briar-android/assets/emoji_travel_places.png b/briar-android/assets/emoji_travel_places.png new file mode 100644 index 000000000..16e2be37d Binary files /dev/null and b/briar-android/assets/emoji_travel_places.png differ diff --git a/briar-android/build.gradle b/briar-android/build.gradle index dc93b62fb..469df63b2 100644 --- a/briar-android/build.gradle +++ b/briar-android/build.gradle @@ -35,6 +35,8 @@ dependencies { compile 'com.google.zxing:core:3.2.1' apt 'com.google.dagger:dagger-compiler:2.0.2' provided 'javax.annotation:jsr250-api:1.0' + compile 'com.jpardogo.materialtabstrip:library:1.1.0' + compile 'com.github.bumptech.glide:glide:3.7.0' testCompile 'junit:junit:4.12' testCompile 'net.jodah:concurrentunit:0.4.2' @@ -60,6 +62,8 @@ dependencyVerification { 'com.android.support:support-vector-drawable:799bafe4c3de812386f0b291f744d5d6876452722dd40189b9ab87dbbf594ea1', 'com.android.support:recyclerview-v7:44040a888e23e0c93162a3377cfe06751080e3c22d369ab0d4301ef60d63b0fe', 'com.android.support:cardview-v7:4595f1c4a28cfa083b6c0920ad4d49e1c2ca4b8302a955e548f68eb63b74931b', + 'com.jpardogo.materialtabstrip:library:24d19232b319f8c73e25793432357919a7ed972186f57a3b2c9093ea74ad8311', + 'com.github.bumptech.glide:glide:76ef123957b5fbaebb05fcbe6606dd58c3bc3fcdadb257f99811d0ac9ea9b88b', ] } diff --git a/briar-android/proguard-rules.txt b/briar-android/proguard-rules.txt index 6a936b7e8..94cfcea72 100644 --- a/briar-android/proguard-rules.txt +++ b/briar-android/proguard-rules.txt @@ -60,3 +60,8 @@ -dontwarn java.nio.** -dontwarn org.codehaus.mojo.animal_sniffer.** -dontwarn org.slf4j.impl.** + +-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** { + **[] $VALUES; + public *; +} \ No newline at end of file diff --git a/briar-android/res/drawable/ic_backspace_black.xml b/briar-android/res/drawable/ic_backspace_black.xml new file mode 100644 index 000000000..b0224405a --- /dev/null +++ b/briar-android/res/drawable/ic_backspace_black.xml @@ -0,0 +1,10 @@ + + + diff --git a/briar-android/res/drawable/ic_emoji_activity.xml b/briar-android/res/drawable/ic_emoji_activity.xml new file mode 100644 index 000000000..cdec17110 --- /dev/null +++ b/briar-android/res/drawable/ic_emoji_activity.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/briar-android/res/drawable/ic_emoji_animals_nature.xml b/briar-android/res/drawable/ic_emoji_animals_nature.xml new file mode 100644 index 000000000..d63c31365 --- /dev/null +++ b/briar-android/res/drawable/ic_emoji_animals_nature.xml @@ -0,0 +1,9 @@ + + + diff --git a/briar-android/res/drawable/ic_emoji_emoticons.xml b/briar-android/res/drawable/ic_emoji_emoticons.xml new file mode 100644 index 000000000..cb97f4e15 --- /dev/null +++ b/briar-android/res/drawable/ic_emoji_emoticons.xml @@ -0,0 +1,28 @@ + + + + + \ No newline at end of file diff --git a/briar-android/res/drawable/ic_emoji_flags.xml b/briar-android/res/drawable/ic_emoji_flags.xml new file mode 100644 index 000000000..68976f28c --- /dev/null +++ b/briar-android/res/drawable/ic_emoji_flags.xml @@ -0,0 +1,9 @@ + + + diff --git a/briar-android/res/drawable/ic_emoji_food_drink.xml b/briar-android/res/drawable/ic_emoji_food_drink.xml new file mode 100644 index 000000000..958664070 --- /dev/null +++ b/briar-android/res/drawable/ic_emoji_food_drink.xml @@ -0,0 +1,9 @@ + + + diff --git a/briar-android/res/drawable/ic_emoji_objects.xml b/briar-android/res/drawable/ic_emoji_objects.xml new file mode 100644 index 000000000..70f0ec6ea --- /dev/null +++ b/briar-android/res/drawable/ic_emoji_objects.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/briar-android/res/drawable/ic_emoji_recent.xml b/briar-android/res/drawable/ic_emoji_recent.xml new file mode 100644 index 000000000..3f891e2e4 --- /dev/null +++ b/briar-android/res/drawable/ic_emoji_recent.xml @@ -0,0 +1,9 @@ + + + diff --git a/briar-android/res/drawable/ic_emoji_smiley_people.xml b/briar-android/res/drawable/ic_emoji_smiley_people.xml new file mode 100644 index 000000000..12dfdcd2e --- /dev/null +++ b/briar-android/res/drawable/ic_emoji_smiley_people.xml @@ -0,0 +1,9 @@ + + + diff --git a/briar-android/res/drawable/ic_emoji_symbols.xml b/briar-android/res/drawable/ic_emoji_symbols.xml new file mode 100644 index 000000000..28d8ff8cf --- /dev/null +++ b/briar-android/res/drawable/ic_emoji_symbols.xml @@ -0,0 +1,9 @@ + + + diff --git a/briar-android/res/drawable/ic_emoji_toggle.xml b/briar-android/res/drawable/ic_emoji_toggle.xml new file mode 100644 index 000000000..b497d47fd --- /dev/null +++ b/briar-android/res/drawable/ic_emoji_toggle.xml @@ -0,0 +1,10 @@ + + + diff --git a/briar-android/res/drawable/ic_emoji_travel_places.xml b/briar-android/res/drawable/ic_emoji_travel_places.xml new file mode 100644 index 000000000..a0534fbbf --- /dev/null +++ b/briar-android/res/drawable/ic_emoji_travel_places.xml @@ -0,0 +1,9 @@ + + + diff --git a/briar-android/res/drawable/ic_keyboard_black.xml b/briar-android/res/drawable/ic_keyboard_black.xml new file mode 100644 index 000000000..f1f26791a --- /dev/null +++ b/briar-android/res/drawable/ic_keyboard_black.xml @@ -0,0 +1,10 @@ + + + diff --git a/briar-android/res/layout/activity_conversation.xml b/briar-android/res/layout/activity_conversation.xml index 8f0fa1a34..cfd7cd4d2 100644 --- a/briar-android/res/layout/activity_conversation.xml +++ b/briar-android/res/layout/activity_conversation.xml @@ -3,9 +3,9 @@ xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" - android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" + android:orientation="vertical" tools:context=".android.contact.ConversationActivity"> - + android:layout_weight="1" + android:background="@color/window_background"/> - - - + android:layout_height="wrap_content" + android:background="@color/button_bar_background" + android:elevation="@dimen/margin_tiny"/> \ No newline at end of file diff --git a/briar-android/res/layout/activity_forum.xml b/briar-android/res/layout/activity_forum.xml index 946ce34e8..f8e1b609c 100644 --- a/briar-android/res/layout/activity_forum.xml +++ b/briar-android/res/layout/activity_forum.xml @@ -6,16 +6,18 @@ android:layout_height="match_parent" android:orientation="vertical"> - - + android:layout_height="wrap_content" + android:background="@color/button_bar_background" + android:elevation="@dimen/margin_tiny"/> \ No newline at end of file diff --git a/briar-android/res/layout/activity_invitations.xml b/briar-android/res/layout/activity_invitations.xml index 25cd76af4..4d3d61cea 100644 --- a/briar-android/res/layout/activity_invitations.xml +++ b/briar-android/res/layout/activity_invitations.xml @@ -1,5 +1,5 @@ - - - - - + + + + + + + + + + + + + + + + + + + diff --git a/briar-android/res/layout/emoji_grid_layout.xml b/briar-android/res/layout/emoji_grid_layout.xml new file mode 100644 index 000000000..c1486e2dc --- /dev/null +++ b/briar-android/res/layout/emoji_grid_layout.xml @@ -0,0 +1,19 @@ + + + + + + \ No newline at end of file diff --git a/briar-android/res/layout/fragment_blog.xml b/briar-android/res/layout/fragment_blog.xml index c0ac19fbe..bfc1c777a 100644 --- a/briar-android/res/layout/fragment_blog.xml +++ b/briar-android/res/layout/fragment_blog.xml @@ -1,5 +1,5 @@ - - - - diff --git a/briar-android/res/layout/introduction_contact_chooser.xml b/briar-android/res/layout/introduction_contact_chooser.xml index 8363191ac..4a99ecbed 100644 --- a/briar-android/res/layout/introduction_contact_chooser.xml +++ b/briar-android/res/layout/introduction_contact_chooser.xml @@ -1,5 +1,5 @@ - - - - - - - - - - - - - - + android:layout_alignLeft="@id/text" + android:layout_below="@id/text" + app:persona="commenter"/> - - - - - - - - - - - - - diff --git a/briar-android/res/layout/text_input_view.xml b/briar-android/res/layout/text_input_view.xml new file mode 100644 index 000000000..6b1aeba51 --- /dev/null +++ b/briar-android/res/layout/text_input_view.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + diff --git a/briar-android/res/values/color.xml b/briar-android/res/values/color.xml index 0ac3140ee..daf3f8b3b 100644 --- a/briar-android/res/values/color.xml +++ b/briar-android/res/values/color.xml @@ -27,6 +27,9 @@ #80ffffff #06b9ff #ff0000 + #ff000000 + + @color/window_background @color/briar_accent diff --git a/briar-android/res/values/dimens.xml b/briar-android/res/values/dimens.xml index c8e7eca7d..f09530cc4 100644 --- a/briar-android/res/values/dimens.xml +++ b/briar-android/res/values/dimens.xml @@ -46,4 +46,17 @@ 15dp 20dp + + 16sp + 32sp + 2dp + 5dp + 2dp + + + 50dp + 220dp + 110dp + 170dp + diff --git a/briar-android/res/values/emoji.xml b/briar-android/res/values/emoji.xml new file mode 100644 index 000000000..ab8e62d01 --- /dev/null +++ b/briar-android/res/values/emoji.xml @@ -0,0 +1,1352 @@ + + + + + + 1f3c1 + 1f3f3 + 1f3f4 + 1f6a9 + 1f3e7 + 1f6ae + 1f6b0 + 267f + 1f6b9 + 1f6ba + 1f6bb + 1f6bc + 1f6be + 1f6c2 + 1f6c3 + 1f6c4 + 1f6c5 + 26a0 + 1f6b8 + 26d4 + 1f6ab + 1f6b3 + 1f6ad + 1f6af + 1f6b1 + 1f6b7 + 2622 + 2623 + 2b06 + 2197 + 27a1 + 2198 + 2b07 + 2199 + 2b05 + 2196 + 2195 + 2194 + 21a9 + 21aa + 2934 + 2935 + 1f503 + 1f504 + 1f519 + 1f51a + 1f51b + 1f51c + 1f51d + 1f6d0 + 269b + 1f549 + 2721 + 2638 + 262f + 271d + 2626 + 262a + 262e + 1f54e + 1f52f + 267b + 1f4db + 269c + 1f530 + 1f531 + 2b55 + 2705 + 2611 + 2714 + 2716 + 274c + 274e + 2795 + 2796 + 2797 + 27b0 + 27bf + 303d + 2733 + 2734 + 2747 + 1f4b1 + 1f4b2 + 203c + 2049 + 2753 + 2754 + 2755 + 2757 + 3030 + a9 + ae + 2122 + 2648 + 2649 + 264a + 264b + 264c + 264d + 264e + 264f + 2650 + 2651 + 2652 + 2653 + 26ce + 1f500 + 1f501 + 1f502 + 25b6 + 23e9 + 23ed + 23ef + 25c0 + 23ea + 23ee + 1f53c + 23eb + 1f53d + 23ec + 23f8 + 23f9 + 23fa + 23cf + 1f3a6 + 1f505 + 1f506 + 1f4f6 + 1f4f5 + 1f4f3 + 1f4f4 + 23,20e3 + 2a,20e3 + 30,20e3 + 31,20e3 + 32,20e3 + 33,20e3 + 34,20e3 + 35,20e3 + 36,20e3 + 37,20e3 + 38,20e3 + 39,20e3 + 1f51f + 1f4af + 1f51e + 1f520 + 1f521 + 1f522 + 1f523 + 1f524 + 1f170 + 1f18e + 1f171 + 1f191 + 1f192 + 1f193 + 2139 + 1f194 + 24c2 + 1f195 + 1f196 + 1f17e + 1f197 + 1f17f + 1f198 + 1f199 + 1f19a + 1f201 + 1f202 + 1f237 + 1f236 + 1f22f + 1f250 + 1f239 + 1f21a + 1f232 + 1f251 + 1f238 + 1f234 + 1f233 + 3297 + 3299 + 1f23a + 1f235 + 25aa + 25ab + 25fb + 25fc + 25fd + 25fe + 2b1b + 2b1c + 1f536 + 1f537 + 1f538 + 1f539 + 1f53a + 1f53b + 1f4a0 + 1f518 + 1f532 + 1f533 + 26aa + 26ab + 1f534 + 1f535 + + + 1f435 + 1f412 + 1f436 + 1f415 + 1f429 + 1f43a + 1f431 + 1f408 + 1f981 + 1f42f + 1f405 + 1f406 + 1f434 + 1f40e + 1f984 + 1f42e + 1f402 + 1f403 + 1f404 + 1f437 + 1f416 + 1f417 + 1f43d + 1f40f + 1f411 + 1f410 + 1f42a + 1f42b + 1f418 + 1f42d + 1f401 + 1f400 + 1f439 + 1f430 + 1f407 + 1f43f + 1f43b + 1f428 + 1f43c + 1f43e + 1f983 + 1f414 + 1f413 + 1f423 + 1f424 + 1f425 + 1f426 + 1f427 + 1f54a + 1f438 + 1f40a + 1f422 + 1f40d + 1f432 + 1f409 + 1f433 + 1f40b + 1f42c + 1f41f + 1f420 + 1f421 + 1f419 + 1f41a + 1f980 + 1f40c + 1f41b + 1f41c + 1f41d + 1f41e + 1f577 + 1f578 + 1f982 + 1f490 + 1f338 + 1f4ae + 1f3f5 + 1f339 + 1f33a + 1f33b + 1f33c + 1f337 + 2618 + 1f331 + 1f332 + 1f333 + 1f334 + 1f335 + 1f33e + 1f33f + 1f340 + 1f341 + 1f342 + 1f343 + + + 1f600 + 1f601 + 1f602 + 1f603 + 1f604 + 1f605 + 1f606 + 1f609 + 1f60a + 1f60b + 1f60e + 1f60d + 1f618 + 1f617 + 1f619 + 1f61a + 263a + 1f642 + 1f917 + 1f607 + 1f914 + 1f610 + 1f611 + 1f636 + 1f644 + 1f60f + 1f623 + 1f625 + 1f62e + 1f910 + 1f62f + 1f62a + 1f62b + 1f634 + 1f60c + 1f913 + 1f61b + 1f61c + 1f61d + 2639 + 1f641 + 1f612 + 1f613 + 1f614 + 1f615 + 1f616 + 1f643 + 1f637 + 1f912 + 1f915 + 1f911 + 1f632 + 1f61e + 1f61f + 1f624 + 1f622 + 1f62d + 1f626 + 1f627 + 1f628 + 1f629 + 1f62c + 1f630 + 1f631 + 1f633 + 1f635 + 1f621 + 1f620 + 1f608 + 1f47f + 1f479 + 1f47a + 1f480 + 2620 + 1f47b + 1f47d + 1f47e + 1f916 + 1f4a9 + 1f63a + 1f638 + 1f639 + 1f63b + 1f63c + 1f63d + 1f640 + 1f63f + 1f63e + 1f648 + 1f649 + 1f64a + 1f466 + 1f467 + 1f468 + 1f469 + 1f474 + 1f475 + 1f476 + 1f471 + 1f46e + 1f472 + 1f473 + 1f477 + 26d1 + 1f478 + 1f482 + 1f575 + 1f385 + 1f47c + 1f46f + 1f486 + 1f487 + 1f470 + 1f64d + 1f64e + 1f645 + 1f646 + 1f481 + 1f64b + 1f647 + 1f64c + 1f64f + 1f5e3 + 1f464 + 1f465 + 1f6b6 + 1f3c3 + 1f483 + 1f574 + 1f46b + 1f46c + 1f46d + 1f48f + 1f468,200d,2764,fe0f,200d,1f48b,200d,1f468 + 1f469,200d,2764,fe0f,200d,1f48b,200d,1f469 + 1f491 + 1f468,200d,2764,fe0f,200d,1f468 + 1f469,200d,2764,fe0f,200d,1f469 + 1f46a + 1f468,200d,1f468,200d,1f466 + 1f468,200d,1f468,200d,1f466,200d,1f466 + 1f468,200d,1f468,200d,1f467 + 1f468,200d,1f468,200d,1f467,200d,1f466 + 1f468,200d,1f468,200d,1f467,200d,1f467 + 1f468,200d,1f469,200d,1f466 + 1f468,200d,1f469,200d,1f466,200d,1f466 + 1f468,200d,1f469,200d,1f467 + 1f468,200d,1f469,200d,1f467,200d,1f466 + 1f468,200d,1f469,200d,1f467,200d,1f467 + 1f469,200d,1f469,200d,1f466 + 1f469,200d,1f469,200d,1f466,200d,1f466 + 1f469,200d,1f469,200d,1f467 + 1f469,200d,1f469,200d,1f467,200d,1f466 + 1f469,200d,1f469,200d,1f467,200d,1f467 + 1f3fb + 1f3fc + 1f3fd + 1f3fe + 1f3ff + 1f4aa + 1f448 + 1f449 + 261d + 1f446 + 1f595 + 1f447 + 270c + 1f596 + 1f918 + 1f590 + 270a + 270b + 1f44a + 1f44c + 1f44d + 1f44e + 1f44b + 1f44f + 1f450 + 270d + 1f485 + 1f442 + 1f443 + 1f463 + 1f440 + 1f441 + 1f445 + 1f444 + 1f48b + 1f498 + 2764 + 1f493 + 1f494 + 1f495 + 1f496 + 1f497 + 1f499 + 1f49a + 1f49b + 1f49c + 1f49d + 1f49e + 1f49f + 2763 + 1f48c + 1f4a4 + 1f4a2 + 1f4a3 + 1f4a5 + 1f4a6 + 1f4a8 + 1f4ab + 1f4ac + 1f5e8 + 1f5ef + 1f4ad + 1f441,200d,1f5e8 + 1f573 + 1f453 + 1f576 + 1f454 + 1f455 + 1f456 + 1f457 + 1f458 + 1f459 + 1f45a + 1f45b + 1f45c + 1f45d + 1f6cd + 1f392 + 1f45e + 1f45f + 1f460 + 1f461 + 1f462 + 1f451 + 1f452 + 1f3a9 + 1f393 + 1f4ff + 1f484 + 1f48d + 1f48e + + + 1f347 + 1f348 + 1f349 + 1f34a + 1f34b + 1f34c + 1f34d + 1f34e + 1f34f + 1f350 + 1f351 + 1f352 + 1f353 + 1f345 + 1f346 + 1f33d + 1f336 + 1f344 + 1f330 + 1f35e + 1f9c0 + 1f356 + 1f357 + 1f354 + 1f35f + 1f355 + 1f32d + 1f32e + 1f32f + 1f37f + 1f372 + 1f371 + 1f358 + 1f359 + 1f35a + 1f35b + 1f35c + 1f35d + 1f360 + 1f362 + 1f363 + 1f364 + 1f365 + 1f361 + 1f366 + 1f367 + 1f368 + 1f369 + 1f36a + 1f382 + 1f370 + 1f36b + 1f36c + 1f36d + 1f36e + 1f36f + 1f37c + 2615 + 1f375 + 1f376 + 1f37e + 1f377 + 1f378 + 1f379 + 1f37a + 1f37b + 1f37d + 1f374 + 1f373 + 1f3fa + + + 1f507 + 1f508 + 1f509 + 1f50a + 1f4e2 + 1f4e3 + 1f4ef + 1f514 + 1f515 + 1f3bc + 1f3b5 + 1f3b6 + 1f399 + 1f39a + 1f39b + 1f3a4 + 1f3a7 + 1f3b7 + 1f3b8 + 1f3b9 + 1f3ba + 1f3bb + 1f4fb + 1f4f1 + 1f4f2 + 260e + 1f4de + 1f4df + 1f4e0 + 1f50b + 1f50c + 1f4bb + 1f5a5 + 1f5a8 + 2328 + 1f5b1 + 1f5b2 + 1f4bd + 1f4be + 1f4bf + 1f4c0 + 1f3a5 + 1f3ac + 1f4fd + 1f4fa + 1f4f7 + 1f4f8 + 1f4f9 + 1f4fc + 1f50d + 1f50e + 1f52c + 1f52d + 1f4e1 + 1f56f + 1f4a1 + 1f526 + 1f3ee + 1f4d4 + 1f4d5 + 1f4d6 + 1f4d7 + 1f4d8 + 1f4d9 + 1f4da + 1f4d3 + 1f4d2 + 1f4c3 + 1f4dc + 1f4c4 + 1f4f0 + 1f5de + 1f4d1 + 1f516 + 1f4b0 + 1f4b4 + 1f4b5 + 1f4b6 + 1f4b7 + 1f4b8 + 1f4b3 + 1f4b9 + 2709 + 1f4e7 + 1f4e8 + 1f4e9 + 1f4e4 + 1f4e5 + 1f4e6 + 1f4eb + 1f4ea + 1f4ec + 1f4ed + 1f4ee + 1f5f3 + 270f + 2712 + 1f58b + 1f58a + 1f58c + 1f58d + 1f4dd + 1f4bc + 1f4c1 + 1f4c2 + 1f5c2 + 1f4c5 + 1f4c6 + 1f5d2 + 1f5d3 + 1f4c7 + 1f4c8 + 1f4c9 + 1f4ca + 1f4cb + 1f4cc + 1f4cd + 1f4ce + 1f587 + 1f4cf + 1f4d0 + 2702 + 1f5c3 + 1f5c4 + 1f5d1 + 1f512 + 1f513 + 1f50f + 1f510 + 1f511 + 1f5dd + 1f528 + 26cf + 2692 + 1f6e0 + 1f527 + 1f529 + 2699 + 1f5dc + 2697 + 2696 + 1f517 + 26d3 + 1f489 + 1f48a + 1f5e1 + 1f52a + 2694 + 1f52b + 1f6e1 + 1f3f9 + 1f6ac + 26b0 + 26b1 + 1f5ff + 1f6e2 + 1f52e + + + 1f383 + 1f384 + 1f386 + 1f387 + 2728 + 1f388 + 1f389 + 1f38a + 1f38b + 1f38c + 1f38d + 1f38e + 1f38f + 1f390 + 1f391 + 1f380 + 1f381 + 1f396 + 1f397 + 1f39e + 1f39f + 1f3ab + 1f3f7 + 26bd + 26be + 1f3c0 + 1f3c8 + 1f3c9 + 1f3be + 1f3b1 + 1f3b3 + 26f3 + 1f3cc + 26f8 + 1f3a3 + 1f3bd + 1f3bf + 26f7 + 1f3c2 + 1f3c4 + 1f3c7 + 1f3ca + 26f9 + 1f3cb + 1f6b4 + 1f6b5 + 1f3ce + 1f3cd + 1f3c5 + 1f3c6 + 1f3cf + 1f3d0 + 1f3d1 + 1f3d2 + 1f3d3 + 1f3f8 + 1f3af + 1f3ae + 1f579 + 1f3b2 + 2660 + 2665 + 2666 + 2663 + 1f0cf + 1f004 + 1f3b4 + + + 1f30d + 1f30e + 1f30f + 1f310 + 1f5fa + 1f3d4 + 26f0 + 1f30b + 1f5fb + 1f3d5 + 1f3d6 + 1f3dc + 1f3dd + 1f3de + 1f3df + 1f3db + 1f3d7 + 1f3d8 + 1f3d9 + 1f3da + 1f3e0 + 1f3e1 + 26ea + 1f54b + 1f54c + 1f54d + 26e9 + 1f3e2 + 1f3e3 + 1f3e4 + 1f3e5 + 1f3e6 + 1f3e8 + 1f3e9 + 1f3ea + 1f3eb + 1f3ec + 1f3ed + 1f3ef + 1f3f0 + 1f492 + 1f5fc + 1f5fd + 1f5fe + 26f2 + 26fa + 1f301 + 1f303 + 1f304 + 1f305 + 1f306 + 1f307 + 1f309 + 2668 + 1f30c + 1f3a0 + 1f3a1 + 1f3a2 + 1f488 + 1f3aa + 1f3ad + 1f5bc + 1f3a8 + 1f3b0 + 1f682 + 1f683 + 1f684 + 1f685 + 1f686 + 1f687 + 1f688 + 1f689 + 1f68a + 1f69d + 1f69e + 1f68b + 1f68c + 1f68d + 1f68e + 1f68f + 1f690 + 1f691 + 1f692 + 1f693 + 1f694 + 1f695 + 1f696 + 1f697 + 1f698 + 1f699 + 1f69a + 1f69b + 1f69c + 1f6b2 + 26fd + 1f6e3 + 1f6e4 + 1f6a8 + 1f6a5 + 1f6a6 + 1f6a7 + 2693 + 26f5 + 1f6a3 + 1f6a4 + 1f6f3 + 26f4 + 1f6e5 + 1f6a2 + 2708 + 1f6e9 + 1f6eb + 1f6ec + 1f4ba + 1f681 + 1f69f + 1f6a0 + 1f6a1 + 1f680 + 1f6f0 + 1f6ce + 1f6aa + 1f6cc + 1f6cf + 1f6cb + 1f6bd + 1f6bf + 1f6c0 + 1f6c1 + 231b + 23f3 + 231a + 23f0 + 23f1 + 23f2 + 1f570 + 1f55b + 1f567 + 1f550 + 1f55c + 1f551 + 1f55d + 1f552 + 1f55e + 1f553 + 1f55f + 1f554 + 1f560 + 1f555 + 1f561 + 1f556 + 1f562 + 1f557 + 1f563 + 1f558 + 1f564 + 1f559 + 1f565 + 1f55a + 1f566 + 1f311 + 1f312 + 1f313 + 1f314 + 1f315 + 1f316 + 1f317 + 1f318 + 1f319 + 1f31a + 1f31b + 1f31c + 1f321 + 2600 + 1f31d + 1f31e + 2b50 + 1f31f + 1f320 + 2601 + 26c5 + 26c8 + 1f324 + 1f325 + 1f326 + 1f327 + 1f328 + 1f329 + 1f32a + 1f32b + 1f32c + 1f300 + 1f308 + 1f302 + 2602 + 2614 + 26f1 + 26a1 + 2744 + 2603 + 26c4 + 2604 + 1f525 + 1f4a7 + 1f30a + + + 1f1e6,1f1e8 + 1f1e6,1f1e9 + 1f1e6,1f1ea + 1f1e6,1f1eb + 1f1e6,1f1ec + 1f1e6,1f1ee + 1f1e6,1f1f1 + 1f1e6,1f1f2 + 1f1e6,1f1f4 + 1f1e6,1f1f6 + 1f1e6,1f1f7 + 1f1e6,1f1f8 + 1f1e6,1f1f9 + 1f1e6,1f1fa + 1f1e6,1f1fc + 1f1e6,1f1fd + 1f1e6,1f1ff + 1f1e7,1f1e6 + 1f1e7,1f1e7 + 1f1e7,1f1e9 + 1f1e7,1f1ea + 1f1e7,1f1eb + 1f1e7,1f1ec + 1f1e7,1f1ed + 1f1e7,1f1ee + 1f1e7,1f1ef + 1f1e7,1f1f1 + 1f1e7,1f1f2 + 1f1e7,1f1f3 + 1f1e7,1f1f4 + 1f1e7,1f1f6 + 1f1e7,1f1f7 + 1f1e7,1f1f8 + 1f1e7,1f1f9 + 1f1e7,1f1fb + 1f1e7,1f1fc + 1f1e7,1f1fe + 1f1e7,1f1ff + 1f1e8,1f1e6 + 1f1e8,1f1e8 + 1f1e8,1f1e9 + 1f1e8,1f1eb + 1f1e8,1f1ec + 1f1e8,1f1ed + 1f1e8,1f1ee + 1f1e8,1f1f0 + 1f1e8,1f1f1 + 1f1e8,1f1f2 + 1f1e8,1f1f3 + 1f1e8,1f1f4 + 1f1e8,1f1f5 + 1f1e8,1f1f7 + 1f1e8,1f1fa + 1f1e8,1f1fb + 1f1e8,1f1fc + 1f1e8,1f1fd + 1f1e8,1f1fe + 1f1e8,1f1ff + 1f1e9,1f1ea + 1f1e9,1f1ec + 1f1e9,1f1ef + 1f1e9,1f1f0 + 1f1e9,1f1f2 + 1f1e9,1f1f4 + 1f1e9,1f1ff + 1f1ea,1f1e6 + 1f1ea,1f1e8 + 1f1ea,1f1ea + 1f1ea,1f1ec + 1f1ea,1f1ed + 1f1ea,1f1f7 + 1f1ea,1f1f8 + 1f1ea,1f1f9 + 1f1ea,1f1fa + 1f1eb,1f1ee + 1f1eb,1f1ef + 1f1eb,1f1f0 + 1f1eb,1f1f2 + 1f1eb,1f1f4 + 1f1eb,1f1f7 + 1f1ec,1f1e6 + 1f1ec,1f1e7 + 1f1ec,1f1e9 + 1f1ec,1f1ea + 1f1ec,1f1eb + 1f1ec,1f1ec + 1f1ec,1f1ed + 1f1ec,1f1ee + 1f1ec,1f1f1 + 1f1ec,1f1f2 + 1f1ec,1f1f3 + 1f1ec,1f1f5 + 1f1ec,1f1f6 + 1f1ec,1f1f7 + 1f1ec,1f1f8 + 1f1ec,1f1f9 + 1f1ec,1f1fa + 1f1ec,1f1fc + 1f1ec,1f1fe + 1f1ed,1f1f0 + 1f1ed,1f1f2 + 1f1ed,1f1f3 + 1f1ed,1f1f7 + 1f1ed,1f1f9 + 1f1ed,1f1fa + 1f1ee,1f1e8 + 1f1ee,1f1e9 + 1f1ee,1f1ea + 1f1ee,1f1f1 + 1f1ee,1f1f2 + 1f1ee,1f1f3 + 1f1ee,1f1f4 + 1f1ee,1f1f6 + 1f1ee,1f1f7 + 1f1ee,1f1f8 + 1f1ee,1f1f9 + 1f1ef,1f1ea + 1f1ef,1f1f2 + 1f1ef,1f1f4 + 1f1ef,1f1f5 + 1f1f0,1f1ea + 1f1f0,1f1ec + 1f1f0,1f1ed + 1f1f0,1f1ee + 1f1f0,1f1f2 + 1f1f0,1f1f3 + 1f1f0,1f1f5 + 1f1f0,1f1f7 + 1f1f0,1f1fc + 1f1f0,1f1fe + 1f1f0,1f1ff + 1f1f1,1f1e6 + 1f1f1,1f1e7 + 1f1f1,1f1e8 + 1f1f1,1f1ee + 1f1f1,1f1f0 + 1f1f1,1f1f7 + 1f1f1,1f1f8 + 1f1f1,1f1f9 + 1f1f1,1f1fa + 1f1f1,1f1fb + 1f1f1,1f1fe + 1f1f2,1f1e6 + 1f1f2,1f1e8 + 1f1f2,1f1e9 + 1f1f2,1f1ea + 1f1f2,1f1eb + 1f1f2,1f1ec + 1f1f2,1f1ed + 1f1f2,1f1f0 + 1f1f2,1f1f1 + 1f1f2,1f1f2 + 1f1f2,1f1f3 + 1f1f2,1f1f4 + 1f1f2,1f1f5 + 1f1f2,1f1f6 + 1f1f2,1f1f7 + 1f1f2,1f1f8 + 1f1f2,1f1f9 + 1f1f2,1f1fa + 1f1f2,1f1fb + 1f1f2,1f1fc + 1f1f2,1f1fd + 1f1f2,1f1fe + 1f1f2,1f1ff + 1f1f3,1f1e6 + 1f1f3,1f1e8 + 1f1f3,1f1ea + 1f1f3,1f1eb + 1f1f3,1f1ec + 1f1f3,1f1ee + 1f1f3,1f1f1 + 1f1f3,1f1f4 + 1f1f3,1f1f5 + 1f1f3,1f1f7 + 1f1f3,1f1fa + 1f1f3,1f1ff + 1f1f4,1f1f2 + 1f1f5,1f1e6 + 1f1f5,1f1ea + 1f1f5,1f1eb + 1f1f5,1f1ec + 1f1f5,1f1ed + 1f1f5,1f1f0 + 1f1f5,1f1f1 + 1f1f5,1f1f2 + 1f1f5,1f1f3 + 1f1f5,1f1f7 + 1f1f5,1f1f8 + 1f1f5,1f1f9 + 1f1f5,1f1fc + 1f1f5,1f1fe + 1f1f6,1f1e6 + 1f1f7,1f1ea + 1f1f7,1f1f4 + 1f1f7,1f1f8 + 1f1f7,1f1fa + 1f1f7,1f1fc + 1f1f8,1f1e6 + 1f1f8,1f1e7 + 1f1f8,1f1e8 + 1f1f8,1f1e9 + 1f1f8,1f1ea + 1f1f8,1f1ec + 1f1f8,1f1ed + 1f1f8,1f1ee + 1f1f8,1f1ef + 1f1f8,1f1f0 + 1f1f8,1f1f1 + 1f1f8,1f1f2 + 1f1f8,1f1f3 + 1f1f8,1f1f4 + 1f1f8,1f1f7 + 1f1f8,1f1f8 + 1f1f8,1f1f9 + 1f1f8,1f1fb + 1f1f8,1f1fd + 1f1f8,1f1fe + 1f1f8,1f1ff + 1f1f9,1f1e6 + 1f1f9,1f1e8 + 1f1f9,1f1e9 + 1f1f9,1f1eb + 1f1f9,1f1ec + 1f1f9,1f1ed + 1f1f9,1f1ef + 1f1f9,1f1f0 + 1f1f9,1f1f1 + 1f1f9,1f1f2 + 1f1f9,1f1f3 + 1f1f9,1f1f4 + 1f1f9,1f1f7 + 1f1f9,1f1f9 + 1f1f9,1f1fb + 1f1f9,1f1fc + 1f1f9,1f1ff + 1f1fa,1f1e6 + 1f1fa,1f1ec + 1f1fa,1f1f2 + 1f1fa,1f1f8 + 1f1fa,1f1fe + 1f1fa,1f1ff + 1f1fb,1f1e6 + 1f1fb,1f1e8 + 1f1fb,1f1ea + 1f1fb,1f1ec + 1f1fb,1f1ee + 1f1fb,1f1f3 + 1f1fb,1f1fa + 1f1fc,1f1eb + 1f1fc,1f1f8 + 1f1fd,1f1f0 + 1f1fe,1f1ea + 1f1fe,1f1f9 + 1f1ff,1f1e6 + 1f1ff,1f1f2 + 1f1ff,1f1fc + + diff --git a/briar-android/src/org/briarproject/android/ActivityComponent.java b/briar-android/src/org/briarproject/android/ActivityComponent.java index baee29d41..3fbe8d3f2 100644 --- a/briar-android/src/org/briarproject/android/ActivityComponent.java +++ b/briar-android/src/org/briarproject/android/ActivityComponent.java @@ -40,6 +40,8 @@ import org.briarproject.android.sharing.ShareForumActivity; import org.briarproject.android.sharing.ShareForumMessageFragment; import org.briarproject.android.sharing.SharingStatusBlogActivity; import org.briarproject.android.sharing.SharingStatusForumActivity; +import org.thoughtcrime.securesms.components.emoji.EmojiProvider; +import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel; import dagger.Component; @@ -114,6 +116,9 @@ public interface ActivityComponent { void inject(RssFeedManageActivity activity); + void inject(EmojiProvider emojiProvider); + void inject(RecentEmojiPageModel recentEmojiPageModel); + // Fragments void inject(ContactListFragment fragment); void inject(ForumListFragment fragment); diff --git a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java index 4ff2695e3..c2bb93fbb 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogFragment.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogFragment.java @@ -25,7 +25,7 @@ import org.briarproject.android.controller.handler.UiResultExceptionHandler; import org.briarproject.android.fragment.BaseFragment; import org.briarproject.android.sharing.ShareBlogActivity; import org.briarproject.android.sharing.SharingStatusBlogActivity; -import org.briarproject.android.util.BriarRecyclerView; +import org.briarproject.android.view.BriarRecyclerView; import org.briarproject.api.blogs.BlogPostHeader; import org.briarproject.api.db.DbException; import org.briarproject.api.identity.Author; diff --git a/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java b/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java index b3e55f848..590a24fba 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogListAdapter.java @@ -15,7 +15,7 @@ import android.widget.TextView; import org.briarproject.R; import org.briarproject.android.util.AndroidUtils; -import org.briarproject.android.util.TextAvatarView; +import org.briarproject.android.view.TextAvatarView; import org.briarproject.api.blogs.Blog; import org.briarproject.api.sync.GroupId; diff --git a/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java b/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java index d870db137..a25bcd763 100644 --- a/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java +++ b/briar-android/src/org/briarproject/android/blogs/BlogPostViewHolder.java @@ -18,7 +18,7 @@ import android.widget.TextView; import org.briarproject.R; import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener; -import org.briarproject.android.util.AuthorView; +import org.briarproject.android.view.AuthorView; import org.briarproject.api.blogs.BlogCommentHeader; import org.briarproject.api.blogs.BlogPostHeader; import org.briarproject.api.identity.Author; diff --git a/briar-android/src/org/briarproject/android/blogs/FeedFragment.java b/briar-android/src/org/briarproject/android/blogs/FeedFragment.java index c7ed70622..a8a8cea9d 100644 --- a/briar-android/src/org/briarproject/android/blogs/FeedFragment.java +++ b/briar-android/src/org/briarproject/android/blogs/FeedFragment.java @@ -22,7 +22,7 @@ import org.briarproject.android.blogs.BlogPostAdapter.OnBlogPostClickListener; import org.briarproject.android.controller.handler.UiResultExceptionHandler; import org.briarproject.android.controller.handler.UiResultHandler; import org.briarproject.android.fragment.BaseFragment; -import org.briarproject.android.util.BriarRecyclerView; +import org.briarproject.android.view.BriarRecyclerView; import org.briarproject.api.blogs.Blog; import org.briarproject.api.blogs.BlogPostHeader; import org.briarproject.api.db.DbException; diff --git a/briar-android/src/org/briarproject/android/blogs/RssFeedManageActivity.java b/briar-android/src/org/briarproject/android/blogs/RssFeedManageActivity.java index 69c50dde6..60230717b 100644 --- a/briar-android/src/org/briarproject/android/blogs/RssFeedManageActivity.java +++ b/briar-android/src/org/briarproject/android/blogs/RssFeedManageActivity.java @@ -14,7 +14,7 @@ import org.briarproject.R; import org.briarproject.android.ActivityComponent; import org.briarproject.android.BriarActivity; import org.briarproject.android.blogs.RssFeedAdapter.RssFeedListener; -import org.briarproject.android.util.BriarRecyclerView; +import org.briarproject.android.view.BriarRecyclerView; import org.briarproject.api.db.DbException; import org.briarproject.api.feed.Feed; import org.briarproject.api.feed.FeedManager; diff --git a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java index bead9c4db..312184f77 100644 --- a/briar-android/src/org/briarproject/android/contact/ContactListFragment.java +++ b/briar-android/src/org/briarproject/android/contact/ContactListFragment.java @@ -20,7 +20,7 @@ import org.briarproject.android.ActivityComponent; import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.fragment.BaseFragment; import org.briarproject.android.keyagreement.KeyAgreementActivity; -import org.briarproject.android.util.BriarRecyclerView; +import org.briarproject.android.view.BriarRecyclerView; import org.briarproject.api.blogs.BlogSharingManager; import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; diff --git a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java index a85723601..305577730 100644 --- a/briar-android/src/org/briarproject/android/contact/ConversationActivity.java +++ b/briar-android/src/org/briarproject/android/contact/ConversationActivity.java @@ -16,9 +16,6 @@ import android.util.SparseArray; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; -import android.view.View; -import android.view.View.OnClickListener; -import android.widget.EditText; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; @@ -27,8 +24,11 @@ import org.briarproject.R; import org.briarproject.android.ActivityComponent; import org.briarproject.android.BriarActivity; import org.briarproject.android.api.AndroidNotificationManager; +import org.briarproject.android.contact.ConversationAdapter.IntroductionHandler; import org.briarproject.android.introduction.IntroductionActivity; -import org.briarproject.android.util.BriarRecyclerView; +import org.briarproject.android.view.BriarRecyclerView; +import org.briarproject.android.view.TextInputView; +import org.briarproject.android.view.TextInputView.TextInputListener; import org.briarproject.api.FormatException; import org.briarproject.api.blogs.BlogSharingManager; import org.briarproject.api.clients.SessionId; @@ -93,8 +93,7 @@ import static org.briarproject.android.contact.ConversationItem.IncomingItem; import static org.briarproject.android.contact.ConversationItem.OutgoingItem; public class ConversationActivity extends BriarActivity - implements EventListener, OnClickListener, - ConversationAdapter.IntroductionHandler { + implements EventListener, IntroductionHandler, TextInputListener { private static final Logger LOG = Logger.getLogger(ConversationActivity.class.getName()); @@ -113,8 +112,7 @@ public class ConversationActivity extends BriarActivity private ImageView toolbarStatus; private TextView toolbarTitle; private BriarRecyclerView list; - private EditText content; - private View sendButton; + private TextInputView textInputView; // Fields that are accessed from background threads must be volatile @Inject @@ -139,6 +137,7 @@ public class ConversationActivity extends BriarActivity private volatile boolean connected = false; private volatile Map bodyCache = new HashMap<>(); + @SuppressWarnings("ConstantConditions") @Override public void onCreate(Bundle state) { super.onCreate(state); @@ -177,13 +176,8 @@ public class ConversationActivity extends BriarActivity list.setAdapter(adapter); list.setEmptyText(getString(R.string.no_private_messages)); - content = (EditText) findViewById(R.id.input_text); - sendButton = findViewById(R.id.btn_send); - if (sendButton != null) { - // Enabled after loading the conversation - sendButton.setEnabled(false); - sendButton.setOnClickListener(this); - } + textInputView = (TextInputView) findViewById(R.id.text_input_container); + textInputView.setListener(this); } @Override @@ -262,6 +256,10 @@ public class ConversationActivity extends BriarActivity @Override public void onBackPressed() { + if (textInputView.isEmojiDrawerOpen()) { + textInputView.hideEmojiDrawer(); + return; + } // FIXME disabled exit transition, because it doesn't work for some reason #318 //supportFinishAfterTransition(); finish(); @@ -367,7 +365,7 @@ public class ConversationActivity extends BriarActivity runOnUiThread(new Runnable() { @Override public void run() { - sendButton.setEnabled(true); + textInputView.setSendButtonEnabled(true); if (headers.isEmpty() && introductions.isEmpty() && invitations.isEmpty()) { // we have no messages, @@ -637,14 +635,12 @@ public class ConversationActivity extends BriarActivity } @Override - public void onClick(View view) { + public void onSendClick(String text) { markMessagesRead(); - String message = content.getText().toString(); - if (message.equals("")) return; + if (text.equals("")) return; long timestamp = System.currentTimeMillis(); timestamp = Math.max(timestamp, getMinTimestampForNewMessage()); - createMessage(StringUtils.toUtf8(message), timestamp); - content.setText(""); + createMessage(StringUtils.toUtf8(text), timestamp); } private long getMinTimestampForNewMessage() { diff --git a/briar-android/src/org/briarproject/android/forum/ForumActivity.java b/briar-android/src/org/briarproject/android/forum/ForumActivity.java index 5f141948f..6b8c5a2b5 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumActivity.java +++ b/briar-android/src/org/briarproject/android/forum/ForumActivity.java @@ -21,8 +21,6 @@ import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; -import android.widget.EditText; -import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; @@ -31,11 +29,13 @@ import org.briarproject.android.ActivityComponent; import org.briarproject.android.BriarActivity; import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.controller.handler.UiResultHandler; +import org.briarproject.android.forum.ForumController.ForumPostListener; import org.briarproject.android.sharing.ShareForumActivity; import org.briarproject.android.sharing.SharingStatusForumActivity; -import org.briarproject.android.util.AndroidUtils; -import org.briarproject.android.util.BriarRecyclerView; -import org.briarproject.android.util.TrustIndicatorView; +import org.briarproject.android.view.AuthorView; +import org.briarproject.android.view.BriarRecyclerView; +import org.briarproject.android.view.TextInputView; +import org.briarproject.android.view.TextInputView.TextInputListener; import org.briarproject.api.forum.Forum; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; @@ -48,8 +48,6 @@ import java.util.Map; import javax.inject.Inject; -import im.delight.android.identicons.IdenticonDrawable; - import static android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP; import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP; import static android.support.v7.widget.RecyclerView.NO_POSITION; @@ -59,7 +57,7 @@ import static android.view.View.VISIBLE; import static android.widget.Toast.LENGTH_SHORT; public class ForumActivity extends BriarActivity implements - ForumController.ForumPostListener { + ForumPostListener, TextInputListener { static final String FORUM_NAME = "briar.FORUM_NAME"; @@ -80,8 +78,7 @@ public class ForumActivity extends BriarActivity implements protected ForumAdapter forumAdapter; private BriarRecyclerView recyclerView; - private EditText textInput; - private ViewGroup inputContainer; + private TextInputView textInput; private LinearLayoutManager linearLayoutManager; private volatile GroupId groupId = null; @@ -101,9 +98,9 @@ public class ForumActivity extends BriarActivity implements forumAdapter = new ForumAdapter(); - inputContainer = (ViewGroup) findViewById(R.id.text_input_container); - inputContainer.setVisibility(GONE); - textInput = (EditText) findViewById(R.id.input_text); + textInput = (TextInputView) findViewById(R.id.text_input_container); + textInput.setVisibility(GONE); + textInput.setListener(this); recyclerView = (BriarRecyclerView) findViewById(R.id.forum_discussion_list); recyclerView.setAdapter(forumAdapter); @@ -140,7 +137,7 @@ public class ForumActivity extends BriarActivity implements @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { super.onRestoreInstanceState(savedInstanceState); - inputContainer.setVisibility( + textInput.setVisibility( savedInstanceState.getBoolean(KEY_INPUT_VISIBILITY) ? VISIBLE : GONE); } @@ -150,7 +147,7 @@ public class ForumActivity extends BriarActivity implements protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(KEY_INPUT_VISIBILITY, - inputContainer.getVisibility() == VISIBLE); + textInput.getVisibility() == VISIBLE); ForumEntry replyEntry = forumAdapter.getReplyEntry(); if (replyEntry != null) { outState.putByteArray(KEY_REPLY_ID, @@ -190,8 +187,10 @@ public class ForumActivity extends BriarActivity implements @Override public void onBackPressed() { - if (inputContainer.getVisibility() == VISIBLE) { - inputContainer.setVisibility(GONE); + if (textInput.isEmojiDrawerOpen()) { + textInput.hideEmojiDrawer(); + } else if (textInput.getVisibility() == VISIBLE) { + textInput.setVisibility(GONE); forumAdapter.setReplyEntry(null); } else { super.onBackPressed(); @@ -202,8 +201,8 @@ public class ForumActivity extends BriarActivity implements // An animation here would be an overkill because of the keyboard // popping up. // only clear the text when the input container was not visible - if (inputContainer.getVisibility() != VISIBLE) { - inputContainer.setVisibility(VISIBLE); + if (textInput.getVisibility() != VISIBLE) { + textInput.setVisibility(VISIBLE); textInput.setText(""); } textInput.requestFocus(); @@ -260,8 +259,8 @@ public class ForumActivity extends BriarActivity implements recyclerView.stopPeriodicUpdate(); } - public void sendMessage(View view) { - String text = textInput.getText().toString(); + @Override + public void onSendClick(String text) { if (text.trim().length() == 0) return; if (forumController.getForum() == null) return; @@ -274,7 +273,7 @@ public class ForumActivity extends BriarActivity implements replyEntry.getMessageId()); } hideSoftKeyboard(textInput); - inputContainer.setVisibility(GONE); + textInput.setVisibility(GONE); forumAdapter.setReplyEntry(null); } @@ -334,10 +333,9 @@ public class ForumActivity extends BriarActivity implements static class ForumViewHolder extends RecyclerView.ViewHolder { - final TextView textView, lvlText, authorText, dateText, repliesText; + final TextView textView, lvlText, repliesText; + final AuthorView author; final View[] lvls; - public final ImageView avatar; - final TrustIndicatorView trust; final View chevron, replyButton; final ViewGroup cell; final View topDivider; @@ -347,8 +345,7 @@ public class ForumActivity extends BriarActivity implements textView = (TextView) v.findViewById(R.id.text); lvlText = (TextView) v.findViewById(R.id.nested_line_text); - authorText = (TextView) v.findViewById(R.id.author); - dateText = (TextView) v.findViewById(R.id.date); + author = (AuthorView) v.findViewById(R.id.author); repliesText = (TextView) v.findViewById(R.id.replies); int[] nestedLineIds = { R.id.nested_line_1, R.id.nested_line_2, R.id.nested_line_3, @@ -358,8 +355,6 @@ public class ForumActivity extends BriarActivity implements for (int i = 0; i < lvls.length; i++) { lvls[i] = v.findViewById(nestedLineIds[i]); } - avatar = (ImageView) v.findViewById(R.id.avatar); - trust = (TrustIndicatorView) v.findViewById(R.id.trustIndicator); chevron = v.findViewById(R.id.chevron); replyButton = v.findViewById(R.id.btn_reply); cell = (ViewGroup) v.findViewById(R.id.forum_cell); @@ -604,7 +599,7 @@ public class ForumActivity extends BriarActivity implements public ForumViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { View v = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.forum_discussion_cell, parent, false); + .inflate(R.layout.list_item_forum_post, parent, false); return new ForumViewHolder(v); } @@ -635,10 +630,9 @@ public class ForumActivity extends BriarActivity implements } else { ui.lvlText.setVisibility(GONE); } - ui.authorText.setText(data.getAuthor()); - ui.dateText.setText(AndroidUtils - .formatDate(ForumActivity.this, data.getTimestamp())); - ui.trust.setTrustLevel(data.getStatus()); + ui.author.setAuthor(data.getAuthor()); + ui.author.setDate(data.getTimestamp()); + ui.author.setAuthorStatus(data.getStatus()); int replies = getReplyCount(data); if (replies == 0) { @@ -648,8 +642,6 @@ public class ForumActivity extends BriarActivity implements .getQuantityString(R.plurals.message_replies, replies, replies)); } - ui.avatar.setImageDrawable( - new IdenticonDrawable(data.getAuthorId().getBytes())); if (hasDescendants(data)) { ui.chevron.setVisibility(VISIBLE); diff --git a/briar-android/src/org/briarproject/android/forum/ForumEntry.java b/briar-android/src/org/briarproject/android/forum/ForumEntry.java index b3f2aed40..f809d7239 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumEntry.java +++ b/briar-android/src/org/briarproject/android/forum/ForumEntry.java @@ -1,6 +1,7 @@ package org.briarproject.android.forum; import org.briarproject.api.forum.ForumPostHeader; +import org.briarproject.api.identity.Author; import org.briarproject.api.identity.Author.Status; import org.briarproject.api.identity.AuthorId; import org.briarproject.api.sync.MessageId; @@ -11,26 +12,24 @@ public class ForumEntry { private final String text; private final int level; private final long timestamp; - private final String author; - private final AuthorId authorId; + private final Author author; private Status status; private boolean isShowingDescendants = true; private boolean isRead = true; ForumEntry(ForumPostHeader h, String text, int level) { - this(h.getId(), text, level, h.getTimestamp(), h.getAuthor().getName(), - h.getAuthor().getId(), h.getAuthorStatus()); + this(h.getId(), text, level, h.getTimestamp(), h.getAuthor(), + h.getAuthorStatus()); this.isRead = h.isRead(); } public ForumEntry(MessageId messageId, String text, int level, - long timestamp, String author, AuthorId authorId, Status status) { + long timestamp, Author author, Status status) { this.messageId = messageId; this.text = text; this.level = level; this.timestamp = timestamp; this.author = author; - this.authorId = authorId; this.status = status; } @@ -46,14 +45,10 @@ public class ForumEntry { return timestamp; } - public String getAuthor() { + public Author getAuthor() { return author; } - AuthorId getAuthorId() { - return authorId; - } - public Status getStatus() { return status; } diff --git a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java index 88759804a..d37f23a02 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java +++ b/briar-android/src/org/briarproject/android/forum/ForumListAdapter.java @@ -13,7 +13,7 @@ import android.widget.TextView; import org.briarproject.R; import org.briarproject.android.util.AndroidUtils; -import org.briarproject.android.util.TextAvatarView; +import org.briarproject.android.view.TextAvatarView; import org.briarproject.api.forum.Forum; import org.briarproject.api.sync.GroupId; diff --git a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java index 11c9bd293..fc9f63b37 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumListFragment.java +++ b/briar-android/src/org/briarproject/android/forum/ForumListFragment.java @@ -18,7 +18,7 @@ import org.briarproject.android.ActivityComponent; import org.briarproject.android.api.AndroidNotificationManager; import org.briarproject.android.fragment.BaseEventFragment; import org.briarproject.android.sharing.InvitationsForumActivity; -import org.briarproject.android.util.BriarRecyclerView; +import org.briarproject.android.view.BriarRecyclerView; import org.briarproject.api.db.DbException; import org.briarproject.api.db.NoSuchGroupException; import org.briarproject.api.event.ContactRemovedEvent; diff --git a/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java b/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java index d443a65b8..a5b89405b 100644 --- a/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java +++ b/briar-android/src/org/briarproject/android/forum/ForumTestControllerImpl.java @@ -3,7 +3,8 @@ package org.briarproject.android.forum; import org.briarproject.android.controller.handler.ResultHandler; import org.briarproject.api.UniqueId; import org.briarproject.api.forum.Forum; -import org.briarproject.api.identity.AuthorId; +import org.briarproject.api.identity.Author; +import org.briarproject.api.identity.AuthorFactory; import org.briarproject.api.sync.GroupId; import org.briarproject.api.sync.MessageId; @@ -20,34 +21,27 @@ import static org.briarproject.api.identity.Author.Status.UNVERIFIED; public class ForumTestControllerImpl implements ForumController { + @Inject + AuthorFactory authorFactory; + private static final Logger LOG = Logger.getLogger(ForumControllerImpl.class.getName()); - private final static String[] AUTHORS = { - "Guðmundur", - "Jónas", - "Geir Þorsteinn Gísli Máni Halldórsson Guðjónsson Mogensen", - "Baldur Friðrik", - "Anna Katrín", - "Þór", - "Anna Þorbjörg", - "Guðrún", - "Helga", - "Haraldur" + private final Author[] AUTHORS = { + authorFactory.createAuthor("Guðmundur", new byte[42]), + authorFactory.createAuthor("Jónas", new byte[42]), + authorFactory.createAuthor( + "Geir Þorsteinn Gísli Máni Halldórsson Guðjónsson Mogensen", + new byte[42]), + authorFactory.createAuthor("Baldur Friðrik", new byte[42]), + authorFactory.createAuthor("Anna Katrín", new byte[42]), + authorFactory.createAuthor("Þór", new byte[42]), + authorFactory.createAuthor("Anna Þorbjörg", new byte[42]), + authorFactory.createAuthor("Guðrún", new byte[42]), + authorFactory.createAuthor("Helga", new byte[42]), + authorFactory.createAuthor("Haraldur", new byte[42]) }; - private final static AuthorId[] AUTHOR_ID = new AuthorId[AUTHORS.length]; - - static { - SecureRandom random = new SecureRandom(); - for (int i = 0; i < AUTHOR_ID.length; i++) { - byte[] b = new byte[UniqueId.LENGTH]; - random.nextBytes(b); - AUTHOR_ID[i] = new AuthorId(b); - - } - } - private final static String SAGA = "Það er upphaf á sögu þessari að Hákon konungur " + "Aðalsteinsfóstri réð fyrir Noregi og var þetta á ofanverðum " + @@ -117,8 +111,7 @@ public class ForumTestControllerImpl implements ForumController { random.nextBytes(b); forumEntries[e] = new ForumEntry(new MessageId(b), SAGA.substring(0, i[e]), - l[e], timestamp, AUTHORS[authorIndex], - AUTHOR_ID[authorIndex], UNVERIFIED); + l[e], timestamp, AUTHORS[authorIndex], UNVERIFIED); } LOG.info("forum entries: " + forumEntries.length); resultHandler.onResult(true); diff --git a/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java index bbbb14e46..ce25c8836 100644 --- a/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java +++ b/briar-android/src/org/briarproject/android/introduction/ContactChooserFragment.java @@ -17,7 +17,7 @@ import org.briarproject.android.contact.ContactListAdapter; import org.briarproject.android.contact.ContactListItem; import org.briarproject.android.contact.ConversationItem; import org.briarproject.android.fragment.BaseFragment; -import org.briarproject.android.util.BriarRecyclerView; +import org.briarproject.android.view.BriarRecyclerView; import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactManager; diff --git a/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java b/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java index 268b16b85..ef8dcc55e 100644 --- a/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java +++ b/briar-android/src/org/briarproject/android/keyagreement/ShowQrCodeFragment.java @@ -23,7 +23,7 @@ import org.briarproject.R; import org.briarproject.android.ActivityComponent; import org.briarproject.android.api.AndroidExecutor; import org.briarproject.android.fragment.BaseEventFragment; -import org.briarproject.android.util.CameraView; +import org.briarproject.android.view.CameraView; import org.briarproject.android.util.QrCodeDecoder; import org.briarproject.android.util.QrCodeUtils; import org.briarproject.api.event.Event; diff --git a/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java b/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java index a1671e2e2..951b49c09 100644 --- a/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java +++ b/briar-android/src/org/briarproject/android/sharing/ContactSelectorFragment.java @@ -17,7 +17,7 @@ import org.briarproject.android.ActivityComponent; import org.briarproject.android.contact.BaseContactListAdapter; import org.briarproject.android.contact.ContactListItem; import org.briarproject.android.fragment.BaseFragment; -import org.briarproject.android.util.BriarRecyclerView; +import org.briarproject.android.view.BriarRecyclerView; import org.briarproject.api.contact.Contact; import org.briarproject.api.contact.ContactId; import org.briarproject.api.contact.ContactManager; diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java b/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java index f4c988454..00852f7e9 100644 --- a/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java +++ b/briar-android/src/org/briarproject/android/sharing/InvitationAdapter.java @@ -10,7 +10,7 @@ import android.widget.Button; import android.widget.TextView; import org.briarproject.R; -import org.briarproject.android.util.TextAvatarView; +import org.briarproject.android.view.TextAvatarView; import org.briarproject.api.contact.Contact; import org.briarproject.api.sharing.InvitationItem; import org.briarproject.util.StringUtils; diff --git a/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java b/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java index fc6761e05..c7d99b14b 100644 --- a/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java +++ b/briar-android/src/org/briarproject/android/sharing/InvitationsActivity.java @@ -7,7 +7,7 @@ import android.widget.Toast; import org.briarproject.R; import org.briarproject.android.BriarActivity; -import org.briarproject.android.util.BriarRecyclerView; +import org.briarproject.android.view.BriarRecyclerView; import org.briarproject.api.event.ContactRemovedEvent; import org.briarproject.api.event.Event; import org.briarproject.api.event.EventBus; diff --git a/briar-android/src/org/briarproject/android/sharing/SharingStatusActivity.java b/briar-android/src/org/briarproject/android/sharing/SharingStatusActivity.java index 6a67448c9..0d69fe6d2 100644 --- a/briar-android/src/org/briarproject/android/sharing/SharingStatusActivity.java +++ b/briar-android/src/org/briarproject/android/sharing/SharingStatusActivity.java @@ -8,7 +8,7 @@ import android.view.MenuItem; import org.briarproject.R; import org.briarproject.android.BriarActivity; import org.briarproject.android.contact.ContactListItem; -import org.briarproject.android.util.BriarRecyclerView; +import org.briarproject.android.view.BriarRecyclerView; import org.briarproject.api.contact.Contact; import org.briarproject.api.db.DbException; import org.briarproject.api.identity.IdentityManager; diff --git a/briar-android/src/org/briarproject/android/util/AndroidUtils.java b/briar-android/src/org/briarproject/android/util/AndroidUtils.java index 759a25e6b..319c1854f 100644 --- a/briar-android/src/org/briarproject/android/util/AndroidUtils.java +++ b/briar-android/src/org/briarproject/android/util/AndroidUtils.java @@ -15,7 +15,6 @@ import android.text.SpannableString; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.format.DateUtils; -import android.text.method.LinkMovementMethod; import android.text.style.ClickableSpan; import android.text.style.ForegroundColorSpan; import android.text.style.URLSpan; @@ -23,6 +22,7 @@ import android.view.View; import android.widget.TextView; import org.briarproject.R; +import org.briarproject.android.view.ArticleMovementMethod; import org.briarproject.android.widget.LinkDialogFragment; import org.briarproject.util.IoUtils; import org.briarproject.util.StringUtils; diff --git a/briar-android/src/org/briarproject/android/util/ArticleMovementMethod.java b/briar-android/src/org/briarproject/android/view/ArticleMovementMethod.java similarity index 97% rename from briar-android/src/org/briarproject/android/util/ArticleMovementMethod.java rename to briar-android/src/org/briarproject/android/view/ArticleMovementMethod.java index ad148a719..d02ca1081 100644 --- a/briar-android/src/org/briarproject/android/util/ArticleMovementMethod.java +++ b/briar-android/src/org/briarproject/android/view/ArticleMovementMethod.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.briarproject.android.util; +package org.briarproject.android.view; import android.text.Layout; import android.text.Spannable; diff --git a/briar-android/src/org/briarproject/android/util/AuthorView.java b/briar-android/src/org/briarproject/android/view/AuthorView.java similarity index 95% rename from briar-android/src/org/briarproject/android/util/AuthorView.java rename to briar-android/src/org/briarproject/android/view/AuthorView.java index 45ab5d347..40f8e5069 100644 --- a/briar-android/src/org/briarproject/android/util/AuthorView.java +++ b/briar-android/src/org/briarproject/android/view/AuthorView.java @@ -1,10 +1,11 @@ -package org.briarproject.android.util; +package org.briarproject.android.view; import android.content.Context; import android.content.Intent; import android.content.res.TypedArray; import android.graphics.Typeface; import android.support.annotation.Nullable; +import android.support.annotation.UiThread; import android.support.v4.app.ActivityOptionsCompat; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; @@ -18,6 +19,8 @@ import android.widget.TextView; import org.briarproject.R; import org.briarproject.android.blogs.BlogActivity; +import org.briarproject.android.util.AndroidUtils; +import org.briarproject.android.view.TrustIndicatorView; import org.briarproject.api.identity.Author; import org.briarproject.api.identity.Author.Status; import org.briarproject.api.sync.GroupId; @@ -34,6 +37,7 @@ import static android.util.TypedValue.COMPLEX_UNIT_PX; import static org.briarproject.android.BriarActivity.GROUP_ID; import static org.briarproject.api.identity.Author.Status.OURSELVES; +@UiThread public class AuthorView extends RelativeLayout { private final CircleImageView avatar; diff --git a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java b/briar-android/src/org/briarproject/android/view/BriarRecyclerView.java similarity index 96% rename from briar-android/src/org/briarproject/android/util/BriarRecyclerView.java rename to briar-android/src/org/briarproject/android/view/BriarRecyclerView.java index affbe9e13..40f455404 100644 --- a/briar-android/src/org/briarproject/android/util/BriarRecyclerView.java +++ b/briar-android/src/org/briarproject/android/view/BriarRecyclerView.java @@ -1,7 +1,8 @@ -package org.briarproject.android.util; +package org.briarproject.android.view; import android.content.Context; import android.content.res.TypedArray; +import android.support.annotation.Nullable; import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView.Adapter; import android.util.AttributeSet; @@ -34,11 +35,11 @@ public class BriarRecyclerView extends FrameLayout { this(context, null, 0); } - public BriarRecyclerView(Context context, AttributeSet attrs) { + public BriarRecyclerView(Context context, @Nullable AttributeSet attrs) { this(context, attrs, 0); } - public BriarRecyclerView(Context context, AttributeSet attrs, + public BriarRecyclerView(Context context, @Nullable AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); diff --git a/briar-android/src/org/briarproject/android/util/BriarRecyclerViewBehavior.java b/briar-android/src/org/briarproject/android/view/BriarRecyclerViewBehavior.java similarity index 96% rename from briar-android/src/org/briarproject/android/util/BriarRecyclerViewBehavior.java rename to briar-android/src/org/briarproject/android/view/BriarRecyclerViewBehavior.java index fc57a27f0..cfaa352cd 100644 --- a/briar-android/src/org/briarproject/android/util/BriarRecyclerViewBehavior.java +++ b/briar-android/src/org/briarproject/android/view/BriarRecyclerViewBehavior.java @@ -1,4 +1,4 @@ -package org.briarproject.android.util; +package org.briarproject.android.view; import android.content.Context; import android.support.design.widget.CoordinatorLayout; diff --git a/briar-android/src/org/briarproject/android/util/CameraView.java b/briar-android/src/org/briarproject/android/view/CameraView.java similarity index 98% rename from briar-android/src/org/briarproject/android/util/CameraView.java rename to briar-android/src/org/briarproject/android/view/CameraView.java index 79ea7eadd..0ce1ad136 100644 --- a/briar-android/src/org/briarproject/android/util/CameraView.java +++ b/briar-android/src/org/briarproject/android/view/CameraView.java @@ -1,4 +1,4 @@ -package org.briarproject.android.util; +package org.briarproject.android.view; import android.content.Context; import android.hardware.Camera; @@ -12,6 +12,8 @@ import android.util.DisplayMetrics; import android.view.SurfaceHolder; import android.view.SurfaceView; +import org.briarproject.android.util.PreviewConsumer; + import java.io.IOException; import java.util.Collections; import java.util.List; diff --git a/briar-android/src/org/briarproject/android/util/TextAvatarView.java b/briar-android/src/org/briarproject/android/view/TextAvatarView.java similarity index 92% rename from briar-android/src/org/briarproject/android/util/TextAvatarView.java rename to briar-android/src/org/briarproject/android/view/TextAvatarView.java index b48d0e7ad..70ab3f398 100644 --- a/briar-android/src/org/briarproject/android/util/TextAvatarView.java +++ b/briar-android/src/org/briarproject/android/view/TextAvatarView.java @@ -1,8 +1,10 @@ -package org.briarproject.android.util; +package org.briarproject.android.view; import android.content.Context; import android.graphics.Color; import android.graphics.drawable.Drawable; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; import android.support.v4.content.ContextCompat; import android.support.v7.widget.AppCompatTextView; import android.util.AttributeSet; @@ -16,6 +18,7 @@ import org.briarproject.api.identity.Author; import de.hdodenhof.circleimageview.CircleImageView; import im.delight.android.identicons.IdenticonDrawable; +@UiThread public class TextAvatarView extends FrameLayout { final private AppCompatTextView character; @@ -23,7 +26,7 @@ public class TextAvatarView extends FrameLayout { final private TextView badge; private int unreadCount; - public TextAvatarView(Context context, AttributeSet attrs) { + public TextAvatarView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); LayoutInflater inflater = (LayoutInflater) context diff --git a/briar-android/src/org/briarproject/android/view/TextInputView.java b/briar-android/src/org/briarproject/android/view/TextInputView.java new file mode 100644 index 000000000..e257a1915 --- /dev/null +++ b/briar-android/src/org/briarproject/android/view/TextInputView.java @@ -0,0 +1,171 @@ +package org.briarproject.android.view; + +import android.content.Context; +import android.os.IBinder; +import android.support.annotation.Nullable; +import android.support.annotation.StringRes; +import android.support.annotation.UiThread; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.inputmethod.InputMethodManager; + +import org.briarproject.R; +import org.thoughtcrime.securesms.components.KeyboardAwareRelativeLayout; +import org.thoughtcrime.securesms.components.emoji.EmojiDrawer; +import org.thoughtcrime.securesms.components.emoji.EmojiDrawer.EmojiEventListener; +import org.thoughtcrime.securesms.components.emoji.EmojiEditText; +import org.thoughtcrime.securesms.components.emoji.EmojiToggle; + +import java.util.logging.Logger; + +import static android.content.Context.INPUT_METHOD_SERVICE; + +@UiThread +public class TextInputView extends KeyboardAwareRelativeLayout + implements EmojiEventListener { + + private static final String TAG = TextInputView.class.getName(); + private static final Logger LOG = Logger.getLogger(TAG); + + private EmojiEditText editText; + private View sendButton; + private EmojiDrawer emojiDrawer; + + private TextInputListener listener; + + public TextInputView(Context context) { + this(context, null); + } + + public TextInputView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public TextInputView(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + + LayoutInflater inflater = (LayoutInflater) context + .getSystemService(Context.LAYOUT_INFLATER_SERVICE); + inflater.inflate(R.layout.text_input_view, this, true); + + // find views + EmojiToggle emojiToggle = (EmojiToggle) findViewById(R.id.emoji_toggle); + editText = (EmojiEditText) findViewById(R.id.input_text); + emojiDrawer = (EmojiDrawer) findViewById(R.id.emoji_drawer); + sendButton = findViewById(R.id.btn_send); + + emojiToggle.attach(emojiDrawer); + emojiToggle.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + onEmojiToggleClicked(); + } + }); + editText.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + showSoftKeyboard(); + } + }); + sendButton.setOnClickListener(new OnClickListener() { + @Override + public void onClick(View v) { + if (listener != null) { + listener.onSendClick(editText.getText().toString()); + editText.setText(""); + } + } + }); + emojiDrawer.setEmojiEventListener(this); + } + + @Override + public void onKeyEvent(KeyEvent keyEvent) { + editText.dispatchKeyEvent(keyEvent); + } + + @Override + public void onEmojiSelected(String emoji) { + editText.insertEmoji(emoji); + } + + private void onEmojiToggleClicked() { + if (isEmojiDrawerOpen()) { + showSoftKeyboard(); + } else { + showEmojiDrawer(); + } + } + + public void setText(String text) { + editText.setText(text); + } + + public void setHint(@StringRes int res) { + editText.setHint(res); + } + + public void setSendButtonEnabled(boolean enabled) { + sendButton.setEnabled(enabled); + } + + public void setListener(TextInputListener listener) { + this.listener = listener; + } + + public interface TextInputListener { + void onSendClick(String text); + } + + public void showSoftKeyboard() { + if (isKeyboardOpen()) return; + + if (emojiDrawer.isShowing()) { + postOnKeyboardOpen(new Runnable() { + @Override + public void run() { + hideEmojiDrawer(); + } + }); + } + editText.post(new Runnable() { + @Override + public void run() { + editText.requestFocus(); + Object o = getContext().getSystemService(INPUT_METHOD_SERVICE); + ((InputMethodManager) o).showSoftInput(editText, 0); + } + }); + } + + public void hideSoftKeyboard() { + IBinder token = editText.getWindowToken(); + Object o = getContext().getSystemService(INPUT_METHOD_SERVICE); + ((InputMethodManager) o).hideSoftInputFromWindow(token, 0); + } + + public void showEmojiDrawer() { + if (isKeyboardOpen()) { + postOnKeyboardClose(new Runnable() { + @Override public void run() { + emojiDrawer.show(getKeyboardHeight()); + } + }); + hideSoftKeyboard(); + } else { + emojiDrawer.show(getKeyboardHeight()); + } + } + + public void hideEmojiDrawer() { + emojiDrawer.hide(); + } + + public boolean isEmojiDrawerOpen() { + return emojiDrawer.isShowing(); + } + +} diff --git a/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java b/briar-android/src/org/briarproject/android/view/TrustIndicatorView.java similarity index 92% rename from briar-android/src/org/briarproject/android/util/TrustIndicatorView.java rename to briar-android/src/org/briarproject/android/view/TrustIndicatorView.java index 97d8e7763..5a88acdf6 100644 --- a/briar-android/src/org/briarproject/android/util/TrustIndicatorView.java +++ b/briar-android/src/org/briarproject/android/view/TrustIndicatorView.java @@ -1,6 +1,7 @@ -package org.briarproject.android.util; +package org.briarproject.android.view; import android.content.Context; +import android.support.annotation.UiThread; import android.support.v4.content.ContextCompat; import android.util.AttributeSet; import android.widget.ImageView; @@ -10,6 +11,7 @@ import org.briarproject.api.identity.Author.Status; import static org.briarproject.api.identity.Author.Status.OURSELVES; +@UiThread public class TrustIndicatorView extends ImageView { public TrustIndicatorView(Context context) { diff --git a/briar-android/src/org/thoughtcrime/securesms/LICENSE b/briar-android/src/org/thoughtcrime/securesms/LICENSE new file mode 100644 index 000000000..94a045322 --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/LICENSE @@ -0,0 +1,621 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS diff --git a/briar-android/src/org/thoughtcrime/securesms/components/KeyboardAwareRelativeLayout.java b/briar-android/src/org/thoughtcrime/securesms/components/KeyboardAwareRelativeLayout.java new file mode 100644 index 000000000..f762d1cb6 --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/KeyboardAwareRelativeLayout.java @@ -0,0 +1,279 @@ +/** + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms.components; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.content.Context; +import android.graphics.Rect; +import android.os.Build; +import android.os.Build.VERSION_CODES; +import android.preference.PreferenceManager; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.util.AttributeSet; +import android.util.Log; +import android.view.Surface; +import android.view.View; +import android.view.WindowManager; +import android.widget.RelativeLayout; + +import org.briarproject.R; + +import java.lang.reflect.Field; +import java.util.HashSet; +import java.util.Set; + +/** + * RelativeLayout that, when a view container, will report back when it thinks a soft keyboard + * has been opened and what its height would be. + */ +@UiThread +public class KeyboardAwareRelativeLayout extends RelativeLayout { + private static final String TAG = + KeyboardAwareRelativeLayout.class.getSimpleName(); + + private final Rect rect = new Rect(); + private final Set hiddenListeners = + new HashSet<>(); + private final Set shownListeners = new HashSet<>(); + private final int minKeyboardSize; + private final int minCustomKeyboardSize; + private final int defaultCustomKeyboardSize; + private final int minCustomKeyboardTopMargin; + private final int statusBarHeight; + + private int viewInset; + + private boolean keyboardOpen = false; + private int rotation = -1; + + public KeyboardAwareRelativeLayout(Context context) { + this(context, null); + } + + public KeyboardAwareRelativeLayout(Context context, + @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public KeyboardAwareRelativeLayout(Context context, + @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + final int statusBarRes = getResources() + .getIdentifier("status_bar_height", "dimen", "android"); + minKeyboardSize = + getResources().getDimensionPixelSize(R.dimen.min_keyboard_size); + minCustomKeyboardSize = getResources() + .getDimensionPixelSize(R.dimen.min_custom_keyboard_size); + defaultCustomKeyboardSize = getResources() + .getDimensionPixelSize(R.dimen.default_custom_keyboard_size); + minCustomKeyboardTopMargin = getResources() + .getDimensionPixelSize(R.dimen.min_custom_keyboard_top_margin); + statusBarHeight = statusBarRes > 0 ? + getResources().getDimensionPixelSize(statusBarRes) : 0; + viewInset = getViewInset(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + updateRotation(); + updateKeyboardState(); + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + + private void updateRotation() { + int oldRotation = rotation; + rotation = getDeviceRotation(); + if (oldRotation != rotation) { + Log.w(TAG, "rotation changed"); + onKeyboardClose(); + } + } + + private void updateKeyboardState() { + if (isLandscape()) { + if (keyboardOpen) onKeyboardClose(); + return; + } + + if (viewInset == 0 && Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) + viewInset = getViewInset(); + final int availableHeight = + this.getRootView().getHeight() - statusBarHeight - viewInset; + getWindowVisibleDisplayFrame(rect); + + final int keyboardHeight = availableHeight - (rect.bottom - rect.top); + + if (keyboardHeight > minKeyboardSize) { + if (getKeyboardHeight() != keyboardHeight) + setKeyboardPortraitHeight(keyboardHeight); + if (!keyboardOpen) onKeyboardOpen(keyboardHeight); + } else if (keyboardOpen) { + onKeyboardClose(); + } + } + + @TargetApi(VERSION_CODES.LOLLIPOP) + private int getViewInset() { + try { + Field attachInfoField = View.class.getDeclaredField("mAttachInfo"); + attachInfoField.setAccessible(true); + Object attachInfo = attachInfoField.get(this); + if (attachInfo != null) { + Field stableInsetsField = + attachInfo.getClass().getDeclaredField("mStableInsets"); + stableInsetsField.setAccessible(true); + Rect insets = (Rect) stableInsetsField.get(attachInfo); + return insets.bottom; + } + } catch (NoSuchFieldException nsfe) { + Log.w(TAG, "field reflection error when measuring view inset", + nsfe); + } catch (IllegalAccessException iae) { + Log.w(TAG, "access reflection error when measuring view inset", + iae); + } + return 0; + } + + protected void onKeyboardOpen(int keyboardHeight) { + Log.w(TAG, "onKeyboardOpen(" + keyboardHeight + ")"); + keyboardOpen = true; + + notifyShownListeners(); + } + + protected void onKeyboardClose() { + Log.w(TAG, "onKeyboardClose()"); + keyboardOpen = false; + notifyHiddenListeners(); + } + + public boolean isKeyboardOpen() { + return keyboardOpen; + } + + public int getKeyboardHeight() { + return isLandscape() ? getKeyboardLandscapeHeight() : + getKeyboardPortraitHeight(); + } + + public boolean isLandscape() { + int rotation = getDeviceRotation(); + return rotation == Surface.ROTATION_90 || + rotation == Surface.ROTATION_270; + } + + private int getDeviceRotation() { + WindowManager windowManager = (WindowManager) getContext() + .getSystemService(Activity.WINDOW_SERVICE); + return windowManager.getDefaultDisplay().getRotation(); + } + + private int getKeyboardLandscapeHeight() { + return Math.max(getHeight(), getRootView().getHeight()) / 2; + } + + private int getKeyboardPortraitHeight() { + int keyboardHeight = + PreferenceManager.getDefaultSharedPreferences(getContext()) + .getInt("keyboard_height_portrait", + defaultCustomKeyboardSize); + return clamp(keyboardHeight, minCustomKeyboardSize, + getRootView().getHeight() - minCustomKeyboardTopMargin); + } + + private int clamp(int value, int min, int max) { + return Math.min(Math.max(value, min), max); + } + + private void setKeyboardPortraitHeight(int height) { + PreferenceManager.getDefaultSharedPreferences(getContext()) + .edit().putInt("keyboard_height_portrait", height).apply(); + } + + public void postOnKeyboardClose(final Runnable runnable) { + if (keyboardOpen) { + addOnKeyboardHiddenListener(new OnKeyboardHiddenListener() { + @Override + public void onKeyboardHidden() { + removeOnKeyboardHiddenListener(this); + runnable.run(); + } + }); + } else { + runnable.run(); + } + } + + public void postOnKeyboardOpen(final Runnable runnable) { + if (!keyboardOpen) { + addOnKeyboardShownListener(new OnKeyboardShownListener() { + @Override + public void onKeyboardShown() { + removeOnKeyboardShownListener(this); + runnable.run(); + } + }); + } else { + runnable.run(); + } + } + + public void addOnKeyboardHiddenListener(OnKeyboardHiddenListener listener) { + hiddenListeners.add(listener); + } + + public void removeOnKeyboardHiddenListener( + OnKeyboardHiddenListener listener) { + hiddenListeners.remove(listener); + } + + public void addOnKeyboardShownListener(OnKeyboardShownListener listener) { + shownListeners.add(listener); + } + + public void removeOnKeyboardShownListener( + OnKeyboardShownListener listener) { + shownListeners.remove(listener); + } + + private void notifyHiddenListeners() { + final Set listeners = + new HashSet<>(hiddenListeners); + for (OnKeyboardHiddenListener listener : listeners) { + listener.onKeyboardHidden(); + } + } + + private void notifyShownListeners() { + final Set listeners = + new HashSet<>(shownListeners); + for (OnKeyboardShownListener listener : listeners) { + listener.onKeyboardShown(); + } + } + + public interface OnKeyboardHiddenListener { + void onKeyboardHidden(); + } + + public interface OnKeyboardShownListener { + void onKeyboardShown(); + } +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/RepeatableImageKey.java b/briar-android/src/org/thoughtcrime/securesms/components/RepeatableImageKey.java new file mode 100644 index 000000000..f1802bd34 --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/RepeatableImageKey.java @@ -0,0 +1,95 @@ +package org.thoughtcrime.securesms.components; + +import android.content.Context; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.support.annotation.UiThread; +import android.util.AttributeSet; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewConfiguration; +import android.widget.ImageButton; + +@UiThread +public class RepeatableImageKey extends ImageButton { + + private KeyEventListener listener; + + public RepeatableImageKey(Context context) { + super(context); + init(); + } + + public RepeatableImageKey(Context context, AttributeSet attrs) { + super(context, attrs); + init(); + } + + public RepeatableImageKey(Context context, AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + private void init() { + setOnClickListener(new RepeaterClickListener()); + setOnTouchListener(new RepeaterTouchListener()); + } + + public void setOnKeyEventListener(KeyEventListener listener) { + this.listener = listener; + } + + private void notifyListener() { + if (this.listener != null) this.listener.onKeyEvent(); + } + + private class RepeaterClickListener implements OnClickListener { + @Override + public void onClick(View v) { + notifyListener(); + } + } + + private class Repeater implements Runnable { + @Override + public void run() { + notifyListener(); + postDelayed(this, VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1 + ? ViewConfiguration.getKeyRepeatDelay() + : 50); + } + } + + private class RepeaterTouchListener implements OnTouchListener { + private Repeater repeater; + + private RepeaterTouchListener() { + this.repeater = new Repeater(); + } + + @Override + public boolean onTouch(View view, MotionEvent motionEvent) { + switch (motionEvent.getAction()) { + case MotionEvent.ACTION_DOWN: + view.postDelayed(repeater, + VERSION.SDK_INT >= VERSION_CODES.HONEYCOMB_MR1 + ? ViewConfiguration.getKeyRepeatTimeout() + : ViewConfiguration.getLongPressTimeout()); + performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP); + return false; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + view.removeCallbacks(repeater); + return false; + default: + return false; + } + } + } + + public interface KeyEventListener { + void onKeyEvent(); + } +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/AnimatingImageSpan.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/AnimatingImageSpan.java new file mode 100644 index 000000000..7f06aff49 --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/AnimatingImageSpan.java @@ -0,0 +1,14 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.graphics.drawable.Drawable; +import android.graphics.drawable.Drawable.Callback; +import android.support.annotation.UiThread; +import android.text.style.ImageSpan; + +@UiThread +public class AnimatingImageSpan extends ImageSpan { + public AnimatingImageSpan(Drawable drawable, Callback callback) { + super(drawable, ALIGN_BOTTOM); + drawable.setCallback(callback); + } +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiDrawer.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiDrawer.java new file mode 100644 index 000000000..2b78041d0 --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiDrawer.java @@ -0,0 +1,202 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.content.Context; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v4.view.PagerAdapter; +import android.support.v4.view.ViewPager; +import android.util.AttributeSet; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.LinearLayout; + +import com.astuetz.PagerSlidingTabStrip; +import com.astuetz.PagerSlidingTabStrip.CustomTabProvider; + +import org.briarproject.R; +import org.thoughtcrime.securesms.components.RepeatableImageKey; +import org.thoughtcrime.securesms.components.RepeatableImageKey.KeyEventListener; +import org.thoughtcrime.securesms.components.emoji.EmojiPageView.EmojiSelectionListener; + +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Logger; + +import static java.util.logging.Level.INFO; + +@UiThread +public class EmojiDrawer extends LinearLayout { + + private static final String TAG = EmojiDrawer.class.getName(); + private static final Logger LOG = Logger.getLogger(TAG); + private static final KeyEvent DELETE_KEY_EVENT = + new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL); + + private ViewPager pager; + private List models; + private PagerSlidingTabStrip strip; + private RecentEmojiPageModel recentModel; + private EmojiEventListener listener; + private EmojiDrawerListener drawerListener; + + public EmojiDrawer(Context context) { + this(context, null); + } + + public EmojiDrawer(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + setOrientation(VERTICAL); + } + + private void initView() { + final View v = LayoutInflater.from(getContext()) + .inflate(R.layout.emoji_drawer, this, true); + initializeResources(v); + initializePageModels(); + initializeEmojiGrid(); + } + + public void setEmojiEventListener(EmojiEventListener listener) { + this.listener = listener; + } + + public void setDrawerListener(EmojiDrawerListener listener) { + this.drawerListener = listener; + } + + private void initializeResources(View v) { + LOG.info("initializeResources()"); + 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 (listener != null) listener.onKeyEvent(DELETE_KEY_EVENT); + } + }); + } + + public boolean isShowing() { + return getVisibility() == VISIBLE; + } + + public void show(int height) { + if (this.pager == null) initView(); + ViewGroup.LayoutParams params = getLayoutParams(); + params.height = height; + if (LOG.isLoggable(INFO)) + LOG.info("showing emoji drawer with height " + params.height); + setLayoutParams(params); + setVisibility(VISIBLE); + if (drawerListener != null) drawerListener.onShown(); + } + + public void hide() { + setVisibility(GONE); + if (drawerListener != null) drawerListener.onHidden(); + LOG.info("hide()"); + } + + private void initializeEmojiGrid() { + pager.setAdapter(new EmojiPagerAdapter(getContext(), + models, + new EmojiSelectionListener() { + @Override + public void onEmojiSelected(String emoji) { + LOG.info("onEmojiSelected()"); + recentModel.onCodePointSelected(emoji); + if (listener != null) listener.onEmojiSelected(emoji); + } + })); + + if (recentModel.getEmoji().length == 0) { + pager.setCurrentItem(1); + } + strip.setViewPager(pager); + } + + private void initializePageModels() { + this.models = new LinkedList<>(); + this.recentModel = new RecentEmojiPageModel(getContext()); + this.models.add(recentModel); + this.models.addAll(EmojiProvider.getInstance(getContext()) + .getStaticPages()); + } + + public static class EmojiPagerAdapter extends PagerAdapter + implements CustomTabProvider { + private Context context; + private List pages; + private EmojiSelectionListener listener; + + private EmojiPagerAdapter(@NonNull Context context, + @NonNull List pages, + @Nullable EmojiSelectionListener listener) { + super(); + this.context = context; + this.pages = pages; + this.listener = listener; + } + + @Override + public int getCount() { + return pages.size(); + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + EmojiPageView page = new EmojiPageView(context); + page.setModel(pages.get(position)); + page.setEmojiSelectedListener(listener); + container.addView(page); + return page; + } + + @Override + public void destroyItem(ViewGroup container, int position, + Object object) { + container.removeView((View) object); + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + + @Override + public View getCustomTabView(ViewGroup viewGroup, int i) { + ImageView image = new ImageView(context); + image.setScaleType(ScaleType.CENTER_INSIDE); + image.setImageResource(pages.get(i).getIcon()); + return image; + } + + @Override + public void tabSelected(View view) { + view.animate().setDuration(300).alpha(1); + } + + @Override + public void tabUnselected(View view) { + view.animate().setDuration(400).alpha(0.4f); + } + } + + public interface EmojiEventListener extends EmojiSelectionListener { + void onKeyEvent(KeyEvent keyEvent); + } + + public interface EmojiDrawerListener { + void onShown(); + + void onHidden(); + } +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java new file mode 100644 index 000000000..a8a25671e --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java @@ -0,0 +1,47 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v7.widget.AppCompatEditText; +import android.text.InputFilter; +import android.util.AttributeSet; + +import org.briarproject.R; +import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable; + +@UiThread +public class EmojiEditText extends AppCompatEditText { + + public EmojiEditText(Context context) { + this(context, null); + } + + public EmojiEditText(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, R.attr.editTextStyle); + } + + public EmojiEditText(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + // this ensures the view is redrawn when invalidated + setLayerType(LAYER_TYPE_SOFTWARE, null); + setFilters(new InputFilter[] {new EmojiFilter(this)}); + } + + public void insertEmoji(String emoji) { + final int start = getSelectionStart(); + final int end = getSelectionEnd(); + + getText().replace(Math.min(start, end), Math.max(start, end), emoji); + setSelection(start + emoji.length()); + } + + @Override + public void invalidateDrawable(@NonNull Drawable drawable) { + if (drawable instanceof EmojiDrawable) invalidate(); + else super.invalidateDrawable(drawable); + } +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiFilter.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiFilter.java new file mode 100644 index 000000000..6e9b751cf --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiFilter.java @@ -0,0 +1,35 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.text.InputFilter; +import android.text.Spannable; +import android.text.Spanned; +import android.text.TextUtils; +import android.widget.TextView; + +@UiThread +public class EmojiFilter implements InputFilter { + + private final TextView view; + + public EmojiFilter(TextView view) { + this.view = view; + } + + @Nullable + @Override + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + + char[] v = new char[end - start]; + TextUtils.getChars(source, start, end, v, 0); + Spannable emojified = EmojiProvider.getInstance(view.getContext()) + .emojify(new String(v), view); + if (source instanceof Spanned && emojified != null) { + TextUtils.copySpansFrom((Spanned) source, start, end, null, + emojified, 0); + } + return emojified; + } +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java new file mode 100644 index 000000000..3b3861b19 --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java @@ -0,0 +1,18 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +public interface EmojiPageModel { + @DrawableRes + int getIcon(); + + @NonNull + String[] getEmoji(); + + boolean hasSpriteMap(); + + @Nullable + String getSprite(); +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java new file mode 100644 index 000000000..d101a9daa --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java @@ -0,0 +1,115 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.content.Context; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.util.AttributeSet; +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.FrameLayout; +import android.widget.GridView; + +import org.briarproject.R; + +@UiThread +public class EmojiPageView extends FrameLayout { + + private final GridView grid; + private EmojiSelectionListener listener; + + public EmojiPageView(Context context) { + this(context, null); + } + + public EmojiPageView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public EmojiPageView(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + final View view = LayoutInflater.from(getContext()) + .inflate(R.layout.emoji_grid_layout, this, true); + 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) { + if (listener != null) + listener.onEmojiSelected(((EmojiView) view).getEmoji()); + } + }); + } + + public void setModel(EmojiPageModel model) { + grid.setAdapter(new EmojiGridAdapter(getContext(), 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; + + private 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.getEmoji().length; + } + + @Nullable + @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 EmojiView view; + final int pad = context.getResources() + .getDimensionPixelSize(R.dimen.emoji_drawer_item_padding); + if (convertView != null && convertView instanceof EmojiView) { + view = (EmojiView) convertView; + } else { + final EmojiView emojiView = new EmojiView(context); + emojiView.setPadding(pad, pad, pad, pad); + emojiView.setLayoutParams( + new AbsListView.LayoutParams(emojiSize + 2 * pad, + emojiSize + 2 * pad)); + view = emojiView; + } + + view.setEmoji(model.getEmoji()[position]); + return view; + } + } + + public interface EmojiSelectionListener { + void onEmojiSelected(String emoji); + } +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPages.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPages.java new file mode 100644 index 000000000..9ad5f429b --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiPages.java @@ -0,0 +1,63 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.content.Context; + +import org.briarproject.R; + +import java.util.Arrays; +import java.util.List; + +class EmojiPages { + static List getPages(Context ctx) { + return Arrays.asList( + new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_smiley_people, + R.array.emoji_smiley_people, + "emoji_smiley_people.png"), + new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_animals_nature, + R.array.emoji_animals_nature, + "emoji_animals_nature.png"), + new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_food_drink, + R.array.emoji_food_drink, + "emoji_food_drink.png"), + new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_travel_places, + R.array.emoji_travel_places, + "emoji_travel_places.png"), + new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_activity, + R.array.emoji_activity, + "emoji_activity.png"), + new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_objects, + R.array.emoji_objects, + "emoji_objects.png"), + new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_symbols, + R.array.emoji_symbols, + "emoji_symbols.png"), + new StaticEmojiPageModel(ctx, R.drawable.ic_emoji_flags, + R.array.emoji_flags, + "emoji_flags.png"), + + new StaticEmojiPageModel(R.drawable.ic_emoji_emoticons, + new String[] { + ":-)", ";-)", "(-:", ":->", ":-D", "\\o/", + ":-P", "B-)", ":-$", ":-*", "O:-)", "=-O", + "O_O", "O_o", "o_O", ":O", ":-!", ":-x", + ":-|", ":-\\", ":-(", ":'(", ":-[", ">:-(", + "^.^", "^_^", "\\(\u02c6\u02da\u02c6)/", + "\u30fd(\u00b0\u25c7\u00b0 )\u30ce", + "\u00af\\(\u00b0_o)/\u00af", + "\u00af\\_(\u30c4)_/\u00af", "(\u00ac_\u00ac)", + "(>_<)", "(\u2565\ufe4f\u2565)", + "(\u261e\uff9f\u30ee\uff9f)\u261e", + "\u261c(\uff9f\u30ee\uff9f\u261c)", + "\u261c(\u2312\u25bd\u2312)\u261e", + "(\u256f\u00b0\u25a1\u00b0)\u256f\ufe35", + "\u253b\u2501\u253b", + "\u252c\u2500\u252c", + "\u30ce(\u00b0\u2013\u00b0\u30ce)", + "(^._.^)\uff89", + "\u0e05^\u2022\ufecc\u2022^\u0e05", + "(\u2022_\u2022)", + " \u25a0-\u25a0\u00ac <(\u2022_\u2022) ", + "(\u25a0_\u25a0\u00ac)" + }, null)); + } +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java new file mode 100644 index 000000000..f53a087d0 --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java @@ -0,0 +1,301 @@ +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.os.AsyncTask; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.util.SparseArray; +import android.widget.TextView; + +import org.briarproject.R; +import org.briarproject.android.BaseActivity; +import org.briarproject.android.api.AndroidExecutor; +import org.thoughtcrime.securesms.components.util.FutureTaskListener; +import org.thoughtcrime.securesms.components.util.ListenableFutureTask; +import org.thoughtcrime.securesms.util.BitmapDecodingException; +import org.thoughtcrime.securesms.util.BitmapUtil; + +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.logging.Logger; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import javax.inject.Inject; + +import static java.util.logging.Level.WARNING; + +public class EmojiProvider { + private static final String TAG = EmojiProvider.class.getSimpleName(); + private static volatile EmojiProvider instance = null; + private static final Paint paint = + new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG); + + @Inject + AndroidExecutor androidExecutor; + + private static final Logger LOG = Logger.getLogger(TAG); + + private final SparseArray offsets = new SparseArray<>(); + + private static final Pattern EMOJI_RANGE = Pattern.compile( + // 0x203c,0x2049 0x20a0-0x32ff 0x1f00-0x1fff 0xfe4e5-0xfe4ee + // |=== !!, ?! ===||==== misc ===||========= emoticons =======||========== flags ==========| + "[\\u203c\\u2049\\u20a0-\\u32ff\\ud83c\\udc00-\\ud83f\\udfff\\udbb9\\udce5-\\udbb9\\udcee]"); + + private static final int EMOJI_RAW_HEIGHT = 64; + private static final int EMOJI_RAW_WIDTH = 64; + private static final int EMOJI_VERT_PAD = 0; + private static final int EMOJI_PER_ROW = 32; + + private final Context context; + private final float decodeScale; + private final float verticalPad; + private final List staticPages; + + public static EmojiProvider getInstance(Context context) { + if (instance == null) { + synchronized (EmojiProvider.class) { + if (instance == null) { + LOG.info("Creating new instance of EmojiProvider"); + instance = new EmojiProvider(context); + ((BaseActivity) context).getActivityComponent() + .inject(instance); + } + } + } + return instance; + } + + private EmojiProvider(Context context) { + this.context = context.getApplicationContext(); + this.decodeScale = Math.min(1f, + context.getResources().getDimension(R.dimen.emoji_drawer_size) / + EMOJI_RAW_HEIGHT); + this.verticalPad = EMOJI_VERT_PAD * this.decodeScale; + staticPages = EmojiPages.getPages(context); + for (EmojiPageModel page : staticPages) { + if (page.hasSpriteMap()) { + final EmojiPageBitmap pageBitmap = new EmojiPageBitmap(page); + for (int i = 0; i < page.getEmoji().length; i++) { + offsets.put(Character.codePointAt(page.getEmoji()[i], 0), + new DrawInfo(pageBitmap, i)); + } + } + } + } + + @Nullable + public Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv) { + if (text == null) return null; + Matcher matches = EMOJI_RANGE.matcher(text); + SpannableStringBuilder builder = new SpannableStringBuilder(text); + + while (matches.find()) { + int codePoint = matches.group().codePointAt(0); + Drawable drawable = getEmojiDrawable(codePoint); + if (drawable != null) { + builder.setSpan(new EmojiSpan(drawable, tv), matches.start(), + matches.end(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + return builder; + } + + @Nullable + public Drawable getEmojiDrawable(int emojiCode) { + return getEmojiDrawable(offsets.get(emojiCode)); + } + + @Nullable + private Drawable getEmojiDrawable(DrawInfo drawInfo) { + if (drawInfo == null) { + return null; + } + + final EmojiDrawable drawable = new EmojiDrawable(drawInfo, decodeScale); + drawInfo.page.get().addListener(new FutureTaskListener() { + @Override + public void onSuccess(final Bitmap result) { + androidExecutor.runOnUiThread(new Runnable() { + @Override + public void run() { + drawable.setBitmap(result); + } + }); + } + + @Override + public void onFailure(Throwable error) { + LOG.log(WARNING, error.toString(), error); + } + }); + return drawable; + } + + public List getStaticPages() { + return staticPages; + } + + + public class EmojiDrawable extends Drawable { + private final DrawInfo info; + private Bitmap bmp; + private float intrinsicWidth; + private float intrinsicHeight; + + @Override + public int getIntrinsicWidth() { + return (int) intrinsicWidth; + } + + @Override + public int getIntrinsicHeight() { + return (int) intrinsicHeight; + } + + private EmojiDrawable(DrawInfo info, float decodeScale) { + this.info = info; + this.intrinsicWidth = EMOJI_RAW_WIDTH * decodeScale; + this.intrinsicHeight = EMOJI_RAW_HEIGHT * decodeScale; + } + + @Override + public void draw(@NonNull Canvas canvas) { + if (bmp == null) { + return; + } + + final int row = info.index / EMOJI_PER_ROW; + final int row_index = info.index % EMOJI_PER_ROW; + + canvas.drawBitmap(bmp, + new Rect((int) (row_index * intrinsicWidth), + (int) (row * intrinsicHeight + row * verticalPad), + (int) ((row_index + 1) * intrinsicWidth), + (int) ((row + 1) * intrinsicHeight + + row * verticalPad)), + getBounds(), + paint); + } + + public void setBitmap(Bitmap bitmap) { + if (bmp == null || !bmp.sameAs(bitmap)) { + bmp = bitmap; + invalidateSelf(); + } + } + + @Override + public int getOpacity() { + return PixelFormat.TRANSLUCENT; + } + + @Override + public void setAlpha(int alpha) { + } + + @Override + public void setColorFilter(ColorFilter cf) { + } + } + + + private static class DrawInfo { + private EmojiPageBitmap page; + int index; + + private DrawInfo(final EmojiPageBitmap page, final int index) { + this.page = page; + this.index = index; + } + + @Override + public String toString() { + return "DrawInfo{ " +"page = " + page +", index = " + index + '}'; + } + } + + + private class EmojiPageBitmap { + private EmojiPageModel model; + private SoftReference bitmapReference; + private ListenableFutureTask task; + + private EmojiPageBitmap(EmojiPageModel model) { + this.model = model; + } + + private ListenableFutureTask get() { + if (bitmapReference != null && bitmapReference.get() != null) { + return new ListenableFutureTask<>(bitmapReference.get()); + } else if (task != null) { + return task; + } else { + Callable callable = new Callable() { + @Override + @Nullable + public Bitmap call() throws Exception { + try { + LOG.info("loading page " + model.getSprite()); + return loadPage(); + } catch (IOException ioe) { + LOG.log(WARNING, ioe.toString(), ioe); + } + return null; + } + }; + task = new ListenableFutureTask<>(callable); + new AsyncTask() { + @Override + protected Void doInBackground(Void... params) { + task.run(); + return null; + } + + @Override + protected void onPostExecute(Void aVoid) { + task = null; + } + }.execute(); + } + return task; + } + + private Bitmap loadPage() throws IOException { + if (bitmapReference != null && bitmapReference.get() != null) + return bitmapReference.get(); + + try { + final Bitmap bitmap = BitmapUtil.createScaledBitmap(context, + "file:///android_asset/" + model.getSprite(), + decodeScale); + bitmapReference = new SoftReference<>(bitmap); + LOG.info("onPageLoaded(" + model.getSprite() + ")"); + return bitmap; + } catch (BitmapDecodingException e) { + LOG.log(WARNING, e.toString(), e); + throw new IOException(e); + } + } + + @Nullable + @Override + public String toString() { + return model.getSprite(); + } + } + +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java new file mode 100644 index 000000000..47269d095 --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java @@ -0,0 +1,39 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.graphics.Paint; +import android.graphics.Paint.FontMetricsInt; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.UiThread; +import android.widget.TextView; + +import org.briarproject.R; + +@UiThread +public class EmojiSpan extends AnimatingImageSpan { + private final int size; + private final FontMetricsInt fm; + + public EmojiSpan(@NonNull Drawable drawable, @NonNull TextView tv) { + super(drawable, tv); + fm = tv.getPaint().getFontMetricsInt(); + size = fm != null ? Math.abs(fm.descent) + Math.abs(fm.ascent) + : tv.getResources().getDimensionPixelSize( + R.dimen.conversation_item_body_text_size); + getDrawable().setBounds(0, 0, size, size); + } + + @Override + public int getSize(Paint paint, CharSequence text, int start, int end, + FontMetricsInt fm) { + if (fm != null && this.fm != null) { + fm.ascent = this.fm.ascent; + fm.descent = this.fm.descent; + fm.top = this.fm.top; + fm.bottom = this.fm.bottom; + return size; + } else { + return super.getSize(paint, text, start, end, fm); + } + } +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java new file mode 100644 index 000000000..df354c193 --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java @@ -0,0 +1,97 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.content.Context; +import android.graphics.Paint.FontMetricsInt; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.text.TextUtils; +import android.text.TextUtils.TruncateAt; +import android.util.AttributeSet; +import android.widget.TextView; + +import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable; + +@UiThread +public class EmojiTextView extends TextView { + + private CharSequence source; + private boolean needsEllipsizing; + + public EmojiTextView(Context context) { + this(context, null); + } + + public EmojiTextView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public EmojiTextView(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + // this ensures the view is redrawn when invalidated + setLayerType(LAYER_TYPE_SOFTWARE, null); + } + + @Override + public void setText(@Nullable CharSequence text, BufferType type) { + source = EmojiProvider.getInstance(getContext()).emojify(text, this); + setTextEllipsized(source); + } + + private void setTextEllipsized(final @Nullable CharSequence source) { + super.setText( + needsEllipsizing ? ellipsize(source) : source, + BufferType.SPANNABLE); + } + + @Override + public void invalidateDrawable(@NonNull Drawable drawable) { + if (drawable instanceof EmojiDrawable) invalidate(); + else super.invalidateDrawable(drawable); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + final int size = MeasureSpec.getSize(widthMeasureSpec); + final int mode = MeasureSpec.getMode(widthMeasureSpec); + if (getEllipsize() == TruncateAt.END && + !TextUtils.isEmpty(source) && + (mode == MeasureSpec.AT_MOST || mode == MeasureSpec.EXACTLY) && + getPaint().breakText(source, 0, source.length() - 1, true, size, + null) != source.length()) { + needsEllipsizing = true; + FontMetricsInt font = getPaint().getFontMetricsInt(); + super.onMeasure( + MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY), + MeasureSpec + .makeMeasureSpec(Math.abs(font.top - font.bottom), + MeasureSpec.EXACTLY)); + } else { + needsEllipsizing = false; + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + @Override + protected void onLayout(boolean changed, int left, int top, int right, + int bottom) { + if (changed) setTextEllipsized(source); + super.onLayout(changed, left, top, right, bottom); + } + + @Nullable + public CharSequence ellipsize(@Nullable CharSequence text) { + if (TextUtils.isEmpty(text) || getWidth() == 0 || + getEllipsize() != TruncateAt.END) { + return text; + } else { + return TextUtils.ellipsize(text, + getPaint(), + getWidth() - getPaddingRight() - getPaddingLeft(), + TruncateAt.END); + } + } + +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java new file mode 100644 index 000000000..d1f472243 --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java @@ -0,0 +1,60 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.widget.ImageButton; + +import org.briarproject.R; +import org.thoughtcrime.securesms.components.emoji.EmojiDrawer.EmojiDrawerListener; + +@UiThread +public class EmojiToggle extends ImageButton implements EmojiDrawerListener { + + private final Drawable emojiToggle; + private final Drawable imeToggle; + + public EmojiToggle(Context context) { + this(context, null); + } + + public EmojiToggle(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public EmojiToggle(Context context, @Nullable AttributeSet attrs, + int defStyle) { + super(context, attrs, defStyle); + + emojiToggle = ContextCompat + .getDrawable(getContext(), R.drawable.ic_emoji_toggle); + imeToggle = ContextCompat + .getDrawable(getContext(), R.drawable.ic_keyboard_black); + setToEmoji(); + } + + public void setToEmoji() { + setImageDrawable(emojiToggle); + } + + public void setToIme() { + setImageDrawable(imeToggle); + } + + public void attach(EmojiDrawer drawer) { + drawer.setDrawerListener(this); + } + + @Override + public void onShown() { + setToIme(); + } + + @Override + public void onHidden() { + setToEmoji(); + } +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiView.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiView.java new file mode 100644 index 000000000..1126f034a --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/EmojiView.java @@ -0,0 +1,89 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.drawable.Drawable; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; +import android.support.v4.content.ContextCompat; +import android.util.AttributeSet; +import android.view.View; + +import org.briarproject.R; + +@UiThread +public class EmojiView extends View implements Drawable.Callback { + private String emoji; + private Drawable drawable; + + private final Paint paint = + new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG); + + public EmojiView(Context context) { + this(context, null); + } + + public EmojiView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public EmojiView(Context context, @Nullable AttributeSet attrs, + int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + public void setEmoji(String emoji) { + this.emoji = emoji; + this.drawable = EmojiProvider.getInstance(getContext()) + .getEmojiDrawable(Character.codePointAt(emoji, 0)); + postInvalidate(); + } + + public String getEmoji() { + return emoji; + } + + @Override + protected void onDraw(Canvas canvas) { + if (drawable != null) { + drawable.setBounds(getPaddingLeft(), + getPaddingTop(), + getWidth() - getPaddingRight(), + getHeight() - getPaddingBottom()); + drawable.setCallback(this); + drawable.draw(canvas); + } else { + float targetFontSize = + 0.75f * getHeight() - getPaddingTop() - getPaddingBottom(); + paint.setTextSize(targetFontSize); + paint.setColor(ContextCompat + .getColor(getContext(), R.color.emoji_text_color)); + paint.setTextAlign(Paint.Align.CENTER); + int xPos = (canvas.getWidth() / 2); + int yPos = (int) ((canvas.getHeight() / 2) - + ((paint.descent() + paint.ascent()) / 2)); + + float overflow = paint.measureText(emoji) / + (getWidth() - getPaddingLeft() - getPaddingRight()); + if (overflow > 1f) { + paint.setTextSize(targetFontSize / overflow); + yPos = (int) ((canvas.getHeight() / 2) - + ((paint.descent() + paint.ascent()) / 2)); + } + canvas.drawText(emoji, xPos, yPos, paint); + } + } + + @Override + public void invalidateDrawable(@NonNull Drawable drawable) { + super.invalidateDrawable(drawable); + postInvalidate(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java new file mode 100644 index 000000000..312d83729 --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java @@ -0,0 +1,144 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.content.Context; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; + +import org.briarproject.R; +import org.briarproject.android.BaseActivity; +import org.briarproject.android.controller.DbController; +import org.briarproject.api.db.DbException; +import org.briarproject.api.settings.Settings; +import org.briarproject.api.settings.SettingsManager; + +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.logging.Logger; + +import javax.inject.Inject; + +import static java.util.logging.Level.INFO; +import static java.util.logging.Level.WARNING; +import static org.briarproject.android.fragment.SettingsFragment.SETTINGS_NAMESPACE; + +@UiThread +public class RecentEmojiPageModel implements EmojiPageModel { + + private static final String TAG = + RecentEmojiPageModel.class.getSimpleName(); + private static final Logger LOG = Logger.getLogger(TAG); + + private static final String EMOJI_LRU_PREFERENCE = "pref_emoji_recent"; + private static final int EMOJI_LRU_SIZE = 50; + + private final LinkedHashSet recentlyUsed; + private Settings settings; + + @Inject + protected SettingsManager settingsManager; + @Inject + protected DbController dbController; + + public RecentEmojiPageModel(Context context) { + if (!(context instanceof BaseActivity)) { + throw new IllegalArgumentException( + "Needs to be created from BaseActivity"); + } + ((BaseActivity) context).getActivityComponent().inject(this); + recentlyUsed = getPersistedCache(); + } + + private LinkedHashSet getPersistedCache() { + String serialized; + try { + settings = settingsManager.getSettings(SETTINGS_NAMESPACE); + serialized = settings.get(EMOJI_LRU_PREFERENCE); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) LOG.log(WARNING, e.toString(), e); + serialized = null; + } + return deserialize(serialized); + } + + @DrawableRes + @Override + public int getIcon() { + return R.drawable.ic_emoji_recent; + } + + @NonNull + @Override + public String[] getEmoji() { + return toReversePrimitiveArray(recentlyUsed); + } + + @Override + public boolean hasSpriteMap() { + return false; + } + + @Override + public String getSprite() { + return null; + } + + public void onCodePointSelected(String emoji) { + if (LOG.isLoggable(INFO)) + LOG.info("onCodePointSelected(" + emoji + ")"); + recentlyUsed.remove(emoji); + recentlyUsed.add(emoji); + + if (recentlyUsed.size() > EMOJI_LRU_SIZE) { + Iterator iterator = recentlyUsed.iterator(); + iterator.next(); + iterator.remove(); + } + save(recentlyUsed); + } + + private String serialize(LinkedHashSet emojis) { + String result = ""; + for (String emoji : emojis) { + result += emoji + ";"; + } + if (!emojis.isEmpty()) + result = result.substring(0, result.length()-1); + return result; + } + + private LinkedHashSet deserialize(@Nullable String str) { + String[] list = str == null ? new String[] {} : str.split(";"); + LinkedHashSet result = new LinkedHashSet<>(list.length); + Collections.addAll(result, list); + return result; + } + + private void save(final LinkedHashSet recentlyUsed) { + dbController.runOnDbThread(new Runnable() { + @Override + public void run() { + String serialized = serialize(recentlyUsed); + settings.put(EMOJI_LRU_PREFERENCE, serialized); + try { + settingsManager.mergeSettings(settings, SETTINGS_NAMESPACE); + } catch (DbException e) { + if (LOG.isLoggable(WARNING)) + LOG.log(WARNING, e.toString(), e); + } + } + }); + } + + private String[] toReversePrimitiveArray( + @NonNull LinkedHashSet emojiSet) { + String[] emojis = new String[emojiSet.size()]; + int i = emojiSet.size() - 1; + for (String emoji : emojiSet) { + emojis[i--] = emoji; + } + return emojis; + } +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java b/briar-android/src/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java new file mode 100644 index 000000000..7c359c831 --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java @@ -0,0 +1,72 @@ +package org.thoughtcrime.securesms.components.emoji; + +import android.content.Context; +import android.support.annotation.ArrayRes; +import android.support.annotation.DrawableRes; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.UiThread; + +@UiThread +public class StaticEmojiPageModel implements EmojiPageModel { + + @DrawableRes + private final int icon; + @NonNull + private final String[] emoji; + @Nullable + private final String sprite; + + public StaticEmojiPageModel(@DrawableRes int icon, @NonNull String[] emoji, + @Nullable String sprite) { + this.icon = icon; + this.emoji = emoji; + this.sprite = sprite; + } + + public StaticEmojiPageModel(Context ctx, @DrawableRes int icon, + @ArrayRes int res, @Nullable String sprite) { + this(icon, getEmoji(ctx, res), sprite); + } + + @DrawableRes + @Override + public int getIcon() { + return icon; + } + + @NonNull + public String[] getEmoji() { + return emoji; + } + + @Override + public boolean hasSpriteMap() { + return sprite != null; + } + + @Nullable + @Override + public String getSprite() { + return sprite; + } + + @NonNull + private static String[] getEmoji(Context ctx, @ArrayRes int res) { + String[] rawStrings = ctx.getResources().getStringArray(res); + String[] emoji = new String[rawStrings.length]; + int i = 0; + for (String codePoint : rawStrings) { + String[] bytes = codePoint.split(","); + int[] codePoints = new int[bytes.length]; + int j = 0; + for (String b : bytes) { + codePoints[j] = Integer.valueOf(b, 16); + } + emoji[i] = new String(codePoints, 0, codePoints.length); + i++; + } + return emoji; + } + +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/util/FutureTaskListener.java b/briar-android/src/org/thoughtcrime/securesms/components/util/FutureTaskListener.java new file mode 100644 index 000000000..80da2eccf --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/util/FutureTaskListener.java @@ -0,0 +1,23 @@ +/** + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms.components.util; + +public interface FutureTaskListener { + void onSuccess(V result); + + void onFailure(Throwable error); +} diff --git a/briar-android/src/org/thoughtcrime/securesms/components/util/ListenableFutureTask.java b/briar-android/src/org/thoughtcrime/securesms/components/util/ListenableFutureTask.java new file mode 100644 index 000000000..128314884 --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/components/util/ListenableFutureTask.java @@ -0,0 +1,110 @@ +/** + * Copyright (C) 2014 Open Whisper Systems + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package org.thoughtcrime.securesms.components.util; + +import android.support.annotation.Nullable; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.FutureTask; + +public class ListenableFutureTask extends FutureTask { + + private final List> listeners = new LinkedList<>(); + + @Nullable + private final Object identifier; + + public ListenableFutureTask(Callable callable) { + this(callable, null); + } + + private ListenableFutureTask(Callable callable, + @Nullable Object identifier) { + super(callable); + this.identifier = identifier; + } + + public ListenableFutureTask(final V result) { + this(result, null); + } + + private ListenableFutureTask(final V result, @Nullable Object identifier) { + super(new Callable() { + @Override + public V call() throws Exception { + return result; + } + }); + this.identifier = identifier; + this.run(); + } + + public synchronized void addListener(FutureTaskListener listener) { + if (this.isDone()) { + callback(listener); + } else { + this.listeners.add(listener); + } + } + + public synchronized void removeListener(FutureTaskListener listener) { + this.listeners.remove(listener); + } + + @Override + protected synchronized void done() { + callback(); + } + + private void callback() { + for (FutureTaskListener listener : listeners) { + callback(listener); + } + } + + private void callback(FutureTaskListener listener) { + if (listener != null) { + try { + listener.onSuccess(get()); + } catch (InterruptedException e) { + throw new AssertionError(e); + } catch (ExecutionException e) { + listener.onFailure(e); + } + } + } + + @Override + public boolean equals(Object other) { + if (other != null && other instanceof ListenableFutureTask && + this.identifier != null) { + return identifier.equals(other); + } else { + return super.equals(other); + } + } + + @Override + public int hashCode() { + if (identifier != null) return identifier.hashCode(); + else return super.hashCode(); + } + +} diff --git a/briar-android/src/org/thoughtcrime/securesms/util/BitmapDecodingException.java b/briar-android/src/org/thoughtcrime/securesms/util/BitmapDecodingException.java new file mode 100644 index 000000000..5a4b41462 --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/util/BitmapDecodingException.java @@ -0,0 +1,12 @@ +package org.thoughtcrime.securesms.util; + +public class BitmapDecodingException extends Exception { + + public BitmapDecodingException(String s) { + super(s); + } + + public BitmapDecodingException(Exception nested) { + super(nested); + } +} diff --git a/briar-android/src/org/thoughtcrime/securesms/util/BitmapUtil.java b/briar-android/src/org/thoughtcrime/securesms/util/BitmapUtil.java new file mode 100644 index 000000000..92a8425fc --- /dev/null +++ b/briar-android/src/org/thoughtcrime/securesms/util/BitmapUtil.java @@ -0,0 +1,95 @@ +package org.thoughtcrime.securesms.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.util.Log; +import android.util.Pair; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.Priority; +import com.bumptech.glide.load.DecodeFormat; +import com.bumptech.glide.load.engine.Resource; +import com.bumptech.glide.load.resource.bitmap.BitmapResource; +import com.bumptech.glide.load.resource.bitmap.Downsampler; +import com.bumptech.glide.load.resource.bitmap.FitCenter; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class BitmapUtil { + + private static final String TAG = BitmapUtil.class.getSimpleName(); + + private static InputStream getInputStreamForModel(Context context, + T model) + throws BitmapDecodingException { + try { + return Glide.buildStreamModelLoader(model, context) + .getResourceFetcher(model, -1, -1) + .loadData(Priority.NORMAL); + } catch (Exception e) { + throw new BitmapDecodingException(e); + } + } + + private static Bitmap createScaledBitmapInto(Context context, T model, + int width, int height) + throws BitmapDecodingException { + final Bitmap rough = Downsampler.AT_LEAST + .decode(getInputStreamForModel(context, model), + Glide.get(context).getBitmapPool(), + width, height, + DecodeFormat.PREFER_RGB_565); + + final Resource resource = BitmapResource + .obtain(rough, Glide.get(context).getBitmapPool()); + final Resource result = + new FitCenter(context).transform(resource, width, height); + + if (result == null) { + throw new BitmapDecodingException("unable to transform Bitmap"); + } + return result.get(); + } + + public static Bitmap createScaledBitmap(Context context, T model, + float scale) + throws BitmapDecodingException { + Pair dimens = + getDimensions(getInputStreamForModel(context, model)); + return createScaledBitmapInto(context, model, + (int) (dimens.first * scale), (int) (dimens.second * scale)); + } + + private static BitmapFactory.Options getImageDimensions( + InputStream inputStream) + throws BitmapDecodingException { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BufferedInputStream fis = new BufferedInputStream(inputStream); + BitmapFactory.decodeStream(fis, null, options); + try { + fis.close(); + } catch (IOException ioe) { + Log.w(TAG, + "failed to close the InputStream after reading image dimensions"); + } + + if (options.outWidth == -1 || options.outHeight == -1) { + throw new BitmapDecodingException( + "Failed to decode image dimensions: " + options.outWidth + + ", " + options.outHeight); + } + + return options; + } + + private static Pair getDimensions(InputStream inputStream) + throws BitmapDecodingException { + BitmapFactory.Options options = getImageDimensions(inputStream); + return new Pair<>(options.outWidth, options.outHeight); + } + +} diff --git a/build.gradle b/build.gradle index ab0a691b0..6896cc0d6 100644 --- a/build.gradle +++ b/build.gradle @@ -14,7 +14,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:2.1.3' + classpath 'com.android.tools.build:gradle:2.2.0' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' classpath 'de.undercouch:gradle-download-task:2.1.0' classpath files('briar-core/libs/gradle-witness.jar')