refactor direct capture

Closes #3516
// FREEBIE
This commit is contained in:
Jake McGinty 2015-06-08 11:07:46 -07:00 committed by Moxie Marlinspike
parent c4a37e38ab
commit 54a37cc658
33 changed files with 1720 additions and 1540 deletions

View File

@ -11,6 +11,7 @@
android:label="Access to TextSecure Secrets"
android:protectionLevel="signature" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<uses-permission android:name="org.thoughtcrime.securesms.ACCESS_SECRETS"/>
<uses-permission android:name="android.permission.READ_PROFILE"/>
<uses-permission android:name="android.permission.WRITE_PROFILE"/>

View File

@ -29,7 +29,7 @@ repositories {
maven { // textdrawable
url 'https://dl.bintray.com/amulyakhare/maven'
}
maven {
maven { // cwac-camera
url 'https://repo.commonsware.com.s3.amazonaws.com'
}
jcenter()
@ -72,7 +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.+'
compile 'com.commonsware.cwac:camera:0.6.12'
provided 'com.squareup.dagger:dagger-compiler:1.2.2'
compile 'org.whispersystems:jobmanager:0.11.0'
@ -124,6 +124,7 @@ dependencyVerification {
'com.squareup.dagger:dagger:789aca24537022e49f91fc6444078d9de8f1dd99e1bfb090f18491b186967883',
'com.doomonafireball.betterpickers:library:132ecd685c95a99e7377c4e27bfadbb2d7ed0bea995944060cd62d4369fdaf3d',
'com.madgag.spongycastle:prov:b8c3fec3a59aac1aa04ccf4dad7179351e54ef7672f53f508151b614c131398a',
'com.commonsware.cwac:camera:dcc93ddbb2f0393114fa1f31a13fe9e6edfcf5dbe96b22bc4b66c7b15e179054',
'org.whispersystems:jobmanager:ea9cb943c4892fb90c1eea1be30efeb85cefca213d52c788419553b58d0ed70d',
'org.whispersystems:libpastelog:550d33c565380d90f4c671e7b8ed5f3a6da55a9fda468373177106b2eb5220b2',
'com.amulyakhare:com.amulyakhare.textdrawable:54c92b5fba38cfd316a07e5a30528068f45ce8515a6890f1297df4c401af5dcb',

View File

@ -1,27 +1,23 @@
<?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" >
<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" />
<size android:height="@dimen/quick_camera_shutter_ring_size" android:width="@dimen/quick_camera_shutter_ring_size" />
</shape>
</item>
<item>
<shape
android:innerRadiusRatio="3"
android:shape="ring"
android:thickness="2dp"
android:useLevel="false" >
<shape android:innerRadiusRatio="3"
android:shape="ring"
android:thickness="2dp"
android:useLevel="false" >
<solid android:color="#40ffffff" />
<size
android:height="52dp"
android:width="52dp" />
<size android:height="@dimen/quick_camera_shutter_ring_size" android:width="@dimen/quick_camera_shutter_ring_size" />
</shape>
</item>
</selector>

View File

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
tools:background="@android:color/darker_gray">
<ImageButton
android:id="@+id/shutter_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:background="@drawable/quick_camera_shutter_ring"
android:src="@drawable/quick_shutter_button"
android:padding="20dp"/>
<ImageButton
android:id="@+id/fullscreen_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:background="#00000000"
android:src="@drawable/quick_camera_fullscreen"
android:padding="20dp"/>
<ImageButton
android:id="@+id/swap_camera_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:background="#00000000"
android:src="@drawable/quick_camera_front"
android:padding="20dp"
android:visibility="invisible"
tools:visibility="visible"/>
</RelativeLayout>

View File

@ -1,5 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
@ -10,7 +9,7 @@
android:background="?conversation_background"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.QuickAttachmentDrawer
<org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/quick_attachment_drawer"
android:layout_width="match_parent"
@ -99,15 +98,17 @@
android:nextFocusForward="@+id/send_button"
android:nextFocusRight="@+id/send_button"
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.camera.HidingImageButton
android:id="@+id/quick_attachment_toggle"
android:layout_width="37dp"
android:layout_height="37dp"
android:layout_gravity="bottom"
android:src="?quick_camera_icon"
android:background="@drawable/touch_highlight_background"
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_toggle_description"
android:padding="10dp"/>
</LinearLayout>
<org.thoughtcrime.securesms.components.AnimatingToggle
android:id="@+id/button_toggle"
@ -156,5 +157,5 @@
</LinearLayout>
</RelativeLayout>
</org.thoughtcrime.securesms.components.QuickAttachmentDrawer>
</org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer>
</org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<org.thoughtcrime.securesms.components.camera.QuickCamera
android:id="@+id/quick_camera"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</merge>

View File

@ -1,37 +1,39 @@
<?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"/>
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/controls"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
tools:background="@android:color/darker_gray">
<ImageButton android:id="@+id/shutter_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_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>

View File

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/controls"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="bottom"
tools:background="@android:color/darker_gray">
<ImageButton android:id="@+id/shutter_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:background="@drawable/quick_camera_shutter_ring"
android:src="@drawable/quick_shutter_button"
android:padding="20dp"/>
<ImageButton android:id="@+id/fullscreen_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:background="#00000000"
android:src="@drawable/quick_camera_fullscreen"
android:padding="20dp"/>
<ImageButton android:id="@+id/swap_camera_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_alignParentRight="true"
android:background="#00000000"
android:src="@drawable/quick_camera_front"
android:padding="20dp"
android:visibility="invisible"
tools:visibility="visible"/>
</RelativeLayout>

View File

@ -22,11 +22,12 @@
<dimen name="media_bubble_height">210dp</dimen>
<dimen name="media_bubble_border_width">3dp</dimen>
<integer name="media_overview_cols">3</integer>
<dimen name="message_details_table_row_pad">10dp</dimen>
<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>
<dimen name="quick_camera_shutter_ring_size">52dp</dimen>
</resources>

View File

@ -512,7 +512,7 @@
<string name="conversation_title_view__conversation_muted">Conversation muted</string>
<!-- conversation_activity -->
<string name="conversation_activity__type_message_push">Send TextSecure message</string>
<string name="conversation_activity__type_message_push">Send via TextSecure</string>
<string name="conversation_activity__type_message_sms_insecure">Send unsecured SMS</string>
<string name="conversation_activity__type_message_mms_insecure">Send unsecured MMS</string>
<string name="conversation_activity__send">Send</string>

View File

@ -40,7 +40,6 @@
<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">

View File

@ -28,6 +28,8 @@ import android.graphics.Color;
import android.graphics.PorterDuff;
import android.graphics.PorterDuff.Mode;
import android.graphics.drawable.ColorDrawable;
import android.content.res.Configuration;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
@ -39,7 +41,6 @@ import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
@ -71,9 +72,10 @@ import org.thoughtcrime.securesms.components.emoji.EmojiPopup;
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.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.components.camera.HidingImageButton;
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.AttachmentDrawerListener;
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.DrawerState;
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
@ -118,8 +120,6 @@ import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@ -140,7 +140,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
implements ConversationFragment.ConversationFragmentListener,
AttachmentManager.AttachmentListener,
RecipientsModifiedListener,
OnKeyboardShownListener
OnKeyboardShownListener,
AttachmentDrawerListener
{
private static final String TAG = ConversationActivity.class.getSimpleName();
@ -157,7 +158,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private static final int PICK_AUDIO = 3;
private static final int PICK_CONTACT_INFO = 4;
private static final int GROUP_EDIT = 5;
private static final int CAPTURE_PHOTO = 6;
private MasterSecret masterSecret;
protected ComposeText composeText;
@ -178,7 +178,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private BroadcastReceiver groupUpdateReceiver;
private Optional<EmojiPopup> emojiPopup = Optional.absent();
private EmojiToggle emojiToggle;
private ImageButton quickAttachmentToggle;
private HidingImageButton quickAttachmentToggle;
private QuickAttachmentDrawer quickAttachmentDrawer;
private Recipients recipients;
@ -262,6 +262,12 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
quickAttachmentDrawer.onPause();
}
@Override public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.w(TAG, String.format("onConfigurationChanged(%d -> %d)", getResources().getConfiguration().orientation, newConfig.orientation));
quickAttachmentDrawer.onConfigurationChanged();
}
@Override
protected void onDestroy() {
saveDraft();
@ -276,11 +282,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Log.w(TAG, "onActivityResult called: " + reqCode + ", " + resultCode + " , " + data);
super.onActivityResult(reqCode, resultCode, data);
if ((data == null && reqCode != CAPTURE_PHOTO) || resultCode != RESULT_OK) return;
if (data == null || resultCode != RESULT_OK) return;
switch (reqCode) {
case PICK_IMAGE:
addAttachmentImage(data.getData());
addAttachmentImage(masterSecret, data.getData());
break;
case PICK_VIDEO:
addAttachmentVideo(data.getData());
@ -291,11 +297,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case PICK_CONTACT_INFO:
addAttachmentContactInfo(data.getData());
break;
case CAPTURE_PHOTO:
if (attachmentManager.getCaptureFile() != null) {
addAttachmentImage(Uri.fromFile(attachmentManager.getCaptureFile()));
}
break;
case GROUP_EDIT:
this.recipients = RecipientFactory.getRecipientsForIds(this, data.getLongArrayExtra(GroupCreateActivity.GROUP_RECIPIENT_EXTRA), true);
titleView.setTitle(recipients);
@ -377,8 +378,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public void onBackPressed() {
if (isEmojiDrawerOpen()) {
hideEmojiPopup(false);
} else if (quickAttachmentDrawer.getDrawerState() != QuickAttachmentDrawer.COLLAPSED) {
quickAttachmentDrawer.setDrawerStateAndAnimate(QuickAttachmentDrawer.COLLAPSED);
} else if (quickAttachmentDrawer.isOpen()) {
quickAttachmentDrawer.close();
} else {
super.onBackPressed();
}
@ -666,7 +667,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Uri draftVideo = getIntent().getParcelableExtra(DRAFT_VIDEO_EXTRA);
if (draftText != null) composeText.setText(draftText);
if (draftImage != null) addAttachmentImage(draftImage);
if (draftImage != null) addAttachmentImage(masterSecret, draftImage);
if (draftAudio != null) addAttachmentAudio(draftAudio);
if (draftVideo != null) addAttachmentVideo(draftVideo);
@ -702,13 +703,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (draft.getType().equals(Draft.TEXT)) {
composeText.setText(draft.getValue());
} else if (draft.getType().equals(Draft.IMAGE)) {
addAttachmentImage(Uri.parse(draft.getValue()));
addAttachmentImage(masterSecret, Uri.parse(draft.getValue()));
} else if (draft.getType().equals(Draft.AUDIO)) {
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()));
}
}
@ -791,7 +790,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
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);
quickAttachmentToggle = (HidingImageButton) findViewById(R.id.quick_attachment_toggle);
int[] attributes = new int[]{R.attr.conversation_item_bubble_background};
TypedArray colors = obtainStyledAttributes(attributes);
@ -842,11 +841,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
composeText.setOnFocusChangeListener(composeKeyPressedListener);
emojiToggle.setOnClickListener(new EmojiToggleListener());
if (quickAttachmentDrawer.hasCamera()) {
QuickAttachmentDrawerToggleListener listener = new QuickAttachmentDrawerToggleListener();
quickAttachmentDrawer.setQuickAttachmentDrawerListener(listener);
quickAttachmentDrawer.setQuickCameraListener(listener);
quickAttachmentToggle.setOnClickListener(listener);
if (QuickAttachmentDrawer.isDeviceSupported(this)) {
quickAttachmentDrawer.setListener(this);
quickAttachmentToggle.setOnClickListener(new QuickAttachmentToggleListener());
} else {
quickAttachmentToggle.setVisibility(View.GONE);
}
@ -957,8 +954,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void addAttachment(int type) {
Log.w("ComposeMessageActivity", "Selected: " + type);
switch (type) {
case AttachmentTypeSelectorAdapter.TAKE_PHOTO:
attachmentManager.capturePhoto(this, CAPTURE_PHOTO); break;
case AttachmentTypeSelectorAdapter.ADD_IMAGE:
AttachmentManager.selectImage(this, PICK_IMAGE); break;
case AttachmentTypeSelectorAdapter.ADD_VIDEO:
@ -970,20 +965,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void addAttachmentEncryptedImage(Uri uri) {
private void addAttachmentImage(MasterSecret masterSecret, Uri imageUri) {
try {
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);
attachmentManager.setImage(masterSecret, imageUri);
} catch (IOException | BitmapDecodingException e) {
Log.w(TAG, e);
attachmentManager.clear();
@ -1065,13 +1049,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
for (Slide slide : attachmentManager.getSlideDeck().getSlides()) {
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()));
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()));
}
return drafts;
@ -1317,11 +1297,29 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void updateToggleButtonState() {
if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) {
buttonToggle.display(attachButton);
quickAttachmentToggle.show();
} else {
buttonToggle.display(sendButton);
quickAttachmentToggle.hide();
}
}
@Override
public void onAttachmentDrawerClosed() {
getSupportActionBar().show();
}
@Override
public void onAttachmentDrawerOpened() {
getSupportActionBar().hide();
}
@Override
public void onImageCapture(@NonNull final Bitmap bitmap) {
attachmentManager.setCaptureImage(masterSecret, bitmap);
quickAttachmentDrawer.close();
}
// Listeners
private class AttachmentTypeListener implements DialogInterface.OnClickListener {
@ -1347,68 +1345,19 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
});
input.hideSoftInputFromWindow(composeText.getWindowToken(), 0);
quickAttachmentDrawer.setDrawerStateAndAnimate(QuickAttachmentDrawer.COLLAPSED);
quickAttachmentDrawer.setDrawerStateAndAnimate(DrawerState.COLLAPSED);
}
}
}
private class QuickAttachmentDrawerToggleListener implements OnClickListener,
QuickAttachmentDrawer.QuickAttachmentDrawerListener,
QuickCamera.QuickCameraListener {
@QuickAttachmentDrawer.DrawerState int nextDrawerState = QuickAttachmentDrawer.HALF_EXPANDED;
private class QuickAttachmentToggleListener implements OnClickListener {
@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();
quickAttachmentDrawer.open();
}
}
@ -1484,8 +1433,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus && isEmojiDrawerOpen()) {
hideEmojiPopup(true);
} else if (hasFocus && quickAttachmentDrawer.getDrawerState() != QuickAttachmentDrawer.COLLAPSED) {
quickAttachmentDrawer.setDrawerStateAndAnimate(QuickAttachmentDrawer.COLLAPSED);
} else if (hasFocus && quickAttachmentDrawer.isOpen()) {
quickAttachmentDrawer.close();
}
}
}

