diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 71e173e18f..9f7889295f 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -33,6 +33,7 @@ <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_CALL_LOG" /> <uses-permission android:name="android.permission.GET_ACCOUNTS" /> + <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" /> <permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE" diff --git a/build.gradle b/build.gradle index 351d21faf2..50309b7083 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,9 @@ repositories { maven { // textdrawable url 'https://dl.bintray.com/amulyakhare/maven' } + maven { + url 'https://repo.commonsware.com.s3.amazonaws.com' + } jcenter() mavenLocal() } @@ -69,6 +72,7 @@ dependencies { exclude group: 'com.android.support', module: 'support-v4' } compile 'com.madgag.spongycastle:prov:1.51.0.0' + compile 'com.commonsware.cwac:camera:0.6.+' provided 'com.squareup.dagger:dagger-compiler:1.2.2' compile 'org.whispersystems:jobmanager:0.11.0' diff --git a/res/drawable-hdpi/quick_camera_dark.png b/res/drawable-hdpi/quick_camera_dark.png new file mode 100755 index 0000000000..aa3a3e5717 Binary files /dev/null and b/res/drawable-hdpi/quick_camera_dark.png differ diff --git a/res/drawable-hdpi/quick_camera_exit_fullscreen.png b/res/drawable-hdpi/quick_camera_exit_fullscreen.png new file mode 100755 index 0000000000..061e737dab Binary files /dev/null and b/res/drawable-hdpi/quick_camera_exit_fullscreen.png differ diff --git a/res/drawable-hdpi/quick_camera_front.png b/res/drawable-hdpi/quick_camera_front.png new file mode 100755 index 0000000000..6fa7c4fa3d Binary files /dev/null and b/res/drawable-hdpi/quick_camera_front.png differ diff --git a/res/drawable-hdpi/quick_camera_fullscreen.png b/res/drawable-hdpi/quick_camera_fullscreen.png new file mode 100755 index 0000000000..bd69358cb5 Binary files /dev/null and b/res/drawable-hdpi/quick_camera_fullscreen.png differ diff --git a/res/drawable-hdpi/quick_camera_hide.png b/res/drawable-hdpi/quick_camera_hide.png new file mode 100755 index 0000000000..724eb1f7ab Binary files /dev/null and b/res/drawable-hdpi/quick_camera_hide.png differ diff --git a/res/drawable-hdpi/quick_camera_light.png b/res/drawable-hdpi/quick_camera_light.png new file mode 100755 index 0000000000..c26a846e37 Binary files /dev/null and b/res/drawable-hdpi/quick_camera_light.png differ diff --git a/res/drawable-hdpi/quick_camera_rear.png b/res/drawable-hdpi/quick_camera_rear.png new file mode 100755 index 0000000000..764e35ab0f Binary files /dev/null and b/res/drawable-hdpi/quick_camera_rear.png differ diff --git a/res/drawable-hdpi/quick_shutter_button.png b/res/drawable-hdpi/quick_shutter_button.png new file mode 100755 index 0000000000..9817b7e7e2 Binary files /dev/null and b/res/drawable-hdpi/quick_shutter_button.png differ diff --git a/res/drawable-mdpi/quick_camera_dark.png b/res/drawable-mdpi/quick_camera_dark.png new file mode 100755 index 0000000000..4dbf6c77bc Binary files /dev/null and b/res/drawable-mdpi/quick_camera_dark.png differ diff --git a/res/drawable-mdpi/quick_camera_exit_fullscreen.png b/res/drawable-mdpi/quick_camera_exit_fullscreen.png new file mode 100755 index 0000000000..2e88a87737 Binary files /dev/null and b/res/drawable-mdpi/quick_camera_exit_fullscreen.png differ diff --git a/res/drawable-mdpi/quick_camera_front.png b/res/drawable-mdpi/quick_camera_front.png new file mode 100755 index 0000000000..ee64e0f670 Binary files /dev/null and b/res/drawable-mdpi/quick_camera_front.png differ diff --git a/res/drawable-mdpi/quick_camera_fullscreen.png b/res/drawable-mdpi/quick_camera_fullscreen.png new file mode 100755 index 0000000000..382fe09edc Binary files /dev/null and b/res/drawable-mdpi/quick_camera_fullscreen.png differ diff --git a/res/drawable-mdpi/quick_camera_hide.png b/res/drawable-mdpi/quick_camera_hide.png new file mode 100755 index 0000000000..6c24766600 Binary files /dev/null and b/res/drawable-mdpi/quick_camera_hide.png differ diff --git a/res/drawable-mdpi/quick_camera_light.png b/res/drawable-mdpi/quick_camera_light.png new file mode 100755 index 0000000000..2a906080fd Binary files /dev/null and b/res/drawable-mdpi/quick_camera_light.png differ diff --git a/res/drawable-mdpi/quick_camera_rear.png b/res/drawable-mdpi/quick_camera_rear.png new file mode 100755 index 0000000000..764e35ab0f Binary files /dev/null and b/res/drawable-mdpi/quick_camera_rear.png differ diff --git a/res/drawable-mdpi/quick_shutter_button.png b/res/drawable-mdpi/quick_shutter_button.png new file mode 100755 index 0000000000..90022770fe Binary files /dev/null and b/res/drawable-mdpi/quick_shutter_button.png differ diff --git a/res/drawable-xhdpi/quick_camera_dark.png b/res/drawable-xhdpi/quick_camera_dark.png new file mode 100755 index 0000000000..753805275b Binary files /dev/null and b/res/drawable-xhdpi/quick_camera_dark.png differ diff --git a/res/drawable-xhdpi/quick_camera_exit_fullscreen.png b/res/drawable-xhdpi/quick_camera_exit_fullscreen.png new file mode 100755 index 0000000000..fbb53e5c6e Binary files /dev/null and b/res/drawable-xhdpi/quick_camera_exit_fullscreen.png differ diff --git a/res/drawable-xhdpi/quick_camera_front.png b/res/drawable-xhdpi/quick_camera_front.png new file mode 100755 index 0000000000..379bc460f8 Binary files /dev/null and b/res/drawable-xhdpi/quick_camera_front.png differ diff --git a/res/drawable-xhdpi/quick_camera_fullscreen.png b/res/drawable-xhdpi/quick_camera_fullscreen.png new file mode 100755 index 0000000000..5709dd8bed Binary files /dev/null and b/res/drawable-xhdpi/quick_camera_fullscreen.png differ diff --git a/res/drawable-xhdpi/quick_camera_hide.png b/res/drawable-xhdpi/quick_camera_hide.png new file mode 100755 index 0000000000..f00aa8bd4e Binary files /dev/null and b/res/drawable-xhdpi/quick_camera_hide.png differ diff --git a/res/drawable-xhdpi/quick_camera_light.png b/res/drawable-xhdpi/quick_camera_light.png new file mode 100755 index 0000000000..3be61d73f4 Binary files /dev/null and b/res/drawable-xhdpi/quick_camera_light.png differ diff --git a/res/drawable-xhdpi/quick_camera_rear.png b/res/drawable-xhdpi/quick_camera_rear.png new file mode 100755 index 0000000000..d27bf85e79 Binary files /dev/null and b/res/drawable-xhdpi/quick_camera_rear.png differ diff --git a/res/drawable-xhdpi/quick_shutter_button.png b/res/drawable-xhdpi/quick_shutter_button.png new file mode 100755 index 0000000000..9324ec8cb3 Binary files /dev/null and b/res/drawable-xhdpi/quick_shutter_button.png differ diff --git a/res/drawable-xxhdpi/quick_camera_dark.png b/res/drawable-xxhdpi/quick_camera_dark.png new file mode 100755 index 0000000000..c1a3549bfc Binary files /dev/null and b/res/drawable-xxhdpi/quick_camera_dark.png differ diff --git a/res/drawable-xxhdpi/quick_camera_exit_fullscreen.png b/res/drawable-xxhdpi/quick_camera_exit_fullscreen.png new file mode 100755 index 0000000000..d041734c13 Binary files /dev/null and b/res/drawable-xxhdpi/quick_camera_exit_fullscreen.png differ diff --git a/res/drawable-xxhdpi/quick_camera_front.png b/res/drawable-xxhdpi/quick_camera_front.png new file mode 100755 index 0000000000..9e35ad5c4a Binary files /dev/null and b/res/drawable-xxhdpi/quick_camera_front.png differ diff --git a/res/drawable-xxhdpi/quick_camera_fullscreen.png b/res/drawable-xxhdpi/quick_camera_fullscreen.png new file mode 100755 index 0000000000..ce96d83e36 Binary files /dev/null and b/res/drawable-xxhdpi/quick_camera_fullscreen.png differ diff --git a/res/drawable-xxhdpi/quick_camera_hide.png b/res/drawable-xxhdpi/quick_camera_hide.png new file mode 100755 index 0000000000..16094f9bc4 Binary files /dev/null and b/res/drawable-xxhdpi/quick_camera_hide.png differ diff --git a/res/drawable-xxhdpi/quick_camera_light.png b/res/drawable-xxhdpi/quick_camera_light.png new file mode 100755 index 0000000000..62d83b253d Binary files /dev/null and b/res/drawable-xxhdpi/quick_camera_light.png differ diff --git a/res/drawable-xxhdpi/quick_camera_rear.png b/res/drawable-xxhdpi/quick_camera_rear.png new file mode 100755 index 0000000000..d9fd33dd37 Binary files /dev/null and b/res/drawable-xxhdpi/quick_camera_rear.png differ diff --git a/res/drawable-xxhdpi/quick_shutter_button.png b/res/drawable-xxhdpi/quick_shutter_button.png new file mode 100755 index 0000000000..4394349386 Binary files /dev/null and b/res/drawable-xxhdpi/quick_shutter_button.png differ diff --git a/res/drawable/quick_camera_shutter_ring.xml b/res/drawable/quick_camera_shutter_ring.xml new file mode 100644 index 0000000000..33690326ca --- /dev/null +++ b/res/drawable/quick_camera_shutter_ring.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<selector xmlns:android="http://schemas.android.com/apk/res/android"> + <item android:state_pressed="true"> + <shape + android:innerRadiusRatio="3" + android:shape="ring" + android:thickness="2dp" + android:useLevel="false" > + <solid android:color="@android:color/white" /> + <size + android:height="52dp" + android:width="52dp" /> + </shape> + </item> + <item> + <shape + android:innerRadiusRatio="3" + android:shape="ring" + android:thickness="2dp" + android:useLevel="false" > + <solid android:color="#40ffffff" /> + <size + android:height="52dp" + android:width="52dp" /> + </shape> + </item> +</selector> \ No newline at end of file diff --git a/res/layout-land/quick_camera_controls.xml b/res/layout-land/quick_camera_controls.xml new file mode 100644 index 0000000000..558d6b77f9 --- /dev/null +++ b/res/layout-land/quick_camera_controls.xml @@ -0,0 +1,37 @@ +<?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> \ No newline at end of file diff --git a/res/layout/conversation_activity.xml b/res/layout/conversation_activity.xml index a83d849113..c9421f4c62 100644 --- a/res/layout/conversation_activity.xml +++ b/res/layout/conversation_activity.xml @@ -10,6 +10,12 @@ android:background="?conversation_background" android:orientation="vertical"> +<org.thoughtcrime.securesms.components.QuickAttachmentDrawer + xmlns:android="http://schemas.android.com/apk/res/android" + android:id="@+id/quick_attachment_drawer" + android:layout_width="match_parent" + android:layout_height="match_parent"> + <RelativeLayout android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1" @@ -95,6 +101,14 @@ tools:hint="Send TextSecure message" /> </LinearLayout> + <ImageButton android:id="@+id/quick_attachment_toggle" + android:layout_width="wrap_content" + android:layout_height="44dp" + android:src="?quick_camera_icon" + android:background="@drawable/touch_highlight_background" + android:contentDescription="@string/conversation_activity__quick_attachment_drawer_toggle_description" + android:padding="10dp"/> + <org.thoughtcrime.securesms.components.AnimatingToggle android:id="@+id/button_toggle" android:layout_width="50dp" @@ -141,4 +155,6 @@ </LinearLayout> </RelativeLayout> + +</org.thoughtcrime.securesms.components.QuickAttachmentDrawer> </org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout> diff --git a/res/layout/quick_camera_controls.xml b/res/layout/quick_camera_controls.xml new file mode 100644 index 0000000000..1e6a697589 --- /dev/null +++ b/res/layout/quick_camera_controls.xml @@ -0,0 +1,37 @@ +<?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_alignParentBottom="true" + android:layout_centerHorizontal="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_alignParentBottom="true" + android:layout_alignParentLeft="true" + android:background="#00000000" + android:src="@drawable/quick_camera_front" + android:padding="20dp" + android:visibility="invisible" + tools:visibility="visible"/> +</RelativeLayout> \ No newline at end of file diff --git a/res/values/attrs.xml b/res/values/attrs.xml index 6d25609aa4..9b8142d0c1 100644 --- a/res/values/attrs.xml +++ b/res/values/attrs.xml @@ -52,6 +52,7 @@ <attr name="emoji_category_places" format="reference"/> <attr name="emoji_category_symbol" format="reference"/> <attr name="emoji_category_emoticons" format="reference"/> + <attr name="quick_camera_icon" format="reference"/> <attr name="conversation_item_background" format="reference"/> <attr name="conversation_item_bubble_background" format="reference|color"/> diff --git a/res/values/dimens.xml b/res/values/dimens.xml index 56b0af4c42..2fdaa5cf2e 100644 --- a/res/values/dimens.xml +++ b/res/values/dimens.xml @@ -28,4 +28,5 @@ <dimen name="color_grid_extra_padding">32dp</dimen> <dimen name="color_grid_item_size">48dp</dimen> + <dimen name="quick_media_drawer_default_height">250dp</dimen> </resources> diff --git a/res/values/strings.xml b/res/values/strings.xml index a28a9126dc..0df6e3941e 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -521,6 +521,7 @@ <string name="conversation_activity__compose_description">Message composition</string> <string name="conversation_activity__emoji_toggle_description">Toggle emoji keyboard</string> <string name="conversation_activity__attachment_thumbnail">Attachment Thumbnail</string> + <string name="conversation_activity__quick_attachment_drawer_toggle_description">Toggle attachment drawer</string> <!-- conversation_item --> <string name="conversation_item__mms_downloading_description">Media message downloading</string> @@ -981,6 +982,9 @@ <!-- transport_selection_list_item --> <string name="transport_selection_list_item__transport_icon">Transport icon</string> + <!-- quick_attachment_drawer --> + <string name="quick_camera_unavailable">Camera unavailable</string> + <!-- EOF --> </resources> diff --git a/res/values/themes.xml b/res/values/themes.xml index 22cd2496b2..a460231e13 100644 --- a/res/values/themes.xml +++ b/res/values/themes.xml @@ -40,6 +40,7 @@ <item name="ic_arrow_forward">@drawable/ic_arrow_forward_dark</item> <item name="lockscreen_watermark">@drawable/lockscreen_watermark_dark</item> <item name="android:windowBackground">@color/black</item> + <item name="conversation_background">@color/black</item> </style> <style name="PopupAnimation" parent="@android:style/Animation"> @@ -124,6 +125,9 @@ <item name="conversation_item_sent_text_indicator_tab_color">#99000000</item> <item name="conversation_item_received_text_primary_color">@color/white</item> <item name="conversation_item_received_text_secondary_color">#BFffffff</item> + + <item name="quick_camera_icon">@drawable/quick_camera_light</item> + <item name="conversation_item_background">@drawable/conversation_item_background</item> <item name="conversation_item_sent_indicator_text_background">@drawable/conversation_item_sent_indicator_text_shape</item> @@ -250,6 +254,8 @@ <item name="emoji_category_symbol">@drawable/emoji_category_symbol_dark</item> <item name="emoji_category_emoticons">@drawable/emoji_category_emoticons_dark</item> + <item name="quick_camera_icon">@drawable/quick_camera_dark</item> + <item name="menu_new_conversation_icon">@drawable/ic_add_white_24dp</item> <item name="menu_group_icon">@drawable/ic_group_white_24dp</item> <item name="menu_search_icon">@drawable/ic_search_white_24dp</item> diff --git a/src/org/thoughtcrime/securesms/ConversationActivity.java b/src/org/thoughtcrime/securesms/ConversationActivity.java index 121af277b4..663c9b2d2d 100644 --- a/src/org/thoughtcrime/securesms/ConversationActivity.java +++ b/src/org/thoughtcrime/securesms/ConversationActivity.java @@ -34,6 +34,7 @@ import android.os.Build; import android.os.Bundle; import android.provider.ContactsContract; import android.support.annotation.NonNull; +import android.support.v4.view.WindowCompat; import android.text.Editable; import android.text.InputType; import android.text.TextWatcher; @@ -70,6 +71,9 @@ import org.thoughtcrime.securesms.components.emoji.EmojiPopup; import org.thoughtcrime.securesms.components.emoji.EmojiToggle; import org.thoughtcrime.securesms.contacts.ContactAccessor; import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData; +import org.thoughtcrime.securesms.components.QuickAttachmentDrawer; +import org.thoughtcrime.securesms.components.QuickCamera; +import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream; import org.thoughtcrime.securesms.crypto.MasterCipher; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.SecurityEvent; @@ -114,6 +118,8 @@ import org.thoughtcrime.securesms.util.concurrent.SettableFuture; import org.whispersystems.libaxolotl.InvalidMessageException; import org.whispersystems.libaxolotl.util.guava.Optional; +import java.io.File; +import java.io.FileOutputStream; import java.io.IOException; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @@ -172,6 +178,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity private BroadcastReceiver groupUpdateReceiver; private Optional<EmojiPopup> emojiPopup = Optional.absent(); private EmojiToggle emojiToggle; + private ImageButton quickAttachmentToggle; + private QuickAttachmentDrawer quickAttachmentDrawer; private Recipients recipients; private long threadId; @@ -193,6 +201,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity protected void onCreate(Bundle state, @NonNull MasterSecret masterSecret) { this.masterSecret = masterSecret; + supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY); setContentView(R.layout.conversation_activity); fragment = initFragment(R.id.fragment_content, new ConversationFragment(), @@ -229,6 +238,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity super.onResume(); dynamicTheme.onResume(this); dynamicLanguage.onResume(this); + quickAttachmentDrawer.onResume(); initializeSecurity(); initializeEnabledCheck(); @@ -249,6 +259,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity super.onPause(); MessageNotifier.setVisibleThread(-1L); if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right); + quickAttachmentDrawer.onPause(); } @Override @@ -366,6 +377,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity public void onBackPressed() { if (isEmojiDrawerOpen()) { hideEmojiPopup(false); + } else if (quickAttachmentDrawer.getDrawerState() != QuickAttachmentDrawer.COLLAPSED) { + quickAttachmentDrawer.setDrawerStateAndAnimate(QuickAttachmentDrawer.COLLAPSED); } else { super.onBackPressed(); } @@ -694,6 +707,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity addAttachmentAudio(Uri.parse(draft.getValue())); } else if (draft.getType().equals(Draft.VIDEO)) { addAttachmentVideo(Uri.parse(draft.getValue())); + } else if (draft.getType().equals(Draft.ENCRYPTED_IMAGE)) { + addAttachmentEncryptedImage(Uri.parse(draft.getValue())); } } @@ -766,6 +781,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity container.addOnKeyboardShownListener(this); + buttonToggle = (AnimatingToggle) findViewById(R.id.button_toggle); + sendButton = (SendButton) findViewById(R.id.send_button); + attachButton = (ImageButton) findViewById(R.id.attach_button); + composeText = (ComposeText) findViewById(R.id.embedded_text_editor); + charactersLeft = (TextView) findViewById(R.id.space_left); + emojiToggle = (EmojiToggle) findViewById(R.id.emoji_toggle); + titleView = (ConversationTitleView) getSupportActionBar().getCustomView(); + unblockButton = (Button) findViewById(R.id.unblock_button); + composePanel = findViewById(R.id.bottom_panel); + quickAttachmentDrawer = (QuickAttachmentDrawer) findViewById(R.id.quick_attachment_drawer); + quickAttachmentToggle = (ImageButton) findViewById(R.id.quick_attachment_toggle); + int[] attributes = new int[]{R.attr.conversation_item_bubble_background}; TypedArray colors = obtainStyledAttributes(attributes); int defaultColor = colors.getColor(0, Color.WHITE); @@ -814,6 +841,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity composeText.setOnClickListener(composeKeyPressedListener); composeText.setOnFocusChangeListener(composeKeyPressedListener); emojiToggle.setOnClickListener(new EmojiToggleListener()); + + if (quickAttachmentDrawer.hasCamera()) { + QuickAttachmentDrawerToggleListener listener = new QuickAttachmentDrawerToggleListener(); + quickAttachmentDrawer.setQuickAttachmentDrawerListener(listener); + quickAttachmentDrawer.setQuickCameraListener(listener); + quickAttachmentToggle.setOnClickListener(listener); + } else { + quickAttachmentToggle.setVisibility(View.GONE); + } } protected void initializeActionBar() { @@ -934,6 +970,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } } + private void addAttachmentEncryptedImage(Uri uri) { + try { + attachmentManager.setEncryptedImage(uri, masterSecret); + } 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); @@ -1018,9 +1065,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } for (Slide slide : attachmentManager.getSlideDeck().getSlides()) { - if (slide.hasAudio()) drafts.add(new Draft(Draft.AUDIO, slide.getUri().toString())); - else if (slide.hasVideo()) drafts.add(new Draft(Draft.VIDEO, slide.getUri().toString())); - else if (slide.hasImage()) drafts.add(new Draft(Draft.IMAGE, slide.getUri().toString())); + String draftType = null; + if (slide.hasAudio()) draftType = Draft.AUDIO; + else if (slide.hasVideo()) draftType = Draft.VIDEO; + 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; @@ -1296,10 +1347,71 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity } }); input.hideSoftInputFromWindow(composeText.getWindowToken(), 0); + quickAttachmentDrawer.setDrawerStateAndAnimate(QuickAttachmentDrawer.COLLAPSED); } } } + private class QuickAttachmentDrawerToggleListener implements OnClickListener, + QuickAttachmentDrawer.QuickAttachmentDrawerListener, + QuickCamera.QuickCameraListener { + @QuickAttachmentDrawer.DrawerState int nextDrawerState = QuickAttachmentDrawer.HALF_EXPANDED; + + @Override + public void onClick(View v) { + InputMethodManager input = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE); + input.hideSoftInputFromWindow(composeText.getWindowToken(), 0); + composeText.clearFocus(); + hideEmojiPopup(false); + quickAttachmentDrawer.setDrawerStateAndAnimate(nextDrawerState); + } + + @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(); + } + } + private class SendButtonListener implements OnClickListener, TextView.OnEditorActionListener { @Override public void onClick(View v) { @@ -1370,8 +1482,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity @Override public void onFocusChange(View v, boolean hasFocus) { - if (hasFocus) { + if (hasFocus && isEmojiDrawerOpen()) { hideEmojiPopup(true); + } else if (hasFocus && quickAttachmentDrawer.getDrawerState() != QuickAttachmentDrawer.COLLAPSED) { + quickAttachmentDrawer.setDrawerStateAndAnimate(QuickAttachmentDrawer.COLLAPSED); } } } diff --git a/src/org/thoughtcrime/securesms/components/CameraView.java b/src/org/thoughtcrime/securesms/components/CameraView.java new file mode 100644 index 0000000000..766e97f462 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/CameraView.java @@ -0,0 +1,515 @@ +/*** + 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); + } + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/components/QuickAttachmentDrawer.java b/src/org/thoughtcrime/securesms/components/QuickAttachmentDrawer.java new file mode 100644 index 0000000000..2ab5d641f3 --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/QuickAttachmentDrawer.java @@ -0,0 +1,503 @@ +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(); + } +} diff --git a/src/org/thoughtcrime/securesms/components/QuickCamera.java b/src/org/thoughtcrime/securesms/components/QuickCamera.java new file mode 100644 index 0000000000..88f50c0d4f --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/QuickCamera.java @@ -0,0 +1,185 @@ +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(); + } + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/components/SurfacePreviewStrategy.java b/src/org/thoughtcrime/securesms/components/SurfacePreviewStrategy.java new file mode 100644 index 0000000000..0b95b8cf7a --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/SurfacePreviewStrategy.java @@ -0,0 +1,72 @@ +/*** + Copyright (c) 2013 CommonsWare, LLC + + 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.hardware.Camera; +import android.media.MediaRecorder; +import android.view.SurfaceHolder; +import android.view.SurfaceView; +import android.view.View; + +import com.commonsware.cwac.camera.PreviewStrategy; + +import java.io.IOException; + +class SurfacePreviewStrategy implements PreviewStrategy, + SurfaceHolder.Callback { + private final CameraView cameraView; + private SurfaceView preview=null; + private SurfaceHolder previewHolder=null; + + @SuppressWarnings("deprecation") + SurfacePreviewStrategy(CameraView cameraView) { + this.cameraView=cameraView; + preview=new SurfaceView(cameraView.getContext()); + previewHolder=preview.getHolder(); + previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); + previewHolder.addCallback(this); + } + + @Override + public void surfaceCreated(SurfaceHolder holder) { + cameraView.previewCreated(); + } + + @Override + public void surfaceChanged(SurfaceHolder holder, int format, + int width, int height) { + cameraView.initPreview(width, height); + } + + @Override + public void surfaceDestroyed(SurfaceHolder holder) { + cameraView.previewDestroyed(); + } + + @Override + public void attach(Camera camera) throws IOException { + camera.setPreviewDisplay(previewHolder); + } + + @Override + public void attach(MediaRecorder recorder) { + recorder.setPreviewDisplay(previewHolder.getSurface()); + } + + @Override + public View getWidget() { + return(preview); + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/components/TexturePreviewStrategy.java b/src/org/thoughtcrime/securesms/components/TexturePreviewStrategy.java new file mode 100644 index 0000000000..d63faeeeda --- /dev/null +++ b/src/org/thoughtcrime/securesms/components/TexturePreviewStrategy.java @@ -0,0 +1,88 @@ +package org.thoughtcrime.securesms.components; +/*** + Copyright (c) 2013 CommonsWare, LLC + + 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. + */ + +import android.annotation.TargetApi; +import android.graphics.SurfaceTexture; +import android.hardware.Camera; +import android.media.MediaRecorder; +import android.os.Build; +import android.view.TextureView; +import android.view.View; + +import com.commonsware.cwac.camera.PreviewStrategy; + +import java.io.IOException; + +@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) +class TexturePreviewStrategy implements PreviewStrategy, + TextureView.SurfaceTextureListener { + private final CameraView cameraView; + private TextureView widget=null; + private SurfaceTexture surface=null; + + TexturePreviewStrategy(CameraView cameraView) { + this.cameraView=cameraView; + widget=new TextureView(cameraView.getContext()); + widget.setSurfaceTextureListener(this); + } + + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, + int width, int height) { + this.surface=surface; + + cameraView.previewCreated(); + cameraView.initPreview(width, height); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, + int width, int height) { + cameraView.previewReset(width, height); + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + cameraView.previewDestroyed(); + + return(true); + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + // no-op + } + + @Override + public void attach(Camera camera) throws IOException { + camera.setPreviewTexture(surface); + } + + @Override + public void attach(MediaRecorder recorder) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + // no-op + } + else { + throw new IllegalStateException( + "Cannot use TextureView with MediaRecorder"); + } + } + + @Override + public View getWidget() { + return(widget); + } +} \ No newline at end of file diff --git a/src/org/thoughtcrime/securesms/components/ThumbnailView.java b/src/org/thoughtcrime/securesms/components/ThumbnailView.java index 4d580995bd..af68acfcbf 100644 --- a/src/org/thoughtcrime/securesms/components/ThumbnailView.java +++ b/src/org/thoughtcrime/securesms/components/ThumbnailView.java @@ -175,8 +175,9 @@ public class ThumbnailView extends FrameLayout { private GenericRequestBuilder buildThumbnailGlideRequest(Slide slide, MasterSecret masterSecret) { final GenericRequestBuilder builder; - if (slide.isDraft()) builder = buildDraftGlideRequest(slide); - else builder = buildEncryptedPartGlideRequest(slide, masterSecret); + if (slide.isDraft() && slide.isEncrypted()) builder = buildEncryptedDraftGlideRequest(slide, masterSecret); + else if (slide.isDraft()) builder = buildDraftGlideRequest(slide); + else builder = buildEncryptedPartGlideRequest(slide, masterSecret); return builder; } @@ -186,6 +187,15 @@ public class ThumbnailView extends FrameLayout { .listener(new PduThumbnailSetListener(slide.getPart())); } + private GenericRequestBuilder buildEncryptedDraftGlideRequest(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) { throw new IllegalStateException("null MasterSecret when loading non-draft thumbnail"); diff --git a/src/org/thoughtcrime/securesms/database/DraftDatabase.java b/src/org/thoughtcrime/securesms/database/DraftDatabase.java index 631878f938..0a923b2016 100644 --- a/src/org/thoughtcrime/securesms/database/DraftDatabase.java +++ b/src/org/thoughtcrime/securesms/database/DraftDatabase.java @@ -101,10 +101,11 @@ public class DraftDatabase extends Database { } public static class Draft { - public static final String TEXT = "text"; - public static final String IMAGE = "image"; - public static final String VIDEO = "video"; - public static final String AUDIO = "audio"; + public static final String TEXT = "text"; + public static final String IMAGE = "image"; + public static final String VIDEO = "video"; + public static final String AUDIO = "audio"; + public static final String ENCRYPTED_IMAGE = "encrypted_image"; private final String type; private final String value; @@ -124,10 +125,11 @@ public class DraftDatabase extends Database { public String getSnippet(Context context) { switch (type) { - case TEXT: return value; - case IMAGE: return context.getString(R.string.DraftDatabase_Draft_image_snippet); - case VIDEO: return context.getString(R.string.DraftDatabase_Draft_video_snippet); - case AUDIO: return context.getString(R.string.DraftDatabase_Draft_audio_snippet); + case TEXT: return value; + case ENCRYPTED_IMAGE: + case IMAGE: return context.getString(R.string.DraftDatabase_Draft_image_snippet); + case VIDEO: return context.getString(R.string.DraftDatabase_Draft_video_snippet); + case AUDIO: return context.getString(R.string.DraftDatabase_Draft_audio_snippet); default: return null; } } diff --git a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java index f1dc5871a2..027a4fb534 100644 --- a/src/org/thoughtcrime/securesms/mms/AttachmentManager.java +++ b/src/org/thoughtcrime/securesms/mms/AttachmentManager.java @@ -24,6 +24,7 @@ import android.net.Uri; import android.os.Build; import android.provider.ContactsContract; import android.provider.MediaStore; +import android.support.annotation.Nullable; import android.util.Log; import android.view.View; import android.view.animation.AlphaAnimation; @@ -36,6 +37,7 @@ import android.widget.Toast; import org.thoughtcrime.securesms.R; import org.thoughtcrime.securesms.components.ThumbnailView; +import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.util.BitmapDecodingException; import java.io.File; @@ -106,11 +108,24 @@ public class AttachmentManager { 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) { + setMedia(slide, null); + } + + 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.addSlide(slide); attachmentView.setVisibility(View.VISIBLE); - thumbnail.setImageResource(slide); + thumbnail.setImageResource(slide, masterSecret); attachmentListener.onAttachmentChanged(); } diff --git a/src/org/thoughtcrime/securesms/mms/ImageSlide.java b/src/org/thoughtcrime/securesms/mms/ImageSlide.java index 606e00c27b..3c962a324b 100644 --- a/src/org/thoughtcrime/securesms/mms/ImageSlide.java +++ b/src/org/thoughtcrime/securesms/mms/ImageSlide.java @@ -20,25 +20,36 @@ import android.content.Context; import android.content.res.Resources.Theme; import android.net.Uri; import android.support.annotation.DrawableRes; +import android.support.annotation.Nullable; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.util.BitmapDecodingException; +import org.thoughtcrime.securesms.util.Util; +import java.io.File; import java.io.IOException; +import java.io.InputStream; import ws.com.google.android.mms.ContentType; import ws.com.google.android.mms.pdu.PduPart; public class ImageSlide extends Slide { private static final String TAG = ImageSlide.class.getSimpleName(); + private boolean encrypted = false; public ImageSlide(Context context, MasterSecret masterSecret, PduPart part) { super(context, masterSecret, part); } public ImageSlide(Context context, Uri uri) throws IOException, BitmapDecodingException { - super(context, constructPartFromUri(uri)); + this(context, null, uri); + } + + public ImageSlide(Context context, MasterSecret masterSecret, Uri uri) throws IOException, BitmapDecodingException { + super(context, masterSecret, constructPartFromByteArrayAndUri(uri, decryptContent(uri, masterSecret), masterSecret != null)); + encrypted = masterSecret != null; } @Override @@ -62,12 +73,32 @@ public class ImageSlide extends Slide { return true; } - private static PduPart constructPartFromUri(Uri uri) + @Override + 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 { PduPart part = new PduPart(); part.setDataUri(uri); + if (data != null) + part.setData(data); + part.setEncrypted(encrypted); part.setContentType(ContentType.IMAGE_JPEG.getBytes()); part.setContentId((System.currentTimeMillis()+"").getBytes()); part.setName(("Image" + System.currentTimeMillis()).getBytes()); diff --git a/src/org/thoughtcrime/securesms/mms/PartAuthority.java b/src/org/thoughtcrime/securesms/mms/PartAuthority.java index 6ffcdc3c86..1ff0b3c326 100644 --- a/src/org/thoughtcrime/securesms/mms/PartAuthority.java +++ b/src/org/thoughtcrime/securesms/mms/PartAuthority.java @@ -5,11 +5,13 @@ import android.content.Context; import android.content.UriMatcher; import android.net.Uri; +import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream; import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.PartDatabase; import org.thoughtcrime.securesms.providers.PartProvider; +import java.io.File; import java.io.IOException; import java.io.InputStream; @@ -46,7 +48,11 @@ public class PartAuthority { partUri = new PartUriParser(uri); return partDatabase.getThumbnailStream(masterSecret, partUri.getPartId()); default: - return context.getContentResolver().openInputStream(uri); + 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); } } catch (SecurityException se) { throw new IOException(se); diff --git a/src/org/thoughtcrime/securesms/mms/Slide.java b/src/org/thoughtcrime/securesms/mms/Slide.java index b4ce8bc981..080bdf1f9c 100644 --- a/src/org/thoughtcrime/securesms/mms/Slide.java +++ b/src/org/thoughtcrime/securesms/mms/Slide.java @@ -66,6 +66,10 @@ public abstract class Slide { return false; } + public boolean isEncrypted() { + return false; + } + public PduPart getPart() { return part; } diff --git a/src/org/thoughtcrime/securesms/util/BitmapUtil.java b/src/org/thoughtcrime/securesms/util/BitmapUtil.java index 74c9f5f797..484d585fab 100644 --- a/src/org/thoughtcrime/securesms/util/BitmapUtil.java +++ b/src/org/thoughtcrime/securesms/util/BitmapUtil.java @@ -209,7 +209,7 @@ public class BitmapUtil { } } - private static Bitmap rotateBitmap(Bitmap bitmap, int angle) { + public 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);