Voice notes

Closes #4610
Closes #3563
// FREEBIE
This commit is contained in:
Moxie Marlinspike 2015-11-18 14:52:26 -08:00
parent 4e8e8978f4
commit bfe4ad6c34
36 changed files with 1075 additions and 169 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 460 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 487 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 333 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 328 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 601 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 557 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 739 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 767 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 964 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1013 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -12,13 +12,17 @@
android:id="@+id/quick_attachment_drawer" android:id="@+id/quick_attachment_drawer"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical"
android:clipToPadding="false"
android:clipChildren="false">
<LinearLayout android:layout_width="match_parent" <LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
android:paddingTop="?attr/actionBarSize" android:paddingTop="?attr/actionBarSize"
android:gravity="bottom"> android:gravity="bottom"
android:clipToPadding="false"
android:clipChildren="false">
<org.thoughtcrime.securesms.components.reminder.ReminderView <org.thoughtcrime.securesms.components.reminder.ReminderView
android:id="@+id/reminder" android:id="@+id/reminder"
@ -66,82 +70,7 @@
</FrameLayout> </FrameLayout>
<LinearLayout android:id="@+id/bottom_panel" <include layout="@layout/conversation_input_panel"/>
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:clickable="true"
android:background="?android:windowBackground"
android:padding="5dp">
<LinearLayout android:id="@+id/compose_bubble"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="@drawable/sent_bubble">
<org.thoughtcrime.securesms.components.emoji.EmojiToggle
android:id="@+id/emoji_toggle"
android:layout_width="37dp"
android:layout_height="37dp"
android:layout_gravity="bottom"
android:background="@drawable/touch_highlight_background"
android:contentDescription="@string/conversation_activity__emoji_toggle_description" />
<org.thoughtcrime.securesms.components.ComposeText
style="@style/ComposeEditText"
android:id="@+id/embedded_text_editor"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="37dp"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:nextFocusForward="@+id/send_button"
android:nextFocusRight="@+id/send_button"
tools:hint="Send TextSecure message">
<requestFocus />
</org.thoughtcrime.securesms.components.ComposeText>
<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"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/circle_tintable"
android:layout_gravity="bottom">
<ImageButton android:id="@+id/attach_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@drawable/circle_touch_highlight_background"
android:src="@drawable/ic_attach_white_24dp"
android:contentDescription="@string/ConversationActivity_add_attachment"
android:nextFocusLeft="@+id/embedded_text_editor" />
<org.thoughtcrime.securesms.components.SendButton
android:id="@+id/send_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/conversation_activity__send"
android:nextFocusLeft="@+id/embedded_text_editor"
android:src="?conversation_transport_sms_indicator"
android:background="@drawable/circle_touch_highlight_background" />
</org.thoughtcrime.securesms.components.AnimatingToggle>
</LinearLayout>
<Button android:id="@+id/unblock_button" <Button android:id="@+id/unblock_button"
android:layout_width="fill_parent" android:layout_width="fill_parent"

View File

@ -0,0 +1,171 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<org.thoughtcrime.securesms.components.InputPanel
android:id="@+id/bottom_panel"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal"
android:clickable="true"
android:background="?android:windowBackground"
android:padding="5dp"
android:clipChildren="false"
android:clipToPadding="false">
<FrameLayout android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:clipChildren="false"
android:clipToPadding="false">
<LinearLayout android:id="@+id/compose_bubble"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/sent_bubble"
android:clipChildren="false"
android:clipToPadding="false">
<org.thoughtcrime.securesms.components.emoji.EmojiToggle
android:id="@+id/emoji_toggle"
android:layout_width="37dp"
android:layout_height="37dp"
android:layout_gravity="bottom"
android:background="@drawable/touch_highlight_background"
android:contentDescription="@string/conversation_activity__emoji_toggle_description" />
<org.thoughtcrime.securesms.components.ComposeText
style="@style/ComposeEditText"
android:id="@+id/embedded_text_editor"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:minHeight="37dp"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:nextFocusForward="@+id/send_button"
android:nextFocusRight="@+id/send_button"
tools:visibility="invisible"
tools:hint="Send TextSecure message" >
<requestFocus />
</org.thoughtcrime.securesms.components.ComposeText>
<org.thoughtcrime.securesms.components.HidingLinearLayout
android:id="@+id/quick_attachment_toggle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clipChildren="false"
android:clipToPadding="false">
<ImageButton
android:id="@+id/quick_camera_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"/>
<org.thoughtcrime.securesms.components.MicrophoneRecorderView
android:id="@+id/recorder_view"
android:layout_width="37dp"
android:layout_height="37dp"
android:clipChildren="false"
android:clipToPadding="false">
<ImageButton
android:id="@+id/quick_audio_toggle"
android:layout_width="37dp"
android:layout_height="37dp"
android:layout_gravity="bottom"
android:src="?quick_mic_icon"
android:background="@null"
android:contentDescription="@string/conversation_activity__quick_attachment_drawer_toggle_description"
android:padding="10dp"/>
<ImageView android:id="@+id/quick_audio_fab"
android:layout_width="74dp"
android:layout_height="74dp"
android:src="@drawable/ic_mic_white_48dp"
android:background="@drawable/circle_tintable"
android:backgroundTint="@color/red_400"
android:visibility="gone"
android:scaleType="center"/>
</org.thoughtcrime.securesms.components.MicrophoneRecorderView>
</org.thoughtcrime.securesms.components.HidingLinearLayout>
</LinearLayout>
<LinearLayout android:id="@+id/recording_container"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:orientation="horizontal">
<TextView android:id="@+id/record_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:ellipsize="none"
android:layout_marginLeft="20dp"
android:text="00:00"
android:textColor="#61737b"
android:textSize="20dp"
android:singleLine="true"
android:visibility="gone"
tools:visibility="visible"/>
<FrameLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="true">
<TextView android:id="@+id/slide_to_cancel"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:drawableLeft="@drawable/ic_keyboard_arrow_left_grey600_24dp"
android:text="@string/conversation_input_panel__slide_to_cancel"
android:textAllCaps="true"
android:textColor="#61737b"
android:textSize="10sp"
android:ellipsize="none"
android:singleLine="true"
android:paddingLeft="20dp"
android:visibility="gone"
tools:visibility="visible"/>
</FrameLayout>
</LinearLayout>
</FrameLayout>
<org.thoughtcrime.securesms.components.AnimatingToggle
android:id="@+id/button_toggle"
android:layout_width="50dp"
android:layout_height="50dp"
android:background="@drawable/circle_tintable"
android:layout_gravity="bottom">
<ImageButton android:id="@+id/attach_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:background="@drawable/circle_touch_highlight_background"
android:src="@drawable/ic_attach_white_24dp"
android:contentDescription="@string/ConversationActivity_add_attachment"
android:nextFocusLeft="@+id/embedded_text_editor" />
<org.thoughtcrime.securesms.components.SendButton
android:id="@+id/send_button"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:contentDescription="@string/conversation_activity__send"
android:nextFocusLeft="@+id/embedded_text_editor"
android:src="?conversation_transport_sms_indicator"
android:background="@drawable/circle_touch_highlight_background" />
</org.thoughtcrime.securesms.components.AnimatingToggle>
</org.thoughtcrime.securesms.components.InputPanel>
</merge>