View File

@ -1,7 +1,8 @@
package org.thoughtcrime.securesms;
import android.content.Intent;
import android.os.Build;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityOptionsCompat;
@ -84,7 +85,7 @@ public class ConversationPopupActivity extends ConversationActivity {
intent.putExtra(ConversationActivity.RECIPIENTS_EXTRA, getRecipients().getIds());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, result);
if ( Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
startActivity(intent, transition.toBundle());
} else {
startActivity(intent);

View File

@ -1,515 +0,0 @@
/***
Copyright (c) 2013-2014 CommonsWare, LLC
Portions Copyright (C) 2007 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.thoughtcrime.securesms.components;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.hardware.Camera;
import android.hardware.Camera.AutoFocusCallback;
import android.hardware.Camera.PreviewCallback;
import android.os.Build;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import java.io.IOException;
import com.commonsware.cwac.camera.CameraHost;
import com.commonsware.cwac.camera.CameraHost.FailureReason;
import com.commonsware.cwac.camera.CameraHostProvider;
import com.commonsware.cwac.camera.PreviewStrategy;
public class CameraView extends ViewGroup implements AutoFocusCallback {
static final String TAG = "CWAC-Camera";
private PreviewStrategy previewStrategy;
private Camera.Size previewSize;
private Camera camera = null;
private boolean inPreview = false;
private CameraHost host = null;
private OnOrientationChange onOrientationChange = null;
private int displayOrientation = -1;
private int outputOrientation = -1;
private int cameraId = -1;
private boolean isAutoFocusing = false;
private int lastPictureOrientation = -1;
public CameraView(Context context) {
super(context);
onOrientationChange = new OnOrientationChange(context.getApplicationContext());
}
public CameraView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CameraView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
onOrientationChange = new OnOrientationChange(context.getApplicationContext());
if (context instanceof CameraHostProvider) {
setHost(((CameraHostProvider)context).getCameraHost());
} else {
throw new IllegalArgumentException("To use the two- or "
+ "three-parameter constructors on CameraView, "
+ "your activity needs to implement the "
+ "CameraHostProvider interface");
}
}
public CameraHost getHost() {
return (host);
}
// must call this after constructor, before onResume()
public void setHost(CameraHost host) {
this.host = host;
if (host.getDeviceProfile().useTextureView()) {
previewStrategy = new TexturePreviewStrategy(this);
} else {
previewStrategy = new SurfacePreviewStrategy(this);
}
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void onResume() {
addView(previewStrategy.getWidget());
if (camera == null) {
try {
cameraId = getHost().getCameraId();
if (cameraId >= 0) {
camera = Camera.open(cameraId);
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
onOrientationChange.enable();
}
setCameraDisplayOrientation();
}
else {
getHost().onCameraFail(FailureReason.NO_CAMERAS_REPORTED);
}
}
catch (Exception e) {
getHost().onCameraFail(FailureReason.UNKNOWN);
}
}
}
public void onPause() {
if (camera != null) {
previewDestroyed();
}
removeView(previewStrategy.getWidget());
onOrientationChange.disable();
lastPictureOrientation=-1;
}
// based on CameraPreview.java from ApiDemos
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int width=
resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
final int height=
resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);
setMeasuredDimension(width, height);
if (width > 0 && height > 0) {
if (camera != null) {
Camera.Size newSize=null;
try {
if (getHost().getRecordingHint() != CameraHost.RecordingHint.STILL_ONLY) {
newSize=
getHost().getPreferredPreviewSizeForVideo(getDisplayOrientation(),
width,
height,
camera.getParameters(),
null);
}
if (newSize == null || newSize.width * newSize.height < 65536) {
newSize=
getHost().getPreviewSize(getDisplayOrientation(),
width, height,
camera.getParameters());
}
}
catch (Exception e) {
android.util.Log.e(getClass().getSimpleName(),
"Could not work with camera parameters?",
e);
// TODO get this out to library clients
}
if (newSize != null) {
if (previewSize == null) {
previewSize=newSize;
}
else if (previewSize.width != newSize.width
|| previewSize.height != newSize.height) {
if (inPreview) {
stopPreview();
}
previewSize=newSize;
initPreview(width, height, false);
}
}
}
}
}
// based on CameraPreview.java from ApiDemos
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed && getChildCount() > 0) {
final View child=getChildAt(0);
final int width=r - l;
final int height=b - t;
int previewWidth=width;
int previewHeight=height;
// handle orientation
if (previewSize != null) {
if (getDisplayOrientation() == 90
|| getDisplayOrientation() == 270) {
previewWidth=previewSize.height;
previewHeight=previewSize.width;
}
else {
previewWidth=previewSize.width;
previewHeight=previewSize.height;
}
}
boolean useFirstStrategy=
(width * previewHeight > height * previewWidth);
boolean useFullBleed=getHost().useFullBleedPreview();
if ((useFirstStrategy && !useFullBleed)
|| (!useFirstStrategy && useFullBleed)) {
final int scaledChildWidth=
previewWidth * height / previewHeight;
child.layout((width - scaledChildWidth) / 2, 0,
(width + scaledChildWidth) / 2, height);
}
else {
final int scaledChildHeight=
previewHeight * width / previewWidth;
child.layout(0, (height - scaledChildHeight) / 2, width,
(height + scaledChildHeight) / 2);
}
}
}
public int getDisplayOrientation() {
return(displayOrientation);
}
public void lockToLandscape(boolean enable) {
if (enable) {
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
onOrientationChange.enable();
}
else {
getActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
onOrientationChange.disable();
}
}
public void restartPreview() {
if (!inPreview) {
startPreview();
}
}
public void autoFocus() {
if (inPreview) {
camera.autoFocus(this);
isAutoFocusing=true;
}
}
public void cancelAutoFocus() {
camera.cancelAutoFocus();
}
public boolean isAutoFocusAvailable() {
return(inPreview);
}
@Override
public void onAutoFocus(boolean success, Camera camera) {
isAutoFocusing=false;
if (getHost() instanceof AutoFocusCallback) {
getHost().onAutoFocus(success, camera);
}
}
public String getFlashMode() {
return(camera.getParameters().getFlashMode());
}
public void setFlashMode(String mode) {
if (camera != null) {
Camera.Parameters params=camera.getParameters();
params.setFlashMode(mode);
camera.setParameters(params);
}
}
public void setOneShotPreviewCallback(PreviewCallback callback) {
if (camera != null)
camera.setOneShotPreviewCallback(callback);
}
public Camera.Parameters getCameraParameters() {
return camera.getParameters();
}
void previewCreated() {
if (camera != null) {
try {
previewStrategy.attach(camera);
}
catch (IOException e) {
getHost().handleException(e);
}
}
}
void previewDestroyed() {
if (camera != null) {
previewStopped();
camera.release();
camera=null;
}
}
void previewReset(int width, int height) {
previewStopped();
initPreview(width, height);
}
private void previewStopped() {
if (inPreview) {
stopPreview();
}
}
public void initPreview(int w, int h) {
initPreview(w, h, true);
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void initPreview(int w, int h, boolean firstRun) {
if (camera != null) {
Camera.Parameters parameters=camera.getParameters();
parameters.setPreviewSize(previewSize.width, previewSize.height);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
parameters.setRecordingHint(getHost().getRecordingHint() != CameraHost.RecordingHint.STILL_ONLY);
}
requestLayout();
camera.setParameters(getHost().adjustPreviewParameters(parameters));
startPreview();
}
}
private void startPreview() {
camera.startPreview();
inPreview=true;
getHost().autoFocusAvailable();
}
private void stopPreview() {
inPreview=false;
getHost().autoFocusUnavailable();
camera.stopPreview();
}
// based on
// http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
// and http://stackoverflow.com/a/10383164/115145
private void setCameraDisplayOrientation() {
Camera.CameraInfo info=new Camera.CameraInfo();
int rotation=
getActivity().getWindowManager().getDefaultDisplay()
.getRotation();
int degrees=0;
DisplayMetrics dm=new DisplayMetrics();
Camera.getCameraInfo(cameraId, info);
getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
switch (rotation) {
case Surface.ROTATION_0:
degrees=0;
break;
case Surface.ROTATION_90:
degrees=90;
break;
case Surface.ROTATION_180:
degrees=180;
break;
case Surface.ROTATION_270:
degrees=270;
break;
}
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
displayOrientation=(info.orientation + degrees) % 360;
displayOrientation=(360 - displayOrientation) % 360;
}
else {
displayOrientation=(info.orientation - degrees + 360) % 360;
}
boolean wasInPreview=inPreview;
if (inPreview) {
stopPreview();
}
camera.setDisplayOrientation(displayOrientation);
if (wasInPreview) {
startPreview();
}
}
public int getCameraPictureOrientation() {
Camera.CameraInfo info=new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
outputOrientation=
getCameraPictureRotation(getActivity().getWindowManager()
.getDefaultDisplay()
.getOrientation());
}
else if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
outputOrientation=(360 - displayOrientation) % 360;
}
else {
outputOrientation=displayOrientation;
}
if (lastPictureOrientation != outputOrientation) {
lastPictureOrientation=outputOrientation;
}
return outputOrientation;
}
// based on:
// http://developer.android.com/reference/android/hardware/Camera.Parameters.html#setRotation(int)
public int getCameraPictureRotation(int orientation) {
Camera.CameraInfo info=new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
int rotation=0;
orientation=(orientation + 45) / 90 * 90;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
rotation=(info.orientation - orientation + 360) % 360;
}
else { // back-facing camera
rotation=(info.orientation + orientation) % 360;
}
return(rotation);
}
Activity getActivity() {
return((Activity)getContext());
}
private class OnOrientationChange extends OrientationEventListener {
private boolean isEnabled=false;
public OnOrientationChange(Context context) {
super(context);
disable();
}
@Override
public void onOrientationChanged(int orientation) {
if (camera != null && orientation != ORIENTATION_UNKNOWN) {
int newOutputOrientation=getCameraPictureRotation(orientation);
if (newOutputOrientation != outputOrientation) {
outputOrientation=newOutputOrientation;
Camera.Parameters params=camera.getParameters();
params.setRotation(outputOrientation);
try {
camera.setParameters(params);
lastPictureOrientation=outputOrientation;
}
catch (Exception e) {
Log.e(getClass().getSimpleName(),
"Exception updating camera parameters in orientation change",
e);
// TODO: get this info out to hosting app
}
}
}
}
@Override
public void enable() {
isEnabled=true;
super.enable();
}
@Override
public void disable() {
isEnabled=false;
super.disable();
}
boolean isEnabled() {
return(isEnabled);
}
}
}

View File

@ -64,7 +64,6 @@ public class KeyboardAwareLinearLayout extends LinearLayoutCompat {
}
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
Log.w(TAG, String.format("onMeasure(%s, %s)", MeasureSpec.toString(widthMeasureSpec), MeasureSpec.toString(heightMeasureSpec)));
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int res = getResources().getIdentifier("status_bar_height", "dimen", "android");

View File

@ -1,503 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.hardware.Camera;
import android.os.Build;
import android.support.annotation.IntDef;
import android.support.annotation.NonNull;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageButton;
import com.commonsware.cwac.camera.SimpleCameraHost;
import org.thoughtcrime.securesms.R;
public class QuickAttachmentDrawer extends ViewGroup {
@IntDef({COLLAPSED, HALF_EXPANDED, FULL_EXPANDED})
public @interface DrawerState {}
public static final int COLLAPSED = 0;
public static final int HALF_EXPANDED = 1;
public static final int FULL_EXPANDED = 2;
private static final float FULL_EXPANDED_ANCHOR_POINT = 1.f;
private static final float COLLAPSED_ANCHOR_POINT = 0.f;
private final ViewDragHelper dragHelper;
private final QuickCamera quickCamera;
private final View controls;
private View coverView;
private ImageButton fullScreenButton;
private @DrawerState int drawerState;
private float slideOffset, initialMotionX, initialMotionY, halfExpandedAnchorPoint;
private boolean initialSetup, hasCamera, startCamera, stopCamera, landscape, belowICS;
private int slideRange, baseHalfHeight;
private Rect drawChildrenRect = new Rect();
private QuickAttachmentDrawerListener listener;
public QuickAttachmentDrawer(Context context) {
this(context, null);
}
public QuickAttachmentDrawer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public QuickAttachmentDrawer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
initialSetup = true;
startCamera = false;
stopCamera = false;
drawerState = COLLAPSED;
baseHalfHeight = getResources().getDimensionPixelSize(R.dimen.quick_media_drawer_default_height);
halfExpandedAnchorPoint = COLLAPSED_ANCHOR_POINT;
int rotation = ((WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation();
landscape = rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
belowICS = android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH;
hasCamera = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA) && Camera.getNumberOfCameras() > 0;
if (hasCamera) {
setBackgroundResource(android.R.color.black);
dragHelper = ViewDragHelper.create(this, 1.f, new ViewDragHelperCallback());
quickCamera = new QuickCamera(context);
controls = inflate(getContext(), R.layout.quick_camera_controls, null);
initializeControlsView();
addView(quickCamera);
addView(controls);
} else {
dragHelper = null;
quickCamera = null;
controls = null;
}
}
public boolean hasCamera() {
return hasCamera;
}
private void initializeHalfExpandedAnchorPoint() {
if (initialSetup) {
if (getChildCount() == 3)
coverView = getChildAt(2);
else
coverView = getChildAt(0);
slideRange = getMeasuredHeight();
int anchorHeight = slideRange - baseHalfHeight;
halfExpandedAnchorPoint = computeSlideOffsetFromCoverBottom(anchorHeight);
initialSetup = false;
}
}
private void initializeControlsView() {
controls.findViewById(R.id.shutter_button).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
boolean crop = drawerState != FULL_EXPANDED;
int imageHeight = crop ? baseHalfHeight : quickCamera.getMeasuredHeight();
Rect previewRect = new Rect(0, 0, quickCamera.getMeasuredWidth(), imageHeight);
quickCamera.takePicture(crop, previewRect);
}
});
final ImageButton swapCameraButton = (ImageButton) controls.findViewById(R.id.swap_camera_button);
if (quickCamera.isMultipleCameras()) {
swapCameraButton.setVisibility(View.VISIBLE);
swapCameraButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
quickCamera.swapCamera();
swapCameraButton.setImageResource(quickCamera.isRearCamera() ? R.drawable.quick_camera_front : R.drawable.quick_camera_rear);
}
});
}
fullScreenButton = (ImageButton) controls.findViewById(R.id.fullscreen_button);
fullScreenButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (drawerState == HALF_EXPANDED || drawerState == COLLAPSED)
setDrawerStateAndAnimate(FULL_EXPANDED);
else if (landscape || belowICS)
setDrawerStateAndAnimate(COLLAPSED);
else
setDrawerStateAndAnimate(HALF_EXPANDED);
}
});
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int paddingLeft = getPaddingLeft();
final int paddingTop = getPaddingTop();
final int childCount = getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final int childHeight = child.getMeasuredHeight();
int childTop = paddingTop;
int childBottom;
int childLeft = paddingLeft;
if (child == quickCamera) {
childTop = computeCameraTopPosition(slideOffset);
childBottom = childTop + childHeight;
if (quickCamera.getMeasuredWidth() < getMeasuredWidth())
childLeft = (getMeasuredWidth() - quickCamera.getMeasuredWidth()) / 2 + paddingLeft;
} else if (child == controls) {
childBottom = getMeasuredHeight();
} else {
childBottom = computeCoverBottomPosition(slideOffset);
childTop = childBottom - childHeight;
}
final int childRight = childLeft + child.getMeasuredWidth();
if (childHeight > 0)
child.layout(childLeft, childTop, childRight, childBottom);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
} else if (heightMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Height must have an exact value or MATCH_PARENT");
}
final int childCount = getChildCount();
if ((hasCamera && childCount != 3) || (!hasCamera && childCount != 1))
throw new IllegalStateException("QuickAttachmentDrawer layouts may only have 1 child.");
int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
for (int i = 0; i < childCount; i++) {
final View child = getChildAt(i);
final LayoutParams lp = child.getLayoutParams();
if (child.getVisibility() == GONE && i == 0) {
continue;
}
int childWidthSpec;
switch (lp.width) {
case LayoutParams.WRAP_CONTENT:
childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
break;
case LayoutParams.MATCH_PARENT:
childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
break;
default:
childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
break;
}
int childHeightSpec;
switch (lp.height) {
case LayoutParams.WRAP_CONTENT:
childHeightSpec = MeasureSpec.makeMeasureSpec(layoutHeight, MeasureSpec.AT_MOST);
break;
case LayoutParams.MATCH_PARENT:
childHeightSpec = MeasureSpec.makeMeasureSpec(layoutHeight, MeasureSpec.EXACTLY);
break;
default:
childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
break;
}
child.measure(childWidthSpec, childHeightSpec);
}
setMeasuredDimension(widthSize, heightSize);
initializeHalfExpandedAnchorPoint();
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (h != oldh)
initialSetup = true;
}
@Override
protected boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) {
boolean result;
final int save = canvas.save(Canvas.CLIP_SAVE_FLAG);
canvas.getClipBounds(drawChildrenRect);
if (child == coverView)
drawChildrenRect.bottom = Math.min(drawChildrenRect.bottom, child.getBottom());
else if (coverView != null)
drawChildrenRect.top = Math.max(drawChildrenRect.top, coverView.getBottom());
canvas.clipRect(drawChildrenRect);
result = super.drawChild(canvas, child, drawingTime);
canvas.restoreToCount(save);
return result;
}
@Override
public void computeScroll() {
if (dragHelper != null && dragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
} else if (stopCamera) {
stopCamera = false;
quickCamera.onPause();
} else if (startCamera) {
startCamera = false;
quickCamera.onResume();
}
}
private void setDrawerState(@DrawerState int drawerState) {
if (hasCamera) {
switch (drawerState) {
case COLLAPSED:
quickCamera.previewCreated();
if (quickCamera.isStarted())
stopCamera = true;
slideOffset = COLLAPSED_ANCHOR_POINT;
startCamera = false;
fullScreenButton.setImageResource(R.drawable.quick_camera_fullscreen);
if (listener != null) listener.onCollapsed();
break;
case HALF_EXPANDED:
if (landscape || belowICS) {
setDrawerState(FULL_EXPANDED);
return;
}
if (!quickCamera.isStarted())
startCamera = true;
slideOffset = halfExpandedAnchorPoint;
stopCamera = false;
fullScreenButton.setImageResource(R.drawable.quick_camera_fullscreen);
if (listener != null) listener.onHalfExpanded();
break;
case FULL_EXPANDED:
if (!quickCamera.isStarted())
startCamera = true;
slideOffset = FULL_EXPANDED_ANCHOR_POINT;
stopCamera = false;
fullScreenButton.setImageResource(landscape || belowICS ? R.drawable.quick_camera_hide : R.drawable.quick_camera_exit_fullscreen);
if (listener != null) listener.onExpanded();
break;
}
this.drawerState = drawerState;
}
}
public
@DrawerState
int getDrawerState() {
return drawerState;
}
public void setDrawerStateAndAnimate(@DrawerState int drawerState) {
setDrawerState(drawerState);
slideTo(slideOffset);
}
public void setQuickAttachmentDrawerListener(QuickAttachmentDrawerListener listener) {
this.listener = listener;
}
public void setQuickCameraListener(QuickCamera.QuickCameraListener listener) {
if (quickCamera != null) quickCamera.setQuickCameraListener(listener);
}
public interface QuickAttachmentDrawerListener {
void onCollapsed();
void onExpanded();
void onHalfExpanded();
}
private class ViewDragHelperCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == controls && !belowICS;
}
@Override
public void onViewDragStateChanged(int state) {
if (dragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
setDrawerState(drawerState);
requestLayout();
}
}
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
int newTop = coverView.getTop() + dy;
final int expandedTop = computeCoverBottomPosition(FULL_EXPANDED_ANCHOR_POINT) - coverView.getHeight();
final int collapsedTop = computeCoverBottomPosition(COLLAPSED_ANCHOR_POINT) - coverView.getHeight();
newTop = Math.min(Math.max(newTop, expandedTop), collapsedTop);
slideOffset = computeSlideOffsetFromCoverBottom(newTop + coverView.getHeight());
requestLayout();
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (releasedChild == controls) {
float direction = -yvel;
int drawerState = COLLAPSED;
if (direction > 1) {
drawerState = FULL_EXPANDED;
} else if (direction < -1) {
boolean halfExpand = (slideOffset > halfExpandedAnchorPoint && !landscape);
drawerState = halfExpand ? HALF_EXPANDED : COLLAPSED;
} else if (!landscape) {
if (halfExpandedAnchorPoint != 1 && slideOffset >= (1.f + halfExpandedAnchorPoint) / 2) {
drawerState = FULL_EXPANDED;
} else if (halfExpandedAnchorPoint == 1 && slideOffset >= 0.5f) {
drawerState = FULL_EXPANDED;
} else if (halfExpandedAnchorPoint != 1 && slideOffset >= halfExpandedAnchorPoint) {
drawerState = HALF_EXPANDED;
} else if (halfExpandedAnchorPoint != 1 && slideOffset >= halfExpandedAnchorPoint / 2) {
drawerState = HALF_EXPANDED;
}
}
setDrawerState(drawerState);
dragHelper.captureChildView(coverView, 0);
dragHelper.settleCapturedViewAt(coverView.getLeft(), computeCoverBottomPosition(slideOffset) - coverView.getHeight());
dragHelper.captureChildView(quickCamera, 0);
dragHelper.settleCapturedViewAt(quickCamera.getLeft(), computeCameraTopPosition(slideOffset));
ViewCompat.postInvalidateOnAnimation(QuickAttachmentDrawer.this);
}
}
@Override
public int getViewVerticalDragRange(View child) {
return slideRange;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (dragHelper != null) {
final int action = MotionEventCompat.getActionMasked(event);
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
dragHelper.cancel();
return false;
}
final float x = event.getX();
final float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN: {
initialMotionX = x;
initialMotionY = y;
break;
}
case MotionEvent.ACTION_MOVE: {
final float adx = Math.abs(x - initialMotionX);
final float ady = Math.abs(y - initialMotionY);
final int dragSlop = dragHelper.getTouchSlop();
if (adx > dragSlop && ady < dragSlop) {
return super.onInterceptTouchEvent(event);
}
if ((ady > dragSlop && adx > ady) || !isDragViewUnder((int) initialMotionX, (int) initialMotionY)) {
dragHelper.cancel();
return false;
}
break;
}
}
return dragHelper.shouldInterceptTouchEvent(event);
}
return super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (dragHelper != null) {
dragHelper.processTouchEvent(event);
return true;
}
return super.onTouchEvent(event);
}
private boolean isDragViewUnder(int x, int y) {
int[] viewLocation = new int[2];
quickCamera.getLocationOnScreen(viewLocation);
int[] parentLocation = new int[2];
this.getLocationOnScreen(parentLocation);
int screenX = parentLocation[0] + x;
int screenY = parentLocation[1] + y;
return screenX >= viewLocation[0] && screenX < viewLocation[0] + quickCamera.getWidth() &&
screenY >= viewLocation[1] && screenY < viewLocation[1] + quickCamera.getHeight();
}
private int computeCameraTopPosition(float slideOffset) {
float clampedOffset = slideOffset - halfExpandedAnchorPoint;
if (clampedOffset < COLLAPSED_ANCHOR_POINT)
clampedOffset = COLLAPSED_ANCHOR_POINT;
else
clampedOffset = clampedOffset / (FULL_EXPANDED_ANCHOR_POINT - halfExpandedAnchorPoint);
float slidePixelOffset = slideOffset * slideRange +
(quickCamera.getMeasuredHeight() - baseHalfHeight) / 2 * (FULL_EXPANDED_ANCHOR_POINT - clampedOffset);
float marginPixelOffset = (getMeasuredHeight() - quickCamera.getMeasuredHeight()) / 2 * clampedOffset;
return (int) (getMeasuredHeight() - slidePixelOffset + marginPixelOffset);
}
private int computeCoverBottomPosition(float slideOffset) {
int slidePixelOffset = (int) (slideOffset * slideRange);
return getMeasuredHeight() - getPaddingBottom() - slidePixelOffset;
}
private void slideTo(float slideOffset) {
if (dragHelper != null && !belowICS) {
dragHelper.smoothSlideViewTo(coverView, coverView.getLeft(), computeCoverBottomPosition(slideOffset) - coverView.getHeight());
dragHelper.smoothSlideViewTo(quickCamera, quickCamera.getLeft(), computeCameraTopPosition(slideOffset));
ViewCompat.postInvalidateOnAnimation(this);
} else {
invalidate();
}
}
private float computeSlideOffsetFromCoverBottom(int topPosition) {
final int topBoundCollapsed = computeCoverBottomPosition(0);
return (float) (topBoundCollapsed - topPosition) / slideRange;
}
public void onPause() {
quickCamera.onPause();
}
public void onResume() {
if (hasCamera && (drawerState == HALF_EXPANDED || drawerState == FULL_EXPANDED))
quickCamera.onResume();
}
}

View File

@ -1,185 +0,0 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.graphics.YuvImage;
import android.hardware.Camera;
import android.os.AsyncTask;
import android.view.ViewGroup;
import android.widget.Toast;
import com.commonsware.cwac.camera.SimpleCameraHost;
import org.thoughtcrime.securesms.R;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
public class QuickCamera extends CameraView {
private QuickCameraListener listener;
private boolean started, savingImage;
private int rotation;
private QuickCameraHost cameraHost;
public QuickCamera(Context context) {
super(context);
started = false;
savingImage = false;
setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
cameraHost = new QuickCameraHost(context);
setHost(cameraHost);
}
@Override
public void onResume() {
super.onResume();
rotation = getCameraPictureOrientation();
started = true;
}
@Override
public void onPause() {
started = false;
super.onPause();
}
public boolean isStarted() {
return started;
}
public void takePicture(final boolean crop, final Rect previewRect) {
setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, Camera camera) {
new AsyncTask<byte[], Void, byte[]>() {
@Override
protected byte[] doInBackground(byte[]... params) {
byte[] data = params[0];
if (savingImage)
return null;
savingImage = true;
try {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
int previewWidth = getCameraParameters().getPreviewSize().width;
int previewHeight = getCameraParameters().getPreviewSize().height;
YuvImage previewImage = new YuvImage(data, ImageFormat.NV21, previewWidth, previewHeight, null);
if (crop) {
float newWidth, newHeight;
if (rotation == 90 || rotation == 270) {
newWidth = previewRect.height();
newHeight = previewRect.width();
} else {
newWidth = previewRect.width();
newHeight = previewRect.height();
}
float centerX = previewWidth / 2;
float centerY = previewHeight / 2;
previewRect.set((int) (centerX - newWidth / 2),
(int) (centerY - newHeight / 2),
(int) (centerX + newWidth / 2),
(int) (centerY + newHeight / 2));
} else if (rotation == 90 || rotation == 270) {
previewRect.set(0, 0, previewRect.height(), previewRect.width());
}
previewImage.compressToJpeg(previewRect, 100, byteArrayOutputStream);
byte[] bytes = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.close();
byteArrayOutputStream = new ByteArrayOutputStream();
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (rotation != 0)
bitmap = rotateBitmap(bitmap, rotation);
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, byteArrayOutputStream);
byte[] finalImageByteArray = byteArrayOutputStream.toByteArray();
byteArrayOutputStream.close();
savingImage = false;
return finalImageByteArray;
} catch (IOException e) {
savingImage = false;
return null;
}
}
@Override
protected void onPostExecute(byte[] data) {
if (data != null && listener != null)
listener.onImageCapture(data);
}
}.execute(data);
}
});
}
private static Bitmap rotateBitmap(Bitmap bitmap, int angle) {
Matrix matrix = new Matrix();
matrix.postRotate(angle);
Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
if (rotated != bitmap) bitmap.recycle();
return rotated;
}
public void setQuickCameraListener(QuickCameraListener listener) {
this.listener = listener;
}
public boolean isMultipleCameras() {
return Camera.getNumberOfCameras() > 1;
}
public boolean isRearCamera() {
return cameraHost.getCameraId() == Camera.CameraInfo.CAMERA_FACING_BACK;
}
public void swapCamera() {
cameraHost.swapCameraId();
onPause();
onResume();
}
public interface QuickCameraListener {
void onImageCapture(final byte[] data);
}
private class QuickCameraHost extends SimpleCameraHost {
int cameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
public QuickCameraHost(Context context) {
super(context);
}
@Override
public Camera.Parameters adjustPreviewParameters(Camera.Parameters parameters) {
List<String> focusModes = parameters.getSupportedFocusModes();
if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE))
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
else if (focusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO))
parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
return parameters;
}
@Override
public int getCameraId() {
return cameraId;
}
public void swapCameraId() {
if (isMultipleCameras()) {
if (cameraId == Camera.CameraInfo.CAMERA_FACING_BACK)
cameraId = Camera.CameraInfo.CAMERA_FACING_FRONT;
else
cameraId = Camera.CameraInfo.CAMERA_FACING_BACK;
}
}
@Override
public void onCameraFail(FailureReason reason) {
super.onCameraFail(reason);
Toast.makeText(getContext(), R.string.quick_camera_unavailable, Toast.LENGTH_SHORT).show();
}
}
}

View File

@ -4,7 +4,6 @@ import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import android.support.annotation.NonNull;
@ -17,6 +16,7 @@ import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.widget.FrameLayout;
import com.bumptech.glide.DrawableTypeRequest;
import com.bumptech.glide.GenericRequestBuilder;
import com.bumptech.glide.Glide;
import com.bumptech.glide.request.RequestListener;
@ -108,10 +108,6 @@ public class ThumbnailView extends FrameLayout {
this.slideDeckFuture.addListener(this.slideDeckListener);
}
public void setImageResource(@NonNull Slide slide) {
setImageResource(slide, null);
}
public void setImageResource(@NonNull Slide slide, @Nullable MasterSecret masterSecret) {
if (Util.equals(slide, this.slide)) {
Log.w(TAG, "Not loading resource, slide was identical");
@ -175,28 +171,22 @@ public class ThumbnailView extends FrameLayout {
private GenericRequestBuilder buildThumbnailGlideRequest(Slide slide, MasterSecret masterSecret) {
final GenericRequestBuilder builder;
if (slide.isDraft() && slide.isEncrypted()) builder = buildEncryptedDraftGlideRequest(slide, masterSecret);
else if (slide.isDraft()) builder = buildDraftGlideRequest(slide);
else builder = buildEncryptedPartGlideRequest(slide, masterSecret);
if (slide.isDraft()) builder = buildDraftGlideRequest(slide, masterSecret);
else builder = buildPartGlideRequest(slide, masterSecret);
return builder;
}
private GenericRequestBuilder buildDraftGlideRequest(Slide slide) {
return Glide.with(getContext()).load(slide.getThumbnailUri()).asBitmap()
.fitCenter()
.listener(new PduThumbnailSetListener(slide.getPart()));
private GenericRequestBuilder buildDraftGlideRequest(Slide slide, MasterSecret masterSecret) {
final DrawableTypeRequest<?> request;
if (masterSecret == null) request = Glide.with(getContext()).load(slide.getThumbnailUri());
else request = Glide.with(getContext()).load(new DecryptableUri(masterSecret, slide.getThumbnailUri()));
return request.asBitmap()
.fitCenter()
.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) {
private GenericRequestBuilder buildPartGlideRequest(Slide slide, MasterSecret masterSecret) {
if (masterSecret == null) {
throw new IllegalStateException("null MasterSecret when loading non-draft thumbnail");
}
@ -290,7 +280,7 @@ public class ThumbnailView extends FrameLayout {
}
}
private static class PduThumbnailSetListener implements RequestListener<Uri, Bitmap> {
private static class PduThumbnailSetListener implements RequestListener<Object, Bitmap> {
private PduPart part;
public PduThumbnailSetListener(@NonNull PduPart part) {
@ -298,12 +288,12 @@ public class ThumbnailView extends FrameLayout {
}
@Override
public boolean onException(Exception e, Uri model, Target<Bitmap> target, boolean isFirstResource) {
public boolean onException(Exception e, Object model, Target<Bitmap> target, boolean isFirstResource) {
return false;
}
@Override
public boolean onResourceReady(Bitmap resource, Uri model, Target<Bitmap> target, boolean isFromMemoryCache, boolean isFirstResource) {
public boolean onResourceReady(Bitmap resource, Object model, Target<Bitmap> target, boolean isFromMemoryCache, boolean isFirstResource) {
part.setThumbnail(resource);
return false;
}

View File

@ -0,0 +1,499 @@
/***
Copyright (c) 2013-2014 CommonsWare, LLC
Portions Copyright (C) 2007 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.thoughtcrime.securesms.components.camera;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.hardware.Camera;
import android.hardware.Camera.PreviewCallback;
import android.os.Build;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.OrientationEventListener;
import android.view.Surface;
import android.view.View;
import android.widget.FrameLayout;
import java.io.IOException;
import java.util.concurrent.CountDownLatch;
import com.commonsware.cwac.camera.CameraHost;
import com.commonsware.cwac.camera.CameraHost.FailureReason;
import com.commonsware.cwac.camera.PreviewStrategy;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.jobqueue.Job;
import org.whispersystems.jobqueue.JobParameters;
@SuppressWarnings("deprecation")
public class CameraView extends FrameLayout {
private static final String TAG = CameraView.class.getSimpleName();
private PreviewStrategy previewStrategy = null;
private Camera.Size previewSize = null;
private volatile Camera camera = null;
private boolean inPreview = false;
private boolean cameraReady = false;
private CameraHost host = null;
private OnOrientationChange onOrientationChange = null;
private int displayOrientation = -1;
private int outputOrientation = -1;
private int cameraId = -1;
private int lastPictureOrientation = -1;
public CameraView(Context context) {
this(context, null);
}
public CameraView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public CameraView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
onOrientationChange = new OnOrientationChange(context.getApplicationContext());
}
public CameraHost getHost() {
return host;
}
public void setHost(CameraHost host) {
this.host = host;
if (host.getDeviceProfile().useTextureView()) {
previewStrategy = new TexturePreviewStrategy(this);
} else {
previewStrategy = new SurfacePreviewStrategy(this);
}
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void onResume() {
addView(previewStrategy.getWidget());
final CameraHost host = getHost();
submitTask(new SerializedAsyncTask<FailureReason>() {
@Override protected FailureReason onRunBackground() {
try {
cameraId = host.getCameraId();
if (cameraId >= 0) {
camera = Camera.open(cameraId);
} else {
return FailureReason.NO_CAMERAS_REPORTED;
}
} catch (Exception e) {
return FailureReason.UNKNOWN;
}
return null;
}
@Override protected void onPostMain(FailureReason result) {
cameraReady = true;
if (result != null) {
host.onCameraFail(result);
return;
}
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
onOrientationChange.enable();
}
setCameraDisplayOrientation();
synchronized (CameraView.this) {
CameraView.this.notifyAll();
}
requestLayout();
invalidate();
}
});
}
public void onPause() {
removeView(previewStrategy.getWidget());
submitTask(new SerializedAsyncTask<Void>() {
@Override protected void onPreMain() {
cameraReady = false;
}
@Override protected Void onRunBackground() {
if (camera != null) {
previewDestroyed();
}
return null;
}
@Override protected void onPostMain(Void avoid) {
onOrientationChange.disable();
previewSize = null;
displayOrientation = -1;
outputOrientation = -1;
cameraId = -1;
lastPictureOrientation = -1;
}
});
}
// based on CameraPreview.java from ApiDemos
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
if (getMeasuredWidth() > 0 && getMeasuredHeight() > 0 && camera != null && cameraReady) {
Camera.Size newSize = null;
try {
if (getHost().getRecordingHint() != CameraHost.RecordingHint.STILL_ONLY) {
newSize = getHost().getPreferredPreviewSizeForVideo(getDisplayOrientation(),
getMeasuredWidth(),
getMeasuredHeight(),
camera.getParameters(),
null);
}
if (newSize == null || newSize.width * newSize.height < 65536) {
newSize = getHost().getPreviewSize(getDisplayOrientation(),
getMeasuredWidth(),
getMeasuredHeight(),
camera.getParameters());
}
} catch (Exception e) {
Log.e(TAG, "Could not work with camera parameters?", e);
// TODO get this out to library clients
}
if (newSize != null) {
if (previewSize == null) {
previewSize = newSize;
synchronized (this) { notifyAll(); }
} else if (previewSize.width != newSize.width || previewSize.height != newSize.height) {
if (inPreview) {
stopPreview();
}
previewSize = newSize;
synchronized (this) { notifyAll(); }
initPreview();
}
}
}
}
// based on CameraPreview.java from ApiDemos
@SuppressWarnings("SuspiciousNameCombination") @Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
if (changed && getChildCount() > 0) {
final View child = getChildAt(0);
final int width = r - l;
final int height = b - t;
final int previewWidth;
final int previewHeight;
// handle orientation
if (previewSize != null && (getDisplayOrientation() == 90 || getDisplayOrientation() == 270)) {
previewWidth = previewSize.height;
previewHeight = previewSize.width;
} else if (previewSize != null) {
previewWidth = previewSize.width;
previewHeight = previewSize.height;
} else {
previewWidth = width;
previewHeight = height;
}
boolean useFirstStrategy = (width * previewHeight > height * previewWidth);
boolean useFullBleed = getHost().useFullBleedPreview();
if ((useFirstStrategy && !useFullBleed) || (!useFirstStrategy && useFullBleed)) {
final int scaledChildWidth = previewWidth * height / previewHeight;
child.layout((width - scaledChildWidth) / 2, 0, (width + scaledChildWidth) / 2, height);
} else {
final int scaledChildHeight = previewHeight * width / previewWidth;
child.layout(0, (height - scaledChildHeight) / 2, width, (height + scaledChildHeight) / 2);
}
}
}
public int getDisplayOrientation() {
return displayOrientation;
}
public void setOneShotPreviewCallback(PreviewCallback callback) {
if (camera != null) camera.setOneShotPreviewCallback(callback);
}
public Camera.Parameters getCameraParameters() {
return camera.getParameters();
}
void previewCreated() {
final CameraHost host = getHost();
submitTask(new PostInitializationTask<Void>() {
@Override protected void onPostMain(Void avoid) {
try {
if (camera != null) {
previewStrategy.attach(camera);
}
} catch (IOException e) {
host.handleException(e);
}
}
});
}
void previewDestroyed() {
if (camera != null) {
previewStopped();
camera.release();
camera = null;
}
}
void previewReset() {
previewStopped();
initPreview();
}
private void previewStopped() {
if (inPreview) {
stopPreview();
}
}
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
public void initPreview() {
submitTask(new PostInitializationTask<Void>() {
@Override protected void onPostMain(Void avoid) {
if (camera != null && cameraReady) {
Camera.Parameters parameters = camera.getParameters();
parameters.setPreviewSize(previewSize.width, previewSize.height);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
parameters.setRecordingHint(getHost().getRecordingHint() != CameraHost.RecordingHint.STILL_ONLY);
}
camera.setParameters(getHost().adjustPreviewParameters(parameters));
startPreview();
requestLayout();
invalidate();
}
}
});
}
private void startPreview() {
Log.w(TAG, "startPreview()");
camera.startPreview();
inPreview = true;
getHost().autoFocusAvailable();
}
private void stopPreview() {
inPreview = false;
getHost().autoFocusUnavailable();
camera.stopPreview();
}
// based on
// http://developer.android.com/reference/android/hardware/Camera.html#setDisplayOrientation(int)
// and http://stackoverflow.com/a/10383164/115145
private void setCameraDisplayOrientation() {
Camera.CameraInfo info = new Camera.CameraInfo();
int rotation = getActivity().getWindowManager().getDefaultDisplay().getRotation();
int degrees = 0;
DisplayMetrics dm = new DisplayMetrics();
Camera.getCameraInfo(cameraId, info);
getActivity().getWindowManager().getDefaultDisplay().getMetrics(dm);
switch (rotation) {
case Surface.ROTATION_0: degrees = 0; break;
case Surface.ROTATION_90: degrees = 90; break;
case Surface.ROTATION_180: degrees = 180; break;
case Surface.ROTATION_270: degrees = 270; break;
}
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
displayOrientation = (info.orientation + degrees ) % 360;
displayOrientation = (360 - displayOrientation) % 360;
}
else {
displayOrientation = (info.orientation - degrees + 360) % 360;
}
boolean wasInPreview = inPreview;
if (inPreview) {
stopPreview();
}
camera.setDisplayOrientation(displayOrientation);
if (wasInPreview) {
startPreview();
}
}
public int getCameraPictureOrientation() {
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
if (getActivity().getRequestedOrientation() != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) {
outputOrientation = getCameraPictureRotation(getActivity().getWindowManager()
.getDefaultDisplay()
.getOrientation());
} else if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
outputOrientation = (360 - displayOrientation) % 360;
} else {
outputOrientation = displayOrientation;
}
if (lastPictureOrientation != outputOrientation) {
lastPictureOrientation = outputOrientation;
}
return outputOrientation;
}
// based on:
// http://developer.android.com/reference/android/hardware/Camera.Parameters.html#setRotation(int)
public int getCameraPictureRotation(int orientation) {
Camera.CameraInfo info = new Camera.CameraInfo();
Camera.getCameraInfo(cameraId, info);
int rotation;
orientation = (orientation + 45) / 90 * 90;
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
rotation = (info.orientation - orientation + 360) % 360;
}
else { // back-facing camera
rotation = (info.orientation + orientation) % 360;
}
return rotation;
}
Activity getActivity() {
return (Activity)getContext();
}
private class OnOrientationChange extends OrientationEventListener {
public OnOrientationChange(Context context) {
super(context);
disable();
}
@Override
public void onOrientationChanged(int orientation) {
if (camera != null && orientation != ORIENTATION_UNKNOWN) {
int newOutputOrientation = getCameraPictureRotation(orientation);
if (newOutputOrientation != outputOrientation) {
outputOrientation = newOutputOrientation;
Camera.Parameters params = camera.getParameters();
params.setRotation(outputOrientation);
try {
camera.setParameters(params);
lastPictureOrientation = outputOrientation;
}
catch (Exception e) {
Log.e(TAG, "Exception updating camera parameters in orientation change", e);
}
}
}
}
}
private void submitTask(SerializedAsyncTask job) {
ApplicationContext.getInstance(getContext()).getJobManager().add(job);
}
private abstract class SerializedAsyncTask<Result> extends Job {
public SerializedAsyncTask() {
super(JobParameters.newBuilder().withGroupId(CameraView.class.getSimpleName()).create());
}
@Override public void onAdded() {}
@Override public final void onRun() {
onWait();
runOnMainSync(new Runnable() {
@Override public void run() {
onPreMain();
}
});
final Result result = onRunBackground();
runOnMainSync(new Runnable() {
@Override public void run() {
onPostMain(result);
}
});
}
@Override public boolean onShouldRetry(Exception e) {
return false;
}
@Override public void onCanceled() { }
private void runOnMainSync(final Runnable runnable) {
final CountDownLatch sync = new CountDownLatch(1);
Util.runOnMain(new Runnable() {
@Override public void run() {
try {
runnable.run();
} finally {
sync.countDown();
}
}
});
try {
sync.await();
} catch (InterruptedException ie) {
throw new AssertionError(ie);
}
}
protected void onWait() {}
protected void onPreMain() {}
protected Result onRunBackground() { return null; }
protected void onPostMain(Result result) {}
}
private abstract class PostInitializationTask<Result> extends SerializedAsyncTask<Result> {
@Override protected void onWait() {
synchronized (CameraView.this) {
while (camera == null || previewSize == null) {
Util.wait(CameraView.this, 0);
}
}
}
}
}

View File

@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.components.camera;
import android.content.Context;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.Animation.AnimationListener;
import android.view.animation.AnimationUtils;
import android.widget.ImageButton;
import org.thoughtcrime.securesms.R;
public class HidingImageButton extends ImageButton {
public HidingImageButton(Context context) {
super(context);
}
public HidingImageButton(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HidingImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void hide() {
final Animation animation = AnimationUtils.loadAnimation(getContext(), R.anim.slide_to_right);
animation.setAnimationListener(new AnimationListener() {
@Override public void onAnimationStart(Animation animation) {}
@Override public void onAnimationRepeat(Animation animation) {}
@Override public void onAnimationEnd(Animation animation) {
setVisibility(GONE);
}
});
animateWith(animation);
}
public void show() {
setVisibility(VISIBLE);
animateWith(AnimationUtils.loadAnimation(getContext(), R.anim.slide_from_right));
}
private void animateWith(Animation animation) {
animation.setDuration(150);
animation.setInterpolator(new FastOutSlowInInterpolator());
startAnimation(animation);
}
}

View File

@ -0,0 +1,540 @@
package org.thoughtcrime.securesms.components.camera;
import android.content.Context;
import android.content.pm.PackageManager;
import android.graphics.Canvas;
import android.graphics.Rect;
import android.hardware.Camera;
import android.os.Build;
import android.os.Build.VERSION;
import android.support.annotation.NonNull;
import android.support.v4.view.MotionEventCompat;
import android.support.v4.view.ViewCompat;
import android.support.v4.widget.ViewDragHelper;
import android.util.AttributeSet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewTreeObserver.OnGlobalLayoutListener;
import android.view.WindowManager;
import android.widget.ImageButton;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.camera.QuickCamera.QuickCameraListener;
import org.thoughtcrime.securesms.util.SoftKeyboardUtil;
public class QuickAttachmentDrawer extends ViewGroup {
private static final String TAG = QuickAttachmentDrawer.class.getSimpleName();
private static final float FULL_EXPANDED_ANCHOR_POINT = 1.f;
private static final float COLLAPSED_ANCHOR_POINT = 0.f;
private final ViewDragHelper dragHelper;
private QuickCamera quickCamera;
private int coverViewPosition;
private View coverView;
private View controls;
private ImageButton fullScreenButton;
private ImageButton swapCameraButton;
private ImageButton shutterButton;
private float slideOffset;
private float initialMotionX;
private float initialMotionY;
private int rotation;
private int slideRange;
private int baseHalfHeight;
private AttachmentDrawerListener listener;
private DrawerState drawerState = DrawerState.COLLAPSED;
private float halfExpandedAnchorPoint = COLLAPSED_ANCHOR_POINT;
private boolean halfModeUnsupported = VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH;
private Rect drawChildrenRect = new Rect();
public QuickAttachmentDrawer(Context context) {
this(context, null);
}
public QuickAttachmentDrawer(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public QuickAttachmentDrawer(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
baseHalfHeight = SoftKeyboardUtil.getKeyboardHeight(getContext());
dragHelper = ViewDragHelper.create(this, 1.f, new ViewDragHelperCallback());
initializeView();
updateHalfExpandedAnchorPoint();
onConfigurationChanged();
}
private void initializeView() {
inflate(getContext(), R.layout.quick_attachment_drawer, this);
quickCamera = (QuickCamera) findViewById(R.id.quick_camera);
updateControlsView();
coverViewPosition = getChildCount();
}
private WindowManager getWindowManager() {
return (WindowManager) getContext().getSystemService(Context.WINDOW_SERVICE);
}
public static boolean isDeviceSupported(Context context) {
return context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA) &&
Camera.getNumberOfCameras() > 0;
}
public boolean isOpen() {
return getDrawerState().isVisible();
}
public void close() {
setDrawerStateAndAnimate(DrawerState.COLLAPSED);
}
public void open() {
setDrawerStateAndAnimate(DrawerState.HALF_EXPANDED);
}
public void onConfigurationChanged() {
int rotation = getWindowManager().getDefaultDisplay().getRotation();
Log.w(TAG, String.format("onNewOrientation(old %d, new %d)", this.rotation, rotation));
final boolean rotationChanged = this.rotation != rotation;
this.rotation = rotation;
if (rotationChanged) {
if (isOpen()) {
quickCamera.onPause();
setDrawerStateAndAnimate(getDrawerState());
}
updateControlsView();
}
}
private void updateControlsView() {
int controlsIndex = indexOfChild(controls);
removeView(controls);
controls = LayoutInflater.from(getContext()).inflate(isLandscape() ? R.layout.quick_camera_controls_land
: R.layout.quick_camera_controls,
this, false);
shutterButton = (ImageButton) controls.findViewById(R.id.shutter_button);
swapCameraButton = (ImageButton) controls.findViewById(R.id.swap_camera_button);
fullScreenButton = (ImageButton) controls.findViewById(R.id.fullscreen_button);
if (quickCamera.isMultipleCameras()) {
swapCameraButton.setVisibility(View.VISIBLE);
swapCameraButton.setOnClickListener(new CameraFlipClickListener());
}
shutterButton.setOnClickListener(new ShutterClickListener());
fullScreenButton.setOnClickListener(new FullscreenClickListener());
addView(controls, controlsIndex);
}
private boolean isLandscape() {
return rotation == Surface.ROTATION_90 || rotation == Surface.ROTATION_270;
}
private boolean isFullscreenOnly() {
return isLandscape() || halfModeUnsupported;
}
private void updateHalfExpandedAnchorPoint() {
getViewTreeObserver().addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
@SuppressWarnings("deprecation") @Override public void onGlobalLayout() {
if(android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) {
getViewTreeObserver().removeOnGlobalLayoutListener(this);
} else {
getViewTreeObserver().removeGlobalOnLayoutListener(this);
}
coverView = getChildAt(coverViewPosition);
slideRange = getMeasuredHeight();
halfExpandedAnchorPoint = computeSlideOffsetFromCoverBottom(slideRange - baseHalfHeight);
}
});
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int paddingLeft = getPaddingLeft();
final int paddingTop = getPaddingTop();
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
final int childHeight = child.getMeasuredHeight();
int childTop = paddingTop;
int childLeft = paddingLeft;
int childBottom;
if (child == quickCamera) {
childTop = computeCameraTopPosition(slideOffset);
childBottom = childTop + childHeight;
if (quickCamera.getMeasuredWidth() < getMeasuredWidth())
childLeft = (getMeasuredWidth() - quickCamera.getMeasuredWidth()) / 2 + paddingLeft;
} else if (child == controls) {
childBottom = getMeasuredHeight();
} else {
childBottom = computeCoverBottomPosition(slideOffset);
childTop = childBottom - childHeight;
}
final int childRight = childLeft + child.getMeasuredWidth();
child.layout(childLeft, childTop, childRight, childBottom);
}
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
if (widthMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Width must have an exact value or MATCH_PARENT");
} else if (heightMode != MeasureSpec.EXACTLY) {
throw new IllegalStateException("Height must have an exact value or MATCH_PARENT");
}
int layoutHeight = heightSize - getPaddingTop() - getPaddingBottom();
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
final LayoutParams lp = child.getLayoutParams();
if (child.getVisibility() == GONE && i == 0) {
continue;
}
int childWidthSpec;
switch (lp.width) {
case LayoutParams.WRAP_CONTENT:
childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.AT_MOST);
break;
case LayoutParams.MATCH_PARENT:
childWidthSpec = MeasureSpec.makeMeasureSpec(widthSize, MeasureSpec.EXACTLY);
break;
default:
childWidthSpec = MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY);
break;
}
int childHeightSpec;
switch (lp.height) {
case LayoutParams.WRAP_CONTENT:
childHeightSpec = MeasureSpec.makeMeasureSpec(layoutHeight, MeasureSpec.AT_MOST);
break;
case LayoutParams.MATCH_PARENT:
childHeightSpec = MeasureSpec.makeMeasureSpec(layoutHeight, MeasureSpec.EXACTLY);
break;
default:
childHeightSpec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
break;
}
child.measure(childWidthSpec, childHeightSpec);
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (h != oldh) updateHalfExpandedAnchorPoint();
}
@Override
protected boolean drawChild(@NonNull Canvas canvas, @NonNull View child, long drawingTime) {
boolean result;
final int save = canvas.save(Canvas.CLIP_SAVE_FLAG);
canvas.getClipBounds(drawChildrenRect);
if (child == coverView)
drawChildrenRect.bottom = Math.min(drawChildrenRect.bottom, child.getBottom());
else if (coverView != null)
drawChildrenRect.top = Math.max(drawChildrenRect.top, coverView.getBottom());
canvas.clipRect(drawChildrenRect);
result = super.drawChild(canvas, child, drawingTime);
canvas.restoreToCount(save);
return result;
}
@Override
public void computeScroll() {
if (dragHelper != null && dragHelper.continueSettling(true)) {
ViewCompat.postInvalidateOnAnimation(this);
} else if (!getDrawerState().isVisible()) {
quickCamera.onPause();
}
}
private void setDrawerState(DrawerState drawerState) {
switch (drawerState) {
case COLLAPSED:
slideOffset = COLLAPSED_ANCHOR_POINT;
fullScreenButton.setImageResource(R.drawable.quick_camera_fullscreen);
if (listener != null) listener.onAttachmentDrawerClosed();
break;
case HALF_EXPANDED:
if (isFullscreenOnly()) {
setDrawerState(DrawerState.FULL_EXPANDED);
return;
}
Log.w(TAG, "HALF_EXPANDED init");
quickCamera.onResume();
slideOffset = halfExpandedAnchorPoint;
fullScreenButton.setImageResource(R.drawable.quick_camera_fullscreen);
if (listener != null) listener.onAttachmentDrawerOpened();
break;
case FULL_EXPANDED:
quickCamera.onResume();
slideOffset = FULL_EXPANDED_ANCHOR_POINT;
fullScreenButton.setImageResource(isFullscreenOnly() ? R.drawable.quick_camera_hide
: R.drawable.quick_camera_exit_fullscreen);
if (listener != null) listener.onAttachmentDrawerOpened();
break;
}
this.drawerState = drawerState;
}
public DrawerState getDrawerState() {
return drawerState;
}
public void setDrawerStateAndAnimate(final DrawerState requestedDrawerState) {
DrawerState oldDrawerState = this.drawerState;
setDrawerState(requestedDrawerState);
if (oldDrawerState != drawerState) {
slideTo(slideOffset);
}
}
public void setListener(AttachmentDrawerListener listener) {
this.listener = listener;
if (quickCamera != null) quickCamera.setQuickCameraListener(listener);
}
public interface AttachmentDrawerListener extends QuickCameraListener {
void onAttachmentDrawerClosed();
void onAttachmentDrawerOpened();
}
private class ViewDragHelperCallback extends ViewDragHelper.Callback {
@Override
public boolean tryCaptureView(View child, int pointerId) {
return child == controls && !halfModeUnsupported;
}
@Override
public void onViewDragStateChanged(int state) {
if (dragHelper.getViewDragState() == ViewDragHelper.STATE_IDLE) {
setDrawerState(drawerState);
requestLayout();
}
}
@Override
public void onViewCaptured(View capturedChild, int activePointerId) {
}
@Override
public void onViewPositionChanged(View changedView, int left, int top, int dx, int dy) {
final int expandedTop = computeCoverBottomPosition(FULL_EXPANDED_ANCHOR_POINT) - coverView.getHeight();
final int collapsedTop = computeCoverBottomPosition(COLLAPSED_ANCHOR_POINT) - coverView.getHeight();
final int newTop = Math.min(Math.max(coverView.getTop() + dy, expandedTop), collapsedTop);
slideOffset = computeSlideOffsetFromCoverBottom(newTop + coverView.getHeight());
requestLayout();
}
@Override
public void onViewReleased(View releasedChild, float xvel, float yvel) {
if (releasedChild == controls) {
float direction = -yvel;
DrawerState drawerState = DrawerState.COLLAPSED;
if (direction > 1) {
drawerState = DrawerState.FULL_EXPANDED;
} else if (direction < -1) {
boolean halfExpand = (slideOffset > halfExpandedAnchorPoint && !isLandscape());
drawerState = halfExpand ? DrawerState.HALF_EXPANDED : DrawerState.COLLAPSED;
} else if (!isLandscape()) {
if (halfExpandedAnchorPoint != 1 && slideOffset >= (1.f + halfExpandedAnchorPoint) / 2) {
drawerState = DrawerState.FULL_EXPANDED;
} else if (halfExpandedAnchorPoint == 1 && slideOffset >= 0.5f) {
drawerState = DrawerState.FULL_EXPANDED;
} else if (halfExpandedAnchorPoint != 1 && slideOffset >= halfExpandedAnchorPoint) {
drawerState = DrawerState.HALF_EXPANDED;
} else if (halfExpandedAnchorPoint != 1 && slideOffset >= halfExpandedAnchorPoint / 2) {
drawerState = DrawerState.HALF_EXPANDED;
}
}
setDrawerState(drawerState);
dragHelper.captureChildView(coverView, 0);
Log.w(TAG, String.format("setting cover at %d", computeCoverBottomPosition(slideOffset) - coverView.getHeight()));
dragHelper.settleCapturedViewAt(coverView.getLeft(), computeCoverBottomPosition(slideOffset) - coverView.getHeight());
dragHelper.captureChildView(quickCamera, 0);
dragHelper.settleCapturedViewAt(quickCamera.getLeft(), computeCameraTopPosition(slideOffset));
ViewCompat.postInvalidateOnAnimation(QuickAttachmentDrawer.this);
}
}
@Override
public int getViewVerticalDragRange(View child) {
return slideRange;
}
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
return top;
}
}
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
if (dragHelper != null) {
final int action = MotionEventCompat.getActionMasked(event);
if (action == MotionEvent.ACTION_CANCEL || action == MotionEvent.ACTION_UP) {
dragHelper.cancel();
return false;
}
final float x = event.getX();
final float y = event.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
initialMotionX = x;
initialMotionY = y;
break;
case MotionEvent.ACTION_MOVE:
final float adx = Math.abs(x - initialMotionX);
final float ady = Math.abs(y - initialMotionY);
final int dragSlop = dragHelper.getTouchSlop();
if (adx > dragSlop && ady < dragSlop) {
return super.onInterceptTouchEvent(event);
}
if ((ady > dragSlop && adx > ady) || !isDragViewUnder((int) initialMotionX, (int) initialMotionY)) {
dragHelper.cancel();
return false;
}
break;
}
return dragHelper.shouldInterceptTouchEvent(event);
}
return super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(@NonNull MotionEvent event) {
if (dragHelper != null) {
dragHelper.processTouchEvent(event);
return true;
}
return super.onTouchEvent(event);
}
// NOTE: Android Studio bug misreports error, squashing the warning.
// https://code.google.com/p/android/issues/detail?id=175977
@SuppressWarnings("ResourceType")
private boolean isDragViewUnder(int x, int y) {
int[] viewLocation = new int[2];
quickCamera.getLocationOnScreen(viewLocation);
int[] parentLocation = new int[2];
this.getLocationOnScreen(parentLocation);
int screenX = parentLocation[0] + x;
int screenY = parentLocation[1] + y;
return screenX >= viewLocation[0] && screenX < viewLocation[0] + quickCamera.getWidth() &&
screenY >= viewLocation[1] && screenY < viewLocation[1] + quickCamera.getHeight();
}
private int computeCameraTopPosition(float slideOffset) {
float clampedOffset = slideOffset - halfExpandedAnchorPoint;
if (clampedOffset < COLLAPSED_ANCHOR_POINT) {
clampedOffset = COLLAPSED_ANCHOR_POINT;
} else {
clampedOffset = clampedOffset / (FULL_EXPANDED_ANCHOR_POINT - halfExpandedAnchorPoint);
}
float slidePixelOffset = slideOffset * slideRange +
(quickCamera.getMeasuredHeight() - baseHalfHeight) / 2 *
(FULL_EXPANDED_ANCHOR_POINT - clampedOffset);
float marginPixelOffset = (getMeasuredHeight() - quickCamera.getMeasuredHeight()) / 2 * clampedOffset;
return (int) (getMeasuredHeight() - slidePixelOffset + marginPixelOffset);
}
private int computeCoverBottomPosition(float slideOffset) {
int slidePixelOffset = (int) (slideOffset * slideRange);
return getMeasuredHeight() - getPaddingBottom() - slidePixelOffset;
}
private void slideTo(float slideOffset) {
if (dragHelper != null && !halfModeUnsupported) {
dragHelper.smoothSlideViewTo(coverView, coverView.getLeft(), computeCoverBottomPosition(slideOffset) - coverView.getHeight());
dragHelper.smoothSlideViewTo(quickCamera, quickCamera.getLeft(), computeCameraTopPosition(slideOffset));
ViewCompat.postInvalidateOnAnimation(this);
} else {
invalidate();
}
}
private float computeSlideOffsetFromCoverBottom(int topPosition) {
final int topBoundCollapsed = computeCoverBottomPosition(0);
return (float) (topBoundCollapsed - topPosition) / slideRange;
}
public void onPause() {
quickCamera.onPause();
}
public void onResume() {
if (drawerState.isVisible()) quickCamera.onResume();
}
public enum DrawerState {
COLLAPSED, HALF_EXPANDED, FULL_EXPANDED;
public boolean isVisible() {
return this == HALF_EXPANDED || this == FULL_EXPANDED;
}
}
private class ShutterClickListener implements OnClickListener {
@Override
public void onClick(View v) {
boolean crop = drawerState != DrawerState.FULL_EXPANDED;
int imageHeight = crop ? baseHalfHeight : quickCamera.getMeasuredHeight();
Rect previewRect = new Rect(0, 0, quickCamera.getMeasuredWidth(), imageHeight);
quickCamera.takePicture(previewRect);
}
}
private class CameraFlipClickListener implements OnClickListener {
@Override
public void onClick(View v) {
quickCamera.swapCamera();
swapCameraButton.setImageResource(quickCamera.isRearCamera() ? R.drawable.quick_camera_front
: R.drawable.quick_camera_rear);
}
}
private class FullscreenClickListener implements OnClickListener {
@Override
public void onClick(View v) {
if (drawerState != DrawerState.FULL_EXPANDED) {
setDrawerStateAndAnimate(DrawerState.FULL_EXPANDED);
} else if (isFullscreenOnly()) {
setDrawerStateAndAnimate(DrawerState.COLLAPSED);
} else {
setDrawerStateAndAnimate(DrawerState.HALF_EXPANDED);
}
}
}
}

View File

@ -0,0 +1,187 @@
package org.thoughtcrime.securesms.components.camera;
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.hardware.Camera;
import android.hardware.Camera.CameraInfo;
import android.hardware.Camera.Parameters;
import android.hardware.Camera.Size;
import android.os.AsyncTask;
import android.os.Build.VERSION_CODES;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.util.Log;
import android.widget.Toast;
import com.commonsware.cwac.camera.SimpleCameraHost;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.BitmapUtil;
import java.io.IOException;
import java.util.List;
@SuppressWarnings("deprecation") public class QuickCamera extends CameraView {
private static final String TAG = QuickCamera.class.getSimpleName();
private QuickCameraListener listener;
private boolean capturing;
private boolean started;
private QuickCameraHost cameraHost;
public QuickCamera(Context context) {
this(context, null);
}
public QuickCamera(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public QuickCamera(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
cameraHost = new QuickCameraHost(context);
setHost(cameraHost);
}
@Override
public void onResume() {
if (started) return;
super.onResume();
started = true;
}
@Override
public void onPause() {
if (!started) return;
super.onPause();
started = false;
}
public boolean isStarted() {
return started;
}
public void takePicture(final Rect previewRect) {
if (capturing) {
Log.w(TAG, "takePicture() called while previous capture pending.");
return;
}
final Parameters cameraParameters = getCameraParameters();
setOneShotPreviewCallback(new Camera.PreviewCallback() {
@Override
public void onPreviewFrame(byte[] data, final Camera camera) {
final int rotation = getCameraPictureOrientation();
new AsyncTask<byte[], Void, Bitmap>() {
@Override
protected Bitmap doInBackground(byte[]... params) {
byte[] data = params[0];
try {
Size previewSize = cameraParameters.getPreviewSize();
return BitmapUtil.createFromNV21(data,
previewSize.width,
previewSize.height,
rotation,
getCroppedRect(previewSize, previewRect, rotation));
} catch (IOException e) {
return null;
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
capturing = false;
if (bitmap != null && listener != null) listener.onImageCapture(bitmap);
}
}.execute(data);
}
});
}
private Rect getCroppedRect(Size cameraPreviewSize, Rect visibleRect, int rotation) {
final int previewWidth = cameraPreviewSize.width;
final int previewHeight = cameraPreviewSize.height;
if (rotation % 180 > 0) {
//noinspection SuspiciousNameCombination
visibleRect.set(visibleRect.top, visibleRect.left, visibleRect.bottom, visibleRect.right);
}
float scale = (float) previewWidth / visibleRect.width();
if (visibleRect.height() * scale > previewHeight) {
scale = (float) previewHeight / visibleRect.height();
}
final float newWidth = visibleRect.width() * scale;
final float newHeight = visibleRect.height() * scale;
final float centerX = previewWidth / 2;
final float centerY = previewHeight / 2;
visibleRect.set((int) (centerX - newWidth / 2),
(int) (centerY - newHeight / 2),
(int) (centerX + newWidth / 2),
(int) (centerY + newHeight / 2));
return visibleRect;
}
public void setQuickCameraListener(QuickCameraListener listener) {
this.listener = listener;
}
public boolean isMultipleCameras() {
return Camera.getNumberOfCameras() > 1;
}
public boolean isRearCamera() {
return cameraHost.getCameraId() == Camera.CameraInfo.CAMERA_FACING_BACK;
}
public void swapCamera() {
cameraHost.swapCameraId();
onPause();
onResume();
}
public interface QuickCameraListener {
void onImageCapture(@NonNull final Bitmap bitmap);
}
private class QuickCameraHost extends SimpleCameraHost {
int cameraId = CameraInfo.CAMERA_FACING_BACK;
public QuickCameraHost(Context context) {
super(context);
}
@TargetApi(VERSION_CODES.ICE_CREAM_SANDWICH) @Override
public Parameters adjustPreviewParameters(Parameters parameters) {
List<String> focusModes = parameters.getSupportedFocusModes();
if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
} else if (focusModes.contains(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
parameters.setFocusMode(Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
}
return parameters;
}
@Override
public int getCameraId() {
return cameraId;
}
public void swapCameraId() {
if (isMultipleCameras()) {
if (cameraId == CameraInfo.CAMERA_FACING_BACK) cameraId = CameraInfo.CAMERA_FACING_FRONT;
else cameraId = CameraInfo.CAMERA_FACING_BACK;
}
}
@Override
public void onCameraFail(FailureReason reason) {
super.onCameraFail(reason);
Toast.makeText(getContext(), R.string.quick_camera_unavailable, Toast.LENGTH_SHORT).show();
}
}
}

View File

@ -12,10 +12,11 @@
limitations under the License.
*/
package org.thoughtcrime.securesms.components;
package org.thoughtcrime.securesms.components.camera;
import android.hardware.Camera;
import android.media.MediaRecorder;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
@ -26,6 +27,7 @@ import java.io.IOException;
class SurfacePreviewStrategy implements PreviewStrategy,
SurfaceHolder.Callback {
private final static String TAG = SurfacePreviewStrategy.class.getSimpleName();
private final CameraView cameraView;
private SurfaceView preview=null;
private SurfaceHolder previewHolder=null;
@ -41,22 +43,26 @@ class SurfacePreviewStrategy implements PreviewStrategy,
@Override
public void surfaceCreated(SurfaceHolder holder) {
Log.w(TAG, "surfaceCreated()");
cameraView.previewCreated();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format,
int width, int height) {
cameraView.initPreview(width, height);
Log.w(TAG, "surfaceChanged()");
cameraView.initPreview();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
Log.w(TAG, "surfaceDestroyed()");
cameraView.previewDestroyed();
}
@Override
public void attach(Camera camera) throws IOException {
Log.w(TAG, "attach(Camera)");
camera.setPreviewDisplay(previewHolder);
}

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.components;
package org.thoughtcrime.securesms.components.camera;
/***
Copyright (c) 2013 CommonsWare, LLC
@ -18,6 +18,7 @@ import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.Log;
import android.view.TextureView;
import android.view.View;
@ -28,6 +29,7 @@ import java.io.IOException;
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
class TexturePreviewStrategy implements PreviewStrategy,
TextureView.SurfaceTextureListener {
private final static String TAG = TexturePreviewStrategy.class.getSimpleName();
private final CameraView cameraView;
private TextureView widget=null;
private SurfaceTexture surface=null;
@ -41,20 +43,23 @@ class TexturePreviewStrategy implements PreviewStrategy,
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface,
int width, int height) {
Log.w(TAG, "onSurfaceTextureAvailable()");
this.surface=surface;
cameraView.previewCreated();
cameraView.initPreview(width, height);
cameraView.initPreview();
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface,
int width, int height) {
cameraView.previewReset(width, height);
Log.w(TAG, "onSurfaceTextureChanged()");
cameraView.previewReset();
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
Log.w(TAG, "onSurfaceTextureDestroyed()");
cameraView.previewDestroyed();
return(true);
@ -67,6 +72,7 @@ class TexturePreviewStrategy implements PreviewStrategy,
@Override
public void attach(Camera camera) throws IOException {
Log.w(TAG, "attach(Camera)");
camera.setPreviewTexture(surface);
}

View File

@ -101,11 +101,10 @@ 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 ENCRYPTED_IMAGE = "encrypted_image";
public static final String TEXT = "text";
public static final String IMAGE = "image";
public static final String VIDEO = "video";
public static final String AUDIO = "audio";
private final String type;
private final String value;
@ -125,11 +124,10 @@ public class DraftDatabase extends Database {
public String getSnippet(Context context) {
switch (type) {
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);
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);
default: return null;
}
}

View File

@ -20,10 +20,10 @@ import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
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;
@ -38,9 +38,9 @@ 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.providers.CaptureProvider;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import java.io.File;
import java.io.IOException;
public class AttachmentManager {
@ -53,7 +53,7 @@ public class AttachmentManager {
private final SlideDeck slideDeck;
private final AttachmentListener attachmentListener;
private File captureFile;
private Uri captureUri;
public AttachmentManager(Activity view, AttachmentListener listener) {
this.attachmentView = view.findViewById(R.id.attachment_editor);
@ -92,12 +92,12 @@ public class AttachmentManager {
}
public void cleanup() {
if (captureFile != null) captureFile.delete();
captureFile = null;
if (captureUri != null) CaptureProvider.getInstance(context).delete(captureUri);
captureUri = null;
}
public void setImage(Uri image) throws IOException, BitmapDecodingException {
setMedia(new ImageSlide(context, image));
public void setImage(MasterSecret masterSecret, Uri image) throws IOException, BitmapDecodingException {
setMedia(new ImageSlide(context, masterSecret, image), masterSecret);
}
public void setVideo(Uri video) throws IOException, MediaTooLargeException {
@ -108,20 +108,11 @@ 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);
@ -137,20 +128,11 @@ public class AttachmentManager {
return slideDeck;
}
public File getCaptureFile() {
return captureFile;
}
public void capturePhoto(Activity activity, int requestCode) {
public void setCaptureImage(MasterSecret masterSecret, Bitmap bitmap) {
try {
Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (captureIntent.resolveActivity(activity.getPackageManager()) != null) {
captureFile = File.createTempFile(String.valueOf(System.currentTimeMillis()), ".jpg", activity.getExternalFilesDir(null));
captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(captureFile));
activity.startActivityForResult(captureIntent, requestCode);
}
} catch (IOException e) {
captureUri = CaptureProvider.getInstance(context).create(masterSecret, bitmap);
setImage(masterSecret, captureUri);
} catch (IOException | BitmapDecodingException e) {
Log.w(TAG, e);
}
}
@ -204,6 +186,6 @@ public class AttachmentManager {
}
public interface AttachmentListener {
public void onAttachmentChanged();
void onAttachmentChanged();
}
}

View File

@ -37,7 +37,6 @@ public class AttachmentTypeSelectorAdapter extends ArrayAdapter<AttachmentTypeSe
public static final int ADD_VIDEO = 2;
public static final int ADD_SOUND = 3;
public static final int ADD_CONTACT_INFO = 4;
public static final int TAKE_PHOTO = 5;
private final Context context;
@ -72,11 +71,10 @@ public class AttachmentTypeSelectorAdapter extends ArrayAdapter<AttachmentTypeSe
private static List<IconListItem> getItemList(Context context) {
List<IconListItem> data = new ArrayList<>(4);
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_take_photo), ResUtil.getDrawableRes(context, R.attr.conversation_attach_photo), TAKE_PHOTO);
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_picture), ResUtil.getDrawableRes(context, R.attr.conversation_attach_image), ADD_IMAGE);
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_video), ResUtil.getDrawableRes(context, R.attr.conversation_attach_video), ADD_VIDEO);
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_audio), ResUtil.getDrawableRes(context, R.attr.conversation_attach_sound), ADD_SOUND);
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_contact), ResUtil.getDrawableRes(context, R.attr.conversation_attach_contact_info), ADD_CONTACT_INFO);
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_picture), ResUtil.getDrawableRes(context, R.attr.conversation_attach_image), ADD_IMAGE);
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_video), ResUtil.getDrawableRes(context, R.attr.conversation_attach_video), ADD_VIDEO);
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_audio), ResUtil.getDrawableRes(context, R.attr.conversation_attach_sound), ADD_SOUND);
addItem(data, context.getString(R.string.AttachmentTypeSelectorAdapter_contact), ResUtil.getDrawableRes(context, R.attr.conversation_attach_contact_info), ADD_CONTACT_INFO);
return data;
}

View File

@ -20,36 +20,25 @@ 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 {
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;
super(context, masterSecret, constructPartFromUri(uri));
}
@Override
@ -73,32 +62,12 @@ public class ImageSlide extends Slide {
return true;
}
@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)
private static PduPart constructPartFromUri(Uri uri)
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());

View File

@ -5,13 +5,12 @@ 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.CaptureProvider;
import org.thoughtcrime.securesms.providers.PartProvider;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@ -22,8 +21,9 @@ public class PartAuthority {
private static final Uri PART_CONTENT_URI = Uri.parse(PART_URI_STRING);
private static final Uri THUMB_CONTENT_URI = Uri.parse(THUMB_URI_STRING);
private static final int PART_ROW = 1;
private static final int THUMB_ROW = 2;
private static final int PART_ROW = 1;
private static final int THUMB_ROW = 2;
private static final int CAPTURE_ROW = 3;
private static final UriMatcher uriMatcher;
@ -31,28 +31,25 @@ public class PartAuthority {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI("org.thoughtcrime.securesms", "part/*/#", PART_ROW);
uriMatcher.addURI("org.thoughtcrime.securesms", "thumb/*/#", THUMB_ROW);
uriMatcher.addURI(CaptureProvider.AUTHORITY, CaptureProvider.EXPECTED_PATH, CAPTURE_ROW);
}
public static InputStream getPartStream(Context context, MasterSecret masterSecret, Uri uri)
throws IOException
{
PartDatabase partDatabase = DatabaseFactory.getPartDatabase(context);
int match = uriMatcher.match(uri);
int match = uriMatcher.match(uri);
try {
switch (match) {
case PART_ROW:
PartUriParser partUri = new PartUriParser(uri);
return partDatabase.getPartStream(masterSecret, partUri.getPartId());
return DatabaseFactory.getPartDatabase(context).getPartStream(masterSecret, partUri.getPartId());
case THUMB_ROW:
partUri = new PartUriParser(uri);
return partDatabase.getThumbnailStream(masterSecret, partUri.getPartId());
return DatabaseFactory.getPartDatabase(context).getThumbnailStream(masterSecret, partUri.getPartId());
case CAPTURE_ROW:
return CaptureProvider.getInstance(context).getStream(masterSecret, ContentUris.parseId(uri));
default:
String tempMediaDir = context.getDir("media", Context.MODE_PRIVATE).getPath();
if (uri.getPath().startsWith(tempMediaDir))
return new DecryptingPartInputStream(new File(uri.getPath()), masterSecret);
else
return context.getContentResolver().openInputStream(uri);
return context.getContentResolver().openInputStream(uri);
}
} catch (SecurityException se) {
throw new IOException(se);

View File

@ -66,10 +66,6 @@ public abstract class Slide {
return false;
}
public boolean isEncrypted() {
return false;
}
public PduPart getPart() {
return part;
}

View File

@ -0,0 +1,62 @@
package org.thoughtcrime.securesms.providers;
import android.content.ContentUris;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Bitmap.CompressFormat;
import android.net.Uri;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
public class CaptureProvider {
private static final String TAG = CaptureProvider.class.getSimpleName();
private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture";
public static final Uri CONTENT_URI = Uri.parse(URI_STRING);
public static final String AUTHORITY = "org.thoughtcrime.securesms";
public static final String EXPECTED_PATH = "capture/#";
private static volatile CaptureProvider instance;
public static CaptureProvider getInstance(Context context) {
if (instance == null) {
synchronized (CaptureProvider.class) {
if (instance == null) {
instance = new CaptureProvider(context);
}
}
}
return instance;
}
private final Context context;
private CaptureProvider(Context context) {
this.context = context.getApplicationContext();
}
public Uri create(MasterSecret masterSecret, Bitmap bitmap) throws IOException {
long id = System.currentTimeMillis();
OutputStream output = new EncryptingPartOutputStream(getFile(id), masterSecret);
bitmap.compress(CompressFormat.JPEG, 100, output);
output.close();
return ContentUris.withAppendedId(CONTENT_URI, id);
}
public boolean delete(Uri uri) {
return getFile(ContentUris.parseId(uri)).delete();
}
public InputStream getStream(MasterSecret masterSecret, long id) throws IOException {
return new DecryptingPartInputStream(getFile(id), masterSecret);
}
private File getFile(long id) {
return new File(context.getDir("captures", Context.MODE_PRIVATE), id + ".capture");
}
}

View File

@ -7,17 +7,20 @@ import android.graphics.Bitmap.Config;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ImageFormat;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.YuvImage;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.net.Uri;
import android.os.Handler;
import android.os.Looper;
import android.support.annotation.NonNull;
import android.util.Log;
import android.util.Pair;
@ -210,6 +213,7 @@ public class BitmapUtil {
}
public static Bitmap rotateBitmap(Bitmap bitmap, int angle) {
if (angle == 0) return bitmap;
Matrix matrix = new Matrix();
matrix.postRotate(angle);
Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);
@ -268,6 +272,24 @@ public class BitmapUtil {
return output;
}
public static Bitmap createFromNV21(@NonNull final byte[] data,
final int width,
final int height,
final int rotation,
final Rect croppingRect)
throws IOException
{
YuvImage previewImage = new YuvImage(data, ImageFormat.NV21,
width, height, null);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
previewImage.compressToJpeg(croppingRect, 100, outputStream);
byte[] bytes = outputStream.toByteArray();
outputStream.close();
outputStream = new ByteArrayOutputStream();
return BitmapUtil.rotateBitmap(BitmapFactory.decodeByteArray(bytes, 0, bytes.length), rotation);
}
public static Bitmap createFromDrawable(final Drawable drawable, final int width, final int height) {
final AtomicBoolean created = new AtomicBoolean(false);
final Bitmap[] result = new Bitmap[1];

View File

@ -0,0 +1,121 @@
package org.thoughtcrime.securesms.util;
import android.app.Activity;
import android.content.Context;
import android.graphics.Rect;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import org.thoughtcrime.securesms.R;
import java.lang.reflect.Field;
public class SoftKeyboardUtil {
private static final String TAG = SoftKeyboardUtil.class.getSimpleName();
private static final Rect rect = new Rect();
public static int onMeasure(ViewGroup view) {
view.getWindowVisibleDisplayFrame(rect);
final int res = view.getResources().getIdentifier("status_bar_height", "dimen", "android");
final int statusBarHeight = res > 0 ? view.getResources().getDimensionPixelSize(res) : 0;
final int availableHeight = view.getRootView().getHeight() - statusBarHeight - getViewInset(view);
final int keyboardHeight = availableHeight - (rect.bottom - rect.top);
final int minKeyboardHeight = view.getResources().getDimensionPixelSize(R.dimen.min_emoji_drawer_height);
if (keyboardHeight > minKeyboardHeight) {
onKeyboardShown(view.getContext(), keyboardHeight);
}
return Math.max(keyboardHeight, minKeyboardHeight);
}
private static int getViewInset(ViewGroup view) {
if (Build.VERSION.SDK_INT < VERSION_CODES.LOLLIPOP) {
return 0;
}
try {
Field attachInfoField = View.class.getDeclaredField("mAttachInfo");
attachInfoField.setAccessible(true);
Object attachInfo = attachInfoField.get(view);
if (attachInfo != null) {
Field stableInsetsField = attachInfo.getClass().getDeclaredField("mStableInsets");
stableInsetsField.setAccessible(true);
Rect insets = (Rect)stableInsetsField.get(attachInfo);
return insets.bottom;
}
} catch (NoSuchFieldException nsfe) {
Log.w(TAG, "field reflection error when measuring view inset", nsfe);
} catch (IllegalAccessException iae) {
Log.w(TAG, "access reflection error when measuring view inset", iae);
}
return 0;
}
private static void onKeyboardShown(Context context, int keyboardHeight) {
Log.w(TAG, "keyboard shown, height " + keyboardHeight);
WindowManager wm = (WindowManager)context.getSystemService(Activity.WINDOW_SERVICE);
if (wm == null || wm.getDefaultDisplay() == null) {
return;
}
int rotation = wm.getDefaultDisplay().getRotation();
switch (rotation) {
case Surface.ROTATION_270:
case Surface.ROTATION_90:
setKeyboardLandscapeHeight(context, keyboardHeight);
break;
case Surface.ROTATION_0:
case Surface.ROTATION_180:
setKeyboardPortraitHeight(context, keyboardHeight);
}
}
public static int getKeyboardHeight(Context context) {
WindowManager wm = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
if (wm == null || wm.getDefaultDisplay() == null) {
throw new AssertionError("WindowManager was null or there is no default display");
}
int rotation = wm.getDefaultDisplay().getRotation();
switch (rotation) {
case Surface.ROTATION_270:
case Surface.ROTATION_90:
return getKeyboardLandscapeHeight(context);
case Surface.ROTATION_0:
case Surface.ROTATION_180:
default:
return getKeyboardPortraitHeight(context);
}
}
private static int getKeyboardLandscapeHeight(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context)
.getInt("keyboard_height_landscape",
context.getResources().getDimensionPixelSize(R.dimen.min_emoji_drawer_height));
}
private static int getKeyboardPortraitHeight(Context context) {
return PreferenceManager.getDefaultSharedPreferences(context)
.getInt("keyboard_height_portrait",
context.getResources().getDimensionPixelSize(R.dimen.min_emoji_drawer_height));
}
private static void setKeyboardLandscapeHeight(Context context, int height) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putInt("keyboard_height_landscape", height).apply();
}
private static void setKeyboardPortraitHeight(Context context, int height) {
PreferenceManager.getDefaultSharedPreferences(context)
.edit().putInt("keyboard_height_portrait", height).apply();
}
}