diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index 9d7a328216..1e4723557c 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -383,7 +383,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
@@ -422,6 +422,11 @@
android:exported="true"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
+
+
diff --git a/res/anim/camera_capture_button_grow.xml b/res/anim/camera_capture_button_grow.xml
new file mode 100644
index 0000000000..e376f57319
--- /dev/null
+++ b/res/anim/camera_capture_button_grow.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/anim/camera_capture_button_shrink.xml b/res/anim/camera_capture_button_shrink.xml
new file mode 100644
index 0000000000..45e82ca61a
--- /dev/null
+++ b/res/anim/camera_capture_button_shrink.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/anim/camera_slide_from_bottom.xml b/res/anim/camera_slide_from_bottom.xml
new file mode 100644
index 0000000000..5d7343cf25
--- /dev/null
+++ b/res/anim/camera_slide_from_bottom.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/res/anim/camera_slide_to_bottom.xml b/res/anim/camera_slide_to_bottom.xml
new file mode 100644
index 0000000000..d50cccb14a
--- /dev/null
+++ b/res/anim/camera_slide_to_bottom.xml
@@ -0,0 +1,8 @@
+
+
+
+
\ No newline at end of file
diff --git a/res/anim/fade_in.xml b/res/anim/fade_in.xml
new file mode 100644
index 0000000000..508f8be39d
--- /dev/null
+++ b/res/anim/fade_in.xml
@@ -0,0 +1,6 @@
+
diff --git a/res/anim/fade_out.xml b/res/anim/fade_out.xml
new file mode 100644
index 0000000000..e8f16d01aa
--- /dev/null
+++ b/res/anim/fade_out.xml
@@ -0,0 +1,6 @@
+
diff --git a/res/anim/stationary.xml b/res/anim/stationary.xml
new file mode 100644
index 0000000000..92cf98d665
--- /dev/null
+++ b/res/anim/stationary.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable-hdpi/baseline_highlight_white_24.png b/res/drawable-hdpi/baseline_highlight_white_24.png
deleted file mode 100644
index 2491e9ecb1..0000000000
Binary files a/res/drawable-hdpi/baseline_highlight_white_24.png and /dev/null differ
diff --git a/res/drawable-hdpi/ic_brush_white_24dp.png b/res/drawable-hdpi/ic_brush_white_24dp.png
deleted file mode 100644
index f813722efe..0000000000
Binary files a/res/drawable-hdpi/ic_brush_white_24dp.png and /dev/null differ
diff --git a/res/drawable-hdpi/ic_camera_front.png b/res/drawable-hdpi/ic_camera_front.png
new file mode 100644
index 0000000000..4895cfc794
Binary files /dev/null and b/res/drawable-hdpi/ic_camera_front.png differ
diff --git a/res/drawable-hdpi/ic_camera_rear.png b/res/drawable-hdpi/ic_camera_rear.png
new file mode 100644
index 0000000000..befb6e2fd7
Binary files /dev/null and b/res/drawable-hdpi/ic_camera_rear.png differ
diff --git a/res/drawable-hdpi/ic_camera_shutter.png b/res/drawable-hdpi/ic_camera_shutter.png
new file mode 100644
index 0000000000..60d3ec9a4c
Binary files /dev/null and b/res/drawable-hdpi/ic_camera_shutter.png differ
diff --git a/res/drawable-hdpi/ic_replay_white_24dp.png b/res/drawable-hdpi/ic_replay_white_24dp.png
deleted file mode 100644
index fc59ca0d51..0000000000
Binary files a/res/drawable-hdpi/ic_replay_white_24dp.png and /dev/null differ
diff --git a/res/drawable-hdpi/ic_scribble_brush.png b/res/drawable-hdpi/ic_scribble_brush.png
new file mode 100644
index 0000000000..6c31c106a3
Binary files /dev/null and b/res/drawable-hdpi/ic_scribble_brush.png differ
diff --git a/res/drawable-hdpi/ic_scribble_delete.png b/res/drawable-hdpi/ic_scribble_delete.png
new file mode 100644
index 0000000000..582e992320
Binary files /dev/null and b/res/drawable-hdpi/ic_scribble_delete.png differ
diff --git a/res/drawable-hdpi/ic_scribble_highlight.png b/res/drawable-hdpi/ic_scribble_highlight.png
new file mode 100644
index 0000000000..8fa4e030fb
Binary files /dev/null and b/res/drawable-hdpi/ic_scribble_highlight.png differ
diff --git a/res/drawable-hdpi/ic_scribble_save.png b/res/drawable-hdpi/ic_scribble_save.png
new file mode 100644
index 0000000000..37ae7639c8
Binary files /dev/null and b/res/drawable-hdpi/ic_scribble_save.png differ
diff --git a/res/drawable-hdpi/ic_scribble_sticker.png b/res/drawable-hdpi/ic_scribble_sticker.png
new file mode 100644
index 0000000000..883e69eed8
Binary files /dev/null and b/res/drawable-hdpi/ic_scribble_sticker.png differ
diff --git a/res/drawable-hdpi/ic_scribble_text.png b/res/drawable-hdpi/ic_scribble_text.png
new file mode 100644
index 0000000000..21e335c99c
Binary files /dev/null and b/res/drawable-hdpi/ic_scribble_text.png differ
diff --git a/res/drawable-hdpi/ic_scribble_undo.png b/res/drawable-hdpi/ic_scribble_undo.png
new file mode 100644
index 0000000000..693cde60d2
Binary files /dev/null and b/res/drawable-hdpi/ic_scribble_undo.png differ
diff --git a/res/drawable-hdpi/ic_text_fields_white_24dp.png b/res/drawable-hdpi/ic_text_fields_white_24dp.png
deleted file mode 100644
index a1ff66d673..0000000000
Binary files a/res/drawable-hdpi/ic_text_fields_white_24dp.png and /dev/null differ
diff --git a/res/drawable-mdpi/baseline_highlight_white_24.png b/res/drawable-mdpi/baseline_highlight_white_24.png
deleted file mode 100644
index b48342dd71..0000000000
Binary files a/res/drawable-mdpi/baseline_highlight_white_24.png and /dev/null differ
diff --git a/res/drawable-mdpi/ic_brush_white_24dp.png b/res/drawable-mdpi/ic_brush_white_24dp.png
deleted file mode 100644
index b06a66460e..0000000000
Binary files a/res/drawable-mdpi/ic_brush_white_24dp.png and /dev/null differ
diff --git a/res/drawable-mdpi/ic_camera_front.png b/res/drawable-mdpi/ic_camera_front.png
new file mode 100644
index 0000000000..dcad59568c
Binary files /dev/null and b/res/drawable-mdpi/ic_camera_front.png differ
diff --git a/res/drawable-mdpi/ic_camera_rear.png b/res/drawable-mdpi/ic_camera_rear.png
new file mode 100644
index 0000000000..b124675c98
Binary files /dev/null and b/res/drawable-mdpi/ic_camera_rear.png differ
diff --git a/res/drawable-mdpi/ic_camera_shutter.png b/res/drawable-mdpi/ic_camera_shutter.png
new file mode 100644
index 0000000000..c649051dcf
Binary files /dev/null and b/res/drawable-mdpi/ic_camera_shutter.png differ
diff --git a/res/drawable-mdpi/ic_replay_white_24dp.png b/res/drawable-mdpi/ic_replay_white_24dp.png
deleted file mode 100644
index 0b90fb1339..0000000000
Binary files a/res/drawable-mdpi/ic_replay_white_24dp.png and /dev/null differ
diff --git a/res/drawable-mdpi/ic_scribble_brush.png b/res/drawable-mdpi/ic_scribble_brush.png
new file mode 100644
index 0000000000..fca4db97af
Binary files /dev/null and b/res/drawable-mdpi/ic_scribble_brush.png differ
diff --git a/res/drawable-mdpi/ic_scribble_delete.png b/res/drawable-mdpi/ic_scribble_delete.png
new file mode 100644
index 0000000000..367b6bb696
Binary files /dev/null and b/res/drawable-mdpi/ic_scribble_delete.png differ
diff --git a/res/drawable-mdpi/ic_scribble_highlight.png b/res/drawable-mdpi/ic_scribble_highlight.png
new file mode 100644
index 0000000000..9427b396f6
Binary files /dev/null and b/res/drawable-mdpi/ic_scribble_highlight.png differ
diff --git a/res/drawable-mdpi/ic_scribble_save.png b/res/drawable-mdpi/ic_scribble_save.png
new file mode 100644
index 0000000000..1bf4c3b0cf
Binary files /dev/null and b/res/drawable-mdpi/ic_scribble_save.png differ
diff --git a/res/drawable-mdpi/ic_scribble_sticker.png b/res/drawable-mdpi/ic_scribble_sticker.png
new file mode 100644
index 0000000000..a727960525
Binary files /dev/null and b/res/drawable-mdpi/ic_scribble_sticker.png differ
diff --git a/res/drawable-mdpi/ic_scribble_text.png b/res/drawable-mdpi/ic_scribble_text.png
new file mode 100644
index 0000000000..e116229292
Binary files /dev/null and b/res/drawable-mdpi/ic_scribble_text.png differ
diff --git a/res/drawable-mdpi/ic_scribble_undo.png b/res/drawable-mdpi/ic_scribble_undo.png
new file mode 100644
index 0000000000..2cef26a92d
Binary files /dev/null and b/res/drawable-mdpi/ic_scribble_undo.png differ
diff --git a/res/drawable-mdpi/ic_text_fields_white_24dp.png b/res/drawable-mdpi/ic_text_fields_white_24dp.png
deleted file mode 100644
index d41ed201e1..0000000000
Binary files a/res/drawable-mdpi/ic_text_fields_white_24dp.png and /dev/null differ
diff --git a/res/drawable-xhdpi/baseline_highlight_white_24.png b/res/drawable-xhdpi/baseline_highlight_white_24.png
deleted file mode 100644
index 3ada66c2d1..0000000000
Binary files a/res/drawable-xhdpi/baseline_highlight_white_24.png and /dev/null differ
diff --git a/res/drawable-xhdpi/ic_brush_white_24dp.png b/res/drawable-xhdpi/ic_brush_white_24dp.png
deleted file mode 100644
index 4d5cc6e12b..0000000000
Binary files a/res/drawable-xhdpi/ic_brush_white_24dp.png and /dev/null differ
diff --git a/res/drawable-xhdpi/ic_camera_front.png b/res/drawable-xhdpi/ic_camera_front.png
new file mode 100644
index 0000000000..f8143b9942
Binary files /dev/null and b/res/drawable-xhdpi/ic_camera_front.png differ
diff --git a/res/drawable-xhdpi/ic_camera_rear.png b/res/drawable-xhdpi/ic_camera_rear.png
new file mode 100644
index 0000000000..5e64fdbbee
Binary files /dev/null and b/res/drawable-xhdpi/ic_camera_rear.png differ
diff --git a/res/drawable-xhdpi/ic_camera_shutter.png b/res/drawable-xhdpi/ic_camera_shutter.png
new file mode 100644
index 0000000000..f343ec1a3d
Binary files /dev/null and b/res/drawable-xhdpi/ic_camera_shutter.png differ
diff --git a/res/drawable-xhdpi/ic_replay_white_24dp.png b/res/drawable-xhdpi/ic_replay_white_24dp.png
deleted file mode 100644
index 72d1d9d45c..0000000000
Binary files a/res/drawable-xhdpi/ic_replay_white_24dp.png and /dev/null differ
diff --git a/res/drawable-xhdpi/ic_scribble_brush.png b/res/drawable-xhdpi/ic_scribble_brush.png
new file mode 100644
index 0000000000..6dba282c76
Binary files /dev/null and b/res/drawable-xhdpi/ic_scribble_brush.png differ
diff --git a/res/drawable-xhdpi/ic_scribble_delete.png b/res/drawable-xhdpi/ic_scribble_delete.png
new file mode 100644
index 0000000000..fc229e0724
Binary files /dev/null and b/res/drawable-xhdpi/ic_scribble_delete.png differ
diff --git a/res/drawable-xhdpi/ic_scribble_highlight.png b/res/drawable-xhdpi/ic_scribble_highlight.png
new file mode 100644
index 0000000000..23e86125c0
Binary files /dev/null and b/res/drawable-xhdpi/ic_scribble_highlight.png differ
diff --git a/res/drawable-xhdpi/ic_scribble_save.png b/res/drawable-xhdpi/ic_scribble_save.png
new file mode 100644
index 0000000000..bbdcc4c899
Binary files /dev/null and b/res/drawable-xhdpi/ic_scribble_save.png differ
diff --git a/res/drawable-xhdpi/ic_scribble_sticker.png b/res/drawable-xhdpi/ic_scribble_sticker.png
new file mode 100644
index 0000000000..9cc0bf0bde
Binary files /dev/null and b/res/drawable-xhdpi/ic_scribble_sticker.png differ
diff --git a/res/drawable-xhdpi/ic_scribble_text.png b/res/drawable-xhdpi/ic_scribble_text.png
new file mode 100644
index 0000000000..e30129ba63
Binary files /dev/null and b/res/drawable-xhdpi/ic_scribble_text.png differ
diff --git a/res/drawable-xhdpi/ic_scribble_undo.png b/res/drawable-xhdpi/ic_scribble_undo.png
new file mode 100644
index 0000000000..b74a133d54
Binary files /dev/null and b/res/drawable-xhdpi/ic_scribble_undo.png differ
diff --git a/res/drawable-xhdpi/ic_text_fields_white_24dp.png b/res/drawable-xhdpi/ic_text_fields_white_24dp.png
deleted file mode 100644
index bd6051f8c8..0000000000
Binary files a/res/drawable-xhdpi/ic_text_fields_white_24dp.png and /dev/null differ
diff --git a/res/drawable-xxhdpi/baseline_highlight_white_24.png b/res/drawable-xxhdpi/baseline_highlight_white_24.png
deleted file mode 100644
index 57a8a0ff08..0000000000
Binary files a/res/drawable-xxhdpi/baseline_highlight_white_24.png and /dev/null differ
diff --git a/res/drawable-xxhdpi/ic_brush_white_24dp.png b/res/drawable-xxhdpi/ic_brush_white_24dp.png
deleted file mode 100644
index 071b38eee7..0000000000
Binary files a/res/drawable-xxhdpi/ic_brush_white_24dp.png and /dev/null differ
diff --git a/res/drawable-xxhdpi/ic_camera_front.png b/res/drawable-xxhdpi/ic_camera_front.png
new file mode 100644
index 0000000000..b053de97ed
Binary files /dev/null and b/res/drawable-xxhdpi/ic_camera_front.png differ
diff --git a/res/drawable-xxhdpi/ic_camera_rear.png b/res/drawable-xxhdpi/ic_camera_rear.png
new file mode 100644
index 0000000000..788e621db7
Binary files /dev/null and b/res/drawable-xxhdpi/ic_camera_rear.png differ
diff --git a/res/drawable-xxhdpi/ic_camera_shutter.png b/res/drawable-xxhdpi/ic_camera_shutter.png
new file mode 100644
index 0000000000..9225f2536c
Binary files /dev/null and b/res/drawable-xxhdpi/ic_camera_shutter.png differ
diff --git a/res/drawable-xxhdpi/ic_replay_white_24dp.png b/res/drawable-xxhdpi/ic_replay_white_24dp.png
deleted file mode 100644
index 5df9d2b99e..0000000000
Binary files a/res/drawable-xxhdpi/ic_replay_white_24dp.png and /dev/null differ
diff --git a/res/drawable-xxhdpi/ic_scribble_brush.png b/res/drawable-xxhdpi/ic_scribble_brush.png
new file mode 100644
index 0000000000..10c37572ce
Binary files /dev/null and b/res/drawable-xxhdpi/ic_scribble_brush.png differ
diff --git a/res/drawable-xxhdpi/ic_scribble_delete.png b/res/drawable-xxhdpi/ic_scribble_delete.png
new file mode 100644
index 0000000000..f127ff2c6d
Binary files /dev/null and b/res/drawable-xxhdpi/ic_scribble_delete.png differ
diff --git a/res/drawable-xxhdpi/ic_scribble_highlight.png b/res/drawable-xxhdpi/ic_scribble_highlight.png
new file mode 100644
index 0000000000..62ee1cf131
Binary files /dev/null and b/res/drawable-xxhdpi/ic_scribble_highlight.png differ
diff --git a/res/drawable-xxhdpi/ic_scribble_save.png b/res/drawable-xxhdpi/ic_scribble_save.png
new file mode 100644
index 0000000000..dba7db4251
Binary files /dev/null and b/res/drawable-xxhdpi/ic_scribble_save.png differ
diff --git a/res/drawable-xxhdpi/ic_scribble_sticker.png b/res/drawable-xxhdpi/ic_scribble_sticker.png
new file mode 100644
index 0000000000..53825760e2
Binary files /dev/null and b/res/drawable-xxhdpi/ic_scribble_sticker.png differ
diff --git a/res/drawable-xxhdpi/ic_scribble_text.png b/res/drawable-xxhdpi/ic_scribble_text.png
new file mode 100644
index 0000000000..7b091c9bac
Binary files /dev/null and b/res/drawable-xxhdpi/ic_scribble_text.png differ
diff --git a/res/drawable-xxhdpi/ic_scribble_undo.png b/res/drawable-xxhdpi/ic_scribble_undo.png
new file mode 100644
index 0000000000..6ffae6f213
Binary files /dev/null and b/res/drawable-xxhdpi/ic_scribble_undo.png differ
diff --git a/res/drawable-xxhdpi/ic_text_fields_white_24dp.png b/res/drawable-xxhdpi/ic_text_fields_white_24dp.png
deleted file mode 100644
index 0f2f0a7ed9..0000000000
Binary files a/res/drawable-xxhdpi/ic_text_fields_white_24dp.png and /dev/null differ
diff --git a/res/drawable-xxxhdpi/baseline_highlight_white_24.png b/res/drawable-xxxhdpi/baseline_highlight_white_24.png
deleted file mode 100644
index 5e2f24bb90..0000000000
Binary files a/res/drawable-xxxhdpi/baseline_highlight_white_24.png and /dev/null differ
diff --git a/res/drawable-xxxhdpi/ic_brush_white_24dp.png b/res/drawable-xxxhdpi/ic_brush_white_24dp.png
deleted file mode 100644
index d26893d2dc..0000000000
Binary files a/res/drawable-xxxhdpi/ic_brush_white_24dp.png and /dev/null differ
diff --git a/res/drawable-xxxhdpi/ic_camera_front.png b/res/drawable-xxxhdpi/ic_camera_front.png
new file mode 100644
index 0000000000..69726bd2fe
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_camera_front.png differ
diff --git a/res/drawable-xxxhdpi/ic_camera_rear.png b/res/drawable-xxxhdpi/ic_camera_rear.png
new file mode 100644
index 0000000000..7ec77723b3
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_camera_rear.png differ
diff --git a/res/drawable-xxxhdpi/ic_camera_shutter.png b/res/drawable-xxxhdpi/ic_camera_shutter.png
new file mode 100644
index 0000000000..8b59faaeb3
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_camera_shutter.png differ
diff --git a/res/drawable-xxxhdpi/ic_replay_white_24dp.png b/res/drawable-xxxhdpi/ic_replay_white_24dp.png
deleted file mode 100644
index c3d5f96adb..0000000000
Binary files a/res/drawable-xxxhdpi/ic_replay_white_24dp.png and /dev/null differ
diff --git a/res/drawable-xxxhdpi/ic_scribble_brush.png b/res/drawable-xxxhdpi/ic_scribble_brush.png
new file mode 100644
index 0000000000..38e12c3284
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_scribble_brush.png differ
diff --git a/res/drawable-xxxhdpi/ic_scribble_delete.png b/res/drawable-xxxhdpi/ic_scribble_delete.png
new file mode 100644
index 0000000000..53dcb4fdaf
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_scribble_delete.png differ
diff --git a/res/drawable-xxxhdpi/ic_scribble_highlight.png b/res/drawable-xxxhdpi/ic_scribble_highlight.png
new file mode 100644
index 0000000000..0b507ab637
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_scribble_highlight.png differ
diff --git a/res/drawable-xxxhdpi/ic_scribble_save.png b/res/drawable-xxxhdpi/ic_scribble_save.png
new file mode 100644
index 0000000000..265a7acc4e
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_scribble_save.png differ
diff --git a/res/drawable-xxxhdpi/ic_scribble_sticker.png b/res/drawable-xxxhdpi/ic_scribble_sticker.png
new file mode 100644
index 0000000000..88d5a53f73
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_scribble_sticker.png differ
diff --git a/res/drawable-xxxhdpi/ic_scribble_text.png b/res/drawable-xxxhdpi/ic_scribble_text.png
new file mode 100644
index 0000000000..001428bfa7
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_scribble_text.png differ
diff --git a/res/drawable-xxxhdpi/ic_scribble_undo.png b/res/drawable-xxxhdpi/ic_scribble_undo.png
new file mode 100644
index 0000000000..b3b526b2ec
Binary files /dev/null and b/res/drawable-xxxhdpi/ic_scribble_undo.png differ
diff --git a/res/drawable-xxxhdpi/ic_text_fields_white_24dp.png b/res/drawable-xxxhdpi/ic_text_fields_white_24dp.png
deleted file mode 100644
index 69ec59a99a..0000000000
Binary files a/res/drawable-xxxhdpi/ic_text_fields_white_24dp.png and /dev/null differ
diff --git a/res/drawable/compose_background_camera.xml b/res/drawable/compose_background_camera.xml
new file mode 100644
index 0000000000..7695987254
--- /dev/null
+++ b/res/drawable/compose_background_camera.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/camera_activity.xml b/res/layout/camera_activity.xml
new file mode 100644
index 0000000000..b50ffc5178
--- /dev/null
+++ b/res/layout/camera_activity.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/camera_controls_landscape.xml b/res/layout/camera_controls_landscape.xml
new file mode 100644
index 0000000000..f0cfd14654
--- /dev/null
+++ b/res/layout/camera_controls_landscape.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/camera_controls_portrait.xml b/res/layout/camera_controls_portrait.xml
new file mode 100644
index 0000000000..33fe8eda32
--- /dev/null
+++ b/res/layout/camera_controls_portrait.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/camera_fragment.xml b/res/layout/camera_fragment.xml
new file mode 100644
index 0000000000..807b425e4c
--- /dev/null
+++ b/res/layout/camera_fragment.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/scribble_activity.xml b/res/layout/scribble_activity.xml
index 644b4936de..137bc98d89 100644
--- a/res/layout/scribble_activity.xml
+++ b/res/layout/scribble_activity.xml
@@ -2,22 +2,10 @@
-
-
-
-
diff --git a/res/layout/scribble_fragment.xml b/res/layout/scribble_fragment.xml
new file mode 100644
index 0000000000..2f42b48932
--- /dev/null
+++ b/res/layout/scribble_fragment.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
diff --git a/res/layout/scribble_fragment_emojidrawer_stub.xml b/res/layout/scribble_fragment_emojidrawer_stub.xml
new file mode 100644
index 0000000000..bf4c603ef9
--- /dev/null
+++ b/res/layout/scribble_fragment_emojidrawer_stub.xml
@@ -0,0 +1,8 @@
+
+
diff --git a/res/layout/scribble_hud.xml b/res/layout/scribble_hud.xml
index b827fbcdf0..1939b27ffe 100644
--- a/res/layout/scribble_hud.xml
+++ b/res/layout/scribble_hud.xml
@@ -1,89 +1,198 @@
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ xmlns:tools="http://schemas.android.com/tools"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical"
+ tools:parentTag="android.widget.LinearLayout"
+ tools:background="@color/core_light_60">
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ android:id="@+id/scribble_compose_container"
+ android:layout_width="match_parent"
+ android:layout_height="wrap_content"
+ android:layout_marginTop="6dp"
+ android:paddingLeft="10dp"
+ android:paddingRight="10dp"
+ android:paddingBottom="6dp"
+ android:visibility="gone"
+ tools:visibility="visible">
-
+ android:layout_weight="1"
+ android:paddingLeft="10dp"
+ android:paddingStart="10dp"
+ android:orientation="horizontal"
+ android:background="@drawable/compose_background_camera">
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values-v19/themes.xml b/res/values-v19/themes.xml
index 99de7fa136..1d73d342bf 100644
--- a/res/values-v19/themes.xml
+++ b/res/values-v19/themes.xml
@@ -3,6 +3,5 @@
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 0bf3d1287f..43806c5289 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -77,6 +77,10 @@
Incoming call
+
+ Camera unavailable.
+ Failed to save image.
+
Remove
Remove profile photo?
diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java
index bf551a1ac3..fe6a12ae7e 100644
--- a/src/org/thoughtcrime/securesms/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/ConversationActivity.java
@@ -52,6 +52,7 @@ import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
+import org.thoughtcrime.securesms.camera.CameraActivity;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.logging.Log;
import android.util.Pair;
@@ -133,6 +134,7 @@ import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.mms.GlideRequests;
+import org.thoughtcrime.securesms.mms.ImageSlide;
import org.thoughtcrime.securesms.mms.LocationSlide;
import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
@@ -233,6 +235,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private static final int PICK_LOCATION = 9;
private static final int PICK_GIF = 10;
private static final int SMS_DEFAULT = 11;
+ private static final int PICK_CAMERA = 12;
+ private static final int EDIT_IMAGE = 13;
private GlideRequests glideRequests;
protected ComposeText composeText;
@@ -495,6 +499,27 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case SMS_DEFAULT:
initializeSecurity(isSecureText, isDefaultSms);
break;
+ case PICK_CAMERA:
+ int imageWidth = data.getIntExtra(CameraActivity.EXTRA_WIDTH, 0);
+ int imageHeight = data.getIntExtra(CameraActivity.EXTRA_HEIGHT, 0);
+ long imageSize = data.getLongExtra(CameraActivity.EXTRA_SIZE, 0);
+ TransportOption transport = data.getParcelableExtra(CameraActivity.EXTRA_TRANSPORT);
+ String message = data.getStringExtra(CameraActivity.EXTRA_MESSAGE);
+ SlideDeck slideDeck = new SlideDeck();
+ long expiresIn = recipient.getExpireMessages() * 1000L;
+ int subscriptionId = sendButton.getSelectedTransport().getSimSubscriptionId().or(-1);
+ boolean initiating = threadId == -1;
+
+ if (transport == null) {
+ throw new IllegalStateException("Received a null transport from the CameraActivity.");
+ }
+
+ sendButton.setTransport(transport);
+
+ slideDeck.addSlide(new ImageSlide(this, data.getData(), imageSize, imageWidth, imageHeight));
+
+ sendMediaMessage(transport.isSms(), message, slideDeck, Collections.emptyList(), expiresIn, subscriptionId, initiating);
+ break;
}
}
@@ -2104,21 +2129,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private class QuickCameraToggleListener implements OnClickListener {
@Override
public void onClick(View v) {
- if (!quickAttachmentDrawer.isShowing()) {
- Permissions.with(ConversationActivity.this)
- .request(Manifest.permission.CAMERA)
- .ifNecessary()
- .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_photo_camera_white_48dp)
- .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
- .onAllGranted(() -> {
- composeText.clearFocus();
- container.show(composeText, quickAttachmentDrawer);
- })
- .onAnyDenied(() -> Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())
- .execute();
- } else {
- container.hideAttachedInput(false);
- }
+ Permissions.with(ConversationActivity.this)
+ .request(Manifest.permission.CAMERA)
+ .ifNecessary()
+ .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_photo_camera_white_48dp)
+ .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
+ .onAllGranted(() -> {
+ composeText.clearFocus();
+ startActivityForResult(CameraActivity.getIntent(ConversationActivity.this, sendButton.getSelectedTransport()), PICK_CAMERA);
+ overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary);
+ })
+ .onAnyDenied(() -> Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())
+ .execute();
}
}
diff --git a/src/org/thoughtcrime/securesms/TransportOption.java b/src/org/thoughtcrime/securesms/TransportOption.java
index 22ba41d62c..999808becb 100644
--- a/src/org/thoughtcrime/securesms/TransportOption.java
+++ b/src/org/thoughtcrime/securesms/TransportOption.java
@@ -1,13 +1,16 @@
package org.thoughtcrime.securesms;
+import android.os.Parcel;
+import android.os.Parcelable;
import android.support.annotation.DrawableRes;
import android.support.annotation.NonNull;
+import android.text.TextUtils;
import org.thoughtcrime.securesms.util.CharacterCalculator;
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
import org.whispersystems.libsignal.util.guava.Optional;
-public class TransportOption {
+public class TransportOption implements Parcelable {
public enum Type {
SMS,
@@ -53,6 +56,16 @@ public class TransportOption {
this.simSubscriptionId = simSubscriptionId;
}
+ TransportOption(Parcel in) {
+ this(Type.valueOf(in.readString()),
+ in.readInt(),
+ in.readInt(),
+ in.readString(),
+ in.readString(),
+ CharacterCalculator.readFromParcel(in),
+ Optional.fromNullable(TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in)),
+ in.readInt() == 1 ? Optional.of(in.readInt()) : Optional.absent());
+ }
public @NonNull Type getType() {
return type;
@@ -96,4 +109,38 @@ public class TransportOption {
return simSubscriptionId;
}
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(type.name());
+ dest.writeInt(drawable);
+ dest.writeInt(backgroundColor);
+ dest.writeString(text);
+ dest.writeString(composeHint);
+ CharacterCalculator.writeToParcel(dest, characterCalculator);
+ TextUtils.writeToParcel(simName.orNull(), dest, flags);
+
+ if (simSubscriptionId.isPresent()) {
+ dest.writeInt(1);
+ dest.writeInt(simSubscriptionId.get());
+ } else {
+ dest.writeInt(0);
+ }
+ }
+
+ public static final Creator CREATOR = new Creator() {
+ @Override
+ public TransportOption createFromParcel(Parcel in) {
+ return new TransportOption(in);
+ }
+
+ @Override
+ public TransportOption[] newArray(int size) {
+ return new TransportOption[size];
+ }
+ };
}
diff --git a/src/org/thoughtcrime/securesms/camera/Camera1Controller.java b/src/org/thoughtcrime/securesms/camera/Camera1Controller.java
new file mode 100644
index 0000000000..54b873375d
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/camera/Camera1Controller.java
@@ -0,0 +1,194 @@
+package org.thoughtcrime.securesms.camera;
+
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.support.annotation.NonNull;
+import android.view.Surface;
+
+import org.thoughtcrime.securesms.logging.Log;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class Camera1Controller {
+
+ private static final String TAG = Camera1Controller.class.getSimpleName();
+
+ private Camera camera;
+ private int cameraId;
+ private OrderEnforcer enforcer;
+ private EventListener eventListener;
+ private SurfaceTexture previewSurface;
+ private int screenRotation;
+
+ public Camera1Controller(int preferredDirection, @NonNull EventListener eventListener) {
+ this.eventListener = eventListener;
+ this.enforcer = new OrderEnforcer<>(Stage.INITIALIZED, Stage.PREVIEW_STARTED);
+ this.cameraId = Camera.getNumberOfCameras() > 1 ? preferredDirection : Camera.CameraInfo.CAMERA_FACING_BACK;
+ }
+
+ public void initialize() {
+ Log.d(TAG, "initialize()");
+
+ if (Camera.getNumberOfCameras() <= 0) {
+ onCameraUnavailable();
+ }
+
+ camera = Camera.open(cameraId);
+
+ Camera.Parameters params = camera.getParameters();
+ Camera.Size maxSize = getMaxSupportedPreviewSize(camera);
+ final List focusModes = params.getSupportedFocusModes();
+
+ params.setPreviewSize(maxSize.width, maxSize.height);
+
+ if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
+ params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
+ } else if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
+ params.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
+ }
+
+ camera.setParameters(params);
+
+ enforcer.markCompleted(Stage.INITIALIZED);
+
+ eventListener.onPropertiesAvailable(getProperties());
+ }
+
+ public void release() {
+ Log.d(TAG, "release() called");
+ enforcer.run(Stage.PREVIEW_STARTED, () -> {
+ Log.d(TAG, "release() executing");
+ previewSurface = null;
+ camera.stopPreview();
+ camera.release();
+ enforcer.reset();
+ });
+ }
+
+ public void linkSurface(@NonNull SurfaceTexture surfaceTexture) {
+ Log.d(TAG, "linkSurface() called");
+ enforcer.run(Stage.INITIALIZED, () -> {
+ try {
+ Log.d(TAG, "linkSurface() executing");
+ previewSurface = surfaceTexture;
+
+ camera.setPreviewTexture(surfaceTexture);
+ camera.startPreview();
+ enforcer.markCompleted(Stage.PREVIEW_STARTED);
+ } catch (IOException e) {
+ Log.w(TAG, "Failed to start preview.", e);
+ eventListener.onCameraUnavailable();
+ }
+ });
+ }
+
+ public int flip() {
+ Log.d(TAG, "flip()");
+ SurfaceTexture surfaceTexture = previewSurface;
+ cameraId = (cameraId == Camera.CameraInfo.CAMERA_FACING_BACK) ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK;
+
+ release();
+ initialize();
+ linkSurface(surfaceTexture);
+ setScreenRotation(screenRotation);
+
+ return cameraId;
+ }
+
+ public void setScreenRotation(int screenRotation) {
+ Log.d(TAG, "setScreenRotation(" + screenRotation + ") called");
+ enforcer.run(Stage.PREVIEW_STARTED, () -> {
+ Log.d(TAG, "setScreenRotation(" + screenRotation + ") executing");
+ this.screenRotation = screenRotation;
+
+ int rotation = getCameraRotationForScreen(screenRotation);
+ camera.setDisplayOrientation(rotation);
+
+ Log.d(TAG, "Set camera rotation to: " + rotation);
+
+ Camera.Parameters params = camera.getParameters();
+ params.setRotation(rotation);
+ camera.setParameters(params);
+ });
+ }
+
+ private void onCameraUnavailable() {
+ enforcer.reset();
+ eventListener.onCameraUnavailable();
+ }
+
+ private Properties getProperties() {
+ Camera.Size previewSize = camera.getParameters().getPreviewSize();
+ return new Properties(Camera.getNumberOfCameras(), previewSize.width, previewSize.height);
+ }
+
+ private Camera.Size getMaxSupportedPreviewSize(Camera camera) {
+ List cameraSizes = camera.getParameters().getSupportedPreviewSizes();
+ Collections.sort(cameraSizes, DESC_SIZE_COMPARATOR);
+ return cameraSizes.get(0);
+ }
+
+ private int getCameraRotationForScreen(int screenRotation) {
+ int degrees = 0;
+
+ switch (screenRotation) {
+ case Surface.ROTATION_0: degrees = 0; break;
+ case Surface.ROTATION_90: degrees = 90; break;
+ case Surface.ROTATION_180: degrees = 180; break;
+ case Surface.ROTATION_270: degrees = 270; break;
+ }
+
+ Camera.CameraInfo info = new Camera.CameraInfo();
+ Camera.getCameraInfo(cameraId, info);
+
+ if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ return (360 - ((info.orientation + degrees) % 360)) % 360;
+ } else {
+ return (info.orientation - degrees + 360) % 360;
+ }
+ }
+
+ private final Comparator DESC_SIZE_COMPARATOR = (o1, o2) -> Integer.compare(o2.width * o2.height, o1.width * o1.height);
+
+ private enum Stage {
+ INITIALIZED, PREVIEW_STARTED
+ }
+
+ class Properties {
+
+ private final int cameraCount;
+ private final int previewWidth;
+ private final int previewHeight;
+
+ public Properties(int cameraCount, int previewWidth, int previewHeight) {
+ this.cameraCount = cameraCount;
+ this.previewWidth = previewWidth;
+ this.previewHeight = previewHeight;
+ }
+
+ int getCameraCount() {
+ return cameraCount;
+ }
+
+ public int getPreviewWidth() {
+ return previewWidth;
+ }
+
+ public int getPreviewHeight() {
+ return previewHeight;
+ }
+
+ @Override
+ public String toString() {
+ return "cameraCount: " + camera + " previewWidth: " + previewWidth + " previewHeight: " + previewHeight;
+ }
+ }
+
+ interface EventListener {
+ void onPropertiesAvailable(@NonNull Properties properties);
+ void onCameraUnavailable();
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/camera/Camera1Fragment.java b/src/org/thoughtcrime/securesms/camera/Camera1Fragment.java
new file mode 100644
index 0000000000..f648c81761
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/camera/Camera1Fragment.java
@@ -0,0 +1,321 @@
+package org.thoughtcrime.securesms.camera;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.res.Configuration;
+import android.graphics.Bitmap;
+import android.graphics.Matrix;
+import android.graphics.PointF;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.media.MediaActionSound;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.GestureDetector;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.TextureView;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.widget.Button;
+import android.widget.ImageButton;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.util.Stopwatch;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.thoughtcrime.securesms.util.concurrent.LifecycleBoundTask;
+
+import java.io.ByteArrayOutputStream;
+
+public class Camera1Fragment extends Fragment implements TextureView.SurfaceTextureListener,
+ Camera1Controller.EventListener
+{
+
+ private static final String TAG = Camera1Fragment.class.getSimpleName();
+
+ private TextureView cameraPreview;
+ private ViewGroup controlsContainer;
+ private ImageButton flipButton;
+ private Button captureButton;
+ private Camera1Controller camera;
+ private Controller controller;
+ private OrderEnforcer orderEnforcer;
+ private ShutterSound shutterSound;
+ private Camera1Controller.Properties properties;
+
+ public static Camera1Fragment newInstance() {
+ return new Camera1Fragment();
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (!(getActivity() instanceof Controller)) {
+ throw new IllegalStateException("Parent activity must implement the Controller interface.");
+ }
+
+ controller = (Controller) getActivity();
+ camera = new Camera1Controller(TextSecurePreferences.getDirectCaptureCameraId(getContext()), this);
+ orderEnforcer = new OrderEnforcer<>(Stage.SURFACE_AVAILABLE, Stage.CAMERA_PROPERTIES_AVAILABLE);
+ shutterSound = Build.VERSION.SDK_INT >= 16 ? new MediaActionShutterSound() : new NoopShutterSound();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.camera_fragment, container, false);
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ cameraPreview = view.findViewById(R.id.camera_preview);
+ controlsContainer = view.findViewById(R.id.camera_controls_container);
+
+ onOrientationChanged(getResources().getConfiguration().orientation);
+
+ cameraPreview.setSurfaceTextureListener(this);
+
+ GestureDetector gestureDetector = new GestureDetector(flipGestureListener);
+ cameraPreview.setOnTouchListener((v, event) -> gestureDetector.onTouchEvent(event));
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ camera.initialize();
+ orderEnforcer.run(Stage.SURFACE_AVAILABLE, () -> {
+ camera.linkSurface(cameraPreview.getSurfaceTexture());
+ camera.setScreenRotation(controller.getDisplayRotation());
+ });
+ orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+ camera.release();
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ onOrientationChanged(newConfig.orientation);
+ }
+
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
+ orderEnforcer.markCompleted(Stage.SURFACE_AVAILABLE);
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
+ orderEnforcer.run(Stage.SURFACE_AVAILABLE, () -> camera.setScreenRotation(controller.getDisplayRotation()));
+ orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
+ return false;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surface) {
+ }
+
+ @Override
+ public void onPropertiesAvailable(@NonNull Camera1Controller.Properties properties) {
+ Log.d(TAG, "Got camera properties: " + properties);
+ this.properties = properties;
+ orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, this::updatePreviewScale);
+ orderEnforcer.markCompleted(Stage.CAMERA_PROPERTIES_AVAILABLE);
+ }
+
+ @Override
+ public void onCameraUnavailable() {
+ controller.onCameraError();
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ private void initControls() {
+ flipButton = getView().findViewById(R.id.camera_flip_button);
+ captureButton = getView().findViewById(R.id.camera_capture_button);
+
+ captureButton.setOnTouchListener((v, event) -> {
+ switch (event.getAction()) {
+ case MotionEvent.ACTION_DOWN:
+ Animation shrinkAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.camera_capture_button_shrink);
+ shrinkAnimation.setFillAfter(true);
+ shrinkAnimation.setFillEnabled(true);
+ captureButton.startAnimation(shrinkAnimation);
+ onCaptureClicked();
+ break;
+ case MotionEvent.ACTION_UP:
+ case MotionEvent.ACTION_CANCEL:
+ case MotionEvent.ACTION_OUTSIDE:
+ Animation growAnimation = AnimationUtils.loadAnimation(getContext(), R.anim.camera_capture_button_grow);
+ growAnimation.setFillAfter(true);
+ growAnimation.setFillEnabled(true);
+ captureButton.startAnimation(growAnimation);
+ captureButton.setEnabled(false);
+ break;
+ }
+ return true;
+ });
+
+ orderEnforcer.run(Stage.CAMERA_PROPERTIES_AVAILABLE, () -> {
+ if (properties.getCameraCount() > 1) {
+ flipButton.setVisibility(properties.getCameraCount() > 1 ? View.VISIBLE : View.GONE);
+ flipButton.setImageResource(TextSecurePreferences.getDirectCaptureCameraId(getContext()) == Camera.CameraInfo.CAMERA_FACING_BACK ? R.drawable.ic_camera_front
+ : R.drawable.ic_camera_rear);
+ flipButton.setOnClickListener(v -> {
+ int newCameraId = camera.flip();
+ flipButton.setImageResource(newCameraId == Camera.CameraInfo.CAMERA_FACING_BACK ? R.drawable.ic_camera_front
+ : R.drawable.ic_camera_rear);
+
+ TextSecurePreferences.setDirectCaptureCameraId(getContext(), newCameraId);
+ });
+ } else {
+ flipButton.setVisibility(View.GONE);
+ }
+ });
+ }
+
+ private void onCaptureClicked() {
+ shutterSound.play();
+ orderEnforcer.reset();
+
+ LifecycleBoundTask.run(getLifecycle(), () -> {
+ Stopwatch fastCaptureTimer = new Stopwatch("Fast Capture");
+
+ Bitmap preview = cameraPreview.getBitmap();
+ fastCaptureTimer.split("captured");
+
+ Bitmap full = preview;
+ if (Build.VERSION.SDK_INT < 28) {
+ PointF scale = getScaleTransform(cameraPreview.getWidth(), cameraPreview.getHeight(), properties.getPreviewWidth(), properties.getPreviewHeight());
+ Matrix matrix = new Matrix();
+
+ matrix.setScale(scale.x, scale.y);
+
+ int adjWidth = (int) (cameraPreview.getWidth() / scale.x);
+ int adjHeight = (int) (cameraPreview.getHeight() / scale.y);
+
+ full = Bitmap.createBitmap(preview, 0, 0, adjWidth, adjHeight, matrix, true);
+ }
+
+ fastCaptureTimer.split("transformed");
+
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ full.compress(Bitmap.CompressFormat.JPEG, 80, stream);
+ fastCaptureTimer.split("compressed");
+
+ byte[] data = stream.toByteArray();
+ fastCaptureTimer.split("bytes");
+ fastCaptureTimer.stop(TAG);
+
+ return data;
+ }, data -> {
+ if (data != null) {
+ controller.onImageCaptured(data);
+ } else {
+ controller.onCameraError();
+ }
+ });
+ }
+
+ private PointF getScaleTransform(float viewWidth, float viewHeight, int cameraWidth, int cameraHeight) {
+ float camWidth = isPortrait() ? Math.min(cameraWidth, cameraHeight) : Math.max(cameraWidth, cameraHeight);
+ float camHeight = isPortrait() ? Math.max(cameraWidth, cameraHeight) : Math.min(cameraWidth, cameraHeight);
+
+ float scaleX = 1;
+ float scaleY = 1;
+
+ if ((camWidth / viewWidth) > (camHeight / viewHeight)) {
+ float targetWidth = viewHeight * (camWidth / camHeight);
+ scaleX = targetWidth / viewWidth;
+ } else {
+ float targetHeight = viewWidth * (camHeight / camWidth);
+ scaleY = targetHeight / viewHeight;
+ }
+
+ return new PointF(scaleX, scaleY);
+ }
+
+ private void onOrientationChanged(int orientation) {
+ int layout = orientation == Configuration.ORIENTATION_PORTRAIT ? R.layout.camera_controls_portrait
+ : R.layout.camera_controls_landscape;
+
+ controlsContainer.removeAllViews();
+ controlsContainer.addView(LayoutInflater.from(getContext()).inflate(layout, controlsContainer, false));
+ initControls();
+ }
+
+ private void updatePreviewScale() {
+ PointF scale = getScaleTransform(cameraPreview.getWidth(), cameraPreview.getHeight(), properties.getPreviewWidth(), properties.getPreviewHeight());
+ Matrix matrix = new Matrix();
+
+ matrix.setScale(scale.x, scale.y);
+ cameraPreview.setTransform(matrix);
+ }
+
+ private boolean isPortrait() {
+ return getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT;
+ }
+
+ private final GestureDetector.OnGestureListener flipGestureListener = new GestureDetector.SimpleOnGestureListener() {
+ @Override
+ public boolean onDown(MotionEvent e) {
+ return true;
+ }
+
+ @Override
+ public boolean onDoubleTap(MotionEvent e) {
+ flipButton.performClick();
+ return true;
+ }
+ };
+
+ public interface Controller {
+ void onCameraError();
+ void onImageCaptured(@NonNull byte[] data);
+ int getDisplayRotation();
+ }
+
+ private enum Stage {
+ SURFACE_AVAILABLE, CAMERA_PROPERTIES_AVAILABLE
+ }
+
+ private interface ShutterSound {
+ void play();
+ }
+
+ @TargetApi(16)
+ private static class MediaActionShutterSound implements ShutterSound {
+
+ private final MediaActionSound mediaActionSound;
+
+ public MediaActionShutterSound() {
+ mediaActionSound = new MediaActionSound();
+ mediaActionSound.load(MediaActionSound.SHUTTER_CLICK);
+ }
+
+ @Override
+ public void play() {
+ mediaActionSound.play(MediaActionSound.SHUTTER_CLICK);
+ }
+ }
+
+ private static class NoopShutterSound implements ShutterSound {
+ @Override
+ public void play() { }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/camera/CameraActivity.java b/src/org/thoughtcrime/securesms/camera/CameraActivity.java
new file mode 100644
index 0000000000..f50191f33c
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/camera/CameraActivity.java
@@ -0,0 +1,166 @@
+package org.thoughtcrime.securesms.camera;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.widget.ImageView;
+import android.widget.Toast;
+
+import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.TransportOption;
+import org.thoughtcrime.securesms.components.GlideDrawableListeningTarget;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader;
+import org.thoughtcrime.securesms.mms.GlideApp;
+import org.thoughtcrime.securesms.providers.MemoryBlobProvider;
+import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
+import org.thoughtcrime.securesms.scribbles.ScribbleFragment;
+import org.thoughtcrime.securesms.util.DynamicLanguage;
+import org.thoughtcrime.securesms.util.MediaUtil;
+import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
+import org.thoughtcrime.securesms.util.concurrent.LifecycleBoundTask;
+import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+public class CameraActivity extends PassphraseRequiredActionBarActivity implements Camera1Fragment.Controller,
+ ScribbleFragment.Controller
+{
+
+ private static final String TAG = CameraActivity.class.getSimpleName();
+
+ private static final String TAG_CAMERA = "camera";
+ private static final String TAG_EDITOR = "editor";
+
+ private static final String KEY_TRANSPORT = "transport";
+
+ public static final String EXTRA_MESSAGE = "message";
+ public static final String EXTRA_TRANSPORT = "transport";
+ public static final String EXTRA_WIDTH = "width";
+ public static final String EXTRA_HEIGHT = "height";
+ public static final String EXTRA_SIZE = "size";
+
+ private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
+
+ private ImageView snapshot;
+ private TransportOption transport;
+ private Uri captureUri;
+ private boolean imageSent;
+
+ public static Intent getIntent(@NonNull Context context, @NonNull TransportOption transport) {
+ Intent intent = new Intent(context, CameraActivity.class);
+ intent.putExtra(KEY_TRANSPORT, transport);
+ return intent;
+ }
+
+ @Override
+ protected void onPreCreate() {
+ dynamicLanguage.onCreate(this);
+ }
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState, boolean ready) {
+ setContentView(R.layout.camera_activity);
+
+ snapshot = findViewById(R.id.camera_snapshot);
+ transport = getIntent().getParcelableExtra(KEY_TRANSPORT);
+
+ if (savedInstanceState == null) {
+ Camera1Fragment fragment = Camera1Fragment.newInstance();
+ getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, fragment, TAG_CAMERA).commit();
+ }
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ dynamicLanguage.onResume(this);
+ }
+
+ @Override
+ public void onBackPressed() {
+ ScribbleFragment editorFragment = (ScribbleFragment) getSupportFragmentManager().findFragmentByTag(TAG_EDITOR);
+ if (editorFragment != null && editorFragment.isEmojiKeyboardVisible()) {
+ editorFragment.dismissEmojiKeyboard();
+ } else {
+ if (editorFragment != null && captureUri != null) {
+ Log.i(TAG, "Cleaning up unused capture: " + captureUri);
+ MemoryBlobProvider.getInstance().delete(captureUri);
+ captureUri = null;
+ }
+ super.onBackPressed();
+ overridePendingTransition(R.anim.stationary, R.anim.camera_slide_to_bottom);
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+
+ if (captureUri != null) {
+ Log.i(TAG, "Cleaning up capture in onDestroy: " + captureUri);
+ MemoryBlobProvider.getInstance().delete(captureUri);
+ }
+ }
+
+ @Override
+ public void onCameraError() {
+ Toast.makeText(this, R.string.CameraActivity_camera_unavailable, Toast.LENGTH_SHORT).show();
+ setResult(RESULT_CANCELED, new Intent());
+ finish();
+ }
+
+ @Override
+ public void onImageCaptured(@NonNull byte[] data) {
+ Log.i(TAG, "Fast image captured.");
+
+ captureUri = MemoryBlobProvider.getInstance().createUri(data);
+ Log.i(TAG, "Fast image stored: " + captureUri.toString());
+
+ SettableFuture result = new SettableFuture<>();
+ GlideApp.with(this).load(new DecryptableStreamUriLoader.DecryptableUri(captureUri)).into(new GlideDrawableListeningTarget(snapshot, result));
+ result.addListener(new AssertedSuccessListener() {
+ @Override
+ public void onSuccess(Boolean result) {
+ ScribbleFragment fragment = ScribbleFragment.newInstance(captureUri, dynamicLanguage.getCurrentLocale(), Optional.of(transport));
+ getSupportFragmentManager().beginTransaction()
+ .setCustomAnimations(R.anim.fade_in, R.anim.fade_out, R.anim.fade_in, R.anim.fade_out)
+ .replace(R.id.fragment_container, fragment, TAG_EDITOR)
+ .addToBackStack(null)
+ .commit();
+ }
+ });
+ }
+
+ @Override
+ public int getDisplayRotation() {
+ return getWindowManager().getDefaultDisplay().getRotation();
+ }
+
+ @Override
+ public void onImageEditComplete(@NonNull Uri uri, int width, int height, long size, @NonNull Optional message, @NonNull Optional transport) {
+ imageSent = true;
+
+ Intent intent = new Intent();
+ intent.setData(uri);
+ intent.putExtra(EXTRA_WIDTH, width);
+ intent.putExtra(EXTRA_HEIGHT, height);
+ intent.putExtra(EXTRA_SIZE, size);
+ intent.putExtra(EXTRA_MESSAGE, message.or(""));
+ intent.putExtra(EXTRA_TRANSPORT, transport.orNull());
+ setResult(RESULT_OK, intent);
+ finish();
+
+ overridePendingTransition(R.anim.stationary, R.anim.camera_slide_to_bottom);
+ }
+
+ @Override
+ public void onImageEditFailure() {
+ Log.w(TAG, "Failed to save edited image.");
+ Toast.makeText(this, R.string.CameraActivity_image_save_failure, Toast.LENGTH_SHORT).show();
+ finish();
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/components/InputAwareLayout.java b/src/org/thoughtcrime/securesms/components/InputAwareLayout.java
index 7fe8a4ca85..d24c78f9cf 100644
--- a/src/org/thoughtcrime/securesms/components/InputAwareLayout.java
+++ b/src/org/thoughtcrime/securesms/components/InputAwareLayout.java
@@ -77,7 +77,7 @@ public class InputAwareLayout extends KeyboardAwareLinearLayout implements OnKey
});
}
- private void hideSoftkey(final EditText inputTarget, @Nullable Runnable runAfterClose) {
+ protected void hideSoftkey(final EditText inputTarget, @Nullable Runnable runAfterClose) {
if (runAfterClose != null) postOnKeyboardClose(runAfterClose);
ServiceUtil.getInputMethodManager(inputTarget.getContext())
diff --git a/src/org/thoughtcrime/securesms/components/KeyboardAwareLinearLayout.java b/src/org/thoughtcrime/securesms/components/KeyboardAwareLinearLayout.java
index b6c1af9592..e23553edf3 100644
--- a/src/org/thoughtcrime/securesms/components/KeyboardAwareLinearLayout.java
+++ b/src/org/thoughtcrime/securesms/components/KeyboardAwareLinearLayout.java
@@ -56,6 +56,7 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
private boolean keyboardOpen = false;
private int rotation = -1;
+ private boolean isFullscreen = false;
public KeyboardAwareLinearLayout(Context context) {
this(context, null);
@@ -98,10 +99,11 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
}
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);
+ final int availableHeight = this.getRootView().getHeight() - viewInset - (!isFullscreen ? statusBarHeight : 0);
+ final int keyboardHeight = availableHeight - (rect.bottom - rect.top);
if (keyboardHeight > minKeyboardSize) {
if (getKeyboardHeight() != keyboardHeight) setKeyboardPortraitHeight(keyboardHeight);
@@ -217,6 +219,10 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
shownListeners.remove(listener);
}
+ public void setFullscreen(boolean isFullscreen) {
+ this.isFullscreen = isFullscreen;
+ }
+
private void notifyHiddenListeners() {
final Set listeners = new HashSet<>(hiddenListeners);
for (OnKeyboardHiddenListener listener : listeners) {
diff --git a/src/org/thoughtcrime/securesms/components/SendButton.java b/src/org/thoughtcrime/securesms/components/SendButton.java
index 9ef421d2b1..0da9c0ebba 100644
--- a/src/org/thoughtcrime/securesms/components/SendButton.java
+++ b/src/org/thoughtcrime/securesms/components/SendButton.java
@@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
+import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.View;
import android.widget.ImageButton;
@@ -83,6 +84,10 @@ public class SendButton extends ImageButton
transportOptions.setDefaultTransport(type);
}
+ public void setTransport(@NonNull TransportOption option) {
+ transportOptions.setSelectedTransport(option);
+ }
+
public void setDefaultSubscriptionId(Optional subscriptionId) {
transportOptions.setDefaultSubscriptionId(subscriptionId);
}
diff --git a/src/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java b/src/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java
index fdaee20644..9eb532eee2 100644
--- a/src/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java
+++ b/src/org/thoughtcrime/securesms/scribbles/ScribbleActivity.java
@@ -1,272 +1,69 @@
package org.thoughtcrime.securesms.scribbles;
-import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Intent;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.PointF;
import android.net.Uri;
-import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
-import android.support.annotation.Nullable;
-import org.thoughtcrime.securesms.logging.Log;
import android.view.View;
import android.widget.Toast;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.mms.GlideApp;
-import org.thoughtcrime.securesms.mms.GlideRequests;
-import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
-import org.thoughtcrime.securesms.scribbles.viewmodel.Font;
-import org.thoughtcrime.securesms.scribbles.viewmodel.Layer;
-import org.thoughtcrime.securesms.scribbles.viewmodel.TextLayer;
-import org.thoughtcrime.securesms.scribbles.widget.MotionView;
-import org.thoughtcrime.securesms.scribbles.widget.ScribbleView;
-import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
-import org.thoughtcrime.securesms.scribbles.widget.entity.ImageEntity;
-import org.thoughtcrime.securesms.scribbles.widget.entity.MotionEntity;
-import org.thoughtcrime.securesms.scribbles.widget.entity.TextEntity;
-import org.thoughtcrime.securesms.util.MediaUtil;
-import org.thoughtcrime.securesms.util.Util;
-import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.util.concurrent.ExecutionException;
+import org.thoughtcrime.securesms.TransportOption;
+import org.thoughtcrime.securesms.util.DynamicLanguage;
+import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
+import org.thoughtcrime.securesms.util.DynamicTheme;
+import org.whispersystems.libsignal.util.guava.Optional;
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
-public class ScribbleActivity extends PassphraseRequiredActionBarActivity implements ScribbleHud.EventListener, VerticalSlideColorPicker.OnColorChangeListener {
+public class ScribbleActivity extends PassphraseRequiredActionBarActivity implements ScribbleFragment.Controller {
private static final String TAG = ScribbleActivity.class.getName();
- public static final int SELECT_STICKER_REQUEST_CODE = 123;
- public static final int SCRIBBLE_REQUEST_CODE = 31424;
+ public static final int SCRIBBLE_REQUEST_CODE = 31424;
- private ScribbleHud scribbleHud;
- private ScribbleView scribbleView;
- private GlideRequests glideRequests;
+ private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
+
+ @Override
+ protected void onPreCreate() {
+ dynamicLanguage.onCreate(this);
+ }
@Override
protected void onCreate(Bundle savedInstanceState, boolean ready) {
setContentView(R.layout.scribble_activity);
- this.glideRequests = GlideApp.with(this);
- this.scribbleHud = findViewById(R.id.scribble_hud);
- this.scribbleView = findViewById(R.id.scribble_view);
-
- scribbleHud.setEventListener(this);
-
- scribbleView.setMotionViewCallback(motionViewCallback);
- scribbleView.setDrawingChangedListener(() -> scribbleHud.setColorPalette(scribbleView.getUniqueColors()));
- scribbleView.setDrawingMode(false);
- scribbleView.setImage(glideRequests, getIntent().getData());
+ if (savedInstanceState == null) {
+ ScribbleFragment fragment = ScribbleFragment.newInstance(getIntent().getData(), dynamicLanguage.getCurrentLocale(), Optional.absent());
+ getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, fragment).commit();
+ }
if (Build.VERSION.SDK_INT >= 19) {
- getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN |
- View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
- View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION);
- }
- }
-
- private void addSticker(final Bitmap pica) {
- Util.runOnMain(() -> {
- Layer layer = new Layer();
- ImageEntity entity = new ImageEntity(layer, pica, scribbleView.getWidth(), scribbleView.getHeight());
-
- scribbleView.addEntityAndPosition(entity);
- });
- }
-
- private void changeTextEntityColor(int selectedColor) {
- TextEntity textEntity = currentTextEntity();
-
- if (textEntity == null) {
- return;
- }
-
- textEntity.getLayer().getFont().setColor(selectedColor);
- textEntity.updateEntity();
- scribbleView.invalidate();
- scribbleHud.setColorPalette(scribbleView.getUniqueColors());
- }
-
- private void startTextEntityEditing() {
- TextEntity textEntity = currentTextEntity();
- if (textEntity != null) {
- scribbleView.startEditing(textEntity);
- }
- }
-
- @Nullable
- private TextEntity currentTextEntity() {
- if (scribbleView != null && scribbleView.getSelectedEntity() instanceof TextEntity) {
- return ((TextEntity) scribbleView.getSelectedEntity());
- } else {
- return null;
- }
- }
-
- protected void addTextSticker() {
- TextLayer textLayer = createTextLayer();
- TextEntity textEntity = new TextEntity(textLayer, scribbleView.getWidth(), scribbleView.getHeight());
- scribbleView.addEntityAndPosition(textEntity);
-
- PointF center = textEntity.absoluteCenter();
- center.y = center.y * 0.5F;
- textEntity.moveCenterTo(center);
-
- scribbleView.invalidate();
-
- startTextEntityEditing();
- changeTextEntityColor(scribbleHud.getActiveColor());
- }
-
- private TextLayer createTextLayer() {
- TextLayer textLayer = new TextLayer();
- Font font = new Font();
-
- font.setColor(scribbleHud.getActiveColor());
- font.setSize(TextLayer.Limits.INITIAL_FONT_SIZE);
-
- textLayer.setFont(font);
-
- return textLayer;
- }
-
- @SuppressLint("StaticFieldLeak")
- @Override
- protected void onActivityResult(int requestCode, int resultCode, Intent data) {
- super.onActivityResult(requestCode, resultCode, data);
- if (resultCode == RESULT_OK) {
- if (requestCode == SELECT_STICKER_REQUEST_CODE) {
- if (data != null) {
- final String stickerFile = data.getStringExtra(StickerSelectActivity.EXTRA_STICKER_FILE);
-
- new AsyncTask() {
- @Override
- protected @Nullable
- Bitmap doInBackground(Void... params) {
- try {
- return BitmapFactory.decodeStream(getAssets().open(stickerFile));
- } catch (IOException e) {
- Log.w(TAG, e);
- return null;
- }
- }
-
- @Override
- protected void onPostExecute(@Nullable Bitmap bitmap) {
- addSticker(bitmap);
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
- }
+ getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN |
+ View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
}
@Override
- public void onModeStarted(@NonNull ScribbleHud.Mode mode) {
- switch (mode) {
- case DRAW:
- scribbleView.setDrawingMode(true);
- scribbleView.setDrawingBrushWidth(ScribbleView.DEFAULT_BRUSH_WIDTH);
- break;
-
- case HIGHLIGHT:
- scribbleView.setDrawingMode(true);
- scribbleView.setDrawingBrushWidth(ScribbleView.DEFAULT_BRUSH_WIDTH * 3);
- break;
-
- case TEXT:
- scribbleView.setDrawingMode(false);
- addTextSticker();
- break;
-
- case STICKER:
- scribbleView.setDrawingMode(false);
- Intent intent = new Intent(this, StickerSelectActivity.class);
- startActivityForResult(intent, SELECT_STICKER_REQUEST_CODE);
- break;
-
- case NONE:
- scribbleView.clearSelection();
- scribbleView.setDrawingMode(false);
- break;
- }
+ protected void onResume() {
+ super.onResume();
+ dynamicLanguage.onResume(this);
}
@Override
- public void onColorChange(int color) {
- scribbleView.setDrawingBrushColor(color);
- changeTextEntityColor(color);
+ public void onImageEditComplete(@NonNull Uri uri, int width, int height, long size, @NonNull Optional message, @NonNull Optional transport) {
+ Intent intent = new Intent();
+ intent.setData(uri);
+ setResult(RESULT_OK, intent);
+
+ finish();
}
@Override
- public void onUndo() {
- scribbleView.undoDrawing();
- scribbleHud.setColorPalette(scribbleView.getUniqueColors());
+ public void onImageEditFailure() {
+ Toast.makeText(ScribbleActivity.this, R.string.ScribbleActivity_save_failure, Toast.LENGTH_SHORT).show();
+ finish();
}
-
- @Override
- public void onDelete() {
- scribbleView.deleteSelected();
- scribbleHud.setColorPalette(scribbleView.getUniqueColors());
- }
-
- @Override
- public void onSave() {
- ListenableFuture future = scribbleView.getRenderedImage(glideRequests);
-
- future.addListener(new ListenableFuture.Listener() {
- @Override
- public void onSuccess(Bitmap result) {
- PersistentBlobProvider provider = PersistentBlobProvider.getInstance(ScribbleActivity.this);
- ByteArrayOutputStream baos = new ByteArrayOutputStream();
- result.compress(Bitmap.CompressFormat.JPEG, 80, baos);
-
- byte[] data = baos.toByteArray();
- baos = null;
- result = null;
-
- Uri uri = provider.create(ScribbleActivity.this, data, MediaUtil.IMAGE_JPEG, null);
- Intent intent = new Intent();
- intent.setData(uri);
- setResult(RESULT_OK, intent);
-
- finish();
- }
-
- @Override
- public void onFailure(ExecutionException e) {
- Log.w(TAG, e);
- Toast.makeText(ScribbleActivity.this, R.string.ScribbleActivity_save_failure, Toast.LENGTH_SHORT).show();
- finish();
- }
- });
- }
-
- private final MotionView.MotionViewCallback motionViewCallback = new MotionView.MotionViewCallback() {
- @Override
- public void onEntitySelected(@Nullable MotionEntity entity) {
- if (entity == null) {
- scribbleHud.enterMode(ScribbleHud.Mode.NONE);
- } else if (entity instanceof TextEntity) {
- int textColor = ((TextEntity) entity).getLayer().getFont().getColor();
-
- scribbleHud.enterMode(ScribbleHud.Mode.TEXT);
- scribbleHud.setActiveColor(textColor);
- } else {
- scribbleHud.enterMode(ScribbleHud.Mode.STICKER);
- }
- }
-
- @Override
- public void onEntityDoubleTap(@NonNull MotionEntity entity) {
- startTextEntityEditing();
- }
- };
}
diff --git a/src/org/thoughtcrime/securesms/scribbles/ScribbleFragment.java b/src/org/thoughtcrime/securesms/scribbles/ScribbleFragment.java
new file mode 100644
index 0000000000..cbd5666dcc
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/scribbles/ScribbleFragment.java
@@ -0,0 +1,306 @@
+package org.thoughtcrime.securesms.scribbles;
+
+import android.annotation.SuppressLint;
+import android.annotation.TargetApi;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.PointF;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.annotation.Nullable;
+import android.support.v4.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.TransportOption;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.mms.GlideApp;
+import org.thoughtcrime.securesms.mms.GlideRequests;
+import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
+import org.thoughtcrime.securesms.scribbles.viewmodel.Font;
+import org.thoughtcrime.securesms.scribbles.viewmodel.Layer;
+import org.thoughtcrime.securesms.scribbles.viewmodel.TextLayer;
+import org.thoughtcrime.securesms.scribbles.widget.MotionView;
+import org.thoughtcrime.securesms.scribbles.widget.ScribbleView;
+import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
+import org.thoughtcrime.securesms.scribbles.widget.entity.ImageEntity;
+import org.thoughtcrime.securesms.scribbles.widget.entity.MotionEntity;
+import org.thoughtcrime.securesms.scribbles.widget.entity.TextEntity;
+import org.thoughtcrime.securesms.util.MediaUtil;
+import org.thoughtcrime.securesms.util.Util;
+import org.thoughtcrime.securesms.util.concurrent.LifecycleBoundTask;
+import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+
+import static android.app.Activity.RESULT_OK;
+
+@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
+public class ScribbleFragment extends Fragment implements ScribbleHud.EventListener, VerticalSlideColorPicker.OnColorChangeListener {
+
+ private static final String TAG = ScribbleFragment.class.getName();
+
+ private static final String KEY_IMAGE_URI = "image_uri";
+ private static final String KEY_LOCALE = "locale";
+ private static final String KEY_TRANSPORT = "compose_mode";
+
+ public static final int SELECT_STICKER_REQUEST_CODE = 123;
+
+ private Controller controller;
+ private ScribbleHud scribbleHud;
+ private ScribbleView scribbleView;
+ private GlideRequests glideRequests;
+
+ public static ScribbleFragment newInstance(@NonNull Uri imageUri, @NonNull Locale locale, Optional transport) {
+ Bundle args = new Bundle();
+ args.putParcelable(KEY_IMAGE_URI, imageUri);
+ args.putSerializable(KEY_LOCALE, locale);
+ args.putParcelable(KEY_TRANSPORT, transport.orNull());
+
+ ScribbleFragment fragment = new ScribbleFragment();
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (!(getActivity() instanceof Controller)) {
+ throw new IllegalStateException("Parent activity must implement Controller interface.");
+ }
+ controller = (Controller) getActivity();
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.scribble_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ this.glideRequests = GlideApp.with(this);
+ this.scribbleHud = view.findViewById(R.id.scribble_hud);
+ this.scribbleView = view.findViewById(R.id.scribble_view);
+
+ scribbleHud.setEventListener(this);
+ scribbleHud.setTransport(Optional.fromNullable(getArguments().getParcelable(KEY_TRANSPORT)));
+ scribbleHud.setFullscreen((getActivity().getWindow().getAttributes().flags & WindowManager.LayoutParams.FLAG_FULLSCREEN) > 0);
+
+ scribbleView.setMotionViewCallback(motionViewCallback);
+ scribbleView.setDrawingChangedListener(() -> scribbleHud.setColorPalette(scribbleView.getUniqueColors()));
+ scribbleView.setDrawingMode(false);
+ scribbleView.setImage(glideRequests, getArguments().getParcelable(KEY_IMAGE_URI));
+ }
+
+ public boolean isEmojiKeyboardVisible() {
+ return scribbleHud.isInputOpen();
+ }
+
+ public void dismissEmojiKeyboard() {
+ scribbleHud.dismissEmojiKeyboard();
+ }
+
+ private void addSticker(final Bitmap pica) {
+ Util.runOnMain(() -> {
+ Layer layer = new Layer();
+ ImageEntity entity = new ImageEntity(layer, pica, scribbleView.getWidth(), scribbleView.getHeight());
+
+ scribbleView.addEntityAndPosition(entity);
+ });
+ }
+
+ private void changeTextEntityColor(int selectedColor) {
+ TextEntity textEntity = currentTextEntity();
+
+ if (textEntity == null) {
+ return;
+ }
+
+ textEntity.getLayer().getFont().setColor(selectedColor);
+ textEntity.updateEntity();
+ scribbleView.invalidate();
+ scribbleHud.setColorPalette(scribbleView.getUniqueColors());
+ }
+
+ private void startTextEntityEditing() {
+ TextEntity textEntity = currentTextEntity();
+ if (textEntity != null) {
+ scribbleView.startEditing(textEntity);
+ }
+ }
+
+ @Nullable
+ private TextEntity currentTextEntity() {
+ if (scribbleView != null && scribbleView.getSelectedEntity() instanceof TextEntity) {
+ return ((TextEntity) scribbleView.getSelectedEntity());
+ } else {
+ return null;
+ }
+ }
+
+ protected void addTextSticker() {
+ TextLayer textLayer = createTextLayer();
+ TextEntity textEntity = new TextEntity(textLayer, scribbleView.getWidth(), scribbleView.getHeight());
+ scribbleView.addEntityAndPosition(textEntity);
+
+ PointF center = textEntity.absoluteCenter();
+ center.y = center.y * 0.5F;
+ textEntity.moveCenterTo(center);
+
+ scribbleView.invalidate();
+
+ startTextEntityEditing();
+ changeTextEntityColor(scribbleHud.getActiveColor());
+ }
+
+ private TextLayer createTextLayer() {
+ TextLayer textLayer = new TextLayer();
+ Font font = new Font();
+
+ font.setColor(scribbleHud.getActiveColor());
+ font.setSize(TextLayer.Limits.INITIAL_FONT_SIZE);
+
+ textLayer.setFont(font);
+
+ return textLayer;
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode == RESULT_OK && requestCode == SELECT_STICKER_REQUEST_CODE && data != null) {
+ final String stickerFile = data.getStringExtra(StickerSelectActivity.EXTRA_STICKER_FILE);
+
+ LifecycleBoundTask.run(getLifecycle(), () -> {
+ try {
+ return BitmapFactory.decodeStream(getContext().getAssets().open(stickerFile));
+ } catch (IOException e) {
+ Log.w(TAG, e);
+ return null;
+ }
+ }, bitmap -> {
+ if (bitmap != null) {
+ addSticker(bitmap);
+ }
+ });
+ }
+ }
+
+ @Override
+ public void onModeStarted(@NonNull ScribbleHud.Mode mode) {
+ switch (mode) {
+ case DRAW:
+ scribbleView.setDrawingMode(true);
+ scribbleView.setDrawingBrushWidth(ScribbleView.DEFAULT_BRUSH_WIDTH);
+ break;
+
+ case HIGHLIGHT:
+ scribbleView.setDrawingMode(true);
+ scribbleView.setDrawingBrushWidth(ScribbleView.DEFAULT_BRUSH_WIDTH * 3);
+ break;
+
+ case TEXT:
+ scribbleView.setDrawingMode(false);
+ addTextSticker();
+ break;
+
+ case STICKER:
+ scribbleView.setDrawingMode(false);
+ Intent intent = new Intent(getContext(), StickerSelectActivity.class);
+ startActivityForResult(intent, SELECT_STICKER_REQUEST_CODE);
+ break;
+
+ case NONE:
+ scribbleView.clearSelection();
+ scribbleView.setDrawingMode(false);
+ break;
+ }
+ }
+
+ @Override
+ public void onColorChange(int color) {
+ scribbleView.setDrawingBrushColor(color);
+ changeTextEntityColor(color);
+ }
+
+ @Override
+ public void onUndo() {
+ scribbleView.undoDrawing();
+ scribbleHud.setColorPalette(scribbleView.getUniqueColors());
+ }
+
+ @Override
+ public void onDelete() {
+ scribbleView.deleteSelected();
+ scribbleHud.setColorPalette(scribbleView.getUniqueColors());
+ }
+
+ @Override
+ public void onEditComplete(@NonNull Optional message, Optional transport) {
+ ListenableFuture future = scribbleView.getRenderedImage(glideRequests);
+
+ future.addListener(new ListenableFuture.Listener() {
+ @Override
+ public void onSuccess(Bitmap result) {
+ PersistentBlobProvider provider = PersistentBlobProvider.getInstance(getContext());
+ ByteArrayOutputStream baos = new ByteArrayOutputStream();
+ result.compress(Bitmap.CompressFormat.JPEG, 80, baos);
+
+ byte[] data = baos.toByteArray();
+
+ controller.onImageEditComplete(provider.create(getContext(), data, MediaUtil.IMAGE_JPEG, null),
+ result.getWidth(),
+ result.getHeight(),
+ data.length,
+ message,
+ transport);
+ }
+
+ @Override
+ public void onFailure(ExecutionException e) {
+ Log.w(TAG, e);
+ controller.onImageEditFailure();
+ }
+ });
+ }
+
+ private final MotionView.MotionViewCallback motionViewCallback = new MotionView.MotionViewCallback() {
+ @Override
+ public void onEntitySelected(@Nullable MotionEntity entity) {
+ if (entity == null) {
+ scribbleHud.enterMode(ScribbleHud.Mode.NONE);
+ } else if (entity instanceof TextEntity) {
+ int textColor = ((TextEntity) entity).getLayer().getFont().getColor();
+
+ scribbleHud.enterMode(ScribbleHud.Mode.TEXT);
+ scribbleHud.setActiveColor(textColor);
+ } else {
+ scribbleHud.enterMode(ScribbleHud.Mode.STICKER);
+ }
+ }
+
+ @Override
+ public void onEntityDoubleTap(@NonNull MotionEntity entity) {
+ startTextEntityEditing();
+ }
+ };
+
+ public interface Controller {
+ void onImageEditComplete(@NonNull Uri uri, int width, int height, long size, @NonNull Optional message, @NonNull Optional transport);
+ void onImageEditFailure();
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/scribbles/ScribbleHud.java b/src/org/thoughtcrime/securesms/scribbles/ScribbleHud.java
index ed2f996da9..50c03d622a 100644
--- a/src/org/thoughtcrime/securesms/scribbles/ScribbleHud.java
+++ b/src/org/thoughtcrime/securesms/scribbles/ScribbleHud.java
@@ -2,26 +2,43 @@ package org.thoughtcrime.securesms.scribbles;
import android.content.Context;
import android.graphics.Color;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
+import android.text.Editable;
+import android.text.TextWatcher;
import android.util.AttributeSet;
+import android.view.KeyEvent;
import android.view.View;
import android.view.ViewGroup;
-import android.widget.FrameLayout;
+import android.view.ViewTreeObserver;
+import android.widget.TextView;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.TransportOption;
+import org.thoughtcrime.securesms.components.ComposeText;
+import org.thoughtcrime.securesms.components.InputAwareLayout;
+import org.thoughtcrime.securesms.components.SendButton;
+import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
+import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.scribbles.widget.ColorPaletteAdapter;
import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
+import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.thoughtcrime.securesms.util.views.Stub;
+import org.whispersystems.libsignal.util.guava.Optional;
+import java.util.Locale;
import java.util.Set;
/**
* The HUD (heads-up display) that contains all of the tools for interacting with
* {@link org.thoughtcrime.securesms.scribbles.widget.ScribbleView}
*/
-public class ScribbleHud extends FrameLayout {
+public class ScribbleHud extends InputAwareLayout implements ViewTreeObserver.OnGlobalLayoutListener {
private View drawButton;
private View highlightButton;
@@ -32,9 +49,20 @@ public class ScribbleHud extends FrameLayout {
private View saveButton;
private VerticalSlideColorPicker colorPicker;
private RecyclerView colorPalette;
+ private ViewGroup inputContainer;
+ private ComposeText composeText;
+ private SendButton sendButton;
+ private ViewGroup sendButtonBkg;
+ private EmojiToggle emojiToggle;
+ private Stub emojiDrawer;
+ private TextView charactersLeft;
private EventListener eventListener;
private ColorPaletteAdapter colorPaletteAdapter;
+ private int visibleHeight;
+ private Locale locale;
+
+ private final Rect visibleBounds = new Rect();
public ScribbleHud(@NonNull Context context) {
super(context);
@@ -51,8 +79,36 @@ public class ScribbleHud extends FrameLayout {
initialize();
}
+ @Override
+ protected void onAttachedToWindow() {
+ super.onAttachedToWindow();
+ getRootView().getViewTreeObserver().addOnGlobalLayoutListener(this);
+ }
+
+ @Override
+ protected void onDetachedFromWindow() {
+ super.onDetachedFromWindow();
+ getRootView().getViewTreeObserver().removeGlobalOnLayoutListener(this);
+ }
+
+ @Override
+ public void onGlobalLayout() {
+ getRootView().getWindowVisibleDisplayFrame(visibleBounds);
+
+ int currentVisibleHeight = visibleBounds.height();
+
+ if (currentVisibleHeight != visibleHeight) {
+ getLayoutParams().height = currentVisibleHeight;
+ layout(visibleBounds.left, visibleBounds.top, visibleBounds.right, visibleBounds.bottom);
+ requestLayout();
+
+ visibleHeight = currentVisibleHeight;
+ }
+ }
+
private void initialize() {
inflate(getContext(), R.layout.scribble_hud, this);
+ setOrientation(VERTICAL);
drawButton = findViewById(R.id.scribble_draw_button);
highlightButton = findViewById(R.id.scribble_highlight_button);
@@ -63,7 +119,20 @@ public class ScribbleHud extends FrameLayout {
saveButton = findViewById(R.id.scribble_save_button);
colorPicker = findViewById(R.id.scribble_color_picker);
colorPalette = findViewById(R.id.scribble_color_palette);
+ inputContainer = findViewById(R.id.scribble_compose_container);
+ composeText = findViewById(R.id.scribble_compose_text);
+ sendButton = findViewById(R.id.scribble_send_button);
+ sendButtonBkg = findViewById(R.id.scribble_send_button_bkg);
+ emojiToggle = findViewById(R.id.scribble_emoji_toggle);
+ emojiDrawer = new Stub<>(findViewById(R.id.scribble_emoji_drawer_stub));
+ charactersLeft = findViewById(R.id.scribble_characters_left);
+ initializeViews();
+ setMode(Mode.NONE);
+ setTransport(Optional.absent());
+ }
+
+ private void initializeViews() {
undoButton.setOnClickListener(v -> {
if (eventListener != null) {
eventListener.onUndo();
@@ -79,24 +148,79 @@ public class ScribbleHud extends FrameLayout {
saveButton.setOnClickListener(v -> {
if (eventListener != null) {
- eventListener.onSave();
+ eventListener.onEditComplete(Optional.absent(), Optional.absent());
}
setMode(Mode.NONE);
});
+ sendButton.setOnClickListener(v -> {
+ if (eventListener != null) {
+ if (isKeyboardOpen()) {
+ hideSoftkey(composeText, null);
+ }
+ eventListener.onEditComplete(Optional.of(composeText.getTextTrimmed()), Optional.of(sendButton.getSelectedTransport()));
+ }
+ setMode(Mode.NONE);
+ });
+
+ sendButton.addOnTransportChangedListener((newTransport, manuallySelected) -> {
+ presentCharactersRemaining();
+ composeText.setTransport(newTransport);
+ sendButtonBkg.getBackground().setColorFilter(newTransport.getBackgroundColor(), PorterDuff.Mode.MULTIPLY);
+ sendButtonBkg.getBackground().invalidateSelf();
+ });
+
+ ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
+
+ composeText.setOnKeyListener(composeKeyPressedListener);
+ composeText.addTextChangedListener(composeKeyPressedListener);
+ composeText.setOnClickListener(composeKeyPressedListener);
+ composeText.setOnFocusChangeListener(composeKeyPressedListener);
+
+ emojiToggle.setOnClickListener(this::onEmojiToggleClicked);
+
colorPaletteAdapter = new ColorPaletteAdapter();
colorPaletteAdapter.setEventListener(colorPicker::setActiveColor);
colorPalette.setLayoutManager(new LinearLayoutManager(getContext()));
colorPalette.setAdapter(colorPaletteAdapter);
+ }
- setMode(Mode.NONE);
+ public void setLocale(@NonNull Locale locale) {
+ this.locale = locale;
+ }
+
+ public void setTransport(@NonNull Optional transport) {
+ if (transport.isPresent()) {
+ saveButton.setVisibility(GONE);
+ inputContainer.setVisibility(VISIBLE);
+ sendButton.setTransport(transport.get());
+ } else {
+ saveButton.setVisibility(VISIBLE);
+ inputContainer.setVisibility(GONE);
+ }
+ }
+
+ public void dismissEmojiKeyboard() {
+ hideCurrentInput(composeText);
}
public void setColorPalette(@NonNull Set colors) {
colorPaletteAdapter.setColors(colors);
}
+ public int getActiveColor() {
+ return colorPicker.getActiveColor();
+ }
+
+ public void setActiveColor(int color) {
+ colorPicker.setActiveColor(color);
+ }
+
+ public void setEventListener(@Nullable EventListener eventListener) {
+ this.eventListener = eventListener;
+ }
+
public void enterMode(@NonNull Mode mode) {
setMode(mode, false);
}
@@ -201,16 +325,44 @@ public class ScribbleHud extends FrameLayout {
stickerButton.setOnClickListener(v -> setMode(Mode.NONE));
}
- public int getActiveColor() {
- return colorPicker.getActiveColor();
+ private void presentCharactersRemaining() {
+ String messageBody = composeText.getTextTrimmed();
+ TransportOption transportOption = sendButton.getSelectedTransport();
+ CharacterState characterState = transportOption.calculateCharacters(messageBody);
+
+ if (characterState.charactersRemaining <= 15 || characterState.messagesSpent > 1) {
+ charactersLeft.setText(String.format(locale,
+ "%d/%d (%d)",
+ characterState.charactersRemaining,
+ characterState.maxMessageSize,
+ characterState.messagesSpent));
+ charactersLeft.setVisibility(View.VISIBLE);
+ } else {
+ charactersLeft.setVisibility(View.GONE);
+ }
}
- public void setActiveColor(int color) {
- colorPicker.setActiveColor(color);
- }
+ private void onEmojiToggleClicked(View v) {
+ if (!emojiDrawer.resolved()) {
+ emojiToggle.attach(emojiDrawer.get());
+ emojiDrawer.get().setEmojiEventListener(new EmojiDrawer.EmojiEventListener() {
+ @Override
+ public void onKeyEvent(KeyEvent keyEvent) {
+ composeText.dispatchKeyEvent(keyEvent);
+ }
- public void setEventListener(@Nullable EventListener eventListener) {
- this.eventListener = eventListener;
+ @Override
+ public void onEmojiSelected(String emoji) {
+ composeText.insertEmoji(emoji);
+ }
+ });
+ }
+
+ if (getCurrentInput() == emojiDrawer.get()) {
+ showSoftkey(composeText);
+ } else {
+ hideSoftkey(composeText, () -> post(() -> show(composeText, emojiDrawer.get())));
+ }
}
private final VerticalSlideColorPicker.OnColorChangeListener standardOnColorChangeListener = new VerticalSlideColorPicker.OnColorChangeListener() {
@@ -236,6 +388,46 @@ public class ScribbleHud extends FrameLayout {
}
};
+ private class ComposeKeyPressedListener implements OnKeyListener, OnClickListener, TextWatcher, OnFocusChangeListener {
+
+ int beforeLength;
+
+ @Override
+ public boolean onKey(View v, int keyCode, KeyEvent event) {
+ if (event.getAction() == KeyEvent.ACTION_DOWN) {
+ if (keyCode == KeyEvent.KEYCODE_ENTER) {
+ if (TextSecurePreferences.isEnterSendsEnabled(getContext())) {
+ sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER));
+ sendButton.dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER));
+ return true;
+ }
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public void onClick(View v) {
+ showSoftkey(composeText);
+ }
+
+ @Override
+ public void beforeTextChanged(CharSequence s, int start, int count,int after) {
+ beforeLength = composeText.getTextTrimmed().length();
+ }
+
+ @Override
+ public void afterTextChanged(Editable s) {
+ presentCharactersRemaining();
+ }
+
+ @Override
+ public void onTextChanged(CharSequence s, int start, int before,int count) {}
+
+ @Override
+ public void onFocusChange(View v, boolean hasFocus) {}
+ }
+
public enum Mode {
NONE, DRAW, HIGHLIGHT, TEXT, STICKER
}
@@ -245,6 +437,6 @@ public class ScribbleHud extends FrameLayout {
void onColorChange(int color);
void onUndo();
void onDelete();
- void onSave();
+ void onEditComplete(@NonNull Optional message, @NonNull Optional transport);
}
}
diff --git a/src/org/thoughtcrime/securesms/util/CharacterCalculator.java b/src/org/thoughtcrime/securesms/util/CharacterCalculator.java
index c58bb0d4f8..f9b95f1fea 100644
--- a/src/org/thoughtcrime/securesms/util/CharacterCalculator.java
+++ b/src/org/thoughtcrime/securesms/util/CharacterCalculator.java
@@ -16,10 +16,34 @@
*/
package org.thoughtcrime.securesms.util;
+import android.os.Parcel;
+import android.support.annotation.NonNull;
+
public abstract class CharacterCalculator {
public abstract CharacterState calculateCharacters(String messageBody);
+ public static CharacterCalculator readFromParcel(@NonNull Parcel in) {
+ switch (in.readInt()) {
+ case 1: return new SmsCharacterCalculator();
+ case 2: return new MmsCharacterCalculator();
+ case 3: return new PushCharacterCalculator();
+ default: throw new IllegalArgumentException("Read an unsupported value for a calculator.");
+ }
+ }
+
+ public static void writeToParcel(@NonNull Parcel dest, @NonNull CharacterCalculator calculator) {
+ if (calculator instanceof SmsCharacterCalculator) {
+ dest.writeInt(1);
+ } else if (calculator instanceof MmsCharacterCalculator) {
+ dest.writeInt(2);
+ } else if (calculator instanceof PushCharacterCalculator) {
+ dest.writeInt(3);
+ } else {
+ throw new IllegalArgumentException("Tried to write an unsupported calculator to a parcel.");
+ }
+ }
+
public static class CharacterState {
public int charactersRemaining;
public int messagesSpent;
diff --git a/src/org/thoughtcrime/securesms/util/concurrent/LifecycleBoundTask.java b/src/org/thoughtcrime/securesms/util/concurrent/LifecycleBoundTask.java
new file mode 100644
index 0000000000..143257a501
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/util/concurrent/LifecycleBoundTask.java
@@ -0,0 +1,49 @@
+package org.thoughtcrime.securesms.util.concurrent;
+
+import android.arch.lifecycle.Lifecycle;
+import android.os.AsyncTask;
+import android.support.annotation.NonNull;
+
+import org.thoughtcrime.securesms.util.Util;
+
+import java.util.concurrent.Callable;
+
+public class LifecycleBoundTask {
+
+ /**
+ * Runs a task in the background and passes the result of the computation to a task that is run
+ * on the main thread. Will only invoke the {@code foregroundTask} if the provided {@link Lifecycle}
+ * is in a valid (i.e. visible) state at that time. In this way, it is very similar to
+ * {@link AsyncTask}, but is safe in that you can guarantee your task won't be called when your
+ * view is in an invalid state.
+ */
+ public static void run(@NonNull Lifecycle lifecycle, @NonNull BackgroundTask backgroundTask, @NonNull ForegroundTask foregroundTask) {
+ if (!isValid(lifecycle)) {
+ return;
+ }
+
+ AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
+ final E result = backgroundTask.run();
+
+ if (isValid(lifecycle)) {
+ Util.runOnMain(() -> {
+ if (isValid(lifecycle)) {
+ foregroundTask.run(result);
+ }
+ });
+ }
+ });
+ }
+
+ private static boolean isValid(@NonNull Lifecycle lifecycle) {
+ return lifecycle.getCurrentState().isAtLeast(Lifecycle.State.CREATED);
+ }
+
+ public interface BackgroundTask {
+ E run();
+ }
+
+ public interface ForegroundTask {
+ void run(E result);
+ }
+}