Add camera preview to message composition

This commit is contained in:
Calvin Hu 2015-04-16 01:38:33 -04:00 committed by Moxie Marlinspike
parent 13eed3baa7
commit c4a37e38ab
55 changed files with 1698 additions and 19 deletions

View File

@ -33,6 +33,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.GET_ACCOUNTS" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE" />
<permission android:name="org.thoughtcrime.securesms.permission.C2D_MESSAGE"

View File

@ -29,6 +29,9 @@ repositories {
maven { // textdrawable
url 'https://dl.bintray.com/amulyakhare/maven'
}
maven {
url 'https://repo.commonsware.com.s3.amazonaws.com'
}
jcenter()
mavenLocal()
}
@ -69,6 +72,7 @@ dependencies {
exclude group: 'com.android.support', module: 'support-v4'
}
compile 'com.madgag.spongycastle:prov:1.51.0.0'
compile 'com.commonsware.cwac:camera:0.6.+'
provided 'com.squareup.dagger:dagger-compiler:1.2.2'
compile 'org.whispersystems:jobmanager:0.11.0'

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 640 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 362 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 193 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 330 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 310 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 231 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 644 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 548 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 560 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 983 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 305 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 784 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape
android:innerRadiusRatio="3"
android:shape="ring"
android:thickness="2dp"
android:useLevel="false" >
<solid android:color="@android:color/white" />
<size
android:height="52dp"
android:width="52dp" />
</shape>
</item>
<item>
<shape
android:innerRadiusRatio="3"
android:shape="ring"
android:thickness="2dp"
android:useLevel="false" >
<solid android:color="#40ffffff" />
<size
android:height="52dp"
android:width="52dp" />
</shape>
</item>
</selector>

View File

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

View File

@ -10,6 +10,12 @@
android:background="?conversation_background"
android:orientation="vertical">
<org.thoughtcrime.securesms.components.QuickAttachmentDrawer
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/quick_attachment_drawer"
android:layout_width="match_parent"
android:layout_height="match_parent">
<RelativeLayout android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:layout_weight="1"
@ -95,6 +101,14 @@
tools:hint="Send TextSecure message" />
</LinearLayout>
<ImageButton android:id="@+id/quick_attachment_toggle"
android:layout_width="wrap_content"
android:layout_height="44dp"
android:src="?quick_camera_icon"
android:background="@drawable/touch_highlight_background"
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_toggle_description"
android:padding="10dp"/>
<org.thoughtcrime.securesms.components.AnimatingToggle
android:id="@+id/button_toggle"
android:layout_width="50dp"
@ -141,4 +155,6 @@
</LinearLayout>
</RelativeLayout>
</org.thoughtcrime.securesms.components.QuickAttachmentDrawer>
</org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout>

View File

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

View File

@ -52,6 +52,7 @@
<attr name="emoji_category_places" format="reference"/>
<attr name="emoji_category_symbol" format="reference"/>
<attr name="emoji_category_emoticons" format="reference"/>
<attr name="quick_camera_icon" format="reference"/>
<attr name="conversation_item_background" format="reference"/>
<attr name="conversation_item_bubble_background" format="reference|color"/>

View File

@ -28,4 +28,5 @@
<dimen name="color_grid_extra_padding">32dp</dimen>
<dimen name="color_grid_item_size">48dp</dimen>
<dimen name="quick_media_drawer_default_height">250dp</dimen>
</resources>

View File

@ -521,6 +521,7 @@
<string name="conversation_activity__compose_description">Message composition</string>
<string name="conversation_activity__emoji_toggle_description">Toggle emoji keyboard</string>
<string name="conversation_activity__attachment_thumbnail">Attachment Thumbnail</string>
<string name="conversation_activity__quick_attachment_drawer_toggle_description">Toggle attachment drawer</string>
<!-- conversation_item -->
<string name="conversation_item__mms_downloading_description">Media message downloading</string>
@ -981,6 +982,9 @@
<!-- transport_selection_list_item -->
<string name="transport_selection_list_item__transport_icon">Transport icon</string>
<!-- quick_attachment_drawer -->
<string name="quick_camera_unavailable">Camera unavailable</string>
<!-- EOF -->
</resources>

