package com.moez.QKSMS.ui.view;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.PorterDuff;
import android.graphics.drawable.AnimationDrawable;
import android.graphics.drawable.Drawable;
import android.media.ExifInterface;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.provider.MediaStore;
import android.support.v4.content.ContextCompat;
import android.text.InputType;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.view.inputmethod.EditorInfo;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.Toast;
import com.github.lzyzsd.circleprogress.DonutProgress;
import com.moez.QKSMS.common.LiveViewManager;
import com.moez.QKSMS.enums.QKPreference;
import com.moez.QKSMS.mmssms.Transaction;
import com.moez.QKSMS.mmssms.Utils;
import com.moez.QKSMS.R;
import com.moez.QKSMS.common.AnalyticsManager;
import com.moez.QKSMS.data.Conversation;
import com.moez.QKSMS.data.ConversationLegacy;
import com.moez.QKSMS.interfaces.ActivityLauncher;
import com.moez.QKSMS.interfaces.RecipientProvider;
import com.moez.QKSMS.common.utils.ImageUtils;
import com.moez.QKSMS.common.utils.PhoneNumberUtils;
import com.moez.QKSMS.common.utils.Units;
import com.moez.QKSMS.transaction.NotificationManager;
import com.moez.QKSMS.transaction.SmsHelper;
import com.moez.QKSMS.ui.ThemeManager;
import com.moez.QKSMS.ui.base.QKActivity;
import com.moez.QKSMS.ui.dialog.DefaultSmsHelper;
import com.moez.QKSMS.ui.dialog.QKDialog;
import com.moez.QKSMS.ui.dialog.mms.MMSSetupFragment;
import com.moez.QKSMS.ui.settings.SettingsFragment;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
public class ComposeView extends LinearLayout implements View.OnClickListener {
public final static String TAG = "ComposeView";
private final String KEY_DELAYED_INFO_DIALOG_SHOWN = "delayed_info_dialog_shown";
private final int ANIMATION_DURATION = 300;
public interface OnSendListener {
void onSend(String[] addresses, String body);
}
enum SendButtonState {
SEND, // send a messaage
ATTACH, // open the attachment panel
CLOSE, // close the attachment panel
CANCEL // cancel a message while it's sending
}
private QKActivity mContext;
private SharedPreferences mPrefs;
private Resources mRes;
private Conversation mConversation;
private ConversationLegacy mConversationLegacy;
private ActivityLauncher mActivityLauncher;
private OnSendListener mOnSendListener;
private RecipientProvider mRecipientProvider;
// Analytics
// This string is sent along to events that happen in ComposeView, so that we know where they're
// happening (i.e. QKReply, QKCompose, etc)
private String mLabel;
// Views
private QKEditText mReplyText;
private FrameLayout mButton;
private DonutProgress mProgress;
private ImageView mButtonBackground;
private ImageView mComposeIcon;
private ImageButton mAttach;
private ImageButton mCamera;
private ImageButton mDelay;
private View mAttachmentPanel;
private QKTextView mLetterCount;
private FrameLayout mAttachmentLayout;
private AttachmentImageView mAttachment;
private ImageButton mCancel;
// State
private boolean mDelayedMessagingEnabled;
private boolean mSendingCancelled;
private boolean mIsSendingBlocked;
private String mSendingBlockedMessage;
private String mCurrentPhotoPath;
private ValueAnimator mProgressAnimator;
private int mDelayDuration = 3000;
private SendButtonState mButtonState = SendButtonState.ATTACH;
private static final int REQUEST_CODE_IMAGE = 0x00F1;
private static final int REQUEST_CODE_CAMERA = 0x00F2;
public ComposeView(Context context, AttributeSet attributeSet) {
this(context, attributeSet, 0);
}
public ComposeView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
mContext = (QKActivity) context;
mPrefs = mContext.getPrefs();
mRes = mContext.getResources();
mDelayedMessagingEnabled = mPrefs.getBoolean(SettingsFragment.DELAYED, false);
try {
mDelayDuration = Integer.parseInt(mPrefs.getString(SettingsFragment.DELAY_DURATION, "3"));
if (mDelayDuration < 1) {
mDelayDuration = 1;
} else if (mDelayDuration > 30) {
mDelayDuration = 30;
}
mDelayDuration *= 1000;
} catch (Exception e) {
mDelayDuration = 3000;
}
}
@Override
public void onFinishInflate() {
super.onFinishInflate();
// Get references to the views
mReplyText = (QKEditText) findViewById(R.id.compose_reply_text);
mButton = (FrameLayout) findViewById(R.id.compose_button);
mProgress = (DonutProgress) findViewById(R.id.progress);
mButtonBackground = (ImageView) findViewById(R.id.compose_button_background);
mComposeIcon = (ImageView) findViewById(R.id.compose_icon);
mAttachmentPanel = findViewById(R.id.attachment_panel);
mAttach = (ImageButton) findViewById(R.id.attach);
mCamera = (ImageButton) findViewById(R.id.camera);
mDelay = (ImageButton) findViewById(R.id.delay);
mLetterCount = (QKTextView) findViewById(R.id.compose_letter_count);
mAttachmentLayout = (FrameLayout) findViewById(R.id.attachment);
mAttachment = (AttachmentImageView) findViewById(R.id.compose_attachment);
mCancel = (ImageButton) findViewById(R.id.cancel);
mButton.setOnClickListener(this);
mAttach.setOnClickListener(this);
mCamera.setOnClickListener(this);
mCancel.setOnClickListener(this);
mDelay.setOnClickListener(this);
LiveViewManager.registerView(QKPreference.THEME, this, key -> {
mButtonBackground.setColorFilter(ThemeManager.getColor(), PorterDuff.Mode.SRC_ATOP);
mComposeIcon.setColorFilter(ThemeManager.getTextOnColorPrimary(), PorterDuff.Mode.SRC_ATOP);
mAttachmentPanel.setBackgroundColor(ThemeManager.getColor());
mAttach.setColorFilter(ThemeManager.getTextOnColorPrimary(), PorterDuff.Mode.SRC_ATOP);
mCamera.setColorFilter(ThemeManager.getTextOnColorPrimary(), PorterDuff.Mode.SRC_ATOP);
updateDelayButton();
mProgress.setUnfinishedStrokeColor(ThemeManager.getTextOnColorSecondary());
mProgress.setFinishedStrokeColor(ThemeManager.getTextOnColorPrimary());
if (ThemeManager.getSentBubbleRes() != 0) mReplyText.setBackgroundResource(ThemeManager.getSentBubbleRes());
});
LiveViewManager.registerView(QKPreference.BACKGROUND, this, key -> {
mReplyText.getBackground().setColorFilter(ThemeManager.getNeutralBubbleColor(), PorterDuff.Mode.SRC_ATOP);
getBackground().setColorFilter(ThemeManager.getBackgroundColor(), PorterDuff.Mode.SRC_ATOP);
});
// There is an option for using the return button instead of the emoticon button in the
// keyboard; set that up here.
switch (Integer.parseInt(mPrefs.getString(SettingsFragment.ENTER_BUTTON, "0"))) {
case 0: // emoji
break;
case 1: // new line
mReplyText.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES |
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE);
mReplyText.setSingleLine(false);
break;
case 2: // send
mReplyText.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
mReplyText.setInputType(InputType.TYPE_CLASS_TEXT |
InputType.TYPE_TEXT_FLAG_CAP_SENTENCES);
mReplyText.setSingleLine(false);
mReplyText.setOnKeyListener(new OnKeyListener() { //Workaround because ACTION_SEND does not support multiline mode
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (keyCode == 66) {
sendSms();
return true;
}
return false;
}});
break;
}
mReplyText.setTextChangedListener(new QKEditText.TextChangedListener() {
@Override
public void onTextChanged(CharSequence s) {
int length = s.length();
updateButtonState(length);
// If the reply is within 10 characters of the SMS limit (160), it will start counting down
// If the reply exceeds the SMS limit, it will count down until an extra message will have to be sent, and shows how many messages will currently be sent
if (length < 150) {
mLetterCount.setText("");
} else if (150 <= length && length <= 160) {
mLetterCount.setText("" + (160 - length));
} else if (160 < length) {
mLetterCount.setText((160 - length % 160) + "/" + (length / 160 + 1));
}
}
});
mProgressAnimator = new ValueAnimator();
mProgressAnimator.setInterpolator(new AccelerateDecelerateInterpolator());
mProgressAnimator.setDuration(mDelayDuration);
mProgressAnimator.setIntValues(0, 360);
mProgressAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
mProgress.setProgress((int) animation.getAnimatedValue());
}
});
mProgressAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mProgress.setVisibility(INVISIBLE);
mProgress.setProgress(0);
if (!mSendingCancelled) {
sendSms();
// In case they only enabled it for a particular message, let's set it back to the pref value
mDelayedMessagingEnabled = mPrefs.getBoolean(SettingsFragment.DELAYED, false);
updateDelayButton();
} else {
mSendingCancelled = false;
updateButtonState();
}
}
});
}
/**
* Sets the ActivityLauncher. This can be an Activity, a Fragment, or in general something that
* implements startActivityForResult(Intent, int), and onActivityResult(int, int, Intent); this
* instance must be able to launch and get results for activties.
* <p/>
* Additionally, in the onActivityResult(int, int, Intent) method, the ActivityLauncher instance
* should pass along that result value to this ComposeFragment, using its own onActivityResult
* method.
*
* @param launcher
*/
public void setActivityLauncher(ActivityLauncher launcher) {
mActivityLauncher = launcher;
}
/**
* Sets a listener to be pinged when an SMS message is sent.
*
* @param l
*/
public void setOnSendListener(OnSendListener l) {
mOnSendListener = l;
}
/**
* Sets a RecipientProvider. The RecipientProvider provides one method, getRecipientAddresses,
* which returns a String[] of recipient addresses. This method will be called when we're trying
* to send an SMS/MMS message, and onOpenConversation has NOT been called with a non-null
* Conversation object, i.e. we cannot use the Conversation object to get recipient addresses.
*
* @param p
*/
public void setRecipientProvider(RecipientProvider p) {
mRecipientProvider = p;
}
/**
* Handles activity results that were started by this View. Returns true if the result was
* handled by this view, false otherwise.
*
* @param requestCode
* @param resultCode
* @param data
*/
public boolean onActivityResult(int requestCode, int resultCode, final Intent data) {
boolean result = false;
if (requestCode == REQUEST_CODE_IMAGE && resultCode == Activity.RESULT_OK) {
result = true;
Toast.makeText(mContext, R.string.compose_loading_attachment, Toast.LENGTH_LONG).show();
new ImageLoaderTask(mContext, data.getData()).execute();
} else if (requestCode == REQUEST_CODE_CAMERA && resultCode == Activity.RESULT_OK) {
result = true;
Toast.makeText(mContext, R.string.compose_loading_attachment, Toast.LENGTH_LONG).show();
new ImageLoaderFromCameraTask().execute((Void[]) null);
}
return result;
}
private void updateButtonState() {
updateButtonState(mReplyText == null ? 0 : mReplyText.getText().length());
}
/**
* Sets the button image based on the length of the reply text, and whether or not the drawable
* is set.
*/
private void updateButtonState(int length) {
SendButtonState buttonState;
if (mAttachmentPanel.getVisibility() == View.VISIBLE) {
buttonState = SendButtonState.CLOSE;
} else if (length > 0 || mAttachment.hasAttachment()) {
buttonState = SendButtonState.SEND;
} else {
buttonState = SendButtonState.ATTACH;
}
updateButtonState(buttonState);
}
private void updateButtonState(SendButtonState buttonState) {
if (mButtonState != buttonState) {
// Check if we need to switch animations
AnimationDrawable animation = null;
if (buttonState == SendButtonState.SEND) {
animation = (AnimationDrawable) ContextCompat.getDrawable(mContext, R.drawable.plus_to_arrow);
} else if (mButtonState == SendButtonState.SEND) {
animation = (AnimationDrawable) ContextCompat.getDrawable(mContext, R.drawable.arrow_to_plus);
}
if (animation != null) {
mComposeIcon.setImageDrawable(animation);
animation.start();
}
// Handle any necessary rotation
float rotation = mComposeIcon.getRotation();
float target = buttonState == SendButtonState.ATTACH || buttonState == SendButtonState.SEND ? 0 : 45;
ObjectAnimator.ofFloat(mComposeIcon, "rotation", rotation, target)
.setDuration(ANIMATION_DURATION)
.start();
mButtonState = buttonState;
}
}
/**
* Sets the text of the Reply edit text.
*
* @param text
*/
public void setText(String text) {
mReplyText.setText(text);
mReplyText.setSelection(mReplyText.getText().length());
}
public void setSendingUnblocked() {
mSendingBlockedMessage = null;
mIsSendingBlocked = false;
}
public void setSendingBlocked(String message) {
mSendingBlockedMessage = message;
mIsSendingBlocked = true;
}
/**
* Requests focus to the Reply edit text.
*/
public void requestReplyTextFocus() {
mReplyText.requestFocus();
}
public void sendDelayedSms() {
mProgress.setVisibility(VISIBLE);
updateButtonState(SendButtonState.CANCEL);
mProgressAnimator.start();
}
public void sendSms() {
String body = mReplyText.getText().toString();
final Drawable attachment;
if (mAttachment.hasAttachment()) {
attachment = mAttachment.getDrawable();
} else {
attachment = null;
}
String[] recipients = null;
if (mConversation != null) {
recipients = mConversation.getRecipients().getNumbers();
for (int i = 0; i < recipients.length; i++) {
recipients[i] = PhoneNumberUtils.stripSeparators(recipients[i]);
}
} else if (mRecipientProvider != null) {
recipients = mRecipientProvider.getRecipientAddresses();
}
// If we have some recipients, send the message!
if (recipients != null && recipients.length > 0) {
clearAttachment();
mReplyText.setText("");
AnalyticsManager.getInstance().sendEvent(
AnalyticsManager.CATEGORY_MESSAGES,
AnalyticsManager.ACTION_SEND_MESSAGE,
mLabel
);
Transaction sendTransaction = new Transaction(mContext, SmsHelper.getSendSettings(mContext));
com.moez.QKSMS.mmssms.Message message = new com.moez.QKSMS.mmssms.Message(body, recipients);
message.setType(com.moez.QKSMS.mmssms.Message.TYPE_SMSMMS);
if (attachment != null) {
message.setImage(ImageUtils.drawableToBitmap(attachment));
}
// Notify the listener about the new text message
if (mOnSendListener != null) {
mOnSendListener.onSend(recipients, message.getSubject());
}
long threadId = mConversation != null ? mConversation.getThreadId() : 0;
if (!message.toString().equals("")) {
sendTransaction.sendNewMessage(message, threadId);
}
NotificationManager.update(mContext);
if (mConversationLegacy != null) {
mConversationLegacy.markRead();
}
// Reset the image button state
updateButtonState();
// Otherwise, show a toast to the user to prompt them to add recipients.
} else {
Toast.makeText(
mContext,
mRes.getString(R.string.error_no_recipients),
Toast.LENGTH_SHORT
).show();
}
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.compose_button:
handleComposeButtonClick();
break;
case R.id.cancel:
clearAttachment();
break;
case R.id.attach:
if (hasSetupMms()) {
mAttachmentPanel.setVisibility(GONE);
updateButtonState();
chooseAttachmentFromGallery();
}
break;
case R.id.camera:
if (hasSetupMms()) {
mAttachmentPanel.setVisibility(GONE);
updateButtonState();
attachFromCamera();
}
break;
case R.id.delay:
if (!mPrefs.getBoolean(KEY_DELAYED_INFO_DIALOG_SHOWN, false) && !mDelayedMessagingEnabled) {
showDelayedMessagingInfo();
} else {
toggleDelayedMessaging();
}
break;
}
}
private void toggleDelayedMessaging() {
mDelayedMessagingEnabled = !mDelayedMessagingEnabled;
updateDelayButton();
mAttachmentPanel.setVisibility(GONE);
updateButtonState();
}
private void showDelayedMessagingInfo() {
new QKDialog()
.setContext(mContext)
.setTitle(R.string.pref_delayed)
.setMessage(R.string.delayed_messaging_info)
.setNegativeButton(R.string.just_once, new OnClickListener() {
@Override
public void onClick(View v) {
toggleDelayedMessaging();
}
})
.setPositiveButton(R.string.enable, new OnClickListener() {
@Override
public void onClick(View v) {
mPrefs.edit().putBoolean(SettingsFragment.DELAYED, true).apply();
toggleDelayedMessaging();
}
})
.show();
mPrefs.edit().putBoolean(KEY_DELAYED_INFO_DIALOG_SHOWN, true).apply(); //This should be changed, the dialog should be shown each time when delayed messaging is disabled.
}
private void handleComposeButtonClick() {
switch (mButtonState) {
case ATTACH:
mAttachmentPanel.setVisibility(VISIBLE);
updateButtonState();
break;
case SEND:
// If the API version is less than KitKat, they can send an SMS; so do this.
if (Build.VERSION.SDK_INT < 19) {
if (mDelayedMessagingEnabled) {
sendDelayedSms();
} else {
sendSms();
}
} else {
// Otherwise... check if we're not the default SMS app
boolean isDefaultSmsApp = Utils.isDefaultSmsApp(mContext);
// Now make sure that a client hasn't blocked sending, i.e. in the welcome
// screen when we have a demo conversation.
if (mIsSendingBlocked) {
// Show the sending blocked message (if it exists)
Toast.makeText(
mContext,
mSendingBlockedMessage,
Toast.LENGTH_SHORT
).show();
} else if (!isDefaultSmsApp) {
// Ask to become the default SMS app
new DefaultSmsHelper(mContext, R.string.not_default_send).showIfNotDefault(this);
} else if (!TextUtils.isEmpty(mReplyText.getText()) || mAttachment.hasAttachment()) {
if (mDelayedMessagingEnabled) {
sendDelayedSms();
} else {
sendSms();
}
}
}
break;
case CLOSE:
mAttachmentPanel.setVisibility(GONE);
updateButtonState();
break;
case CANCEL:
mSendingCancelled = true;
mProgressAnimator.end();
//updateButtonState();
break;
}
}
private boolean hasSetupMms() {
if (TextUtils.isEmpty(mPrefs.getString(SettingsFragment.MMSC_URL, ""))
&& TextUtils.isEmpty(mPrefs.getString(SettingsFragment.MMS_PROXY, ""))
&& TextUtils.isEmpty(mPrefs.getString(SettingsFragment.MMS_PORT, ""))) {
// Not so fast! You need to set up MMS first.
MMSSetupFragment f = new MMSSetupFragment();
Bundle args = new Bundle();
args.putBoolean(MMSSetupFragment.ARG_ASK_FIRST, true);
f.setArguments(args);
((Activity) mContext).getFragmentManager()
.beginTransaction()
.add(f, MMSSetupFragment.TAG)
.commit();
return false;
}
return true;
}
private void attachFromCamera() {
AnalyticsManager.getInstance().sendEvent(
AnalyticsManager.CATEGORY_MESSAGES,
AnalyticsManager.ACTION_ATTACH_FROM_CAMERA,
mLabel
);
Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (cameraIntent.resolveActivity(mContext.getPackageManager()) != null) {
File file = null;
try {
file = createImageFile();
} catch (IOException e) {
e.printStackTrace();
}
if (file != null) {
cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(file));
mActivityLauncher.startActivityForResult(cameraIntent, REQUEST_CODE_CAMERA);
}
} else {
// Send a toast saying there was a camera error
if (mContext != null) {
String message = mContext.getResources().getString(R.string.attachment_camera_error);
Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
}
}
}
private void chooseAttachmentFromGallery() {
AnalyticsManager.getInstance().sendEvent(
AnalyticsManager.CATEGORY_MESSAGES,
AnalyticsManager.ACTION_ATTACH_IMAGE,
mLabel
);
try {
Intent photoPickerIntent = new Intent(Intent.ACTION_PICK);
photoPickerIntent.setType("image/*");
mActivityLauncher.startActivityForResult(photoPickerIntent, REQUEST_CODE_IMAGE);
} catch (ActivityNotFoundException e) {
// Send a toast saying no picture apps
if (mContext != null) {
String message = mContext.getResources().getString(R.string.attachment_app_not_found);
Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
}
}
}
private File createImageFile() throws IOException {
// Create an image file name
String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String imageFileName = "JPEG_" + timeStamp + "_";
File storageDir = Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES);
File image = File.createTempFile(
imageFileName, /* prefix */
".jpg", /* suffix */
storageDir /* directory */
);
// Save a file: path for use with ACTION_VIEW intents
mCurrentPhotoPath = image.getAbsolutePath();
return image;
}
/**
* Sets the conversation for this compose view. This will setup the ComposeView with drafts.
*
* @param conversationLegacy
*/
public void onOpenConversation(Conversation conversation, ConversationLegacy conversationLegacy) {
long threadId = mConversation != null ? mConversation.getThreadId() : -1;
if (threadId > 0) sendPendingDelayedMessage();
long newThreadId = conversation != null ? conversation.getThreadId() : -1;
if (mConversation != null && mConversationLegacy != null && threadId != newThreadId) {
// Save the old draft first before updating the conversation objects.
saveDraft();
}
mConversation = conversation;
mConversationLegacy = conversationLegacy;
// If the conversation was different, set up the draft here.
if (threadId != newThreadId || newThreadId == -1) {
setupDraft();
}
}
/**
* If there's a pending delayed message, end the progress animation and go ahead with sending the message
*/
private void sendPendingDelayedMessage() {
if (mButtonState == SendButtonState.CANCEL && mProgressAnimator != null) {
mProgressAnimator.end();
}
}
/**
* Saves a draft to the conversation.
*/
public void saveDraft() {
// If the conversation_reply view is null, then we won't worry about saving drafts at all. We also don't save
// drafts if a message is about to be sent (delayed)
if (mReplyText != null && mButtonState != SendButtonState.CANCEL) {
String draft = mReplyText.getText().toString();
if (mConversation != null) {
if (mConversationLegacy.hasDraft() && TextUtils.isEmpty(draft)) {
mConversationLegacy.clearDrafts();
} else if (!TextUtils.isEmpty(draft) &&
(!mConversationLegacy.hasDraft() || !draft.equals(mConversationLegacy.getDraft()))) {
mConversationLegacy.saveDraft(draft);
}
} else {
// Only show the draft if we saved text, not if we just cleared some
if (!TextUtils.isEmpty(draft)) {
if (mRecipientProvider != null) {
String[] addresses = mRecipientProvider.getRecipientAddresses();
if (addresses != null && addresses.length > 0) {
// save the message for each of the addresses
for (int i = 0; i < addresses.length; i++) {
ContentValues values = new ContentValues();
values.put("address", addresses[i]);
values.put("date", System.currentTimeMillis());
values.put("read", 1);
values.put("type", 4);
// attempt to create correct thread id
long threadId = Utils.getOrCreateThreadId(mContext, addresses[i]);
Log.v(TAG, "saving message with thread id: " + threadId);
values.put("thread_id", threadId);
Uri messageUri = mContext.getContentResolver().insert(Uri.parse("content://sms/draft"), values);
Log.v(TAG, "inserted to uri: " + messageUri);
ConversationLegacy mConversationLegacy = new ConversationLegacy(mContext, threadId);
mConversationLegacy.saveDraft(draft);
}
}
}
}
}
}
}
/**
* Displays the draft message to the user.
*/
private void setupDraft() {
if (mConversationLegacy != null) {
if (mConversationLegacy.hasDraft()) {
String text = mConversationLegacy.getDraft();
mReplyText.setText(text);
mReplyText.setSelection(text != null ? text.length() : 0);
clearAttachment();
} else {
// Since this view can be reused, it's important to set the text to empty when there
// isn't a new draft. Or else the previous conversation's draft can be carried on to
// this new conversation.
mReplyText.setText("");
clearAttachment();
}
}
}
/**
* Loads message data from an intent. Currently supports text/plain and image/* ACTION_SEND
* intents.
*
* @param intent The intent with the data to load.
*/
public void loadMessageFromIntent(final Intent intent) {
String type = intent == null ? null : intent.getType();
if (intent != null) {
if (type != null) {
if ("text/plain".equals(type)) {
mReplyText.setText(intent.getStringExtra(Intent.EXTRA_TEXT));
} else if (type.startsWith("image/")) {
Uri uri = intent.getData();
// If the Uri is null, try looking elsewhere for it. [1] [2]
// [1]: http://stackoverflow.com/questions/10386885/intent-filter-intent-getdata-returns-null
// [2]: http://developer.android.com/reference/android/content/Intent.html#ACTION_SEND
if (uri == null) {
Bundle extras = intent.getExtras();
if (extras != null) {
uri = (Uri) extras.get(Intent.EXTRA_STREAM);
}
}
new ImageLoaderTask(mContext, uri).execute();
// If the Uri is still null here, throw the exception.
if (uri == null) {
// TODO show the user some kind of feedback
}
}
} else {
if (intent.getExtras() != null) {
String body = intent.getExtras().getString("sms_body");
if (body != null) {
mReplyText.setText(body);
}
}
}
}
}
/**
* Clears the image from the attachment view.
*/
public void clearAttachment() {
mAttachment.setImageBitmap(null);
mAttachmentLayout.setVisibility(View.GONE);
updateButtonState();
}
/**
* Sets the image of the attachment view.
*
* @param imageBitmap the bitmap
*/
public void setAttachment(Bitmap imageBitmap) {
if (imageBitmap == null) {
clearAttachment();
} else {
AnalyticsManager.getInstance().sendEvent(
AnalyticsManager.CATEGORY_MESSAGES,
AnalyticsManager.ACTION_ATTACH_IMAGE,
mLabel
);
mAttachment.setImageBitmap(imageBitmap);
mAttachmentLayout.setVisibility(View.VISIBLE);
updateButtonState();
}
}
public void setLabel(String label) {
mLabel = label;
}
public static Bitmap rotateImage(Bitmap source, float angle) {
Matrix matrix = new Matrix();
matrix.postRotate(angle);
return Bitmap.createBitmap(source, 0, 0, source.getWidth(), source.getHeight(), matrix,
true);
}
private class ImageLoaderFromCameraTask extends AsyncTask<Void, Void, Bitmap> {
@Override
protected Bitmap doInBackground(Void... params) {// Get the dimensions of the View
int targetW = mAttachment.getMaxWidth();
int targetH = mAttachment.getMaxHeight();
// Get the dimensions of the bitmap
BitmapFactory.Options bmOptions = new BitmapFactory.Options();
bmOptions.inJustDecodeBounds = true;
int photoW = bmOptions.outWidth;
int photoH = bmOptions.outHeight;
// Determine how much to scale down the image
int scaleFactor = Math.min(photoW / targetW, photoH / targetH);
// Decode the image file into a Bitmap sized to fill the View
bmOptions.inJustDecodeBounds = false;
bmOptions.inSampleSize = scaleFactor;
bmOptions.inPurgeable = true;
Bitmap bitmap = BitmapFactory.decodeFile(mCurrentPhotoPath, bmOptions);
long maxAttachmentSize =
SmsHelper.getSendSettings(mContext).getMaxAttachmentSize();
bitmap = ImageUtils.shrink(bitmap, 90, maxAttachmentSize);
// Now, rotation the bitmap according to the Exif data.
ExifInterface ei = null;
try {
ei = new ExifInterface(mCurrentPhotoPath);
} catch (IOException e) {
e.printStackTrace();
}
int orientation = ei.getAttributeInt(ExifInterface.TAG_ORIENTATION,
ExifInterface.ORIENTATION_UNDEFINED);
switch(orientation) {
case ExifInterface.ORIENTATION_ROTATE_90:
return rotateImage(bitmap, 90);
case ExifInterface.ORIENTATION_ROTATE_180:
return rotateImage(bitmap, 180);
case ExifInterface.ORIENTATION_ROTATE_270:
return rotateImage(bitmap, 270);
case ExifInterface.ORIENTATION_NORMAL:
default:
return rotateImage(bitmap, 0); // No rotation
}
}
@Override
protected void onPostExecute(Bitmap bitmap) {
super.onPostExecute(bitmap);
setAttachment(bitmap);
mCurrentPhotoPath = null;
}
}
private class ImageLoaderTask extends AsyncTask<Uri, Void, Void> {
final Context mContext;
final Uri mUri;
final Handler mHandler;
public ImageLoaderTask(final Context context, final Uri uri) {
mContext = context;
mUri = uri;
mHandler = new Handler();
}
public void execute() {
execute(mUri);
}
@Override
protected Void doInBackground(Uri... params) {
if (params.length < 1) {
Log.e(TAG, "ImageLoaderTask called with no Uri");
return null;
}
try {
Uri uri = params[0];
// Decode the image from the Uri into a bitmap [1], and shrink it
// according to the user's settings.
// [1]: http://stackoverflow.com/questions/13930009/how-can-i-get-an-image-from-another-application
InputStream inputStream = mContext.getContentResolver().openInputStream(uri);
Bitmap bitmap = BitmapFactory.decodeStream(inputStream);
long maxAttachmentSize =
SmsHelper.getSendSettings(mContext).getMaxAttachmentSize();
bitmap = ImageUtils.shrink(bitmap, 90, maxAttachmentSize);
// Now, rotation the bitmap according to the Exif data.
final int rotation = ImageUtils.getOrientation(mContext, uri);
Matrix matrix = new Matrix();
matrix.postRotate(rotation);
bitmap = Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(),
bitmap.getHeight(), matrix, true);
// Can't post UI updates on a background thread.
final Bitmap imageBitmap = bitmap;
mHandler.post(new Runnable() {
@Override
public void run() {
setAttachment(imageBitmap);
}
});
} catch (FileNotFoundException | NullPointerException e) {
// Make a toast to the user that the file they've requested to view
// isn't available.
mHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(
mContext,
mRes.getString(R.string.error_file_not_found),
Toast.LENGTH_SHORT
).show();
}
});
}
return null;
}
}
private void updateDelayButton() {
mDelay.setColorFilter(mDelayedMessagingEnabled ?
ThemeManager.getTextOnColorPrimary() : ThemeManager.getTextOnColorSecondary(),
PorterDuff.Mode.SRC_ATOP);
}
public boolean isReplyTextEmpty() {
return TextUtils.isEmpty(mReplyText.getText());
}
}