Support for direct photo capture from app.

Make the send button toggle to an attach button when the
compose text and attachments are emmpty.

Part of #520
Closes #3186

// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-05-18 10:26:32 -07:00
parent dc903e49af
commit fe4e2fcadb
18 changed files with 234 additions and 28 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 636 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 653 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 469 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 781 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -97,19 +97,37 @@
android:contentDescription="@string/conversation_activity__compose_description"
android:textColor="?conversation_editor_text_color" />
<org.thoughtcrime.securesms.components.SendButton
android:id="@+id/send_button"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="center_vertical"
android:background="@drawable/touch_highlight_background"
android:contentDescription="@string/conversation_activity__send"
android:nextFocusLeft="@+id/embedded_text_editor"
android:padding="12dp"
android:src="?conversation_transport_sms_indicator"
android:focusable="true"
android:clickable="false"
android:enabled="false" />
<org.thoughtcrime.securesms.components.AnimatingToggle
android:id="@+id/button_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<ImageButton
android:id="@+id/attach_button"
android:layout_gravity="center_vertical"
android:background="@drawable/touch_highlight_background"
android:padding="12dp"
android:src="?conversation_attach_file"
android:contentDescription="@string/ConversationActivity_add_attachment"
android:nextFocusLeft="@+id/embedded_text_editor"
android:layout_width="wrap_content"
android:layout_height="fill_parent"/>
<org.thoughtcrime.securesms.components.SendButton
android:id="@+id/send_button"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_gravity="center_vertical"
android:background="@drawable/touch_highlight_background"
android:contentDescription="@string/conversation_activity__send"
android:nextFocusLeft="@+id/embedded_text_editor"
android:padding="12dp"
android:src="?conversation_transport_sms_indicator"
android:focusable="true"
android:clickable="false"
android:enabled="false" />
</org.thoughtcrime.securesms.components.AnimatingToggle>
</LinearLayout>

View file

@ -37,6 +37,7 @@
<attr name="conversation_attach_video" format="reference"/>
<attr name="conversation_attach_sound" format="reference"/>
<attr name="conversation_attach_contact_info" format="reference"/>
<attr name="conversation_attach_photo" format="reference"/>
<attr name="conversation_item_background" format="reference"/>
<attr name="conversation_item_received_background" format="reference" />

View file

@ -50,6 +50,7 @@
<string name="AttachmentManager_cant_open_media_selection">Can\'t find an app to select media.</string>
<!-- AttachmentTypeSelectorAdapter -->
<string name="AttachmentTypeSelectorAdapter_take_photo">Take Photo</string>
<string name="AttachmentTypeSelectorAdapter_picture">Picture</string>
<string name="AttachmentTypeSelectorAdapter_video">Video</string>
<string name="AttachmentTypeSelectorAdapter_audio">Audio</string>

View file

@ -53,6 +53,7 @@
<item name="conversation_attach_video">@drawable/ic_movie_creation_light</item>
<item name="conversation_attach_sound">@drawable/ic_volume_up_light</item>
<item name="conversation_attach_contact_info">@drawable/ic_account_box_light</item>
<item name="conversation_attach_photo">@drawable/ic_photo_camera_light</item>
<item name="conversation_item_background">@drawable/conversation_item_background</item>
<item name="conversation_item_received_background">@color/conversation_item_received_background_light</item>
@ -188,6 +189,7 @@
<item name="conversation_attach_video">@drawable/ic_movie_creation_dark</item>
<item name="conversation_attach_sound">@drawable/ic_volume_up_dark</item>
<item name="conversation_attach_contact_info">@drawable/ic_account_box_dark</item>
<item name="conversation_attach_photo">@drawable/ic_photo_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>

View file