View File

@ -40,6 +40,7 @@
<item name="ic_arrow_forward">@drawable/ic_arrow_forward_dark</item>
<item name="lockscreen_watermark">@drawable/lockscreen_watermark_dark</item>
<item name="android:windowBackground">@color/black</item>
<item name="conversation_background">@color/black</item>
</style>
<style name="PopupAnimation" parent="@android:style/Animation">
@ -124,6 +125,9 @@
<item name="conversation_item_sent_text_indicator_tab_color">#99000000</item>
<item name="conversation_item_received_text_primary_color">@color/white</item>
<item name="conversation_item_received_text_secondary_color">#BFffffff</item>
<item name="quick_camera_icon">@drawable/quick_camera_light</item>
<item name="conversation_item_background">@drawable/conversation_item_background</item>
<item name="conversation_item_sent_indicator_text_background">@drawable/conversation_item_sent_indicator_text_shape</item>
@ -250,6 +254,8 @@
<item name="emoji_category_symbol">@drawable/emoji_category_symbol_dark</item>
<item name="emoji_category_emoticons">@drawable/emoji_category_emoticons_dark</item>
<item name="quick_camera_icon">@drawable/quick_camera_dark</item>
<item name="menu_new_conversation_icon">@drawable/ic_add_white_24dp</item>
<item name="menu_group_icon">@drawable/ic_group_white_24dp</item>
<item name="menu_search_icon">@drawable/ic_search_white_24dp</item>

View File

