Add camera preview to message composition
|
@ -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"
|
||||
|
|
|
@ -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'
|
||||
|
|
BIN
res/drawable-hdpi/quick_camera_dark.png
Executable file
After Width: | Height: | Size: 457 B |
BIN
res/drawable-hdpi/quick_camera_exit_fullscreen.png
Executable file
After Width: | Height: | Size: 236 B |
BIN
res/drawable-hdpi/quick_camera_front.png
Executable file
After Width: | Height: | Size: 640 B |
BIN
res/drawable-hdpi/quick_camera_fullscreen.png
Executable file
After Width: | Height: | Size: 231 B |
BIN
res/drawable-hdpi/quick_camera_hide.png
Executable file
After Width: | Height: | Size: 324 B |
BIN
res/drawable-hdpi/quick_camera_light.png
Executable file
After Width: | Height: | Size: 469 B |
BIN
res/drawable-hdpi/quick_camera_rear.png
Executable file
After Width: | Height: | Size: 310 B |
BIN
res/drawable-hdpi/quick_shutter_button.png
Executable file
After Width: | Height: | Size: 1.2 KiB |
BIN
res/drawable-mdpi/quick_camera_dark.png
Executable file
After Width: | Height: | Size: 326 B |
BIN
res/drawable-mdpi/quick_camera_exit_fullscreen.png
Executable file
After Width: | Height: | Size: 193 B |
BIN
res/drawable-mdpi/quick_camera_front.png
Executable file
After Width: | Height: | Size: 362 B |
BIN
res/drawable-mdpi/quick_camera_fullscreen.png
Executable file
After Width: | Height: | Size: 193 B |
BIN
res/drawable-mdpi/quick_camera_hide.png
Executable file
After Width: | Height: | Size: 290 B |
BIN
res/drawable-mdpi/quick_camera_light.png
Executable file
After Width: | Height: | Size: 330 B |
BIN
res/drawable-mdpi/quick_camera_rear.png
Executable file
After Width: | Height: | Size: 310 B |
BIN
res/drawable-mdpi/quick_shutter_button.png
Executable file
After Width: | Height: | Size: 915 B |
BIN
res/drawable-xhdpi/quick_camera_dark.png
Executable file
After Width: | Height: | Size: 534 B |
BIN
res/drawable-xhdpi/quick_camera_exit_fullscreen.png
Executable file
After Width: | Height: | Size: 231 B |
BIN
res/drawable-xhdpi/quick_camera_front.png
Executable file
After Width: | Height: | Size: 644 B |
BIN
res/drawable-xhdpi/quick_camera_fullscreen.png
Executable file
After Width: | Height: | Size: 234 B |
BIN
res/drawable-xhdpi/quick_camera_hide.png
Executable file
After Width: | Height: | Size: 406 B |
BIN
res/drawable-xhdpi/quick_camera_light.png
Executable file
After Width: | Height: | Size: 548 B |
BIN
res/drawable-xhdpi/quick_camera_rear.png
Executable file
After Width: | Height: | Size: 560 B |
BIN
res/drawable-xhdpi/quick_shutter_button.png
Executable file
After Width: | Height: | Size: 1.7 KiB |
BIN
res/drawable-xxhdpi/quick_camera_dark.png
Executable file
After Width: | Height: | Size: 781 B |
BIN
res/drawable-xxhdpi/quick_camera_exit_fullscreen.png
Executable file
After Width: | Height: | Size: 303 B |
BIN
res/drawable-xxhdpi/quick_camera_front.png
Executable file
After Width: | Height: | Size: 983 B |
BIN
res/drawable-xxhdpi/quick_camera_fullscreen.png
Executable file
After Width: | Height: | Size: 305 B |
BIN
res/drawable-xxhdpi/quick_camera_hide.png
Executable file
After Width: | Height: | Size: 539 B |
BIN
res/drawable-xxhdpi/quick_camera_light.png
Executable file
After Width: | Height: | Size: 795 B |
BIN
res/drawable-xxhdpi/quick_camera_rear.png
Executable file
After Width: | Height: | Size: 784 B |
BIN
res/drawable-xxhdpi/quick_shutter_button.png
Executable file
After Width: | Height: | Size: 2.5 KiB |
27
res/drawable/quick_camera_shutter_ring.xml
Normal file
|
@ -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>
|
37
res/layout-land/quick_camera_controls.xml
Normal file
|
@ -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>
|
|
@ -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>
|
||||
|
|
37
res/layout/quick_camera_controls.xml
Normal file
|
@ -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>
|
|
@ -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"/>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
515
src/org/thoughtcrime/securesms/components/CameraView.java
Normal file
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
185
src/org/thoughtcrime/securesms/components/QuickCamera.java
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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");
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -66,6 +66,10 @@ public abstract class Slide {
|
|||
return false;
|
||||
}
|
||||
|
||||
public boolean isEncrypted() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public PduPart getPart() {
|
||||
return part;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|