@ -24,7 +24,6 @@ import android.content.Intent;
import android.content.IntentFilter;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.support.annotation.NonNull;
@ -44,6 +43,7 @@ import android.view.View.OnFocusChangeListener;
import android.view.View.OnKeyListener;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.ImageButton;
import android.widget.TextView;
import android.widget.Toast;
@ -51,9 +51,9 @@ import com.afollestad.materialdialogs.AlertDialogWrapper;
import com.google.protobuf.ByteString;
import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener;
import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.components.ComposeText;
import org.thoughtcrime.securesms.components.emoji.EmojiDrawer;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider;
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
import org.thoughtcrime.securesms.components.SendButton;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
@ -136,10 +136,13 @@ 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;
private ComposeText composeText;
private AnimatingToggle buttonToggle;
private SendButton sendButton;
private ImageButton attachButton;
private TextView charactersLeft;
private ConversationFragment fragment;
@ -237,7 +240,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
Log.w(TAG, "onActivityResult called: " + reqCode + ", " + resultCode + " , " + data);
super.onActivityResult(reqCode, resultCode, data);
if (data == null || resultCode != RESULT_OK) return;
if ((data == null && reqCode != CAPTURE_PHOTO) || resultCode != RESULT_OK) return;
switch (reqCode) {
case PICK_IMAGE:
addAttachmentImage(data.getData());
@ -251,6 +255,11 @@ 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);
initializeTitleBar();
@ -596,6 +605,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (draftText == null && draftImage == null && draftAudio == null && draftVideo == null) {
initializeDraftFromDatabase();
} else {
updateToggleButtonState();
}
}
@ -631,6 +642,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
addAttachmentVideo(Uri.parse(draft.getValue()));
}
}
updateToggleButtonState();
}
}.execute();
}
@ -675,7 +688,9 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
private void initializeViews() {
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);
emojiDrawer = Optional.absent();
@ -687,6 +702,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
SendButtonListener sendButtonListener = new SendButtonListener();
ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
attachButton.setOnClickListener(new AttachButtonListener());
sendButton.setOnClickListener(sendButtonListener);
sendButton.setEnabled(true);
sendButton.addOnTransportChangedListener(new OnTransportChangedListener() {
@ -775,6 +791,8 @@ 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:
@ -989,6 +1007,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
fragment.scrollToBottom();
attachmentManager.cleanup();
}
private void sendMessage() {
@ -1081,6 +1100,13 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}.execute(message);
}
private void updateToggleButtonState() {
if (composeText.getText().length() == 0 && !attachmentManager.isAttachmentPresent()) {
buttonToggle.display(attachButton);
} else {
buttonToggle.display(sendButton);
}
}
// Listeners
@ -1124,7 +1150,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
private class AttachButtonListener implements OnClickListener {
@Override
public void onClick(View v) {
handleAddAttachment();
}
}
private class ComposeKeyPressedListener implements OnKeyListener, OnClickListener, TextWatcher, OnFocusChangeListener {
int beforeLength;
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() == KeyEvent.ACTION_DOWN) {
@ -1146,12 +1182,20 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,int after) {
beforeLength = composeText.getText().length();
}
@Override
public void afterTextChanged(Editable s) {
calculateCharactersRemaining();
if (composeText.getText().length() == 0 || beforeLength == 0) {
updateToggleButtonState();
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count,int after) {}
@Override
public void onTextChanged(CharSequence s, int start, int before,int count) {}
@ -1176,5 +1220,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override
public void onAttachmentChanged() {
initializeSecurity();
updateToggleButtonState();
}
}

View file