View File

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

View File

@ -51,6 +51,9 @@
<string name="AttachmentTypeSelectorAdapter_audio">Audio</string> <string name="AttachmentTypeSelectorAdapter_audio">Audio</string>
<string name="AttachmentTypeSelectorAdapter_contact">Contact info</string> <string name="AttachmentTypeSelectorAdapter_contact">Contact info</string>
<!-- AudioSlidePlayer -->
<string name="AudioSlidePlayer_error_playing_audio">Error playing audio!</string>
<!-- BlockedContactsActivity --> <!-- BlockedContactsActivity -->
<string name="BlockedContactsActivity_blocked_contacts">Blocked contacts</string> <string name="BlockedContactsActivity_blocked_contacts">Blocked contacts</string>
@ -128,6 +131,8 @@
<string name="ConversationActivity_unblock">Unblock</string> <string name="ConversationActivity_unblock">Unblock</string>
<string name="ConversationActivity_attachment_exceeds_size_limits">Attachment exceeds size limits for the type of message you\'re sending.</string> <string name="ConversationActivity_attachment_exceeds_size_limits">Attachment exceeds size limits for the type of message you\'re sending.</string>
<string name="ConversationActivity_quick_camera_unavailable">Camera unavailable</string> <string name="ConversationActivity_quick_camera_unavailable">Camera unavailable</string>
<string name="ConversationActivity_unable_to_record_audio">Unable to record audio!</string>
<string name="ConversationActivity_error_sending_voice_note">Error sending voice note...</string>
<!-- ConversationFragment --> <!-- ConversationFragment -->
<string name="ConversationFragment_message_details">Message details</string> <string name="ConversationFragment_message_details">Message details</string>
@ -275,6 +280,9 @@
<string name="ImportFragment_no_encrypted_backup_found">No encrypted backup found!</string> <string name="ImportFragment_no_encrypted_backup_found">No encrypted backup found!</string>
<string name="ImportFragment_restore_complete">Restore complete!</string> <string name="ImportFragment_restore_complete">Restore complete!</string>
<!-- InputPanel -->
<string name="InputPanel_tap_and_hold_to_record_a_voice_note_release_to_send">Tap and hold to record a voice note, release to send</string>
<!-- InviteActivity --> <!-- InviteActivity -->
<string name="InviteActivity_share">Share</string> <string name="InviteActivity_share">Share</string>
<string name="InviteActivity_send_sms">Send SMS</string> <string name="InviteActivity_send_sms">Send SMS</string>
@ -620,6 +628,9 @@
<string name="conversation_activity__attachment_thumbnail">Attachment Thumbnail</string> <string name="conversation_activity__attachment_thumbnail">Attachment Thumbnail</string>
<string name="conversation_activity__quick_attachment_drawer_toggle_description">Toggle attachment drawer</string> <string name="conversation_activity__quick_attachment_drawer_toggle_description">Toggle attachment drawer</string>
<!-- conversation_input_panel -->
<string name="conversation_input_panel__slide_to_cancel">SLIDE TO CANCEL</string>
<!-- conversation_item --> <!-- conversation_item -->
<string name="conversation_item__mms_downloading_description">Media message downloading</string> <string name="conversation_item__mms_downloading_description">Media message downloading</string>
<string name="conversation_item__mms_image_description">Media message</string> <string name="conversation_item__mms_image_description">Media message</string>
@ -1132,7 +1143,6 @@
<string name="device_link_fragment__link_device">Link device</string> <string name="device_link_fragment__link_device">Link device</string>
<string name="device_list_fragment__link_new_device">Link new device</string> <string name="device_list_fragment__link_new_device">Link new device</string>
<!-- EOF --> <!-- EOF -->
</resources> </resources>

View File

@ -147,6 +147,7 @@
<item name="conversation_item_received_text_secondary_color">#BFffffff</item> <item name="conversation_item_received_text_secondary_color">#BFffffff</item>
<item name="quick_camera_icon">@drawable/quick_camera_light</item> <item name="quick_camera_icon">@drawable/quick_camera_light</item>
<item name="quick_mic_icon">@drawable/ic_mic_grey600_24dp</item>
<item name="conversation_item_sent_indicator_text_background">@drawable/conversation_item_sent_indicator_text_shape</item> <item name="conversation_item_sent_indicator_text_background">@drawable/conversation_item_sent_indicator_text_shape</item>
@ -272,6 +273,7 @@
<item name="emoji_category_emoticons">@drawable/emoji_category_emoticons_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="quick_camera_icon">@drawable/quick_camera_dark</item>
<item name="quick_mic_icon">@drawable/ic_mic_white_24dp</item>
<item name="menu_new_conversation_icon">@drawable/ic_add_white_24dp</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_group_icon">@drawable/ic_group_white_24dp</item>

View File