@ -34,6 +34,7 @@ import android.os.Build;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
import android.support.v4.view.WindowCompat;
import android.text.Editable;
import android.text.InputType;
import android.text.TextWatcher;
@ -70,6 +71,9 @@ import org.thoughtcrime.securesms.components.emoji.EmojiPopup;
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
import org.thoughtcrime.securesms.components.QuickAttachmentDrawer;
import org.thoughtcrime.securesms.components.QuickCamera;
import org.thoughtcrime.securesms.crypto.EncryptingPartOutputStream;
import org.thoughtcrime.securesms.crypto.MasterCipher;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.crypto.SecurityEvent;
@ -114,6 +118,8 @@ import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.libaxolotl.InvalidMessageException;
import org.whispersystems.libaxolotl.util.guava.Optional;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
@ -172,6 +178,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private BroadcastReceiver groupUpdateReceiver;
private Optional<EmojiPopup> emojiPopup = Optional.absent();
private EmojiToggle emojiToggle;
private ImageButton quickAttachmentToggle;
private QuickAttachmentDrawer quickAttachmentDrawer;
private Recipients recipients;
private long threadId;
@ -193,6 +201,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
protected void onCreate(Bundle state, @NonNull MasterSecret masterSecret) {
this.masterSecret = masterSecret;
supportRequestWindowFeature(WindowCompat.FEATURE_ACTION_BAR_OVERLAY);
setContentView(R.layout.conversation_activity);
fragment = initFragment(R.id.fragment_content, new ConversationFragment(),
@ -229,6 +238,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
super.onResume();
dynamicTheme.onResume(this);
dynamicLanguage.onResume(this);
quickAttachmentDrawer.onResume();
initializeSecurity();
initializeEnabledCheck();
@ -249,6 +259,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
super.onPause();
MessageNotifier.setVisibleThread(-1L);
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
quickAttachmentDrawer.onPause();
}
@Override
@ -366,6 +377,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public void onBackPressed() {
if (isEmojiDrawerOpen()) {
hideEmojiPopup(false);
} else if (quickAttachmentDrawer.getDrawerState() != QuickAttachmentDrawer.COLLAPSED) {
quickAttachmentDrawer.setDrawerStateAndAnimate(QuickAttachmentDrawer.COLLAPSED);
} else {
super.onBackPressed();
}
@ -694,6 +707,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
addAttachmentAudio(Uri.parse(draft.getValue()));
} else if (draft.getType().equals(Draft.VIDEO)) {
addAttachmentVideo(Uri.parse(draft.getValue()));
} else if (draft.getType().equals(Draft.ENCRYPTED_IMAGE)) {
addAttachmentEncryptedImage(Uri.parse(draft.getValue()));
}
}
@ -766,6 +781,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
container.addOnKeyboardShownListener(this);
buttonToggle = (AnimatingToggle) findViewById(R.id.button_toggle);
sendButton = (SendButton) findViewById(R.id.send_button);
attachButton = (ImageButton) findViewById(R.id.attach_button);
composeText = (ComposeText) findViewById(R.id.embedded_text_editor);
charactersLeft = (TextView) findViewById(R.id.space_left);
emojiToggle = (EmojiToggle) findViewById(R.id.emoji_toggle);
titleView = (ConversationTitleView) getSupportActionBar().getCustomView();
unblockButton = (Button) findViewById(R.id.unblock_button);
composePanel = findViewById(R.id.bottom_panel);
quickAttachmentDrawer = (QuickAttachmentDrawer) findViewById(R.id.quick_attachment_drawer);
quickAttachmentToggle = (ImageButton) findViewById(R.id.quick_attachment_toggle);
int[] attributes = new int[]{R.attr.conversation_item_bubble_background};
TypedArray colors = obtainStyledAttributes(attributes);
int defaultColor = colors.getColor(0, Color.WHITE);
@ -814,6 +841,15 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
composeText.setOnClickListener(composeKeyPressedListener);
composeText.setOnFocusChangeListener(composeKeyPressedListener);
emojiToggle.setOnClickListener(new EmojiToggleListener());
if (quickAttachmentDrawer.hasCamera()) {
QuickAttachmentDrawerToggleListener listener = new QuickAttachmentDrawerToggleListener();
quickAttachmentDrawer.setQuickAttachmentDrawerListener(listener);
quickAttachmentDrawer.setQuickCameraListener(listener);
quickAttachmentToggle.setOnClickListener(listener);
} else {
quickAttachmentToggle.setVisibility(View.GONE);
}
}
protected void initializeActionBar() {
@ -934,6 +970,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private void addAttachmentEncryptedImage(Uri uri) {
try {
attachmentManager.setEncryptedImage(uri, masterSecret);
} catch (IOException | BitmapDecodingException e) {
Log.w(TAG, e);
attachmentManager.clear();
Toast.makeText(this, R.string.ConversationActivity_sorry_there_was_an_error_setting_your_attachment,
Toast.LENGTH_LONG).show();
}
}
private void addAttachmentImage(Uri imageUri) {
try {
attachmentManager.setImage(imageUri);
@ -1018,9 +1065,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
for (Slide slide : attachmentManager.getSlideDeck().getSlides()) {
if (slide.hasAudio()) drafts.add(new Draft(Draft.AUDIO, slide.getUri().toString()));
else if (slide.hasVideo()) drafts.add(new Draft(Draft.VIDEO, slide.getUri().toString()));
else if (slide.hasImage()) drafts.add(new Draft(Draft.IMAGE, slide.getUri().toString()));
String draftType = null;
if (slide.hasAudio()) draftType = Draft.AUDIO;
else if (slide.hasVideo()) draftType = Draft.VIDEO;
else if (slide.hasImage()) draftType = slide.isEncrypted() ? Draft.ENCRYPTED_IMAGE : Draft.IMAGE;
if (draftType != null)
drafts.add(new Draft(draftType, slide.getUri().toString()));
}
return drafts;
@ -1296,10 +1347,71 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
});
input.hideSoftInputFromWindow(composeText.getWindowToken(), 0);
quickAttachmentDrawer.setDrawerStateAndAnimate(QuickAttachmentDrawer.COLLAPSED);
}
}
}
private class QuickAttachmentDrawerToggleListener implements OnClickListener,
QuickAttachmentDrawer.QuickAttachmentDrawerListener,
QuickCamera.QuickCameraListener {
@QuickAttachmentDrawer.DrawerState int nextDrawerState = QuickAttachmentDrawer.HALF_EXPANDED;
@Override
public void onClick(View v) {
InputMethodManager input = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
input.hideSoftInputFromWindow(composeText.getWindowToken(), 0);
composeText.clearFocus();
hideEmojiPopup(false);
quickAttachmentDrawer.setDrawerStateAndAnimate(nextDrawerState);
}
@Override
public void onCollapsed() {
getSupportActionBar().show();
nextDrawerState = QuickAttachmentDrawer.HALF_EXPANDED;
}
@Override
public void onExpanded() {
getSupportActionBar().hide();
nextDrawerState = QuickAttachmentDrawer.COLLAPSED;
}
@Override
public void onHalfExpanded() {
getSupportActionBar().hide();
nextDrawerState = QuickAttachmentDrawer.COLLAPSED;
}
@Override
public void onImageCapture(final byte[] data) {
quickAttachmentDrawer.setDrawerStateAndAnimate(QuickAttachmentDrawer.COLLAPSED);
new AsyncTask<Void, Void, Uri>() {
@Override
protected Uri doInBackground(Void... voids) {
try {
File tempDirectory = getDir("media", Context.MODE_PRIVATE);
File tempFile = File.createTempFile("image", ".jpg", tempDirectory);
FileOutputStream fileOutputStream = new EncryptingPartOutputStream(tempFile, masterSecret);
fileOutputStream.write(data);
fileOutputStream.close();
return Uri.fromFile(tempFile);
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
@Override
protected void onPostExecute(Uri uri) {
if (uri != null)
addAttachmentEncryptedImage(uri);
}
}.execute();
}
}
private class SendButtonListener implements OnClickListener, TextView.OnEditorActionListener {
@Override
public void onClick(View v) {
@ -1370,8 +1482,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus) {
if (hasFocus && isEmojiDrawerOpen()) {
hideEmojiPopup(true);
} else if (hasFocus && quickAttachmentDrawer.getDrawerState() != QuickAttachmentDrawer.COLLAPSED) {
quickAttachmentDrawer.setDrawerStateAndAnimate(QuickAttachmentDrawer.COLLAPSED);
}
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,72 @@
/***
Copyright (c) 2013 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package org.thoughtcrime.securesms.components;
import android.hardware.Camera;
import android.media.MediaRecorder;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import com.commonsware.cwac.camera.PreviewStrategy;
import java.io.IOException;
class SurfacePreviewStrategy implements PreviewStrategy,
SurfaceHolder.Callback {
private final CameraView cameraView;
private SurfaceView preview=null;
private SurfaceHolder previewHolder=null;
@SuppressWarnings("deprecation")
SurfacePreviewStrategy(CameraView cameraView) {
this.cameraView=cameraView;
preview=new SurfaceView(cameraView.getContext());
previewHolder=preview.getHolder();
previewHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
previewHolder.addCallback(this);
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
cameraView.previewCreated();
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format,
int width, int height) {
cameraView.initPreview(width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
cameraView.previewDestroyed();
}
@Override
public void attach(Camera camera) throws IOException {
camera.setPreviewDisplay(previewHolder);
}
@Override
public void attach(MediaRecorder recorder) {
recorder.setPreviewDisplay(previewHolder.getSurface());
}
@Override
public View getWidget() {
return(preview);
}
}

View File

@ -0,0 +1,88 @@
package org.thoughtcrime.securesms.components;
/***
Copyright (c) 2013 CommonsWare, LLC
Licensed under the Apache License, Version 2.0 (the "License"); you may
not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import android.annotation.TargetApi;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.media.MediaRecorder;
import android.os.Build;
import android.view.TextureView;
import android.view.View;
import com.commonsware.cwac.camera.PreviewStrategy;
import java.io.IOException;
@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH)
class TexturePreviewStrategy implements PreviewStrategy,
TextureView.SurfaceTextureListener {
private final CameraView cameraView;
private TextureView widget=null;
private SurfaceTexture surface=null;
TexturePreviewStrategy(CameraView cameraView) {
this.cameraView=cameraView;
widget=new TextureView(cameraView.getContext());
widget.setSurfaceTextureListener(this);
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface,
int width, int height) {
this.surface=surface;
cameraView.previewCreated();
cameraView.initPreview(width, height);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface,
int width, int height) {
cameraView.previewReset(width, height);
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
cameraView.previewDestroyed();
return(true);
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
// no-op
}
@Override
public void attach(Camera camera) throws IOException {
camera.setPreviewTexture(surface);
}
@Override
public void attach(MediaRecorder recorder) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
// no-op
}
else {
throw new IllegalStateException(
"Cannot use TextureView with MediaRecorder");
}
}
@Override
public View getWidget() {
return(widget);
}
}

View File

@ -175,8 +175,9 @@ public class ThumbnailView extends FrameLayout {
private GenericRequestBuilder buildThumbnailGlideRequest(Slide slide, MasterSecret masterSecret) {
final GenericRequestBuilder builder;
if (slide.isDraft()) builder = buildDraftGlideRequest(slide);
else builder = buildEncryptedPartGlideRequest(slide, masterSecret);
if (slide.isDraft() && slide.isEncrypted()) builder = buildEncryptedDraftGlideRequest(slide, masterSecret);
else if (slide.isDraft()) builder = buildDraftGlideRequest(slide);
else builder = buildEncryptedPartGlideRequest(slide, masterSecret);
return builder;
}
@ -186,6 +187,15 @@ public class ThumbnailView extends FrameLayout {
.listener(new PduThumbnailSetListener(slide.getPart()));
}
private GenericRequestBuilder buildEncryptedDraftGlideRequest(Slide slide, MasterSecret masterSecret) {
if (masterSecret == null) {
throw new IllegalStateException("null MasterSecret when loading encrypted draft thumbnail");
}
return Glide.with(getContext()).load(new DecryptableUri(masterSecret, slide.getThumbnailUri()))
.fitCenter();
}
private GenericRequestBuilder buildEncryptedPartGlideRequest(Slide slide, MasterSecret masterSecret) {
if (masterSecret == null) {
throw new IllegalStateException("null MasterSecret when loading non-draft thumbnail");

View File

@ -101,10 +101,11 @@ public class DraftDatabase extends Database {
}
public static class Draft {
public static final String TEXT = "text";
public static final String IMAGE = "image";
public static final String VIDEO = "video";
public static final String AUDIO = "audio";
public static final String TEXT = "text";
public static final String IMAGE = "image";
public static final String VIDEO = "video";
public static final String AUDIO = "audio";
public static final String ENCRYPTED_IMAGE = "encrypted_image";
private final String type;
private final String value;
@ -124,10 +125,11 @@ public class DraftDatabase extends Database {
public String getSnippet(Context context) {
switch (type) {
case TEXT: return value;
case IMAGE: return context.getString(R.string.DraftDatabase_Draft_image_snippet);
case VIDEO: return context.getString(R.string.DraftDatabase_Draft_video_snippet);
case AUDIO: return context.getString(R.string.DraftDatabase_Draft_audio_snippet);
case TEXT: return value;
case ENCRYPTED_IMAGE:
case IMAGE: return context.getString(R.string.DraftDatabase_Draft_image_snippet);
case VIDEO: return context.getString(R.string.DraftDatabase_Draft_video_snippet);
case AUDIO: return context.getString(R.string.DraftDatabase_Draft_audio_snippet);
default: return null;
}
}

View File

@ -24,6 +24,7 @@ import android.net.Uri;
import android.os.Build;
import android.provider.ContactsContract;
import android.provider.MediaStore;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.View;
import android.view.animation.AlphaAnimation;
@ -36,6 +37,7 @@ import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import java.io.File;
@ -106,11 +108,24 @@ public class AttachmentManager {
setMedia(new AudioSlide(context, audio));
}
public void setEncryptedImage(Uri uri, MasterSecret masterSecret) throws IOException, BitmapDecodingException {
setMedia(new ImageSlide(context, masterSecret, uri), masterSecret);
}
public void setMedia(final Slide slide) {
setMedia(slide, null);
}
public void setMedia(final Slide slide, @Nullable MasterSecret masterSecret) {
Slide thumbnailSlide = slideDeck.getThumbnailSlide(context);
if (thumbnailSlide != null && thumbnailSlide.isEncrypted()) {
Uri dataUri = slideDeck.getThumbnailSlide(context).getPart().getDataUri();
new File(dataUri.getPath()).delete();
}
slideDeck.clear();
slideDeck.addSlide(slide);
attachmentView.setVisibility(View.VISIBLE);
thumbnail.setImageResource(slide);
thumbnail.setImageResource(slide, masterSecret);
attachmentListener.onAttachmentChanged();
}

View File

@ -20,25 +20,36 @@ import android.content.Context;
import android.content.res.Resources.Theme;
import android.net.Uri;
import android.support.annotation.DrawableRes;
import android.support.annotation.Nullable;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import org.thoughtcrime.securesms.util.Util;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import ws.com.google.android.mms.ContentType;
import ws.com.google.android.mms.pdu.PduPart;
public class ImageSlide extends Slide {
private static final String TAG = ImageSlide.class.getSimpleName();
private boolean encrypted = false;
public ImageSlide(Context context, MasterSecret masterSecret, PduPart part) {
super(context, masterSecret, part);
}
public ImageSlide(Context context, Uri uri) throws IOException, BitmapDecodingException {
super(context, constructPartFromUri(uri));
this(context, null, uri);
}
public ImageSlide(Context context, MasterSecret masterSecret, Uri uri) throws IOException, BitmapDecodingException {
super(context, masterSecret, constructPartFromByteArrayAndUri(uri, decryptContent(uri, masterSecret), masterSecret != null));
encrypted = masterSecret != null;
}
@Override
@ -62,12 +73,32 @@ public class ImageSlide extends Slide {
return true;
}
private static PduPart constructPartFromUri(Uri uri)
@Override
public boolean isEncrypted() {
return encrypted;
}
private static byte[] decryptContent(Uri uri, MasterSecret masterSecret) {
try {
if (masterSecret != null) {
InputStream inputStream = new DecryptingPartInputStream(new File(uri.getPath()), masterSecret);
return Util.readFully(inputStream);
}
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
private static PduPart constructPartFromByteArrayAndUri(Uri uri, @Nullable byte[] data, boolean encrypted)
throws IOException, BitmapDecodingException
{
PduPart part = new PduPart();
part.setDataUri(uri);
if (data != null)
part.setData(data);
part.setEncrypted(encrypted);
part.setContentType(ContentType.IMAGE_JPEG.getBytes());
part.setContentId((System.currentTimeMillis()+"").getBytes());
part.setName(("Image" + System.currentTimeMillis()).getBytes());

View File

@ -5,11 +5,13 @@ import android.content.Context;
import android.content.UriMatcher;
import android.net.Uri;
import org.thoughtcrime.securesms.crypto.DecryptingPartInputStream;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.PartDatabase;
import org.thoughtcrime.securesms.providers.PartProvider;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
@ -46,7 +48,11 @@ public class PartAuthority {
partUri = new PartUriParser(uri);
return partDatabase.getThumbnailStream(masterSecret, partUri.getPartId());
default:
return context.getContentResolver().openInputStream(uri);
String tempMediaDir = context.getDir("media", Context.MODE_PRIVATE).getPath();
if (uri.getPath().startsWith(tempMediaDir))
return new DecryptingPartInputStream(new File(uri.getPath()), masterSecret);
else
return context.getContentResolver().openInputStream(uri);
}
} catch (SecurityException se) {
throw new IOException(se);

View File

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

View File

@ -209,7 +209,7 @@ public class BitmapUtil {
}
}
private static Bitmap rotateBitmap(Bitmap bitmap, int angle) {
public static Bitmap rotateBitmap(Bitmap bitmap, int angle) {
Matrix matrix = new Matrix();
matrix.postRotate(angle);
Bitmap rotated = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true);