Implemented new camera capture flow.
A new, fullscreen camera capture flow that easily allows you to capture and edit a photo before sending it. Replaces the current half-screen camera button.
@ -383,7 +383,7 @@
|
|||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name=".scribbles.StickerSelectActivity"
|
<activity android:name=".scribbles.StickerSelectActivity"
|
||||||
android:theme="@style/TextSecure.LightTheme"
|
android:theme="@style/TextSecure.ScribbleTheme"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<activity android:name="com.soundcloud.android.crop.CropImageActivity" />
|
<activity android:name="com.soundcloud.android.crop.CropImageActivity" />
|
||||||
@ -422,6 +422,11 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
|
<activity android:name=".camera.CameraActivity"
|
||||||
|
android:theme="@style/TextSecure.ScribbleTheme"
|
||||||
|
android:windowSoftInputMode="stateHidden"
|
||||||
|
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||||
|
|
||||||
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
|
||||||
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
|
||||||
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>
|
||||||
|
13
res/anim/camera_capture_button_grow.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<scale
|
||||||
|
android:duration="100"
|
||||||
|
android:interpolator="@android:anim/decelerate_interpolator"
|
||||||
|
android:fromXScale=".85"
|
||||||
|
android:fromYScale=".85"
|
||||||
|
android:toXScale="1.0"
|
||||||
|
android:toYScale="1.0"
|
||||||
|
android:pivotX="50%"
|
||||||
|
android:pivotY="50%" />
|
||||||
|
</set>
|
13
res/anim/camera_capture_button_shrink.xml
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<scale
|
||||||
|
android:duration="100"
|
||||||
|
android:interpolator="@android:anim/decelerate_interpolator"
|
||||||
|
android:fromXScale="1.0"
|
||||||
|
android:fromYScale="1.0"
|
||||||
|
android:toXScale=".85"
|
||||||
|
android:toYScale=".85"
|
||||||
|
android:pivotX="50%"
|
||||||
|
android:pivotY="50%" />
|
||||||
|
</set>
|
8
res/anim/camera_slide_from_bottom.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||||
|
<translate
|
||||||
|
android:interpolator="@android:anim/decelerate_interpolator"
|
||||||
|
android:duration="350"
|
||||||
|
android:fromYDelta="100%"
|
||||||
|
android:toYDelta="0%" />
|
||||||
|
</set>
|
8
res/anim/camera_slide_to_bottom.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android" >
|
||||||
|
<translate
|
||||||
|
android:interpolator="@android:anim/decelerate_interpolator"
|
||||||
|
android:duration="250"
|
||||||
|
android:fromYDelta="0%"
|
||||||
|
android:toYDelta="100%" />
|
||||||
|
</set>
|
6
res/anim/fade_in.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<alpha
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:interpolator="@android:interpolator/decelerate_quad"
|
||||||
|
android:fromAlpha="0.0"
|
||||||
|
android:toAlpha="1.0"
|
||||||
|
android:duration="@android:integer/config_shortAnimTime" />
|
6
res/anim/fade_out.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<alpha
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:interpolator="@android:interpolator/decelerate_quad"
|
||||||
|
android:fromAlpha="1.0"
|
||||||
|
android:toAlpha="0.0"
|
||||||
|
android:duration="@android:integer/config_shortAnimTime" />
|
7
res/anim/stationary.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<translate
|
||||||
|
android:duration="250"
|
||||||
|
android:fromYDelta="0%"
|
||||||
|
android:toYDelta="0%" />
|
||||||
|
</set>
|
Before Width: | Height: | Size: 961 B |
Before Width: | Height: | Size: 788 B |
BIN
res/drawable-hdpi/ic_camera_front.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
res/drawable-hdpi/ic_camera_rear.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
res/drawable-hdpi/ic_camera_shutter.png
Normal file
After Width: | Height: | Size: 6.5 KiB |
Before Width: | Height: | Size: 1.2 KiB |
BIN
res/drawable-hdpi/ic_scribble_brush.png
Normal file
After Width: | Height: | Size: 982 B |
BIN
res/drawable-hdpi/ic_scribble_delete.png
Normal file
After Width: | Height: | Size: 721 B |
BIN
res/drawable-hdpi/ic_scribble_highlight.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
res/drawable-hdpi/ic_scribble_save.png
Normal file
After Width: | Height: | Size: 718 B |
BIN
res/drawable-hdpi/ic_scribble_sticker.png
Normal file
After Width: | Height: | Size: 2.0 KiB |
BIN
res/drawable-hdpi/ic_scribble_text.png
Normal file
After Width: | Height: | Size: 926 B |
BIN
res/drawable-hdpi/ic_scribble_undo.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 708 B |
Before Width: | Height: | Size: 654 B |
Before Width: | Height: | Size: 506 B |
BIN
res/drawable-mdpi/ic_camera_front.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
res/drawable-mdpi/ic_camera_rear.png
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
res/drawable-mdpi/ic_camera_shutter.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 838 B |
BIN
res/drawable-mdpi/ic_scribble_brush.png
Normal file
After Width: | Height: | Size: 537 B |
BIN
res/drawable-mdpi/ic_scribble_delete.png
Normal file
After Width: | Height: | Size: 399 B |
BIN
res/drawable-mdpi/ic_scribble_highlight.png
Normal file
After Width: | Height: | Size: 609 B |
BIN
res/drawable-mdpi/ic_scribble_save.png
Normal file
After Width: | Height: | Size: 421 B |
BIN
res/drawable-mdpi/ic_scribble_sticker.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
res/drawable-mdpi/ic_scribble_text.png
Normal file
After Width: | Height: | Size: 503 B |
BIN
res/drawable-mdpi/ic_scribble_undo.png
Normal file
After Width: | Height: | Size: 788 B |
Before Width: | Height: | Size: 482 B |
Before Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 984 B |
BIN
res/drawable-xhdpi/ic_camera_front.png
Normal file
After Width: | Height: | Size: 3.4 KiB |
BIN
res/drawable-xhdpi/ic_camera_rear.png
Normal file
After Width: | Height: | Size: 2.8 KiB |
BIN
res/drawable-xhdpi/ic_camera_shutter.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
Before Width: | Height: | Size: 1.7 KiB |
BIN
res/drawable-xhdpi/ic_scribble_brush.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
res/drawable-xhdpi/ic_scribble_delete.png
Normal file
After Width: | Height: | Size: 906 B |
BIN
res/drawable-xhdpi/ic_scribble_highlight.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
res/drawable-xhdpi/ic_scribble_save.png
Normal file
After Width: | Height: | Size: 916 B |
BIN
res/drawable-xhdpi/ic_scribble_sticker.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
res/drawable-xhdpi/ic_scribble_text.png
Normal file
After Width: | Height: | Size: 982 B |
BIN
res/drawable-xhdpi/ic_scribble_undo.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 539 B |
Before Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.7 KiB |
BIN
res/drawable-xxhdpi/ic_camera_front.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
BIN
res/drawable-xxhdpi/ic_camera_rear.png
Normal file
After Width: | Height: | Size: 5.1 KiB |
BIN
res/drawable-xxhdpi/ic_camera_shutter.png
Normal file
After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 3.1 KiB |
BIN
res/drawable-xxhdpi/ic_scribble_brush.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
res/drawable-xxhdpi/ic_scribble_delete.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
res/drawable-xxhdpi/ic_scribble_highlight.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
BIN
res/drawable-xxhdpi/ic_scribble_save.png
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
res/drawable-xxhdpi/ic_scribble_sticker.png
Normal file
After Width: | Height: | Size: 5.3 KiB |
BIN
res/drawable-xxhdpi/ic_scribble_text.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
res/drawable-xxhdpi/ic_scribble_undo.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
Before Width: | Height: | Size: 763 B |
Before Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.4 KiB |
BIN
res/drawable-xxxhdpi/ic_camera_front.png
Normal file
After Width: | Height: | Size: 9.4 KiB |
BIN
res/drawable-xxxhdpi/ic_camera_rear.png
Normal file
After Width: | Height: | Size: 7.5 KiB |
BIN
res/drawable-xxxhdpi/ic_camera_shutter.png
Normal file
After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 4.2 KiB |
BIN
res/drawable-xxxhdpi/ic_scribble_brush.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
res/drawable-xxxhdpi/ic_scribble_delete.png
Normal file
After Width: | Height: | Size: 2.3 KiB |
BIN
res/drawable-xxxhdpi/ic_scribble_highlight.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
res/drawable-xxxhdpi/ic_scribble_save.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
BIN
res/drawable-xxxhdpi/ic_scribble_sticker.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
res/drawable-xxxhdpi/ic_scribble_text.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
res/drawable-xxxhdpi/ic_scribble_undo.png
Normal file
After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 840 B |
16
res/drawable/compose_background_camera.xml
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle">
|
||||||
|
|
||||||
|
<stroke
|
||||||
|
android:width="1dp"
|
||||||
|
android:color="@color/core_light_45" />
|
||||||
|
|
||||||
|
<solid
|
||||||
|
android:color="@color/transparent_black_90" />
|
||||||
|
|
||||||
|
<corners
|
||||||
|
android:radius="20dp" />
|
||||||
|
|
||||||
|
</shape>
|
21
res/layout/camera_activity.xml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/core_black">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/camera_snapshot"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:scaleType="centerInside"
|
||||||
|
tools:src="@color/grey_800"/>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/fragment_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
33
res/layout/camera_controls_landscape.xml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_marginRight="16dp"
|
||||||
|
android:layout_marginEnd="16dp"
|
||||||
|
android:layout_gravity="right|end">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/camera_capture_button"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:background="@drawable/ic_camera_shutter" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/camera_flip_button"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@+id/camera_capture_button"
|
||||||
|
android:layout_marginTop="40dp"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:src="@drawable/ic_camera_front"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
34
res/layout/camera_controls_portrait.xml
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginBottom="16dp"
|
||||||
|
android:layout_gravity="bottom">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/camera_capture_button"
|
||||||
|
android:layout_width="80dp"
|
||||||
|
android:layout_height="80dp"
|
||||||
|
android:layout_centerHorizontal="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:background="@drawable/ic_camera_shutter" />
|
||||||
|
|
||||||
|
<ImageButton
|
||||||
|
android:id="@+id/camera_flip_button"
|
||||||
|
android:layout_width="48dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_toLeftOf="@+id/camera_capture_button"
|
||||||
|
android:layout_toStartOf="@+id/camera_capture_button"
|
||||||
|
android:layout_marginRight="40dp"
|
||||||
|
android:layout_marginEnd="40dp"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:src="@drawable/ic_camera_front"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
||||||
|
|
19
res/layout/camera_fragment.xml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:orientation="vertical"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<TextureView
|
||||||
|
android:id="@+id/camera_preview"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/camera_controls_container"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
@ -2,22 +2,10 @@
|
|||||||
<FrameLayout
|
<FrameLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:id="@+id/activity_main"
|
android:id="@+id/fragment_container"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/black"
|
android:background="@color/black"
|
||||||
tools:context="org.thoughtcrime.securesms.scribbles.ScribbleActivity">
|
tools:context="org.thoughtcrime.securesms.scribbles.ScribbleActivity">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.scribbles.widget.ScribbleView
|
|
||||||
android:id="@+id/scribble_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:gravity="center" />
|
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.scribbles.ScribbleHud
|
|
||||||
android:id="@+id/scribble_hud"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
|
22
res/layout/scribble_fragment.xml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<FrameLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:id="@+id/activity_main"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
tools:context="org.thoughtcrime.securesms.scribbles.ScribbleActivity">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.scribbles.widget.ScribbleView
|
||||||
|
android:id="@+id/scribble_view"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center"
|
||||||
|
android:gravity="center" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.scribbles.ScribbleHud
|
||||||
|
android:id="@+id/scribble_hud"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
8
res/layout/scribble_fragment_emojidrawer_stub.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<org.thoughtcrime.securesms.components.emoji.EmojiDrawer
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:id="@+id/emoji_drawer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:background="@color/core_light_90"
|
||||||
|
android:visibility="gone" />
|
@ -1,21 +1,32 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<merge
|
<merge
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto">
|
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">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="0dp"
|
||||||
|
android:layout_weight="1">
|
||||||
|
|
||||||
<ImageView
|
<ImageView
|
||||||
android:id="@+id/scribble_save_button"
|
android:id="@+id/scribble_save_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="top|left"
|
android:layout_gravity="top|left|start"
|
||||||
android:src="@drawable/ic_check_white_24dp"
|
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
|
android:src="@drawable/ic_check_white_24dp"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="wrap_content"
|
||||||
android:layout_gravity="right|top"
|
android:layout_gravity="top|right|end"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:animateLayoutChanges="true">
|
android:animateLayoutChanges="true">
|
||||||
|
|
||||||
@ -23,7 +34,7 @@
|
|||||||
android:id="@+id/scribble_text_button"
|
android:id="@+id/scribble_text_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_text_fields_white_24dp"
|
android:src="@drawable/ic_scribble_text"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||||
|
|
||||||
@ -31,7 +42,7 @@
|
|||||||
android:id="@+id/scribble_draw_button"
|
android:id="@+id/scribble_draw_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_brush_white_24dp"
|
android:src="@drawable/ic_scribble_brush"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||||
|
|
||||||
@ -39,7 +50,7 @@
|
|||||||
android:id="@+id/scribble_highlight_button"
|
android:id="@+id/scribble_highlight_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/baseline_highlight_white_24"
|
android:src="@drawable/ic_scribble_highlight"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||||
|
|
||||||
@ -47,7 +58,7 @@
|
|||||||
android:id="@+id/scribble_sticker_button"
|
android:id="@+id/scribble_sticker_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_tag_faces_white_24dp"
|
android:src="@drawable/ic_scribble_sticker"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||||
|
|
||||||
@ -55,7 +66,7 @@
|
|||||||
android:id="@+id/scribble_undo_button"
|
android:id="@+id/scribble_undo_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_replay_white_24dp"
|
android:src="@drawable/ic_scribble_undo"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||||
|
|
||||||
@ -63,7 +74,7 @@
|
|||||||
android:id="@+id/scribble_delete_button"
|
android:id="@+id/scribble_delete_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:src="@drawable/ic_delete_white_24dp"
|
android:src="@drawable/ic_scribble_delete"
|
||||||
android:padding="12dp"
|
android:padding="12dp"
|
||||||
android:background="?attr/selectableItemBackgroundBorderless"/>
|
android:background="?attr/selectableItemBackgroundBorderless"/>
|
||||||
|
|
||||||
@ -86,4 +97,102 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
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">
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:paddingLeft="10dp"
|
||||||
|
android:paddingStart="10dp"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:background="@drawable/compose_background_camera">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.emoji.EmojiToggle
|
||||||
|
android:id="@+id/scribble_emoji_toggle"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="@dimen/conversation_compose_height"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:paddingLeft="4dp"
|
||||||
|
android:paddingStart="4dp"
|
||||||
|
android:paddingRight="6dp"
|
||||||
|
android:paddingEnd="6dp"
|
||||||
|
android:background="?selectableItemBackgroundBorderless"
|
||||||
|
android:contentDescription="@string/conversation_activity__emoji_toggle_description" />
|
||||||
|
|
||||||
|
<Space
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="@dimen/conversation_compose_height" />
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.ComposeText
|
||||||
|
style="@style/ComposeEditText"
|
||||||
|
android:id="@+id/scribble_compose_text"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="center_vertical"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:nextFocusForward="@+id/send_button"
|
||||||
|
android:nextFocusRight="@+id/send_button"
|
||||||
|
tools:visibility="invisible"
|
||||||
|
tools:hint="Send TextSecure message" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/scribble_send_button_bkg"
|
||||||
|
android:layout_width="@dimen/conversation_compose_height"
|
||||||
|
android:layout_height="@dimen/conversation_compose_height"
|
||||||
|
android:layout_marginLeft="12dp"
|
||||||
|
android:layout_marginStart="12dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
|
android:background="@drawable/circle_tintable"
|
||||||
|
tools:backgroundTint="@color/core_blue">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.SendButton
|
||||||
|
android:id="@+id/scribble_send_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:paddingTop="6dp"
|
||||||
|
android:paddingRight="6dp"
|
||||||
|
android:paddingBottom="6dp"
|
||||||
|
android:paddingLeft="11dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:contentDescription="@string/conversation_activity__send"
|
||||||
|
android:src="?conversation_transport_sms_indicator"
|
||||||
|
android:background="@drawable/circle_touch_highlight_background" />
|
||||||
|
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/scribble_characters_left"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_marginTop="6dp"
|
||||||
|
android:layout_marginLeft="10dp"
|
||||||
|
android:layout_marginRight="10dp"
|
||||||
|
android:paddingBottom="6dp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:visibility="visible"
|
||||||
|
tools:text="160/160 (1)" />
|
||||||
|
|
||||||
|
<ViewStub
|
||||||
|
android:id="@+id/scribble_emoji_drawer_stub"
|
||||||
|
android:layout="@layout/scribble_fragment_emojidrawer_stub"
|
||||||
|
android:inflatedId="@+id/emoji_drawer"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"/>
|
||||||
|
|
||||||
</merge>
|
</merge>
|
@ -3,6 +3,5 @@
|
|||||||
<style name="TextSecure.ScribbleTheme" parent="TextSecure.DarkNoActionBar">
|
<style name="TextSecure.ScribbleTheme" parent="TextSecure.DarkNoActionBar">
|
||||||
<item name="android:windowNoTitle">true</item>
|
<item name="android:windowNoTitle">true</item>
|
||||||
<item name="android:windowFullscreen">true</item>
|
<item name="android:windowFullscreen">true</item>
|
||||||
<item name="android:windowTranslucentNavigation">true</item>
|
|
||||||
</style>
|
</style>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -77,6 +77,10 @@
|
|||||||
<!-- CallScreen -->
|
<!-- CallScreen -->
|
||||||
<string name="CallScreen_Incoming_call">Incoming call</string>
|
<string name="CallScreen_Incoming_call">Incoming call</string>
|
||||||
|
|
||||||
|
<!-- CameraActivity -->
|
||||||
|
<string name="CameraActivity_camera_unavailable">Camera unavailable.</string>
|
||||||
|
<string name="CameraActivity_image_save_failure">Failed to save image.</string>
|
||||||
|
|
||||||
<!-- ClearProfileActivity -->
|
<!-- ClearProfileActivity -->
|
||||||
<string name="ClearProfileActivity_remove">Remove</string>
|
<string name="ClearProfileActivity_remove">Remove</string>
|
||||||
<string name="ClearProfileActivity_remove_profile_photo">Remove profile photo?</string>
|
<string name="ClearProfileActivity_remove_profile_photo">Remove profile photo?</string>
|
||||||
|
@ -52,6 +52,7 @@ import android.text.Editable;
|
|||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.camera.CameraActivity;
|
||||||
import org.thoughtcrime.securesms.database.GroupDatabase;
|
import org.thoughtcrime.securesms.database.GroupDatabase;
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
import org.thoughtcrime.securesms.logging.Log;
|
||||||
import android.util.Pair;
|
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.AudioSlide;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||||
|
import org.thoughtcrime.securesms.mms.ImageSlide;
|
||||||
import org.thoughtcrime.securesms.mms.LocationSlide;
|
import org.thoughtcrime.securesms.mms.LocationSlide;
|
||||||
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
import org.thoughtcrime.securesms.mms.MediaConstraints;
|
||||||
import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
|
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_LOCATION = 9;
|
||||||
private static final int PICK_GIF = 10;
|
private static final int PICK_GIF = 10;
|
||||||
private static final int SMS_DEFAULT = 11;
|
private static final int SMS_DEFAULT = 11;
|
||||||
|
private static final int PICK_CAMERA = 12;
|
||||||
|
private static final int EDIT_IMAGE = 13;
|
||||||
|
|
||||||
private GlideRequests glideRequests;
|
private GlideRequests glideRequests;
|
||||||
protected ComposeText composeText;
|
protected ComposeText composeText;
|
||||||
@ -495,6 +499,27 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
case SMS_DEFAULT:
|
case SMS_DEFAULT:
|
||||||
initializeSecurity(isSecureText, isDefaultSms);
|
initializeSecurity(isSecureText, isDefaultSms);
|
||||||
break;
|
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,7 +2129,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
private class QuickCameraToggleListener implements OnClickListener {
|
private class QuickCameraToggleListener implements OnClickListener {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
if (!quickAttachmentDrawer.isShowing()) {
|
|
||||||
Permissions.with(ConversationActivity.this)
|
Permissions.with(ConversationActivity.this)
|
||||||
.request(Manifest.permission.CAMERA)
|
.request(Manifest.permission.CAMERA)
|
||||||
.ifNecessary()
|
.ifNecessary()
|
||||||
@ -2112,13 +2136,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
|
.withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
|
||||||
.onAllGranted(() -> {
|
.onAllGranted(() -> {
|
||||||
composeText.clearFocus();
|
composeText.clearFocus();
|
||||||
container.show(composeText, quickAttachmentDrawer);
|
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())
|
.onAnyDenied(() -> Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())
|
||||||
.execute();
|
.execute();
|
||||||
} else {
|
|
||||||
container.hideAttachedInput(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
|
import android.os.Parcel;
|
||||||
|
import android.os.Parcelable;
|
||||||
import android.support.annotation.DrawableRes;
|
import android.support.annotation.DrawableRes;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.util.CharacterCalculator;
|
import org.thoughtcrime.securesms.util.CharacterCalculator;
|
||||||
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
||||||
import org.whispersystems.libsignal.util.guava.Optional;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
|
|
||||||
public class TransportOption {
|
public class TransportOption implements Parcelable {
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
SMS,
|
SMS,
|
||||||
@ -53,6 +56,16 @@ public class TransportOption {
|
|||||||
this.simSubscriptionId = simSubscriptionId;
|
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() {
|
public @NonNull Type getType() {
|
||||||
return type;
|
return type;
|
||||||
@ -96,4 +109,38 @@ public class TransportOption {
|
|||||||
return simSubscriptionId;
|
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<TransportOption> CREATOR = new Creator<TransportOption>() {
|
||||||
|
@Override
|
||||||
|
public TransportOption createFromParcel(Parcel in) {
|
||||||
|
return new TransportOption(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TransportOption[] newArray(int size) {
|
||||||
|
return new TransportOption[size];
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
194
src/org/thoughtcrime/securesms/camera/Camera1Controller.java
Normal file
@ -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<Stage> 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<String> 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<Camera.Size> 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<Camera.Size> 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();
|
||||||
|
}
|
||||||
|
}
|
321
src/org/thoughtcrime/securesms/camera/Camera1Fragment.java
Normal file
@ -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<Stage> 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() { }
|
||||||
|
}
|
||||||
|
}
|
166
src/org/thoughtcrime/securesms/camera/CameraActivity.java
Normal file
@ -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<Boolean> result = new SettableFuture<>();
|
||||||
|
GlideApp.with(this).load(new DecryptableStreamUriLoader.DecryptableUri(captureUri)).into(new GlideDrawableListeningTarget(snapshot, result));
|
||||||
|
result.addListener(new AssertedSuccessListener<Boolean>() {
|
||||||
|
@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<String> message, @NonNull Optional<TransportOption> 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();
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
if (runAfterClose != null) postOnKeyboardClose(runAfterClose);
|
||||||
|
|
||||||
ServiceUtil.getInputMethodManager(inputTarget.getContext())
|
ServiceUtil.getInputMethodManager(inputTarget.getContext())
|
||||||
|
@ -56,6 +56,7 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
|||||||
|
|
||||||
private boolean keyboardOpen = false;
|
private boolean keyboardOpen = false;
|
||||||
private int rotation = -1;
|
private int rotation = -1;
|
||||||
|
private boolean isFullscreen = false;
|
||||||
|
|
||||||
public KeyboardAwareLinearLayout(Context context) {
|
public KeyboardAwareLinearLayout(Context context) {
|
||||||
this(context, null);
|
this(context, null);
|
||||||
@ -98,9 +99,10 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (viewInset == 0 && Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) viewInset = getViewInset();
|
if (viewInset == 0 && Build.VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) viewInset = getViewInset();
|
||||||
final int availableHeight = this.getRootView().getHeight() - statusBarHeight - viewInset;
|
|
||||||
getWindowVisibleDisplayFrame(rect);
|
getWindowVisibleDisplayFrame(rect);
|
||||||
|
|
||||||
|
final int availableHeight = this.getRootView().getHeight() - viewInset - (!isFullscreen ? statusBarHeight : 0);
|
||||||
final int keyboardHeight = availableHeight - (rect.bottom - rect.top);
|
final int keyboardHeight = availableHeight - (rect.bottom - rect.top);
|
||||||
|
|
||||||
if (keyboardHeight > minKeyboardSize) {
|
if (keyboardHeight > minKeyboardSize) {
|
||||||
@ -217,6 +219,10 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
|||||||
shownListeners.remove(listener);
|
shownListeners.remove(listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setFullscreen(boolean isFullscreen) {
|
||||||
|
this.isFullscreen = isFullscreen;
|
||||||
|
}
|
||||||
|
|
||||||
private void notifyHiddenListeners() {
|
private void notifyHiddenListeners() {
|
||||||
final Set<OnKeyboardHiddenListener> listeners = new HashSet<>(hiddenListeners);
|
final Set<OnKeyboardHiddenListener> listeners = new HashSet<>(hiddenListeners);
|
||||||
for (OnKeyboardHiddenListener listener : listeners) {
|
for (OnKeyboardHiddenListener listener : listeners) {
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
@ -83,6 +84,10 @@ public class SendButton extends ImageButton
|
|||||||
transportOptions.setDefaultTransport(type);
|
transportOptions.setDefaultTransport(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setTransport(@NonNull TransportOption option) {
|
||||||
|
transportOptions.setSelectedTransport(option);
|
||||||
|
}
|
||||||
|
|
||||||
public void setDefaultSubscriptionId(Optional<Integer> subscriptionId) {
|
public void setDefaultSubscriptionId(Optional<Integer> subscriptionId) {
|
||||||
transportOptions.setDefaultSubscriptionId(subscriptionId);
|
transportOptions.setDefaultSubscriptionId(subscriptionId);
|
||||||
}
|
}
|
||||||
|
@ -1,238 +1,59 @@
|
|||||||
package org.thoughtcrime.securesms.scribbles;
|
package org.thoughtcrime.securesms.scribbles;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.graphics.PointF;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
import org.thoughtcrime.securesms.logging.Log;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
import org.thoughtcrime.securesms.TransportOption;
|
||||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||||
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
|
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||||
import org.thoughtcrime.securesms.scribbles.viewmodel.Font;
|
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||||
import org.thoughtcrime.securesms.scribbles.viewmodel.Layer;
|
import org.whispersystems.libsignal.util.guava.Optional;
|
||||||
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;
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
|
@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();
|
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 final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||||
private ScribbleView scribbleView;
|
|
||||||
private GlideRequests glideRequests;
|
@Override
|
||||||
|
protected void onPreCreate() {
|
||||||
|
dynamicLanguage.onCreate(this);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
protected void onCreate(Bundle savedInstanceState, boolean ready) {
|
||||||
setContentView(R.layout.scribble_activity);
|
setContentView(R.layout.scribble_activity);
|
||||||
|
|
||||||
this.glideRequests = GlideApp.with(this);
|
if (savedInstanceState == null) {
|
||||||
this.scribbleHud = findViewById(R.id.scribble_hud);
|
ScribbleFragment fragment = ScribbleFragment.newInstance(getIntent().getData(), dynamicLanguage.getCurrentLocale(), Optional.absent());
|
||||||
this.scribbleView = findViewById(R.id.scribble_view);
|
getSupportFragmentManager().beginTransaction().add(R.id.fragment_container, fragment).commit();
|
||||||
|
}
|
||||||
scribbleHud.setEventListener(this);
|
|
||||||
|
|
||||||
scribbleView.setMotionViewCallback(motionViewCallback);
|
|
||||||
scribbleView.setDrawingChangedListener(() -> scribbleHud.setColorPalette(scribbleView.getUniqueColors()));
|
|
||||||
scribbleView.setDrawingMode(false);
|
|
||||||
scribbleView.setImage(glideRequests, getIntent().getData());
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= 19) {
|
if (Build.VERSION.SDK_INT >= 19) {
|
||||||
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN |
|
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_FULLSCREEN |
|
||||||
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
|
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<Void, Void, Bitmap>() {
|
|
||||||
@Override
|
|
||||||
protected @Nullable
|
|
||||||
Bitmap doInBackground(Void... params) {
|
|
||||||
try {
|
|
||||||
return BitmapFactory.decodeStream(getAssets().open(stickerFile));
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostExecute(@Nullable Bitmap bitmap) {
|
protected void onResume() {
|
||||||
addSticker(bitmap);
|
super.onResume();
|
||||||
}
|
dynamicLanguage.onResume(this);
|
||||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onModeStarted(@NonNull ScribbleHud.Mode mode) {
|
public void onImageEditComplete(@NonNull Uri uri, int width, int height, long size, @NonNull Optional<String> message, @NonNull Optional<TransportOption> transport) {
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@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 onSave() {
|
|
||||||
ListenableFuture<Bitmap> future = scribbleView.getRenderedImage(glideRequests);
|
|
||||||
|
|
||||||
future.addListener(new ListenableFuture.Listener<Bitmap>() {
|
|
||||||
@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 intent = new Intent();
|
||||||
intent.setData(uri);
|
intent.setData(uri);
|
||||||
setResult(RESULT_OK, intent);
|
setResult(RESULT_OK, intent);
|
||||||
@ -241,32 +62,8 @@ public class ScribbleActivity extends PassphraseRequiredActionBarActivity implem
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(ExecutionException e) {
|
public void onImageEditFailure() {
|
||||||
Log.w(TAG, e);
|
|
||||||
Toast.makeText(ScribbleActivity.this, R.string.ScribbleActivity_save_failure, Toast.LENGTH_SHORT).show();
|
Toast.makeText(ScribbleActivity.this, R.string.ScribbleActivity_save_failure, Toast.LENGTH_SHORT).show();
|
||||||
finish();
|
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();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
306
src/org/thoughtcrime/securesms/scribbles/ScribbleFragment.java
Normal file
@ -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<TransportOption> 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<String> message, Optional<TransportOption> transport) {
|
||||||
|
ListenableFuture<Bitmap> future = scribbleView.getRenderedImage(glideRequests);
|
||||||
|
|
||||||
|
future.addListener(new ListenableFuture.Listener<Bitmap>() {
|
||||||
|
@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<String> message, @NonNull Optional<TransportOption> transport);
|
||||||
|
void onImageEditFailure();
|
||||||
|
}
|
||||||
|
}
|
@ -2,26 +2,43 @@ package org.thoughtcrime.securesms.scribbles;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
|
import android.graphics.PorterDuff;
|
||||||
|
import android.graphics.Rect;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
import android.support.v7.widget.RecyclerView;
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.text.Editable;
|
||||||
|
import android.text.TextWatcher;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
|
import android.view.KeyEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
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.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.ColorPaletteAdapter;
|
||||||
import org.thoughtcrime.securesms.scribbles.widget.VerticalSlideColorPicker;
|
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;
|
import java.util.Set;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The HUD (heads-up display) that contains all of the tools for interacting with
|
* The HUD (heads-up display) that contains all of the tools for interacting with
|
||||||
* {@link org.thoughtcrime.securesms.scribbles.widget.ScribbleView}
|
* {@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 drawButton;
|
||||||
private View highlightButton;
|
private View highlightButton;
|
||||||
@ -32,9 +49,20 @@ public class ScribbleHud extends FrameLayout {
|
|||||||
private View saveButton;
|
private View saveButton;
|
||||||
private VerticalSlideColorPicker colorPicker;
|
private VerticalSlideColorPicker colorPicker;
|
||||||
private RecyclerView colorPalette;
|
private RecyclerView colorPalette;
|
||||||
|
private ViewGroup inputContainer;
|
||||||
|
private ComposeText composeText;
|
||||||
|
private SendButton sendButton;
|
||||||
|
private ViewGroup sendButtonBkg;
|
||||||
|
private EmojiToggle emojiToggle;
|
||||||
|
private Stub<EmojiDrawer> emojiDrawer;
|
||||||
|
private TextView charactersLeft;
|
||||||
|
|
||||||
private EventListener eventListener;
|
private EventListener eventListener;
|
||||||
private ColorPaletteAdapter colorPaletteAdapter;
|
private ColorPaletteAdapter colorPaletteAdapter;
|
||||||
|
private int visibleHeight;
|
||||||
|
private Locale locale;
|
||||||
|
|
||||||
|
private final Rect visibleBounds = new Rect();
|
||||||
|
|
||||||
public ScribbleHud(@NonNull Context context) {
|
public ScribbleHud(@NonNull Context context) {
|
||||||
super(context);
|
super(context);
|
||||||
@ -51,8 +79,36 @@ public class ScribbleHud extends FrameLayout {
|
|||||||
initialize();
|
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() {
|
private void initialize() {
|
||||||
inflate(getContext(), R.layout.scribble_hud, this);
|
inflate(getContext(), R.layout.scribble_hud, this);
|
||||||
|
setOrientation(VERTICAL);
|
||||||
|
|
||||||
drawButton = findViewById(R.id.scribble_draw_button);
|
drawButton = findViewById(R.id.scribble_draw_button);
|
||||||
highlightButton = findViewById(R.id.scribble_highlight_button);
|
highlightButton = findViewById(R.id.scribble_highlight_button);
|
||||||
@ -63,7 +119,20 @@ public class ScribbleHud extends FrameLayout {
|
|||||||
saveButton = findViewById(R.id.scribble_save_button);
|
saveButton = findViewById(R.id.scribble_save_button);
|
||||||
colorPicker = findViewById(R.id.scribble_color_picker);
|
colorPicker = findViewById(R.id.scribble_color_picker);
|
||||||
colorPalette = findViewById(R.id.scribble_color_palette);
|
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 -> {
|
undoButton.setOnClickListener(v -> {
|
||||||
if (eventListener != null) {
|
if (eventListener != null) {
|
||||||
eventListener.onUndo();
|
eventListener.onUndo();
|
||||||
@ -79,24 +148,79 @@ public class ScribbleHud extends FrameLayout {
|
|||||||
|
|
||||||
saveButton.setOnClickListener(v -> {
|
saveButton.setOnClickListener(v -> {
|
||||||
if (eventListener != null) {
|
if (eventListener != null) {
|
||||||
eventListener.onSave();
|
eventListener.onEditComplete(Optional.absent(), Optional.absent());
|
||||||
}
|
}
|
||||||
setMode(Mode.NONE);
|
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 = new ColorPaletteAdapter();
|
||||||
colorPaletteAdapter.setEventListener(colorPicker::setActiveColor);
|
colorPaletteAdapter.setEventListener(colorPicker::setActiveColor);
|
||||||
|
|
||||||
colorPalette.setLayoutManager(new LinearLayoutManager(getContext()));
|
colorPalette.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
colorPalette.setAdapter(colorPaletteAdapter);
|
colorPalette.setAdapter(colorPaletteAdapter);
|
||||||
|
}
|
||||||
|
|
||||||
setMode(Mode.NONE);
|
public void setLocale(@NonNull Locale locale) {
|
||||||
|
this.locale = locale;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTransport(@NonNull Optional<TransportOption> 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<Integer> colors) {
|
public void setColorPalette(@NonNull Set<Integer> colors) {
|
||||||
colorPaletteAdapter.setColors(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) {
|
public void enterMode(@NonNull Mode mode) {
|
||||||
setMode(mode, false);
|
setMode(mode, false);
|
||||||
}
|
}
|
||||||
@ -201,16 +325,44 @@ public class ScribbleHud extends FrameLayout {
|
|||||||
stickerButton.setOnClickListener(v -> setMode(Mode.NONE));
|
stickerButton.setOnClickListener(v -> setMode(Mode.NONE));
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getActiveColor() {
|
private void presentCharactersRemaining() {
|
||||||
return colorPicker.getActiveColor();
|
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) {
|
private void onEmojiToggleClicked(View v) {
|
||||||
colorPicker.setActiveColor(color);
|
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) {
|
@Override
|
||||||
this.eventListener = eventListener;
|
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() {
|
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 {
|
public enum Mode {
|
||||||
NONE, DRAW, HIGHLIGHT, TEXT, STICKER
|
NONE, DRAW, HIGHLIGHT, TEXT, STICKER
|
||||||
}
|
}
|
||||||
@ -245,6 +437,6 @@ public class ScribbleHud extends FrameLayout {
|
|||||||
void onColorChange(int color);
|
void onColorChange(int color);
|
||||||
void onUndo();
|
void onUndo();
|
||||||
void onDelete();
|
void onDelete();
|
||||||
void onSave();
|
void onEditComplete(@NonNull Optional<String> message, @NonNull Optional<TransportOption> transport);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|