@ -0,0 +1,108 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.support.annotation.NonNull;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
public class AnimatingToggle extends FrameLayout {
private static final int SPEED_MILLIS = 200;
public AnimatingToggle(Context context) {
super(context);
}
public AnimatingToggle(Context context, AttributeSet attrs) {
super(context, attrs);
}
public AnimatingToggle(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void addView(@NonNull View child, int index, ViewGroup.LayoutParams params) {
super.addView(child, index, params);
if (getChildCount() == 1) child.setVisibility(View.VISIBLE);
else child.setVisibility(View.GONE);
}
public void display(View view) {
if (view.getVisibility() == View.VISIBLE) return;
int oldViewIndex = getVisibleViewIndex();
int newViewIndex = getViewIndex(view);
int sign;
if (oldViewIndex < newViewIndex) sign = -1;
else sign = 1;
TranslateAnimation oldViewAnimation = createTranslation(0.0f, sign * 1.0f);
TranslateAnimation newViewAnimation = createTranslation(sign * -1.0f, 0.0f);
animateOut(oldViewIndex, oldViewAnimation);
animateIn(newViewIndex, newViewAnimation);
}
private void animateOut(int viewIndex, TranslateAnimation animation) {
final View view = getChildAt(viewIndex);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
view.setVisibility(View.GONE);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
});
view.startAnimation(animation);
}
private void animateIn(int viewIndex, TranslateAnimation animation) {
final View view = getChildAt(viewIndex);
view.setVisibility(View.VISIBLE);
view.startAnimation(animation);
}
private int getVisibleViewIndex() {
for (int i=0;i<getChildCount();i++) {
if (getChildAt(i).getVisibility() == View.VISIBLE) return i;
}
throw new AssertionError("No visible view?");
}
private int getViewIndex(View view) {
for (int i=0;i<getChildCount();i++) {
if (getChildAt(i) == view) return i;
}
throw new IllegalArgumentException("Not a parent of this view.");
}
private TranslateAnimation createTranslation(float startY, float endY) {
TranslateAnimation translateAnimation = new TranslateAnimation(Animation.RELATIVE_TO_SELF, 0.0f,
Animation.RELATIVE_TO_SELF, 0.0f,
Animation.RELATIVE_TO_SELF, startY,
Animation.RELATIVE_TO_SELF, endY);
translateAnimation.setDuration(SPEED_MILLIS);
return translateAnimation;
}
}

View file

@ -23,6 +23,7 @@ import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.ContactsContract;
import android.provider.MediaStore;
import android.util.Log;
import android.view.View;
import android.widget.Button;
@ -32,20 +33,23 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.ThumbnailView;
import org.thoughtcrime.securesms.util.BitmapDecodingException;
import java.io.File;
import java.io.IOException;
public class AttachmentManager {
private final static String TAG = AttachmentManager.class.getSimpleName();
private final Context context;
private final View attachmentView;
private final ThumbnailView thumbnail;
private final Button removeButton;
private final SlideDeck slideDeck;
private final Context context;
private final View attachmentView;
private final ThumbnailView thumbnail;
private final Button removeButton;
private final SlideDeck slideDeck;
private final AttachmentListener attachmentListener;
private File captureFile;
public AttachmentManager(Activity view, AttachmentListener listener) {
this.attachmentView = (View)view.findViewById(R.id.attachment_editor);
this.attachmentView = view.findViewById(R.id.attachment_editor);
this.thumbnail = (ThumbnailView)view.findViewById(R.id.attachment_thumbnail);
this.removeButton = (Button)view.findViewById(R.id.remove_image_button);
this.slideDeck = new SlideDeck();
@ -61,6 +65,11 @@ public class AttachmentManager {
attachmentListener.onAttachmentChanged();
}
public void cleanup() {
if (captureFile != null) captureFile.delete();
captureFile = null;
}
public void setImage(Uri image) throws IOException, BitmapDecodingException {
setMedia(new ImageSlide(context, image));
}
@ -78,6 +87,7 @@ public class AttachmentManager {
slideDeck.addSlide(slide);
attachmentView.setVisibility(View.VISIBLE);
thumbnail.setImageResource(slide);
attachmentListener.onAttachmentChanged();
}
public boolean isAttachmentPresent() {
@ -88,6 +98,24 @@ public class AttachmentManager {
return slideDeck;
}
public File getCaptureFile() {
return captureFile;
}
public void capturePhoto(Activity activity, int requestCode) {
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) {
Log.w(TAG, e);
}
}
public static void selectVideo(Activity activity, int requestCode) {
selectMediaType(activity, "video/*", requestCode);
}
@ -132,6 +160,7 @@ public class AttachmentManager {
@Override
public void onClick(View v) {
clear();
cleanup();
}
}

View file

@ -37,6 +37,7 @@ 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;
@ -71,10 +72,11 @@ 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_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_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);
return data;
}