@ -31,8 +31,10 @@ import android.graphics.drawable.ColorDrawable;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Vibrator;
import android.provider.ContactsContract; import android.provider.ContactsContract;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.view.WindowCompat; import android.support.v4.view.WindowCompat;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.text.Editable; import android.text.Editable;
@ -58,15 +60,17 @@ import com.google.protobuf.ByteString;
import org.thoughtcrime.redphone.RedPhone; import org.thoughtcrime.redphone.RedPhone;
import org.thoughtcrime.redphone.RedPhoneService; import org.thoughtcrime.redphone.RedPhoneService;
import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener; import org.thoughtcrime.securesms.TransportOptions.OnTransportChangedListener;
import org.thoughtcrime.securesms.audio.AudioRecorder;
import org.thoughtcrime.securesms.audio.AudioSlidePlayer; import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
import org.thoughtcrime.securesms.color.MaterialColor; import org.thoughtcrime.securesms.color.MaterialColor;
import org.thoughtcrime.securesms.components.AnimatingToggle; import org.thoughtcrime.securesms.components.AnimatingToggle;
import org.thoughtcrime.securesms.components.AttachmentTypeSelector; import org.thoughtcrime.securesms.components.AttachmentTypeSelector;
import org.thoughtcrime.securesms.components.ComposeText; import org.thoughtcrime.securesms.components.ComposeText;
import org.thoughtcrime.securesms.components.HidingLinearLayout;
import org.thoughtcrime.securesms.components.InputAwareLayout; import org.thoughtcrime.securesms.components.InputAwareLayout;
import org.thoughtcrime.securesms.components.InputPanel;
import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener; import org.thoughtcrime.securesms.components.KeyboardAwareLinearLayout.OnKeyboardShownListener;
import org.thoughtcrime.securesms.components.SendButton; import org.thoughtcrime.securesms.components.SendButton;
import org.thoughtcrime.securesms.components.camera.HidingImageButton;
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer; import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer;
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.AttachmentDrawerListener; import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.AttachmentDrawerListener;
import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.DrawerState; import org.thoughtcrime.securesms.components.camera.QuickAttachmentDrawer.DrawerState;
@ -91,11 +95,13 @@ import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.mms.AttachmentManager; import org.thoughtcrime.securesms.mms.AttachmentManager;
import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType; import org.thoughtcrime.securesms.mms.AttachmentManager.MediaType;
import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter; import org.thoughtcrime.securesms.mms.AttachmentTypeSelectorAdapter;
import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.mms.MediaConstraints; import org.thoughtcrime.securesms.mms.MediaConstraints;
import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingGroupMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage; import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.mms.SlideDeck;
import org.thoughtcrime.securesms.notifications.MessageNotifier; import org.thoughtcrime.securesms.notifications.MessageNotifier;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider; import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.Recipient;
@ -131,6 +137,7 @@ import java.io.IOException;
import java.security.NoSuchAlgorithmException; import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom; import java.security.SecureRandom;
import java.util.List; import java.util.List;
import java.util.concurrent.ExecutionException;
import static org.thoughtcrime.securesms.TransportOption.Type; import static org.thoughtcrime.securesms.TransportOption.Type;
import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord; import static org.thoughtcrime.securesms.database.GroupDatabase.GroupRecord;
@ -148,7 +155,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
AttachmentManager.AttachmentListener, AttachmentManager.AttachmentListener,
RecipientsModifiedListener, RecipientsModifiedListener,
OnKeyboardShownListener, OnKeyboardShownListener,
AttachmentDrawerListener AttachmentDrawerListener,
InputPanel.Listener
{ {
private static final String TAG = ConversationActivity.class.getSimpleName(); private static final String TAG = ConversationActivity.class.getSimpleName();
@ -175,17 +183,18 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private Button unblockButton; private Button unblockButton;
private InputAwareLayout container; private InputAwareLayout container;
private View composePanel; private View composePanel;
private View composeBubble;
protected ReminderView reminderView; protected ReminderView reminderView;
private AttachmentTypeSelector attachmentTypeSelector; private AttachmentTypeSelector attachmentTypeSelector;
private AttachmentManager attachmentManager; private AttachmentManager attachmentManager;
private AudioRecorder audioRecorder;
private BroadcastReceiver securityUpdateReceiver; private BroadcastReceiver securityUpdateReceiver;
private BroadcastReceiver groupUpdateReceiver; private BroadcastReceiver groupUpdateReceiver;
private EmojiDrawer emojiDrawer; private EmojiDrawer emojiDrawer;
private EmojiToggle emojiToggle; private EmojiToggle emojiToggle;
protected HidingImageButton quickAttachmentToggle; protected HidingLinearLayout quickAttachmentToggle;
private QuickAttachmentDrawer quickAttachmentDrawer; private QuickAttachmentDrawer quickAttachmentDrawer;
private InputPanel inputPanel;
private Recipients recipients; private Recipients recipients;
private long threadId; private long threadId;
@ -276,6 +285,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MessageNotifier.setVisibleThread(-1L); MessageNotifier.setVisibleThread(-1L);
if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right); if (isFinishing()) overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_right);
quickAttachmentDrawer.onPause(); quickAttachmentDrawer.onPause();
inputPanel.onPause();
AudioSlidePlayer.stopAll(); AudioSlidePlayer.stopAll();
} }
@ -844,13 +855,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
emojiDrawer = ViewUtil.findById(this, R.id.emoji_drawer); emojiDrawer = ViewUtil.findById(this, R.id.emoji_drawer);
unblockButton = ViewUtil.findById(this, R.id.unblock_button); unblockButton = ViewUtil.findById(this, R.id.unblock_button);
composePanel = ViewUtil.findById(this, R.id.bottom_panel); composePanel = ViewUtil.findById(this, R.id.bottom_panel);
composeBubble = ViewUtil.findById(this, R.id.compose_bubble);
container = ViewUtil.findById(this, R.id.layout_container); container = ViewUtil.findById(this, R.id.layout_container);
reminderView = ViewUtil.findById(this, R.id.reminder); reminderView = ViewUtil.findById(this, R.id.reminder);
quickAttachmentDrawer = ViewUtil.findById(this, R.id.quick_attachment_drawer); quickAttachmentDrawer = ViewUtil.findById(this, R.id.quick_attachment_drawer);
quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle); quickAttachmentToggle = ViewUtil.findById(this, R.id.quick_attachment_toggle);
inputPanel = ViewUtil.findById(this, R.id.bottom_panel);
ImageButton quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
View composeBubble = ViewUtil.findById(this, R.id.compose_bubble);
container.addOnKeyboardShownListener(this); container.addOnKeyboardShownListener(this);
inputPanel.setListener(this);
int[] attributes = new int[]{R.attr.conversation_item_bubble_background}; int[] attributes = new int[]{R.attr.conversation_item_bubble_background};
TypedArray colors = obtainStyledAttributes(attributes); TypedArray colors = obtainStyledAttributes(attributes);
@ -860,6 +875,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
attachmentTypeSelector = new AttachmentTypeSelector(this, new AttachmentTypeListener()); attachmentTypeSelector = new AttachmentTypeSelector(this, new AttachmentTypeListener());
attachmentManager = new AttachmentManager(this, this); attachmentManager = new AttachmentManager(this, this);
audioRecorder = new AudioRecorder(this, masterSecret);
SendButtonListener sendButtonListener = new SendButtonListener(); SendButtonListener sendButtonListener = new SendButtonListener();
ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener(); ComposeKeyPressedListener composeKeyPressedListener = new ComposeKeyPressedListener();
@ -920,9 +936,10 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (QuickAttachmentDrawer.isDeviceSupported(this)) { if (QuickAttachmentDrawer.isDeviceSupported(this)) {
quickAttachmentDrawer.setListener(this); quickAttachmentDrawer.setListener(this);
quickAttachmentToggle.setOnClickListener(new QuickAttachmentToggleListener()); quickCameraToggle.setOnClickListener(new QuickCameraToggleListener());
} else { } else {
quickAttachmentToggle.disable(); quickCameraToggle.setVisibility(View.GONE);
quickCameraToggle.setEnabled(false);
} }
} }
@ -1254,10 +1271,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private void sendMediaMessage(final boolean forceSms) private void sendMediaMessage(final boolean forceSms)
throws InvalidMessageException throws InvalidMessageException
{ {
sendMediaMessage(forceSms, getMessage(), attachmentManager.buildSlideDeck());
}
private ListenableFuture<Void> sendMediaMessage(final boolean forceSms, String body, SlideDeck slideDeck)
throws InvalidMessageException
{
final SettableFuture<Void> future = new SettableFuture<>();
final Context context = getApplicationContext(); final Context context = getApplicationContext();
OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(recipients, OutgoingMediaMessage outgoingMessage = new OutgoingMediaMessage(recipients,
attachmentManager.buildSlideDeck(), slideDeck,
getMessage(), body,
System.currentTimeMillis(), System.currentTimeMillis(),
distributionType); distributionType);
@ -1277,8 +1301,11 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
@Override @Override
protected void onPostExecute(Long result) { protected void onPostExecute(Long result) {
sendComplete(result); sendComplete(result);
future.set(null);
} }
}.execute(outgoingMessage); }.execute(outgoingMessage);
return future;
} }
private void sendTextMessage(final boolean forceSms) private void sendTextMessage(final boolean forceSms)
@ -1344,6 +1371,81 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
quickAttachmentToggle.disable(); quickAttachmentToggle.disable();
} }
@Override
public void onRecorderStarted() {
try {
Vibrator vibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE);
vibrator.vibrate(20);
audioRecorder.startRecording();
} catch (IOException e) {
Log.w(TAG, e);
}
}
@Override
public void onRecorderFinished() {
Vibrator vibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE);
vibrator.vibrate(20);
ListenableFuture<Pair<Uri, Long>> future = audioRecorder.stopRecording();
future.addListener(new ListenableFuture.Listener<Pair<Uri, Long>>() {
@Override
public void onSuccess(final @NonNull Pair<Uri, Long> result) {
try {
boolean forceSms = sendButton.isManualSelection() && sendButton.getSelectedTransport().isSms();
AudioSlide audioSlide = new AudioSlide(ConversationActivity.this, result.first, result.second);
SlideDeck slideDeck = new SlideDeck();
slideDeck.addSlide(audioSlide);
sendMediaMessage(forceSms, "", slideDeck).addListener(new AssertedSuccessListener<Void>() {
@Override
public void onSuccess(Void nothing) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
PersistentBlobProvider.getInstance(ConversationActivity.this).delete(result.first);
return null;
}
}.execute();
}
});
} catch (IOException | InvalidMessageException e) {
Log.w(TAG, e);
Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_error_sending_voice_note, Toast.LENGTH_LONG).show();
}
}
@Override
public void onFailure(ExecutionException e) {
Toast.makeText(ConversationActivity.this, R.string.ConversationActivity_unable_to_record_audio, Toast.LENGTH_LONG).show();
}
});
}
@Override
public void onRecorderCanceled() {
Vibrator vibrator = (Vibrator)getSystemService(Context.VIBRATOR_SERVICE);
vibrator.vibrate(50);
ListenableFuture<Pair<Uri, Long>> future = audioRecorder.stopRecording();
future.addListener(new ListenableFuture.Listener<Pair<Uri, Long>>() {
@Override
public void onSuccess(final Pair<Uri, Long> result) {
new AsyncTask<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
PersistentBlobProvider.getInstance(ConversationActivity.this).delete(result.first);
return null;
}
}.execute();
}
@Override
public void onFailure(ExecutionException e) {}
});
}
// Listeners // Listeners
private class AttachmentTypeListener implements AttachmentTypeSelector.AttachmentClickedListener { private class AttachmentTypeListener implements AttachmentTypeSelector.AttachmentClickedListener {
@ -1361,7 +1463,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
} }
} }
private class QuickAttachmentToggleListener implements OnClickListener { private class QuickCameraToggleListener implements OnClickListener {
@Override @Override
public void onClick(View v) { public void onClick(View v) {
if (!quickAttachmentDrawer.isShowing()) { if (!quickAttachmentDrawer.isShowing()) {

View File

@ -135,7 +135,7 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen
private void cancelSmsSelection() { private void cancelSmsSelection() {
contactsFragment.reset(); contactsFragment.reset();
updateSmsButtonText(); updateSmsButtonText();
ViewUtil.animateOut(smsSendFrame, slideOutAnimation); ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE);
} }
private class ShareClickListener implements OnClickListener { private class ShareClickListener implements OnClickListener {
@ -244,7 +244,7 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen
final Context context = getContext(); final Context context = getContext();
if (context == null) return; if (context == null) return;
ViewUtil.animateOut(smsSendFrame, slideOutAnimation).addListener(new Listener<Boolean>() { ViewUtil.animateOut(smsSendFrame, slideOutAnimation, View.GONE).addListener(new Listener<Boolean>() {
@Override @Override
public void onSuccess(Boolean result) { public void onSuccess(Boolean result) {
contactsFragment.reset(); contactsFragment.reset();

View File

@ -0,0 +1,201 @@
package org.thoughtcrime.securesms.audio;
import android.content.Context;
import android.media.MediaRecorder;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.support.annotation.NonNull;
import android.util.Log;
import android.util.Pair;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.providers.PersistentBlobProvider;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import org.whispersystems.jobqueue.Job;
import org.whispersystems.jobqueue.JobParameters;
import java.io.IOException;
public class AudioRecorder {
private static final String TAG = AudioRecorder.class.getSimpleName();
private final Context context;
private final MasterSecret masterSecret;
private final PersistentBlobProvider blobProvider;
private MediaRecorder mediaRecorder;
private Uri captureUri;
private ParcelFileDescriptor fd;
public AudioRecorder(@NonNull Context context, @NonNull MasterSecret masterSecret) {
this.context = context;
this.masterSecret = masterSecret;
this.blobProvider = PersistentBlobProvider.getInstance(context.getApplicationContext());
}
public void startRecording() throws IOException {
Log.w(TAG, "startRecording()");
ApplicationContext.getInstance(context)
.getJobManager()
.add(new StartRecordingJob());
}
public @NonNull ListenableFuture<Pair<Uri, Long>> stopRecording() {
Log.w(TAG, "stopRecording()");
StopRecordingJob stopRecordingJob = new StopRecordingJob();
ApplicationContext.getInstance(context)
.getJobManager()
.add(stopRecordingJob);
return stopRecordingJob.getFuture();
}
private class StopRecordingJob extends Job {
private final SettableFuture<Pair<Uri, Long>> future = new SettableFuture<>();
public StopRecordingJob() {
super(JobParameters.newBuilder()
.withGroupId(AudioRecorder.class.getSimpleName())
.create());
}
public ListenableFuture<Pair<Uri, Long>> getFuture() {
return future;
}
@Override
public void onAdded() {}
@Override
public void onRun() {
if (mediaRecorder == null) {
sendToFuture(new IOException("MediaRecorder was never initialized successfully!"));
return;
}
try {
mediaRecorder.stop();
} catch (Exception e) {
Log.w(TAG, e);
}
try {
fd.close();
} catch (IOException e) {
Log.w("AudioRecorder", e);
}
mediaRecorder.release();
mediaRecorder = null;
try {
long size = MediaUtil.getMediaSize(context, masterSecret, captureUri);
sendToFuture(new Pair<>(captureUri, size));
} catch (IOException ioe) {
Log.w(TAG, ioe);
sendToFuture(ioe);
}
captureUri = null;
fd = null;
}
@Override
public boolean onShouldRetry(Exception e) {
return false;
}
@Override
public void onCanceled() {}
private void sendToFuture(final @NonNull Pair<Uri, Long> result) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
future.set(result);
}
});
}
private void sendToFuture(final @NonNull Exception exception) {
Util.runOnMain(new Runnable() {
@Override
public void run() {
future.setException(exception);
}
});
}
}
private class StartRecordingJob extends Job {
public StartRecordingJob() {
super(JobParameters.newBuilder()
.withGroupId(AudioRecorder.class.getSimpleName())
.create());
}
@Override
public void onAdded() {}
@Override
public void onRun() throws Exception {
if (mediaRecorder != null) {
throw new AssertionError("We can only record once at a time.");
}
ParcelFileDescriptor fds[] = ParcelFileDescriptor.createPipe();
fd = fds[1];
captureUri = blobProvider.create(masterSecret, new ParcelFileDescriptor.AutoCloseInputStream(fds[0]));
mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.RAW_AMR);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
mediaRecorder.setOutputFile(fds[1].getFileDescriptor());
mediaRecorder.prepare();
try {
mediaRecorder.start();
} catch (Exception e) {
Log.w(TAG, e);
throw new IOException(e);
}
}
@Override
public boolean onShouldRetry(Exception e) {
return false;
}
@Override
public void onCanceled() {
try {
if (fd != null) {
fd.close();
}
if (captureUri != null) {
blobProvider.delete(captureUri);
}
fd = null;
mediaRecorder = null;
captureUri = null;
} catch (IOException e) {
Log.w(TAG, e);
}
}
}
}

View File

@ -9,7 +9,9 @@ import android.support.annotation.NonNull;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.util.Log; import android.util.Log;
import android.util.Pair; import android.util.Pair;
import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.MasterSecret; import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.mms.AudioSlide; import org.thoughtcrime.securesms.mms.AudioSlide;
import org.thoughtcrime.securesms.util.Util; import org.thoughtcrime.securesms.util.Util;
@ -111,7 +113,20 @@ public class AudioSlidePlayer {
@Override @Override
public boolean onError(MediaPlayer mp, int what, int extra) { public boolean onError(MediaPlayer mp, int what, int extra) {
Log.w(TAG, "MediaPlayer Error: " + what + " , " + extra); Log.w(TAG, "MediaPlayer Error: " + what + " , " + extra);
Toast.makeText(context, R.string.AudioSlidePlayer_error_playing_audio, Toast.LENGTH_SHORT).show();
synchronized (AudioSlidePlayer.this) {
mediaPlayer = null;
if (audioAttachmentServer != null) {
audioAttachmentServer.stop();
audioAttachmentServer = null;
}
}
notifyOnStop(); notifyOnStop();
progressEventHandler.removeMessages(0);
return true; return true;
} }
}); });

View File

@ -52,7 +52,7 @@ public class AnimatingToggle extends FrameLayout {
public void display(@Nullable View view) { public void display(@Nullable View view) {
if (view == current) return; if (view == current) return;
if (current != null) ViewUtil.animateOut(current, outAnimation); if (current != null) ViewUtil.animateOut(current, outAnimation, View.GONE);
if (view != null) ViewUtil.animateIn(view, inAnimation); if (view != null) ViewUtil.animateIn(view, inAnimation);
current = view; current = view;

View File

@ -0,0 +1,83 @@
package org.thoughtcrime.securesms.components;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet;
import android.view.ViewGroup;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnimationUtils;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.LinearLayout;
import org.thoughtcrime.securesms.R;
public class HidingLinearLayout extends LinearLayout {
public HidingLinearLayout(Context context) {
super(context);
}
public HidingLinearLayout(Context context, AttributeSet attrs) {
super(context, attrs);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public HidingLinearLayout(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public void hide() {
if (!isEnabled() || getVisibility() == GONE) return;
AnimationSet animation = new AnimationSet(true);
animation.addAnimation(new ScaleAnimation(1, 0, 1, 1, Animation.RELATIVE_TO_SELF, 1f, Animation.RELATIVE_TO_SELF, 0.5f));
animation.addAnimation(new AlphaAnimation(1, 0));
animation.setDuration(100);
animation.setAnimationListener(new Animation.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() {
if (!isEnabled() || getVisibility() == VISIBLE) return;
setVisibility(VISIBLE);
AnimationSet animation = new AnimationSet(true);
animation.addAnimation(new ScaleAnimation(0, 1, 1, 1, Animation.RELATIVE_TO_SELF, 1f, Animation.RELATIVE_TO_SELF, 0.5f));
animation.addAnimation(new AlphaAnimation(0, 1));
animation.setDuration(100);
animateWith(animation);
}
private void animateWith(Animation animation) {
animation.setDuration(150);
animation.setInterpolator(new FastOutSlowInInterpolator());
startAnimation(animation);
}
public void disable() {
setVisibility(GONE);
setEnabled(false);
}
}

View File

@ -0,0 +1,256 @@
package org.thoughtcrime.securesms.components;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.TranslateAnimation;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
import org.thoughtcrime.securesms.util.concurrent.SettableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
public class InputPanel extends LinearLayout implements MicrophoneRecorderView.Listener {
private static final String TAG = InputPanel.class.getSimpleName();
private static final int FADE_TIME = 150;
private View emojiToggle;
private View composeText;
private View quickCameraToggle;
private View quickAudioToggle;
private View buttonToggle;
private View recordingContainer;
private MicrophoneRecorderView microphoneRecorderView;
private SlideToCancel slideToCancel;
private RecordTime recordTime;
private @Nullable Listener listener;
public InputPanel(Context context) {
super(context);
}
public InputPanel(Context context, AttributeSet attrs) {
super(context, attrs);
}
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
public InputPanel(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
this.emojiToggle = ViewUtil.findById(this, R.id.emoji_toggle);
this.composeText = ViewUtil.findById(this, R.id.embedded_text_editor);
this.quickCameraToggle = ViewUtil.findById(this, R.id.quick_camera_toggle);
this.quickAudioToggle = ViewUtil.findById(this, R.id.quick_audio_toggle);
this.buttonToggle = ViewUtil.findById(this, R.id.button_toggle);
this.recordingContainer = ViewUtil.findById(this, R.id.recording_container);
this.recordTime = new RecordTime((TextView) ViewUtil.findById(this, R.id.record_time));
this.slideToCancel = new SlideToCancel(ViewUtil.findById(this, R.id.slide_to_cancel));
this.microphoneRecorderView = ViewUtil.findById(this, R.id.recorder_view);
this.microphoneRecorderView.setListener(this);
if (Build.VERSION.SDK_INT < 14) {
this.microphoneRecorderView.setVisibility(View.GONE);
this.microphoneRecorderView.setClickable(false);
}
}
public void setListener(@Nullable Listener listener) {
this.listener = listener;
}
@Override
public void onRecordPressed(float startPositionX) {
if (listener != null) listener.onRecorderStarted();
recordTime.display();
slideToCancel.display(startPositionX);
ViewUtil.fadeOut(emojiToggle, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(composeText, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(quickCameraToggle, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(quickAudioToggle, FADE_TIME, View.INVISIBLE);
ViewUtil.fadeOut(buttonToggle, FADE_TIME, View.INVISIBLE);
}
@Override
public void onRecordReleased(float x) {
onRecordHideEvent(x);
if (listener != null) {
Log.w(TAG, "Elapsed time: " + recordTime.getElapsedTimeMillis());
if (recordTime.getElapsedTimeMillis() > 1000) {
listener.onRecorderFinished();
} else {
Toast.makeText(getContext(), R.string.InputPanel_tap_and_hold_to_record_a_voice_note_release_to_send, Toast.LENGTH_LONG).show();
listener.onRecorderCanceled();
}
}
}
@Override
public void onRecordMoved(float x, float absoluteX) {
slideToCancel.moveTo(x);
if (absoluteX / recordingContainer.getWidth() <= 0.5) {
this.microphoneRecorderView.cancelAction();
}
}
@Override
public void onRecordCanceled(float x) {
onRecordHideEvent(x);
if (listener != null) listener.onRecorderCanceled();
}
public void onPause() {
this.microphoneRecorderView.cancelAction();
}
private void onRecordHideEvent(float x) {
ListenableFuture<Void> future = slideToCancel.hide(x);
future.addListener(new AssertedSuccessListener<Void>() {
@Override
public void onSuccess(Void result) {
ViewUtil.fadeIn(emojiToggle, FADE_TIME);
ViewUtil.fadeIn(composeText, FADE_TIME);
ViewUtil.fadeIn(quickCameraToggle, FADE_TIME);
ViewUtil.fadeIn(quickAudioToggle, FADE_TIME);
ViewUtil.fadeIn(buttonToggle, FADE_TIME);
recordTime.hide();
}
});
}
public interface Listener {
public void onRecorderStarted();
public void onRecorderFinished();
public void onRecorderCanceled();
}
private static class SlideToCancel {
private final View slideToCancelView;
private float startPositionX;
public SlideToCancel(View slideToCancelView) {
this.slideToCancelView = slideToCancelView;
}
public void display(float startPositionX) {
this.startPositionX = startPositionX;
ViewUtil.fadeIn(this.slideToCancelView, FADE_TIME);
}
public ListenableFuture<Void> hide(float x) {
final SettableFuture<Void> future = new SettableFuture<>();
float offset = -Math.max(0, this.startPositionX - x);
AnimationSet animation = new AnimationSet(true);
animation.addAnimation(new TranslateAnimation(Animation.ABSOLUTE, offset,
Animation.ABSOLUTE, 0,
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 0));
animation.addAnimation(new AlphaAnimation(1, 0));
animation.setDuration(MicrophoneRecorderView.ANIMATION_DURATION);
animation.setFillBefore(true);
animation.setFillAfter(false);
animation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {}
@Override
public void onAnimationEnd(Animation animation) {
future.set(null);
}
@Override
public void onAnimationRepeat(Animation animation) {}
});
slideToCancelView.setVisibility(View.GONE);
slideToCancelView.startAnimation(animation);
return future;
}
public void moveTo(float x) {
float offset = -Math.max(0, this.startPositionX - x);
Animation animation = new TranslateAnimation(Animation.ABSOLUTE, offset,
Animation.ABSOLUTE, offset,
Animation.RELATIVE_TO_SELF, 0,
Animation.RELATIVE_TO_SELF, 0);
animation.setDuration(0);
animation.setFillAfter(true);
animation.setFillBefore(true);
slideToCancelView.startAnimation(animation);
}
}
private static class RecordTime implements Runnable {
private final TextView recordTimeView;
private final AtomicLong startTime = new AtomicLong(0);
private final Handler handler = new Handler();
private RecordTime(TextView recordTimeView) {
this.recordTimeView = recordTimeView;
}
public void display() {
this.startTime.set(System.currentTimeMillis());
this.recordTimeView.setText("00:00");
ViewUtil.fadeIn(this.recordTimeView, FADE_TIME);
handler.postDelayed(this, TimeUnit.SECONDS.toMillis(1));
}
public void hide() {
this.startTime.set(0);
ViewUtil.fadeOut(this.recordTimeView, FADE_TIME, View.INVISIBLE);
}
public long getElapsedTimeMillis() {
return System.currentTimeMillis() - startTime.get();
}
@Override
public void run() {
long localStartTime = startTime.get();
if (localStartTime > 0) {
long elapsedTime = System.currentTimeMillis() - localStartTime;
recordTimeView.setText(String.format("%02d:%02d",
TimeUnit.MILLISECONDS.toMinutes(elapsedTime),
TimeUnit.MILLISECONDS.toSeconds(elapsedTime) -
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(elapsedTime))));
handler.postDelayed(this, TimeUnit.SECONDS.toMillis(1));
}
}
}
}

View File

@ -0,0 +1,184 @@
package org.thoughtcrime.securesms.components;
import android.content.Context;
import android.graphics.PorterDuff;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnticipateOvershootInterpolator;
import android.view.animation.DecelerateInterpolator;
import android.view.animation.OvershootInterpolator;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
public class MicrophoneRecorderView extends FrameLayout implements View.OnTouchListener {
public static final int ANIMATION_DURATION = 200;
private FloatingRecordButton floatingRecordButton;
private @Nullable Listener listener;
private boolean actionInProgress;
public MicrophoneRecorderView(Context context) {
super(context);
}
public MicrophoneRecorderView(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
ImageView recordButtonFab = ViewUtil.findById(this, R.id.quick_audio_fab);
this.floatingRecordButton = new FloatingRecordButton(getContext(), recordButtonFab);
View recordButton = ViewUtil.findById(this, R.id.quick_audio_toggle);
recordButton.setOnTouchListener(this);
}
public void cancelAction() {
if (this.actionInProgress) {
this.actionInProgress = false;
this.floatingRecordButton.hide(this.floatingRecordButton.lastPositionX);
if (listener != null) listener.onRecordCanceled(this.floatingRecordButton.lastPositionX);
}
}
@Override
public boolean onTouch(View v, final MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
this.actionInProgress = true;
this.floatingRecordButton.display(event.getX());
if (listener != null) listener.onRecordPressed(event.getX());
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
if (this.actionInProgress) {
this.actionInProgress = false;
this.floatingRecordButton.hide(event.getX());
if (listener != null) listener.onRecordReleased(event.getX());
}
break;
case MotionEvent.ACTION_MOVE:
if (this.actionInProgress) {
this.floatingRecordButton.moveTo(event.getX());
if (listener != null) listener.onRecordMoved(event.getX(), event.getRawX());
}
break;
}
return false;
}
public void setListener(@Nullable Listener listener) {
this.listener = listener;
}
public interface Listener {
public void onRecordPressed(float x);
public void onRecordReleased(float x);
public void onRecordCanceled(float x);
public void onRecordMoved(float x, float absoluteX);
}
private static class FloatingRecordButton {
private final ImageView recordButtonFab;
private float startPositionX;
private float lastPositionX;
public FloatingRecordButton(Context context, ImageView recordButtonFab) {
this.recordButtonFab = recordButtonFab;
this.recordButtonFab.getBackground().setColorFilter(context.getResources()
.getColor(R.color.red_500),
PorterDuff.Mode.SRC_IN);
}
public void display(float x) {
this.startPositionX = x;
this.lastPositionX = x;
recordButtonFab.setVisibility(View.VISIBLE);
AnimationSet animation = new AnimationSet(true);
animation.addAnimation(new TranslateAnimation(Animation.RELATIVE_TO_SELF, -.25f,
Animation.RELATIVE_TO_SELF, -.25f,
Animation.RELATIVE_TO_SELF, -.25f,
Animation.RELATIVE_TO_SELF, -.25f));
animation.addAnimation(new ScaleAnimation(.5f, 1f, .5f, 1f,
Animation.RELATIVE_TO_SELF, .5f,
Animation.RELATIVE_TO_SELF, .5f));
animation.setFillBefore(true);
animation.setFillAfter(true);
animation.setDuration(ANIMATION_DURATION);
animation.setInterpolator(new OvershootInterpolator());
recordButtonFab.startAnimation(animation);
}
public void moveTo(float x) {
this.lastPositionX = x;
float offset = -Math.max(0, this.startPositionX - x);
int widthAdjustment = -(recordButtonFab.getWidth() / 4);
Animation translateAnimation = new TranslateAnimation(Animation.ABSOLUTE, widthAdjustment + offset,
Animation.ABSOLUTE, widthAdjustment + offset,
Animation.RELATIVE_TO_SELF, -.25f,
Animation.RELATIVE_TO_SELF, -.25f);
translateAnimation.setDuration(0);
translateAnimation.setFillAfter(true);
translateAnimation.setFillBefore(true);
recordButtonFab.startAnimation(translateAnimation);
}
public void hide(float x) {
this.lastPositionX = x;
float offset = -Math.max(0, this.startPositionX - x);
int widthAdjustment = -(recordButtonFab.getWidth() / 4);
AnimationSet animation = new AnimationSet(false);
Animation scaleAnimation = new ScaleAnimation(1, 0.5f, 1, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f,
Animation.RELATIVE_TO_SELF, 0.5f);
Animation translateAnimation = new TranslateAnimation(Animation.ABSOLUTE, offset + widthAdjustment,
Animation.ABSOLUTE, widthAdjustment,
Animation.RELATIVE_TO_SELF, -.25f,
Animation.RELATIVE_TO_SELF, -.25f);
scaleAnimation.setInterpolator(new AnticipateOvershootInterpolator(1.5f));
translateAnimation.setInterpolator(new DecelerateInterpolator());
animation.addAnimation(scaleAnimation);
animation.addAnimation(translateAnimation);
animation.setDuration(ANIMATION_DURATION);
animation.setFillBefore(true);
animation.setFillAfter(false);
animation.setInterpolator(new AnticipateOvershootInterpolator(1.5f));
recordButtonFab.setVisibility(View.GONE);
recordButtonFab.clearAnimation();
recordButtonFab.startAnimation(animation);
}
}
}

View File

@ -1,57 +0,0 @@
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() {
if (!isEnabled() || getVisibility() == GONE) return;
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() {
if (!isEnabled() || getVisibility() == VISIBLE) return;
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);
}
public void disable() {
setVisibility(GONE);
setEnabled(false);
}
}

View File

@ -20,11 +20,16 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class PersistentBlobProvider { public class PersistentBlobProvider {
private static final String TAG = PersistentBlobProvider.class.getSimpleName(); private static final String TAG = PersistentBlobProvider.class.getSimpleName();
private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture"; private static final String URI_STRING = "content://org.thoughtcrime.securesms/capture";
public static final Uri CONTENT_URI = Uri.parse(URI_STRING); public static final Uri CONTENT_URI = Uri.parse(URI_STRING);
public static final String AUTHORITY = "org.thoughtcrime.securesms"; public static final String AUTHORITY = "org.thoughtcrime.securesms";
@ -48,7 +53,8 @@ public class PersistentBlobProvider {
} }
private final Context context; private final Context context;
private final Map<Long, byte[]> cache = new HashMap<>(); private final Map<Long, byte[]> cache = Collections.synchronizedMap(new HashMap<Long, byte[]>());
private final ExecutorService executor = Executors.newCachedThreadPool();
private PersistentBlobProvider(Context context) { private PersistentBlobProvider(Context context) {
this.context = context.getApplicationContext(); this.context = context.getApplicationContext();
@ -75,24 +81,22 @@ public class PersistentBlobProvider {
return ContentUris.withAppendedId(uniqueUri, id); return ContentUris.withAppendedId(uniqueUri, id);
} }
private void persistToDisk(final MasterSecret masterSecret, final long id, private void persistToDisk(final MasterSecret masterSecret, final long id, final InputStream input) {
final InputStream input) executor.submit(new Runnable() {
{ @Override
new AsyncTask<Void, Void, Void>() { public void run() {
@Override protected Void doInBackground(Void... params) {
try { try {
final OutputStream output = new EncryptingPartOutputStream(getFile(id), masterSecret); OutputStream output = new EncryptingPartOutputStream(getFile(id), masterSecret);
Log.w(TAG, "Starting stream copy....");
Util.copy(input, output); Util.copy(input, output);
Log.w(TAG, "Stream copy finished...");
} catch (IOException e) { } catch (IOException e) {
Log.w(TAG, e); Log.w(TAG, e);
} }
return null;
}
@Override protected void onPostExecute(Void aVoid) {
cache.remove(id); cache.remove(id);
} }
}.execute(); });
} }
public Uri createForExternal(@NonNull Recipients recipients) throws IOException { public Uri createForExternal(@NonNull Recipients recipients) throws IOException {
@ -136,4 +140,5 @@ public class PersistentBlobProvider {
return false; return false;
} }
} }
} }

View File

@ -130,12 +130,16 @@ public class ViewUtil {
} }
public static ListenableFuture<Boolean> fadeOut(final @NonNull View view, final int duration) { public static ListenableFuture<Boolean> fadeOut(final @NonNull View view, final int duration) {
return animateOut(view, getAlphaAnimation(1f, 0f, duration)); return fadeOut(view, duration, View.GONE);
} }
public static ListenableFuture<Boolean> animateOut(final @NonNull View view, final @NonNull Animation animation) { public static ListenableFuture<Boolean> fadeOut(@NonNull View view, int duration, int visibility) {
return animateOut(view, getAlphaAnimation(1f, 0f, duration), visibility);
}
public static ListenableFuture<Boolean> animateOut(final @NonNull View view, final @NonNull Animation animation, final int visibility) {
final SettableFuture future = new SettableFuture(); final SettableFuture future = new SettableFuture();
if (view.getVisibility() == View.GONE) { if (view.getVisibility() == visibility) {
future.set(true); future.set(true);
} else { } else {
view.clearAnimation(); view.clearAnimation();
@ -150,7 +154,7 @@ public class ViewUtil {
@Override @Override
public void onAnimationEnd(Animation animation) { public void onAnimationEnd(Animation animation) {
view.setVisibility(View.GONE); view.setVisibility(visibility);
future.set(true); future.set(true);
} }
}); });