mirror of
https://github.com/oxen-io/session-android.git
synced 2025-02-22 05:28:27 +00:00
parent
c4a37e38ab
commit
54a37cc658
@ -11,6 +11,7 @@
|
|||||||
android:label="Access to TextSecure Secrets"
|
android:label="Access to TextSecure Secrets"
|
||||||
android:protectionLevel="signature" />
|
android:protectionLevel="signature" />
|
||||||
|
|
||||||
|
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
||||||
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
|
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
|
||||||
<uses-permission android:name="android.permission.READ_PROFILE"/>
|
<uses-permission android:name="android.permission.READ_PROFILE"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_PROFILE"/>
|
<uses-permission android:name="android.permission.WRITE_PROFILE"/>
|
||||||
|
@ -29,7 +29,7 @@ repositories {
|
|||||||
maven { // textdrawable
|
maven { // textdrawable
|
||||||
url 'https://dl.bintray.com/amulyakhare/maven'
|
url 'https://dl.bintray.com/amulyakhare/maven'
|
||||||
}
|
}
|
||||||
maven {
|
maven { // cwac-camera
|
||||||
url 'https://repo.commonsware.com.s3.amazonaws.com'
|
url 'https://repo.commonsware.com.s3.amazonaws.com'
|
||||||
}
|
}
|
||||||
jcenter()
|
jcenter()
|
||||||
@ -72,7 +72,7 @@ dependencies {
|
|||||||
exclude group: 'com.android.support', module: 'support-v4'
|
exclude group: 'com.android.support', module: 'support-v4'
|
||||||
}
|
}
|
||||||
compile 'com.madgag.spongycastle:prov:1.51.0.0'
|
compile 'com.madgag.spongycastle:prov:1.51.0.0'
|
||||||
compile 'com.commonsware.cwac:camera:0.6.+'
|
compile 'com.commonsware.cwac:camera:0.6.12'
|
||||||
provided 'com.squareup.dagger:dagger-compiler:1.2.2'
|
provided 'com.squareup.dagger:dagger-compiler:1.2.2'
|
||||||
|
|
||||||
compile 'org.whispersystems:jobmanager:0.11.0'
|
compile 'org.whispersystems:jobmanager:0.11.0'
|
||||||
@ -124,6 +124,7 @@ dependencyVerification {
|
|||||||
'com.squareup.dagger:dagger:789aca24537022e49f91fc6444078d9de8f1dd99e1bfb090f18491b186967883',
|
'com.squareup.dagger:dagger:789aca24537022e49f91fc6444078d9de8f1dd99e1bfb090f18491b186967883',
|
||||||
'com.doomonafireball.betterpickers:library:132ecd685c95a99e7377c4e27bfadbb2d7ed0bea995944060cd62d4369fdaf3d',
|
'com.doomonafireball.betterpickers:library:132ecd685c95a99e7377c4e27bfadbb2d7ed0bea995944060cd62d4369fdaf3d',
|
||||||
'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a',
|
'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a',
|
||||||
|
'com.commonsware.cwac:camera:dcc93ddbb2f0393114fa1f31a13fe9e6edfcf5dbe96b22bc4b66c7b15e179054',
|
||||||
'org.whispersystems:jobmanager:ea9cb943c4892fb90c1eea1be30efeb85cefca213d52c788419553b58d0ed70d',
|
'org.whispersystems:jobmanager:ea9cb943c4892fb90c1eea1be30efeb85cefca213d52c788419553b58d0ed70d',
|
||||||
'org.whispersystems:libpastelog:550d33c565380d90f4c671e7b8ed5f3a6da55a9fda468373177106b2eb5220b2',
|
'org.whispersystems:libpastelog:550d33c565380d90f4c671e7b8ed5f3a6da55a9fda468373177106b2eb5220b2',
|
||||||
'com.amulyakhare:com.amulyakhare.textdrawable:54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb',
|
'com.amulyakhare:com.amulyakhare.textdrawable:54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb',
|
||||||
|
@ -1,27 +1,23 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
<item android:state_pressed="true">
|
<item android:state_pressed="true">
|
||||||
<shape
|
<shape android:innerRadiusRatio="3"
|
||||||
android:innerRadiusRatio="3"
|
|
||||||
android:shape="ring"
|
android:shape="ring"
|
||||||
android:thickness="2dp"
|
android:thickness="2dp"
|
||||||
android:useLevel="false">
|
android:useLevel="false">
|
||||||
<solid android:color="@android:color/white" />
|
<solid android:color="@android:color/white" />
|
||||||
<size
|
<size android:height="@dimen/quick_camera_shutter_ring_size" android:width="@dimen/quick_camera_shutter_ring_size" />
|
||||||
android:height="52dp"
|
|
||||||
android:width="52dp" />
|
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
<item>
|
<item>
|
||||||
<shape
|
<shape android:innerRadiusRatio="3"
|
||||||
android:innerRadiusRatio="3"
|
|
||||||
android:shape="ring"
|
android:shape="ring"
|
||||||
android:thickness="2dp"
|
android:thickness="2dp"
|
||||||
android:useLevel="false" >
|
android:useLevel="false" >
|
||||||
<solid android:color="#40ffffff" />
|
<solid android:color="#40ffffff" />
|
||||||
<size
|
<size android:height="@dimen/quick_camera_shutter_ring_size" android:width="@dimen/quick_camera_shutter_ring_size" />
|
||||||
android:height="52dp"
|
|
||||||
android:width="52dp" />
|
|
||||||
</shape>
|
</shape>
|
||||||
</item>
|
</item>
|
||||||
</selector>
|
</selector>
|
@ -1,37 +0,0 @@
|
|||||||
<?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="match_parent"
|
|
||||||
android:gravity="bottom"
|
|
||||||
tools:background="@android:color/darker_gray">
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/shutter_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:background="@drawable/quick_camera_shutter_ring"
|
|
||||||
android:src="@drawable/quick_shutter_button"
|
|
||||||
android:padding="20dp"/>
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/fullscreen_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentBottom="true"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:background="#00000000"
|
|
||||||
android:src="@drawable/quick_camera_fullscreen"
|
|
||||||
android:padding="20dp"/>
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/swap_camera_button"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_alignParentTop="true"
|
|
||||||
android:layout_alignParentRight="true"
|
|
||||||
android:background="#00000000"
|
|
||||||
android:src="@drawable/quick_camera_front"
|
|
||||||
android:padding="20dp"
|
|
||||||
android:visibility="invisible"
|
|
||||||
tools:visibility="visible"/>
|
|
||||||
</RelativeLayout>
|
|
@ -1,5 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
|
<org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
|
||||||
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"
|
||||||
@ -10,7 +9,7 @@
|
|||||||
android:background="?conversation_background"
|
android:background="?conversation_background"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.QuickAttachmentDrawer
|
<org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:id="@+id/quick_attachment_drawer"
|
android:id="@+id/quick_attachment_drawer"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@ -99,15 +98,17 @@
|
|||||||
android:nextFocusForward="@+id/send_button"
|
android:nextFocusForward="@+id/send_button"
|
||||||
android:nextFocusRight="@+id/send_button"
|
android:nextFocusRight="@+id/send_button"
|
||||||
tools:hint="Send TextSecure message" />
|
tools:hint="Send TextSecure message" />
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<ImageButton android:id="@+id/quick_attachment_toggle"
|
<org.thoughtcrime.securesms.components.camera.HidingImageButton
|
||||||
android:layout_width="wrap_content"
|
android:id="@+id/quick_attachment_toggle"
|
||||||
android:layout_height="44dp"
|
android:layout_width="37dp"
|
||||||
|
android:layout_height="37dp"
|
||||||
|
android:layout_gravity="bottom"
|
||||||
android:src="?quick_camera_icon"
|
android:src="?quick_camera_icon"
|
||||||
android:background="@drawable/touch_highlight_background"
|
android:background="@drawable/touch_highlight_background"
|
||||||
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_toggle_description"
|
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_toggle_description"
|
||||||
android:padding="10dp"/>
|
android:padding="10dp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<org.thoughtcrime.securesms.components.AnimatingToggle
|
<org.thoughtcrime.securesms.components.AnimatingToggle
|
||||||
android:id="@+id/button_toggle"
|
android:id="@+id/button_toggle"
|
||||||
@ -156,5 +157,5 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
</org.thoughtcrime.securesms.components.QuickAttachmentDrawer>
|
</org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer>
|
||||||
</org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout>
|
</org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout>
|
||||||
|
9
res/layout/quick_attachment_drawer.xml
Normal file
9
res/layout/quick_attachment_drawer.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<merge xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
|
||||||
|
<org.thoughtcrime.securesms.components.camera.QuickCamera
|
||||||
|
android:id="@+id/quick_camera"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
|
</merge>
|
@ -1,12 +1,13 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<RelativeLayout 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/controls"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:gravity="bottom"
|
android:gravity="bottom"
|
||||||
tools:background="@android:color/darker_gray">
|
tools:background="@android:color/darker_gray">
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/shutter_button"
|
<ImageButton android:id="@+id/shutter_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
@ -14,8 +15,8 @@
|
|||||||
android:background="@drawable/quick_camera_shutter_ring"
|
android:background="@drawable/quick_camera_shutter_ring"
|
||||||
android:src="@drawable/quick_shutter_button"
|
android:src="@drawable/quick_shutter_button"
|
||||||
android:padding="20dp"/>
|
android:padding="20dp"/>
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/fullscreen_button"
|
<ImageButton android:id="@+id/fullscreen_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
@ -23,8 +24,8 @@
|
|||||||
android:background="#00000000"
|
android:background="#00000000"
|
||||||
android:src="@drawable/quick_camera_fullscreen"
|
android:src="@drawable/quick_camera_fullscreen"
|
||||||
android:padding="20dp" />
|
android:padding="20dp" />
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/swap_camera_button"
|
<ImageButton android:id="@+id/swap_camera_button"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_alignParentBottom="true"
|
android:layout_alignParentBottom="true"
|
||||||
@ -34,4 +35,5 @@
|
|||||||
android:padding="20dp"
|
android:padding="20dp"
|
||||||
android:visibility="invisible"
|
android:visibility="invisible"
|
||||||
tools:visibility="visible" />
|
tools:visibility="visible" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
38
res/layout/quick_camera_controls_land.xml
Normal file
38
res/layout/quick_camera_controls_land.xml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?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:id="@+id/controls"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:gravity="bottom"
|
||||||
|
tools:background="@android:color/darker_gray">
|
||||||
|
|
||||||
|
<ImageButton android:id="@+id/shutter_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:background="@drawable/quick_camera_shutter_ring"
|
||||||
|
android:src="@drawable/quick_shutter_button"
|
||||||
|
android:padding="20dp"/>
|
||||||
|
|
||||||
|
<ImageButton android:id="@+id/fullscreen_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:background="#00000000"
|
||||||
|
android:src="@drawable/quick_camera_fullscreen"
|
||||||
|
android:padding="20dp"/>
|
||||||
|
|
||||||
|
<ImageButton android:id="@+id/swap_camera_button"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:background="#00000000"
|
||||||
|
android:src="@drawable/quick_camera_front"
|
||||||
|
android:padding="20dp"
|
||||||
|
android:visibility="invisible"
|
||||||
|
tools:visibility="visible"/>
|
||||||
|
</RelativeLayout>
|
@ -22,11 +22,12 @@
|
|||||||
<dimen name="media_bubble_height">210dp</dimen>
|
<dimen name="media_bubble_height">210dp</dimen>
|
||||||
<dimen name="media_bubble_border_width">3dp</dimen>
|
<dimen name="media_bubble_border_width">3dp</dimen>
|
||||||
|
|
||||||
|
|
||||||
<integer name="media_overview_cols">3</integer>
|
<integer name="media_overview_cols">3</integer>
|
||||||
<dimen name="message_details_table_row_pad">10dp</dimen>
|
<dimen name="message_details_table_row_pad">10dp</dimen>
|
||||||
|
|
||||||
<dimen name="color_grid_extra_padding">32dp</dimen>
|
<dimen name="color_grid_extra_padding">32dp</dimen>
|
||||||
<dimen name="color_grid_item_size">48dp</dimen>
|
<dimen name="color_grid_item_size">48dp</dimen>
|
||||||
|
|
||||||
<dimen name="quick_media_drawer_default_height">250dp</dimen>
|
<dimen name="quick_media_drawer_default_height">250dp</dimen>
|
||||||
|
<dimen name="quick_camera_shutter_ring_size">52dp</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
@ -512,7 +512,7 @@
|
|||||||
<string name="conversation_title_view__conversation_muted">Conversation muted</string>
|
<string name="conversation_title_view__conversation_muted">Conversation muted</string>
|
||||||
|
|
||||||
<!-- conversation_activity -->
|
<!-- conversation_activity -->
|
||||||
<string name="conversation_activity__type_message_push">Send TextSecure message</string>
|
<string name="conversation_activity__type_message_push">Send via TextSecure</string>
|
||||||
<string name="conversation_activity__type_message_sms_insecure">Send unsecured SMS</string>
|
<string name="conversation_activity__type_message_sms_insecure">Send unsecured SMS</string>
|
||||||
<string name="conversation_activity__type_message_mms_insecure">Send unsecured MMS</string>
|
<string name="conversation_activity__type_message_mms_insecure">Send unsecured MMS</string>
|
||||||
<string name="conversation_activity__send">Send</string>
|
<string name="conversation_activity__send">Send</string>
|
||||||
|
@ -40,7 +40,6 @@
|
|||||||
<item name="ic_arrow_forward">@drawable/ic_arrow_forward_dark</item>
|
<item name="ic_arrow_forward">@drawable/ic_arrow_forward_dark</item>
|
||||||
<item name="lockscreen_watermark">@drawable/lockscreen_watermark_dark</item>
|
<item name="lockscreen_watermark">@drawable/lockscreen_watermark_dark</item>
|
||||||
<item name="android:windowBackground">@color/black</item>
|
<item name="android:windowBackground">@color/black</item>
|
||||||
<item name="conversation_background">@color/black</item>
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style name="PopupAnimation" parent="@android:style/Animation">
|
<style name="PopupAnimation" parent="@android:style/Animation">
|
||||||
|
@ -28,6 +28,8 @@ import android.graphics.Color;
|
|||||||
import android.graphics.PorterDuff;
|
import android.graphics.PorterDuff;
|
||||||
import android.graphics.PorterDuff.Mode;
|
import android.graphics.PorterDuff.Mode;
|
||||||
import android.graphics.drawable.ColorDrawable;
|
import android.graphics.drawable.ColorDrawable;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
@ -39,7 +41,6 @@ import android.text.Editable;
|
|||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
import android.text.TextWatcher;
|
import android.text.TextWatcher;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.Gravity;
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
@ -71,9 +72,10 @@ import org.thoughtcrime.securesms.components.emoji.EmojiPopup;
|
|||||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||||
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
||||||
import org.thoughtcrime.securesms.components.QuickAttachmentDrawer;
|
import org.thoughtcrime.securesms.components.camera.HidingImageButton;
|
||||||
import org.thoughtcrime.securesms.components.QuickCamera;
|
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.AttachmentDrawerListener;
|
||||||
import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
|
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.DrawerState;
|
||||||
|
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
import org.thoughtcrime.securesms.crypto.MasterCipher;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
import org.thoughtcrime.securesms.crypto.SecurityEvent;
|
||||||
@ -118,8 +120,6 @@ import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
|
|||||||
import org.whispersystems.libaxolotl.InvalidMessageException;
|
import org.whispersystems.libaxolotl.InvalidMessageException;
|
||||||
import org.whispersystems.libaxolotl.util.guava.Optional;
|
import org.whispersystems.libaxolotl.util.guava.Optional;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
@ -140,7 +140,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
implements ConversationFragment.ConversationFragmentListener,
|
implements ConversationFragment.ConversationFragmentListener,
|
||||||
AttachmentManager.AttachmentListener,
|
AttachmentManager.AttachmentListener,
|
||||||
RecipientsModifiedListener,
|
RecipientsModifiedListener,
|
||||||
OnKeyboardShownListener
|
OnKeyboardShownListener,
|
||||||
|
AttachmentDrawerListener
|
||||||
{
|
{
|
||||||
private static final String TAG = ConversationActivity.class.getSimpleName();
|
private static final String TAG = ConversationActivity.class.getSimpleName();
|
||||||
|
|
||||||
@ -157,7 +158,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
private static final int PICK_AUDIO = 3;
|
private static final int PICK_AUDIO = 3;
|
||||||
private static final int PICK_CONTACT_INFO = 4;
|
private static final int PICK_CONTACT_INFO = 4;
|
||||||
private static final int GROUP_EDIT = 5;
|
private static final int GROUP_EDIT = 5;
|
||||||
private static final int CAPTURE_PHOTO = 6;
|
|
||||||
|
|
||||||
private MasterSecret masterSecret;
|
private MasterSecret masterSecret;
|
||||||
protected ComposeText composeText;
|
protected ComposeText composeText;
|
||||||
@ -178,7 +178,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
private BroadcastReceiver groupUpdateReceiver;
|
private BroadcastReceiver groupUpdateReceiver;
|
||||||
private Optional<EmojiPopup> emojiPopup = Optional.absent();
|
private Optional<EmojiPopup> emojiPopup = Optional.absent();
|
||||||
private EmojiToggle emojiToggle;
|
private EmojiToggle emojiToggle;
|
||||||
private ImageButton quickAttachmentToggle;
|
private HidingImageButton quickAttachmentToggle;
|
||||||
private QuickAttachmentDrawer quickAttachmentDrawer;
|
private QuickAttachmentDrawer quickAttachmentDrawer;
|
||||||
|
|
||||||
private Recipients recipients;
|
private Recipients recipients;
|
||||||
@ -262,6 +262,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
quickAttachmentDrawer.onPause();
|
quickAttachmentDrawer.onPause();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override public void onConfigurationChanged(Configuration newConfig) {
|
||||||
|
super.onConfigurationChanged(newConfig);
|
||||||
|
Log.w(TAG, String.format("onConfigurationChanged(%d -> %d)", getResources().getConfiguration().orientation, newConfig.orientation));
|
||||||
|
quickAttachmentDrawer.onConfigurationChanged();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onDestroy() {
|
protected void onDestroy() {
|
||||||
saveDraft();
|
saveDraft();
|
||||||
@ -276,11 +282,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
Log.w(TAG, "onActivityResult called: " + reqCode + ", " + resultCode + " , " + data);
|
Log.w(TAG, "onActivityResult called: " + reqCode + ", " + resultCode + " , " + data);
|
||||||
super.onActivityResult(reqCode, resultCode, data);
|
super.onActivityResult(reqCode, resultCode, data);
|
||||||
|
|
||||||
if ((data == null && reqCode != CAPTURE_PHOTO) || resultCode != RESULT_OK) return;
|
if (data == null || resultCode != RESULT_OK) return;
|
||||||
|
|
||||||
switch (reqCode) {
|
switch (reqCode) {
|
||||||
case PICK_IMAGE:
|
case PICK_IMAGE:
|
||||||
addAttachmentImage(data.getData());
|
addAttachmentImage(masterSecret, data.getData());
|
||||||
break;
|
break;
|
||||||
case PICK_VIDEO:
|
case PICK_VIDEO:
|
||||||
addAttachmentVideo(data.getData());
|
addAttachmentVideo(data.getData());
|
||||||
@ -291,11 +297,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
case PICK_CONTACT_INFO:
|
case PICK_CONTACT_INFO:
|
||||||
addAttachmentContactInfo(data.getData());
|
addAttachmentContactInfo(data.getData());
|
||||||
break;
|
break;
|
||||||
case CAPTURE_PHOTO:
|
|
||||||
if (attachmentManager.getCaptureFile() != null) {
|
|
||||||
addAttachmentImage(Uri.fromFile(attachmentManager.getCaptureFile()));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case GROUP_EDIT:
|
case GROUP_EDIT:
|
||||||
this.recipients = RecipientFactory.getRecipientsForIds(this, data.getLongArrayExtra(GroupCreateActivity.GROUP_RECIPIENT_EXTRA), true);
|
this.recipients = RecipientFactory.getRecipientsForIds(this, data.getLongArrayExtra(GroupCreateActivity.GROUP_RECIPIENT_EXTRA), true);
|
||||||
titleView.setTitle(recipients);
|
titleView.setTitle(recipients);
|
||||||
@ -377,8 +378,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
if (isEmojiDrawerOpen()) {
|
if (isEmojiDrawerOpen()) {
|
||||||
hideEmojiPopup(false);
|
hideEmojiPopup(false);
|
||||||
} else if (quickAttachmentDrawer.getDrawerState() != QuickAttachmentDrawer.COLLAPSED) {
|
} else if (quickAttachmentDrawer.isOpen()) {
|
||||||
quickAttachmentDrawer.setDrawerStateAndAnimate(QuickAttachmentDrawer.COLLAPSED);
|
quickAttachmentDrawer.close();
|
||||||
} else {
|
} else {
|
||||||
super.onBackPressed();
|
super.onBackPressed();
|
||||||
}
|
}
|
||||||
@ -666,7 +667,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
Uri draftVideo = getIntent().getParcelableExtra(DRAFT_VIDEO_EXTRA);
|
Uri draftVideo = getIntent().getParcelableExtra(DRAFT_VIDEO_EXTRA);
|
||||||
|
|
||||||
if (draftText != null) composeText.setText(draftText);
|
if (draftText != null) composeText.setText(draftText);
|
||||||
if (draftImage != null) addAttachmentImage(draftImage);
|
if (draftImage != null) addAttachmentImage(masterSecret, draftImage);
|
||||||
if (draftAudio != null) addAttachmentAudio(draftAudio);
|
if (draftAudio != null) addAttachmentAudio(draftAudio);
|
||||||
if (draftVideo != null) addAttachmentVideo(draftVideo);
|
if (draftVideo != null) addAttachmentVideo(draftVideo);
|
||||||
|
|
||||||
@ -702,13 +703,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
if (draft.getType().equals(Draft.TEXT)) {
|
if (draft.getType().equals(Draft.TEXT)) {
|
||||||
composeText.setText(draft.getValue());
|
composeText.setText(draft.getValue());
|
||||||
} else if (draft.getType().equals(Draft.IMAGE)) {
|
} else if (draft.getType().equals(Draft.IMAGE)) {
|
||||||
addAttachmentImage(Uri.parse(draft.getValue()));
|
addAttachmentImage(masterSecret, Uri.parse(draft.getValue()));
|
||||||
} else if (draft.getType().equals(Draft.AUDIO)) {
|
} else if (draft.getType().equals(Draft.AUDIO)) {
|
||||||
addAttachmentAudio(Uri.parse(draft.getValue()));
|
addAttachmentAudio(Uri.parse(draft.getValue()));
|
||||||
} else if (draft.getType().equals(Draft.VIDEO)) {
|
} else if (draft.getType().equals(Draft.VIDEO)) {
|
||||||
addAttachmentVideo(Uri.parse(draft.getValue()));
|
addAttachmentVideo(Uri.parse(draft.getValue()));
|
||||||
} else if (draft.getType().equals(Draft.ENCRYPTED_IMAGE)) {
|
|
||||||
addAttachmentEncryptedImage(Uri.parse(draft.getValue()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -791,7 +790,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
unblockButton = (Button) findViewById(R.id.unblock_button);
|
unblockButton = (Button) findViewById(R.id.unblock_button);
|
||||||
composePanel = findViewById(R.id.bottom_panel);
|
composePanel = findViewById(R.id.bottom_panel);
|
||||||
quickAttachmentDrawer = (QuickAttachmentDrawer) findViewById(R.id.quick_attachment_drawer);
|
quickAttachmentDrawer = (QuickAttachmentDrawer) findViewById(R.id.quick_attachment_drawer);
|
||||||
quickAttachmentToggle = (ImageButton) findViewById(R.id.quick_attachment_toggle);
|
quickAttachmentToggle = (HidingImageButton) findViewById(R.id.quick_attachment_toggle);
|
||||||
|
|
||||||
int[] attributes = new int[]{R.attr.conversation_item_bubble_background};
|
int[] attributes = new int[]{R.attr.conversation_item_bubble_background};
|
||||||
TypedArray colors = obtainStyledAttributes(attributes);
|
TypedArray colors = obtainStyledAttributes(attributes);
|
||||||
@ -842,11 +841,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
composeText.setOnFocusChangeListener(composeKeyPressedListener);
|
composeText.setOnFocusChangeListener(composeKeyPressedListener);
|
||||||
emojiToggle.setOnClickListener(new EmojiToggleListener());
|
emojiToggle.setOnClickListener(new EmojiToggleListener());
|
||||||
|
|
||||||
if (quickAttachmentDrawer.hasCamera()) {
|
if (QuickAttachmentDrawer.isDeviceSupported(this)) {
|
||||||
QuickAttachmentDrawerToggleListener listener = new QuickAttachmentDrawerToggleListener();
|
quickAttachmentDrawer.setListener(this);
|
||||||
quickAttachmentDrawer.setQuickAttachmentDrawerListener(listener);
|
quickAttachmentToggle.setOnClickListener(new QuickAttachmentToggleListener());
|
||||||
quickAttachmentDrawer.setQuickCameraListener(listener);
|
|
||||||
quickAttachmentToggle.setOnClickListener(listener);
|
|
||||||
} else {
|
} else {
|
||||||
quickAttachmentToggle.setVisibility(View.GONE);
|
quickAttachmentToggle.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
@ -957,8 +954,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
private void addAttachment(int type) {
|
private void addAttachment(int type) {
|
||||||
Log.w("ComposeMessageActivity", "Selected: " + type);
|
Log.w("ComposeMessageActivity", "Selected: " + type);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case AttachmentTypeSelectorAdapter.TAKE_PHOTO:
|
|
||||||
attachmentManager.capturePhoto(this, CAPTURE_PHOTO); break;
|
|
||||||
case AttachmentTypeSelectorAdapter.ADD_IMAGE:
|
case AttachmentTypeSelectorAdapter.ADD_IMAGE:
|
||||||
AttachmentManager.selectImage(this, PICK_IMAGE); break;
|
AttachmentManager.selectImage(this, PICK_IMAGE); break;
|
||||||
case AttachmentTypeSelectorAdapter.ADD_VIDEO:
|
case AttachmentTypeSelectorAdapter.ADD_VIDEO:
|
||||||
@ -970,20 +965,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void addAttachmentEncryptedImage(Uri uri) {
|
private void addAttachmentImage(MasterSecret masterSecret, Uri imageUri) {
|
||||||
try {
|
try {
|
||||||
attachmentManager.setEncryptedImage(uri, masterSecret);
|
attachmentManager.setImage(masterSecret, imageUri);
|
||||||
} catch (IOException | BitmapDecodingException e) {
|
|
||||||
Log.w(TAG, e);
|
|
||||||
attachmentManager.clear();
|
|
||||||
Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
|
|
||||||
Toast.LENGTH_LONG).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void addAttachmentImage(Uri imageUri) {
|
|
||||||
try {
|
|
||||||
attachmentManager.setImage(imageUri);
|
|
||||||
} catch (IOException | BitmapDecodingException e) {
|
} catch (IOException | BitmapDecodingException e) {
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
attachmentManager.clear();
|
attachmentManager.clear();
|
||||||
@ -1065,13 +1049,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (Slide slide : attachmentManager.getSlideDeck().getSlides()) {
|
for (Slide slide : attachmentManager.getSlideDeck().getSlides()) {
|
||||||
String draftType = null;
|
if (slide.hasAudio()) drafts.add(new Draft(Draft.AUDIO, slide.getUri().toString()));
|
||||||
if (slide.hasAudio()) draftType = Draft.AUDIO;
|
else if (slide.hasVideo()) drafts.add(new Draft(Draft.VIDEO, slide.getUri().toString()));
|
||||||
else if (slide.hasVideo()) draftType = Draft.VIDEO;
|
else if (slide.hasImage()) drafts.add(new Draft(Draft.IMAGE, slide.getUri().toString()));
|
||||||
else if (slide.hasImage()) draftType = slide.isEncrypted() ? Draft.ENCRYPTED_IMAGE : Draft.IMAGE;
|
|
||||||
|
|
||||||
if (draftType != null)
|
|
||||||
drafts.add(new Draft(draftType, slide.getUri().toString()));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return drafts;
|
return drafts;
|
||||||
@ -1317,11 +1297,29 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
private void updateToggleButtonState() {
|
private void updateToggleButtonState() {
|
||||||
if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) {
|
if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) {
|
||||||
buttonToggle.display(attachButton);
|
buttonToggle.display(attachButton);
|
||||||
|
quickAttachmentToggle.show();
|
||||||
} else {
|
} else {
|
||||||
buttonToggle.display(sendButton);
|
buttonToggle.display(sendButton);
|
||||||
|
quickAttachmentToggle.hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttachmentDrawerClosed() {
|
||||||
|
getSupportActionBar().show();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAttachmentDrawerOpened() {
|
||||||
|
getSupportActionBar().hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onImageCapture(@NonNull final Bitmap bitmap) {
|
||||||
|
attachmentManager.setCaptureImage(masterSecret, bitmap);
|
||||||
|
quickAttachmentDrawer.close();
|
||||||
|
}
|
||||||
|
|
||||||
// Listeners
|
// Listeners
|
||||||
|
|
||||||
private class AttachmentTypeListener implements DialogInterface.OnClickListener {
|
private class AttachmentTypeListener implements DialogInterface.OnClickListener {
|
||||||
@ -1347,68 +1345,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
input.hideSoftInputFromWindow(composeText.getWindowToken(), 0);
|
input.hideSoftInputFromWindow(composeText.getWindowToken(), 0);
|
||||||
quickAttachmentDrawer.setDrawerStateAndAnimate(QuickAttachmentDrawer.COLLAPSED);
|
quickAttachmentDrawer.setDrawerStateAndAnimate(DrawerState.COLLAPSED);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class QuickAttachmentDrawerToggleListener implements OnClickListener,
|
private class QuickAttachmentToggleListener implements OnClickListener {
|
||||||
QuickAttachmentDrawer.QuickAttachmentDrawerListener,
|
|
||||||
QuickCamera.QuickCameraListener {
|
|
||||||
@QuickAttachmentDrawer.DrawerState int nextDrawerState = QuickAttachmentDrawer.HALF_EXPANDED;
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(View v) {
|
||||||
InputMethodManager input = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
InputMethodManager input = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||||
input.hideSoftInputFromWindow(composeText.getWindowToken(), 0);
|
input.hideSoftInputFromWindow(composeText.getWindowToken(), 0);
|
||||||
composeText.clearFocus();
|
composeText.clearFocus();
|
||||||
hideEmojiPopup(false);
|
hideEmojiPopup(false);
|
||||||
quickAttachmentDrawer.setDrawerStateAndAnimate(nextDrawerState);
|
quickAttachmentDrawer.open();
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCollapsed() {
|
|
||||||
getSupportActionBar().show();
|
|
||||||
nextDrawerState = QuickAttachmentDrawer.HALF_EXPANDED;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onExpanded() {
|
|
||||||
getSupportActionBar().hide();
|
|
||||||
nextDrawerState = QuickAttachmentDrawer.COLLAPSED;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onHalfExpanded() {
|
|
||||||
getSupportActionBar().hide();
|
|
||||||
nextDrawerState = QuickAttachmentDrawer.COLLAPSED;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onImageCapture(final byte[] data) {
|
|
||||||
quickAttachmentDrawer.setDrawerStateAndAnimate(QuickAttachmentDrawer.COLLAPSED);
|
|
||||||
new AsyncTask<Void, Void, Uri>() {
|
|
||||||
@Override
|
|
||||||
protected Uri doInBackground(Void... voids) {
|
|
||||||
try {
|
|
||||||
File tempDirectory = getDir("media", Context.MODE_PRIVATE);
|
|
||||||
File tempFile = File.createTempFile("image", ".jpg", tempDirectory);
|
|
||||||
FileOutputStream fileOutputStream = new EncryptingPartOutputStream(tempFile, masterSecret);
|
|
||||||
fileOutputStream.write(data);
|
|
||||||
fileOutputStream.close();
|
|
||||||
return Uri.fromFile(tempFile);
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(Uri uri) {
|
|
||||||
if (uri != null)
|
|
||||||
addAttachmentEncryptedImage(uri);
|
|
||||||
}
|
|
||||||
}.execute();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1484,8 +1433,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||||||
public void onFocusChange(View v, boolean hasFocus) {
|
public void onFocusChange(View v, boolean hasFocus) {
|
||||||
if (hasFocus && isEmojiDrawerOpen()) {
|
if (hasFocus && isEmojiDrawerOpen()) {
|
||||||
hideEmojiPopup(true);
|
hideEmojiPopup(true);
|
||||||
} else if (hasFocus && quickAttachmentDrawer.getDrawerState() != QuickAttachmentDrawer.COLLAPSED) {
|
} else if (hasFocus && quickAttachmentDrawer.isOpen()) {
|
||||||
quickAttachmentDrawer.setDrawerStateAndAnimate(QuickAttachmentDrawer.COLLAPSED);
|
quickAttachmentDrawer.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
package org.thoughtcrime.securesms;
|
package org.thoughtcrime.securesms;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.os.Build;
|
import android.os.Build.VERSION;
|
||||||
|
import android.os.Build.VERSION_CODES;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
import android.support.v4.app.ActivityOptionsCompat;
|
import android.support.v4.app.ActivityOptionsCompat;
|
||||||
@ -84,7 +85,7 @@ public class ConversationPopupActivity extends ConversationActivity {
|
|||||||
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, getRecipients().getIds());
|
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, getRecipients().getIds());
|
||||||
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, result);
|
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, result);
|
||||||
|
|
||||||
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
|
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
|
||||||
startActivity(intent, transition.toBundle());
|
startActivity(intent, transition.toBundle());
|
||||||
} else {
|
} else {
|
||||||
startActivity(intent);
|
startActivity(intent);
|
||||||
|
@ -1,515 +0,0 @@
|
|||||||
/***
|
|
||||||
Copyright (c) 2013-2014 CommonsWare, LLC
|
|
||||||
Portions Copyright (C) 2007 The Android Open Source Project
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
||||||
not use this file except in compliance with the License. You may obtain
|
|
||||||
a copy of the License at
|
|
||||||
http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
Unless required by applicable law or agreed to in writing, software
|
|
||||||
distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
See the License for the specific language governing permissions and
|
|
||||||
limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.app.Activity;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.ActivityInfo;
|
|
||||||
import android.hardware.Camera;
|
|
||||||
import android.hardware.Camera.AutoFocusCallback;
|
|
||||||
import android.hardware.Camera.PreviewCallback;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.util.DisplayMetrics;
|
|
||||||
import android.util.Log;
|
|
||||||
import android.view.OrientationEventListener;
|
|
||||||
import android.view.Surface;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import com.commonsware.cwac.camera.CameraHost;
|
|
||||||
import com.commonsware.cwac.camera.CameraHost.FailureReason;
|
|
||||||
import com.commonsware.cwac.camera.CameraHostProvider;
|
|
||||||
import com.commonsware.cwac.camera.PreviewStrategy;
|
|
||||||
|
|
||||||
public class CameraView extends ViewGroup implements AutoFocusCallback {
|
|
||||||
static final String TAG = "CWAC-Camera";
|
|
||||||
private PreviewStrategy previewStrategy;
|
|
||||||
private Camera.Size previewSize;
|
|
||||||
private Camera camera = null;
|
|
||||||
private boolean inPreview = false;
|
|
||||||
private CameraHost host = null;
|
|
||||||
private OnOrientationChange onOrientationChange = null;
|
|
||||||
private int displayOrientation = -1;
|
|
||||||
private int outputOrientation = -1;
|
|
||||||
private int cameraId = -1;
|
|
||||||
private boolean isAutoFocusing = false;
|
|
||||||
private int lastPictureOrientation = -1;
|
|
||||||
|
|
||||||
public CameraView(Context context) {
|
|
||||||
super(context);
|
|
||||||
|
|
||||||
onOrientationChange = new OnOrientationChange(context.getApplicationContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
public CameraView(Context context, AttributeSet attrs) {
|
|
||||||
this(context, attrs, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public CameraView(Context context, AttributeSet attrs, int defStyle) {
|
|
||||||
super(context, attrs, defStyle);
|
|
||||||
|
|
||||||
onOrientationChange = new OnOrientationChange(context.getApplicationContext());
|
|
||||||
|
|
||||||
if (context instanceof CameraHostProvider) {
|
|
||||||
setHost(((CameraHostProvider)context).getCameraHost());
|
|
||||||
} else {
|
|
||||||
throw new IllegalArgumentException("To use the two- or "
|
|
||||||
+ "three-parameter constructors on CameraView, "
|
|
||||||
+ "your activity needs to implement the "
|
|
||||||
+ "CameraHostProvider interface");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public CameraHost getHost() {
|
|
||||||
return (host);
|
|
||||||
}
|
|
||||||
|
|
||||||
// must call this after constructor, before onResume()
|
|
||||||
|
|
||||||
public void setHost(CameraHost host) {
|
|
||||||
this.host = host;
|
|
||||||
|
|
||||||
if (host.getDeviceProfile().useTextureView()) {
|
|
||||||
previewStrategy = new TexturePreviewStrategy(this);
|
|
||||||
} else {
|
|
||||||
previewStrategy = new SurfacePreviewStrategy(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
|
||||||
public void onResume() {
|
|
||||||
addView(previewStrategy.getWidget());
|
|
||||||
|
|
||||||
if (camera == null) {
|
|
||||||
try {
|
|
||||||
cameraId = getHost().getCameraId();
|
|
||||||
|
|
||||||
if (cameraId >= 0) {
|
|
||||||
camera = Camera.open(cameraId);
|
|
||||||
|
|
||||||
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
|
|
||||||
onOrientationChange.enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
setCameraDisplayOrientation();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
getHost().onCameraFail(FailureReason.NO_CAMERAS_REPORTED);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
getHost().onCameraFail(FailureReason.UNKNOWN);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onPause() {
|
|
||||||
if (camera != null) {
|
|
||||||
previewDestroyed();
|
|
||||||
}
|
|
||||||
|
|
||||||
removeView(previewStrategy.getWidget());
|
|
||||||
onOrientationChange.disable();
|
|
||||||
lastPictureOrientation=-1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// based on CameraPreview.java from ApiDemos
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
||||||
final int width=
|
|
||||||
resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
|
|
||||||
final int height=
|
|
||||||
resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);
|
|
||||||
setMeasuredDimension(width, height);
|
|
||||||
|
|
||||||
if (width > 0 && height > 0) {
|
|
||||||
if (camera != null) {
|
|
||||||
Camera.Size newSize=null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (getHost().getRecordingHint() != CameraHost.RecordingHint.STILL_ONLY) {
|
|
||||||
|
|
||||||
newSize=
|
|
||||||
getHost().getPreferredPreviewSizeForVideo(getDisplayOrientation(),
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
camera.getParameters(),
|
|
||||||
null);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newSize == null || newSize.width * newSize.height < 65536) {
|
|
||||||
newSize=
|
|
||||||
getHost().getPreviewSize(getDisplayOrientation(),
|
|
||||||
width, height,
|
|
||||||
camera.getParameters());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
android.util.Log.e(getClass().getSimpleName(),
|
|
||||||
"Could not work with camera parameters?",
|
|
||||||
e);
|
|
||||||
// TODO get this out to library clients
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newSize != null) {
|
|
||||||
if (previewSize == null) {
|
|
||||||
previewSize=newSize;
|
|
||||||
}
|
|
||||||
else if (previewSize.width != newSize.width
|
|
||||||
|| previewSize.height != newSize.height) {
|
|
||||||
if (inPreview) {
|
|
||||||
stopPreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
previewSize=newSize;
|
|
||||||
initPreview(width, height, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// based on CameraPreview.java from ApiDemos
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
||||||
if (changed && getChildCount() > 0) {
|
|
||||||
final View child=getChildAt(0);
|
|
||||||
final int width=r - l;
|
|
||||||
final int height=b - t;
|
|
||||||
int previewWidth=width;
|
|
||||||
int previewHeight=height;
|
|
||||||
|
|
||||||
// handle orientation
|
|
||||||
|
|
||||||
if (previewSize != null) {
|
|
||||||
if (getDisplayOrientation() == 90
|
|
||||||
|| getDisplayOrientation() == 270) {
|
|
||||||
previewWidth=previewSize.height;
|
|
||||||
previewHeight=previewSize.width;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
previewWidth=previewSize.width;
|
|
||||||
previewHeight=previewSize.height;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean useFirstStrategy=
|
|
||||||
(width * previewHeight > height * previewWidth);
|
|
||||||
boolean useFullBleed=getHost().useFullBleedPreview();
|
|
||||||
|
|
||||||
if ((useFirstStrategy && !useFullBleed)
|
|
||||||
|| (!useFirstStrategy && useFullBleed)) {
|
|
||||||
final int scaledChildWidth=
|
|
||||||
previewWidth * height / previewHeight;
|
|
||||||
child.layout((width - scaledChildWidth) / 2, 0,
|
|
||||||
(width + scaledChildWidth) / 2, height);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
final int scaledChildHeight=
|
|
||||||
previewHeight * width / previewWidth;
|
|
||||||
child.layout(0, (height - scaledChildHeight) / 2, width,
|
|
||||||
(height + scaledChildHeight) / 2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getDisplayOrientation() {
|
|
||||||
return(displayOrientation);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void lockToLandscape(boolean enable) {
|
|
||||||
if (enable) {
|
|
||||||
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
|
||||||
onOrientationChange.enable();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
|
||||||
onOrientationChange.disable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void restartPreview() {
|
|
||||||
if (!inPreview) {
|
|
||||||
startPreview();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void autoFocus() {
|
|
||||||
if (inPreview) {
|
|
||||||
camera.autoFocus(this);
|
|
||||||
isAutoFocusing=true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void cancelAutoFocus() {
|
|
||||||
camera.cancelAutoFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAutoFocusAvailable() {
|
|
||||||
return(inPreview);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onAutoFocus(boolean success, Camera camera) {
|
|
||||||
isAutoFocusing=false;
|
|
||||||
|
|
||||||
if (getHost() instanceof AutoFocusCallback) {
|
|
||||||
getHost().onAutoFocus(success, camera);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getFlashMode() {
|
|
||||||
return(camera.getParameters().getFlashMode());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setFlashMode(String mode) {
|
|
||||||
if (camera != null) {
|
|
||||||
Camera.Parameters params=camera.getParameters();
|
|
||||||
|
|
||||||
params.setFlashMode(mode);
|
|
||||||
camera.setParameters(params);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOneShotPreviewCallback(PreviewCallback callback) {
|
|
||||||
if (camera != null)
|
|
||||||
camera.setOneShotPreviewCallback(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Camera.Parameters getCameraParameters() {
|
|
||||||
return camera.getParameters();
|
|
||||||
}
|
|
||||||
|
|
||||||
void previewCreated() {
|
|
||||||
if (camera != null) {
|
|
||||||
try {
|
|
||||||
previewStrategy.attach(camera);
|
|
||||||
}
|
|
||||||
catch (IOException e) {
|
|
||||||
getHost().handleException(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void previewDestroyed() {
|
|
||||||
if (camera != null) {
|
|
||||||
previewStopped();
|
|
||||||
camera.release();
|
|
||||||
camera=null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void previewReset(int width, int height) {
|
|
||||||
previewStopped();
|
|
||||||
initPreview(width, height);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void previewStopped() {
|
|
||||||
if (inPreview) {
|
|
||||||
stopPreview();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initPreview(int w, int h) {
|
|
||||||
initPreview(w, h, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
|
||||||
public void initPreview(int w, int h, boolean firstRun) {
|
|
||||||
if (camera != null) {
|
|
||||||
Camera.Parameters parameters=camera.getParameters();
|
|
||||||
|
|
||||||
parameters.setPreviewSize(previewSize.width, previewSize.height);
|
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
|
||||||
parameters.setRecordingHint(getHost().getRecordingHint() != CameraHost.RecordingHint.STILL_ONLY);
|
|
||||||
}
|
|
||||||
|
|
||||||
requestLayout();
|
|
||||||
|
|
||||||
camera.setParameters(getHost().adjustPreviewParameters(parameters));
|
|
||||||
startPreview();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void startPreview() {
|
|
||||||
camera.startPreview();
|
|
||||||
inPreview=true;
|
|
||||||
getHost().autoFocusAvailable();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopPreview() {
|
|
||||||
inPreview=false;
|
|
||||||
getHost().autoFocusUnavailable();
|
|
||||||
camera.stopPreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
// based on
|
|
||||||
// http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
|
|
||||||
// and http://stackoverflow.com/a/10383164/115145
|
|
||||||
|
|
||||||
private void setCameraDisplayOrientation() {
|
|
||||||
Camera.CameraInfo info=new Camera.CameraInfo();
|
|
||||||
int rotation=
|
|
||||||
getActivity().getWindowManager().getDefaultDisplay()
|
|
||||||
.getRotation();
|
|
||||||
int degrees=0;
|
|
||||||
DisplayMetrics dm=new DisplayMetrics();
|
|
||||||
|
|
||||||
Camera.getCameraInfo(cameraId, info);
|
|
||||||
getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
|
|
||||||
|
|
||||||
switch (rotation) {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
|
||||||
displayOrientation=(info.orientation + degrees) % 360;
|
|
||||||
displayOrientation=(360 - displayOrientation) % 360;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
displayOrientation=(info.orientation - degrees + 360) % 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean wasInPreview=inPreview;
|
|
||||||
|
|
||||||
if (inPreview) {
|
|
||||||
stopPreview();
|
|
||||||
}
|
|
||||||
|
|
||||||
camera.setDisplayOrientation(displayOrientation);
|
|
||||||
|
|
||||||
if (wasInPreview) {
|
|
||||||
startPreview();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public int getCameraPictureOrientation() {
|
|
||||||
Camera.CameraInfo info=new Camera.CameraInfo();
|
|
||||||
|
|
||||||
Camera.getCameraInfo(cameraId, info);
|
|
||||||
|
|
||||||
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
|
|
||||||
outputOrientation=
|
|
||||||
getCameraPictureRotation(getActivity().getWindowManager()
|
|
||||||
.getDefaultDisplay()
|
|
||||||
.getOrientation());
|
|
||||||
}
|
|
||||||
else if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
|
||||||
outputOrientation=(360 - displayOrientation) % 360;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
outputOrientation=displayOrientation;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (lastPictureOrientation != outputOrientation) {
|
|
||||||
lastPictureOrientation=outputOrientation;
|
|
||||||
}
|
|
||||||
return outputOrientation;
|
|
||||||
}
|
|
||||||
|
|
||||||
// based on:
|
|
||||||
// http://developer.android.com/reference/android/hardware/Camera.Parameters.html#setRotation(int)
|
|
||||||
|
|
||||||
public int getCameraPictureRotation(int orientation) {
|
|
||||||
Camera.CameraInfo info=new Camera.CameraInfo();
|
|
||||||
Camera.getCameraInfo(cameraId, info);
|
|
||||||
int rotation=0;
|
|
||||||
|
|
||||||
orientation=(orientation + 45) / 90 * 90;
|
|
||||||
|
|
||||||
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
|
||||||
rotation=(info.orientation - orientation + 360) % 360;
|
|
||||||
}
|
|
||||||
else { // back-facing camera
|
|
||||||
rotation=(info.orientation + orientation) % 360;
|
|
||||||
}
|
|
||||||
|
|
||||||
return(rotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
Activity getActivity() {
|
|
||||||
return((Activity)getContext());
|
|
||||||
}
|
|
||||||
|
|
||||||
private class OnOrientationChange extends OrientationEventListener {
|
|
||||||
private boolean isEnabled=false;
|
|
||||||
|
|
||||||
public OnOrientationChange(Context context) {
|
|
||||||
super(context);
|
|
||||||
disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onOrientationChanged(int orientation) {
|
|
||||||
if (camera != null && orientation != ORIENTATION_UNKNOWN) {
|
|
||||||
int newOutputOrientation=getCameraPictureRotation(orientation);
|
|
||||||
|
|
||||||
if (newOutputOrientation != outputOrientation) {
|
|
||||||
outputOrientation=newOutputOrientation;
|
|
||||||
|
|
||||||
Camera.Parameters params=camera.getParameters();
|
|
||||||
|
|
||||||
params.setRotation(outputOrientation);
|
|
||||||
|
|
||||||
try {
|
|
||||||
camera.setParameters(params);
|
|
||||||
lastPictureOrientation=outputOrientation;
|
|
||||||
}
|
|
||||||
catch (Exception e) {
|
|
||||||
Log.e(getClass().getSimpleName(),
|
|
||||||
"Exception updating camera parameters in orientation change",
|
|
||||||
e);
|
|
||||||
// TODO: get this info out to hosting app
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void enable() {
|
|
||||||
isEnabled=true;
|
|
||||||
super.enable();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void disable() {
|
|
||||||
isEnabled=false;
|
|
||||||
super.disable();
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean isEnabled() {
|
|
||||||
return(isEnabled);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -64,7 +64,6 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
Log.w(TAG, String.format("onMeasure(%s, %s)", MeasureSpec.toString(widthMeasureSpec), MeasureSpec.toString(heightMeasureSpec)));
|
|
||||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
|
||||||
int res = getResources().getIdentifier("status_bar_height", "dimen", "android");
|
int res = getResources().getIdentifier("status_bar_height", "dimen", "android");
|
||||||
|
@ -1,503 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.pm.PackageManager;
|
|
||||||
import android.graphics.Canvas;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.hardware.Camera;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.support.annotation.IntDef;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
import android.support.v4.view.MotionEventCompat;
|
|
||||||
import android.support.v4.view.ViewCompat;
|
|
||||||
import android.support.v4.widget.ViewDragHelper;
|
|
||||||
import android.util.AttributeSet;
|
|
||||||
import android.view.MotionEvent;
|
|
||||||
import android.view.Surface;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.view.WindowManager;
|
|
||||||
import android.widget.ImageButton;
|
|
||||||
|
|
||||||
import com.commonsware.cwac.camera.SimpleCameraHost;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
|
||||||
|
|
||||||
public class QuickAttachmentDrawer extends ViewGroup {
|
|
||||||
@IntDef({COLLAPSED, HALF_EXPANDED, FULL_EXPANDED})
|
|
||||||
public @interface DrawerState {}
|
|
||||||
|
|
||||||
public static final int COLLAPSED = 0;
|
|
||||||
public static final int HALF_EXPANDED = 1;
|
|
||||||
public static final int FULL_EXPANDED = 2;
|
|
||||||
|
|
||||||
private static final float FULL_EXPANDED_ANCHOR_POINT = 1.f;
|
|
||||||
private static final float COLLAPSED_ANCHOR_POINT = 0.f;
|
|
||||||
|
|
||||||
private final ViewDragHelper dragHelper;
|
|
||||||
private final QuickCamera quickCamera;
|
|
||||||
private final View controls;
|
|
||||||
private View coverView;
|
|
||||||
private ImageButton fullScreenButton;
|
|
||||||
private @DrawerState int drawerState;
|
|
||||||
private float slideOffset, initialMotionX, initialMotionY, halfExpandedAnchorPoint;
|
|
||||||
private boolean initialSetup, hasCamera, startCamera, stopCamera, landscape, belowICS;
|
|
||||||
private int slideRange, baseHalfHeight;
|
|
||||||
private Rect drawChildrenRect = new Rect();
|
|
||||||
private QuickAttachmentDrawerListener listener;
|
|
||||||
|
|
||||||
public QuickAttachmentDrawer(Context context) {
|
|
||||||
this(context, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public QuickAttachmentDrawer(Context context, AttributeSet attrs) {
|
|
||||||
this(context, attrs, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
public QuickAttachmentDrawer(Context context, AttributeSet attrs, int defStyle) {
|
|
||||||
super(context, attrs, defStyle);
|
|
||||||
initialSetup = true;
|
|
||||||
startCamera = false;
|
|
||||||
stopCamera = false;
|
|
||||||
drawerState = COLLAPSED;
|
|
||||||
baseHalfHeight = getResources().getDimensionPixelSize(R.dimen.quick_media_drawer_default_height);
|
|
||||||
halfExpandedAnchorPoint = COLLAPSED_ANCHOR_POINT;
|
|
||||||
int rotation = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation();
|
|
||||||
landscape = rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
|
|
||||||
belowICS = android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH;
|
|
||||||
hasCamera = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA) && Camera.getNumberOfCameras() > 0;
|
|
||||||
if (hasCamera) {
|
|
||||||
setBackgroundResource(android.R.color.black);
|
|
||||||
dragHelper = ViewDragHelper.create(this, 1.f, new ViewDragHelperCallback());
|
|
||||||
quickCamera = new QuickCamera(context);
|
|
||||||
controls = inflate(getContext(), R.layout.quick_camera_controls, null);
|
|
||||||
initializeControlsView();
|
|
||||||
addView(quickCamera);
|
|
||||||
addView(controls);
|
|
||||||
} else {
|
|
||||||
dragHelper = null;
|
|
||||||
quickCamera = null;
|
|
||||||
controls = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean hasCamera() {
|
|
||||||
return hasCamera;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeHalfExpandedAnchorPoint() {
|
|
||||||
if (initialSetup) {
|
|
||||||
if (getChildCount() == 3)
|
|
||||||
coverView = getChildAt(2);
|
|
||||||
else
|
|
||||||
coverView = getChildAt(0);
|
|
||||||
slideRange = getMeasuredHeight();
|
|
||||||
int anchorHeight = slideRange - baseHalfHeight;
|
|
||||||
halfExpandedAnchorPoint = computeSlideOffsetFromCoverBottom(anchorHeight);
|
|
||||||
initialSetup = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void initializeControlsView() {
|
|
||||||
controls.findViewById(R.id.shutter_button).setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
boolean crop = drawerState != FULL_EXPANDED;
|
|
||||||
int imageHeight = crop ? baseHalfHeight : quickCamera.getMeasuredHeight();
|
|
||||||
Rect previewRect = new Rect(0, 0, quickCamera.getMeasuredWidth(), imageHeight);
|
|
||||||
quickCamera.takePicture(crop, previewRect);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
final ImageButton swapCameraButton = (ImageButton) controls.findViewById(R.id.swap_camera_button);
|
|
||||||
if (quickCamera.isMultipleCameras()) {
|
|
||||||
swapCameraButton.setVisibility(View.VISIBLE);
|
|
||||||
swapCameraButton.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
quickCamera.swapCamera();
|
|
||||||
swapCameraButton.setImageResource(quickCamera.isRearCamera() ? R.drawable.quick_camera_front : R.drawable.quick_camera_rear);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
fullScreenButton = (ImageButton) controls.findViewById(R.id.fullscreen_button);
|
|
||||||
fullScreenButton.setOnClickListener(new OnClickListener() {
|
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
if (drawerState == HALF_EXPANDED || drawerState == COLLAPSED)
|
|
||||||
setDrawerStateAndAnimate(FULL_EXPANDED);
|
|
||||||
else if (landscape || belowICS)
|
|
||||||
setDrawerStateAndAnimate(COLLAPSED);
|
|
||||||
else
|
|
||||||
setDrawerStateAndAnimate(HALF_EXPANDED);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
|
||||||
final int paddingLeft = getPaddingLeft();
|
|
||||||
final int paddingTop = getPaddingTop();
|
|
||||||
|
|
||||||
final int childCount = getChildCount();
|
|
||||||
|
|
||||||
for (int i = 0; i < childCount; i++) {
|
|
||||||
final View child = getChildAt(i);
|
|
||||||
|
|
||||||
final int childHeight = child.getMeasuredHeight();
|
|
||||||
int childTop = paddingTop;
|
|
||||||
int childBottom;
|
|
||||||
int childLeft = paddingLeft;
|
|
||||||
|
|
||||||
if (child == quickCamera) {
|
|
||||||
childTop = computeCameraTopPosition(slideOffset);
|
|
||||||
childBottom = childTop + childHeight;
|
|
||||||
if (quickCamera.getMeasuredWidth() < getMeasuredWidth())
|
|
||||||
childLeft = (getMeasuredWidth() - quickCamera.getMeasuredWidth()) / 2 + paddingLeft;
|
|
||||||
} else if (child == controls) {
|
|
||||||
childBottom = getMeasuredHeight();
|
|
||||||
} else {
|
|
||||||
childBottom = computeCoverBottomPosition(slideOffset);
|
|
||||||
childTop = childBottom - childHeight;
|
|
||||||
}
|
|
||||||
final int childRight = childLeft + child.getMeasuredWidth();
|
|
||||||
|
|
||||||
if (childHeight > 0)
|
|
||||||
child.layout(childLeft, childTop, childRight, childBottom);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
|
||||||
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
|
|
||||||
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
|
|
||||||
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
|
|
||||||
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
|
|
||||||
|
|
||||||
if (widthMode != MeasureSpec.EXACTLY) {
|
|
||||||
throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
|
|
||||||
} else if (heightMode != MeasureSpec.EXACTLY) {
|
|
||||||
throw new IllegalStateException("Height must have an exact value or MATCH_PARENT");
|
|
||||||
}
|
|
||||||
|
|
||||||
final int childCount = getChildCount();
|
|
||||||
if ((hasCamera && childCount != 3) || (!hasCamera && childCount != 1))
|
|
||||||
throw new IllegalStateException("QuickAttachmentDrawer layouts may only have 1 child.");
|
|
||||||
|
|
||||||
int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
|
|
||||||
|
|
||||||
for (int i = 0; i < childCount; i++) {
|
|
||||||
final View child = getChildAt(i);
|
|
||||||
final LayoutParams lp = child.getLayoutParams();
|
|
||||||
|
|
||||||
if (child.getVisibility() == GONE && i == 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int childWidthSpec;
|
|
||||||
switch (lp.width) {
|
|
||||||
case LayoutParams.WRAP_CONTENT:
|
|
||||||
childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
|
|
||||||
break;
|
|
||||||
case LayoutParams.MATCH_PARENT:
|
|
||||||
childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
int childHeightSpec;
|
|
||||||
switch (lp.height) {
|
|
||||||
case LayoutParams.WRAP_CONTENT:
|
|
||||||
childHeightSpec = MeasureSpec.makeMeasureSpec(layoutHeight, MeasureSpec.AT_MOST);
|
|
||||||
break;
|
|
||||||
case LayoutParams.MATCH_PARENT:
|
|
||||||
childHeightSpec = MeasureSpec.makeMeasureSpec(layoutHeight, MeasureSpec.EXACTLY);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
child.measure(childWidthSpec, childHeightSpec);
|
|
||||||
}
|
|
||||||
|
|
||||||
setMeasuredDimension(widthSize, heightSize);
|
|
||||||
initializeHalfExpandedAnchorPoint();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
|
||||||
super.onSizeChanged(w, h, oldw, oldh);
|
|
||||||
if (h != oldh)
|
|
||||||
initialSetup = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) {
|
|
||||||
boolean result;
|
|
||||||
final int save = canvas.save(Canvas.CLIP_SAVE_FLAG);
|
|
||||||
|
|
||||||
canvas.getClipBounds(drawChildrenRect);
|
|
||||||
if (child == coverView)
|
|
||||||
drawChildrenRect.bottom = Math.min(drawChildrenRect.bottom, child.getBottom());
|
|
||||||
else if (coverView != null)
|
|
||||||
drawChildrenRect.top = Math.max(drawChildrenRect.top, coverView.getBottom());
|
|
||||||
canvas.clipRect(drawChildrenRect);
|
|
||||||
result = super.drawChild(canvas, child, drawingTime);
|
|
||||||
canvas.restoreToCount(save);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void computeScroll() {
|
|
||||||
if (dragHelper != null && dragHelper.continueSettling(true)) {
|
|
||||||
ViewCompat.postInvalidateOnAnimation(this);
|
|
||||||
} else if (stopCamera) {
|
|
||||||
stopCamera = false;
|
|
||||||
quickCamera.onPause();
|
|
||||||
} else if (startCamera) {
|
|
||||||
startCamera = false;
|
|
||||||
quickCamera.onResume();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setDrawerState(@DrawerState int drawerState) {
|
|
||||||
if (hasCamera) {
|
|
||||||
switch (drawerState) {
|
|
||||||
case COLLAPSED:
|
|
||||||
quickCamera.previewCreated();
|
|
||||||
if (quickCamera.isStarted())
|
|
||||||
stopCamera = true;
|
|
||||||
slideOffset = COLLAPSED_ANCHOR_POINT;
|
|
||||||
startCamera = false;
|
|
||||||
fullScreenButton.setImageResource(R.drawable.quick_camera_fullscreen);
|
|
||||||
if (listener != null) listener.onCollapsed();
|
|
||||||
break;
|
|
||||||
case HALF_EXPANDED:
|
|
||||||
if (landscape || belowICS) {
|
|
||||||
setDrawerState(FULL_EXPANDED);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!quickCamera.isStarted())
|
|
||||||
startCamera = true;
|
|
||||||
slideOffset = halfExpandedAnchorPoint;
|
|
||||||
stopCamera = false;
|
|
||||||
fullScreenButton.setImageResource(R.drawable.quick_camera_fullscreen);
|
|
||||||
if (listener != null) listener.onHalfExpanded();
|
|
||||||
break;
|
|
||||||
case FULL_EXPANDED:
|
|
||||||
if (!quickCamera.isStarted())
|
|
||||||
startCamera = true;
|
|
||||||
slideOffset = FULL_EXPANDED_ANCHOR_POINT;
|
|
||||||
stopCamera = false;
|
|
||||||
fullScreenButton.setImageResource(landscape || belowICS ? R.drawable.quick_camera_hide : R.drawable.quick_camera_exit_fullscreen);
|
|
||||||
if (listener != null) listener.onExpanded();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.drawerState = drawerState;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public
|
|
||||||
@DrawerState
|
|
||||||
int getDrawerState() {
|
|
||||||
return drawerState;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setDrawerStateAndAnimate(@DrawerState int drawerState) {
|
|
||||||
setDrawerState(drawerState);
|
|
||||||
slideTo(slideOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setQuickAttachmentDrawerListener(QuickAttachmentDrawerListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setQuickCameraListener(QuickCamera.QuickCameraListener listener) {
|
|
||||||
if (quickCamera != null) quickCamera.setQuickCameraListener(listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface QuickAttachmentDrawerListener {
|
|
||||||
void onCollapsed();
|
|
||||||
void onExpanded();
|
|
||||||
void onHalfExpanded();
|
|
||||||
}
|
|
||||||
|
|
||||||
private class ViewDragHelperCallback extends ViewDragHelper.Callback {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean tryCaptureView(View child, int pointerId) {
|
|
||||||
return child == controls && !belowICS;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewDragStateChanged(int state) {
|
|
||||||
if (dragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
|
|
||||||
setDrawerState(drawerState);
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewCaptured(View capturedChild, int activePointerId) {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
|
|
||||||
int newTop = coverView.getTop() + dy;
|
|
||||||
final int expandedTop = computeCoverBottomPosition(FULL_EXPANDED_ANCHOR_POINT) - coverView.getHeight();
|
|
||||||
final int collapsedTop = computeCoverBottomPosition(COLLAPSED_ANCHOR_POINT) - coverView.getHeight();
|
|
||||||
newTop = Math.min(Math.max(newTop, expandedTop), collapsedTop);
|
|
||||||
slideOffset = computeSlideOffsetFromCoverBottom(newTop + coverView.getHeight());
|
|
||||||
requestLayout();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onViewReleased(View releasedChild, float xvel, float yvel) {
|
|
||||||
if (releasedChild == controls) {
|
|
||||||
float direction = -yvel;
|
|
||||||
int drawerState = COLLAPSED;
|
|
||||||
|
|
||||||
if (direction > 1) {
|
|
||||||
drawerState = FULL_EXPANDED;
|
|
||||||
} else if (direction < -1) {
|
|
||||||
boolean halfExpand = (slideOffset > halfExpandedAnchorPoint && !landscape);
|
|
||||||
drawerState = halfExpand ? HALF_EXPANDED : COLLAPSED;
|
|
||||||
} else if (!landscape) {
|
|
||||||
if (halfExpandedAnchorPoint != 1 && slideOffset >= (1.f + halfExpandedAnchorPoint) / 2) {
|
|
||||||
drawerState = FULL_EXPANDED;
|
|
||||||
} else if (halfExpandedAnchorPoint == 1 && slideOffset >= 0.5f) {
|
|
||||||
drawerState = FULL_EXPANDED;
|
|
||||||
} else if (halfExpandedAnchorPoint != 1 && slideOffset >= halfExpandedAnchorPoint) {
|
|
||||||
drawerState = HALF_EXPANDED;
|
|
||||||
} else if (halfExpandedAnchorPoint != 1 && slideOffset >= halfExpandedAnchorPoint / 2) {
|
|
||||||
drawerState = HALF_EXPANDED;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setDrawerState(drawerState);
|
|
||||||
dragHelper.captureChildView(coverView, 0);
|
|
||||||
dragHelper.settleCapturedViewAt(coverView.getLeft(), computeCoverBottomPosition(slideOffset) - coverView.getHeight());
|
|
||||||
dragHelper.captureChildView(quickCamera, 0);
|
|
||||||
dragHelper.settleCapturedViewAt(quickCamera.getLeft(), computeCameraTopPosition(slideOffset));
|
|
||||||
ViewCompat.postInvalidateOnAnimation(QuickAttachmentDrawer.this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getViewVerticalDragRange(View child) {
|
|
||||||
return slideRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int clampViewPositionVertical(View child, int top, int dy) {
|
|
||||||
return top;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onInterceptTouchEvent(MotionEvent event) {
|
|
||||||
if (dragHelper != null) {
|
|
||||||
final int action = MotionEventCompat.getActionMasked(event);
|
|
||||||
|
|
||||||
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
|
|
||||||
dragHelper.cancel();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
final float x = event.getX();
|
|
||||||
final float y = event.getY();
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case MotionEvent.ACTION_DOWN: {
|
|
||||||
initialMotionX = x;
|
|
||||||
initialMotionY = y;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
case MotionEvent.ACTION_MOVE: {
|
|
||||||
final float adx = Math.abs(x - initialMotionX);
|
|
||||||
final float ady = Math.abs(y - initialMotionY);
|
|
||||||
final int dragSlop = dragHelper.getTouchSlop();
|
|
||||||
|
|
||||||
if (adx > dragSlop && ady < dragSlop) {
|
|
||||||
return super.onInterceptTouchEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((ady > dragSlop && adx > ady) || !isDragViewUnder((int) initialMotionX, (int) initialMotionY)) {
|
|
||||||
dragHelper.cancel();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dragHelper.shouldInterceptTouchEvent(event);
|
|
||||||
}
|
|
||||||
return super.onInterceptTouchEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onTouchEvent(@NonNull MotionEvent event) {
|
|
||||||
if (dragHelper != null) {
|
|
||||||
dragHelper.processTouchEvent(event);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return super.onTouchEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean isDragViewUnder(int x, int y) {
|
|
||||||
int[] viewLocation = new int[2];
|
|
||||||
quickCamera.getLocationOnScreen(viewLocation);
|
|
||||||
int[] parentLocation = new int[2];
|
|
||||||
this.getLocationOnScreen(parentLocation);
|
|
||||||
int screenX = parentLocation[0] + x;
|
|
||||||
int screenY = parentLocation[1] + y;
|
|
||||||
return screenX >= viewLocation[0] && screenX < viewLocation[0] + quickCamera.getWidth() &&
|
|
||||||
screenY >= viewLocation[1] && screenY < viewLocation[1] + quickCamera.getHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
private int computeCameraTopPosition(float slideOffset) {
|
|
||||||
float clampedOffset = slideOffset - halfExpandedAnchorPoint;
|
|
||||||
if (clampedOffset < COLLAPSED_ANCHOR_POINT)
|
|
||||||
clampedOffset = COLLAPSED_ANCHOR_POINT;
|
|
||||||
else
|
|
||||||
clampedOffset = clampedOffset / (FULL_EXPANDED_ANCHOR_POINT - halfExpandedAnchorPoint);
|
|
||||||
float slidePixelOffset = slideOffset * slideRange +
|
|
||||||
(quickCamera.getMeasuredHeight() - baseHalfHeight) / 2 * (FULL_EXPANDED_ANCHOR_POINT - clampedOffset);
|
|
||||||
float marginPixelOffset = (getMeasuredHeight() - quickCamera.getMeasuredHeight()) / 2 * clampedOffset;
|
|
||||||
return (int) (getMeasuredHeight() - slidePixelOffset + marginPixelOffset);
|
|
||||||
}
|
|
||||||
|
|
||||||
private int computeCoverBottomPosition(float slideOffset) {
|
|
||||||
int slidePixelOffset = (int) (slideOffset * slideRange);
|
|
||||||
return getMeasuredHeight() - getPaddingBottom() - slidePixelOffset;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void slideTo(float slideOffset) {
|
|
||||||
if (dragHelper != null && !belowICS) {
|
|
||||||
dragHelper.smoothSlideViewTo(coverView, coverView.getLeft(), computeCoverBottomPosition(slideOffset) - coverView.getHeight());
|
|
||||||
dragHelper.smoothSlideViewTo(quickCamera, quickCamera.getLeft(), computeCameraTopPosition(slideOffset));
|
|
||||||
ViewCompat.postInvalidateOnAnimation(this);
|
|
||||||
} else {
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private float computeSlideOffsetFromCoverBottom(int topPosition) {
|
|
||||||
final int topBoundCollapsed = computeCoverBottomPosition(0);
|
|
||||||
return (float) (topBoundCollapsed - topPosition) / slideRange;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onPause() {
|
|
||||||
quickCamera.onPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onResume() {
|
|
||||||
if (hasCamera && (drawerState == HALF_EXPANDED || drawerState == FULL_EXPANDED))
|
|
||||||
quickCamera.onResume();
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,185 +0,0 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.graphics.Bitmap;
|
|
||||||
import android.graphics.BitmapFactory;
|
|
||||||
import android.graphics.ImageFormat;
|
|
||||||
import android.graphics.Matrix;
|
|
||||||
import android.graphics.Rect;
|
|
||||||
import android.graphics.YuvImage;
|
|
||||||
import android.hardware.Camera;
|
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.commonsware.cwac.camera.SimpleCameraHost;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class QuickCamera extends CameraView {
|
|
||||||
private QuickCameraListener listener;
|
|
||||||
private boolean started, savingImage;
|
|
||||||
private int rotation;
|
|
||||||
private QuickCameraHost cameraHost;
|
|
||||||
|
|
||||||
public QuickCamera(Context context) {
|
|
||||||
super(context);
|
|
||||||
started = false;
|
|
||||||
savingImage = false;
|
|
||||||
setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
|
|
||||||
cameraHost = new QuickCameraHost(context);
|
|
||||||
setHost(cameraHost);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onResume() {
|
|
||||||
super.onResume();
|
|
||||||
rotation = getCameraPictureOrientation();
|
|
||||||
started = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onPause() {
|
|
||||||
started = false;
|
|
||||||
super.onPause();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isStarted() {
|
|
||||||
return started;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void takePicture(final boolean crop, final Rect previewRect) {
|
|
||||||
setOneShotPreviewCallback(new Camera.PreviewCallback() {
|
|
||||||
@Override
|
|
||||||
public void onPreviewFrame(byte[] data, Camera camera) {
|
|
||||||
new AsyncTask<byte[], Void, byte[]>() {
|
|
||||||
@Override
|
|
||||||
protected byte[] doInBackground(byte[]... params) {
|
|
||||||
byte[] data = params[0];
|
|
||||||
if (savingImage)
|
|
||||||
return null;
|
|
||||||
savingImage = true;
|
|
||||||
try {
|
|
||||||
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
|
|
||||||
int previewWidth = getCameraParameters().getPreviewSize().width;
|
|
||||||
int previewHeight = getCameraParameters().getPreviewSize().height;
|
|
||||||
YuvImage previewImage = new YuvImage(data, ImageFormat.NV21, previewWidth, previewHeight, null);
|
|
||||||
|
|
||||||
if (crop) {
|
|
||||||
float newWidth, newHeight;
|
|
||||||
if (rotation == 90 || rotation == 270) {
|
|
||||||
newWidth = previewRect.height();
|
|
||||||
newHeight = previewRect.width();
|
|
||||||
} else {
|
|
||||||
newWidth = previewRect.width();
|
|
||||||
newHeight = previewRect.height();
|
|
||||||
}
|
|
||||||
float centerX = previewWidth / 2;
|
|
||||||
float centerY = previewHeight / 2;
|
|
||||||
previewRect.set((int) (centerX - newWidth / 2),
|
|
||||||
(int) (centerY - newHeight / 2),
|
|
||||||
(int) (centerX + newWidth / 2),
|
|
||||||
(int) (centerY + newHeight / 2));
|
|
||||||
} else if (rotation == 90 || rotation == 270) {
|
|
||||||
previewRect.set(0, 0, previewRect.height(), previewRect.width());
|
|
||||||
}
|
|
||||||
previewImage.compressToJpeg(previewRect, 100, byteArrayOutputStream);
|
|
||||||
byte[] bytes = byteArrayOutputStream.toByteArray();
|
|
||||||
byteArrayOutputStream.close();
|
|
||||||
byteArrayOutputStream = new ByteArrayOutputStream();
|
|
||||||
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
|
|
||||||
if (rotation != 0)
|
|
||||||
bitmap = rotateBitmap(bitmap, rotation);
|
|
||||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream);
|
|
||||||
byte[] finalImageByteArray = byteArrayOutputStream.toByteArray();
|
|
||||||
byteArrayOutputStream.close();
|
|
||||||
savingImage = false;
|
|
||||||
return finalImageByteArray;
|
|
||||||
} catch (IOException e) {
|
|
||||||
savingImage = false;
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onPostExecute(byte[] data) {
|
|
||||||
if (data != null && listener != null)
|
|
||||||
listener.onImageCapture(data);
|
|
||||||
}
|
|
||||||
}.execute(data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Bitmap rotateBitmap(Bitmap bitmap, int angle) {
|
|
||||||
Matrix matrix = new Matrix();
|
|
||||||
matrix.postRotate(angle);
|
|
||||||
Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
|
|
||||||
if (rotated != bitmap) bitmap.recycle();
|
|
||||||
return rotated;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setQuickCameraListener(QuickCameraListener listener) {
|
|
||||||
this.listener = listener;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isMultipleCameras() {
|
|
||||||
return Camera.getNumberOfCameras() > 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isRearCamera() {
|
|
||||||
return cameraHost.getCameraId() == Camera.CameraInfo.CAMERA_FACING_BACK;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void swapCamera() {
|
|
||||||
cameraHost.swapCameraId();
|
|
||||||
onPause();
|
|
||||||
onResume();
|
|
||||||
}
|
|
||||||
|
|
||||||
public interface QuickCameraListener {
|
|
||||||
void onImageCapture(final byte[] data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class QuickCameraHost extends SimpleCameraHost {
|
|
||||||
int cameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
|
|
||||||
|
|
||||||
public QuickCameraHost(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Camera.Parameters adjustPreviewParameters(Camera.Parameters parameters) {
|
|
||||||
List<String> focusModes = parameters.getSupportedFocusModes();
|
|
||||||
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE))
|
|
||||||
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
|
|
||||||
else if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO))
|
|
||||||
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
|
|
||||||
return parameters;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCameraId() {
|
|
||||||
return cameraId;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void swapCameraId() {
|
|
||||||
if (isMultipleCameras()) {
|
|
||||||
if (cameraId == Camera.CameraInfo.CAMERA_FACING_BACK)
|
|
||||||
cameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;
|
|
||||||
else
|
|
||||||
cameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onCameraFail(FailureReason reason) {
|
|
||||||
super.onCameraFail(reason);
|
|
||||||
Toast.makeText(getContext(), R.string.quick_camera_unavailable, Toast.LENGTH_SHORT).show();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -4,7 +4,6 @@ import android.annotation.TargetApi;
|
|||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.graphics.Bitmap;
|
import android.graphics.Bitmap;
|
||||||
import android.net.Uri;
|
|
||||||
import android.os.Build.VERSION;
|
import android.os.Build.VERSION;
|
||||||
import android.os.Build.VERSION_CODES;
|
import android.os.Build.VERSION_CODES;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
@ -17,6 +16,7 @@ import android.view.animation.Animation;
|
|||||||
import android.view.animation.Animation.AnimationListener;
|
import android.view.animation.Animation.AnimationListener;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import com.bumptech.glide.DrawableTypeRequest;
|
||||||
import com.bumptech.glide.GenericRequestBuilder;
|
import com.bumptech.glide.GenericRequestBuilder;
|
||||||
import com.bumptech.glide.Glide;
|
import com.bumptech.glide.Glide;
|
||||||
import com.bumptech.glide.request.RequestListener;
|
import com.bumptech.glide.request.RequestListener;
|
||||||
@ -108,10 +108,6 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
this.slideDeckFuture.addListener(this.slideDeckListener);
|
this.slideDeckFuture.addListener(this.slideDeckListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setImageResource(@NonNull Slide slide) {
|
|
||||||
setImageResource(slide, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setImageResource(@NonNull Slide slide, @Nullable MasterSecret masterSecret) {
|
public void setImageResource(@NonNull Slide slide, @Nullable MasterSecret masterSecret) {
|
||||||
if (Util.equals(slide, this.slide)) {
|
if (Util.equals(slide, this.slide)) {
|
||||||
Log.w(TAG, "Not loading resource, slide was identical");
|
Log.w(TAG, "Not loading resource, slide was identical");
|
||||||
@ -175,28 +171,22 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
private GenericRequestBuilder buildThumbnailGlideRequest(Slide slide, MasterSecret masterSecret) {
|
private GenericRequestBuilder buildThumbnailGlideRequest(Slide slide, MasterSecret masterSecret) {
|
||||||
|
|
||||||
final GenericRequestBuilder builder;
|
final GenericRequestBuilder builder;
|
||||||
if (slide.isDraft() && slide.isEncrypted()) builder = buildEncryptedDraftGlideRequest(slide, masterSecret);
|
if (slide.isDraft()) builder = buildDraftGlideRequest(slide, masterSecret);
|
||||||
else if (slide.isDraft()) builder = buildDraftGlideRequest(slide);
|
else builder = buildPartGlideRequest(slide, masterSecret);
|
||||||
else builder = buildEncryptedPartGlideRequest(slide, masterSecret);
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private GenericRequestBuilder buildDraftGlideRequest(Slide slide) {
|
private GenericRequestBuilder buildDraftGlideRequest(Slide slide, MasterSecret masterSecret) {
|
||||||
return Glide.with(getContext()).load(slide.getThumbnailUri()).asBitmap()
|
final DrawableTypeRequest<?> request;
|
||||||
|
if (masterSecret == null) request = Glide.with(getContext()).load(slide.getThumbnailUri());
|
||||||
|
else request = Glide.with(getContext()).load(new DecryptableUri(masterSecret, slide.getThumbnailUri()));
|
||||||
|
|
||||||
|
return request.asBitmap()
|
||||||
.fitCenter()
|
.fitCenter()
|
||||||
.listener(new PduThumbnailSetListener(slide.getPart()));
|
.listener(new PduThumbnailSetListener(slide.getPart()));
|
||||||
}
|
}
|
||||||
|
|
||||||
private GenericRequestBuilder buildEncryptedDraftGlideRequest(Slide slide, MasterSecret masterSecret) {
|
private GenericRequestBuilder buildPartGlideRequest(Slide slide, MasterSecret masterSecret) {
|
||||||
if (masterSecret == null) {
|
|
||||||
throw new IllegalStateException("null MasterSecret when loading encrypted draft thumbnail");
|
|
||||||
}
|
|
||||||
|
|
||||||
return Glide.with(getContext()).load(new DecryptableUri(masterSecret, slide.getThumbnailUri()))
|
|
||||||
.fitCenter();
|
|
||||||
}
|
|
||||||
|
|
||||||
private GenericRequestBuilder buildEncryptedPartGlideRequest(Slide slide, MasterSecret masterSecret) {
|
|
||||||
if (masterSecret == null) {
|
if (masterSecret == null) {
|
||||||
throw new IllegalStateException("null MasterSecret when loading non-draft thumbnail");
|
throw new IllegalStateException("null MasterSecret when loading non-draft thumbnail");
|
||||||
}
|
}
|
||||||
@ -290,7 +280,7 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class PduThumbnailSetListener implements RequestListener<Uri, Bitmap> {
|
private static class PduThumbnailSetListener implements RequestListener<Object, Bitmap> {
|
||||||
private PduPart part;
|
private PduPart part;
|
||||||
|
|
||||||
public PduThumbnailSetListener(@NonNull PduPart part) {
|
public PduThumbnailSetListener(@NonNull PduPart part) {
|
||||||
@ -298,12 +288,12 @@ public class ThumbnailView extends FrameLayout {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onException(Exception e, Uri model, Target<Bitmap> target, boolean isFirstResource) {
|
public boolean onException(Exception e, Object model, Target<Bitmap> target, boolean isFirstResource) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onResourceReady(Bitmap resource, Uri model, Target<Bitmap> target, boolean isFromMemoryCache, boolean isFirstResource) {
|
public boolean onResourceReady(Bitmap resource, Object model, Target<Bitmap> target, boolean isFromMemoryCache, boolean isFirstResource) {
|
||||||
part.setThumbnail(resource);
|
part.setThumbnail(resource);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
499
src/org/thoughtcrime/securesms/components/camera/CameraView.java
Normal file
499
src/org/thoughtcrime/securesms/components/camera/CameraView.java
Normal file
@ -0,0 +1,499 @@
|
|||||||
|
/***
|
||||||
|
Copyright (c) 2013-2014 CommonsWare, LLC
|
||||||
|
Portions Copyright (C) 2007 The Android Open Source Project
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
not use this file except in compliance with the License. You may obtain
|
||||||
|
a copy of the License at
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.thoughtcrime.securesms.components.camera;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.ActivityInfo;
|
||||||
|
import android.hardware.Camera;
|
||||||
|
import android.hardware.Camera.PreviewCallback;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.DisplayMetrics;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.OrientationEventListener;
|
||||||
|
import android.view.Surface;
|
||||||
|
import android.view.View;
|
||||||
|
import android.widget.FrameLayout;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
|
||||||
|
import com.commonsware.cwac.camera.CameraHost;
|
||||||
|
import com.commonsware.cwac.camera.CameraHost.FailureReason;
|
||||||
|
import com.commonsware.cwac.camera.PreviewStrategy;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.ApplicationContext;
|
||||||
|
import org.thoughtcrime.securesms.util.Util;
|
||||||
|
import org.whispersystems.jobqueue.Job;
|
||||||
|
import org.whispersystems.jobqueue.JobParameters;
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation")
|
||||||
|
public class CameraView extends FrameLayout {
|
||||||
|
private static final String TAG = CameraView.class.getSimpleName();
|
||||||
|
|
||||||
|
private PreviewStrategy previewStrategy = null;
|
||||||
|
private Camera.Size previewSize = null;
|
||||||
|
private volatile Camera camera = null;
|
||||||
|
private boolean inPreview = false;
|
||||||
|
private boolean cameraReady = false;
|
||||||
|
private CameraHost host = null;
|
||||||
|
private OnOrientationChange onOrientationChange = null;
|
||||||
|
private int displayOrientation = -1;
|
||||||
|
private int outputOrientation = -1;
|
||||||
|
private int cameraId = -1;
|
||||||
|
private int lastPictureOrientation = -1;
|
||||||
|
|
||||||
|
public CameraView(Context context) {
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CameraView(Context context, AttributeSet attrs) {
|
||||||
|
this(context, attrs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CameraView(Context context, AttributeSet attrs, int defStyle) {
|
||||||
|
super(context, attrs, defStyle);
|
||||||
|
|
||||||
|
onOrientationChange = new OnOrientationChange(context.getApplicationContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
public CameraHost getHost() {
|
||||||
|
return host;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHost(CameraHost host) {
|
||||||
|
this.host = host;
|
||||||
|
|
||||||
|
if (host.getDeviceProfile().useTextureView()) {
|
||||||
|
previewStrategy = new TexturePreviewStrategy(this);
|
||||||
|
} else {
|
||||||
|
previewStrategy = new SurfacePreviewStrategy(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||||
|
public void onResume() {
|
||||||
|
addView(previewStrategy.getWidget());
|
||||||
|
final CameraHost host = getHost();
|
||||||
|
submitTask(new SerializedAsyncTask<FailureReason>() {
|
||||||
|
@Override protected FailureReason onRunBackground() {
|
||||||
|
try {
|
||||||
|
cameraId = host.getCameraId();
|
||||||
|
if (cameraId >= 0) {
|
||||||
|
camera = Camera.open(cameraId);
|
||||||
|
} else {
|
||||||
|
return FailureReason.NO_CAMERAS_REPORTED;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
return FailureReason.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onPostMain(FailureReason result) {
|
||||||
|
cameraReady = true;
|
||||||
|
if (result != null) {
|
||||||
|
host.onCameraFail(result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
|
||||||
|
onOrientationChange.enable();
|
||||||
|
}
|
||||||
|
setCameraDisplayOrientation();
|
||||||
|
synchronized (CameraView.this) {
|
||||||
|
CameraView.this.notifyAll();
|
||||||
|
}
|
||||||
|
requestLayout();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onPause() {
|
||||||
|
removeView(previewStrategy.getWidget());
|
||||||
|
submitTask(new SerializedAsyncTask<Void>() {
|
||||||
|
@Override protected void onPreMain() {
|
||||||
|
cameraReady = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected Void onRunBackground() {
|
||||||
|
if (camera != null) {
|
||||||
|
previewDestroyed();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override protected void onPostMain(Void avoid) {
|
||||||
|
onOrientationChange.disable();
|
||||||
|
previewSize = null;
|
||||||
|
displayOrientation = -1;
|
||||||
|
outputOrientation = -1;
|
||||||
|
cameraId = -1;
|
||||||
|
lastPictureOrientation = -1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// based on CameraPreview.java from ApiDemos
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
|
||||||
|
if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0 && camera != null && cameraReady) {
|
||||||
|
Camera.Size newSize = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (getHost().getRecordingHint() != CameraHost.RecordingHint.STILL_ONLY) {
|
||||||
|
newSize = getHost().getPreferredPreviewSizeForVideo(getDisplayOrientation(),
|
||||||
|
getMeasuredWidth(),
|
||||||
|
getMeasuredHeight(),
|
||||||
|
camera.getParameters(),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
if (newSize == null || newSize.width * newSize.height < 65536) {
|
||||||
|
newSize = getHost().getPreviewSize(getDisplayOrientation(),
|
||||||
|
getMeasuredWidth(),
|
||||||
|
getMeasuredHeight(),
|
||||||
|
camera.getParameters());
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "Could not work with camera parameters?", e);
|
||||||
|
// TODO get this out to library clients
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newSize != null) {
|
||||||
|
if (previewSize == null) {
|
||||||
|
previewSize = newSize;
|
||||||
|
synchronized (this) { notifyAll(); }
|
||||||
|
} else if (previewSize.width != newSize.width || previewSize.height != newSize.height) {
|
||||||
|
if (inPreview) {
|
||||||
|
stopPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
previewSize = newSize;
|
||||||
|
synchronized (this) { notifyAll(); }
|
||||||
|
initPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// based on CameraPreview.java from ApiDemos
|
||||||
|
|
||||||
|
@SuppressWarnings("SuspiciousNameCombination") @Override
|
||||||
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||||
|
if (changed && getChildCount() > 0) {
|
||||||
|
final View child = getChildAt(0);
|
||||||
|
final int width = r - l;
|
||||||
|
final int height = b - t;
|
||||||
|
final int previewWidth;
|
||||||
|
final int previewHeight;
|
||||||
|
|
||||||
|
// handle orientation
|
||||||
|
|
||||||
|
if (previewSize != null && (getDisplayOrientation() == 90 || getDisplayOrientation() == 270)) {
|
||||||
|
previewWidth = previewSize.height;
|
||||||
|
previewHeight = previewSize.width;
|
||||||
|
} else if (previewSize != null) {
|
||||||
|
previewWidth = previewSize.width;
|
||||||
|
previewHeight = previewSize.height;
|
||||||
|
} else {
|
||||||
|
previewWidth = width;
|
||||||
|
previewHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean useFirstStrategy = (width * previewHeight > height * previewWidth);
|
||||||
|
boolean useFullBleed = getHost().useFullBleedPreview();
|
||||||
|
|
||||||
|
if ((useFirstStrategy && !useFullBleed) || (!useFirstStrategy && useFullBleed)) {
|
||||||
|
final int scaledChildWidth = previewWidth * height / previewHeight;
|
||||||
|
child.layout((width - scaledChildWidth) / 2, 0, (width + scaledChildWidth) / 2, height);
|
||||||
|
} else {
|
||||||
|
final int scaledChildHeight = previewHeight * width / previewWidth;
|
||||||
|
child.layout(0, (height - scaledChildHeight) / 2, width, (height + scaledChildHeight) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDisplayOrientation() {
|
||||||
|
return displayOrientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setOneShotPreviewCallback(PreviewCallback callback) {
|
||||||
|
if (camera != null) camera.setOneShotPreviewCallback(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Camera.Parameters getCameraParameters() {
|
||||||
|
return camera.getParameters();
|
||||||
|
}
|
||||||
|
|
||||||
|
void previewCreated() {
|
||||||
|
final CameraHost host = getHost();
|
||||||
|
submitTask(new PostInitializationTask<Void>() {
|
||||||
|
@Override protected void onPostMain(Void avoid) {
|
||||||
|
try {
|
||||||
|
if (camera != null) {
|
||||||
|
previewStrategy.attach(camera);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
host.handleException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void previewDestroyed() {
|
||||||
|
if (camera != null) {
|
||||||
|
previewStopped();
|
||||||
|
camera.release();
|
||||||
|
camera = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void previewReset() {
|
||||||
|
previewStopped();
|
||||||
|
initPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void previewStopped() {
|
||||||
|
if (inPreview) {
|
||||||
|
stopPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||||
|
public void initPreview() {
|
||||||
|
submitTask(new PostInitializationTask<Void>() {
|
||||||
|
@Override protected void onPostMain(Void avoid) {
|
||||||
|
if (camera != null && cameraReady) {
|
||||||
|
Camera.Parameters parameters = camera.getParameters();
|
||||||
|
|
||||||
|
parameters.setPreviewSize(previewSize.width, previewSize.height);
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
|
||||||
|
parameters.setRecordingHint(getHost().getRecordingHint() != CameraHost.RecordingHint.STILL_ONLY);
|
||||||
|
}
|
||||||
|
|
||||||
|
camera.setParameters(getHost().adjustPreviewParameters(parameters));
|
||||||
|
startPreview();
|
||||||
|
requestLayout();
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startPreview() {
|
||||||
|
Log.w(TAG, "startPreview()");
|
||||||
|
camera.startPreview();
|
||||||
|
inPreview = true;
|
||||||
|
getHost().autoFocusAvailable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void stopPreview() {
|
||||||
|
inPreview = false;
|
||||||
|
getHost().autoFocusUnavailable();
|
||||||
|
camera.stopPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
// based on
|
||||||
|
// http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
|
||||||
|
// and http://stackoverflow.com/a/10383164/115145
|
||||||
|
private void setCameraDisplayOrientation() {
|
||||||
|
Camera.CameraInfo info = new Camera.CameraInfo();
|
||||||
|
int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
|
||||||
|
int degrees = 0;
|
||||||
|
DisplayMetrics dm = new DisplayMetrics();
|
||||||
|
|
||||||
|
Camera.getCameraInfo(cameraId, info);
|
||||||
|
getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
|
||||||
|
|
||||||
|
switch (rotation) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
||||||
|
displayOrientation = (info.orientation + degrees ) % 360;
|
||||||
|
displayOrientation = (360 - displayOrientation) % 360;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
displayOrientation = (info.orientation - degrees + 360) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean wasInPreview = inPreview;
|
||||||
|
|
||||||
|
if (inPreview) {
|
||||||
|
stopPreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
camera.setDisplayOrientation(displayOrientation);
|
||||||
|
|
||||||
|
if (wasInPreview) {
|
||||||
|
startPreview();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getCameraPictureOrientation() {
|
||||||
|
Camera.CameraInfo info = new Camera.CameraInfo();
|
||||||
|
|
||||||
|
Camera.getCameraInfo(cameraId, info);
|
||||||
|
|
||||||
|
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
|
||||||
|
outputOrientation = getCameraPictureRotation(getActivity().getWindowManager()
|
||||||
|
.getDefaultDisplay()
|
||||||
|
.getOrientation());
|
||||||
|
} else if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
||||||
|
outputOrientation = (360 - displayOrientation) % 360;
|
||||||
|
} else {
|
||||||
|
outputOrientation = displayOrientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastPictureOrientation != outputOrientation) {
|
||||||
|
lastPictureOrientation = outputOrientation;
|
||||||
|
}
|
||||||
|
return outputOrientation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// based on:
|
||||||
|
// http://developer.android.com/reference/android/hardware/Camera.Parameters.html#setRotation(int)
|
||||||
|
|
||||||
|
public int getCameraPictureRotation(int orientation) {
|
||||||
|
Camera.CameraInfo info = new Camera.CameraInfo();
|
||||||
|
Camera.getCameraInfo(cameraId, info);
|
||||||
|
int rotation;
|
||||||
|
|
||||||
|
orientation = (orientation + 45) / 90 * 90;
|
||||||
|
|
||||||
|
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
||||||
|
rotation = (info.orientation - orientation + 360) % 360;
|
||||||
|
}
|
||||||
|
else { // back-facing camera
|
||||||
|
rotation = (info.orientation + orientation) % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
Activity getActivity() {
|
||||||
|
return (Activity)getContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OnOrientationChange extends OrientationEventListener {
|
||||||
|
public OnOrientationChange(Context context) {
|
||||||
|
super(context);
|
||||||
|
disable();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOrientationChanged(int orientation) {
|
||||||
|
if (camera != null && orientation != ORIENTATION_UNKNOWN) {
|
||||||
|
int newOutputOrientation = getCameraPictureRotation(orientation);
|
||||||
|
|
||||||
|
if (newOutputOrientation != outputOrientation) {
|
||||||
|
outputOrientation = newOutputOrientation;
|
||||||
|
|
||||||
|
Camera.Parameters params = camera.getParameters();
|
||||||
|
|
||||||
|
params.setRotation(outputOrientation);
|
||||||
|
|
||||||
|
try {
|
||||||
|
camera.setParameters(params);
|
||||||
|
lastPictureOrientation = outputOrientation;
|
||||||
|
}
|
||||||
|
catch (Exception e) {
|
||||||
|
Log.e(TAG, "Exception updating camera parameters in orientation change", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void submitTask(SerializedAsyncTask job) {
|
||||||
|
ApplicationContext.getInstance(getContext()).getJobManager().add(job);
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract class SerializedAsyncTask<Result> extends Job {
|
||||||
|
|
||||||
|
public SerializedAsyncTask() {
|
||||||
|
super(JobParameters.newBuilder().withGroupId(CameraView.class.getSimpleName()).create());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void onAdded() {}
|
||||||
|
|
||||||
|
@Override public final void onRun() {
|
||||||
|
onWait();
|
||||||
|
runOnMainSync(new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
onPreMain();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
final Result result = onRunBackground();
|
||||||
|
|
||||||
|
runOnMainSync(new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
onPostMain(result);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public boolean onShouldRetry(Exception e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override public void onCanceled() { }
|
||||||
|
|
||||||
|
private void runOnMainSync(final Runnable runnable) {
|
||||||
|
final CountDownLatch sync = new CountDownLatch(1);
|
||||||
|
Util.runOnMain(new Runnable() {
|
||||||
|
@Override public void run() {
|
||||||
|
try {
|
||||||
|
runnable.run();
|
||||||
|
} finally {
|
||||||
|
sync.countDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
sync.await();
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
throw new AssertionError(ie);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void onWait() {}
|
||||||
|
protected void onPreMain() {}
|
||||||
|
protected Result onRunBackground() { return null; }
|
||||||
|
protected void onPostMain(Result result) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
private abstract class PostInitializationTask<Result> extends SerializedAsyncTask<Result> {
|
||||||
|
@Override protected void onWait() {
|
||||||
|
synchronized (CameraView.this) {
|
||||||
|
while (camera == null || previewSize == null) {
|
||||||
|
Util.wait(CameraView.this, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,50 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.camera;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.animation.Animation;
|
||||||
|
import android.view.animation.Animation.AnimationListener;
|
||||||
|
import android.view.animation.AnimationUtils;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
|
||||||
|
public class HidingImageButton extends ImageButton {
|
||||||
|
public HidingImageButton(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HidingImageButton(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public HidingImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
super(context, attrs, defStyleAttr);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void hide() {
|
||||||
|
final Animation animation = AnimationUtils.loadAnimation(getContext(), R.anim.slide_to_right);
|
||||||
|
animation.setAnimationListener(new AnimationListener() {
|
||||||
|
@Override public void onAnimationStart(Animation animation) {}
|
||||||
|
@Override public void onAnimationRepeat(Animation animation) {}
|
||||||
|
@Override public void onAnimationEnd(Animation animation) {
|
||||||
|
setVisibility(GONE);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
animateWith(animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void show() {
|
||||||
|
setVisibility(VISIBLE);
|
||||||
|
animateWith(AnimationUtils.loadAnimation(getContext(), R.anim.slide_from_right));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void animateWith(Animation animation) {
|
||||||
|
animation.setDuration(150);
|
||||||
|
animation.setInterpolator(new FastOutSlowInInterpolator());
|
||||||
|
startAnimation(animation);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,540 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.camera;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.hardware.Camera;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Build.VERSION;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.v4.view.MotionEventCompat;
|
||||||
|
import android.support.v4.view.ViewCompat;
|
||||||
|
import android.support.v4.widget.ViewDragHelper;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.Surface;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
import android.widget.ImageButton;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.components.camera.QuickCamera.QuickCameraListener;
|
||||||
|
import org.thoughtcrime.securesms.util.SoftKeyboardUtil;
|
||||||
|
|
||||||
|
public class QuickAttachmentDrawer extends ViewGroup {
|
||||||
|
private static final String TAG = QuickAttachmentDrawer.class.getSimpleName();
|
||||||
|
private static final float FULL_EXPANDED_ANCHOR_POINT = 1.f;
|
||||||
|
private static final float COLLAPSED_ANCHOR_POINT = 0.f;
|
||||||
|
|
||||||
|
private final ViewDragHelper dragHelper;
|
||||||
|
|
||||||
|
private QuickCamera quickCamera;
|
||||||
|
private int coverViewPosition;
|
||||||
|
private View coverView;
|
||||||
|
private View controls;
|
||||||
|
private ImageButton fullScreenButton;
|
||||||
|
private ImageButton swapCameraButton;
|
||||||
|
private ImageButton shutterButton;
|
||||||
|
private float slideOffset;
|
||||||
|
private float initialMotionX;
|
||||||
|
private float initialMotionY;
|
||||||
|
private int rotation;
|
||||||
|
private int slideRange;
|
||||||
|
private int baseHalfHeight;
|
||||||
|
private AttachmentDrawerListener listener;
|
||||||
|
|
||||||
|
private DrawerState drawerState = DrawerState.COLLAPSED;
|
||||||
|
private float halfExpandedAnchorPoint = COLLAPSED_ANCHOR_POINT;
|
||||||
|
private boolean halfModeUnsupported = VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH;
|
||||||
|
private Rect drawChildrenRect = new Rect();
|
||||||
|
|
||||||
|
public QuickAttachmentDrawer(Context context) {
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuickAttachmentDrawer(Context context, AttributeSet attrs) {
|
||||||
|
this(context, attrs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuickAttachmentDrawer(Context context, AttributeSet attrs, int defStyle) {
|
||||||
|
super(context, attrs, defStyle);
|
||||||
|
baseHalfHeight = SoftKeyboardUtil.getKeyboardHeight(getContext());
|
||||||
|
dragHelper = ViewDragHelper.create(this, 1.f, new ViewDragHelperCallback());
|
||||||
|
initializeView();
|
||||||
|
updateHalfExpandedAnchorPoint();
|
||||||
|
onConfigurationChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initializeView() {
|
||||||
|
inflate(getContext(), R.layout.quick_attachment_drawer, this);
|
||||||
|
quickCamera = (QuickCamera) findViewById(R.id.quick_camera);
|
||||||
|
updateControlsView();
|
||||||
|
|
||||||
|
coverViewPosition = getChildCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
private WindowManager getWindowManager() {
|
||||||
|
return (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isDeviceSupported(Context context) {
|
||||||
|
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA) &&
|
||||||
|
Camera.getNumberOfCameras() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOpen() {
|
||||||
|
return getDrawerState().isVisible();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() {
|
||||||
|
setDrawerStateAndAnimate(DrawerState.COLLAPSED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void open() {
|
||||||
|
setDrawerStateAndAnimate(DrawerState.HALF_EXPANDED);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onConfigurationChanged() {
|
||||||
|
int rotation = getWindowManager().getDefaultDisplay().getRotation();
|
||||||
|
Log.w(TAG, String.format("onNewOrientation(old %d, new %d)", this.rotation, rotation));
|
||||||
|
final boolean rotationChanged = this.rotation != rotation;
|
||||||
|
this.rotation = rotation;
|
||||||
|
if (rotationChanged) {
|
||||||
|
if (isOpen()) {
|
||||||
|
quickCamera.onPause();
|
||||||
|
setDrawerStateAndAnimate(getDrawerState());
|
||||||
|
}
|
||||||
|
updateControlsView();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateControlsView() {
|
||||||
|
int controlsIndex = indexOfChild(controls);
|
||||||
|
removeView(controls);
|
||||||
|
controls = LayoutInflater.from(getContext()).inflate(isLandscape() ? R.layout.quick_camera_controls_land
|
||||||
|
: R.layout.quick_camera_controls,
|
||||||
|
this, false);
|
||||||
|
shutterButton = (ImageButton) controls.findViewById(R.id.shutter_button);
|
||||||
|
swapCameraButton = (ImageButton) controls.findViewById(R.id.swap_camera_button);
|
||||||
|
fullScreenButton = (ImageButton) controls.findViewById(R.id.fullscreen_button);
|
||||||
|
if (quickCamera.isMultipleCameras()) {
|
||||||
|
swapCameraButton.setVisibility(View.VISIBLE);
|
||||||
|
swapCameraButton.setOnClickListener(new CameraFlipClickListener());
|
||||||
|
}
|
||||||
|
shutterButton.setOnClickListener(new ShutterClickListener());
|
||||||
|
fullScreenButton.setOnClickListener(new FullscreenClickListener());
|
||||||
|
addView(controls, controlsIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isLandscape() {
|
||||||
|
return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isFullscreenOnly() {
|
||||||
|
return isLandscape() || halfModeUnsupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateHalfExpandedAnchorPoint() {
|
||||||
|
getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
|
||||||
|
@SuppressWarnings("deprecation") @Override public void onGlobalLayout() {
|
||||||
|
if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
|
getViewTreeObserver().removeOnGlobalLayoutListener(this);
|
||||||
|
} else {
|
||||||
|
getViewTreeObserver().removeGlobalOnLayoutListener(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
coverView = getChildAt(coverViewPosition);
|
||||||
|
slideRange = getMeasuredHeight();
|
||||||
|
halfExpandedAnchorPoint = computeSlideOffsetFromCoverBottom(slideRange - baseHalfHeight);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||||
|
final int paddingLeft = getPaddingLeft();
|
||||||
|
final int paddingTop = getPaddingTop();
|
||||||
|
|
||||||
|
for (int i = 0; i < getChildCount(); i++) {
|
||||||
|
final View child = getChildAt(i);
|
||||||
|
final int childHeight = child.getMeasuredHeight();
|
||||||
|
|
||||||
|
int childTop = paddingTop;
|
||||||
|
int childLeft = paddingLeft;
|
||||||
|
int childBottom;
|
||||||
|
|
||||||
|
if (child == quickCamera) {
|
||||||
|
childTop = computeCameraTopPosition(slideOffset);
|
||||||
|
childBottom = childTop + childHeight;
|
||||||
|
if (quickCamera.getMeasuredWidth() < getMeasuredWidth())
|
||||||
|
childLeft = (getMeasuredWidth() - quickCamera.getMeasuredWidth()) / 2 + paddingLeft;
|
||||||
|
} else if (child == controls) {
|
||||||
|
childBottom = getMeasuredHeight();
|
||||||
|
} else {
|
||||||
|
childBottom = computeCoverBottomPosition(slideOffset);
|
||||||
|
childTop = childBottom - childHeight;
|
||||||
|
}
|
||||||
|
final int childRight = childLeft + child.getMeasuredWidth();
|
||||||
|
|
||||||
|
child.layout(childLeft, childTop, childRight, childBottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
|
||||||
|
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
|
||||||
|
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
|
||||||
|
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
|
||||||
|
|
||||||
|
if (widthMode != MeasureSpec.EXACTLY) {
|
||||||
|
throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
|
||||||
|
} else if (heightMode != MeasureSpec.EXACTLY) {
|
||||||
|
throw new IllegalStateException("Height must have an exact value or MATCH_PARENT");
|
||||||
|
}
|
||||||
|
|
||||||
|
int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
|
||||||
|
|
||||||
|
for (int i = 0; i < getChildCount(); i++) {
|
||||||
|
final View child = getChildAt(i);
|
||||||
|
final LayoutParams lp = child.getLayoutParams();
|
||||||
|
|
||||||
|
if (child.getVisibility() == GONE && i == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int childWidthSpec;
|
||||||
|
switch (lp.width) {
|
||||||
|
case LayoutParams.WRAP_CONTENT:
|
||||||
|
childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
|
||||||
|
break;
|
||||||
|
case LayoutParams.MATCH_PARENT:
|
||||||
|
childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int childHeightSpec;
|
||||||
|
switch (lp.height) {
|
||||||
|
case LayoutParams.WRAP_CONTENT:
|
||||||
|
childHeightSpec = MeasureSpec.makeMeasureSpec(layoutHeight, MeasureSpec.AT_MOST);
|
||||||
|
break;
|
||||||
|
case LayoutParams.MATCH_PARENT:
|
||||||
|
childHeightSpec = MeasureSpec.makeMeasureSpec(layoutHeight, MeasureSpec.EXACTLY);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
child.measure(childWidthSpec, childHeightSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
setMeasuredDimension(widthSize, heightSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
|
||||||
|
super.onSizeChanged(w, h, oldw, oldh);
|
||||||
|
if (h != oldh) updateHalfExpandedAnchorPoint();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) {
|
||||||
|
boolean result;
|
||||||
|
final int save = canvas.save(Canvas.CLIP_SAVE_FLAG);
|
||||||
|
|
||||||
|
canvas.getClipBounds(drawChildrenRect);
|
||||||
|
if (child == coverView)
|
||||||
|
drawChildrenRect.bottom = Math.min(drawChildrenRect.bottom, child.getBottom());
|
||||||
|
else if (coverView != null)
|
||||||
|
drawChildrenRect.top = Math.max(drawChildrenRect.top, coverView.getBottom());
|
||||||
|
canvas.clipRect(drawChildrenRect);
|
||||||
|
result = super.drawChild(canvas, child, drawingTime);
|
||||||
|
canvas.restoreToCount(save);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void computeScroll() {
|
||||||
|
if (dragHelper != null && dragHelper.continueSettling(true)) {
|
||||||
|
ViewCompat.postInvalidateOnAnimation(this);
|
||||||
|
} else if (!getDrawerState().isVisible()) {
|
||||||
|
quickCamera.onPause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDrawerState(DrawerState drawerState) {
|
||||||
|
switch (drawerState) {
|
||||||
|
case COLLAPSED:
|
||||||
|
slideOffset = COLLAPSED_ANCHOR_POINT;
|
||||||
|
fullScreenButton.setImageResource(R.drawable.quick_camera_fullscreen);
|
||||||
|
if (listener != null) listener.onAttachmentDrawerClosed();
|
||||||
|
break;
|
||||||
|
case HALF_EXPANDED:
|
||||||
|
if (isFullscreenOnly()) {
|
||||||
|
setDrawerState(DrawerState.FULL_EXPANDED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.w(TAG, "HALF_EXPANDED init");
|
||||||
|
quickCamera.onResume();
|
||||||
|
slideOffset = halfExpandedAnchorPoint;
|
||||||
|
fullScreenButton.setImageResource(R.drawable.quick_camera_fullscreen);
|
||||||
|
if (listener != null) listener.onAttachmentDrawerOpened();
|
||||||
|
break;
|
||||||
|
case FULL_EXPANDED:
|
||||||
|
quickCamera.onResume();
|
||||||
|
slideOffset = FULL_EXPANDED_ANCHOR_POINT;
|
||||||
|
fullScreenButton.setImageResource(isFullscreenOnly() ? R.drawable.quick_camera_hide
|
||||||
|
: R.drawable.quick_camera_exit_fullscreen);
|
||||||
|
if (listener != null) listener.onAttachmentDrawerOpened();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
this.drawerState = drawerState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public DrawerState getDrawerState() {
|
||||||
|
return drawerState;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDrawerStateAndAnimate(final DrawerState requestedDrawerState) {
|
||||||
|
DrawerState oldDrawerState = this.drawerState;
|
||||||
|
setDrawerState(requestedDrawerState);
|
||||||
|
if (oldDrawerState != drawerState) {
|
||||||
|
slideTo(slideOffset);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setListener(AttachmentDrawerListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
if (quickCamera != null) quickCamera.setQuickCameraListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface AttachmentDrawerListener extends QuickCameraListener {
|
||||||
|
void onAttachmentDrawerClosed();
|
||||||
|
void onAttachmentDrawerOpened();
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ViewDragHelperCallback extends ViewDragHelper.Callback {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean tryCaptureView(View child, int pointerId) {
|
||||||
|
return child == controls && !halfModeUnsupported;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewDragStateChanged(int state) {
|
||||||
|
if (dragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
|
||||||
|
setDrawerState(drawerState);
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewCaptured(View capturedChild, int activePointerId) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
|
||||||
|
final int expandedTop = computeCoverBottomPosition(FULL_EXPANDED_ANCHOR_POINT) - coverView.getHeight();
|
||||||
|
final int collapsedTop = computeCoverBottomPosition(COLLAPSED_ANCHOR_POINT) - coverView.getHeight();
|
||||||
|
final int newTop = Math.min(Math.max(coverView.getTop() + dy, expandedTop), collapsedTop);
|
||||||
|
slideOffset = computeSlideOffsetFromCoverBottom(newTop + coverView.getHeight());
|
||||||
|
requestLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onViewReleased(View releasedChild, float xvel, float yvel) {
|
||||||
|
if (releasedChild == controls) {
|
||||||
|
float direction = -yvel;
|
||||||
|
DrawerState drawerState = DrawerState.COLLAPSED;
|
||||||
|
|
||||||
|
if (direction > 1) {
|
||||||
|
drawerState = DrawerState.FULL_EXPANDED;
|
||||||
|
} else if (direction < -1) {
|
||||||
|
boolean halfExpand = (slideOffset > halfExpandedAnchorPoint && !isLandscape());
|
||||||
|
drawerState = halfExpand ? DrawerState.HALF_EXPANDED : DrawerState.COLLAPSED;
|
||||||
|
} else if (!isLandscape()) {
|
||||||
|
if (halfExpandedAnchorPoint != 1 && slideOffset >= (1.f + halfExpandedAnchorPoint) / 2) {
|
||||||
|
drawerState = DrawerState.FULL_EXPANDED;
|
||||||
|
} else if (halfExpandedAnchorPoint == 1 && slideOffset >= 0.5f) {
|
||||||
|
drawerState = DrawerState.FULL_EXPANDED;
|
||||||
|
} else if (halfExpandedAnchorPoint != 1 && slideOffset >= halfExpandedAnchorPoint) {
|
||||||
|
drawerState = DrawerState.HALF_EXPANDED;
|
||||||
|
} else if (halfExpandedAnchorPoint != 1 && slideOffset >= halfExpandedAnchorPoint / 2) {
|
||||||
|
drawerState = DrawerState.HALF_EXPANDED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setDrawerState(drawerState);
|
||||||
|
dragHelper.captureChildView(coverView, 0);
|
||||||
|
Log.w(TAG, String.format("setting cover at %d", computeCoverBottomPosition(slideOffset) - coverView.getHeight()));
|
||||||
|
dragHelper.settleCapturedViewAt(coverView.getLeft(), computeCoverBottomPosition(slideOffset) - coverView.getHeight());
|
||||||
|
dragHelper.captureChildView(quickCamera, 0);
|
||||||
|
dragHelper.settleCapturedViewAt(quickCamera.getLeft(), computeCameraTopPosition(slideOffset));
|
||||||
|
ViewCompat.postInvalidateOnAnimation(QuickAttachmentDrawer.this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getViewVerticalDragRange(View child) {
|
||||||
|
return slideRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int clampViewPositionVertical(View child, int top, int dy) {
|
||||||
|
return top;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onInterceptTouchEvent(MotionEvent event) {
|
||||||
|
if (dragHelper != null) {
|
||||||
|
final int action = MotionEventCompat.getActionMasked(event);
|
||||||
|
|
||||||
|
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
|
||||||
|
dragHelper.cancel();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final float x = event.getX();
|
||||||
|
final float y = event.getY();
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
initialMotionX = x;
|
||||||
|
initialMotionY = y;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
final float adx = Math.abs(x - initialMotionX);
|
||||||
|
final float ady = Math.abs(y - initialMotionY);
|
||||||
|
final int dragSlop = dragHelper.getTouchSlop();
|
||||||
|
|
||||||
|
if (adx > dragSlop && ady < dragSlop) {
|
||||||
|
return super.onInterceptTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((ady > dragSlop && adx > ady) || !isDragViewUnder((int) initialMotionX, (int) initialMotionY)) {
|
||||||
|
dragHelper.cancel();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return dragHelper.shouldInterceptTouchEvent(event);
|
||||||
|
}
|
||||||
|
return super.onInterceptTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(@NonNull MotionEvent event) {
|
||||||
|
if (dragHelper != null) {
|
||||||
|
dragHelper.processTouchEvent(event);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onTouchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Android Studio bug misreports error, squashing the warning.
|
||||||
|
// https://code.google.com/p/android/issues/detail?id=175977
|
||||||
|
@SuppressWarnings("ResourceType")
|
||||||
|
private boolean isDragViewUnder(int x, int y) {
|
||||||
|
int[] viewLocation = new int[2];
|
||||||
|
quickCamera.getLocationOnScreen(viewLocation);
|
||||||
|
int[] parentLocation = new int[2];
|
||||||
|
this.getLocationOnScreen(parentLocation);
|
||||||
|
int screenX = parentLocation[0] + x;
|
||||||
|
int screenY = parentLocation[1] + y;
|
||||||
|
return screenX >= viewLocation[0] && screenX < viewLocation[0] + quickCamera.getWidth() &&
|
||||||
|
screenY >= viewLocation[1] && screenY < viewLocation[1] + quickCamera.getHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
private int computeCameraTopPosition(float slideOffset) {
|
||||||
|
float clampedOffset = slideOffset - halfExpandedAnchorPoint;
|
||||||
|
if (clampedOffset < COLLAPSED_ANCHOR_POINT) {
|
||||||
|
clampedOffset = COLLAPSED_ANCHOR_POINT;
|
||||||
|
} else {
|
||||||
|
clampedOffset = clampedOffset / (FULL_EXPANDED_ANCHOR_POINT - halfExpandedAnchorPoint);
|
||||||
|
}
|
||||||
|
float slidePixelOffset = slideOffset * slideRange +
|
||||||
|
(quickCamera.getMeasuredHeight() - baseHalfHeight) / 2 *
|
||||||
|
(FULL_EXPANDED_ANCHOR_POINT - clampedOffset);
|
||||||
|
float marginPixelOffset = (getMeasuredHeight() - quickCamera.getMeasuredHeight()) / 2 * clampedOffset;
|
||||||
|
return (int) (getMeasuredHeight() - slidePixelOffset + marginPixelOffset);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int computeCoverBottomPosition(float slideOffset) {
|
||||||
|
int slidePixelOffset = (int) (slideOffset * slideRange);
|
||||||
|
return getMeasuredHeight() - getPaddingBottom() - slidePixelOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void slideTo(float slideOffset) {
|
||||||
|
if (dragHelper != null && !halfModeUnsupported) {
|
||||||
|
dragHelper.smoothSlideViewTo(coverView, coverView.getLeft(), computeCoverBottomPosition(slideOffset) - coverView.getHeight());
|
||||||
|
dragHelper.smoothSlideViewTo(quickCamera, quickCamera.getLeft(), computeCameraTopPosition(slideOffset));
|
||||||
|
ViewCompat.postInvalidateOnAnimation(this);
|
||||||
|
} else {
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float computeSlideOffsetFromCoverBottom(int topPosition) {
|
||||||
|
final int topBoundCollapsed = computeCoverBottomPosition(0);
|
||||||
|
return (float) (topBoundCollapsed - topPosition) / slideRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onPause() {
|
||||||
|
quickCamera.onPause();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onResume() {
|
||||||
|
if (drawerState.isVisible()) quickCamera.onResume();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum DrawerState {
|
||||||
|
COLLAPSED, HALF_EXPANDED, FULL_EXPANDED;
|
||||||
|
|
||||||
|
public boolean isVisible() {
|
||||||
|
return this == HALF_EXPANDED || this == FULL_EXPANDED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ShutterClickListener implements OnClickListener {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
boolean crop = drawerState != DrawerState.FULL_EXPANDED;
|
||||||
|
int imageHeight = crop ? baseHalfHeight : quickCamera.getMeasuredHeight();
|
||||||
|
Rect previewRect = new Rect(0, 0, quickCamera.getMeasuredWidth(), imageHeight);
|
||||||
|
quickCamera.takePicture(previewRect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class CameraFlipClickListener implements OnClickListener {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
quickCamera.swapCamera();
|
||||||
|
swapCameraButton.setImageResource(quickCamera.isRearCamera() ? R.drawable.quick_camera_front
|
||||||
|
: R.drawable.quick_camera_rear);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FullscreenClickListener implements OnClickListener {
|
||||||
|
@Override
|
||||||
|
public void onClick(View v) {
|
||||||
|
if (drawerState != DrawerState.FULL_EXPANDED) {
|
||||||
|
setDrawerStateAndAnimate(DrawerState.FULL_EXPANDED);
|
||||||
|
} else if (isFullscreenOnly()) {
|
||||||
|
setDrawerStateAndAnimate(DrawerState.COLLAPSED);
|
||||||
|
} else {
|
||||||
|
setDrawerStateAndAnimate(DrawerState.HALF_EXPANDED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,187 @@
|
|||||||
|
package org.thoughtcrime.securesms.components.camera;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.hardware.Camera;
|
||||||
|
import android.hardware.Camera.CameraInfo;
|
||||||
|
import android.hardware.Camera.Parameters;
|
||||||
|
import android.hardware.Camera.Size;
|
||||||
|
import android.os.AsyncTask;
|
||||||
|
import android.os.Build.VERSION_CODES;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.commonsware.cwac.camera.SimpleCameraHost;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
import org.thoughtcrime.securesms.util.BitmapUtil;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@SuppressWarnings("deprecation") public class QuickCamera extends CameraView {
|
||||||
|
private static final String TAG = QuickCamera.class.getSimpleName();
|
||||||
|
|
||||||
|
private QuickCameraListener listener;
|
||||||
|
private boolean capturing;
|
||||||
|
private boolean started;
|
||||||
|
private QuickCameraHost cameraHost;
|
||||||
|
|
||||||
|
public QuickCamera(Context context) {
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuickCamera(Context context, AttributeSet attrs) {
|
||||||
|
this(context, attrs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public QuickCamera(Context context, AttributeSet attrs, int defStyle) {
|
||||||
|
super(context, attrs, defStyle);
|
||||||
|
cameraHost = new QuickCameraHost(context);
|
||||||
|
setHost(cameraHost);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onResume() {
|
||||||
|
if (started) return;
|
||||||
|
super.onResume();
|
||||||
|
started = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPause() {
|
||||||
|
if (!started) return;
|
||||||
|
super.onPause();
|
||||||
|
started = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isStarted() {
|
||||||
|
return started;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void takePicture(final Rect previewRect) {
|
||||||
|
if (capturing) {
|
||||||
|
Log.w(TAG, "takePicture() called while previous capture pending.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Parameters cameraParameters = getCameraParameters();
|
||||||
|
setOneShotPreviewCallback(new Camera.PreviewCallback() {
|
||||||
|
@Override
|
||||||
|
public void onPreviewFrame(byte[] data, final Camera camera) {
|
||||||
|
final int rotation = getCameraPictureOrientation();
|
||||||
|
|
||||||
|
new AsyncTask<byte[], Void, Bitmap>() {
|
||||||
|
@Override
|
||||||
|
protected Bitmap doInBackground(byte[]... params) {
|
||||||
|
byte[] data = params[0];
|
||||||
|
try {
|
||||||
|
Size previewSize = cameraParameters.getPreviewSize();
|
||||||
|
|
||||||
|
return BitmapUtil.createFromNV21(data,
|
||||||
|
previewSize.width,
|
||||||
|
previewSize.height,
|
||||||
|
rotation,
|
||||||
|
getCroppedRect(previewSize, previewRect, rotation));
|
||||||
|
} catch (IOException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onPostExecute(Bitmap bitmap) {
|
||||||
|
capturing = false;
|
||||||
|
if (bitmap != null && listener != null) listener.onImageCapture(bitmap);
|
||||||
|
}
|
||||||
|
}.execute(data);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private Rect getCroppedRect(Size cameraPreviewSize, Rect visibleRect, int rotation) {
|
||||||
|
final int previewWidth = cameraPreviewSize.width;
|
||||||
|
final int previewHeight = cameraPreviewSize.height;
|
||||||
|
|
||||||
|
if (rotation % 180 > 0) {
|
||||||
|
//noinspection SuspiciousNameCombination
|
||||||
|
visibleRect.set(visibleRect.top, visibleRect.left, visibleRect.bottom, visibleRect.right);
|
||||||
|
}
|
||||||
|
|
||||||
|
float scale = (float) previewWidth / visibleRect.width();
|
||||||
|
if (visibleRect.height() * scale > previewHeight) {
|
||||||
|
scale = (float) previewHeight / visibleRect.height();
|
||||||
|
}
|
||||||
|
final float newWidth = visibleRect.width() * scale;
|
||||||
|
final float newHeight = visibleRect.height() * scale;
|
||||||
|
final float centerX = previewWidth / 2;
|
||||||
|
final float centerY = previewHeight / 2;
|
||||||
|
visibleRect.set((int) (centerX - newWidth / 2),
|
||||||
|
(int) (centerY - newHeight / 2),
|
||||||
|
(int) (centerX + newWidth / 2),
|
||||||
|
(int) (centerY + newHeight / 2));
|
||||||
|
return visibleRect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQuickCameraListener(QuickCameraListener listener) {
|
||||||
|
this.listener = listener;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isMultipleCameras() {
|
||||||
|
return Camera.getNumberOfCameras() > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRearCamera() {
|
||||||
|
return cameraHost.getCameraId() == Camera.CameraInfo.CAMERA_FACING_BACK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void swapCamera() {
|
||||||
|
cameraHost.swapCameraId();
|
||||||
|
onPause();
|
||||||
|
onResume();
|
||||||
|
}
|
||||||
|
|
||||||
|
public interface QuickCameraListener {
|
||||||
|
void onImageCapture(@NonNull final Bitmap bitmap);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class QuickCameraHost extends SimpleCameraHost {
|
||||||
|
int cameraId = CameraInfo.CAMERA_FACING_BACK;
|
||||||
|
|
||||||
|
public QuickCameraHost(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(VERSION_CODES.ICE_CREAM_SANDWICH) @Override
|
||||||
|
public Parameters adjustPreviewParameters(Parameters parameters) {
|
||||||
|
List<String> focusModes = parameters.getSupportedFocusModes();
|
||||||
|
if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
|
||||||
|
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
|
||||||
|
} else if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
|
||||||
|
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
|
||||||
|
}
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCameraId() {
|
||||||
|
return cameraId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void swapCameraId() {
|
||||||
|
if (isMultipleCameras()) {
|
||||||
|
if (cameraId == CameraInfo.CAMERA_FACING_BACK) cameraId = CameraInfo.CAMERA_FACING_FRONT;
|
||||||
|
else cameraId = CameraInfo.CAMERA_FACING_BACK;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCameraFail(FailureReason reason) {
|
||||||
|
super.onCameraFail(reason);
|
||||||
|
Toast.makeText(getContext(), R.string.quick_camera_unavailable, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,10 +12,11 @@
|
|||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components.camera;
|
||||||
|
|
||||||
import android.hardware.Camera;
|
import android.hardware.Camera;
|
||||||
import android.media.MediaRecorder;
|
import android.media.MediaRecorder;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.SurfaceHolder;
|
import android.view.SurfaceHolder;
|
||||||
import android.view.SurfaceView;
|
import android.view.SurfaceView;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -26,6 +27,7 @@ import java.io.IOException;
|
|||||||
|
|
||||||
class SurfacePreviewStrategy implements PreviewStrategy,
|
class SurfacePreviewStrategy implements PreviewStrategy,
|
||||||
SurfaceHolder.Callback {
|
SurfaceHolder.Callback {
|
||||||
|
private final static String TAG = SurfacePreviewStrategy.class.getSimpleName();
|
||||||
private final CameraView cameraView;
|
private final CameraView cameraView;
|
||||||
private SurfaceView preview=null;
|
private SurfaceView preview=null;
|
||||||
private SurfaceHolder previewHolder=null;
|
private SurfaceHolder previewHolder=null;
|
||||||
@ -41,22 +43,26 @@ class SurfacePreviewStrategy implements PreviewStrategy,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void surfaceCreated(SurfaceHolder holder) {
|
public void surfaceCreated(SurfaceHolder holder) {
|
||||||
|
Log.w(TAG, "surfaceCreated()");
|
||||||
cameraView.previewCreated();
|
cameraView.previewCreated();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void surfaceChanged(SurfaceHolder holder, int format,
|
public void surfaceChanged(SurfaceHolder holder, int format,
|
||||||
int width, int height) {
|
int width, int height) {
|
||||||
cameraView.initPreview(width, height);
|
Log.w(TAG, "surfaceChanged()");
|
||||||
|
cameraView.initPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void surfaceDestroyed(SurfaceHolder holder) {
|
public void surfaceDestroyed(SurfaceHolder holder) {
|
||||||
|
Log.w(TAG, "surfaceDestroyed()");
|
||||||
cameraView.previewDestroyed();
|
cameraView.previewDestroyed();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void attach(Camera camera) throws IOException {
|
public void attach(Camera camera) throws IOException {
|
||||||
|
Log.w(TAG, "attach(Camera)");
|
||||||
camera.setPreviewDisplay(previewHolder);
|
camera.setPreviewDisplay(previewHolder);
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,4 @@
|
|||||||
package org.thoughtcrime.securesms.components;
|
package org.thoughtcrime.securesms.components.camera;
|
||||||
/***
|
/***
|
||||||
Copyright (c) 2013 CommonsWare, LLC
|
Copyright (c) 2013 CommonsWare, LLC
|
||||||
|
|
||||||
@ -18,6 +18,7 @@ import android.graphics.SurfaceTexture;
|
|||||||
import android.hardware.Camera;
|
import android.hardware.Camera;
|
||||||
import android.media.MediaRecorder;
|
import android.media.MediaRecorder;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.TextureView;
|
import android.view.TextureView;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ import java.io.IOException;
|
|||||||
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||||
class TexturePreviewStrategy implements PreviewStrategy,
|
class TexturePreviewStrategy implements PreviewStrategy,
|
||||||
TextureView.SurfaceTextureListener {
|
TextureView.SurfaceTextureListener {
|
||||||
|
private final static String TAG = TexturePreviewStrategy.class.getSimpleName();
|
||||||
private final CameraView cameraView;
|
private final CameraView cameraView;
|
||||||
private TextureView widget=null;
|
private TextureView widget=null;
|
||||||
private SurfaceTexture surface=null;
|
private SurfaceTexture surface=null;
|
||||||
@ -41,20 +43,23 @@ class TexturePreviewStrategy implements PreviewStrategy,
|
|||||||
@Override
|
@Override
|
||||||
public void onSurfaceTextureAvailable(SurfaceTexture surface,
|
public void onSurfaceTextureAvailable(SurfaceTexture surface,
|
||||||
int width, int height) {
|
int width, int height) {
|
||||||
|
Log.w(TAG, "onSurfaceTextureAvailable()");
|
||||||
this.surface=surface;
|
this.surface=surface;
|
||||||
|
|
||||||
cameraView.previewCreated();
|
cameraView.previewCreated();
|
||||||
cameraView.initPreview(width, height);
|
cameraView.initPreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSurfaceTextureSizeChanged(SurfaceTexture surface,
|
public void onSurfaceTextureSizeChanged(SurfaceTexture surface,
|
||||||
int width, int height) {
|
int width, int height) {
|
||||||
cameraView.previewReset(width, height);
|
Log.w(TAG, "onSurfaceTextureChanged()");
|
||||||
|
cameraView.previewReset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
|
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
|
||||||
|
Log.w(TAG, "onSurfaceTextureDestroyed()");
|
||||||
cameraView.previewDestroyed();
|
cameraView.previewDestroyed();
|
||||||
|
|
||||||
return(true);
|
return(true);
|
||||||
@ -67,6 +72,7 @@ class TexturePreviewStrategy implements PreviewStrategy,
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void attach(Camera camera) throws IOException {
|
public void attach(Camera camera) throws IOException {
|
||||||
|
Log.w(TAG, "attach(Camera)");
|
||||||
camera.setPreviewTexture(surface);
|
camera.setPreviewTexture(surface);
|
||||||
}
|
}
|
||||||
|
|
@ -105,7 +105,6 @@ public class DraftDatabase extends Database {
|
|||||||
public static final String IMAGE = "image";
|
public static final String IMAGE = "image";
|
||||||
public static final String VIDEO = "video";
|
public static final String VIDEO = "video";
|
||||||
public static final String AUDIO = "audio";
|
public static final String AUDIO = "audio";
|
||||||
public static final String ENCRYPTED_IMAGE = "encrypted_image";
|
|
||||||
|
|
||||||
private final String type;
|
private final String type;
|
||||||
private final String value;
|
private final String value;
|
||||||
@ -126,7 +125,6 @@ public class DraftDatabase extends Database {
|
|||||||
public String getSnippet(Context context) {
|
public String getSnippet(Context context) {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TEXT: return value;
|
case TEXT: return value;
|
||||||
case ENCRYPTED_IMAGE:
|
|
||||||
case IMAGE: return context.getString(R.string.DraftDatabase_Draft_image_snippet);
|
case IMAGE: return context.getString(R.string.DraftDatabase_Draft_image_snippet);
|
||||||
case VIDEO: return context.getString(R.string.DraftDatabase_Draft_video_snippet);
|
case VIDEO: return context.getString(R.string.DraftDatabase_Draft_video_snippet);
|
||||||
case AUDIO: return context.getString(R.string.DraftDatabase_Draft_audio_snippet);
|
case AUDIO: return context.getString(R.string.DraftDatabase_Draft_audio_snippet);
|
||||||
|
@ -20,10 +20,10 @@ import android.app.Activity;
|
|||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.provider.ContactsContract;
|
import android.provider.ContactsContract;
|
||||||
import android.provider.MediaStore;
|
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@ -38,9 +38,9 @@ import android.widget.Toast;
|
|||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.components.ThumbnailView;
|
import org.thoughtcrime.securesms.components.ThumbnailView;
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
|
import org.thoughtcrime.securesms.providers.CaptureProvider;
|
||||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
public class AttachmentManager {
|
public class AttachmentManager {
|
||||||
@ -53,7 +53,7 @@ public class AttachmentManager {
|
|||||||
private final SlideDeck slideDeck;
|
private final SlideDeck slideDeck;
|
||||||
private final AttachmentListener attachmentListener;
|
private final AttachmentListener attachmentListener;
|
||||||
|
|
||||||
private File captureFile;
|
private Uri captureUri;
|
||||||
|
|
||||||
public AttachmentManager(Activity view, AttachmentListener listener) {
|
public AttachmentManager(Activity view, AttachmentListener listener) {
|
||||||
this.attachmentView = view.findViewById(R.id.attachment_editor);
|
this.attachmentView = view.findViewById(R.id.attachment_editor);
|
||||||
@ -92,12 +92,12 @@ public class AttachmentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void cleanup() {
|
public void cleanup() {
|
||||||
if (captureFile != null) captureFile.delete();
|
if (captureUri != null) CaptureProvider.getInstance(context).delete(captureUri);
|
||||||
captureFile = null;
|
captureUri = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setImage(Uri image) throws IOException, BitmapDecodingException {
|
public void setImage(MasterSecret masterSecret, Uri image) throws IOException, BitmapDecodingException {
|
||||||
setMedia(new ImageSlide(context, image));
|
setMedia(new ImageSlide(context, masterSecret, image), masterSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setVideo(Uri video) throws IOException, MediaTooLargeException {
|
public void setVideo(Uri video) throws IOException, MediaTooLargeException {
|
||||||
@ -108,20 +108,11 @@ public class AttachmentManager {
|
|||||||
setMedia(new AudioSlide(context, audio));
|
setMedia(new AudioSlide(context, audio));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEncryptedImage(Uri uri, MasterSecret masterSecret) throws IOException, BitmapDecodingException {
|
|
||||||
setMedia(new ImageSlide(context, masterSecret, uri), masterSecret);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMedia(final Slide slide) {
|
public void setMedia(final Slide slide) {
|
||||||
setMedia(slide, null);
|
setMedia(slide, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMedia(final Slide slide, @Nullable MasterSecret masterSecret) {
|
public void setMedia(final Slide slide, @Nullable MasterSecret masterSecret) {
|
||||||
Slide thumbnailSlide = slideDeck.getThumbnailSlide(context);
|
|
||||||
if (thumbnailSlide != null && thumbnailSlide.isEncrypted()) {
|
|
||||||
Uri dataUri = slideDeck.getThumbnailSlide(context).getPart().getDataUri();
|
|
||||||
new File(dataUri.getPath()).delete();
|
|
||||||
}
|
|
||||||
slideDeck.clear();
|
slideDeck.clear();
|
||||||
slideDeck.addSlide(slide);
|
slideDeck.addSlide(slide);
|
||||||
attachmentView.setVisibility(View.VISIBLE);
|
attachmentView.setVisibility(View.VISIBLE);
|
||||||
@ -137,20 +128,11 @@ public class AttachmentManager {
|
|||||||
return slideDeck;
|
return slideDeck;
|
||||||
}
|
}
|
||||||
|
|
||||||
public File getCaptureFile() {
|
public void setCaptureImage(MasterSecret masterSecret, Bitmap bitmap) {
|
||||||
return captureFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void capturePhoto(Activity activity, int requestCode) {
|
|
||||||
try {
|
try {
|
||||||
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
captureUri = CaptureProvider.getInstance(context).create(masterSecret, bitmap);
|
||||||
|
setImage(masterSecret, captureUri);
|
||||||
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
|
} catch (IOException | BitmapDecodingException e) {
|
||||||
captureFile = File.createTempFile(String.valueOf(System.currentTimeMillis()), ".jpg", activity.getExternalFilesDir(null));
|
|
||||||
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(captureFile));
|
|
||||||
activity.startActivityForResult(captureIntent, requestCode);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
Log.w(TAG, e);
|
Log.w(TAG, e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -204,6 +186,6 @@ public class AttachmentManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public interface AttachmentListener {
|
public interface AttachmentListener {
|
||||||
public void onAttachmentChanged();
|
void onAttachmentChanged();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,6 @@ public class AttachmentTypeSelectorAdapter extends ArrayAdapter<AttachmentTypeSe
|
|||||||
public static final int ADD_VIDEO = 2;
|
public static final int ADD_VIDEO = 2;
|
||||||
public static final int ADD_SOUND = 3;
|
public static final int ADD_SOUND = 3;
|
||||||
public static final int ADD_CONTACT_INFO = 4;
|
public static final int ADD_CONTACT_INFO = 4;
|
||||||
public static final int TAKE_PHOTO = 5;
|
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
@ -72,7 +71,6 @@ public class AttachmentTypeSelectorAdapter extends ArrayAdapter<AttachmentTypeSe
|
|||||||
|
|
||||||
private static List<IconListItem> getItemList(Context context) {
|
private static List<IconListItem> getItemList(Context context) {
|
||||||
List<IconListItem> data = new ArrayList<>(4);
|
List<IconListItem> data = new ArrayList<>(4);
|
||||||
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_take_photo), ResUtil.getDrawableRes(context, R.attr.conversation_attach_photo), TAKE_PHOTO);
|
|
||||||
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_picture), ResUtil.getDrawableRes(context, R.attr.conversation_attach_image), ADD_IMAGE);
|
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_picture), ResUtil.getDrawableRes(context, R.attr.conversation_attach_image), ADD_IMAGE);
|
||||||
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_video), ResUtil.getDrawableRes(context, R.attr.conversation_attach_video), ADD_VIDEO);
|
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_video), ResUtil.getDrawableRes(context, R.attr.conversation_attach_video), ADD_VIDEO);
|
||||||
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_audio), ResUtil.getDrawableRes(context, R.attr.conversation_attach_sound), ADD_SOUND);
|
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_audio), ResUtil.getDrawableRes(context, R.attr.conversation_attach_sound), ADD_SOUND);
|
||||||
|
@ -20,36 +20,25 @@ import android.content.Context;
|
|||||||
import android.content.res.Resources.Theme;
|
import android.content.res.Resources.Theme;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.support.annotation.DrawableRes;
|
import android.support.annotation.DrawableRes;
|
||||||
import android.support.annotation.Nullable;
|
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
|
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
import org.thoughtcrime.securesms.util.BitmapDecodingException;
|
||||||
import org.thoughtcrime.securesms.util.Util;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
|
||||||
|
|
||||||
import ws.com.google.android.mms.ContentType;
|
import ws.com.google.android.mms.ContentType;
|
||||||
import ws.com.google.android.mms.pdu.PduPart;
|
import ws.com.google.android.mms.pdu.PduPart;
|
||||||
|
|
||||||
public class ImageSlide extends Slide {
|
public class ImageSlide extends Slide {
|
||||||
private static final String TAG = ImageSlide.class.getSimpleName();
|
private static final String TAG = ImageSlide.class.getSimpleName();
|
||||||
private boolean encrypted = false;
|
|
||||||
|
|
||||||
public ImageSlide(Context context, MasterSecret masterSecret, PduPart part) {
|
public ImageSlide(Context context, MasterSecret masterSecret, PduPart part) {
|
||||||
super(context, masterSecret, part);
|
super(context, masterSecret, part);
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImageSlide(Context context, Uri uri) throws IOException, BitmapDecodingException {
|
|
||||||
this(context, null, uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
public ImageSlide(Context context, MasterSecret masterSecret, Uri uri) throws IOException, BitmapDecodingException {
|
public ImageSlide(Context context, MasterSecret masterSecret, Uri uri) throws IOException, BitmapDecodingException {
|
||||||
super(context, masterSecret, constructPartFromByteArrayAndUri(uri, decryptContent(uri, masterSecret), masterSecret != null));
|
super(context, masterSecret, constructPartFromUri(uri));
|
||||||
encrypted = masterSecret != null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -73,32 +62,12 @@ public class ImageSlide extends Slide {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private static PduPart constructPartFromUri(Uri uri)
|
||||||
public boolean isEncrypted() {
|
|
||||||
return encrypted;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static byte[] decryptContent(Uri uri, MasterSecret masterSecret) {
|
|
||||||
try {
|
|
||||||
if (masterSecret != null) {
|
|
||||||
InputStream inputStream = new DecryptingPartInputStream(new File(uri.getPath()), masterSecret);
|
|
||||||
return Util.readFully(inputStream);
|
|
||||||
}
|
|
||||||
} catch (IOException e) {
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static PduPart constructPartFromByteArrayAndUri(Uri uri, @Nullable byte[] data, boolean encrypted)
|
|
||||||
throws IOException, BitmapDecodingException
|
throws IOException, BitmapDecodingException
|
||||||
{
|
{
|
||||||
PduPart part = new PduPart();
|
PduPart part = new PduPart();
|
||||||
|
|
||||||
part.setDataUri(uri);
|
part.setDataUri(uri);
|
||||||
if (data != null)
|
|
||||||
part.setData(data);
|
|
||||||
part.setEncrypted(encrypted);
|
|
||||||
part.setContentType(ContentType.IMAGE_JPEG.getBytes());
|
part.setContentType(ContentType.IMAGE_JPEG.getBytes());
|
||||||
part.setContentId((System.currentTimeMillis()+"").getBytes());
|
part.setContentId((System.currentTimeMillis()+"").getBytes());
|
||||||
part.setName(("Image" + System.currentTimeMillis()).getBytes());
|
part.setName(("Image" + System.currentTimeMillis()).getBytes());
|
||||||
|
@ -5,13 +5,12 @@ import android.content.Context;
|
|||||||
import android.content.UriMatcher;
|
import android.content.UriMatcher;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
|
|
||||||
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||||
import org.thoughtcrime.securesms.database.PartDatabase;
|
import org.thoughtcrime.securesms.database.PartDatabase;
|
||||||
|
import org.thoughtcrime.securesms.providers.CaptureProvider;
|
||||||
import org.thoughtcrime.securesms.providers.PartProvider;
|
import org.thoughtcrime.securesms.providers.PartProvider;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
|
||||||
@ -24,6 +23,7 @@ public class PartAuthority {
|
|||||||
|
|
||||||
private static final int PART_ROW = 1;
|
private static final int PART_ROW = 1;
|
||||||
private static final int THUMB_ROW = 2;
|
private static final int THUMB_ROW = 2;
|
||||||
|
private static final int CAPTURE_ROW = 3;
|
||||||
|
|
||||||
private static final UriMatcher uriMatcher;
|
private static final UriMatcher uriMatcher;
|
||||||
|
|
||||||
@ -31,27 +31,24 @@ public class PartAuthority {
|
|||||||
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||||
uriMatcher.addURI("org.thoughtcrime.securesms", "part/*/#", PART_ROW);
|
uriMatcher.addURI("org.thoughtcrime.securesms", "part/*/#", PART_ROW);
|
||||||
uriMatcher.addURI("org.thoughtcrime.securesms", "thumb/*/#", THUMB_ROW);
|
uriMatcher.addURI("org.thoughtcrime.securesms", "thumb/*/#", THUMB_ROW);
|
||||||
|
uriMatcher.addURI(CaptureProvider.AUTHORITY, CaptureProvider.EXPECTED_PATH, CAPTURE_ROW);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static InputStream getPartStream(Context context, MasterSecret masterSecret, Uri uri)
|
public static InputStream getPartStream(Context context, MasterSecret masterSecret, Uri uri)
|
||||||
throws IOException
|
throws IOException
|
||||||
{
|
{
|
||||||
PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context);
|
|
||||||
int match = uriMatcher.match(uri);
|
int match = uriMatcher.match(uri);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (match) {
|
switch (match) {
|
||||||
case PART_ROW:
|
case PART_ROW:
|
||||||
PartUriParser partUri = new PartUriParser(uri);
|
PartUriParser partUri = new PartUriParser(uri);
|
||||||
return partDatabase.getPartStream(masterSecret, partUri.getPartId());
|
return DatabaseFactory.getPartDatabase(context).getPartStream(masterSecret, partUri.getPartId());
|
||||||
case THUMB_ROW:
|
case THUMB_ROW:
|
||||||
partUri = new PartUriParser(uri);
|
partUri = new PartUriParser(uri);
|
||||||
return partDatabase.getThumbnailStream(masterSecret, partUri.getPartId());
|
return DatabaseFactory.getPartDatabase(context).getThumbnailStream(masterSecret, partUri.getPartId());
|
||||||
|
case CAPTURE_ROW:
|
||||||
|
return CaptureProvider.getInstance(context).getStream(masterSecret, ContentUris.parseId(uri));
|
||||||
default:
|
default:
|
||||||
String tempMediaDir = context.getDir("media", Context.MODE_PRIVATE).getPath();
|
|
||||||
if (uri.getPath().startsWith(tempMediaDir))
|
|
||||||
return new DecryptingPartInputStream(new File(uri.getPath()), masterSecret);
|
|
||||||
else
|
|
||||||
return context.getContentResolver().openInputStream(uri);
|
return context.getContentResolver().openInputStream(uri);
|
||||||
}
|
}
|
||||||
} catch (SecurityException se) {
|
} catch (SecurityException se) {
|
||||||
|
@ -66,10 +66,6 @@ public abstract class Slide {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEncrypted() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public PduPart getPart() {
|
public PduPart getPart() {
|
||||||
return part;
|
return part;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,62 @@
|
|||||||
|
package org.thoughtcrime.securesms.providers;
|
||||||
|
|
||||||
|
import android.content.ContentUris;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Bitmap.CompressFormat;
|
||||||
|
import android.net.Uri;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
|
||||||
|
import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
|
||||||
|
import org.thoughtcrime.securesms.crypto.MasterSecret;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
|
||||||
|
public class CaptureProvider {
|
||||||
|
private static final String TAG = CaptureProvider.class.getSimpleName();
|
||||||
|
private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture";
|
||||||
|
public static final Uri CONTENT_URI = Uri.parse(URI_STRING);
|
||||||
|
public static final String AUTHORITY = "org.thoughtcrime.securesms";
|
||||||
|
public static final String EXPECTED_PATH = "capture/#";
|
||||||
|
|
||||||
|
private static volatile CaptureProvider instance;
|
||||||
|
public static CaptureProvider getInstance(Context context) {
|
||||||
|
if (instance == null) {
|
||||||
|
synchronized (CaptureProvider.class) {
|
||||||
|
if (instance == null) {
|
||||||
|
instance = new CaptureProvider(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
private CaptureProvider(Context context) {
|
||||||
|
this.context = context.getApplicationContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Uri create(MasterSecret masterSecret, Bitmap bitmap) throws IOException {
|
||||||
|
long id = System.currentTimeMillis();
|
||||||
|
OutputStream output = new EncryptingPartOutputStream(getFile(id), masterSecret);
|
||||||
|
bitmap.compress(CompressFormat.JPEG, 100, output);
|
||||||
|
output.close();
|
||||||
|
return ContentUris.withAppendedId(CONTENT_URI, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean delete(Uri uri) {
|
||||||
|
return getFile(ContentUris.parseId(uri)).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public InputStream getStream(MasterSecret masterSecret, long id) throws IOException {
|
||||||
|
return new DecryptingPartInputStream(getFile(id), masterSecret);
|
||||||
|
}
|
||||||
|
|
||||||
|
private File getFile(long id) {
|
||||||
|
return new File(context.getDir("captures", Context.MODE_PRIVATE), id + ".capture");
|
||||||
|
}
|
||||||
|
}
|
@ -7,17 +7,20 @@ import android.graphics.Bitmap.Config;
|
|||||||
import android.graphics.BitmapFactory;
|
import android.graphics.BitmapFactory;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
|
import android.graphics.ImageFormat;
|
||||||
import android.graphics.Matrix;
|
import android.graphics.Matrix;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.PorterDuff;
|
import android.graphics.PorterDuff;
|
||||||
import android.graphics.PorterDuffXfermode;
|
import android.graphics.PorterDuffXfermode;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.YuvImage;
|
||||||
import android.graphics.drawable.BitmapDrawable;
|
import android.graphics.drawable.BitmapDrawable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.util.Pair;
|
import android.util.Pair;
|
||||||
|
|
||||||
@ -210,6 +213,7 @@ public class BitmapUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static Bitmap rotateBitmap(Bitmap bitmap, int angle) {
|
public static Bitmap rotateBitmap(Bitmap bitmap, int angle) {
|
||||||
|
if (angle == 0) return bitmap;
|
||||||
Matrix matrix = new Matrix();
|
Matrix matrix = new Matrix();
|
||||||
matrix.postRotate(angle);
|
matrix.postRotate(angle);
|
||||||
Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
|
Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
|
||||||
@ -268,6 +272,24 @@ public class BitmapUtil {
|
|||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Bitmap createFromNV21(@NonNull final byte[] data,
|
||||||
|
final int width,
|
||||||
|
final int height,
|
||||||
|
final int rotation,
|
||||||
|
final Rect croppingRect)
|
||||||
|
throws IOException
|
||||||
|
{
|
||||||
|
YuvImage previewImage = new YuvImage(data, ImageFormat.NV21,
|
||||||
|
width, height, null);
|
||||||
|
|
||||||
|
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
|
previewImage.compressToJpeg(croppingRect, 100, outputStream);
|
||||||
|
byte[] bytes = outputStream.toByteArray();
|
||||||
|
outputStream.close();
|
||||||
|
outputStream = new ByteArrayOutputStream();
|
||||||
|
return BitmapUtil.rotateBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.length), rotation);
|
||||||
|
}
|
||||||
|
|
||||||
public static Bitmap createFromDrawable(final Drawable drawable, final int width, final int height) {
|
public static Bitmap createFromDrawable(final Drawable drawable, final int width, final int height) {
|
||||||
final AtomicBoolean created = new AtomicBoolean(false);
|
final AtomicBoolean created = new AtomicBoolean(false);
|
||||||
final Bitmap[] result = new Bitmap[1];
|
final Bitmap[] result = new Bitmap[1];
|
||||||
|
121
src/org/thoughtcrime/securesms/util/SoftKeyboardUtil.java
Normal file
121
src/org/thoughtcrime/securesms/util/SoftKeyboardUtil.java
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
package org.thoughtcrime.securesms.util;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Build.VERSION_CODES;
|
||||||
|
import android.preference.PreferenceManager;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.Surface;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
import org.thoughtcrime.securesms.R;
|
||||||
|
|
||||||
|
import java.lang.reflect.Field;
|
||||||
|
|
||||||
|
public class SoftKeyboardUtil {
|
||||||
|
private static final String TAG = SoftKeyboardUtil.class.getSimpleName();
|
||||||
|
private static final Rect rect = new Rect();
|
||||||
|
|
||||||
|
public static int onMeasure(ViewGroup view) {
|
||||||
|
view.getWindowVisibleDisplayFrame(rect);
|
||||||
|
final int res = view.getResources().getIdentifier("status_bar_height", "dimen", "android");
|
||||||
|
final int statusBarHeight = res > 0 ? view.getResources().getDimensionPixelSize(res) : 0;
|
||||||
|
final int availableHeight = view.getRootView().getHeight() - statusBarHeight - getViewInset(view);
|
||||||
|
final int keyboardHeight = availableHeight - (rect.bottom - rect.top);
|
||||||
|
final int minKeyboardHeight = view.getResources().getDimensionPixelSize(R.dimen.min_emoji_drawer_height);
|
||||||
|
|
||||||
|
if (keyboardHeight > minKeyboardHeight) {
|
||||||
|
onKeyboardShown(view.getContext(), keyboardHeight);
|
||||||
|
}
|
||||||
|
return Math.max(keyboardHeight, minKeyboardHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getViewInset(ViewGroup view) {
|
||||||
|
if (Build.VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Field attachInfoField = View.class.getDeclaredField("mAttachInfo");
|
||||||
|
attachInfoField.setAccessible(true);
|
||||||
|
Object attachInfo = attachInfoField.get(view);
|
||||||
|
if (attachInfo != null) {
|
||||||
|
Field stableInsetsField = attachInfo.getClass().getDeclaredField("mStableInsets");
|
||||||
|
stableInsetsField.setAccessible(true);
|
||||||
|
Rect insets = (Rect)stableInsetsField.get(attachInfo);
|
||||||
|
return insets.bottom;
|
||||||
|
}
|
||||||
|
} catch (NoSuchFieldException nsfe) {
|
||||||
|
Log.w(TAG, "field reflection error when measuring view inset", nsfe);
|
||||||
|
} catch (IllegalAccessException iae) {
|
||||||
|
Log.w(TAG, "access reflection error when measuring view inset", iae);
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void onKeyboardShown(Context context, int keyboardHeight) {
|
||||||
|
Log.w(TAG, "keyboard shown, height " + keyboardHeight);
|
||||||
|
|
||||||
|
WindowManager wm = (WindowManager)context.getSystemService(Activity.WINDOW_SERVICE);
|
||||||
|
if (wm == null || wm.getDefaultDisplay() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int rotation = wm.getDefaultDisplay().getRotation();
|
||||||
|
|
||||||
|
switch (rotation) {
|
||||||
|
case Surface.ROTATION_270:
|
||||||
|
case Surface.ROTATION_90:
|
||||||
|
setKeyboardLandscapeHeight(context, keyboardHeight);
|
||||||
|
break;
|
||||||
|
case Surface.ROTATION_0:
|
||||||
|
case Surface.ROTATION_180:
|
||||||
|
setKeyboardPortraitHeight(context, keyboardHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getKeyboardHeight(Context context) {
|
||||||
|
WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
|
||||||
|
if (wm == null || wm.getDefaultDisplay() == null) {
|
||||||
|
throw new AssertionError("WindowManager was null or there is no default display");
|
||||||
|
}
|
||||||
|
|
||||||
|
int rotation = wm.getDefaultDisplay().getRotation();
|
||||||
|
|
||||||
|
switch (rotation) {
|
||||||
|
case Surface.ROTATION_270:
|
||||||
|
case Surface.ROTATION_90:
|
||||||
|
return getKeyboardLandscapeHeight(context);
|
||||||
|
case Surface.ROTATION_0:
|
||||||
|
case Surface.ROTATION_180:
|
||||||
|
default:
|
||||||
|
return getKeyboardPortraitHeight(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getKeyboardLandscapeHeight(Context context) {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getInt("keyboard_height_landscape",
|
||||||
|
context.getResources().getDimensionPixelSize(R.dimen.min_emoji_drawer_height));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int getKeyboardPortraitHeight(Context context) {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getInt("keyboard_height_portrait",
|
||||||
|
context.getResources().getDimensionPixelSize(R.dimen.min_emoji_drawer_height));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setKeyboardLandscapeHeight(Context context, int height) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.edit().putInt("keyboard_height_landscape", height).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setKeyboardPortraitHeight(Context context, int height) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.edit().putInt("keyboard_height_portrait", height).apply();
|
||||||
|
}
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user