blob: 32d91160c9aae1a1ce1cb2a36fdb4e692da5049f [file] [log] [blame]
/*
* Copyright (c) 2016-2017, The Linux Foundation. All rights reserved.
* Not a Contribution.
* Copyright (C) 2008 Esmertec AG.
* Copyright (C) 2008 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.mms.ui;
import static android.content.res.Configuration.KEYBOARDHIDDEN_NO;
import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_ABORT;
import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_COMPLETE;
import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_START;
import static com.android.mms.transaction.ProgressCallbackEntity.PROGRESS_STATUS_ACTION;
import static com.android.mms.ui.MessageListAdapter.COLUMN_ID;
import static com.android.mms.ui.MessageListAdapter.COLUMN_MMS_LOCKED;
import static com.android.mms.ui.MessageListAdapter.COLUMN_MSG_TYPE;
import static com.android.mms.ui.MessageListAdapter.COLUMN_SMS_ADDRESS;
import static com.android.mms.ui.MessageListAdapter.COLUMN_SMS_BODY;
import static com.android.mms.ui.MessageListAdapter.COLUMN_SMS_DATE;
import static com.android.mms.ui.MessageListAdapter.COLUMN_SMS_DATE_SENT;
import static com.android.mms.ui.MessageListAdapter.COLUMN_SMS_LOCKED;
import static com.android.mms.ui.MessageListAdapter.COLUMN_SMS_READ;
import static com.android.mms.ui.MessageListAdapter.COLUMN_SMS_STATUS;
import static com.android.mms.ui.MessageListAdapter.COLUMN_SMS_TYPE;
import static com.android.mms.ui.MessageListAdapter.COLUMN_SUB_ID;
import static com.android.mms.ui.MessageListAdapter.COLUMN_THREAD_ID;
import static com.android.mms.ui.MessageListAdapter.PROJECTION;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.regex.Pattern;
import java.util.Set;
import android.Manifest;
import android.animation.AnimatorSet;
import android.animation.ArgbEvaluator;
import android.animation.ObjectAnimator;
import android.app.ActionBar;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.ProgressDialog;
import android.content.ActivityNotFoundException;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.ComponentName;
import android.content.DialogInterface;
import android.content.DialogInterface.OnClickListener;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SqliteWrapper;
import android.drm.DrmStore;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.media.RingtoneManager;
import android.net.ConnectivityManager;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
import android.os.Parcelable;
import android.os.PersistableBundle;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.preference.PreferenceManager;
import android.provider.ContactsContract;
import android.provider.ContactsContract.QuickContact;
import android.provider.DocumentsContract.Document;
import android.provider.Telephony;
import android.provider.ContactsContract.CommonDataKinds.Email;
import android.provider.ContactsContract.CommonDataKinds.Phone;
import android.provider.ContactsContract.Contacts;
import android.provider.ContactsContract.Intents;
import android.provider.MediaStore.Audio;
import android.provider.MediaStore.Images;
import android.provider.MediaStore.Video;
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.provider.Telephony.Mms;
import android.provider.Telephony.Sms;
import android.provider.Telephony.Sms.Conversations;
import androidx.viewpager.widget.ViewPager;
import androidx.viewpager.widget.ViewPager.OnPageChangeListener;
import android.telephony.CarrierConfigManager;
import android.telephony.PhoneNumberUtils;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.SmsManager;
import android.telephony.SmsMessage;
import android.telephony.TelephonyManager;
import android.telecom.TelecomManager;
import android.telecom.VideoProfile;
import android.text.Editable;
import android.text.InputFilter;
import android.text.InputFilter.LengthFilter;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.method.TextKeyListener;
import android.text.style.URLSpan;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.view.ActionMode;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.view.View;
import android.view.View.OnKeyListener;
import android.view.ViewStub;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.webkit.MimeTypeMap;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.FrameLayout.LayoutParams;
import android.widget.GridView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import android.widget.Toolbar;
import android.widget.Button;
import com.android.ex.chips.RecipientEditTextView;
import com.android.mms.LogTag;
import com.android.mms.MmsApp;
import com.android.mms.MmsConfig;
import com.android.mms.R;
import com.android.mms.TempFileProvider;
import com.android.mms.data.Contact;
import com.android.mms.data.ContactList;
import com.android.mms.data.Conversation;
import com.android.mms.data.Conversation.ConversationQueryHandler;
import com.android.mms.data.WorkingMessage;
import com.android.mms.data.WorkingMessage.MessageStatusListener;
import com.android.mms.drm.DrmUtils;
import com.android.mms.model.ContentRestrictionFactory;
import com.android.mms.model.MediaModel;
import com.android.mms.model.SlideModel;
import com.android.mms.model.SlideshowModel;
import com.android.mms.transaction.MessagingNotification;
import com.android.mms.ui.MessageListView.OnSizeChangedListener;
import com.android.mms.ui.MessageUtils.ResizeImageResultCallback;
import com.android.mms.ui.zoom.ZoomGestureOverlayView;
import com.android.mms.ui.zoom.ZoomGestureOverlayView.IZoomListener;
import com.android.mms.ui.zoom.ZoomMessageListItem;
import com.android.mms.util.ContactRecipientEntryUtils;
import com.android.mms.util.DraftCache;
import com.android.mms.util.MaterialColorMapUtils;
import com.android.mms.util.MaterialColorMapUtils.MaterialPalette;
import com.android.mms.util.PhoneNumberFormatter;
import com.android.mms.util.SendingProgressTokenManager;
import com.android.mms.widget.MmsWidgetProvider;
import com.android.mmswrapper.ConstantsWrapper;
import com.android.mmswrapper.MediaFileWrapper;
import com.android.mmswrapper.SmsManagerWrapper;
import com.android.mmswrapper.SubscriptionManagerWrapper;
import com.android.mmswrapper.TelephonyManagerWrapper;
import com.android.mmswrapper.TelephonyWrapper;
import com.google.android.mms.ContentType;
import com.google.android.mms.MmsException;
import com.google.android.mms.pdu.EncodedStringValue;
import com.google.android.mms.pdu.PduBody;
import com.google.android.mms.pdu.PduPart;
import com.google.android.mms.pdu.PduPersister;
import com.google.android.mms.pdu.SendReq;
import java.util.Arrays;
import java.util.regex.Matcher;
import org.codeaurora.presenceserv.IPresenceService;
import org.codeaurora.presenceserv.IPresenceServiceCB;
import java.util.HashSet;
import android.sysprop.TelephonyProperties;
/**
* This is the main UI for:
* 1. Composing a new message;
* 2. Viewing/managing message history of a conversation.
*
* This activity can handle following parameters from the intent
* by which it's launched.
* thread_id long Identify the conversation to be viewed. When creating a
* new message, this parameter shouldn't be present.
* msg_uri Uri The message which should be opened for editing in the editor.
* address String The addresses of the recipients in current conversation.
* exit_on_sent boolean Exit this activity after the message is sent.
*/
public class ComposeMessageActivity extends Activity
implements View.OnClickListener, TextView.OnEditorActionListener,
MessageStatusListener, Contact.UpdateListener, IZoomListener {
public static final int REQUEST_CODE_ATTACH_IMAGE = 100;
public static final int REQUEST_CODE_TAKE_PICTURE = 101;
public static final int REQUEST_CODE_ATTACH_VIDEO = 102;
public static final int REQUEST_CODE_TAKE_VIDEO = 103;
public static final int REQUEST_CODE_ATTACH_SOUND = 104;
public static final int REQUEST_CODE_RECORD_SOUND = 105;
public static final int REQUEST_CODE_CREATE_SLIDESHOW = 106;
public static final int REQUEST_CODE_ECM_EXIT_DIALOG = 107;
public static final int REQUEST_CODE_ADD_CONTACT = 108;
public static final int REQUEST_CODE_PICK = 109;
public static final int REQUEST_CODE_ATTACH_ADD_CONTACT_INFO = 110;
public static final int REQUEST_CODE_ATTACH_ADD_CONTACT_VCARD = 111;
public static final int REQUEST_CODE_ATTACH_REPLACE_CONTACT_INFO = 112;
public static final int REQUEST_CODE_SCM_EXIT_DIALOG = 113;
private static final String TAG = LogTag.TAG;
private static final boolean DEBUG = false;
private static final boolean TRACE = false;
private static final boolean LOCAL_LOGV = false;
private static final boolean DEBUG_MULTI_CHOICE = true;
// Menu ID
private static final int MENU_ADD_SUBJECT = 0;
private static final int MENU_DELETE_THREAD = 1;
private static final int MENU_ADD_ATTACHMENT = 2;
private static final int MENU_DISCARD = 3;
private static final int MENU_SEND = 4;
private static final int MENU_CALL_RECIPIENT = 5;
private static final int MENU_CONVERSATION_LIST = 6;
private static final int MENU_DEBUG_DUMP = 7;
private static final int MENU_SEND_BY_SLOT1 = 9;
private static final int MENU_SEND_BY_SLOT2 = 10;
// Context menu ID
private static final int MENU_VIEW_CONTACT = 12;
private static final int MENU_ADD_TO_CONTACTS = 13;
private static final int MENU_COPY_PHONENUMBER = 14;
private static final int MENU_EDIT_MESSAGE = 15;
private static final int MENU_VIEW_SLIDESHOW = 16;
private static final int MENU_VIEW_MESSAGE_DETAILS = 17;
private static final int MENU_DELETE_MESSAGE = 18;
private static final int MENU_SEARCH = 19;
private static final int MENU_DELIVERY_REPORT = 20;
private static final int MENU_FORWARD_MESSAGE = 21;
private static final int MENU_CALL_BACK = 22;
private static final int MENU_SEND_EMAIL = 23;
private static final int MENU_COPY_MESSAGE_TEXT = 24;
private static final int MENU_COPY_TO_SDCARD = 25;
private static final int MENU_ADD_ADDRESS_TO_CONTACTS = 27;
private static final int MENU_LOCK_MESSAGE = 28;
private static final int MENU_UNLOCK_MESSAGE = 29;
private static final int MENU_SAVE_RINGTONE = 30;
private static final int MENU_PREFERENCES = 31;
private static final int MENU_GROUP_PARTICIPANTS = 32;
private static final int MENU_IMPORT_TEMPLATE = 33;
private static final int MENU_COPY_TO_SIM = 34;
private static final int MENU_RESEND = 35;
private static final int MENU_COPY_EXTRACT_URL = 36;
private static final int MENU_VIDEOCALL_RECIPIENT = 37;
private static final int RECIPIENTS_MAX_LENGTH = 312;
private static final int MESSAGE_LIST_QUERY_TOKEN = 9527;
private static final int MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN = 9528;
private static final int DELETE_MESSAGE_TOKEN = 9700;
private static final int CHARS_REMAINING_BEFORE_COUNTER_SHOWN = 10;
private static final long NO_DATE_FOR_DIALOG = -1L;
protected static final String KEY_EXIT_ON_SENT = "exit_on_sent";
protected static final String KEY_FORWARDED_MESSAGE = "forwarded_message";
protected static final String KEY_REPLY_MESSAGE = "reply_message";
private static final String EXIT_ECM_RESULT = "exit_ecm_result";
private static final String INTENT_MULTI_PICK_ACTION = "com.android.contacts.action.MULTI_PICK";
private static final String EXTRA_START_COMPOSE_FROM = "start_compose_from";
private static String FILE_PATH_COLUMN = "_data";
private static String BROADCAST_DATA_SCHEME = "file";
private static String URI_SCHEME_CONTENT = "content";
private static String URI_HOST_MEDIA = "media";
// When the conversation has a lot of messages and a new message is sent, the list is scrolled
// so the user sees the just sent message. If we have to scroll the list more than 20 items,
// then a scroll shortcut is invoked to move the list near the end before scrolling.
private static final int MAX_ITEMS_TO_INVOKE_SCROLL_SHORTCUT = 20;
// Any change in height in the message list view greater than this threshold will not
// cause a smooth scroll. Instead, we jump the list directly to the desired position.
private static final int SMOOTH_SCROLL_THRESHOLD = 200;
// To reduce janky interaction when message history + draft loads and keyboard opening
// query the messages + draft after the keyboard opens. This controls that behavior.
private static final boolean DEFER_LOADING_MESSAGES_AND_DRAFT = true;
// The max amount of delay before we force load messages and draft.
// 500ms is determined empirically. We want keyboard to have a chance to be shown before
// we force loading. However, there is at least one use case where the keyboard never shows
// even if we tell it to (turning off and on the screen). So we need to force load the
// messages+draft after the max delay.
private static final int LOADING_MESSAGES_AND_DRAFT_MAX_DELAY_MS = 500;
private static final int MSG_ADD_ATTACHMENT_FAILED = 1;
private static final int DIALOG_IMPORT_TEMPLATE = 1;
private static final int MSG_COPY_TO_SIM_SUCCESS = 2;
private static final int KILOBYTE = 1024;
// The max length of characters for subject.
private static final int SUBJECT_MAX_LENGTH = MmsConfig.getMaxSubjectLength();
// The number of buttons in two send button mode
private static final int NUMBER_OF_BUTTONS = 2;
// Preferred CDMA subscription mode is NV.
private static final int CDMA_SUBSCRIPTION_NV = 1;
// The default displaying page when selecting attachments.
private static final int DEFAULT_ATTACHMENT_PAGER = 0;
private static final int SAVE_ATTACHMENT_PERMISSION_REQUEST_CODE = 2016;
private ContentResolver mContentResolver;
private BackgroundQueryHandler mBackgroundQueryHandler;
private Conversation mConversation; // Conversation we are working in
// When mSendDiscreetMode is true, this activity only allows a user to type in and send
// a single sms, send the message, and then exits. The message history and menus are hidden.
private boolean mSendDiscreetMode;
private boolean mForwardMessageMode;
private boolean mReplyMessageMode;
private Toolbar mToolBar;
private View mTopPanel; // View containing the recipient and subject editors
private View mBottomPanel; // View containing the text editor, send button, ec.
private EditText mTextEditor; // Text editor to type your message into
private TextView mTextCounter; // Shows the number of characters used in text editor
private View mAttachmentSelector; // View containing the added attachment types
private ImageButton mAddAttachmentButton; // The button for add attachment
private ViewPager mAttachmentPager; // Attachment selector pager
private AttachmentPagerAdapter mAttachmentPagerAdapter; // Attachment selector pager adapter
private ImageButton mSendButtonMms; // Press to send mms
private TextView mSendButtonMmsText; // The text on MMS send button
private ImageButton mSendButtonSms; // Press to send sms
private EditText mSubjectTextEditor; // Text editor for MMS subject
private TextView mTextCounterSec; // The second send button text counter
private View mSendLayoutMmsFir; // The first mms send layout with sim indicator
private View mSendLayoutSmsFir; // The first sms send layout with sim indicator
private View mSendLayoutMmsSec; // The second mms send layout with sim indicator
private View mSendLayoutSmsSec; // The second sms send layout with sim indicator
private TextView mSendButtonMmsViewSec; // The second mms send button without sim indicator
private ImageButton mSendButtonSmsViewSec; // The second sms send button without sim indicator
private ImageView mIndicatorForSimMmsFir, mIndicatorForSimSmsFir;
private ImageView mIndicatorForSimMmsSec, mIndicatorForSimSmsSec;
private ZoomGestureOverlayView mZoomGestureOverlayView; // overlay for handling zoom
private ImageButton mBackView;
private View mDeviderView;
private AttachmentEditor mAttachmentEditor;
private View mAttachmentEditorScrollView;
private MessageListView mMsgListView; // ListView for messages in this conversation
public MessageListAdapter mMsgListAdapter; // and its corresponding ListAdapter
private ModeCallback mModeCallback;
private RecipientsEditor mRecipientsEditor; // UI control for editing recipients
private ImageButton mRecipientsPicker; // UI control for recipients picker
private ImageButton mRecipientsPickerGroups; // UI control for group recipients picker
private ImageButton mClearButton;
// For HW keyboard, 'mIsKeyboardOpen' indicates if the HW keyboard is open.
// For SW keyboard, 'mIsKeyboardOpen' should always be true.
private boolean mIsKeyboardOpen;
private boolean mIsLandscape; // Whether we're in landscape mode
private boolean mToastForDraftSave; // Whether to notify the user that a draft is being saved
private boolean mSentMessage; // true if the user has sent a message while in this
// activity. On a new compose message case, when the first
// message is sent is a MMS w/ attachment, the list blanks
// for a second before showing the sent message. But we'd
// think the message list is empty, thus show the recipients
// editor thinking it's a draft message. This flag should
// help clarify the situation.
private WorkingMessage mWorkingMessage; // The message currently being composed.
private AlertDialog mInvalidRecipientDialog, mMsgDetailDialog;
private boolean mWaitingForSubActivity;
private boolean mInAsyncAddAttathProcess = false;
private int mLastRecipientCount; // Used for warning the user on too many recipients.
private boolean mSendingMessage; // Indicates the current message is sending, and shouldn't send again.
private Intent mAddContactIntent; // Intent used to add a new contact
private String mBodyString; // Only used as a temporary to hold a message body
private Uri mTempMmsUri; // Only used as a temporary to hold a slideshow uri
private long mTempThreadId; // Only used as a temporary to hold a threadId
private AsyncDialog mAsyncDialog; // Used for background tasks.
private String mDebugRecipients;
private int mLastSmoothScrollPosition;
private boolean mScrollOnSend; // Flag that we need to scroll the list to the end.
private boolean mIsReplaceAttachment;
private int mCurrentAttachmentPager;
private int mSavedScrollPosition = -1; // we save the ListView's scroll position in onPause(),
// so we can remember it after re-entering the activity.
// If the value >= 0, then we jump to that line. If the
// value is maxint, then we jump to the end.
private long mLastMessageId;
private AlertDialog mMsimDialog; // Used for MSIM subscription choose
// Record the resend sms recipient when the sms send to more than one recipient
private String mResendSmsRecipient;
private static final String INTENT_ACTION_LTE_DATA_ONLY_DIALOG =
"com.qualcomm.qti.phonefeature.DISABLE_TDD_LTE";
private static final String LTE_DATA_ONLY_KEY = "network_band";
private static final int LTE_DATA_ONLY_MODE = 2;
private static final String SIM_STATE_CHANGE_ACTION =
"android.intent.action.SIM_STATE_CHANGED";
/**
* Whether this activity is currently running (i.e. not paused)
*/
private boolean mIsRunning;
// we may call loadMessageAndDraft() from a few different places. This is used to make
// sure we only load message+draft once.
private boolean mMessagesAndDraftLoaded;
/**
* Whether the attachment error is in the case of sendMms.
*/
private boolean mIsAttachmentErrorOnSend = false;
// whether we should load the draft. For example, after attaching a photo and coming back
// in onActivityResult(), we should not load the draft because that will mess up the draft
// state of mWorkingMessage. Also, if we are handling a Send or Forward Message Intent,
// we should not load the draft.
private boolean mShouldLoadDraft;
// Whether or not we are currently enabled for SMS. This field is updated in onStart to make
// sure we notice if the user has changed the default SMS app.
private boolean mIsSmsEnabled;
private boolean mIsAirplaneModeOn = false;
private Handler mHandler = new Handler();
private boolean mIsRTL = false;
// keys for extras and icicles
public final static String THREAD_ID = "thread_id";
private final static String RECIPIENTS = "recipients";
public final static String MANAGE_MODE = "manage_mode";
private final static String MESSAGE_ID = "message_id";
private final static String MESSAGE_TYPE = "message_type";
private final static String MESSAGE_BODY = "message_body";
private final static String MESSAGE_SUBJECT = "message_subject";
private final static String MESSAGE_SUBJECT_CHARSET = "message_subject_charset";
private final static String NEED_RESEND = "needResend";
private final static int MSG_ONLY_ONE_FAIL_LIST_ITEM = 1;
private static final String LINE_BREAK = "\n";
private static final String COLON = ":";
private static final String LEFT_PARENTHESES = "(";
private static final String RIGHT_PARENTHESES = ")";
private boolean isLocked = false;
private boolean mIsPickingContact = false;
// List for contacts picked from People.
private ContactList mRecipientsPickList = null;
/**
* Whether the recipients is picked from Contacts
*/
private boolean mIsProcessPickedRecipients = false;
private int mExistsRecipientsCount = 0;
private final static String MSG_SUBJECT_SIZE = "subject_size";
private int mResizeImageCount = 0;
private Object mObjectLock = new Object();
private boolean mShowAttachIcon = false;
private final static int REPLACE_ATTACHMEN_MASK = 1 << 16;
private boolean mShowTwoButtons = false;
private boolean mSendMmsSupportViaWiFi = false;
private boolean isAvoidingSavingDraft = false;
private IPresenceService mService;
private static final int PRESENCE_AVAILABILITY_FETCH = 0;
private Handler mAvailabilityfetchHandler;
private boolean mVideoCapable = true;
private boolean mIsBound;
private boolean mEnablePresence;
private static final String PRESENCESERV = "com.qualcomm.qti.presenceserv";
private static final String PRESENCESERV_PRESENCESERVICE =
"com.qualcomm.qti.presenceserv.PresenceService";
private static Drawable sDefaultContactImage;
private static int sPrimaryColorDark;
private Drawable mAvatarDrawable;
private static int mActionBarColor;
private static int mSendContactColor;
private boolean mIsEnableSelectCopy = false;
private int mAccentColor = 0;
private int mStatusBarColor = 0;
private final IntentFilter mAirplaneModeFilter = new IntentFilter(Intent.ACTION_AIRPLANE_MODE_CHANGED);
private final IntentFilter mSIMStatusChangeFilter = new IntentFilter(SIM_STATE_CHANGE_ACTION);
@SuppressWarnings("unused")
public static void log(String logMsg) {
Thread current = Thread.currentThread();
long tid = current.getId();
StackTraceElement[] stack = current.getStackTrace();
String methodName = stack[3].getMethodName();
// Prepend current thread ID and name of calling method to the message.
logMsg = "[" + tid + "] [" + methodName + "] " + logMsg;
Log.d(TAG, logMsg);
}
//==========================================================
// Inner classes
//==========================================================
private void editSlideshow() {
final int subjectSize = mWorkingMessage.hasSubject()
? mWorkingMessage.getSubject().toString().getBytes().length : 0;
// The user wants to edit the slideshow. That requires us to persist the slideshow to
// disk as a PDU in saveAsMms. This code below does that persisting in a background
// task. If the task takes longer than a half second, a progress dialog is displayed.
// Once the PDU persisting is done, another runnable on the UI thread get executed to start
// the SlideshowEditActivity.
getAsyncDialog().runAsync(new Runnable() {
@Override
public void run() {
// This runnable gets run in a background thread.
mTempMmsUri = mWorkingMessage.saveAsMms(false);
}
}, new Runnable() {
@Override
public void run() {
// Once the above background thread is complete, this runnable is run
// on the UI thread.
if (mTempMmsUri == null) {
return;
}
Intent intent = new Intent(ComposeMessageActivity.this,
SlideshowEditActivity.class);
intent.setData(mTempMmsUri);
intent.putExtra(MSG_SUBJECT_SIZE, subjectSize);
startActivityForResult(intent, REQUEST_CODE_CREATE_SLIDESHOW);
}
}, R.string.building_slideshow_title);
}
private void pickContacts(int mode, int requestCode) {
Intent intent = new Intent(INTENT_MULTI_PICK_ACTION, Contacts.CONTENT_URI);
intent.putExtra(MultiPickContactsActivity.MODE, mode);
startActivityForResult(intent, requestCode);
}
private final Handler mAttachmentEditorHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case AttachmentEditor.MSG_EDIT_SLIDESHOW: {
editSlideshow();
break;
}
case AttachmentEditor.MSG_SEND_SLIDESHOW: {
if (isPreparedForSending()) {
ComposeMessageActivity.this.confirmSendMessageIfNeeded();
}
break;
}
case AttachmentEditor.MSG_VIEW_IMAGE:
case AttachmentEditor.MSG_PLAY_VIDEO:
case AttachmentEditor.MSG_PLAY_AUDIO:
case AttachmentEditor.MSG_PLAY_SLIDESHOW:
case AttachmentEditor.MSG_VIEW_VCARD:
if (mWorkingMessage.getSlideshow() != null) {
viewMmsMessageAttachment(msg.what);
}
break;
case AttachmentEditor.MSG_REPLACE_IMAGE:
case AttachmentEditor.MSG_REPLACE_VIDEO:
case AttachmentEditor.MSG_REPLACE_AUDIO:
case AttachmentEditor.MSG_REPLACE_VCARD:
if (mAttachmentSelector.getVisibility() == View.VISIBLE
&& mIsReplaceAttachment) {
mAttachmentSelector.setVisibility(View.GONE);
} else {
showAttachmentSelector(true);
Toast.makeText(ComposeMessageActivity.this,
R.string.replace_current_attachment, Toast.LENGTH_SHORT).show();
}
break;
case AttachmentEditor.MSG_REMOVE_ATTACHMENT:
// Update the icon state in attachment selector.
if (mAttachmentSelector.getVisibility() == View.VISIBLE
&& !mIsReplaceAttachment) {
showAttachmentSelector(true);
}
mWorkingMessage.removeAttachment(true);
break;
default:
break;
}
}
};
private void viewMmsMessageAttachment(final int requestCode) {
SlideshowModel slideshow = mWorkingMessage.getSlideshow();
if (slideshow == null) {
throw new IllegalStateException("mWorkingMessage.getSlideshow() == null");
}
if (slideshow.isSimple()) {
MessageUtils.viewSimpleSlideshow(this, slideshow);
} else {
// The user wants to view the slideshow. That requires us to persist the slideshow to
// disk as a PDU in saveAsMms. This code below does that persisting in a background
// task. If the task takes longer than a half second, a progress dialog is displayed.
// Once the PDU persisting is done, another runnable on the UI thread get executed to
// start the SlideshowActivity.
getAsyncDialog().runAsync(new Runnable() {
@Override
public void run() {
// This runnable gets run in a background thread.
mTempMmsUri = mWorkingMessage.saveAsMms(false);
}
}, new Runnable() {
@Override
public void run() {
// Once the above background thread is complete, this runnable is run
// on the UI thread.
if (mTempMmsUri == null) {
return;
}
SlideshowModel slideshowModel = mWorkingMessage.getSlideshow();
if (requestCode == AttachmentEditor.MSG_PLAY_AUDIO &&
(slideshowModel != null) && slideshowModel.isSimpleAudio()) {
MediaModel mm = slideshowModel.get(0).getAudio();
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
intent.setDataAndType(mm.getUri(), mm.getContentType());
startActivityForResult(intent, requestCode);
return;
}
MessageUtils.launchSlideshowActivity(ComposeMessageActivity.this, mTempMmsUri,
requestCode);
}
}, R.string.building_slideshow_title);
}
}
private final Handler mMessageListItemHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
MessageItem msgItem = (MessageItem) msg.obj;
if (msgItem != null) {
switch (msg.what) {
case MessageListItem.MSG_LIST_DETAILS:
showMessageDetails(msgItem);
break;
case MessageListItem.MSG_LIST_EDIT:
editMessageItem(msgItem);
drawBottomPanel();
break;
case MessageListItem.MSG_LIST_RESEND:
resendMessage(msgItem);
break;
case MessageListItem.MSG_LIST_PLAY:
switch (msgItem.mAttachmentType) {
case WorkingMessage.IMAGE:
case WorkingMessage.VIDEO:
try {
Intent intent = new Intent(getContext(),
PlayVideoOrPicActivity.class);
intent.putExtra(PlayVideoOrPicActivity.VIDEO_PIC_TYPE,
msgItem.mAttachmentType);
intent.putExtra(PlayVideoOrPicActivity.VIDEO_PIC_PATH,
MessageUtils.getPath(getContext(), msgItem));
startActivity(intent);
} catch (IOException e) {
Log.i(TAG,Log.getStackTraceString(e));
}
break;
case WorkingMessage.AUDIO:
case WorkingMessage.VCARD:
case WorkingMessage.SLIDESHOW:
MessageUtils.viewMmsMessageAttachment(ComposeMessageActivity.this,
msgItem.mMessageUri, msgItem.mSlideshow,
getAsyncDialog());
break;
}
break;
default:
Log.w(TAG, "Unknown message: " + msg.what);
return;
}
}
}
};
private boolean showMessageDetails(MessageItem msgItem) {
Cursor cursor = mMsgListAdapter.getCursorForItem(msgItem);
if (cursor == null) {
return false;
}
int subjectSize = (msgItem.mSubject == null) ? 0 : msgItem.mSubject.getBytes().length;
int messageSize = msgItem.mMessageSize + subjectSize;
if (DEBUG) {
Log.v(TAG,"showMessageDetails subjectSize = " + subjectSize);
Log.v(TAG,"showMessageDetails messageSize = " + messageSize);
}
String messageDetails = MessageUtils.getMessageDetails(
ComposeMessageActivity.this, cursor, messageSize);
mMsgDetailDialog = new AlertDialog.Builder(ComposeMessageActivity.this)
.setTitle(R.string.message_details_title)
.setMessage(messageDetails)
.setCancelable(true)
.show();
return true;
}
private final OnKeyListener mSubjectKeyListener = new OnKeyListener() {
@Override
public boolean onKey(View v, int keyCode, KeyEvent event) {
if (event.getAction() != KeyEvent.ACTION_DOWN) {
return false;
}
// When the subject editor is empty, press "DEL" to hide the input field.
if ((keyCode == KeyEvent.KEYCODE_DEL) && (mSubjectTextEditor.length() == 0)) {
showSubjectEditor(false);
mWorkingMessage.setSubject(null, true);
updateSendButtonState();
return true;
}
return false;
}
};
/**
* Return the messageItem associated with the type ("mms" or "sms") and message id.
* @param type Type of the message: "mms" or "sms"
* @param msgId Message id of the message. This is the _id of the sms or pdu row and is
* stored in the MessageItem
* @param createFromCursorIfNotInCache true if the item is not found in the MessageListAdapter's
* cache and the code can create a new MessageItem based on the position of the current cursor.
* If false, the function returns null if the MessageItem isn't in the cache.
* @return MessageItem or null if not found and createFromCursorIfNotInCache is false
*/
private MessageItem getMessageItem(String type, long msgId,
boolean createFromCursorIfNotInCache) {
return mMsgListAdapter.getCachedMessageItem(type, msgId,
createFromCursorIfNotInCache ? mMsgListAdapter.getCursor() : null);
}
private boolean isCursorValid() {
// Check whether the cursor is valid or not.
Cursor cursor = mMsgListAdapter.getCursor();
if (cursor.isClosed() || cursor.isBeforeFirst() || cursor.isAfterLast()) {
Log.e(TAG, "Bad cursor.", new RuntimeException());
return false;
}
return true;
}
private void resetCounter() {
mTextCounter.setText("");
mTextCounter.setVisibility(View.GONE);
if (mShowTwoButtons) {
mTextCounterSec.setText("");
mTextCounterSec.setVisibility(View.GONE);
}
}
private AsyncTask<String, Void, int[]> getAsyncTask() {
AsyncTask<String, Void, int[]> asyncTask =
new AsyncTask<String, Void, int[]>(){
@Override
protected int[] doInBackground(String... params) {
Log.d(TAG, " doInBackground Thread id: "+Thread.currentThread().getId());
if (params != null && params.length > 0) {
Log.d("count","doInBackground: " + params[0]);
return SmsMessage.calculateLength(params[0], false);
}
return null;
}
@Override
protected void onPostExecute(int[] result) {
if (result == null || result.length < 3)
return;
/* SmsMessage.calculateLength returns an int[4] with:
* int[0] being the number of SMS's required,
* int[1] the number of code units used,
* int[2] is the number of code units remaining until the next message.
* int[3] is the encoding type that should be used for the message.
*/
int msgCount = result[0];
int remainingInCurrentMessage = result[2];
Log.d("count", "onPostExecute: msgCount: " + msgCount
+ "; remainingInCurrentMessage: " + remainingInCurrentMessage);
if (!MmsConfig.getMultipartSmsEnabled()) {
// The provider doesn't support multi-part sms's
// so as soon as the user types an sms longer than one segment,
// we have to turn the message into an mms.
mWorkingMessage.setLengthRequiresMms(msgCount > 1, true);
} else {
int threshold =
MmsConfig.getSmsToMmsTextThreshold(ComposeMessageActivity.this);
mWorkingMessage.setLengthRequiresMms(threshold > 0
&& msgCount > threshold, true);
}
// Show the counter only if:
// - We are not in MMS mode
// - We are going to send more than one message OR we are getting close
boolean showCounter = false;
if (!mWorkingMessage.requiresMms() &&
(msgCount > 1 ||
remainingInCurrentMessage
<= CHARS_REMAINING_BEFORE_COUNTER_SHOWN)) {
showCounter = true;
}
if (mShowTwoButtons) {
showTwoSmsOrMmsSendButton(mWorkingMessage.requiresMms());
} else {
showSmsOrMmsSendButton(mWorkingMessage.requiresMms());
}
if (showCounter) {
// Update the remaining characters and number of messages required.
String counterText = msgCount > 1 ? remainingInCurrentMessage
+ " / " + msgCount : String.valueOf(remainingInCurrentMessage);
mTextCounter.setText(counterText);
mTextCounter.setVisibility(View.VISIBLE);
if (mShowTwoButtons) {
mTextCounterSec.setText(counterText);
mTextCounterSec.setVisibility(View.VISIBLE);
}
} else {
mTextCounter.setVisibility(View.GONE);
if (mShowTwoButtons) {
mTextCounterSec.setVisibility(View.GONE);
}
}
}
};
return asyncTask;
}
private void updateCounter(CharSequence text, int start, int before, int count) {
WorkingMessage workingMessage = mWorkingMessage;
if (workingMessage.requiresMms()) {
// If we're not removing text (i.e. no chance of converting back to SMS
// because of this change) and we're in MMS mode, just bail out since we
// then won't have to calculate the length unnecessarily.
final boolean textRemoved = (before > count);
if (!textRemoved) {
if (mShowTwoButtons) {
showTwoSmsOrMmsSendButton(workingMessage.requiresMms());
} else {
showSmsOrMmsSendButton(workingMessage.requiresMms());
}
return;
}
}
if (text != null && text.length() > 0) {
getAsyncTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR, text.toString());
}
}
@Override
public void startActivityForResult(Intent intent, int requestCode)
{
// requestCode >= 0 means the activity in question is a sub-activity.
if (requestCode >= 0) {
mWaitingForSubActivity = true;
}
// The camera and other activities take a long time to hide the keyboard so we pre-hide
// it here. However, if we're opening up the quick contact window while typing, don't
// mess with the keyboard.
if (mIsKeyboardOpen && !QuickContact.ACTION_QUICK_CONTACT.equals(intent.getAction())) {
hideKeyboard();
}
super.startActivityForResult(intent, requestCode);
}
private void showConvertToMmsToast() {
Toast.makeText(this, R.string.converting_to_picture_message, Toast.LENGTH_SHORT).show();
}
private void showConvertToSmsToast() {
Toast.makeText(this, R.string.converting_to_text_message, Toast.LENGTH_SHORT).show();
}
private class DeleteMessageListener implements OnClickListener {
private final MessageItem mMessageItem;
public DeleteMessageListener(MessageItem messageItem) {
mMessageItem = messageItem;
}
@Override
public void onClick(DialogInterface dialog, int whichButton) {
dialog.dismiss();
new AsyncTask<Void, Void, Void>() {
protected Void doInBackground(Void... none) {
if (mMessageItem.isMms()) {
WorkingMessage.removeThumbnailsFromCache(mMessageItem.getSlideshow());
MmsApp.getApplication().getPduLoaderManager()
.removePdu(mMessageItem.mMessageUri);
// Delete the message *after* we've removed the thumbnails because we
// need the pdu and slideshow for removeThumbnailsFromCache to work.
}
Boolean deletingLastItem = false;
Cursor cursor = mMsgListAdapter != null ? mMsgListAdapter.getCursor() : null;
if (cursor != null) {
cursor.moveToLast();
long msgId = cursor.getLong(COLUMN_ID);
deletingLastItem = msgId == mMessageItem.mMsgId;
}
mBackgroundQueryHandler.startDelete(DELETE_MESSAGE_TOKEN,
deletingLastItem, mMessageItem.mMessageUri,
mMessageItem.mLocked ? null : "locked=0", null);
return null;
}
}.execute();
}
}
private class DiscardDraftListener implements OnClickListener {
@Override
public void onClick(DialogInterface dialog, int whichButton) {
mWorkingMessage.discard();
dialog.dismiss();
finish();
}
}
private class SendIgnoreInvalidRecipientListener implements OnClickListener {
private int mSubscription = MessageUtils.SUB_INVALID;
public SendIgnoreInvalidRecipientListener(int subscription) {
mSubscription = subscription;
}
@Override
public void onClick(DialogInterface dialog, int whichButton) {
if (isMmsWithMobileDataOff(mWorkingMessage.requiresMms(), mSubscription)) {
showMobileDataDisabledDialog(mSubscription);
} else if ((MessageUtils.getPhoneCount()) > 1) {
int subId = (mSubscription == MessageUtils.SUB_INVALID)
? SubscriptionManager.getDefaultSmsSubscriptionId()
: mSubscription;
sendMsimMessage(true, subId);
} else {
sendMessage(true);
}
dialog.dismiss();
}
}
private class CancelSendingListener implements OnClickListener {
@Override
public void onClick(DialogInterface dialog, int whichButton) {
if (isRecipientsEditorVisible()) {
mRecipientsEditor.requestFocus();
}
dialog.dismiss();
}
}
private void dismissMsimDialog() {
if (mMsimDialog != null && mMsimDialog.isShowing()) {
mMsimDialog.dismiss();
}
}
private void launchMsimDialog(final boolean bCheckEcmMode, final boolean isMms) {
if (mMsimDialog != null && mMsimDialog.isShowing()) {
mMsimDialog.dismiss();
}
LogTag.debugD("LaunchMsimDialog");
AlertDialog.Builder builder = new AlertDialog.Builder(ComposeMessageActivity.this);
LayoutInflater inflater = (LayoutInflater) getSystemService(LAYOUT_INFLATER_SERVICE);
View layout = inflater.inflate(R.layout.multi_sim_sms_sender,
(ViewGroup)findViewById(R.id.layout_root));
builder.setView(layout);
builder.setOnKeyListener(new DialogInterface.OnKeyListener() {
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_BACK: {
dismissMsimDialog();
return true;
}
case KeyEvent.KEYCODE_SEARCH: {
return true;
}
}
return false;
}
}
);
builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dismissMsimDialog();
}
});
ContactList recipients = isRecipientsEditorVisible() ?
mRecipientsEditor.constructContactsFromInput(false) : getRecipients();
builder.setTitle(getResources().getString(R.string.to_address_label)
+ recipients.formatNamesAndNumbers(","));
mMsimDialog = builder.create();
mMsimDialog.setCanceledOnTouchOutside(true);
int[] smsBtnIds = {R.id.BtnSimOne, R.id.BtnSimTwo, R.id.BtnSimThree};
int phoneCount = MessageUtils.getPhoneCount();
Button[] smsBtns = new Button[phoneCount];
List<SubscriptionInfo> subInfoList = SubscriptionManager.from(
getApplicationContext()).getActiveSubscriptionInfoList();
String displayName = null;
for (int i = 0; i < phoneCount; i++) {
final int phoneId = i;
smsBtns[i] = (Button) layout.findViewById(smsBtnIds[i]);
smsBtns[i].setVisibility(View.VISIBLE);
if(subInfoList != null) {
displayName = "SIM " + (phoneId + 1);
for (SubscriptionInfo info : subInfoList) {
if (info.getSimSlotIndex() == phoneId) {
displayName = (phoneId + 1) + ": " + info.getDisplayName().toString();
break;
}
}
}
smsBtns[i].setText(displayName);
smsBtns[i].setOnClickListener(
new View.OnClickListener() {
public void onClick(View v) {
dismissMsimDialog();
int[] subIds = SubscriptionManager.getSubId(phoneId);
if ((subIds == null) || (subIds.length <= 0)) {
Toast.makeText(ComposeMessageActivity.this,
getString(R.string.send_via_invalid_sub),
Toast.LENGTH_LONG).show();
return;
}
int subId = subIds[0];
LogTag.debugD("LaunchMsimDialog: subscription selected " + subId);
if (!SubscriptionManager.from(getApplicationContext())
.isActiveSubId(subId)) {
Toast.makeText(ComposeMessageActivity.this,
getString(R.string.send_via_invalid_sub),
Toast.LENGTH_LONG).show();
} else if (isMmsWithMobileDataOff(isMms, subId)) {
showMobileDataDisabledDialog(subId);
} else {
sendMsimMessage(bCheckEcmMode, subId);
}
}
});
}
mMsimDialog.show();
}
private void sendMsimMessage(boolean bCheckEcmMode, int subscription) {
LogTag.debugD("sendMsimMessage subscription: " + subscription);
mWorkingMessage.setWorkingMessageSub(subscription);
sendMessage(bCheckEcmMode);
}
private boolean isLTEOnlyMode() {
try {
int tddOnly = Settings.Global.getInt(getContentResolver(), LTE_DATA_ONLY_KEY);
int network = Settings.Global.getInt(getContentResolver(),
ConstantsWrapper.SettingsGlobal.PREFERRED_NETWORK_MODE);
return network == ConstantsWrapper.RIL.NETWORK_MODE_LTE_ONLY
&& tddOnly == LTE_DATA_ONLY_MODE;
} catch (SettingNotFoundException snfe) {
Log.w(TAG, "isLTEOnlyMode: Could not find PREFERRED_NETWORK_MODE!");
}
return false;
}
private boolean isLTEOnlyMode(int subscription) {
try {
int tddOnly = TelephonyManagerWrapper.getIntAtIndex(getContentResolver(),
LTE_DATA_ONLY_KEY, subscription);
int network = TelephonyManagerWrapper.getIntAtIndex(getContentResolver(),
ConstantsWrapper.SettingsGlobal.PREFERRED_NETWORK_MODE, subscription);
return network == ConstantsWrapper.RIL.NETWORK_MODE_LTE_ONLY
&& tddOnly == LTE_DATA_ONLY_MODE;
} catch (SettingNotFoundException snfe) {
Log.w(TAG, "isLTEOnlyMode: Could not find PREFERRED_NETWORK_MODE!");
}
return false;
}
private void showDisableLTEOnlyDialog(int subscription) {
Intent intent = new Intent();
intent.setAction(INTENT_ACTION_LTE_DATA_ONLY_DIALOG);
intent.putExtra(ConstantsWrapper.Phone.SLOT_KEY, subscription);
startActivity(intent);
}
private boolean isMmsApnUnmetered(int subId) {
try {
boolean isRoaming = TelephonyManagerWrapper.isNetworkRoaming(this, subId);
Log.d(TAG, "isMmsApnUnmetered: isRoaming=" + isRoaming);
String carrierConfig;
if (isRoaming) {
carrierConfig = CarrierConfigManager.KEY_CARRIER_METERED_ROAMING_APN_TYPES_STRINGS;
} else {
carrierConfig = CarrierConfigManager.KEY_CARRIER_METERED_APN_TYPES_STRINGS;
}
CarrierConfigManager configManager = (CarrierConfigManager) getSystemService(
Context.CARRIER_CONFIG_SERVICE);
if (configManager == null) {
Log.e(TAG, "Carrier config service is not available");
return false;
}
PersistableBundle b = configManager.getConfigForSubId(subId);
if (b == null) {
Log.e(TAG, "Can't get the config. subId = " + subId);
return false;
}
String[] meteredApnTypes = b.getStringArray(carrierConfig);
if (meteredApnTypes == null) {
Log.e(TAG, carrierConfig + " is not available. " + "subId = " + subId);
return false;
}
HashSet<String> meteredApnSet = new HashSet<>(Arrays.asList(meteredApnTypes));
Log.d(TAG, "For subId = " + subId + ", metered APN types are "
+ Arrays.toString(meteredApnSet.toArray()));
if (meteredApnSet.contains(ConstantsWrapper.Phone.APN_TYPE_ALL)) {
Log.d(TAG, "All APN types are metered.");
return false;
}
if (meteredApnSet.contains(ConstantsWrapper.Phone.APN_TYPE_MMS)) {
Log.d(TAG, "mms is metered.");
return false;
}
Log.d(TAG, "mms is not metered.");
return true;
} catch (Exception e) {
Log.d(TAG, "isMmsApnUnmetered has exception: " + e);
return false;
}
}
private void confirmSendMessageIfNeeded(int subscription) {
if (isLTEOnlyMode(subscription)) {
showDisableLTEOnlyDialog(subscription);
return;
}
boolean isMms = mWorkingMessage.requiresMms();
if (!isRecipientsEditorVisible()) {
if (isMmsWithMobileDataOff(isMms, subscription)) {
showMobileDataDisabledDialog(subscription);
} else {
sendMsimMessage(true, subscription);
}
return;
}
if (mRecipientsEditor.hasInvalidRecipient(isMms)) {
showInvalidRecipientDialog(subscription);
} else if (isMmsWithMobileDataOff(isMms, subscription)) {
showMobileDataDisabledDialog(subscription);
} else {
if (!TextUtils.isEmpty(getString(R.string.mms_recipient_Limit))
&& isMms
&& checkForMmsRecipients(getString(R.string.mms_recipient_Limit), true)) {
return;
}
// The recipients editor is still open. Make sure we use what's showing there
// as the destination.
ContactList contacts = mRecipientsEditor.constructContactsFromInput(false);
mDebugRecipients = contacts.serialize();
sendMsimMessage(true, subscription);
}
}
private void confirmSendMessageIfNeeded() {
TelephonyManager tm = (TelephonyManager)getSystemService(
Context.TELEPHONY_SERVICE);
boolean isMultiSim = TelephonyManagerWrapper.isMultiSimEnabled(tm);
int slot = SubscriptionManagerWrapper.getSlotId(
SmsManager.getDefault().getDefaultSmsSubscriptionId());
if ((isMultiSim && isLTEOnlyMode(slot))
|| (!isMultiSim && isLTEOnlyMode())) {
showDisableLTEOnlyDialog(slot);
LogTag.debugD("return for disable LTEOnly");
return;
}
boolean isMms = mWorkingMessage.requiresMms();
int defaultSubId = SubscriptionManager.getDefaultSmsSubscriptionId();
if (!isRecipientsEditorVisible()) {
if (isMultiSim) {
if ((tm.getPhoneCount()) > 1 &&
MessageUtils.isMsimIccCardActive()) {
if(MessageUtils.isSMSPromptEnabled(ComposeMessageActivity.this)) {
launchMsimDialog(true, isMms);
} else {
sendMsimMessageNotPrompt(true, isMms, defaultSubId);
}
} else {
sendMsimMessageNotPrompt(true, isMms, defaultSubId);
}
} else if (isMmsWithMobileDataOff(isMms, defaultSubId)) {
showMobileDataDisabledDialog();
} else {
sendMessage(true);
}
return;
}
if (mRecipientsEditor.hasInvalidRecipient(isMms)) {
showInvalidRecipientDialog();
} else if (TelephonyManager.getDefault().isMultiSimEnabled()) {
if ((tm.getPhoneCount()) > 1 &&
MessageUtils.isMsimIccCardActive()) {
if(MessageUtils.isSMSPromptEnabled(ComposeMessageActivity.this)) {
launchMsimDialog(true, isMms);
} else {
sendMsimMessageNotPrompt(true, isMms, defaultSubId);
}
} else {
sendMsimMessageNotPrompt(true, isMms, defaultSubId);
}
} else if (isMmsWithMobileDataOff(isMms, defaultSubId)) {
showMobileDataDisabledDialog(defaultSubId);
} else {
if (!TextUtils.isEmpty(getString(R.string.mms_recipient_Limit))
&& isMms
&& checkForMmsRecipients(getString(R.string.mms_recipient_Limit), true)) {
return;
}
// The recipients editor is still open. Make sure we use what's showing there
// as the destination.
ContactList contacts = mRecipientsEditor.constructContactsFromInput(false);
mDebugRecipients = contacts.serialize();
sendMessage(true);
}
}
private void showInvalidRecipientDialog() {
showInvalidRecipientDialog(MessageUtils.SUB_INVALID);
}
private void showInvalidRecipientDialog(int subscription) {
boolean isMms = mWorkingMessage.requiresMms();
if (mRecipientsEditor.getValidRecipientsCount(isMms)
> MessageUtils.ALL_RECIPIENTS_INVALID) {
String title = getResourcesString(R.string.has_invalid_recipient,
mRecipientsEditor.formatInvalidNumbers(isMms));
mInvalidRecipientDialog = new AlertDialog.Builder(this)
.setTitle(title)
.setMessage(R.string.invalid_recipient_message)
.setPositiveButton(R.string.try_to_send,
new SendIgnoreInvalidRecipientListener(subscription))
.setNegativeButton(R.string.no, new CancelSendingListener())
.show();
} else {
mInvalidRecipientDialog = new AlertDialog.Builder(this)
.setTitle(R.string.cannot_send_message)
.setMessage(R.string.cannot_send_message_reason)
.setPositiveButton(R.string.yes, new CancelSendingListener())
.show();
}
}
private void sendMsimMessageNotPrompt(boolean bCheckEcmMode, boolean isMms, int subId) {
if (isMmsWithMobileDataOff(isMms, subId)) {
showMobileDataDisabledDialog(subId);
} else {
sendMsimMessage(bCheckEcmMode, subId);
}
}
private boolean isMmsWithMobileDataOff(boolean isMms, int subscription) {
LogTag.debug("isMmsWithMobileDataOff subscription: " + subscription);
return isMms && !mSendMmsSupportViaWiFi && isMmsApnUnmetered(subscription)
&& !MessageUtils.isMobileDataEnabled(getApplicationContext(), subscription);
}
private void showMobileDataDisabledDialog() {
showMobileDataDisabledDialog(MessageUtils.SUB_INVALID);
}
private void showMobileDataDisabledDialog(final int subscription) {
LogTag.debug("showMobileDataDisabledDialog subscription: " + subscription);
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.send);
builder.setMessage(getString(R.string.mobile_data_disable));
builder.setPositiveButton(R.string.yes, new OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
if ((MessageUtils.getPhoneCount()) > 1
&& (subscription != MessageUtils.SUB_INVALID)) {
sendMsimMessage(true, subscription);
} else {
sendMessage(true);
}
dialog.dismiss();
}
});
builder.setNegativeButton(R.string.no, null);
builder.show();
}
private final TextWatcher mRecipientsWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// This is a workaround for bug 1609057. Since onUserInteraction() is
// not called when the user touches the soft keyboard, we pretend it was
// called when textfields changes. This should be removed when the bug
// is fixed.
onUserInteraction();
}
@Override
public void afterTextChanged(Editable s) {
// Bug 1474782 describes a situation in which we send to
// the wrong recipient. We have been unable to reproduce this,
// but the best theory we have so far is that the contents of
// mRecipientList somehow become stale when entering
// ComposeMessageActivity via onNewIntent(). This assertion is
// meant to catch one possible path to that, of a non-visible
// mRecipientsEditor having its TextWatcher fire and refreshing
// mRecipientList with its stale contents.
if (!isRecipientsEditorVisible()) {
IllegalStateException e = new IllegalStateException(
"afterTextChanged called with invisible mRecipientsEditor");
// Make sure the crash is uploaded to the service so we
// can see if this is happening in the field.
Log.w(TAG,
"RecipientsWatcher: afterTextChanged called with invisible mRecipientsEditor");
return;
}
if (recipientCount() <= 1 && !TextUtils.isEmpty(s.toString())
&& !mRecipientsEditor.containsMultiContacts(new SpannableString(s + "1"))) {
mClearButton.setVisibility(View.VISIBLE);
mRecipientsPicker.setVisibility(View.GONE);
} else {
mClearButton.setVisibility(View.GONE);
mRecipientsPicker.setVisibility(View.VISIBLE);
}
mWorkingMessage.setWorkingRecipients(mRecipientsEditor.getNumbers());
mWorkingMessage.setHasEmail(mRecipientsEditor.containsEmail(), true);
checkForTooManyRecipients();
// If pick recipients from Contacts,
// then only update title once when process finished
if (mIsProcessPickedRecipients) {
return;
}
if (mRecipientsPickList != null) {
// Update UI with mRecipientsPickList, which is picked from
// People.
updateTitle(mRecipientsPickList);
mRecipientsPickList = null;
} else {
updateTitleForRecipientsChange(s);
}
// If we have gone to zero recipients, disable send button.
updateSendButtonState();
}
};
private void updateTitleForRecipientsChange(Editable s) {
// If we have gone to zero recipients, we need to update the title.
if (TextUtils.isEmpty(s.toString().trim())) {
constructContactAndUpdateTitle();
}
// Walk backwards in the text box, skipping spaces. If the last
// character is a comma, update the title bar.
for (int pos = s.length() - 1; pos >= 0; pos--) {
char c = s.charAt(pos);
if (c == ' ')
continue;
if (c == ',') {
constructContactAndUpdateTitle();
}
break;
}
}
private void constructContactAndUpdateTitle() {
ContactList contacts = mRecipientsEditor.constructContactsFromInput(false);
updateTitle(contacts);
}
private boolean checkForMmsRecipients(String strLimit, boolean isMmsSend) {
if (mWorkingMessage.requiresMms()) {
int recipientLimit = Integer.parseInt(strLimit);
final int currentRecipients = recipientCount();
if (recipientLimit < currentRecipients) {
if (currentRecipients != mLastRecipientCount || isMmsSend) {
mLastRecipientCount = currentRecipients;
String tooManyMsg = getString(R.string.too_many_recipients, currentRecipients,
recipientLimit);
Toast.makeText(ComposeMessageActivity.this,
tooManyMsg, Toast.LENGTH_LONG).show();
}
return true;
} else {
mLastRecipientCount = currentRecipients;
}
}
return false;
}
private void checkForTooManyRecipients() {
if (!TextUtils.isEmpty(getString(R.string.mms_recipient_Limit))
&& checkForMmsRecipients(getString(R.string.mms_recipient_Limit), false)) {
return;
}
final int recipientLimit = MmsConfig.getRecipientLimit();
if (recipientLimit != Integer.MAX_VALUE && recipientLimit > 0) {
final int recipientCount = recipientCount();
boolean tooMany = recipientCount > recipientLimit;
int index = mRecipientsEditor.getSelectionStart();
Editable editable = mRecipientsEditor.getText();
if (recipientCount != mLastRecipientCount) {
// Don't warn the user on every character they type when they're over the limit,
// only when the actual # of recipients changes.
mLastRecipientCount = recipientCount;
if (tooMany && (index > 0)) {
String tooManyMsg = getString(R.string.too_many_recipients, recipientCount,
recipientLimit);
Toast.makeText(ComposeMessageActivity.this,
tooManyMsg, Toast.LENGTH_LONG).show();
editable.delete(index-1 , index);
}
}
}
}
private final class RecipientsMenuClickListener implements MenuItem.OnMenuItemClickListener {
private final Contact mRecipient;
RecipientsMenuClickListener(Contact recipient) {
mRecipient = recipient;
}
@Override
public boolean onMenuItemClick(MenuItem item) {
if (null == mRecipient) {
return false;
}
switch (item.getItemId()) {
// Context menu handlers for the recipients editor.
case MENU_VIEW_CONTACT: {
Uri contactUri = mRecipient.getUri();
Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
startActivity(intent);
return true;
}
case MENU_ADD_TO_CONTACTS: {
mAddContactIntent = ConversationList.createAddContactIntent(
mRecipient.getNumber());
ComposeMessageActivity.this.startActivityForResult(mAddContactIntent,
REQUEST_CODE_ADD_CONTACT);
return true;
}
case MENU_COPY_PHONENUMBER: {
copyToClipboard(mRecipient.getNumber());
return true;
}
}
return false;
}
}
private boolean canAddToContacts(Contact contact) {
// There are some kind of automated messages, like STK messages, that we don't want
// to add to contacts. These names begin with special characters, like, "*Info".
final String name = contact.getName();
if (!TextUtils.isEmpty(contact.getNumber())) {
char c = contact.getNumber().charAt(0);
if (isSpecialChar(c)) {
return false;
}
}
if (!TextUtils.isEmpty(name)) {
char c = name.charAt(0);
if (isSpecialChar(c)) {
return false;
}
}
if (!(ContactRecipientEntryUtils.isEmailAddress(name) ||
TelephonyWrapper.Mms.isPhoneNumber(name) ||
contact.isMe())) {
return false;
}
return true;
}
private boolean isSpecialChar(char c) {
return c == '*' || c == '%' || c == '$';
}
private Uri getSelectedUriFromMessageList(ListView listView, int position) {
// If the context menu was opened over a uri, get that uri.
MessageListItem msglistItem = (MessageListItem) listView.getChildAt(position);
if (msglistItem == null) {
// FIXME: Should get the correct view. No such interface in ListView currently
// to get the view by position. The ListView.getChildAt(position) cannot
// get correct view since the list doesn't create one child for each item.
// And if setSelection(position) then getSelectedView(),
// cannot get corrent view when in touch mode.
return null;
}
TextView textView;
CharSequence text = null;
int selStart = -1;
int selEnd = -1;
//check if message sender is selected
textView = (TextView) msglistItem.getBodyTextView();
if (textView != null) {
text = textView.getText();
selStart = textView.getSelectionStart();
selEnd = textView.getSelectionEnd();
}
// Check that some text is actually selected, rather than the cursor
// just being placed within the TextView.
if (selStart != selEnd) {
int min = Math.min(selStart, selEnd);
int max = Math.max(selStart, selEnd);
URLSpan[] urls = ((Spanned) text).getSpans(min, max,
URLSpan.class);
if (urls.length == 1) {
return Uri.parse(urls[0].getURL());
}
}
//no uri was selected
return null;
}
private Uri getContactUriForEmail(String emailAddress) {
Cursor cursor = SqliteWrapper.query(this, getContentResolver(),
Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(emailAddress)),
new String[] { Email.CONTACT_ID, Contacts.DISPLAY_NAME }, null, null, null);
if (cursor != null) {
try {
while (cursor.moveToNext()) {
String name = cursor.getString(1);
if (!TextUtils.isEmpty(name)) {
return ContentUris.withAppendedId(Contacts.CONTENT_URI, cursor.getLong(0));
}
}
} finally {
cursor.close();
}
}
return null;
}
private Uri getContactUriForPhoneNumber(String phoneNumber) {
Contact contact = Contact.get(phoneNumber, false);
if (contact.existsInDatabase()) {
return contact.getUri();
}
return null;
}
private void editMessageItem(MessageItem msgItem) {
if ("sms".equals(msgItem.mType)) {
editSmsMessageItem(msgItem);
} else {
editMmsMessageItem(msgItem);
}
if (msgItem.isFailedMessage() && mMsgListAdapter.getCount() <= 1) {
// For messages with bad addresses, let the user re-edit the recipients.
initRecipientsEditor();
}
}
private void editSmsMessageItem(MessageItem msgItem) {
editSmsMessageItem(msgItem.mMsgId, msgItem.mBody);
}
private void editSmsMessageItem(long msgId, String msgBody) {
// When the message being edited is the only message in the conversation, the delete
// below does something subtle. The trigger "delete_obsolete_threads_pdu" sees that a
// thread contains no messages and silently deletes the thread. Meanwhile, the mConversation
// object still holds onto the old thread_id and code thinks there's a backing thread in
// the DB when it really has been deleted. Here we try and notice that situation and
// clear out the thread_id. Later on, when Conversation.ensureThreadId() is called, we'll
// create a new thread if necessary.
synchronized(mConversation) {
if (mConversation.getMessageCount() <= 1) {
mConversation.clearThreadId();
MessagingNotification.setCurrentlyDisplayedThreadId(
MessagingNotification.THREAD_NONE);
}
}
// Delete the old undelivered SMS and load its content.
Uri uri = ContentUris.withAppendedId(Sms.CONTENT_URI, msgId);
int count = SqliteWrapper.delete(ComposeMessageActivity.this,
mContentResolver, uri, null, null);
mWorkingMessage.setText(msgBody);
// if the ListView only has one message and delete the message success
// the uri of conversation will be null, so it can't qurey info from DB,
// so the mMsgListAdapter should change Cursor to null
if (count > 0) {
if (mMsgListAdapter.getCount() == MSG_ONLY_ONE_FAIL_LIST_ITEM) {
mMsgListAdapter.changeCursor(null);
}
}
}
private void editMmsMessageItem(MessageItem msgItem) {
editMmsMessageItem(msgItem.mMessageUri, msgItem.mSubject);
}
private void editMmsMessageItem(Uri uri, String subject) {
// Load the selected message in as the working message.
WorkingMessage newWorkingMessage = WorkingMessage.load(this, uri);
if (newWorkingMessage == null) {
return;
}
// Discard the current message in progress.
mWorkingMessage.discard();
mWorkingMessage = newWorkingMessage;
mWorkingMessage.setConversation(mConversation);
drawTopPanel(false);
// WorkingMessage.load() above only loads the slideshow. Set the
// subject here because we already know what it is and avoid doing
// another DB lookup in load() just to get it.
mWorkingMessage.setSubject(subject, false);
if (mWorkingMessage.hasSubject()) {
showSubjectEditor(true);
}
}
private void copyToClipboard(String str) {
ClipboardManager clipboard = (ClipboardManager)getSystemService(Context.CLIPBOARD_SERVICE);
clipboard.setPrimaryClip(ClipData.newPlainText(null, str));
}
private void resendMessage(MessageItem msgItem) {
if (MessageUtils.isSMSPromptEnabled(ComposeMessageActivity.this)) {
mWorkingMessage.setWorkingMessageSub(msgItem.mSubId);
}
if (msgItem.isMms()) {
// If it is mms, we delete current mms and use current mms
// uri to create new working message object.
WorkingMessage newWorkingMessage = WorkingMessage.load(this, msgItem.mMessageUri);
if (newWorkingMessage == null)
return;
// Discard the current message in progress.
mWorkingMessage.discard();
mWorkingMessage = newWorkingMessage;
mWorkingMessage.setConversation(mConversation);
mWorkingMessage.setSubject(msgItem.mSubject, false);
} else {
if (getRecipients().size() > 1) {
// If the number is more than one when send sms, there will show serveral msg items
// the recipient of msg item is not equal with recipients of conversation
// so we should record the recipient of this msg item.
mWorkingMessage.setResendMultiRecipients(true);
mResendSmsRecipient = msgItem.mAddress;
}
editSmsMessageItem(msgItem);
}
sendMessage(true);
}
private boolean isAllowForwardMessage(MessageItem msgItem) {
if (msgItem.getSlideshow() == null) {
return false;
}
int messageSize = msgItem.getSlideshow().getTotalMessageSize();
int smilSize = msgItem.getSlideshow().getSMILSize();
int forwardStrSize = getString(R.string.forward_prefix).getBytes().length;
int subjectSize = (msgItem.mSubject == null) ? 0 : msgItem.mSubject.getBytes().length;
int totalSize = messageSize + forwardStrSize + subjectSize + smilSize;
if (DEBUG) {
Log.e(TAG,"isAllowForwardMessage messageSize = "+ messageSize
+ ", forwardStrSize = "+forwardStrSize+ ", subjectSize = "+subjectSize
+ ", totalSize = " + totalSize);
}
return totalSize <= (MmsConfig.getMaxMessageSize() - SlideshowModel.SLIDESHOW_SLOP);
}
/**
* Looks to see if there are any valid parts of the attachment that can be copied to a SD card.
* @param msgId
*/
private boolean haveSomethingToCopyToSDCard(long msgId) {
PduBody body = null;
try {
body = SlideshowModel.getPduBody(this,
ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "haveSomethingToCopyToSDCard can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
boolean result = false;
int partNum = body.getPartsNum();
for(int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
String type = new String(part.getContentType());
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("[CMA] haveSomethingToCopyToSDCard: part[" + i + "] contentType=" + type);
}
if (ContentType.isImageType(type) || ContentType.isVideoType(type) ||
ContentType.isAudioType(type) || DrmUtils.isDrmType(type)) {
result = true;
break;
}
}
return result;
}
/**
* Copies media from an Mms to the DrmProvider
* @param msgId
*/
private boolean saveRingtone(long msgId) {
boolean result = true;
PduBody body = null;
try {
body = SlideshowModel.getPduBody(this,
ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "copyToDrmProvider can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
int partNum = body.getPartsNum();
for(int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
String type = new String(part.getContentType());
if (DrmUtils.isDrmType(type)) {
// All parts (but there's probably only a single one) have to be successful
// for a valid result.
result &= copyPart(part, Long.toHexString(msgId));
}
}
return result;
}
/**
* Returns true if any part is drm'd audio with ringtone rights.
* @param msgId
* @return true if one of the parts is drm'd audio with rights to save as a ringtone.
*/
private boolean isDrmRingtoneWithRights(long msgId) {
PduBody body = null;
try {
body = SlideshowModel.getPduBody(this,
ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "isDrmRingtoneWithRights can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
int partNum = body.getPartsNum();
for (int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
String type = new String(part.getContentType());
if (DrmUtils.isDrmType(type)) {
String mimeType = MmsApp.getApplication().getDrmManagerClient()
.getOriginalMimeType(part.getDataUri());
if (ContentType.isAudioType(mimeType) && DrmUtils.haveRightsForAction(part.getDataUri(),
DrmStore.Action.RINGTONE)) {
return true;
}
}
}
return false;
}
/**
* Returns true if all drm'd parts are forwardable.
* @param msgId
* @return true if all drm'd parts are forwardable.
*/
private boolean isForwardable(long msgId) {
PduBody body = null;
try {
body = SlideshowModel.getPduBody(this,
ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "getDrmMimeType can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
int partNum = body.getPartsNum();
for (int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
String type = new String(part.getContentType());
if (DrmUtils.isDrmType(type) && !DrmUtils.haveRightsForAction(part.getDataUri(),
DrmStore.Action.TRANSFER)) {
return false;
}
}
return true;
}
private int getDrmMimeMenuStringRsrc(long msgId) {
if (isDrmRingtoneWithRights(msgId)) {
return R.string.save_ringtone;
}
return 0;
}
private int getDrmMimeSavedStringRsrc(long msgId, boolean success) {
if (isDrmRingtoneWithRights(msgId)) {
return success ? R.string.saved_ringtone : R.string.saved_ringtone_fail;
}
return 0;
}
/**
* Copies media from an Mms to the "download" directory on the SD card. If any of the parts
* are audio types, drm'd or not, they're copied to the "Ringtones" directory.
* @param msgId
*/
private boolean copyMedia(long msgId) {
boolean result = true;
PduBody body = null;
try {
body = SlideshowModel.getPduBody(this,
ContentUris.withAppendedId(Mms.CONTENT_URI, msgId));
} catch (MmsException e) {
Log.e(TAG, "copyMedia can't load pdu body: " + msgId);
}
if (body == null) {
return false;
}
int partNum = body.getPartsNum();
for(int i = 0; i < partNum; i++) {
PduPart part = body.getPart(i);
// all parts have to be successful for a valid result.
result &= copyPart(part, Long.toHexString(msgId));
}
return result;
}
private boolean copyPart(PduPart part, String fallback) {
Uri uri = part.getDataUri();
String type = new String(part.getContentType());
boolean isDrm = DrmUtils.isDrmType(type);
if (isDrm) {
type = MmsApp.getApplication().getDrmManagerClient()
.getOriginalMimeType(part.getDataUri());
}
if (!ContentType.isImageType(type)
&& !ContentType.isVideoType(type)
&& !ContentType.isAudioType(type)
&& !(ContentType.TEXT_VCARD.toLowerCase().equals(type.toLowerCase()))
&& !(ContentType.AUDIO_OGG.toLowerCase().equals(type.toLowerCase()))) {
return true; // we only save pictures, videos, and sounds. Skip the text parts,
// the app (smil) parts, and other type that we can't handle.
// Return true to pretend that we successfully saved the part so
// the whole save process will be counted a success.
}
InputStream input = null;
FileOutputStream fout = null;
try {
input = mContentResolver.openInputStream(uri);
if (input instanceof FileInputStream) {
FileInputStream fin = (FileInputStream) input;
byte[] location = part.getName();
if (location == null) {
location = part.getFilename();
}
if (location == null) {
location = part.getContentLocation();
}
String fileName;
if (location == null) {
// Use fallback name.
fileName = fallback;
} else {
// For locally captured videos, fileName can end up being something like this:
// /mnt/sdcard/Android/data/com.android.mms/cache/.temp1.3gp
fileName = new String(location);
}
File originalFile = new File(fileName);
fileName = originalFile.getName(); // Strip the full path of where the "part" is
// stored down to just the leaf filename.
// Depending on the location, there may be an
// extension already on the name or not. If we've got audio, put the attachment
// in the Ringtones directory.
String dir = Environment.getExternalStorageDirectory() + "/"
+ (ContentType.isAudioType(type) ? Environment.DIRECTORY_RINGTONES :
Environment.DIRECTORY_DOWNLOADS) + "/";
String extension;
int index;
if ((index = fileName.lastIndexOf('.')) == -1) {
extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(type);
} else {
extension = fileName.substring(index + 1, fileName.length());
fileName = fileName.substring(0, index);
}
if (isDrm) {
extension += DrmUtils.getConvertExtension(type);
}
// Remove leading periods. The gallery ignores files starting with a period.
fileName = fileName.replaceAll("^.", "");
File file = getUniqueDestination(dir + fileName, extension);
// make sure the path is valid and directories created for this file.
File parentFile = file.getParentFile();
if (!parentFile.exists() && !parentFile.mkdirs()) {
Log.e(TAG, "[MMS] copyPart: mkdirs for " + parentFile.getPath() + " failed!");
return false;
}
fout = new FileOutputStream(file);
byte[] buffer = new byte[8000];
int size = 0;
while ((size=fin.read(buffer)) != -1) {
fout.write(buffer, 0, size);
}
// Notify other applications listening to scanner events
// that a media file has been added to the sd card
sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE,
Uri.fromFile(file)));
}
} catch (IOException e) {
// Ignore
Log.e(TAG, "IOException caught while opening or reading stream", e);
return false;
} finally {
if (null != input) {
try {
input.close();
} catch (IOException e) {
// Ignore
Log.e(TAG, "IOException caught while closing stream", e);
return false;
}
}
if (null != fout) {
try {
fout.close();
} catch (IOException e) {
// Ignore
Log.e(TAG, "IOException caught while closing stream", e);
return false;
}
}
}
return true;
}
private File getUniqueDestination(String base, String extension) {
File file = new File(base + "." + extension);
for (int i = 2; file.exists(); i++) {
file = new File(base + "_" + i + "." + extension);
}
return file;
}
private void showDeliveryReport(long messageId, String type) {
Intent intent = new Intent(this, DeliveryReportActivity.class);
intent.putExtra(MESSAGE_ID, messageId);
intent.putExtra(MESSAGE_TYPE, type);
startActivity(intent);
}
private final IntentFilter mHttpProgressFilter = new IntentFilter(PROGRESS_STATUS_ACTION);
private final BroadcastReceiver mHttpProgressReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (PROGRESS_STATUS_ACTION.equals(intent.getAction())) {
long token = intent.getLongExtra("token",
SendingProgressTokenManager.NO_TOKEN);
if (token != mConversation.getThreadId()) {
return;
}
int progress = intent.getIntExtra("progress", 0);
switch (progress) {
case PROGRESS_START:
setProgressBarVisibility(true);
break;
case PROGRESS_ABORT:
case PROGRESS_COMPLETE:
setProgressBarVisibility(false);
break;
default:
setProgress(100 * progress);
}
}
}
};
private final BroadcastReceiver mMediaStateReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Log.i(TAG, "mMediaStateReceiver action = " + intent.getAction());
checkAttachFileState(context);
}
};
private static ContactList sEmptyContactList;
private ContactList getRecipients() {
// If the recipients editor is visible, the conversation has
// not really officially 'started' yet. Recipients will be set
// on the conversation once it has been saved or sent. In the
// meantime, let anyone who needs the recipient list think it
// is empty rather than giving them a stale one.
if (isRecipientsEditorVisible()) {
if (sEmptyContactList == null) {
sEmptyContactList = new ContactList();
}
return sEmptyContactList;
}
return mConversation.getRecipients();
}
private void updateTitle(ContactList list) {
String title = null;
String subTitle = null;
int cnt = list.size();
switch (cnt) {
case 0: {
String recipient = null;
if (mRecipientsEditor != null) {
recipient = mRecipientsEditor.getText().toString();
}
if (MessageUtils.isWapPushNumber(recipient)) {
String[] mAddresses = recipient.split(":");
title = mAddresses[getResources().getInteger(
R.integer.wap_push_address_index)];
} else {
title = TextUtils.isEmpty(recipient)
? getString(R.string.new_message) : recipient;
}
break;
}
case 1: {
title = list.get(0).getName(); // get name returns the number if there's no
// name available.
String number = list.get(0).getNumber();
if (MessageUtils.isWapPushNumber(number)) {
String[] mTitleNumber = number.split(":");
number = mTitleNumber[getResources().getInteger(
R.integer.wap_push_address_index)];
}
if (MessageUtils.isWapPushNumber(title)) {
String[] mTitle = title.split(":");
title = mTitle[getResources().getInteger(R.integer.wap_push_address_index)];
}
if (mTextCounter.isLayoutRtl() && !TextUtils.isEmpty(number.trim())) {
// Change the phonenumber display normally for RTL.
if (title.equals(number)) {
title = PhoneNumberUtils.formatNumber(number, number,
MmsApp.getApplication().getCurrentCountryIso());
if (title.charAt(0) != '\u202D') {
title = '\u202D' + title + '\u202C';
}
} else {
subTitle = PhoneNumberUtils.formatNumber(number, number,
MmsApp.getApplication().getCurrentCountryIso());
if (subTitle.charAt(0) != '\u202D') {
subTitle = '\u202D' + subTitle + '\u202C';
}
}
} else {
if (!title.equals(number)) {
subTitle = PhoneNumberUtils.formatNumber(number, number,
MmsApp.getApplication().getCurrentCountryIso());
}
}
break;
}
default: {
// Handle multiple recipients
title = list.formatNames(", ");
subTitle = getResources().getQuantityString(R.plurals.recipient_count, cnt, cnt);
break;
}
}
mDebugRecipients = list.serialize();
// the cnt is already be added recipients count
mExistsRecipientsCount = cnt;
// Update the statusbar color
if(cnt >= 1) {
Contact contact = list.get(0);
int color = contact.getContactColor();
if (color == 0) {
color = sPrimaryColorDark;
}
updateColorPalette(color);
setActionBarColor(color);
} else {
updateColorPalette(sPrimaryColorDark);
setActionBarColor(sPrimaryColorDark);
}
if (mToolBar != null) {
mToolBar.setTitle(title);
mToolBar.setSubtitle(subTitle);
}
}
// Get the recipients editor ready to be displayed onscreen.
private void initRecipientsEditor() {
if (isRecipientsEditorVisible()) {
return;
}
// Must grab the recipients before the view is made visible because getRecipients()
// returns empty recipients when the editor is visible.
final ContactList recipients = getRecipients();
ViewStub stub = (ViewStub)findViewById(R.id.recipients_editor_stub);
if (stub != null) {
View stubView = stub.inflate();
mRecipientsEditor = (RecipientsEditor) stubView.findViewById(R.id.recipients_editor);
mRecipientsPicker = (ImageButton) stubView.findViewById(R.id.recipients_picker);
mClearButton = (ImageButton) stubView.findViewById(R.id.clear);
} else {
mRecipientsEditor = (RecipientsEditor)findViewById(R.id.recipients_editor);
mRecipientsEditor.setVisibility(View.VISIBLE);
mRecipientsPicker = (ImageButton)findViewById(R.id.recipients_picker);
mRecipientsPicker.setVisibility(View.VISIBLE);
mClearButton = (ImageButton) findViewById(R.id.clear);
}
mClearButton.setVisibility(View.GONE);
mClearButton.setOnClickListener(this);
mRecipientsPicker.setOnClickListener(this);
mRecipientsEditor.addTextChangedListener(mRecipientsWatcher);
mRecipientsEditor.setAdapter(new ChipsRecipientAdapter(this));
// Populate recipient editor with runnable thread to ensure display correctly.
final Runnable r = new Runnable() {
@Override
public void run() {
Uri uri = getIntent().getData();
if (uri != null) {
String recipient = Conversation.getRecipients(uri);
mRecipientsEditor.setText(recipient + ",");
} else {
mRecipientsEditor.setText(null);
}
mRecipientsEditor.populateWithAvatorDrawable(recipients);
}
};
mHandler.post(r);
// TODO : Remove the max length limitation due to the multiple phone picker is added and the
// user is able to select a large number of recipients from the Contacts. The coming
// potential issue is that it is hard for user to edit a recipient from hundred of
// recipients in the editor box. We may redesign the editor box UI for this use case.
// mRecipientsEditor.setFilters(new InputFilter[] {
// new InputFilter.LengthFilter(RECIPIENTS_MAX_LENGTH) });
mRecipientsEditor.setOnSelectChipRunnable(new Runnable() {
@Override
public void run() {
// After the user selects an item in the pop-up contacts list, move the
// focus to the text editor if there is only one recipient. This helps
// the common case of selecting one recipient and then typing a message,
// but avoids annoying a user who is trying to add five recipients and
// keeps having focus stolen away.
if (mRecipientsEditor.getRecipientCount() == 1) {
// if we're in extract mode then don't request focus
final InputMethodManager inputManager = (InputMethodManager)
getSystemService(Context.INPUT_METHOD_SERVICE);
if (inputManager == null || !inputManager.isFullscreenMode()) {
mTextEditor.requestFocus();
}
}
}
});
mRecipientsEditor.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (!hasFocus) {
RecipientsEditor editor = (RecipientsEditor) v;
ContactList contacts = editor.constructContactsFromInput(false);
updateTitle(contacts);
} else {
if (mAttachmentSelector.getVisibility() == View.VISIBLE) {
mAttachmentSelector.setVisibility(View.GONE);
}
}
}
});
PhoneNumberFormatter.setPhoneNumberFormattingTextWatcher(this, mRecipientsEditor);
if (mRecipientsEditor != null) {
if (getResources().getConfiguration().orientation
== Configuration.ORIENTATION_PORTRAIT) {
mRecipientsEditor.setOrientation(RecipientEditTextView.ScreenOrientation.PORTRAIT);
} else {
mRecipientsEditor.setOrientation(RecipientEditTextView.ScreenOrientation.LANDSCAPE);
}
}
mTopPanel.setVisibility(View.VISIBLE);
mToolBar.setVisibility(View.GONE);
}
@Override
protected void onCreate(Bundle savedInstanceState) {
mIsSmsEnabled = MmsConfig.isSmsEnabled(this);
super.onCreate(savedInstanceState);
if (MessageUtils.checkPermissionsIfNeeded(this)) {
return;
}
if (MessageUtils.checkIsPhoneMemoryFull(this)) {
ComposeMessageActivity.this.finish();
return;
}
resetConfiguration(getResources().getConfiguration());
final Window window = ComposeMessageActivity.this.getWindow();
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
setProgressBarVisibility(false);
mShowAttachIcon = getResources().getBoolean(R.bool.config_show_attach_icon_always);
boolean isBtnStyle = getResources().getBoolean(R.bool.config_btnstyle);
mShowTwoButtons = isBtnStyle && MessageUtils.isMsimIccCardActive();
// Initialize members for UI elements.
initResourceRefs();
mContentResolver = getContentResolver();
mBackgroundQueryHandler = new BackgroundQueryHandler(mContentResolver);
initialize(savedInstanceState, 0);
mEnablePresence = this.getResources().getBoolean(
R.bool.config_regional_presence_enable);
if (mEnablePresence) {
if (!mIsBound) {
bindService(this);
}
mAvailabilityfetchHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case PRESENCE_AVAILABILITY_FETCH:
getWindow().invalidatePanelMenu(Window.FEATURE_OPTIONS_PANEL);
if (DEBUG) Log.d(TAG, "AvailabilityFetch result updateContact");
break;
default:
break;
}
}
};
}
if (TRACE) {
android.os.Debug.startMethodTracing("compose");
}
}
private final BroadcastReceiver mSimBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (intent.getAction().equals(SIM_STATE_CHANGE_ACTION)) {
boolean mIsMsimIccCardActived = MessageUtils.isMsimIccCardActive();
log("mSimBroadcastReceiver mIsMsimIccCardActived"
+ mIsMsimIccCardActived);
if (mMsgListAdapter != null) {
mMsgListAdapter.setIsMsimIccCardActived(mIsMsimIccCardActived);
}
}
}
};
@Override
public void onZoomWithScale(float scale) {
if (mMsgListView != null) {
mMsgListView.handleZoomWithScale(scale, false);
}
if (mTextEditor != null) {
ZoomMessageListItem.zoomViewByScale(this, mTextEditor, scale, true);
}
}
private void showSubjectEditor(boolean show) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("" + show);
}
if (mSubjectTextEditor == null) {
// Don't bother to initialize the subject editor if
// we're just going to hide it.
if (show == false) {
return;
}
mSubjectTextEditor = (EditText)findViewById(R.id.subject);
mSubjectTextEditor.setFilters(new InputFilter[] {
new LengthFilter(SUBJECT_MAX_LENGTH)});
}
mSubjectTextEditor.setOnKeyListener(show ? mSubjectKeyListener : null);
if (show) {
mSubjectTextEditor.addTextChangedListener(mSubjectEditorWatcher);
} else {
mSubjectTextEditor.removeTextChangedListener(mSubjectEditorWatcher);
}
mSubjectTextEditor.setText(mWorkingMessage.getSubject());
mSubjectTextEditor.setVisibility(show ? View.VISIBLE : View.GONE);
mDeviderView.setVisibility(show ? View.VISIBLE : View.GONE);
hideOrShowTopPanel();
}
private void hideOrShowTopPanel() {
boolean anySubViewsVisible = isRecipientsEditorVisible();
mTopPanel.setVisibility(anySubViewsVisible ? View.VISIBLE : View.GONE);
mToolBar.setVisibility(anySubViewsVisible ? View.GONE : View.VISIBLE);
}
public void initialize(Bundle savedInstanceState, long originalThreadId) {
// Create a new empty working message.
mWorkingMessage = WorkingMessage.createEmpty(this);
mSendMmsSupportViaWiFi = getResources().getBoolean(R.bool.support_send_mms_over_wifi);
// Read parameters or previously saved state of this activity. This will load a new
// mConversation
initActivityState(savedInstanceState);
if (LogTag.SEVERE_WARNING && originalThreadId != 0 &&
originalThreadId == mConversation.getThreadId()) {
LogTag.warnPossibleRecipientMismatch("ComposeMessageActivity.initialize: " +
" threadId didn't change from: " + originalThreadId, this);
}
log("savedInstanceState = " + savedInstanceState +
" intent = " + getIntent() +
" mConversation = " + mConversation);
if (MessageUtils.cancelFailedToDeliverNotification(getIntent(), this)) {
// Show a pop-up dialog to inform user the message was
// failed to deliver.
undeliveredMessageDialog(getMessageDate(null));
}
MessageUtils.cancelFailedDownloadNotification(getIntent(), this);
// Set up the message history ListAdapter
initMessageList();
mShouldLoadDraft = true;
// Load the draft for this thread, if we aren't already handling
// existing data, such as a shared picture or forwarded message.
boolean isForwardedMessage = false;
// We don't attempt to handle the Intent.ACTION_SEND when saveInstanceState is non-null.
// saveInstanceState is non-null when this activity is killed. In that case, we already
// handled the attachment or the send, so we don't try and parse the intent again.
if (savedInstanceState == null && (handleSendIntent() || handleForwardedMessage())) {
mShouldLoadDraft = false;
}
// Let the working message know what conversation it belongs to
mWorkingMessage.setConversation(mConversation);
handleResendMessage();
// Show the recipients editor if we don't have a valid thread. Hide it otherwise.
if (mConversation.getThreadId() <= 0) {
// Hide the recipients editor so the call to initRecipientsEditor won't get
// short-circuited.
hideRecipientEditor();
initRecipientsEditor();
updateColorPalette(sPrimaryColorDark);
setActionBarColor(sPrimaryColorDark);
} else {
hideRecipientEditor();
}
updateSendButtonState();
drawTopPanel(false);
if (!mShouldLoadDraft) {
// We're not loading a draft, so we can draw the bottom panel immediately.
drawBottomPanel();
}
onKeyboardStateChanged();
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("update title, mConversation=" + mConversation.toString());
}
updateTitle(mConversation.getRecipients());
if (isForwardedMessage && isRecipientsEditorVisible()) {
// The user is forwarding the message to someone. Put the focus on the
// recipient editor rather than in the message editor.
mRecipientsEditor.requestFocus();
}
mMsgListAdapter.setIsGroupConversation(mConversation.getRecipients().size() > 1);
}
private void handleResendMessage() {
// In mailbox mode, click sent failed message in outbox folder, re-send message.
Intent intent = getIntent();
boolean needResend = intent.getBooleanExtra(NEED_RESEND, false);
if (!needResend) {
return;
}
long messageId = intent.getLongExtra(MESSAGE_ID, 0);
String messageType = intent.getStringExtra(MESSAGE_TYPE);
if (messageId != 0 && !TextUtils.isEmpty(messageType)) {
if ("sms".equals(messageType)) {
String messageBody = intent.getStringExtra(MESSAGE_BODY);
editSmsMessageItem(messageId, messageBody);
drawBottomPanel();
invalidateOptionsMenu();
} else {
Uri messageUri = ContentUris.withAppendedId(Mms.CONTENT_URI, messageId);
String messageSubject = "";
String subject = intent.getStringExtra(MESSAGE_SUBJECT);
if (!TextUtils.isEmpty(subject)) {
int subjectCharset = intent.getIntExtra(MESSAGE_SUBJECT_CHARSET, 0);
EncodedStringValue v = new EncodedStringValue(subjectCharset,
PduPersister.getBytes(subject));
messageSubject = MessageUtils.cleanseMmsSubject(this, v.getString());
}
editMmsMessageItem(messageUri, messageSubject);
drawBottomPanel();
invalidateOptionsMenu();
}
}
}
private void resetEditorText() {
// We have to remove the text change listener while the text editor gets cleared and
// we subsequently turn the message back into SMS. When the listener is listening while
// doing the clearing, it's fighting to update its counts and itself try and turn
// the message one way or the other.
mTextEditor.removeTextChangedListener(mTextEditorWatcher);
// Clear the text box.
TextKeyListener.clear(mTextEditor.getText());
mTextEditor.addTextChangedListener(mTextEditorWatcher);
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (!(MessageUtils.hasBasicPermissions() && MessageUtils
.hasPermissions(MessageUtils.sSMSExtendPermissions)))
{
return;
}
setIntent(intent);
Conversation conversation = null;
mSentMessage = false;
// If we have been passed a thread_id, use that to find our
// conversation.
// Note that originalThreadId might be zero but if this is a draft and we save the
// draft, ensureThreadId gets called async from WorkingMessage.asyncUpdateDraftSmsMessage
// the thread will get a threadId behind the UI thread's back.
long threadId = intent.getLongExtra(THREAD_ID, 0);
Uri intentUri = intent.getData();
if (null == mConversation) {
mConversation = Conversation.get(this, intentUri, false);
}
long originalThreadId = mConversation.getThreadId();
boolean sameThread = false;
if (threadId > 0) {
conversation = Conversation.get(this, threadId, false);
} else {
if (mConversation.getThreadId() == 0) {
// We've got a draft. Make sure the working recipients are synched
// to the conversation so when we compare conversations later in this function,
// the compare will work.
if (null != mWorkingMessage) {
mWorkingMessage.syncWorkingRecipients();
}
}
// Get the "real" conversation based on the intentUri. The intentUri might specify
// the conversation by a phone number or by a thread id. We'll typically get a threadId
// based uri when the user pulls down a notification while in ComposeMessageActivity and
// we end up here in onNewIntent. mConversation can have a threadId of zero when we're
// working on a draft. When a new message comes in for that same recipient, a
// conversation will get created behind CMA's back when the message is inserted into
// the database and the corresponding entry made in the threads table. The code should
// use the real conversation as soon as it can rather than finding out the threadId
// when sending with "ensureThreadId".
conversation = Conversation.get(this, intentUri, false);
}
LogTag.debugD("onNewIntent: data=" + intentUri + ", thread_id extra is " + threadId +
", new conversation=" + conversation + ", mConversation=" + mConversation);
// this is probably paranoid to compare both thread_ids and recipient lists,
// but we want to make double sure because this is a last minute fix for Froyo
// and the previous code checked thread ids only.
// (we cannot just compare thread ids because there is a case where mConversation
// has a stale/obsolete thread id (=1) that could collide against the new thread_id(=1),
// even though the recipient lists are different)
sameThread = (conversation.getThreadId() == mConversation.getThreadId()
&& mConversation.getThreadId() != 0 && conversation.equals(mConversation));
LogTag.debugD("sameThread:" + sameThread);
if (!sameThread) {
LogTag.debugD("onNewIntent: different conversation");
mMessagesAndDraftLoaded = false;
if (mConversation.getThreadId() == 0 || conversation.getThreadId() == 0) {
mConversation = conversation;
mWorkingMessage.setConversation(mConversation);
updateThreadIdIfRunning();
invalidateOptionsMenu();
if (mMsgListAdapter != null) {
mMsgListAdapter.changeCursor(null);
mMsgListAdapter.cancelBackgroundLoading();
}
}
saveDraft(false); // if we've got a draft, save it first
resetEditorText();
initialize(null, originalThreadId);
}
loadMessagesAndDraft(0);
}
private void sanityCheckConversation() {
if (mWorkingMessage.getConversation() != mConversation) {
LogTag.warnPossibleRecipientMismatch(
"ComposeMessageActivity: mWorkingMessage.mConversation=" +
mWorkingMessage.getConversation() + ", mConversation=" +
mConversation + ", MISMATCH!", this);
}
}
@Override
protected void onRestart() {
super.onRestart();
if (mWorkingMessage.isDiscarded()) {
// If the message isn't worth saving, don't resurrect it. Doing so can lead to
// a situation where a new incoming message gets the old thread id of the discarded
// draft. This activity can end up displaying the recipients of the old message with
// the contents of the new message. Recognize that dangerous situation and bail out
// to the ConversationList where the user can enter this in a clean manner.
if (mWorkingMessage.isWorthSaving() || mInAsyncAddAttathProcess) {
if (LogTag.VERBOSE) {
log("onRestart: mWorkingMessage.unDiscard()");
}
mWorkingMessage.unDiscard(); // it was discarded in onStop().
sanityCheckConversation();
} else if (isRecipientsEditorVisible() && recipientCount() > 0) {
if (LogTag.VERBOSE) {
log("onRestart: goToConversationList");
}
goToConversationList();
}
}
}
@Override
protected void onStart() {
super.onStart();
boolean isSmsEnabled = MmsConfig.isSmsEnabled(this);
if (isSmsEnabled != mIsSmsEnabled) {
mIsSmsEnabled = isSmsEnabled;
invalidateOptionsMenu();
}
initFocus();
if (isRecipientsEditorVisible()) {
mRecipientsEditor.addTextChangedListener(mRecipientsWatcher);
}
// Register a BroadcastReceiver to listen on HTTP I/O process.
registerReceiver(mHttpProgressReceiver, mHttpProgressFilter);
// Register a BroadcastReceiver to listen on SD card state.
registerReceiver(mMediaStateReceiver, getMediaStateFilter());
registerReceiver(mAirplaneModeBroadcastReceiver, mAirplaneModeFilter);
registerReceiver(mSimBroadcastReceiver, mSIMStatusChangeFilter);
// figure out whether we need to show the keyboard or not.
// if there is draft to be loaded for 'mConversation', we'll show the keyboard;
// otherwise we hide the keyboard. In any event, delay loading
// message history and draft (controlled by DEFER_LOADING_MESSAGES_AND_DRAFT).
int mode = WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE;
if (DraftCache.getInstance().hasDraft(mConversation.getThreadId())) {
mode |= WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
} else if (mConversation.getThreadId() <= 0) {
// For composing a new message, bring up the softkeyboard so the user can
// immediately enter recipients. This call won't do anything on devices with
// a hard keyboard.
mode |= WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE;
} else {
mode |= WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN;
}
getWindow().setSoftInputMode(mode);
// reset mMessagesAndDraftLoaded
mMessagesAndDraftLoaded = false;
CharSequence text = mWorkingMessage.getText();
if (text != null) {
mTextEditor.setTextKeepState(text);
}
if (!DEFER_LOADING_MESSAGES_AND_DRAFT) {
loadMessagesAndDraft(1);
} else {
// HACK: force load messages+draft after max delay, if it's not already loaded.
// this is to work around when coming out of sleep mode. WindowManager behaves
// strangely and hides the keyboard when it should be shown, or sometimes initially
// shows it when we want to hide it. In that case, we never get the onSizeChanged()
// callback w/ keyboard shown, so we wouldn't know to load the messages+draft.
mHandler.postDelayed(new Runnable() {
public void run() {
loadMessagesAndDraft(2);
}
}, LOADING_MESSAGES_AND_DRAFT_MAX_DELAY_MS);
}
// Update the fasttrack info in case any of the recipients' contact info changed
// while we were paused. This can happen, for example, if a user changes or adds
// an avatar associated with a contact.
mWorkingMessage.syncWorkingRecipients();
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("update title, mConversation=" + mConversation.toString());
}
updateTitle(mConversation.getRecipients());
ActionBar actionBar = getActionBar();
actionBar.setDisplayHomeAsUpEnabled(true);
}
public void loadMessageContent() {
// Don't let any markAsRead DB updates occur before we've loaded the messages for
// the thread. Unblocking occurs when we're done querying for the conversation
// items.
mConversation.blockMarkAsRead(true);
mConversation.markAsRead(); // dismiss any notifications for this convo
startMsgListQuery();
updateSendFailedNotification();
}
/**
* Load message history and draft. This method should be called from main thread.
* @param debugFlag shows where this is being called from
*/
private void loadMessagesAndDraft(int debugFlag) {
if (!mSendDiscreetMode && !mMessagesAndDraftLoaded) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "### CMA.loadMessagesAndDraft: flag=" + debugFlag);
}
loadMessageContent();
boolean drawBottomPanel = true;
long threadId = mWorkingMessage.getConversation().getThreadId();
// Do not load draft when forwarding to the same recipients.
if (mShouldLoadDraft && !MessageUtils.sSameRecipientList.contains(threadId)) {
if (loadDraft()) {
drawBottomPanel = false;
}
}
if (drawBottomPanel) {
drawBottomPanel();
}
mMessagesAndDraftLoaded = true;
}
}
private void updateSendFailedNotification() {
final long threadId = mConversation.getThreadId();
if (threadId <= 0)
return;
// updateSendFailedNotificationForThread makes a database call, so do the work off
// of the ui thread.
new Thread(new Runnable() {
@Override
public void run() {
MessagingNotification.updateSendFailedNotificationForThread(
ComposeMessageActivity.this, threadId);
}
}, "ComposeMessageActivity.updateSendFailedNotification").start();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString(RECIPIENTS, mConversation.getRecipients().serialize());
mWorkingMessage.writeStateToBundle(outState);
if (mSendDiscreetMode) {
outState.putBoolean(KEY_EXIT_ON_SENT, mSendDiscreetMode);
}
if (mForwardMessageMode) {
outState.putBoolean(KEY_FORWARDED_MESSAGE, mForwardMessageMode);
}
}
@Override
protected void onResume() {
super.onResume();
// OLD: get notified of presence updates to update the titlebar.
// NEW: we are using ContactHeaderWidget which displays presence, but updating presence
// there is out of our control.
//Contact.startPresenceObserver();
mIsPickingContact = false;
addRecipientsListeners();
setSendButtonImage();
if (isRecipientsEditorVisible()) {
mRecipientsEditor.addTextChangedListener(mRecipientsWatcher);
}
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("update title, mConversation=" + mConversation.toString());
}
// There seems to be a bug in the framework such that setting the title
// here gets overwritten to the original title. Do this delayed as a
// workaround.
mMessageListItemHandler.postDelayed(new Runnable() {
@Override
public void run() {
ContactList recipients = isRecipientsEditorVisible() ?
mRecipientsEditor.constructContactsFromInput(false) : getRecipients();
updateTitle(recipients);
}
}, 100);
mIsRunning = true;
updateThreadIdIfRunning();
mIsAirplaneModeOn = MessageUtils.isAirplaneModeOn(this);
if (getResources().getBoolean(R.bool.def_custom_preferences_settings)) {
setBackgroundWallpaper();
setTextFontsize();
}
}
@Override
protected void onPause() {
super.onPause();
if (DEBUG) {
Log.v(TAG, "onPause: setCurrentlyDisplayedThreadId: " +
MessagingNotification.THREAD_NONE);
}
MessagingNotification.setCurrentlyDisplayedThreadId(MessagingNotification.THREAD_NONE);
// OLD: stop getting notified of presence updates to update the titlebar.
// NEW: we are using ContactHeaderWidget which displays presence, but updating presence
// there is out of our control.
//Contact.stopPresenceObserver();
removeRecipientsListeners();
if (isRecipientsEditorVisible()) {
mRecipientsEditor.removeTextChangedListener(mRecipientsWatcher);
}
// remove any callback to display a progress spinner
if (mAsyncDialog != null) {
mAsyncDialog.clearPendingProgressDialog();
}
// Remember whether the list is scrolled to the end when we're paused so we can rescroll
// to the end when resumed.
if (mMsgListAdapter != null &&
mMsgListView.getLastVisiblePosition() >= mMsgListAdapter.getCount() - 1) {
mSavedScrollPosition = Integer.MAX_VALUE;
} else {
mSavedScrollPosition = mMsgListView.getFirstVisiblePosition();
}
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "onPause: mSavedScrollPosition=" + mSavedScrollPosition);
}
mConversation.markAsRead();
mIsRunning = false;
if (mMMSAudioPlayer != null && mMediaPlayer != null && mMediaPlayer.isPlaying()) {
mMMSAudioPlayer.pause();
}
}
@Override
protected void onStop() {
super.onStop();
// No need to do the querying when finished this activity
mBackgroundQueryHandler.cancelOperation(MESSAGE_LIST_QUERY_TOKEN);
// Allow any blocked calls to update the thread's read status.
mConversation.blockMarkAsRead(false);
// Simply setting the choice mode causes the previous choice mode to finish and we exit
// multi-select mode (if we're in it) and remove all the selections.
mMsgListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
if (mMsgListAdapter != null) {
// resets multi select mode
getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
}
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("save draft");
}
if(!isAvoidingSavingDraft)
{
saveDraft(true);
// set 'mShouldLoadDraft' to true, so when coming back to ComposeMessageActivity, we would
// load the draft, unless we are coming back to the activity after attaching a photo, etc,
// in which case we should set 'mShouldLoadDraft' to false.
mShouldLoadDraft = true;
isAvoidingSavingDraft = false;
}
// Cleanup the BroadcastReceiver.
unregisterReceiver(mHttpProgressReceiver);
unregisterReceiver(mMediaStateReceiver);
unregisterReceiver(mAirplaneModeBroadcastReceiver);
unregisterReceiver(mSimBroadcastReceiver);
if (mAttachmentSelector.getVisibility() == View.VISIBLE) {
mAttachmentSelector.setVisibility(View.GONE);
}
}
@Override
protected void onDestroy() {
if (TRACE) {
android.os.Debug.stopMethodTracing();
}
if (mZoomGestureOverlayView != null) {
mZoomGestureOverlayView.removeZoomListener(this);
}
if (mMsgListAdapter != null) {
mMsgListAdapter.changeCursor(null);
mMsgListAdapter.cancelBackgroundLoading();
}
if (mInvalidRecipientDialog != null && mInvalidRecipientDialog.isShowing()) {
mInvalidRecipientDialog.dismiss();
mInvalidRecipientDialog = null;
}
if (mMsgDetailDialog != null && mMsgDetailDialog.isShowing()) {
mMsgDetailDialog.dismiss();
mMsgDetailDialog = null;
}
if (mEnablePresence) {
if (mIsBound) {
unbindService(this);
}
}
super.onDestroy();
mMMSAudioPlayer.releaseMediaPlayer();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (mRecipientsEditor != null) {
if (getResources().getConfiguration().orientation
== Configuration.ORIENTATION_PORTRAIT) {
mRecipientsEditor.setOrientation(RecipientEditTextView.ScreenOrientation.PORTRAIT);
} else {
mRecipientsEditor.setOrientation(RecipientEditTextView.ScreenOrientation.LANDSCAPE);
}
}
if (resetConfiguration(newConfig)) {
// Have to re-layout the attachment editor because we have different layouts
// depending on whether we're portrait or landscape.
drawTopPanel(isSubjectEditorVisible());
}
if (LOCAL_LOGV) {
Log.v(TAG, "CMA.onConfigurationChanged: " + newConfig +
", mIsKeyboardOpen=" + mIsKeyboardOpen);
}
onKeyboardStateChanged();
// If locale changed, we need reload the source of mInvalidRecipientDialog's
// title and message from xml file.
if (mInvalidRecipientDialog != null && mInvalidRecipientDialog.isShowing()) {
mInvalidRecipientDialog.dismiss();
showInvalidRecipientDialog();
}
mInvalidRecipientDialog = null;
if (mAttachmentSelector.getVisibility() == View.VISIBLE) {
setAttachmentSelectorHeight();
resetGridColumnsCount();
}
}
// returns true if landscape/portrait configuration has changed
private boolean resetConfiguration(Configuration config) {
mIsKeyboardOpen = config.keyboardHidden == KEYBOARDHIDDEN_NO;
boolean isLandscape = config.orientation == Configuration.ORIENTATION_LANDSCAPE;
if (mIsLandscape != isLandscape) {
mIsLandscape = isLandscape;
return true;
}
return false;
}
private void onKeyboardStateChanged() {
// If the keyboard is hidden, don't show focus highlights for
// things that cannot receive input.
mTextEditor.setEnabled(mIsSmsEnabled);
if (!mIsSmsEnabled) {
if (mRecipientsEditor != null) {
mRecipientsEditor.setFocusableInTouchMode(false);
}
if (mSubjectTextEditor != null) {
mSubjectTextEditor.setFocusableInTouchMode(false);
}
mTextEditor.setFocusableInTouchMode(false);
mTextEditor.setHint(R.string.sending_disabled_not_default_app);
} else if (mIsKeyboardOpen) {
if (mRecipientsEditor != null) {
mRecipientsEditor.setFocusableInTouchMode(true);
}
if (mSubjectTextEditor != null) {
mSubjectTextEditor.setFocusableInTouchMode(true);
}
mTextEditor.setFocusableInTouchMode(true);
mTextEditor.setHint(R.string.type_to_compose_text_enter_to_send);
} else {
if (mRecipientsEditor != null) {
mRecipientsEditor.setFocusable(false);
}
if (mSubjectTextEditor != null) {
mSubjectTextEditor.setFocusable(false);
}
mTextEditor.setFocusable(false);
mTextEditor.setHint(R.string.open_keyboard_to_compose_message);
}
}
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
switch (keyCode) {
case KeyEvent.KEYCODE_DEL:
if ((mMsgListAdapter != null) && mMsgListView.isFocused()) {
Cursor cursor;
try {
cursor = (Cursor) mMsgListView.getSelectedItem();
} catch (ClassCastException e) {
Log.e(TAG, "Unexpected ClassCastException.", e);
return super.onKeyDown(keyCode, event);
}
if (cursor != null) {
String type = cursor.getString(COLUMN_MSG_TYPE);
long msgId = cursor.getLong(COLUMN_ID);
MessageItem msgItem = mMsgListAdapter.getCachedMessageItem(type, msgId,
cursor);
if (msgItem != null) {
DeleteMessageListener l = new DeleteMessageListener(msgItem);
confirmDeleteDialog(l, msgItem.mLocked);
}
return true;
}
}
break;
case KeyEvent.KEYCODE_DPAD_CENTER:
case KeyEvent.KEYCODE_ENTER:
if (isPreparedForSending()) {
confirmSendMessageIfNeeded();
return true;
}
break;
case KeyEvent.KEYCODE_BACK:
if (mAttachmentSelector.getVisibility() == View.VISIBLE) {
mAttachmentSelector.setVisibility(View.GONE);
} else {
exitComposeMessageActivity(new Runnable() {
@Override
public void run() {
finish();
}
});
}
return true;
}
return super.onKeyDown(keyCode, event);
}
private void exitComposeMessageActivity(final Runnable exit) {
// If the message is empty, just quit -- finishing the
// activity will cause an empty draft to be deleted.
if (!mWorkingMessage.isWorthSaving()) {
exit.run();
mWorkingMessage.discard();
return;
}
// If the recipient is empty, the meesgae shouldn't be saved, and should pop up the
// confirm delete dialog.
if (isRecipientEmpty()) {
MessageUtils.showDiscardDraftConfirmDialog(this,
new DiscardDraftListener(), getValidNumCount());
return;
}
mToastForDraftSave = true;
exit.run();
}
private int getValidNumCount() {
// If mRecipientsEditor is empty we need show empty info.
int count = MessageUtils.ALL_RECIPIENTS_EMPTY;
if (!TextUtils.isEmpty(mRecipientsEditor.getText())) {
count = mRecipientsEditor.getValidRecipientsCount(mWorkingMessage.requiresMms());
}
return count;
}
private boolean isRecipientEmpty() {
return isRecipientsEditorVisible()
&& (mRecipientsEditor.getValidRecipientsCount(mWorkingMessage.requiresMms())
!= MessageUtils.ALL_RECIPIENTS_VALID
|| (0 == mRecipientsEditor.getRecipientCount()));
}
private void goToConversationList() {
finish();
String currentClass = "";
Intent intent = getIntent();
if (intent != null) {
currentClass = intent.getStringExtra(EXTRA_START_COMPOSE_FROM);
}
if(SearchConversationActivity.class.getSimpleName().equals(currentClass)){
super.onBackPressed();
} else if (NotificationConversationList.class.getSimpleName().equals(currentClass)) {
startActivity(new Intent(this, NotificationConversationList.class));
} else {
startActivity(new Intent(this, ConversationList.class));
}
}
private void hideRecipientEditor() {
if (mRecipientsEditor != null) {
mRecipientsEditor.removeTextChangedListener(mRecipientsWatcher);
mRecipientsEditor.setVisibility(View.GONE);
if (mRecipientsPicker != null) {
mRecipientsPicker.setVisibility(View.GONE);
}
hideOrShowTopPanel();
}
}
private boolean isRecipientsEditorVisible() {
return (null != mRecipientsEditor)
&& (View.VISIBLE == mRecipientsEditor.getVisibility());
}
private boolean isSubjectEditorVisible() {
return (null != mSubjectTextEditor)
&& (View.VISIBLE == mSubjectTextEditor.getVisibility());
}
@Override
public void onAttachmentChanged() {
// Have to make sure we're on the UI thread. This function can be called off of the UI
// thread when we're adding multi-attachments
runOnUiThread(new Runnable() {
@Override
public void run() {
drawBottomPanel();
updateSendButtonState();
drawTopPanel(isSubjectEditorVisible());
}
});
}
@Override
public void onProtocolChanged(final boolean convertToMms) {
// Have to make sure we're on the UI thread. This function can be called off of the UI
// thread when we're adding multi-attachments
runOnUiThread(new Runnable() {
@Override
public void run() {
if (mShowTwoButtons) {
showTwoSmsOrMmsSendButton(convertToMms);
} else {
showSmsOrMmsSendButton(convertToMms);
}
if (convertToMms) {
// In the case we went from a long sms with a counter to an mms because
// the user added an attachment or a subject, hide the counter --
// it doesn't apply to mms.
mTextCounter.setVisibility(View.GONE);
if (mShowTwoButtons) {
mTextCounterSec.setVisibility(View.GONE);
}
showConvertToMmsToast();
} else {
mTextCounter.setVisibility(View.VISIBLE);
if (mShowTwoButtons) {
mTextCounterSec.setVisibility(View.VISIBLE);
}
showConvertToSmsToast();
}
updateSendButtonState();
}
});
}
// Show or hide the Sms or Mms button as appropriate. Return the view so that the caller
// can adjust the enableness and focusability.
private View showSmsOrMmsSendButton(boolean isMms) {
View showButton;
View hideButton;
if (isMms) {
showButton = mSendButtonMms;
hideButton = mSendButtonSms;
mSendButtonMmsText.setVisibility(View.VISIBLE);
} else {
showButton = mSendButtonSms;
hideButton = mSendButtonMms;
mSendButtonMmsText.setVisibility(View.GONE);
}
showButton.setVisibility(View.VISIBLE);
hideButton.setVisibility(View.GONE);
hideButton.setEnabled(false);
return showButton;
}
private View[] showTwoSmsOrMmsSendButton(boolean isMms) {
View[] showButton = new View[NUMBER_OF_BUTTONS];
View[] hideButton = new View[NUMBER_OF_BUTTONS];
if (isMms) {
showButton[ConstantsWrapper.Phone.SUB1] = mSendLayoutMmsFir;
showButton[ConstantsWrapper.Phone.SUB2] = mSendLayoutMmsSec;
hideButton[ConstantsWrapper.Phone.SUB1] = mSendLayoutSmsFir;
hideButton[ConstantsWrapper.Phone.SUB2] = mSendLayoutSmsSec;
} else {
showButton[ConstantsWrapper.Phone.SUB1] = mSendLayoutSmsFir;
showButton[ConstantsWrapper.Phone.SUB2] = mSendLayoutSmsSec;
hideButton[ConstantsWrapper.Phone.SUB1] = mSendLayoutMmsFir;
hideButton[ConstantsWrapper.Phone.SUB2] = mSendLayoutMmsSec;
}
showButton[ConstantsWrapper.Phone.SUB1].setVisibility(View.VISIBLE);
showButton[ConstantsWrapper.Phone.SUB2].setVisibility(View.VISIBLE);
hideButton[ConstantsWrapper.Phone.SUB1].setVisibility(View.GONE);
hideButton[ConstantsWrapper.Phone.SUB2].setVisibility(View.GONE);
return showButton;
}
Runnable mResetMessageRunnable = new Runnable() {
@Override
public void run() {
resetMessage();
}
};
@Override
public void onPreMessageSent() {
runOnUiThread(mResetMessageRunnable);
}
@Override
public void onMessageSent() {
// This callback can come in on any thread; put it on the main thread to avoid
// concurrency problems
runOnUiThread(new Runnable() {
@Override
public void run() {
// If we already have messages in the list adapter, it
// will be auto-requerying; don't thrash another query in.
// TODO: relying on auto-requerying seems unreliable when priming an MMS into the
// outbox. Need to investigate.
// if (mMsgListAdapter.getCount() == 0) {
if (LogTag.VERBOSE) {
log("onMessageSent");
}
startMsgListQuery();
// }
// The thread ID could have changed if this is a new message that we just inserted
// into the database (and looked up or created a thread for it)
updateThreadIdIfRunning();
}
});
}
@Override
public void onMaxPendingMessagesReached() {
saveDraft(false);
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(ComposeMessageActivity.this, R.string.too_many_unsent_mms,
Toast.LENGTH_LONG).show();
}
});
}
@Override
public void onAttachmentError(final int error) {
runOnUiThread(new Runnable() {
@Override
public void run() {
mIsAttachmentErrorOnSend = true;
handleAddAttachmentError(error, R.string.type_picture);
onMessageSent(); // now requery the list of messages
}
});
}
// We don't want to show the "call" option unless there is only one
// recipient and it's a phone number.
private boolean isRecipientCallable() {
ContactList recipients = getRecipients();
return (recipients.size() == 1 && !recipients.containsEmail()
&& !(MessageUtils.isWapPushNumber(recipients.get(0).getNumber())));
}
private void dialRecipient() {
if (isRecipientCallable()) {
String number = getRecipients().get(0).getNumber();
if (TextUtils.isEmpty(number)) {
return;
}
startActivity(new Intent(Intent.ACTION_CALL_PRIVILEGED,
Uri.parse("tel:" + number)));
}
}
private void videoCallRecipient() {
String number = getRecipients().get(0).getNumber();
final Intent intent = new Intent(Intent.ACTION_CALL, getCallUri(number));
intent.putExtra(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE,
VideoProfile.STATE_BIDIRECTIONAL);
startActivity(intent);
}
public Uri getCallUri(String number) {
if (isUriNumber(number)) {
return Uri.fromParts("sip", number, null);
}
return Uri.fromParts("tel", number, null);
}
public boolean isUriNumber(String number) {
return number != null && (number.contains("@") || number.contains("%40"));
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
super.onPrepareOptionsMenu(menu) ;
menu.clear();
if (mSendDiscreetMode && !mForwardMessageMode && !mReplyMessageMode) {
// When we're in send-a-single-message mode from the lock screen, don't show
// any menus.
return true;
}
if (mEnablePresence) {
if (isRecipientCallable()) {
final String number = getRecipients().get(0).getNumber();
MenuItem item = menu.add(0, MENU_VIDEOCALL_RECIPIENT, 0, R.string.menu_call);
mVideoCapable = getVTCapability(number);
new Thread(new Runnable(){
public void run(){
if (null != number) {
boolean newVTCapable = startAvailabilityFetch(number);
if (mVideoCapable != newVTCapable) {
mAvailabilityfetchHandler
.sendEmptyMessage(PRESENCE_AVAILABILITY_FETCH);
}
}
}
}).start();
if (mVideoCapable) {
item.setIcon(R.drawable.ic_attach_capture_video_holo_light);
item.setEnabled(true);
} else {
item.setIcon(R.drawable.ic_attach_capture_video_disable);
item.setEnabled(false);
}
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
}
// Don't show the call icon if the device don't support voice calling.
boolean voiceCapable = ConstantsWrapper.getConfigVoiceCapable(this);
if (isRecipientCallable() && voiceCapable) {
MenuItem item = menu.add(0, MENU_CALL_RECIPIENT, 0, R.string.menu_call)
.setIcon(R.drawable.call)
.setTitle(R.string.menu_call);
if (!isRecipientsEditorVisible()) {
// If we're not composing a new message, show the call icon in the actionbar
item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
}
}
if (MmsConfig.getMmsEnabled() && mIsSmsEnabled) {
if (showAddAttachementButton()) {
mAddAttachmentButton.setVisibility(View.VISIBLE);
}
}
if (isPreparedForSending() && mIsSmsEnabled) {
if (mShowTwoButtons) {
menu.add(0, MENU_SEND_BY_SLOT1, 0, R.string.send_by_slot1)
.setIcon(android.R.drawable.ic_menu_send);
menu.add(0, MENU_SEND_BY_SLOT2, 0, R.string.send_by_slot2)
.setIcon(android.R.drawable.ic_menu_send);
} else {
menu.add(0, MENU_SEND, 0, R.string.send).setIcon(android.R.drawable.ic_menu_send);
}
}
if (getRecipients().size() > 1) {
menu.add(0, MENU_GROUP_PARTICIPANTS, 0, R.string.menu_group_participants);
}
if (mMsgListAdapter.getCount() > 0 && mIsSmsEnabled) {
// Removed search as part of b/1205708
//menu.add(0, MENU_SEARCH, 0, R.string.menu_search).setIcon(
// R.drawable.ic_menu_search);
Cursor cursor = mMsgListAdapter.getCursor();
if ((null != cursor) && (cursor.getCount() > 0)) {
menu.add(0, MENU_DELETE_THREAD, 0, R.string.delete_thread).setIcon(
android.R.drawable.ic_menu_delete);
}
} else if (mIsSmsEnabled) {
menu.add(0, MENU_DISCARD, 0, R.string.discard).setIcon(android.R.drawable.ic_menu_delete);
}
buildAddAddressToContactMenuItem(menu);
if (LogTag.DEBUG_DUMP) {
menu.add(0, MENU_DEBUG_DUMP, 0, R.string.menu_debug_dump);
}
return true;
}
private void buildAddAddressToContactMenuItem(Menu menu) {
// bug #7087793: for group of recipients, remove "Add to People" action. Rely on
// individually creating contacts for unknown phone numbers by touching the individual
// sender's avatars, one at a time
ContactList contacts = getRecipients();
if (contacts.size() != 1) {
return;
}
// if we don't have a contact for the recipient, create a menu item to add the number
// to contacts.
Contact c = contacts.get(0);
if (!c.existsInDatabase() && canAddToContacts(c)) {
Intent intent = ConversationList.createAddContactIntent(c.getNumber());
menu.add(0, MENU_ADD_ADDRESS_TO_CONTACTS, 0, R.string.menu_add_to_contacts)
.setIcon(android.R.drawable.ic_menu_add)
.setIntent(intent);
}
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_ADD_SUBJECT:
showSubjectEditor(true);
mWorkingMessage.setSubject("", true);
updateSendButtonState();
mSubjectTextEditor.requestFocus();
break;
case MENU_DISCARD:
mWorkingMessage.discard();
finish();
break;
case MENU_SEND:
if (isPreparedForSending()) {
confirmSendMessageIfNeeded();
}
break;
case MENU_SEND_BY_SLOT1:
if (isPreparedForSending()) {
confirmSendMessageIfNeeded(
SubscriptionManagerWrapper.getSubId(
ConstantsWrapper.Phone.SUB1)[0]);
}
break;
case MENU_SEND_BY_SLOT2:
if (isPreparedForSending()) {
confirmSendMessageIfNeeded(
SubscriptionManagerWrapper.getSubId(
ConstantsWrapper.Phone.SUB2)[0]);
}
break;
case MENU_SEARCH:
onSearchRequested();
break;
case MENU_DELETE_THREAD:
confirmDeleteThread(mConversation.getThreadId());
break;
case android.R.id.home:
case MENU_CONVERSATION_LIST:
exitComposeMessageActivity(new Runnable() {
@Override
public void run() {
goToConversationList();
}
});
break;
case MENU_CALL_RECIPIENT:
dialRecipient();
break;
case MENU_VIDEOCALL_RECIPIENT:
videoCallRecipient();
break;
case MENU_IMPORT_TEMPLATE:
showDialog(DIALOG_IMPORT_TEMPLATE);
break;
case MENU_GROUP_PARTICIPANTS:
{
Intent intent = new Intent(this, RecipientListActivity.class);
intent.putExtra(THREAD_ID, mConversation.getThreadId());
startActivity(intent);
break;
}
case MENU_VIEW_CONTACT: {
// View the contact for the first (and only) recipient.
ContactList list = getRecipients();
if (list.size() == 1 && list.get(0).existsInDatabase()) {
Uri contactUri = list.get(0).getUri();
Intent intent = new Intent(Intent.ACTION_VIEW, contactUri);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
startActivity(intent);
}
break;
}
case MENU_ADD_ADDRESS_TO_CONTACTS:
mAddContactIntent = item.getIntent();
startActivityForResult(mAddContactIntent, REQUEST_CODE_ADD_CONTACT);
break;
case MENU_PREFERENCES: {
Intent intent = new Intent(this, MessagingPreferenceActivity.class);
startActivityIfNeeded(intent, -1);
break;
}
case MENU_DEBUG_DUMP:
mWorkingMessage.dump();
Conversation.dump();
LogTag.dumpInternalTables(this);
break;
}
return true;
}
@Override
protected Dialog onCreateDialog(int id) {
switch (id) {
case DIALOG_IMPORT_TEMPLATE:
return showImportTemplateDialog();
}
return super.onCreateDialog(id);
}
@Override
protected void onPrepareDialog(int id, Dialog dialog) {
switch (id) {
case DIALOG_IMPORT_TEMPLATE:
removeDialog(id);
break;
}
super.onPrepareDialog(id, dialog);
}
private Dialog showImportTemplateDialog(){
String [] smsTempArray = null;
Uri uri = Uri.parse("content://com.android.mms.MessageTemplateProvider/messages");
Cursor cur = null;
try {
cur = getContentResolver().query(uri, null, null, null, null);
if (cur != null && cur.moveToFirst()) {
int index = 0;
smsTempArray = new String[cur.getCount()];
String title = null;
do {
title = cur.getString(cur.getColumnIndex("message"));
smsTempArray[index++] = title;
} while (cur.moveToNext());
}
} finally {
if (cur != null) {
cur.close();
}
}
TemplateSelectListener listener = new TemplateSelectListener(smsTempArray);
return new AlertDialog.Builder(ComposeMessageActivity.this)
.setTitle(R.string.message_template)
.setItems(smsTempArray, listener)
.create();
}
private class TemplateSelectListener implements DialogInterface.OnClickListener {
private String[] mTempArray;
TemplateSelectListener(String[] tempArray) {
mTempArray = tempArray;
}
@Override
public void onClick(DialogInterface dialog, int which) {
// TODO Auto-generated method stub
if (mTempArray != null && mTempArray.length > which) {
// If the subject EditText is visible and has the focus,
// add the string from the template to the subject EditText
// or else add the string to the message EditText.
EditText etSubject = ComposeMessageActivity.this.mSubjectTextEditor;
if (isSubjectEditorVisible() && etSubject.hasFocus()) {
int subjectIndex = etSubject.getSelectionStart();
etSubject.getText().insert(subjectIndex, mTempArray[which]);
} else {
EditText et = ComposeMessageActivity.this.mTextEditor;
int index = et.getSelectionStart();
et.getText().insert(index, mTempArray[which]);
// Need require foucus,if do not do so,foucus still on mRecipientEditor,
// so mRecipientsWatcher will call afterTextChanged to do
// setWorkingRecipients(...), and then mWorkingRecipients != null and will
// call setRecipients() set mThreadId = 0. Because of mThreadId = 0,
// asyncDeleteDraftSmsMessage will can not delete draft successful.
et.requestFocus();
}
}
}
}
private void confirmDeleteThread(long threadId) {
Conversation.startQueryHaveLockedMessages(mBackgroundQueryHandler,
threadId, ConversationList.HAVE_LOCKED_MESSAGES_TOKEN);
}
// static class SystemProperties { // TODO, temp class to get unbundling working
// static int getInt(String s, int value) {
// return value; // just return the default value or now
// }
// }
private int getSlideNumber() {
int slideNum = 0;
SlideshowModel slideshow = mWorkingMessage.getSlideshow();
if (slideshow != null) {
slideNum = slideshow.size();
}
return slideNum;
}
private boolean showAddAttachementButton() {
if (!mShowAttachIcon) {
return !mWorkingMessage.hasAttachment();
} else {
return !mWorkingMessage.hasVcard()
&& getSlideNumber() < MmsConfig.getMaxSlideNumber();
}
}
private boolean isAppendRequest(int requestCode) {
return (requestCode & REPLACE_ATTACHMEN_MASK) == 0;
}
private int getRequestCode(int requestCode) {
return requestCode & ~REPLACE_ATTACHMEN_MASK;
}
private int getMakRequestCode(boolean replace, int requestCode) {
if (replace) {
return requestCode | REPLACE_ATTACHMEN_MASK;
}
return requestCode;
}
private void addAttachment(int type, boolean replace) {
// Calculate the size of the current slide if we're doing a replace so the
// slide size can optionally be used in computing how much room is left for an attachment.
int currentSlideSize = 0;
SlideshowModel slideShow = mWorkingMessage.getSlideshow();
if (replace && slideShow != null) {
WorkingMessage.removeThumbnailsFromCache(slideShow);
SlideModel slide = slideShow.get(0);
currentSlideSize = slide.getSlideSize();
}
switch (type) {
case AttachmentPagerAdapter.ADD_SUBJECT:
showSubjectEditor(true);
mWorkingMessage.setSubject("", true);
updateSendButtonState();
mSubjectTextEditor.requestFocus();
break;
case AttachmentPagerAdapter.ADD_IMAGE:
MessageUtils.selectImage(this,
getMakRequestCode(replace, REQUEST_CODE_ATTACH_IMAGE));
break;
case AttachmentPagerAdapter.TAKE_PICTURE: {
MessageUtils.capturePicture(this,
getMakRequestCode(replace, REQUEST_CODE_TAKE_PICTURE));
break;
}
case AttachmentPagerAdapter.ADD_VIDEO:
MessageUtils.selectVideo(this,
getMakRequestCode(replace, REQUEST_CODE_ATTACH_VIDEO));
break;
case AttachmentPagerAdapter.RECORD_VIDEO: {
long sizeLimit = computeAttachmentSizeLimit(slideShow, currentSlideSize);
if (sizeLimit > 0) {
MessageUtils.recordVideo(this,
getMakRequestCode(replace, REQUEST_CODE_TAKE_VIDEO), sizeLimit);
} else {
Toast.makeText(this,
getString(R.string.message_too_big_for_video),
Toast.LENGTH_SHORT).show();
}
}
break;
case AttachmentPagerAdapter.ADD_SOUND:
MessageUtils.selectAudio(this,
getMakRequestCode(replace, REQUEST_CODE_ATTACH_SOUND));
break;
case AttachmentPagerAdapter.RECORD_SOUND:
long sizeLimit = computeAttachmentSizeLimit(slideShow, currentSlideSize);
MessageUtils.recordSound(this,
getMakRequestCode(replace, REQUEST_CODE_RECORD_SOUND), sizeLimit);
break;
case AttachmentPagerAdapter.ADD_SLIDESHOW:
editSlideshow();
break;
case AttachmentPagerAdapter.ADD_CONTACT_AS_TEXT:
pickContacts(MultiPickContactsActivity.MODE_INFO,
REQUEST_CODE_ATTACH_ADD_CONTACT_INFO);
break;
case AttachmentPagerAdapter.ADD_CONTACT_AS_VCARD:
pickContacts(MultiPickContactsActivity.MODE_VCARD,
REQUEST_CODE_ATTACH_ADD_CONTACT_VCARD);
break;
case AttachmentPagerAdapter.ADD_TEMPLATE:
showDialog(DIALOG_IMPORT_TEMPLATE);
break;
default:
break;
}
}
public static long computeAttachmentSizeLimit(SlideshowModel slideShow, int currentSlideSize) {
// Computer attachment size limit. Subtract 1K for some text.
long sizeLimit = MmsConfig.getMaxMessageSize() - SlideshowModel.SLIDESHOW_SLOP;
if (slideShow != null) {
sizeLimit = sizeLimit -slideShow.getCurrentMessageSize()-
slideShow.getTotalTextMessageSize();
// We're about to ask the camera to capture some video (or the sound recorder
// to record some audio) which will eventually replace the content on the current
// slide. Since the current slide already has some content (which was subtracted
// out just above) and that content is going to get replaced, we can add the size of the
// current slide into the available space used to capture a video (or audio).
sizeLimit += currentSlideSize;
}
return sizeLimit;
}
private void showAttachmentSelector(final boolean replace) {
mAttachmentPager = (ViewPager) findViewById(R.id.attachments_selector_pager);
mIsReplaceAttachment = replace;
mCurrentAttachmentPager = DEFAULT_ATTACHMENT_PAGER;
hideKeyboard();
if (mAttachmentPagerAdapter == null) {
mAttachmentPagerAdapter = new AttachmentPagerAdapter(this);
}
boolean showSubject = false;
if (MmsConfig.getMmsEnabled() && mIsSmsEnabled) {
if (!isSubjectEditorVisible()) {
showSubject = true;
}
}
mAttachmentPagerAdapter.setExistAttachmentType(mWorkingMessage.hasAttachment(),
mWorkingMessage.hasVcard(), mWorkingMessage.hasSlideshow(), replace, showSubject);
mAttachmentPagerAdapter.setGridItemClickListener(new OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
if (view != null) {
addAttachment((mCurrentAttachmentPager > DEFAULT_ATTACHMENT_PAGER ? position
+ mAttachmentPagerAdapter.PAGE_GRID_COUNT : position), replace);
if (mIsRTL) {
addAttachment((mCurrentAttachmentPager > DEFAULT_ATTACHMENT_PAGER ? position
: mAttachmentPagerAdapter.PAGE_GRID_COUNT + position), replace);
}
mAttachmentSelector.setVisibility(View.GONE);
}
}
});
setAttachmentSelectorHeight();
mAttachmentPager.setAdapter(mAttachmentPagerAdapter);
mAttachmentPager.setCurrentItem(((mIsRTL) ? 1 : 0));
mCurrentAttachmentPager = ((mIsRTL) ? 1 : 0);
mAttachmentPager.setOnPageChangeListener(mAttachmentPagerChangeListener);
mAttachmentSelector.setVisibility(View.VISIBLE);
// Delay 200ms for drawing view completed.
mHandler.postDelayed(new Runnable() {
@Override
public void run() {
mAttachmentSelector.requestFocus();
}
}, 200);
}
private final OnPageChangeListener mAttachmentPagerChangeListener = new OnPageChangeListener() {
@Override
public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
}
@Override
public void onPageSelected(int position) {
updateAttachmentSelectorIndicator(position);
mCurrentAttachmentPager = position;
}
@Override
public void onPageScrollStateChanged(int state) {
}
};
private void updateAttachmentSelectorIndicator(int pagerPosition) {
ImageView pagerIndicatorFirst = (ImageView) mAttachmentSelector.findViewById(
R.id.pager_indicator_first);
ImageView pagerIndicatorSecond = (ImageView) mAttachmentSelector.findViewById(
R.id.pager_indicator_second);
if (mIsRTL) {
pagerIndicatorSecond.setImageResource(pagerPosition == 0 ? R.drawable.dot_chosen
: R.drawable.dot_unchosen);
pagerIndicatorFirst.setImageResource(pagerPosition == 0 ? R.drawable.dot_unchosen
: R.drawable.dot_chosen);
return;
}
pagerIndicatorFirst.setImageResource(pagerPosition == 0 ? R.drawable.dot_chosen
: R.drawable.dot_unchosen);
pagerIndicatorSecond.setImageResource(pagerPosition == 0 ? R.drawable.dot_unchosen
: R.drawable.dot_chosen);
}
private void setAttachmentSelectorHeight() {
// Show different lines of grid for horizontal and vertical screen.
Configuration configuration = getResources().getConfiguration();
LayoutParams params = (LayoutParams) mAttachmentPager.getLayoutParams();
int pagerHeight = (int) (mAttachmentPagerAdapter.GRID_ITEM_HEIGHT
* getResources().getDisplayMetrics().density + 0.5f);
params.height = (configuration.orientation == configuration.ORIENTATION_PORTRAIT)
? pagerHeight * 2 : pagerHeight;
mAttachmentPager.setLayoutParams(params);
}
private void resetGridColumnsCount() {
Configuration configuration = getResources().getConfiguration();
ArrayList<GridView> pagerGridViews = mAttachmentPagerAdapter.getPagerGridViews();
for (GridView grid : pagerGridViews) {
grid.setNumColumns((configuration.orientation == configuration.ORIENTATION_PORTRAIT)
? mAttachmentPagerAdapter.GRID_COLUMN_COUNT
: mAttachmentPagerAdapter.GRID_COLUMN_COUNT * 2);
}
}
@Override
protected void onActivityResult(int maskResultCode, int resultCode, Intent data) {
if (LogTag.VERBOSE) {
log("onActivityResult: requestCode=" + getRequestCode(maskResultCode) +
", resultCode=" + resultCode + ", data=" + data);
}
mWaitingForSubActivity = false; // We're back!
mShouldLoadDraft = false;
int requestCode = getRequestCode(maskResultCode);
boolean append = isAppendRequest(maskResultCode);
if (mWorkingMessage.isFakeMmsForDraft()) {
// We no longer have to fake the fact we're an Mms. At this point we are or we aren't,
// based on attachments and other Mms attrs.
mWorkingMessage.removeFakeMmsForDraft();
}
if (requestCode == REQUEST_CODE_PICK) {
mWorkingMessage.asyncDeleteDraftSmsMessage(mConversation);
}
if (requestCode == REQUEST_CODE_ADD_CONTACT) {
// The user might have added a new contact. When we tell contacts to add a contact
// and tap "Done", we're not returned to Messaging. If we back out to return to
// messaging after adding a contact, the resultCode is RESULT_CANCELED. Therefore,
// assume a contact was added and get the contact and force our cached contact to
// get reloaded with the new info (such as contact name). After the
// contact is reloaded, the function onUpdate() in this file will get called
// and it will update the title bar, etc.
if (mAddContactIntent != null) {
String address =
mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.EMAIL);
if (address == null) {
address =
mAddContactIntent.getStringExtra(ContactsContract.Intents.Insert.PHONE);
}
if (address != null) {
Contact contact = Contact.get(address, false);
if (contact != null) {
contact.reload();
}
}
}
}
if (resultCode != RESULT_OK) {
if (LogTag.VERBOSE) log("bail due to resultCode=" + resultCode);
return;
}
if (MmsConfig.isCreationModeEnabled()) {
ContentRestrictionFactory.reset();
}
switch (requestCode) {
case REQUEST_CODE_CREATE_SLIDESHOW:
if (data != null) {
WorkingMessage newMessage = WorkingMessage.load(this, data.getData());
if (newMessage != null) {
// Here we should keep the subject from the old mWorkingMessage.
setNewMessageSubject(newMessage);
mWorkingMessage = newMessage;
mWorkingMessage.setConversation(mConversation);
updateThreadIdIfRunning();
updateMmsSizeIndicator();
drawTopPanel(false);
drawBottomPanel();
updateSendButtonState();
}
}
break;
case REQUEST_CODE_TAKE_PICTURE: {
// create a file based uri and pass to addImage(). We want to read the JPEG
// data directly from file (using UriImage) instead of decoding it into a Bitmap,
// which takes up too much memory and could easily lead to OOM.
File file = new File(TempFileProvider.getScrapPath(this));
Uri uri = Uri.fromFile(file);
// Remove the old captured picture's thumbnail from the cache
MmsApp.getApplication().getThumbnailManager().removeThumbnail(uri);
addImageAsync(uri, append);
break;
}
case REQUEST_CODE_ATTACH_IMAGE: {
if (data != null) {
addImageAsync(data.getData(), append);
}
break;
}
case REQUEST_CODE_TAKE_VIDEO:
Uri videoUri = TempFileProvider.renameScrapFile(".3gp",
Integer.toString(getSlideNumber()), this);
// Remove the old captured video's thumbnail from the cache
MmsApp.getApplication().getThumbnailManager().removeThumbnail(videoUri);
addVideoAsync(videoUri, append); // can handle null videoUri
break;
case REQUEST_CODE_ATTACH_VIDEO:
if (data != null) {
addVideoAsync(data.getData(), append);
}
break;
case REQUEST_CODE_ATTACH_SOUND: {
// Attempt to add the audio to the attachment.
Uri uri = (Uri) data.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI);
if (uri == null) {
uri = data.getData();
} else if (Settings.System.DEFAULT_RINGTONE_URI.equals(uri)) {
break;
}
addAudio(uri, append);
drawBottomPanel();
break;
}
case REQUEST_CODE_RECORD_SOUND:
if (data != null) {
addAudio(data.getData(), append);
}
break;
case REQUEST_CODE_ECM_EXIT_DIALOG:
boolean outOfEmergencyMode = false;
if (data != null) {
outOfEmergencyMode = data.getBooleanExtra(EXIT_ECM_RESULT, false);
} else {
outOfEmergencyMode = resultCode == RESULT_OK;
}
LogTag.debugD("outOfEmergencyMode:" + outOfEmergencyMode);
if (outOfEmergencyMode) {
sendMessage(false);
}
break;
case REQUEST_CODE_SCM_EXIT_DIALOG:
boolean isScbmExit = resultCode == RESULT_OK;
LogTag.debugD("isScbmExit:" + isScbmExit);
if (isScbmExit) {
sendMessage(false);
}
break;
case REQUEST_CODE_PICK:
if (data != null && mRecipientsEditor != null) {
processPickResult(data);
}
break;
case REQUEST_CODE_ATTACH_REPLACE_CONTACT_INFO:
// Caused by user choose to replace the attachment, so we need remove
// the attachment and then add the contact info to text.
if (data != null) {
mWorkingMessage.removeAttachment(true);
}
case REQUEST_CODE_ATTACH_ADD_CONTACT_INFO:
if (data != null) {
String newText = mWorkingMessage.getText() +
data.getStringExtra(MultiPickContactsActivity.EXTRA_INFO);
mWorkingMessage.setText(newText);
}
break;
case REQUEST_CODE_ATTACH_ADD_CONTACT_VCARD:
if (data != null) {
String extraVCard = data.getStringExtra(MultiPickContactsActivity.EXTRA_VCARD);
if (extraVCard != null) {
Uri vcard = Uri.parse(extraVCard);
addVcard(vcard);
}
}
break;
default:
if (LogTag.VERBOSE) log("bail due to unknown requestCode=" + requestCode);
break;
}
}
/**
* Set newWorkingMessage's subject from mWorkingMessage. If we create a new
* slideshow. We will drop the old workingMessage and create a new one. And
* we should keep the subject of the old workingMessage.
*/
private void setNewMessageSubject(WorkingMessage newWorkingMessage) {
if (null != newWorkingMessage && mWorkingMessage.hasSubject()) {
newWorkingMessage.setSubject(mWorkingMessage.getSubject(), true);
}
}
private void updateMmsSizeIndicator() {
mAttachmentEditorHandler.post(mUpdateMmsSizeIndRunnable);
}
private Runnable mUpdateMmsSizeIndRunnable = new Runnable() {
@Override
public void run() {
if (mWorkingMessage.getSlideshow() != null) {
mWorkingMessage.getSlideshow().updateTotalMessageSize();
}
mAttachmentEditor.update(mWorkingMessage);
}
};
private void processPickResult(final Intent data) {
// The EXTRA_PHONE_URIS stores the phone's urls that were selected by user in the
// multiple phone picker.
Bundle bundle = data.getExtras().getBundle("result");
if (null == bundle) {
bundle = new Bundle();
}
Bundle onlyNumberBundle = data.getExtras().getBundle("result_only_number");
final Bundle numberBundle = (null != onlyNumberBundle) ? onlyNumberBundle : (new Bundle());
final Set<String> keySet = bundle.keySet();
final int recipientCount = (keySet != null) ? keySet.size() : 0;
final Set<String> numberKeySet = numberBundle.keySet();
final int numberRecipientCount = (numberKeySet != null) ? numberKeySet.size() : 0;
// If total recipients count > recipientLimit,
// then forbid add reipients to RecipientsEditor
final int recipientLimit = MmsConfig.getRecipientLimit();
int totalRecipientsCount = mExistsRecipientsCount + recipientCount + numberRecipientCount;
if (recipientLimit != Integer.MAX_VALUE && totalRecipientsCount > recipientLimit) {
new AlertDialog.Builder(this)
.setMessage(getString(R.string.too_many_recipients, totalRecipientsCount,
recipientLimit))
.setPositiveButton(android.R.string.ok, new OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// if already exists some recipients,
// then new pick recipients with exists recipients count
// can't more than recipient limit count.
int newPickRecipientsCount = recipientLimit - mExistsRecipientsCount;
if (newPickRecipientsCount <= 0) {
return;
}
if (newPickRecipientsCount >= numberRecipientCount) {
inputNumbers(numberBundle, numberKeySet, numberRecipientCount);
newPickRecipientsCount -= numberRecipientCount;
processAddRecipients(keySet, newPickRecipientsCount);
} else {
inputNumbers(numberBundle, numberKeySet, newPickRecipientsCount);
}
}
})
.setNegativeButton(android.R.string.cancel, null)
.create().show();
return;
}
inputNumbers(numberBundle, numberKeySet, numberRecipientCount);
processAddRecipients(keySet, recipientCount);
}
private void inputNumbers(final Bundle bundle,
final Set<String> keySet,
final int numberRecipientCount) {
Iterator<String> it = keySet.iterator();
int i = 0;
while (it.hasNext()) {
i++;
String number = bundle.getStringArray(it.next())[0];
mRecipientsEditor.append(number+",");
if (i == numberRecipientCount) {
break;
}
}
}
private Uri[] buildUris(final Set<String> keySet, final int newPickRecipientsCount) {
Uri[] newUris = new Uri[newPickRecipientsCount];
Iterator<String> it = keySet.iterator();
int i = 0;
while (it.hasNext()) {
String id = it.next();
newUris[i++] = ContentUris.withAppendedId(Phone.CONTENT_URI, Integer.parseInt(id));
if (i == newPickRecipientsCount) {
break;
}
}
return newUris;
}
private void processAddRecipients(final Set<String> keySet, final int newPickRecipientsCount) {
// if process pick result that is pick recipients from Contacts
mIsProcessPickedRecipients = true;
final Handler handler = new Handler();
final ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.setTitle(getText(R.string.pick_too_many_recipients));
progressDialog.setMessage(getText(R.string.adding_recipients));
progressDialog.setIndeterminate(true);
progressDialog.setCancelable(false);
final Runnable showProgress = new Runnable() {
@Override
public void run() {
progressDialog.show();
}
};
// Only show the progress dialog if we can not finish off parsing the return data in 1s,
// otherwise the dialog could flicker.
handler.postDelayed(showProgress, 1000);
new Thread(new Runnable() {
@Override
public void run() {
final ContactList list;
try {
list = ContactList.blockingGetByUris(buildUris(keySet, newPickRecipientsCount));
} finally {
handler.removeCallbacks(showProgress);
}
if (mRecipientsEditor != null) {
ContactList exsitList = mRecipientsEditor.constructContactsFromInput(true);
// Remove the repeat recipients.
if (exsitList.equals(list)) {
exsitList.clear();
list.addAll(0, exsitList);
} else {
list.removeAll(exsitList);
list.addAll(0, exsitList);
}
}
// TODO: there is already code to update the contact header
// widget and recipients
// editor if the contacts change. we can re-use that code.
final Runnable populateWorker = new Runnable() {
@Override
public void run() {
//Boolean param in order to allow the RecipientEntry to sort,
//no need for sort RecipientEntry who added by button,
//so whatever true or false.
mRecipientsEditor.populateWithAvatorDrawable(list);
// Set value for mRecipientsPickList and
// mRecipientsWatcher will update the UI.
mRecipientsPickList = list;
updateTitle(list);
// if process finished, then dismiss the progress dialog
progressDialog.dismiss();
// if populate finished, then recipients pick process
// end
mIsProcessPickedRecipients = false;
updateSendButtonState();
}
};
handler.post(populateWorker);
}
}, "ComoseMessageActivity.processPickResult").start();
}
private final ResizeImageResultCallback mResizeImageCallback = new ResizeImageResultCallback() {
// TODO: make this produce a Uri, that's what we want anyway
@Override
public void onResizeResult(PduPart part, boolean append) {
synchronized (mObjectLock) {
mResizeImageCount = mResizeImageCount - 1;
if (mResizeImageCount <= 0) {
log("finish resize all images.");
mObjectLock.notifyAll();
}
}
if (part == null) {
handleAddAttachmentError(WorkingMessage.UNKNOWN_ERROR, R.string.type_picture);
return;
}
Context context = ComposeMessageActivity.this;
PduPersister persister = PduPersister.getPduPersister(context);
int result;
Uri messageUri = mWorkingMessage.saveAsMms(true);
if (messageUri == null) {
result = WorkingMessage.UNKNOWN_ERROR;
} else {
try {
Uri dataUri = persister.persistPart(part,
ContentUris.parseId(messageUri), null);
result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, dataUri, append);
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("ResizeImageResultCallback: dataUri=" + dataUri);
}
} catch (MmsException e) {
result = WorkingMessage.UNKNOWN_ERROR;
}
}
updateMmsSizeIndicator();
drawBottomPanel();
handleAddAttachmentError(result, R.string.type_picture);
}
};
private void handleAddAttachmentError(final int error, final int mediaTypeStringId) {
if (error == WorkingMessage.OK) {
return;
}
Log.d(TAG, "handleAddAttachmentError: " + error);
runOnUiThread(new Runnable() {
@Override
public void run() {
Resources res = getResources();
String mediaType = res.getString(mediaTypeStringId);
String title, message;
switch(error) {
case WorkingMessage.UNKNOWN_ERROR:
message = res.getString(R.string.failed_to_add_media, mediaType);
Toast.makeText(ComposeMessageActivity.this, message, Toast.LENGTH_SHORT).show();
return;
case WorkingMessage.UNSUPPORTED_TYPE:
title = res.getString(R.string.unsupported_media_format, mediaType);
message = res.getString(R.string.select_different_media, mediaType);
break;
case WorkingMessage.MESSAGE_SIZE_EXCEEDED:
title = res.getString(R.string.exceed_message_size_limitation,
mediaType);
// We should better prompt the "message size limit reached,
// cannot send out message" while we send out the Mms.
if (mIsAttachmentErrorOnSend) {
message = res.getString(R.string.media_size_limit);
mIsAttachmentErrorOnSend = false;
} else {
message = res.getString(R.string.failed_to_add_media, mediaType);
}
break;
case WorkingMessage.IMAGE_TOO_LARGE:
title = res.getString(R.string.failed_to_resize_image);
message = res.getString(R.string.resize_image_error_information);
break;
case WorkingMessage.NEGATIVE_MESSAGE_OR_INCREASE_SIZE:
title = res.getString(R.string.illegal_message_or_increase_size);
message = res.getString(R.string.failed_to_add_media, mediaType);
break;
default:
throw new IllegalArgumentException("unknown error " + error);
}
MessageUtils.showErrorDialog(ComposeMessageActivity.this, title, message);
}
});
}
private void addImageAsync(final Uri uri, final boolean append) {
mInAsyncAddAttathProcess = true;
getAsyncDialog().runAsync(new Runnable() {
@Override
public void run() {
addImage(uri, append);
mInAsyncAddAttathProcess = false;
}
}, null, R.string.adding_attachments_title);
}
private void addImage(final Uri uri, final boolean append) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("addImage: append=" + append + ", uri=" + uri);
}
int result = mWorkingMessage.setAttachment(WorkingMessage.IMAGE, uri, append);
if (result == WorkingMessage.IMAGE_TOO_LARGE ||
result == WorkingMessage.MESSAGE_SIZE_EXCEEDED) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("resize image " + uri);
}
mResizeImageCount ++;
MessageUtils.resizeImageAsync(ComposeMessageActivity.this,
uri, mWorkingMessage.hasSlideshow() ? mWorkingMessage.getSlideshow()
.getCurrentMessageSize() : 0, mAttachmentEditorHandler,
mResizeImageCallback, append);
return;
}
if ((MmsConfig.isCreationModeEnabled())
&& (result == WorkingMessage.UNSUPPORTED_TYPE_WARNING)) {
final Runnable runnable = new Runnable() {
@Override
public void run() {
addImage(uri, append);
}
};
runOnUiThread(new Runnable() {
@Override
public void run() {
handleUnsupportedTypeWarning(runnable);
}
});
return;
}
updateMmsSizeIndicator();
handleAddAttachmentError(result, R.string.type_picture);
}
private void addVideoAsync(final Uri uri, final boolean append) {
mInAsyncAddAttathProcess = true;
getAsyncDialog().runAsync(new Runnable() {
@Override
public void run() {
addVideo(uri, append);
mInAsyncAddAttathProcess = false;
}
}, null, R.string.adding_attachments_title);
}
private void addVideo(final Uri uri, final boolean append) {
mInAsyncAddAttathProcess = true;
if (uri != null) {
int result = mWorkingMessage.setAttachment(WorkingMessage.VIDEO, uri, append);
if (MmsConfig.isCreationModeEnabled()) {
if (result == WorkingMessage.UNSUPPORTED_TYPE_WARNING) {
final Runnable runnable = new Runnable() {
@Override
public void run() {
addVideo(uri, append);
mInAsyncAddAttathProcess = false;
}
};
runOnUiThread(new Runnable() {
@Override
public void run() {
handleUnsupportedTypeWarning(runnable);
}
});
return;
}
if (result == WorkingMessage.MESSAGE_SIZE_EXCEEDED_WARNING) {
final Runnable runnable = new Runnable() {
@Override
public void run() {
addVideo(uri, append);
}
};
runOnUiThread(new Runnable() {
@Override
public void run() {
handleMessageSizeExceededWarning(runnable);
}
});
return;
}
}
updateMmsSizeIndicator();
handleAddAttachmentError(result, R.string.type_video);
}
}
private void addAudio(final Uri uri, final boolean append) {
if (uri != null) {
int result = mWorkingMessage.setAttachment(WorkingMessage.AUDIO, uri, append);
if (MmsConfig.isCreationModeEnabled()) {
if (result == WorkingMessage.UNSUPPORTED_TYPE_WARNING) {
final Runnable runnable = new Runnable() {
@Override
public void run() {
addAudio(uri, append);
}
};
runOnUiThread(new Runnable() {
@Override
public void run() {
handleUnsupportedTypeWarning(runnable);
}
});
return;
}
if (result == WorkingMessage.MESSAGE_SIZE_EXCEEDED_WARNING) {
final Runnable runnable = new Runnable() {
@Override
public void run() {
addAudio(uri, append);
}
};
runOnUiThread(new Runnable() {
@Override
public void run() {
handleMessageSizeExceededWarning(runnable);
}
});
return;
}
}
updateMmsSizeIndicator();
handleAddAttachmentError(result, R.string.type_audio);
}
}
private void addVcard(Uri uri) {
int result = mWorkingMessage.setAttachment(WorkingMessage.VCARD, uri, false);
handleAddAttachmentError(result, R.string.type_vcard);
}
AsyncDialog getAsyncDialog() {
if (mAsyncDialog == null) {
mAsyncDialog = new AsyncDialog(this);
}
return mAsyncDialog;
}
private boolean handleForwardedMessage() {
Intent intent = getIntent();
// If this is a forwarded message, it will have an Intent extra
// indicating so. If not, bail out.
if (!mForwardMessageMode) {
if (mConversation != null) {
mConversation.setHasMmsForward(false);
}
return false;
}
if (mConversation != null) {
mConversation.setHasMmsForward(true);
String[] recipientNumber = intent.getStringArrayExtra("msg_recipient");
mConversation.setForwardRecipientNumber(recipientNumber);
}
Uri uri = intent.getParcelableExtra("msg_uri");
if (Log.isLoggable(LogTag.APP, Log.DEBUG)) {
log("" + uri);
}
if (uri != null) {
mWorkingMessage = WorkingMessage.load(this, uri);
mWorkingMessage.setSubject(intent.getStringExtra("subject"), false);
} else {
mWorkingMessage.setText(intent.getStringExtra("sms_body"));
}
// let's clear the message thread for forwarded messages
mMsgListAdapter.changeCursor(null);
return true;
}
private Uri formatUri(Uri uri) {
if (uri == null) {
return uri;
}
String uriString = uri.toString();
if (uriString.startsWith("content://0@mms")) {
uriString = uriString.replace("content://0@mms","content://mms");
}
return Uri.parse(uriString);
}
// Handle send actions, where we're told to send a picture(s) or text.
private boolean handleSendIntent() {
Intent intent = getIntent();
Bundle extras = intent.getExtras();
if (extras == null) {
return false;
}
final String mimeType = intent.getType();
String action = intent.getAction();
if (Intent.ACTION_SEND.equals(action)) {
if (extras.containsKey(Intent.EXTRA_STREAM)) {
final Uri uri = formatUri((Uri)extras.getParcelable(Intent.EXTRA_STREAM));
getAsyncDialog().runAsync(new Runnable() {
@Override
public void run() {
MmsApp.getApplication().getThumbnailManager().removeThumbnail(uri);
MmsApp.getApplication().getThumbnailManager().removeBackingStoreThumbnail(uri);
addAttachment(mimeType, uri, false);
}
}, null, R.string.adding_attachments_title);
return true;
} else if (extras.containsKey(Intent.EXTRA_TEXT)) {
mWorkingMessage.setText(extras.getString(Intent.EXTRA_TEXT));
return true;
}
} else if (Intent.ACTION_SEND_MULTIPLE.equals(action) &&
extras.containsKey(Intent.EXTRA_STREAM)) {
SlideshowModel slideShow = mWorkingMessage.getSlideshow();
final ArrayList<Parcelable> uris = extras.getParcelableArrayList(Intent.EXTRA_STREAM);
int currentSlideCount = slideShow != null ? slideShow.size() : 0;
int importCount = uris.size();
if (importCount + currentSlideCount > MmsConfig.getMaxSlideNumber()) {
importCount = Math.min(MmsConfig.getMaxSlideNumber() - currentSlideCount,
importCount);
Toast.makeText(ComposeMessageActivity.this,
getString(R.string.too_many_attachments,
MmsConfig.getMaxSlideNumber(), importCount),
Toast.LENGTH_LONG).show();
}
// Attach all the pictures/videos asynchronously off of the UI thread.
// Show a progress dialog if adding all the slides hasn't finished
// within half a second.
final int numberToImport = importCount;
getAsyncDialog().runAsync(new Runnable() {
@Override
public void run() {
setRequestedOrientation(mIsLandscape ?
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
: ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
String type = mimeType;
for (int i = 0; i < numberToImport; i++) {
Parcelable uri = uris.get(i);
if (uri != null && "*/*".equals(mimeType)) {
type = getAttachmentMimeType((Uri) uri);
}
addAttachment(type, (Uri) uri, true);
}
updateMmsSizeIndicator();
synchronized (mObjectLock) {
if (mResizeImageCount != 0) {
waitForResizeImages();
}
}
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
}
}, null, R.string.adding_attachments_title);
return true;
}
return false;
}
private void waitForResizeImages() {
try {
log("wait for " + mResizeImageCount + " resize image finish. ");
mObjectLock.wait();
} catch (InterruptedException ex) {
// try again by virtue of the loop unless mQueryPending is false
}
}
private String getAttachmentMimeType(Uri uri) {
ContentResolver contentResolver = getContentResolver();
String attachmentType = "*/*";
if (MediaModel.isFileUri(uri)) {
String ext = MimeTypeMap.getFileExtensionFromUrl(uri.getPath());
if (MessageUtils.is3GPP(ext) || MessageUtils.is3GPP2(ext)) {
attachmentType = MessageUtils.getMimeType(uri.getPath(), ext);
Log.d(TAG, "remap file " + ext + " to " + attachmentType);
}
} else {
attachmentType = contentResolver.getType(uri);
if (TextUtils.isEmpty(attachmentType)) {
Cursor cursor = null;
try {
cursor = contentResolver.query(uri, null, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
attachmentType = cursor.getString(cursor.getColumnIndexOrThrow(
Document.COLUMN_MIME_TYPE));
}
} catch (SQLiteException e) {
Log.e(TAG, "getAttachmentMimeType " + e);
} catch (IllegalArgumentException ex) {
Log.e(TAG, "getAttachmentMimeType " + ex);
} finally {
if (null != cursor) {
cursor.close();
}
}
attachmentType = (attachmentType == null) ? "" : attachmentType;
}
}
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.d(TAG, "getAttachmentMimeType " + uri + " ="+ attachmentType);
}
return attachmentType;
}
private boolean isAudioFile(Uri uri) {
String path = uri.getPath();
String mimeType = MediaFileWrapper.getMimeTypeForFile(path);
int fileType = MediaFileWrapper.getFileTypeForMimeType(mimeType);
return MediaFileWrapper.isAudioFileType(fileType);
}
private boolean isImageFile(Uri uri) {
String path = uri.getPath();
String mimeType = MediaFileWrapper.getMimeTypeForFile(path);
int fileType = MediaFileWrapper.getFileTypeForMimeType(mimeType);
return MediaFileWrapper.isImageFileType(fileType);
}
private boolean isVideoFile(Uri uri) {
String path = uri.getPath();
String mimeType = MediaFileWrapper.getMimeTypeForFile(path);
int fileType = MediaFileWrapper.getFileTypeForMimeType(mimeType);
return MediaFileWrapper.isVideoFileType(fileType);
}
// mVideoUri will look like this: content://media/external/video/media
private static final String mVideoUri = Video.Media.getContentUri("external").toString();
// mImageUri will look like this: content://media/external/images/media
private static final String mImageUri = Images.Media.getContentUri("external").toString();
// mAudioUri will look like this: content://media/external/audio/media
private static final String mAudioUri = Audio.Media.getContentUri("external").toString();
private void addAttachment(String type, Uri uri, boolean append) {
if (uri != null) {
// When we're handling Intent.ACTION_SEND_MULTIPLE, the passed in items can be
// videos, and/or images, and/or some other unknown types we don't handle. When
// a single attachment is "shared" the type will specify an image or video. When
// there are multiple types, the type passed in is "*/*". In that case, we've got
// to look at the uri to figure out if it is an image or video.
if (MmsConfig.isCreationModeEnabled()) {
ContentRestrictionFactory.reset();
}
boolean wildcard = "*/*".equals(type);
if (type.startsWith("image/") || (wildcard && uri.toString().startsWith(mImageUri))
|| (wildcard && isImageFile(uri))) {
addImage(uri, append);
} else if (type.startsWith("video/") ||
(wildcard && uri.toString().startsWith(mVideoUri))
|| (wildcard && isVideoFile(uri))) {
addVideo(uri, append);
} else if (type.startsWith("audio/")
|| (wildcard && uri.toString().startsWith(mAudioUri))
|| (wildcard && isAudioFile(uri))) {
addAudio(uri, append);
} else if (this.getResources().getBoolean(R.bool.config_vcard)
&& (type.equals("text/x-vcard")
|| (wildcard && isVcardFile(uri)))) {
addVcard(uri);
} else {
// Add prompt when file type is not image/video/audio.
Message msg = Message.obtain(mAddAttachmentHandler,
MSG_ADD_ATTACHMENT_FAILED, uri);
mAddAttachmentHandler.sendMessage(msg);
}
}
}
// handler for handle add attachment failt.
private Handler mAddAttachmentHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case MSG_ADD_ATTACHMENT_FAILED:
Toast.makeText(ComposeMessageActivity.this,
getAttachmentPostfix((Uri) msg.obj), Toast.LENGTH_SHORT)
.show();
break;
default:
break;
}
}
private String getAttachmentPostfix(Uri uri) {
// if uri is valid,parse it as normal.
if (isValidUri(uri)) {
int lastDot = uri.toString().lastIndexOf(".");
String postfix = uri.toString().substring(lastDot + 1);
return getResourcesString(R.string.unsupported_media_format,
postfix);
} else {
// if uri is invalid,show just show unsupported "Unsupported format".
return getResources().getString(R.string.unsupported_format);
}
}
//Used to check the uri is valid or not.
private boolean isValidUri(Uri uri) {
String path = uri == null ? null : uri.toString();
if (null != path && path.contains("/")) {
String fileName = path.substring(path.lastIndexOf("/"));
if (null != fileName && !fileName.isEmpty()
&& fileName.contains(".")) {
String fileType = fileName.substring(fileName
.lastIndexOf(".") + 1);
return !TextUtils.isEmpty(fileType) && fileType.trim().length() > 0;
}
}
return false;
}
};
private String getResourcesString(int id, String mediaName) {
Resources r = getResources();
return r.getString(id, mediaName);
}
/**
* draw the compose view at the bottom of the screen.
*/
private void drawBottomPanel() {
// Reset the counter for text editor.
resetCounter();
if (mWorkingMessage.hasSlideshow()) {
if (mShowTwoButtons) {
mTextEditor.setVisibility(View.GONE);
mAttachmentEditor.requestFocus();
return;
} else {
mTextEditor.setVisibility(View.INVISIBLE);
mTextEditor.setText("");
mAttachmentEditor.hideSlideshowSendButton();
mAttachmentEditor.requestFocus();
return;
}
}
if (LOCAL_LOGV) {
Log.v(TAG, "CMA.drawBottomPanel");
}
if (mTextEditor.getVisibility() != View.VISIBLE) {
mTextEditor.setVisibility(View.VISIBLE);
}
CharSequence text = mWorkingMessage.getText();
// TextView.setTextKeepState() doesn't like null input.
if (text != null && mIsSmsEnabled) {
mTextEditor.setTextKeepState(text);
// Set the edit caret to the end of the text.
mTextEditor.setSelection(mTextEditor.length());
} else {
mTextEditor.setText("");
}
onKeyboardStateChanged();
}
private void hideBottomPanel() {
if (LOCAL_LOGV) {
Log.v(TAG, "CMA.hideBottomPanel");
}
mBottomPanel.setVisibility(View.INVISIBLE);
}
private void drawTopPanel(boolean showSubjectEditor) {
boolean showingAttachment = mAttachmentEditor.update(mWorkingMessage);
mAttachmentEditorScrollView.setVisibility(showingAttachment ? View.VISIBLE : View.GONE);
showSubjectEditor(showSubjectEditor || mWorkingMessage.hasSubject());
int subjectSize = mWorkingMessage.hasSubject()
? mWorkingMessage.getSubject().toString().getBytes().length : 0;
if (mWorkingMessage.getSlideshow()!= null) {
mWorkingMessage.getSlideshow().setSubjectSize(subjectSize);
}
if (mShowTwoButtons) {
mAttachmentEditor.hideSlideshowSendButton();
}
invalidateOptionsMenu();
onKeyboardStateChanged();
}
//==========================================================
// Interface methods
//==========================================================
@Override
public void onClick(View v) {
mIsRTL = (v.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL);
if ((v == mSendButtonSms || v == mSendButtonMms) && isPreparedForSending()) {
if (v == mSendButtonSms) {
LogTag.debugD("send SMS button clicked");
} else {
LogTag.debugD("send MMS button clicked");
}
try {
if (MessageUtils.isWfcUnavailable(getContext())) {
MessageUtils.pupConnectWifiAlertDialog(getContext());
MessageUtils.pupConnectWifiNotification(getContext());
LogTag.debugD("not send for pupConnectWifiAlertDialog");
return;
}
} catch (SettingNotFoundException e) {
e.printStackTrace();
}
if (mShowTwoButtons) {
confirmSendMessageIfNeeded(SubscriptionManagerWrapper.
getSubId(ConstantsWrapper.Phone.SUB1)[0]);
} else {
confirmSendMessageIfNeeded();
}
} else if ((v == mSendButtonSmsViewSec || v == mSendButtonMmsViewSec) &&
mShowTwoButtons && isPreparedForSending()) {
confirmSendMessageIfNeeded(SubscriptionManagerWrapper.
getSubId(ConstantsWrapper.Phone.SUB2)[0]);
} else if (v == mRecipientsPicker) {
launchMultiplePhonePicker();
} else if ((v == mAddAttachmentButton)) {
if (mAttachmentSelector.getVisibility() == View.VISIBLE && !mIsReplaceAttachment) {
mAttachmentSelector.setVisibility(View.GONE);
} else {
showAttachmentSelector(false);
if (mWorkingMessage.hasAttachment()) {
Toast.makeText(this, R.string.add_another_attachment, Toast.LENGTH_SHORT)
.show();
}
}
} else if (v == mBackView) {
exitComposeMessageActivity(new Runnable() {
@Override
public void run() {
goToConversationList();
}
});
} else if (v == mClearButton) {
mRecipientsEditor.setText("");
}
}
private void launchMultiplePhonePicker() {
Intent intent = new Intent(INTENT_MULTI_PICK_ACTION, Contacts.CONTENT_URI);
String exsitNumbers = mRecipientsEditor.getExsitNumbers();
if (!TextUtils.isEmpty(exsitNumbers)) {
intent.putExtra(ConstantsWrapper.IntentConstants.EXTRA_PHONE_URIS, exsitNumbers);
}
try {
startActivityForResult(intent, REQUEST_CODE_PICK);
} catch (ActivityNotFoundException ex) {
Toast.makeText(this, R.string.contact_app_not_found, Toast.LENGTH_SHORT).show();
}
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (event != null) {
// if shift key is down, then we want to insert the '\n' char in the TextView;
// otherwise, the default action is to send the message.
if (!event.isShiftPressed() && event.getAction() == KeyEvent.ACTION_DOWN) {
if (isPreparedForSending()) {
confirmSendMessageIfNeeded();
}
return true;
}
return false;
}
if (isPreparedForSending()) {
confirmSendMessageIfNeeded();
}
return true;
}
private final TextWatcher mTextEditorWatcher = new TextWatcher() {
private boolean mIsChanged = false;
private String mTextBefore = "";
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
if (!mIsChanged) {
mTextBefore = s.length() > 0 ? s.toString() : "";
}
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (mIsChanged) {
return;
}
if (mWorkingMessage.hasAttachment()) {
if ((before != count) && !mAttachmentEditor.canAddTextForMms(s)) {
if (mTextEditor != null) {
mIsChanged = true;
mTextEditor.setText(mTextBefore);
mIsChanged = false;
Toast.makeText(ComposeMessageActivity.this,
R.string.cannot_add_text_anymore, Toast.LENGTH_SHORT).show();
}
mAttachmentEditor.canAddTextForMms(mTextBefore);
return;
}
}
// This is a workaround for bug 1609057. Since onUserInteraction() is
// not called when the user touches the soft keyboard, we pretend it was
// called when textfields changes. This should be removed when the bug
// is fixed.
onUserInteraction();
mWorkingMessage.setText(s);
updateSendButtonState();
updateCounter(s, start, before, count);
ensureCorrectButtonHeight();
}
@Override
public void afterTextChanged(Editable s) {
}
};
/**
* Ensures that if the text edit box extends past two lines then the
* button will be shifted up to allow enough space for the character
* counter string to be placed beneath it.
*/
private void ensureCorrectButtonHeight() {
int currentTextLines = mTextEditor.getLineCount();
if (currentTextLines <= 2) {
mTextCounter.setVisibility(View.GONE);
if (mShowTwoButtons) {
mTextCounterSec.setVisibility(View.GONE);
}
}
else if (currentTextLines > 2 && mTextCounter.getVisibility() == View.GONE) {
// Making the counter invisible ensures that it is used to correctly
// calculate the position of the send button even if we choose not to
// display the text.
mTextCounter.setVisibility(View.INVISIBLE);
}
}
private final TextWatcher mSubjectEditorWatcher = new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
if (s.toString().getBytes().length <= SUBJECT_MAX_LENGTH) {
mWorkingMessage.setSubject(s, true);
updateSendButtonState();
if (s.toString().getBytes().length == SUBJECT_MAX_LENGTH
&& before < SUBJECT_MAX_LENGTH) {
Toast.makeText(ComposeMessageActivity.this,
R.string.subject_full, Toast.LENGTH_SHORT).show();
}
}
}
@Override
public void afterTextChanged(Editable s) {
if (s.toString().getBytes().length > SUBJECT_MAX_LENGTH) {
String subject = s.toString();
Toast.makeText(ComposeMessageActivity.this,
R.string.subject_full, Toast.LENGTH_SHORT).show();
while (subject.getBytes().length > SUBJECT_MAX_LENGTH) {
subject = subject.substring(0, subject.length() - 1);
}
s.clear();
s.append(subject);
}
}
};
//==========================================================
// Private methods
//==========================================================
/**
* Initialize all UI elements from resources.
*/
private void initResourceRefs() {
View inflate = getLayoutInflater().inflate(R.layout.compose_message_activity, null);
mZoomGestureOverlayView = new ZoomGestureOverlayView(this);
mZoomGestureOverlayView.addZoomListener(this);
mZoomGestureOverlayView.addView(inflate);
mZoomGestureOverlayView.setEventsInterceptionEnabled(true);
mZoomGestureOverlayView.setGestureVisible(false);
setContentView(mZoomGestureOverlayView);
mToolBar = (Toolbar) findViewById(R.id.toolbar);
setActionBar(mToolBar);
if (sPrimaryColorDark == 0) {
sPrimaryColorDark = getResources().getColor(R.color.primary_color_dark);
}
mBackView = (ImageButton)findViewById(R.id.back_view);
mBackView.setOnClickListener(this);
mDeviderView = findViewById(R.id.subject_text_editor_divider);
mMsgListView = (MessageListView) findViewById(R.id.history);
mMsgListView.setDivider(null); // no divider so we look like IM conversation.
// called to enable us to show some padding between the message list and the
// input field but when the message list is scrolled that padding area is filled
// in with message content
mMsgListView.setClipToPadding(false);
mMsgListView.setOnSizeChangedListener(new OnSizeChangedListener() {
public void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "onSizeChanged: w=" + width + " h=" + height +
" oldw=" + oldWidth + " oldh=" + oldHeight);
}
if (!mMessagesAndDraftLoaded && (oldHeight-height > SMOOTH_SCROLL_THRESHOLD)) {
// perform the delayed loading now, after keyboard opens
loadMessagesAndDraft(3);
}
// The message list view changed size, most likely because the keyboard
// appeared or disappeared or the user typed/deleted chars in the message
// box causing it to change its height when expanding/collapsing to hold more
// lines of text.
smoothScrollToEnd(false, height - oldHeight);
}
});
if (mShowTwoButtons) {
initTwoSendButton();
} else {
mBottomPanel = findViewById(R.id.bottom_panel);
mBottomPanel.setVisibility(View.VISIBLE);
mTextEditor = (EditText) findViewById(R.id.embedded_text_editor);
mTextCounter = (TextView) findViewById(R.id.text_counter);
mAddAttachmentButton = (ImageButton) findViewById(R.id.add_attachment_first);
mSendButtonMms = (ImageButton) findViewById(R.id.send_button_mms);
mSendButtonMmsText = (TextView) findViewById(R.id.send_button_mms_text);
mSendButtonSms = (ImageButton) findViewById(R.id.send_button_sms);
mAddAttachmentButton.setOnClickListener(this);
mSendButtonMms.setOnClickListener(this);
mSendButtonSms.setOnClickListener(this);
}
mTextEditor.setOnEditorActionListener(this);
mTextEditor.addTextChangedListener(mTextEditorWatcher);
SharedPreferences sp = PreferenceManager.getDefaultSharedPreferences(getContext());
mIsEnableSelectCopy = sp.getBoolean(MessagingPreferenceActivity.ENABLE_SELECTABLE_COPY,
MessagingPreferenceActivity.ENABLE_SELECTABLE_COPY_DEFAULT_VALUE);
float mTextSize = sp.getFloat(MessagingPreferenceActivity.ZOOM_MESSAGE,
MmsConfig.DEFAULT_FONT_SIZE);
mTextEditor.setTextSize((int) mTextSize + ZoomMessageListItem.DIFF_FONT_SIZE);
if (getResources().getInteger(R.integer.limit_count) == 0) {
mTextEditor.setFilters(new InputFilter[] {
new LengthFilter(MmsConfig.getMaxTextLimit())});
} else if (getResources().getInteger(R.integer.slide_text_limit_size) != 0) {
mTextEditor.setFilters(new InputFilter[] {
new LengthFilter(getResources().getInteger(R.integer.slide_text_limit_size))});
}
if (getResources().getInteger(R.integer.limit_count) == 0) {
mTextEditor.setFilters(new InputFilter[] {
new LengthFilter(MmsConfig.getMaxTextLimit())});
} else if (getResources().getInteger(R.integer.slide_text_limit_size) != 0) {
mTextEditor.setFilters(new InputFilter[] {
new LengthFilter(getResources().getInteger(R.integer.slide_text_limit_size))});
}
mTextEditor.setOnFocusChangeListener(new View.OnFocusChangeListener() {
@Override
public void onFocusChange(View v, boolean hasFocus) {
if (hasFocus && mAttachmentSelector.getVisibility() == View.VISIBLE) {
mAttachmentSelector.setVisibility(View.GONE);
}
}
});
mTopPanel = findViewById(R.id.recipients_subject_linear);
mTopPanel.setFocusable(false);
mAttachmentEditor = (AttachmentEditor) findViewById(R.id.attachment_editor);
mAttachmentEditor.setHandler(mAttachmentEditorHandler);
mAttachmentEditorScrollView = findViewById(R.id.attachment_editor_scroll_view);
mAttachmentSelector = findViewById(R.id.attachments_selector);
}
private void initTwoSendButton() {
mBottomPanel = findViewById(R.id.bottom_panel_btnstyle);
mBottomPanel.setVisibility(View.VISIBLE);
mTextEditor = (EditText) findViewById(R.id.embedded_text_editor_btnstyle);
mTextCounter = (TextView) findViewById(R.id.first_text_counter);
mAddAttachmentButton = (ImageButton) findViewById(R.id.add_attachment_second);
mSendButtonMms = (ImageButton) findViewById(R.id.first_send_button_mms_view);
mSendButtonSms = (ImageButton) findViewById(R.id.first_send_button_sms_view);
mSendLayoutMmsFir = findViewById(R.id.first_send_button_mms);
mSendLayoutSmsFir = findViewById(R.id.first_send_button_sms);
mIndicatorForSimMmsFir = (ImageView) findViewById(R.id.first_sim_card_indicator_mms);
mIndicatorForSimSmsFir = (ImageView) findViewById(R.id.first_sim_card_indicator_sms);
mIndicatorForSimMmsFir.setImageDrawable(MessageUtils
.getMultiSimIcon(this, ConstantsWrapper.Phone.SUB1));
mIndicatorForSimSmsFir.setImageDrawable(MessageUtils
.getMultiSimIcon(this, ConstantsWrapper.Phone.SUB1));
mAddAttachmentButton.setOnClickListener(this);
mSendButtonMms.setOnClickListener(this);
mSendButtonSms.setOnClickListener(this);
mTextCounterSec = (TextView) findViewById(R.id.second_text_counter);
mSendButtonMmsViewSec = (TextView) findViewById(R.id.second_send_button_mms_view);
mSendButtonSmsViewSec = (ImageButton) findViewById(R.id.second_send_button_sms_view);
mSendLayoutMmsSec = findViewById(R.id.second_send_button_mms);
mSendLayoutSmsSec = findViewById(R.id.second_send_button_sms);
mIndicatorForSimMmsSec = (ImageView) findViewById(R.id.second_sim_card_indicator_mms);
mIndicatorForSimSmsSec = (ImageView) findViewById(R.id.second_sim_card_indicator_sms);
mIndicatorForSimMmsSec.setImageDrawable(MessageUtils
.getMultiSimIcon(this, ConstantsWrapper.Phone.SUB2));
mIndicatorForSimSmsSec.setImageDrawable(MessageUtils
.getMultiSimIcon(this, ConstantsWrapper.Phone.SUB2));
mSendButtonMmsViewSec.setOnClickListener(this);
mSendButtonSmsViewSec.setOnClickListener(this);
}
private void confirmDeleteDialog(OnClickListener listener, boolean locked) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setCancelable(true);
builder.setMessage(locked ? R.string.confirm_delete_locked_message :
R.string.confirm_delete_message);
builder.setPositiveButton(R.string.delete, listener);
builder.setNegativeButton(R.string.no, null);
builder.show();
}
void undeliveredMessageDialog(long date) {
String body;
if (date >= 0) {
body = getString(R.string.undelivered_msg_dialog_body,
MessageUtils.formatTimeStampString(this, date));
} else {
// FIXME: we can not get sms retry time.
body = getString(R.string.undelivered_sms_dialog_body);
}
Toast.makeText(this, body, Toast.LENGTH_LONG).show();
}
private Runnable messageListQueryRunnable = new Runnable() {
@Override
public void run() {
startMsgListQuery(MESSAGE_LIST_QUERY_TOKEN);
}
};
private void startMsgListQuery() {
mHandler.removeCallbacks(messageListQueryRunnable);
mHandler.postDelayed(messageListQueryRunnable, 200);
}
private void startMsgListQuery(int token) {
if (mSendDiscreetMode || MessageUtils.isMailboxMode()) {
return;
}
Uri conversationUri = mConversation.getUri();
if (conversationUri == null) {
log("##### startMsgListQuery: conversationUri is null, bail!");
return;
}
long threadId = mConversation.getThreadId();
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("startMsgListQuery for " + conversationUri + ", threadId=" + threadId +
" token: " + token + " mConversation: " + mConversation);
}
// Cancel any pending queries
mBackgroundQueryHandler.cancelOperation(token);
try {
// Kick off the new query
mBackgroundQueryHandler.startQuery(
token,
threadId /* cookie */,
conversationUri,
PROJECTION,
null, null, null);
} catch (SQLiteException e) {
SqliteWrapper.checkSQLiteException(this, e);
}
}
private void initMessageList() {
if (mMsgListAdapter != null) {
LogTag.debugD("setOnDataSetChangedListener");
mMsgListAdapter.setOnDataSetChangedListener(mDataSetChangedListener);
return;
}
String highlightString = getIntent().getStringExtra("highlight");
Pattern highlight = highlightString == null
? null
: Pattern.compile("\\b" + Pattern.quote(highlightString), Pattern.CASE_INSENSITIVE);
// Initialize the list adapter with a null cursor.
mMsgListAdapter = new MessageListAdapter(this, null, mMsgListView, true, highlight);
mMsgListAdapter.setIsMsimIccCardActived(MessageUtils.isMsimIccCardActive());
mMsgListAdapter.setOnDataSetChangedListener(mDataSetChangedListener);
mMsgListAdapter.setMsgListItemHandler(mMessageListItemHandler);
mMsgListAdapter.setMMSAudioPlayer(mMMSAudioPlayer);
mMsgListView.setAdapter(mMsgListAdapter);
mMsgListView.setItemsCanFocus(false);
mMsgListView.setVisibility((mSendDiscreetMode || MessageUtils.isMailboxMode())
? View.INVISIBLE : View.VISIBLE);
mMsgListView.setOnItemClickListener(new OnItemDoubleClickListener() {
@Override
public void onItemSingleClick(AdapterView<?> parent, View view, int position, long id) {
if (view != null && view instanceof MessageListItem) {
((MessageListItem) view).onMessageListItemClick();
}
}
@Override
public void onItemDoubleClick(AdapterView<?> parent, View view, int position, long id) {
if (mIsEnableSelectCopy && view != null && view instanceof MessageListItem) {
((MessageListItem) view).startSelectableCopyActivity();
}
}
});
mModeCallback = new ModeCallback();
mMsgListView.setMultiChoiceModeListener(mModeCallback);
mMsgListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
}
/**
* Load the draft
*
* If mWorkingMessage has content in memory that's worth saving, return false.
* Otherwise, call the async operation to load draft and return true.
*/
private boolean loadDraft() {
if (mWorkingMessage.isWorthSaving()) {
Log.w(TAG, "CMA.loadDraft: called with non-empty working message, bail");
if (mConversation.hasDraft() &&
mConversation.getMessageCount() == 0) {
mWorkingMessage.asyncDeleteDraftSmsMessage(mConversation);
mConversation.clearThreadId();
mWorkingMessage.setConversation(mConversation);
}
return false;
}
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("CMA.loadDraft");
}
mWorkingMessage = WorkingMessage.loadDraft(this, mConversation,
new Runnable() {
@Override
public void run() {
updateMmsSizeIndicator();
// It decides whether or not to display the subject editText view,
// according to the situation whether there's subject
// or the editText view is visible before leaving it.
drawTopPanel(isSubjectEditorVisible());
drawBottomPanel();
updateSendButtonState();
}
});
// WorkingMessage.loadDraft() can return a new WorkingMessage object that doesn't
// have its conversation set. Make sure it is set.
mWorkingMessage.setConversation(mConversation);
return true;
}
private void saveDraft(boolean isStopping) {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
LogTag.debug("saveDraft");
}
// TODO: Do something better here. Maybe make discard() legal
// to call twice and make isEmpty() return true if discarded
// so it is caught in the clause above this one?
if (mWorkingMessage.isDiscarded()) {
return;
}
if ((!mWaitingForSubActivity &&
!mWorkingMessage.isWorthSaving() &&
(!isRecipientsEditorVisible() || recipientCount() == 0)) ||
MessageUtils.checkIsPhoneMemoryFull(this)) {
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("not worth saving, discard WorkingMessage and bail");
}
mWorkingMessage.discard();
return;
}
mWorkingMessage.saveDraft(isStopping);
if (mToastForDraftSave) {
Toast.makeText(this, R.string.message_saved_as_draft,
Toast.LENGTH_SHORT).show();
}
}
private boolean isPreparedForSending() {
if (mIsAirplaneModeOn && !isImsRegistered()) {
LogTag.debugD("airplane mode on and ims not registered");
return false;
}
int recipientCount = recipientCount();
if (getContext().getResources().getBoolean(R.bool.enable_send_blank_message)) {
Log.d(TAG, "Blank SMS");
return (MessageUtils.getActivatedIccCardCount() > 0
|| isCdmaNVMode()) && recipientCount > 0
&& recipientCount <= MmsConfig.getRecipientLimit()
&& mIsSmsEnabled;
} else {
return (MessageUtils.getActivatedIccCardCount() > 0
|| isCdmaNVMode()
|| isImsRegistered())
&& recipientCount > 0
&& recipientCount <= MmsConfig.getRecipientLimit()
&& mIsSmsEnabled
&& (mWorkingMessage.hasAttachment()
|| mWorkingMessage.hasText() || mWorkingMessage.hasSubject());
}
}
private boolean isImsRegistered() {
boolean isImsReg = false;
TelephonyManager tm =
(TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
if ((tm.getPhoneCount()) > 1 && MessageUtils.isMsimIccCardActive()) {
int defaultSubId = SubscriptionManager.getDefaultSmsSubscriptionId();
LogTag.debugD("isImsRegistered: defaultSubId: " + defaultSubId);
if (SubscriptionManager.isValidSubscriptionId(defaultSubId)) {
isImsReg = tm.isImsRegistered(defaultSubId);
} else {
int subId = SubscriptionManagerWrapper.getSubIdBySlotId(0);
LogTag.debugD("isImsRegistered: phoneId 0, subId: " + subId);
if (subId == SubscriptionManagerWrapper.INVALID_SUBSCRIPTION_ID) {
subId = SubscriptionManagerWrapper.getSubIdBySlotId(1);
LogTag.debugD("isImsRegistered: phoneId 1, subId: " + subId);
}
isImsReg = tm.isImsRegistered(subId);
}
} else {
isImsReg = tm.isImsRegistered();
}
LogTag.debugD("isImsRegistered: is IMS register ? " + isImsReg);
return isImsReg;
}
private BroadcastReceiver mAirplaneModeBroadcastReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String action = intent.getAction();
if (action.equals(Intent.ACTION_AIRPLANE_MODE_CHANGED)) {
mIsAirplaneModeOn = intent.getBooleanExtra("state", false);
updateSendButtonState();
}
}
};
private boolean isCdmaNVMode() {
TelephonyManager tm = (TelephonyManager)getSystemService(
Context.TELEPHONY_SERVICE);
if (TelephonyManagerWrapper.isMultiSimEnabled(tm)) {
Log.d(TAG, "isCdmaNVMode: CDMA NV mode just for single SIM");
return false;
}
int activePhoneType = TelephonyManagerWrapper.getCurrentPhoneType(tm);
int cdmaSubscriptionMode = Settings.Global.getInt(getContentResolver(),
ConstantsWrapper.SettingsGlobal.CDMA_SUBSCRIPTION_MODE, CDMA_SUBSCRIPTION_NV);
Log.d(TAG, "isCdmaNVMode: activePhoneType=" + activePhoneType + " cdmaSubscriptionMode="
+ cdmaSubscriptionMode);
if ((activePhoneType == TelephonyManager.PHONE_TYPE_CDMA) &&
cdmaSubscriptionMode == CDMA_SUBSCRIPTION_NV) {
return true;
}
return false;
}
private int recipientCount() {
int recipientCount;
// To avoid creating a bunch of invalid Contacts when the recipients
// editor is in flux, we keep the recipients list empty. So if the
// recipients editor is showing, see if there is anything in it rather
// than consulting the empty recipient list.
if (isRecipientsEditorVisible()) {
recipientCount = mRecipientsEditor.getRecipientCount();
} else {
recipientCount = getRecipients().size();
}
return recipientCount;
}
private boolean checkMessageSizeExceeded(){
int messageSizeLimit = MmsConfig.getMaxMessageSize();
int mmsCurrentSize = 0;
boolean indicatorSizeOvered = false;
SlideshowModel slideShow = mWorkingMessage.getSlideshow();
if (slideShow != null) {
mmsCurrentSize = slideShow.getTotalMessageSize();
// The AttachmentEditor only can edit text if there only one silde.
// And the slide already includes text size, need to recalculate the total size.
if (mWorkingMessage.hasText() && slideShow.size() == 1) {
int totalTextSize = slideShow.getTotalTextMessageSize();
int currentTextSize = mWorkingMessage.getText().toString().getBytes().length;
int subjectSize = slideShow.getSubjectSize();
mmsCurrentSize = mmsCurrentSize - totalTextSize + currentTextSize;
indicatorSizeOvered = getSizeWithOverHead(mmsCurrentSize + subjectSize)
> (MmsConfig.getMaxMessageSize() / KILOBYTE);
}
} else if (mWorkingMessage.hasText()) {
mmsCurrentSize = mWorkingMessage.getText().toString().getBytes().length;
}
Log.v(TAG, "compose mmsCurrentSize = " + mmsCurrentSize
+ ", indicatorSizeOvered = " + indicatorSizeOvered);
// Mms max size is 300k, but we reserved 1k just in case there are other over size problem.
// In this way, here the first condition will always false.
// Therefore add indicatorSizeOvered in it.
// If indicator displays larger than 300k, it can not send this Mms.
if (mmsCurrentSize > messageSizeLimit || indicatorSizeOvered) {
mIsAttachmentErrorOnSend = true;
handleAddAttachmentError(WorkingMessage.MESSAGE_SIZE_EXCEEDED,
R.string.type_picture);
return true;
}
return false;
}
private int getSizeWithOverHead(int size) {
return (size + KILOBYTE -1) / KILOBYTE + 1;
}
private ContactList getRecipientForEmergency() {
ContactList contactList;
if (isRecipientsEditorVisible()) {
contactList = mWorkingMessage.getWorkingContactLists();
} else {
contactList = mConversation.getRecipients();
}
return contactList;
}
public boolean isEmergencySmsSupport() {
PersistableBundle b;
boolean eSmsCarrierSupport = false;
ContactList recipients = getRecipientForEmergency();
if (recipients == null || recipients.size() != 1) {
Log.d(TAG, "recipient is invalid");
return false;
}
int subId = mWorkingMessage.mCurrentConvSubId;
Log.d(TAG, "isEmergencySmsSupport subId: " + subId);
if (!PhoneNumberUtils.isLocalEmergencyNumber(getApplicationContext(),
subId, recipients.get(0).getNumber())) {
Log.d(TAG, "isEmergencySmsSupport not an emergency number");
return false;
}
CarrierConfigManager configManager = (CarrierConfigManager) getApplicationContext()
.getSystemService(Context.CARRIER_CONFIG_SERVICE);
if (configManager == null) {
Log.d(TAG, "isEmergencySmsSupport configManager is null");
return false;
}
b = configManager.getConfigForSubId(subId);
if (b == null) {
Log.d(TAG, "isEmergencySmsSupport PersistableBundle is null");
return false;
}
eSmsCarrierSupport = b.getBoolean(
CarrierConfigManager.KEY_SUPPORT_EMERGENCY_SMS_OVER_IMS_BOOL);
Log.d(TAG,"isEmergencySmsSupport emergencySmsCarrierSupport: " + eSmsCarrierSupport);
return eSmsCarrierSupport;
}
private void sendMessage(boolean bCheckEcmMode) {
// Check message size, if >= max message size, do not send message.
if(checkMessageSizeExceeded()){
LogTag.debugD("MessageSizeExceeded");
return;
}
if (bCheckEcmMode) {
// TODO: expose this in telephony layer for SDK build
boolean inEcm = TelephonyProperties.in_ecm_mode().orElse(false);
boolean inScm = SystemProperties.getBoolean("ril.inscbm", false);
Log.d(TAG,"ecm mode: " + inEcm);
Log.d(TAG,"scm mode: " + inScm);
if ((inEcm || inScm) && !isEmergencySmsSupport()) {
try {
if (inEcm) {
startActivityForResult(
new Intent(ConstantsWrapper.TelephonyIntent.
ACTION_SHOW_NOTICE_ECM_BLOCK_OTHERS, null),
REQUEST_CODE_ECM_EXIT_DIALOG);
} else {
startActivityForResult(
new Intent(ConstantsWrapper.TelephonyIntent.
ACTION_SHOW_NOTICE_SCM_BLOCK_OTHERS, null),
REQUEST_CODE_SCM_EXIT_DIALOG);
}
return;
} catch (ActivityNotFoundException e) {
// continue to send message
Log.e(TAG, "Cannot find EmergencyCallbackModeExitDialog", e);
}
}
}
// Make the recipients editor lost focus, recipients editor will shrink
// and filter useless char in recipients to avoid send sms failed.
if (isRecipientsEditorVisible()
&& mRecipientsEditor.isFocused()
&& !mWorkingMessage.requiresMms()) {
mTextEditor.requestFocus();
}
if (!mSendingMessage) {
if (LogTag.SEVERE_WARNING) {
String sendingRecipients = mConversation.getRecipients().serialize();
if (!sendingRecipients.equals(mDebugRecipients)) {
String workingRecipients = mWorkingMessage.getWorkingRecipients();
if (workingRecipients != null && !mDebugRecipients.equals(workingRecipients)) {
LogTag.warnPossibleRecipientMismatch("ComposeMessageActivity.sendMessage" +
" recipients in window: \"" +
mDebugRecipients + "\" differ from recipients from conv: \"" +
sendingRecipients + "\" and working recipients: " +
workingRecipients, this);
}
}
sanityCheckConversation();
}
// send can change the recipients. Make sure we remove the listeners first and then add
// them back once the recipient list has settled.
removeRecipientsListeners();
if (mWorkingMessage.getResendMultiRecipients()) {
// If resend sms recipient is more than one, use mResendSmsRecipient
LogTag.debugD("mWorkingMessage send mResendSmsRecipient=" + mResendSmsRecipient);
mWorkingMessage.send(mResendSmsRecipient);
} else {
LogTag.debugD("mWorkingMessage send mDebugRecipients=" + mDebugRecipients);
mWorkingMessage.send(mDebugRecipients);
}
mSentMessage = true;
mSendingMessage = true;
addRecipientsListeners();
mScrollOnSend = true; // in the next onQueryComplete, scroll the list to the end.
}
// But bail out if we are supposed to exit after the message is sent.
if (mSendDiscreetMode || MessageUtils.isMailboxMode()) {
finish();
}
}
private void resetMessage() {
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("resetMessage");
}
// Make the attachment editor hide its view.
mAttachmentEditor.hideView();
mAttachmentEditorScrollView.setVisibility(View.GONE);
// Hide the subject editor.
showSubjectEditor(false);
// Focus to the text editor.
mTextEditor.requestFocus();
// We have to remove the text change listener while the text editor gets cleared and
// we subsequently turn the message back into SMS. When the listener is listening while
// doing the clearing, it's fighting to update its counts and itself try and turn
// the message one way or the other.
mTextEditor.removeTextChangedListener(mTextEditorWatcher);
// Clear the text box.
TextKeyListener.clear(mTextEditor.getText());
mWorkingMessage.clearConversation(mConversation, false);
mWorkingMessage = WorkingMessage.createEmpty(this);
mWorkingMessage.setConversation(mConversation);
hideRecipientEditor();
drawBottomPanel();
// "Or not", in this case.
updateSendButtonState();
// Our changes are done. Let the listener respond to text changes once again.
mTextEditor.addTextChangedListener(mTextEditorWatcher);
// Close the soft on-screen keyboard if we're in landscape mode so the user can see the
// conversation.
if (mIsLandscape || isInMultiWindowMode()) {
hideKeyboard();
}
mLastRecipientCount = 0;
mSendingMessage = false;
invalidateOptionsMenu();
if (mAttachmentSelector.getVisibility() == View.VISIBLE) {
mAttachmentSelector.setVisibility(View.GONE);
}
}
private void hideKeyboard() {
InputMethodManager inputMethodManager =
(InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
inputMethodManager.hideSoftInputFromWindow(mTextEditor.getWindowToken(), 0);
}
private void updateSendButtonState() {
boolean enable = false;
if (isPreparedForSending()) {
enable = true;
}
boolean requiresMms = mWorkingMessage.requiresMms();
if (mShowTwoButtons) {
View[] sendButtons = showTwoSmsOrMmsSendButton(requiresMms);
if (sendButtons[ConstantsWrapper.Phone.SUB1] == mSendLayoutMmsFir
&& sendButtons[ConstantsWrapper.Phone.SUB2] == mSendLayoutMmsSec) {
mSendButtonMms.setEnabled(enable);
mSendButtonMmsViewSec.setEnabled(enable);
mSendButtonMms.setFocusable(enable);
mSendButtonMmsViewSec.setFocusable(enable);
} else if (sendButtons[ConstantsWrapper.Phone.SUB1] == mSendLayoutSmsFir
&& sendButtons[ConstantsWrapper.Phone.SUB2] == mSendLayoutSmsSec) {
mSendButtonSms.setEnabled(enable);
mSendButtonSmsViewSec.setEnabled(enable);
mSendButtonSms.setFocusable(enable);
mSendButtonSmsViewSec.setFocusable(enable);
}
} else {
View sendButton = showSmsOrMmsSendButton(requiresMms);
sendButton.setEnabled(enable);
sendButton.setFocusable(enable);
}
setSendButtonImage();
}
private void setSendButtonImage() {
Contact contact = Contact.getMe(false);
if (sDefaultContactImage == null) {
sDefaultContactImage = this.getResources().getDrawable(R.drawable.default_avatar);
}
mAvatarDrawable = contact.getAvatar(this, sDefaultContactImage);
if (mAvatarDrawable.equals(sDefaultContactImage)) {
if (mWorkingMessage.requiresMms()) {
mSendButtonMms.setBackground(this.getResources().getDrawable(
R.drawable.send_arrow_background));
mSendButtonMms.setImageDrawable(this.getResources().getDrawable(
R.drawable.send_button_selector));
mSendButtonMms.setScaleType(ImageButton.ScaleType.CENTER);
} else {
mSendButtonSms.setImageDrawable(this.getResources().getDrawable(
R.drawable.send_button_selector));
mSendButtonSms.setBackground(this.getResources().getDrawable(
R.drawable.send_arrow_background));
mSendButtonSms.setScaleType(ImageButton.ScaleType.CENTER);
}
} else {
if (mWorkingMessage.requiresMms()) {
if (mSendButtonMms.isEnabled()) {
mSendButtonMms.setScaleType(ImageButton.ScaleType.CENTER);
mSendButtonMms.setImageDrawable(getResources().getDrawable(R.drawable.ic_send));
mSendButtonMms.setBackground(this.getResources().getDrawable(
R.drawable.send_arrow_background));
} else {
mSendButtonMms.setScaleType(ImageButton.ScaleType.FIT_CENTER);
mSendButtonMms.setImageDrawable(mAvatarDrawable);
mSendButtonMms.setBackground(null);
}
} else {
if (mSendButtonSms.isEnabled()) {
mSendButtonSms.setScaleType(ImageButton.ScaleType.CENTER);
mSendButtonSms.setImageDrawable(getResources().getDrawable(R.drawable.ic_send));
mSendButtonSms.setBackground(this.getResources().getDrawable(
R.drawable.send_arrow_background));
} else {
mSendButtonSms.setScaleType(ImageButton.ScaleType.FIT_CENTER);
mSendButtonSms.setImageDrawable(mAvatarDrawable);
mSendButtonSms.setBackground(null);
}
}
}
}
private long getMessageDate(Uri uri) {
if (uri != null) {
Cursor cursor = SqliteWrapper.query(this, mContentResolver,
uri, new String[] { Mms.DATE }, null, null, null);
if (cursor != null) {
try {
if ((cursor.getCount() == 1) && cursor.moveToFirst()) {
return cursor.getLong(0) * 1000L;
}
} finally {
cursor.close();
}
}
}
return NO_DATE_FOR_DIALOG;
}
private void initActivityState(Bundle bundle) {
Intent intent = getIntent();
if (bundle != null) {
setIntent(getIntent().setAction(Intent.ACTION_VIEW));
String recipients = bundle.getString(RECIPIENTS);
if (LogTag.VERBOSE) log("get mConversation by recipients " + recipients);
mConversation = Conversation.get(this,
ContactList.getByNumbers(recipients,
false /* don't block */, true /* replace number */), false);
addRecipientsListeners();
mSendDiscreetMode = bundle.getBoolean(KEY_EXIT_ON_SENT, false);
mForwardMessageMode = bundle.getBoolean(KEY_FORWARDED_MESSAGE, false);
if (mSendDiscreetMode) {
mMsgListView.setVisibility(View.INVISIBLE);
}
mWorkingMessage.readStateFromBundle(bundle);
return;
}
// If we have been passed a thread_id, use that to find our conversation.
long threadId = intent.getLongExtra(THREAD_ID, 0);
if (threadId > 0) {
if (LogTag.VERBOSE) log("get mConversation by threadId " + threadId);
mConversation = Conversation.get(this, threadId, false);
} else {
Uri intentData = intent.getData();
if (intentData != null) {
// try to get a conversation based on the data URI passed to our intent.
if (LogTag.VERBOSE) log("get mConversation by intentData " + intentData);
mConversation = Conversation.get(this, intentData, false);
mWorkingMessage.setText(getBody(intentData));
} else {
// special intent extra parameter to specify the address
String address = intent.getStringExtra("address");
if (!TextUtils.isEmpty(address)) {
if (LogTag.VERBOSE) log("get mConversation by address " + address);
mConversation = Conversation.get(this, ContactList.getByNumbers(address,
false /* don't block */, true /* replace number */), false);
} else {
if (LogTag.VERBOSE) log("create new conversation");
mConversation = Conversation.createNew(this);
}
}
}
addRecipientsListeners();
updateThreadIdIfRunning();
mSendDiscreetMode = intent.getBooleanExtra(KEY_EXIT_ON_SENT, false);
mForwardMessageMode = intent.getBooleanExtra(KEY_FORWARDED_MESSAGE, false);
mReplyMessageMode = intent.getBooleanExtra(KEY_REPLY_MESSAGE, false);
if (mSendDiscreetMode) {
mMsgListView.setVisibility(View.INVISIBLE);
}
if (intent.hasExtra("sms_body")) {
mWorkingMessage.setText(intent.getStringExtra("sms_body"));
}
mWorkingMessage.setSubject(intent.getStringExtra("subject"), false);
}
private void initFocus() {
if (!mIsKeyboardOpen) {
return;
}
// If the recipients editor is visible, there is nothing in it,
// and the text editor is not already focused, focus the
// recipients editor.
if (isRecipientsEditorVisible()
&& !mTextEditor.isFocused()) {
mRecipientsEditor.requestFocus();
return;
}
// If we decided not to focus the recipients editor, focus the text editor.
mTextEditor.requestFocus();
}
private final MessageListAdapter.OnDataSetChangedListener
mDataSetChangedListener = new MessageListAdapter.OnDataSetChangedListener() {
@Override
public void onDataSetChanged(MessageListAdapter adapter) {
}
@Override
public void onContentChanged(MessageListAdapter adapter) {
startMsgListQuery();
}
};
/**
* smoothScrollToEnd will scroll the message list to the bottom if the list is already near
* the bottom. Typically this is called to smooth scroll a newly received message into view.
* It's also called when sending to scroll the list to the bottom, regardless of where it is,
* so the user can see the just sent message. This function is also called when the message
* list view changes size because the keyboard state changed or the compose message field grew.
*
* @param force always scroll to the bottom regardless of current list position
* @param listSizeChange the amount the message list view size has vertically changed
*/
private void smoothScrollToEnd(boolean force, int listSizeChange) {
int lastItemVisible = mMsgListView.getLastVisiblePosition();
int lastItemInList = mMsgListAdapter.getCount() - 1;
if (lastItemVisible < 0 || lastItemInList < 0) {
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "smoothScrollToEnd: lastItemVisible=" + lastItemVisible +
", lastItemInList=" + lastItemInList +
", mMsgListView not ready");
}
return;
}
View lastChildVisible =
mMsgListView.getChildAt(lastItemVisible - mMsgListView.getFirstVisiblePosition());
int lastVisibleItemBottom = 0;
int lastVisibleItemHeight = 0;
if (lastChildVisible != null) {
lastVisibleItemBottom = lastChildVisible.getBottom();
lastVisibleItemHeight = lastChildVisible.getHeight();
}
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "smoothScrollToEnd newPosition: " + lastItemInList +
" mLastSmoothScrollPosition: " + mLastSmoothScrollPosition +
" first: " + mMsgListView.getFirstVisiblePosition() +
" lastItemVisible: " + lastItemVisible +
" lastVisibleItemBottom: " + lastVisibleItemBottom +
" lastVisibleItemBottom + listSizeChange: " +
(lastVisibleItemBottom + listSizeChange) +
" mMsgListView.getHeight() - mMsgListView.getPaddingBottom(): " +
(mMsgListView.getHeight() - mMsgListView.getPaddingBottom()) +
" listSizeChange: " + listSizeChange);
}
// Only scroll if the list if we're responding to a newly sent message (force == true) or
// the list is already scrolled to the end. This code also has to handle the case where
// the listview has changed size (from the keyboard coming up or down or the message entry
// field growing/shrinking) and it uses that grow/shrink factor in listSizeChange to
// compute whether the list was at the end before the resize took place.
// For example, when the keyboard comes up, listSizeChange will be negative, something
// like -524. The lastChild listitem's bottom value will be the old value before the
// keyboard became visible but the size of the list will have changed. The test below
// add listSizeChange to bottom to figure out if the old position was already scrolled
// to the bottom. We also scroll the list if the last item is taller than the size of the
// list. This happens when the keyboard is up and the last item is an mms with an
// attachment thumbnail, such as picture. In this situation, we want to scroll the list so
// the bottom of the thumbnail is visible and the top of the item is scroll off the screen.
int listHeight = mMsgListView.getHeight();
boolean lastItemTooTall = lastVisibleItemHeight > listHeight;
boolean willScroll = force ||
((listSizeChange != 0 || lastItemInList != mLastSmoothScrollPosition) &&
lastVisibleItemBottom + listSizeChange <=
listHeight - mMsgListView.getPaddingBottom());
if (willScroll || (lastItemTooTall && lastItemInList == lastItemVisible)) {
if (Math.abs(listSizeChange) > SMOOTH_SCROLL_THRESHOLD) {
// When the keyboard comes up, the window manager initiates a cross fade
// animation that conflicts with smooth scroll. Handle that case by jumping the
// list directly to the end.
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "keyboard state changed. setSelection=" + lastItemInList);
}
if (lastItemTooTall) {
// If the height of the last item is taller than the whole height of the list,
// we need to scroll that item so that its top is negative or above the top of
// the list. That way, the bottom of the last item will be exposed above the
// keyboard.
mMsgListView.setSelectionFromTop(lastItemInList,
listHeight - lastVisibleItemHeight);
} else {
mMsgListView.setSelection(lastItemInList);
}
} else if (lastItemInList - lastItemVisible > MAX_ITEMS_TO_INVOKE_SCROLL_SHORTCUT) {
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "too many to scroll, setSelection=" + lastItemInList);
}
mMsgListView.setSelection(lastItemInList);
} else {
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
Log.v(TAG, "smooth scroll to " + lastItemInList);
}
if (lastItemTooTall) {
// If the height of the last item is taller than the whole height of the list,
// we need to scroll that item so that its top is negative or above the top of
// the list. That way, the bottom of the last item will be exposed above the
// keyboard. We should use smoothScrollToPositionFromTop here, but it doesn't
// seem to work -- the list ends up scrolling to a random position.
mMsgListView.setSelectionFromTop(lastItemInList,
listHeight - lastVisibleItemHeight);
} else {
mMsgListView.smoothScrollToPosition(lastItemInList);
}
mLastSmoothScrollPosition = lastItemInList;
}
}
}
private final class BackgroundQueryHandler extends ConversationQueryHandler {
public BackgroundQueryHandler(ContentResolver contentResolver) {
super(contentResolver);
}
@Override
protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
if (ComposeMessageActivity.this.isFinishing()) {
Log.w(TAG, "ComposeMessageActivity is finished, do nothing ");
if (cursor != null) {
cursor.close();
}
return;
}
switch(token) {
case MESSAGE_LIST_QUERY_TOKEN:
mConversation.blockMarkAsRead(false);
// check consistency between the query result and 'mConversation'
long tid = (Long) cookie;
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("##### onQueryComplete: msg history result for threadId " + tid);
}
if (tid != mConversation.getThreadId()) {
if (mConversation.getThreadId() == 0) {
// Do nothing
// mConversation.setThreadId(tid);
} else {
log("onQueryComplete: msg history query result is for threadId " +
tid + ", but mConversation has threadId " +
mConversation.getThreadId() + " starting a new query");
if (cursor != null) {
cursor.close();
}
startMsgListQuery();
return;
}
}
// check consistency b/t mConversation & mWorkingMessage.mConversation
ComposeMessageActivity.this.sanityCheckConversation();
int newSelectionPos = -1;
long targetMsgId = getIntent().getLongExtra("select_id", -1);
if (targetMsgId != -1) {
if (cursor != null) {
cursor.moveToPosition(-1);
while (cursor.moveToNext()) {
long msgId = cursor.getLong(COLUMN_ID);
if (msgId == targetMsgId) {
newSelectionPos = cursor.getPosition();
break;
}
}
}
} else if (mSavedScrollPosition != -1) {
// mSavedScrollPosition is set when this activity pauses. If equals maxint,
// it means the message list was scrolled to the end. Meanwhile, messages
// could have been received. When the activity resumes and we were
// previously scrolled to the end, jump the list so any new messages are
// visible.
if (mSavedScrollPosition == Integer.MAX_VALUE) {
int cnt = mMsgListAdapter.getCount();
if (cnt > 0) {
// Have to wait until the adapter is loaded before jumping to
// the end.
newSelectionPos = cnt - 1;
mSavedScrollPosition = -1;
}
} else {
// remember the saved scroll position before the activity is paused.
// reset it after the message list query is done
newSelectionPos = mSavedScrollPosition;
mSavedScrollPosition = -1;
}
}
mMsgListAdapter.changeCursor(cursor);
if (newSelectionPos != -1) {
mMsgListView.setSelection(newSelectionPos); // jump the list to the pos
} else {
int count = mMsgListAdapter.getCount();
long lastMsgId = 0;
if (cursor != null && count > 0) {
cursor.moveToLast();
lastMsgId = cursor.getLong(COLUMN_ID);
}
// mScrollOnSend is set when we send a message. We always want to scroll
// the message list to the end when we send a message, but have to wait
// until the DB has changed. We also want to scroll the list when a
// new message has arrived.
smoothScrollToEnd(mScrollOnSend || lastMsgId != mLastMessageId, 0);
mLastMessageId = lastMsgId;
mScrollOnSend = false;
}
// Adjust the conversation's message count to match reality. The
// conversation's message count is eventually used in
// WorkingMessage.clearConversation to determine whether to delete
// the conversation or not.
mConversation.setMessageCount(mMsgListAdapter.getCount());
// Once we have completed the query for the message history, if
// there is nothing in the cursor and we are not composing a new
// message, we must be editing a draft in a new conversation (unless
// mSentMessage is true).
// Show the recipients editor to give the user a chance to add
// more people before the conversation begins.
if (cursor != null && cursor.getCount() == 0
&& !isRecipientsEditorVisible() && !mSentMessage) {
initRecipientsEditor();
mRecipientsEditor.addTextChangedListener(mRecipientsWatcher);
}
// FIXME: freshing layout changes the focused view to an unexpected
// one. In this situation, mRecipientsEditor has higher priority to
// get the focus.
if (isRecipientsEditorVisible()) {
mRecipientsEditor.requestFocus();
} else {
mTextEditor.requestFocus();
}
invalidateOptionsMenu(); // some menu items depend on the adapter's count
return;
case ConversationList.HAVE_LOCKED_MESSAGES_TOKEN:
@SuppressWarnings("unchecked")
ArrayList<Long> threadIds = (ArrayList<Long>)cookie;
ConversationList.confirmDeleteThreadDialog(
new ConversationList.DeleteThreadListener(threadIds,
mBackgroundQueryHandler, null, ComposeMessageActivity.this),
threadIds,
cursor != null && cursor.getCount() > 0,
ComposeMessageActivity.this);
if (cursor != null) {
cursor.close();
}
break;
case MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN:
// check consistency between the query result and 'mConversation'
tid = (Long) cookie;
if (LogTag.VERBOSE || Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("##### onQueryComplete (after delete): msg history result for threadId "
+ tid);
}
if (cursor == null) {
return;
}
if (tid > 0 && cursor.getCount() == 0) {
// We just deleted the last message and the thread will get deleted
// by a trigger in the database. Clear the threadId so next time we
// need the threadId a new thread will get created.
log("##### MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN clearing thread id: "
+ tid);
Conversation conv = Conversation.get(ComposeMessageActivity.this, tid,
false);
if (conv != null) {
conv.clearThreadId();
conv.setDraftState(false);
}
// The last message in this converation was just deleted. Send the user
// to the conversation list.
exitComposeMessageActivity(new Runnable() {
@Override
public void run() {
goToConversationList();
}
});
}
cursor.close();
}
}
private Runnable updateAttachTypeRunnable = new Runnable() {
@Override
public void run() {
Log.d(TAG, "updateAttachTypeRunnable after onDeleteComplete");
updateThreadAttachType();
}
};
@Override
protected void onDeleteComplete(int token, Object cookie, int result) {
super.onDeleteComplete(token, cookie, result);
mHandler.removeCallbacks(updateAttachTypeRunnable);
mHandler.postDelayed(updateAttachTypeRunnable, 200);
switch(token) {
case ConversationList.DELETE_CONVERSATION_TOKEN:
mConversation.setMessageCount(0);
// fall through
case DELETE_MESSAGE_TOKEN:
if (cookie instanceof Boolean && ((Boolean)cookie).booleanValue()) {
// If we just deleted the last message, reset the saved id.
mLastMessageId = 0;
}
// Update the notification for new messages since they
// may be deleted.
MessagingNotification.nonBlockingUpdateNewMessageIndicator(
ComposeMessageActivity.this, MessagingNotification.THREAD_NONE, false);
// Update the notification for failed messages since they
// may be deleted.
updateSendFailedNotification();
break;
}
// If we're deleting the whole conversation, throw away
// our current working message and bail.
if (token == ConversationList.DELETE_CONVERSATION_TOKEN) {
ContactList recipients = mConversation.getRecipients();
mWorkingMessage.discard();
// Remove any recipients referenced by this single thread from the
// contacts cache. It's possible for two or more threads to reference
// the same contact. That's ok if we remove it. We'll recreate that contact
// when we init all Conversations below.
if (recipients != null) {
for (Contact contact : recipients) {
contact.removeFromCache();
}
}
// Make sure the conversation cache reflects the threads in the DB.
Conversation.init(getApplicationContext());
finish();
} else if (token == DELETE_MESSAGE_TOKEN) {
// Check to see if we just deleted the last message
startMsgListQuery(MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN);
}
MmsWidgetProvider.notifyDatasetChanged(getApplicationContext());
}
}
private void updateThreadAttachType() {
long threadId = mConversation.getThreadId();
String attachmentInfo = Conversation.getAttachmentInfo(getContext(),
Conversation.getLatestMessageAttachmentUri(getContext(), threadId));
Uri uri = Conversation.getUri(threadId);
ContentValues values = new ContentValues();
values.put(TelephonyWrapper.ATTACHMENT_INFO, attachmentInfo);
getContext().getContentResolver().update(uri, values, null, null);
}
@Override
public void onUpdate(final Contact updated) {
// Using an existing handler for the post, rather than conjuring up a new one.
mMessageListItemHandler.post(new Runnable() {
@Override
public void run() {
ContactList recipients = isRecipientsEditorVisible() ?
mRecipientsEditor.constructContactsFromInput(false) : getRecipients();
if (Log.isLoggable(LogTag.APP, Log.VERBOSE)) {
log("[CMA] onUpdate contact updated: " + updated);
log("[CMA] onUpdate recipients: " + recipients);
}
updateTitle(recipients);
// The contact information for one (or more) of the recipients has changed.
// Rebuild the message list so each MessageItem will get the last contact info.
ComposeMessageActivity.this.mMsgListAdapter.notifyDataSetChanged();
// Don't do this anymore. When we're showing chips, we don't want to switch from
// chips to text.
// if (mRecipientsEditor != null) {
// mRecipientsEditor.populate(recipients);
// }
}
});
}
private void addRecipientsListeners() {
Contact.addListener(this);
}
private void removeRecipientsListeners() {
Contact.removeListener(this);
}
public static Intent createIntent(Context context, long threadId) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
public static Intent createIntent(Context context, long threadId, String from) {
Intent intent = new Intent(context, ComposeMessageActivity.class);
intent.putExtra(EXTRA_START_COMPOSE_FROM, from);
if (threadId > 0) {
intent.setData(Conversation.getUri(threadId));
}
return intent;
}
private String getBody(Uri uri) {
if (uri == null) {
return null;
}
String urlStr = uri.getSchemeSpecificPart();
if (!urlStr.contains("?")) {
return null;
}
urlStr = urlStr.substring(urlStr.indexOf('?') + 1);
String[] params = urlStr.split("&");
for (String p : params) {
if (p.startsWith("body=")) {
try {
return URLDecoder.decode(p.substring(5), "UTF-8");
} catch (UnsupportedEncodingException e) { }
}
}
return null;
}
private void updateThreadIdIfRunning() {
if (mIsRunning && mConversation != null) {
if (DEBUG) {
Log.v(TAG, "updateThreadIdIfRunning: threadId: " +
mConversation.getThreadId());
}
MessagingNotification.setCurrentlyDisplayedThreadId(mConversation.getThreadId());
} else {
if (DEBUG) {
Log.v(TAG, "updateThreadIdIfRunning: mIsRunning: " + mIsRunning +
" mConversation: " + mConversation);
}
}
// If we're not running, but resume later, the current thread ID will be set in onResume()
}
// Handler for handle copy mms to SIM with toast.
private Handler mCopyToSimWithToastHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
int sum = 0;
int success = 0;
switch (msg.what) {
case MSG_COPY_TO_SIM_SUCCESS:
sum = msg.arg1;
success = msg.arg2;
break;
default:
break;
}
String toast = getString(R.string.copy_to_sim_success, sum, success);
Toast.makeText(ComposeMessageActivity.this, toast,
Toast.LENGTH_SHORT).show();
}
};
private class CopyToSimSelectListener implements DialogInterface.OnClickListener {
private CopyOnWriteArrayList<MessageItem> msgItems;
private int slot;
public CopyToSimSelectListener(CopyOnWriteArrayList<MessageItem> msgItems) {
super();
this.msgItems = msgItems;
}
public void onClick(DialogInterface dialog, int which) {
if (which >= 0) {
slot = which;
} else if (which == DialogInterface.BUTTON_POSITIVE) {
if (MessageUtils.hasInvalidSmsRecipient(getContext(), msgItems)) {
showInvalidCopyDialog();
} else {
int [] subId = SubscriptionManagerWrapper.getSubId(slot);
if ((subId != null) && (subId.length > 0)) {
new Thread(new CopyToSimThread(msgItems, subId[0])).start();
}
}
}
}
}
private void showInvalidCopyDialog() {
AlertDialog invalidCopyDialog = new AlertDialog.Builder(this)
.setTitle(R.string.copy_to_sim_fail)
.setMessage(R.string.cannot_copy_to_sim_reason)
.setPositiveButton(R.string.yes, null)
.show();
}
private class CopyToSimThread extends Thread {
private CopyOnWriteArrayList<MessageItem> msgItems;
private int subscription;
public CopyToSimThread(CopyOnWriteArrayList<MessageItem> msgItems) {
this.msgItems = msgItems;
this.subscription = SmsManager.getDefault().getDefaultSmsSubscriptionId();
}
public CopyToSimThread(CopyOnWriteArrayList<MessageItem> msgItems, int subscription) {
this.msgItems = msgItems;
this.subscription = subscription;
}
@Override
public void run() {
Message msg = mCopyToSimWithToastHandler.obtainMessage();
int sum = msgItems.size();
int success = 0;
for (MessageItem msgItem : msgItems) {
if (copyToSim(msgItem, subscription)) {
success++;
}
}
msg.what = MSG_COPY_TO_SIM_SUCCESS;
msg.arg1 = sum;
msg.arg2 = success;
Log.d(TAG, "copy sms to sim: sum=" + sum + ", success=" + success);
msg.sendToTarget();
}
}
private boolean copyToSim(MessageItem msgItem) {
return copyToSim(msgItem, SmsManager.getDefault().getDefaultSmsSubscriptionId());
}
private boolean copyToSim(MessageItem msgItem, int subId) {
int boxId = msgItem.mBoxId;
String address = msgItem.mAddress;
if (MessageUtils.isWapPushNumber(address)) {
String[] number = address.split(":");
address = number[0];
}
String text = msgItem.mBody;
long timestamp = msgItem.mDate != 0 ? msgItem.mDate : System.currentTimeMillis();
SmsManager sm = SmsManager.getDefault();
ArrayList<String> messages = SmsManager.getDefault().divideMessage(text);
boolean ret = true;
for (String message : messages) {
ret = MessageUtils.insertMessageIntoIcc(subId, address, message, boxId, timestamp);
if (!ret) {
break;
}
}
return ret;
}
private IntentFilter getMediaStateFilter() {
IntentFilter filter = new IntentFilter();
filter.addAction(Intent.ACTION_MEDIA_BAD_REMOVAL);
filter.addAction(Intent.ACTION_MEDIA_REMOVED);
filter.addAction(Intent.ACTION_MEDIA_UNMOUNTED);
filter.addDataScheme(BROADCAST_DATA_SCHEME);
return filter;
}
private String getAttachFilePath(Context context, Uri uri){
if (URI_SCHEME_CONTENT.equals(uri.getScheme())
&& URI_HOST_MEDIA.equals(uri.getHost())) {
Cursor c = context.getContentResolver().query(uri, null,
null, null, null);
if (c != null) {
try {
if (c.moveToFirst()) {
return c.getString(c.getColumnIndex(FILE_PATH_COLUMN));
}
} finally {
c.close();
}
}
return null;
} else {
return uri.getPath().toString();
}
}
private void checkAttachFileState(Context context) {
if (mWorkingMessage.hasAttachment() && !mWorkingMessage.hasSlideshow()) {
ArrayList<Uri> attachFileUris = mWorkingMessage.getSlideshow().getAttachFileUri();
for (Uri uri : attachFileUris) {
Log.i(TAG, "Attach file uri is " + uri);
if (uri == null) {
continue;
}
String path = getAttachFilePath(context, uri);
Log.i(TAG, "File path is " + path);
File f = null;
if (!TextUtils.isEmpty(path)) {
f = new File(path);
}
if (f == null || !f.exists()) {
Log.i(TAG, "set attachment null");
Toast.makeText(ComposeMessageActivity.this,
R.string.cannot_send_attach_reason,
Toast.LENGTH_SHORT).show();
mWorkingMessage.setAttachment(WorkingMessage.TEXT, null, false);
break;
}
}
}
}
// Get the path of uri and compare it to ".vcf" to judge whether it is a
// vcard file.
private boolean isVcardFile(Uri uri) {
String path = uri.getPath();
return null != path && path.toLowerCase().endsWith(".vcf");
}
private ListView getListView() {
return mMsgListView;
}
private void logMultiChoice(String msg) {
if (DEBUG_MULTI_CHOICE) {
Log.d(TAG, msg);
}
}
private Context getContext() {
return ComposeMessageActivity.this;
}
private class ModeCallback implements ListView.MultiChoiceModeListener {
private View mMultiSelectActionBarView;
private TextView mSelectedConvCount;
private ImageView mSelectedAll;
// build action bar with a spinner
private SelectionMenu mSelectionMenu;
// need define variable to keep info of mms count, lock count, unlock
// count.
private int mMmsSelected = 0;
private int mUnlockedCount = 0;
private int mCheckedCount = 0;
private boolean mDeleteLockedMessages = false;
private Menu mMenu;
private WorkThread mWorkThread;
public final static int WORK_TOKEN_DELETE = 0;
public final static int WORK_TOKEN_LOCK = 1;
public final static int WORK_TOKEN_UNLOCK = 2;
ArrayList<Integer> mSelectedPos = new ArrayList<Integer>();
ArrayList<Uri> mSelectedMsg = new ArrayList<Uri>();
ArrayList<MessageItem> mMessageItems = new ArrayList<MessageItem>();
ArrayList<Uri> mSelectedLockedMsg = new ArrayList<Uri>();
public void checkAll(boolean isCheck) {
resetSelectData();
for (int i = 0; i < getListView().getCount(); i++) {
MessageItem item = getMessageItemByPos(i);
boolean isBurnMsg = false;
getListView().setItemChecked(i, isCheck);
}
}
private void resetSelectData() {
mMmsSelected = 0;
mUnlockedCount = 0;
mCheckedCount = 0;
}
private class DeleteMessagesListener implements OnClickListener {
public void setDeleteLockedMessage(boolean deleteLockedMessage) {
mDeleteLockedMessages = deleteLockedMessage;
}
public void onClick(DialogInterface dialog, int whichButton) {
deleteMessages();
}
}
public class WorkThread extends Thread {
private int mToken = -1;
public WorkThread() {
super("WorkThread");
}
public void startWork(int token) {
mToken = token;
this.start();
}
public void run() {
switch (mToken) {
case WORK_TOKEN_DELETE:
deleteCheckedMessage();
break;
case WORK_TOKEN_LOCK:
lockMessage(true);
break;
case WORK_TOKEN_UNLOCK:
lockMessage(false);
break;
default:
break;
}
}
}
private void lockMessage(boolean lock) {
final ContentValues values = new ContentValues(1);
values.put("locked", lock ? 1 : 0);
for (Uri uri : mSelectedMsg) {
getContentResolver().update(uri, values, null, null);
}
}
private WorkThread getWorkThread() {
mWorkThread = new WorkThread();
mWorkThread.setPriority(Thread.MAX_PRIORITY);
return mWorkThread;
}
private void deleteMessages() {
getWorkThread().startWork(WORK_TOKEN_DELETE);
}
private void deleteCheckedMessage() {
for (Uri uri : mSelectedMsg) {
if (!mDeleteLockedMessages && mSelectedLockedMsg.contains(uri)) {
continue;
}
mBackgroundQueryHandler.startDelete(
WORK_TOKEN_DELETE,
mConversation.getThreadId(),
uri,
null,
null);
}
mDeleteLockedMessages = false;
startMsgListQuery(MESSAGE_LIST_QUERY_AFTER_DELETE_TOKEN);
}
private void calculateSelectedMsgUri() {
mSelectedMsg.clear();
mSelectedLockedMsg.clear();
for (Integer pos : mSelectedPos) {
Cursor c = (Cursor) getListView().getAdapter().getItem(pos);
String type = c.getString(COLUMN_MSG_TYPE);
logMultiChoice("message type is:" + type);
if ("sms".equals(type)) {
mSelectedMsg.add(ContentUris.withAppendedId(
Sms.CONTENT_URI, c.getLong(COLUMN_ID)));
if (c.getInt(COLUMN_SMS_LOCKED) != 0) {
mSelectedLockedMsg.add(ContentUris.withAppendedId(
Sms.CONTENT_URI, c.getLong(COLUMN_ID)));
}
} else if ("mms".equals(type)) {
mSelectedMsg.add(ContentUris.withAppendedId(
Mms.CONTENT_URI, c.getLong(COLUMN_ID)));
if (c.getInt(COLUMN_MMS_LOCKED) != 0) {
mSelectedLockedMsg.add(ContentUris.withAppendedId(
Mms.CONTENT_URI, c.getLong(COLUMN_ID)));
}
}
}
}
private void recordAllSelectedItems() {
// must calculate all checked msg before multi selection done.
SparseBooleanArray booleanArray = getListView()
.getCheckedItemPositions();
mSelectedPos.clear();
logMultiChoice("booleanArray = " + booleanArray);
for (int i = 0; i < booleanArray.size(); i++) {
int pos = booleanArray.keyAt(i);
boolean checked = booleanArray.get(pos);
logMultiChoice("pos=" + pos + ",checked=" + checked);
if (checked) {
mSelectedPos.add(pos);
}
}
calculateSelectedMsgUri();
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
logMultiChoice("onCreateActionMode");
mMmsSelected = 0;
mUnlockedCount = 0;
mCheckedCount = 0;
mMenu = menu;
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.compose_multi_select_menu, menu);
getWindow().setStatusBarColor(
getResources().getColor(R.color.statubar_select_background));
if (mMultiSelectActionBarView == null) {
mMultiSelectActionBarView = LayoutInflater.from(getContext())
.inflate(R.layout.action_mode, null);
}
mode.setCustomView(mMultiSelectActionBarView);
mSelectionMenu = new SelectionMenu(getContext(),
(Button) mMultiSelectActionBarView
.findViewById(R.id.selection_menu),
new PopupList.OnPopupItemClickListener() {
@Override
public boolean onPopupItemClick(int itemId) {
if (itemId == SelectionMenu.SELECT_OR_DESELECT) {
boolean selectAll = getListView().getCheckedItemCount() <
getMsgCount() ? true : false;
checkAll(selectAll);
mSelectionMenu.updateSelectAllMode(selectAll);
}
return true;
}
},
(ImageView) mMultiSelectActionBarView
.findViewById(R.id.expand));
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
logMultiChoice("onPrepareActionMode");
if (mMultiSelectActionBarView == null) {
ViewGroup v = (ViewGroup) LayoutInflater
.from(getContext())
.inflate(
R.layout.conversation_list_multi_select_actionbar,
null);
mode.setCustomView(v);
mSelectedConvCount = (TextView) v
.findViewById(R.id.selected_conv_count);
}
if (MessageUtils.getActivatedIccCardCount() < 1) {
MenuItem item = menu.findItem(R.id.copy_to_sim);
if (item != null) {
item.setVisible(false);
}
}
return true;
}
private void showMessageDetail() {
Cursor c = (Cursor) getListView().getAdapter().getItem(
mSelectedPos.get(0));
String type = c.getString(COLUMN_MSG_TYPE);
if (type.equals("sms")) {
// this only for sms
MessageUtils.showSmsMessageContent(getContext(), c.getLong(COLUMN_ID));
} else if (type.equals("mms")) {
MessageUtils.viewMmsMessageAttachment(
ComposeMessageActivity.this, mSelectedMsg.get(0), null,
new AsyncDialog(ComposeMessageActivity.this));
}
}
private void saveAttachment(long msgId) {
int resId = copyMedia(msgId) ? R.string.copy_to_sdcard_success
: R.string.copy_to_sdcard_fail;
Toast.makeText(ComposeMessageActivity.this, resId,
Toast.LENGTH_SHORT).show();
}
private void showReport() {
Cursor c = (Cursor) getListView().getAdapter().getItem(
mSelectedPos.get(0));
showDeliveryReport(c.getLong(COLUMN_ID), c.getString(COLUMN_MSG_TYPE));
}
private void resendCheckedMessage() {
Cursor c = (Cursor) getListView().getAdapter().getItem(mSelectedPos.get(0));
MessageItem item = mMsgListAdapter.getCachedMessageItem(
c.getString(COLUMN_MSG_TYPE), c.getLong(COLUMN_ID), c);
if (getResources().getBoolean(R.bool.config_resend_to_edit)) {
editMessageItem(item);
drawBottomPanel();
} else {
resendMessage(item);
}
}
private void copySmsToSim() {
mMessageItems.clear();
for (Integer pos : mSelectedPos) {
Cursor c = (Cursor) mMsgListAdapter.getItem(pos);
mMessageItems.add(mMsgListAdapter.getCachedMessageItem(
c.getString(COLUMN_MSG_TYPE), c.getLong(COLUMN_ID), c));
}
CopyOnWriteArrayList<MessageItem> messageItems =
new CopyOnWriteArrayList<MessageItem>(mMessageItems);
if (MessageUtils.getActivatedIccCardCount() > 1) {
String[] items = new String[MessageUtils.getPhoneCount()];
for (int i = 0; i < items.length; i++) {
items[i] = MessageUtils.getMultiSimName(
ComposeMessageActivity.this, i);
}
CopyToSimSelectListener listener = new CopyToSimSelectListener(
messageItems);
new AlertDialog.Builder(ComposeMessageActivity.this)
.setTitle(R.string.copy_to_sim)
.setPositiveButton(android.R.string.ok, listener)
.setSingleChoiceItems(items, 0, listener)
.setCancelable(true).show();
} else {
if (MessageUtils.hasInvalidSmsRecipient(getContext(), messageItems)) {
showInvalidCopyDialog();
} else {
new Thread(new CopyToSimThread(messageItems)).start();
}
}
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
recordAllSelectedItems();
switch (item.getItemId()) {
case R.id.forward:
int position = mSelectedPos.get(0).intValue();
MessageItem msgItem = getMessageItemByPos(position);
if (msgItem != null &&
msgItem.isMms() &&
!isAllowForwardMessage(msgItem)) {
Toast.makeText(ComposeMessageActivity.this,
R.string.forward_size_over,
Toast.LENGTH_SHORT).show();
return false;
}
forwardMessageCheck();
break;
case R.id.delete:
confirmDeleteDialog(new DeleteMessagesListener(), mCheckedCount != mUnlockedCount);
break;
case R.id.lock:
if (item.getTitle().equals(
getContext().getString(R.string.menu_lock))) {
getWorkThread().startWork(WORK_TOKEN_LOCK);
} else {
getWorkThread().startWork(WORK_TOKEN_UNLOCK);
}
break;
case R.id.resend:
mode.finish();
resendCheckedMessage();
return true;
case R.id.copy_to_sim:
copySmsToSim();
break;
case R.id.detail:
showMessageDetail();
break;
case R.id.save_attachment:
if (MessageUtils.hasStoragePermission()) {
saveAttachment();
} else {
requestPermissions(new String[] {
Manifest.permission.WRITE_EXTERNAL_STORAGE
}, SAVE_ATTACHMENT_PERMISSION_REQUEST_CODE);
}
break;
case R.id.report:
showReport();
break;
case R.id.copy_text:
int pos = mSelectedPos.get(0).intValue();
MessageItem copyItem = getMessageItemByPos(pos);
if (copyItem != null && copyItem.isSms()) {
copyToClipboard(copyItem.mBody);
}
break;
case R.id.more:
prepareActionMode(mode);
return true;
default:
break;
}
mode.finish();
return true;
}
private void prepareActionMode(ActionMode mode) {
if (mMultiSelectActionBarView == null) {
ViewGroup v = (ViewGroup) LayoutInflater.from(getContext())
.inflate(R.layout.conversation_list_multi_select_actionbar, null);
mode.setCustomView(v);
mSelectedConvCount = (TextView) v
.findViewById(R.id.selected_conv_count);
}
if (MessageUtils.getActivatedIccCardCount() < 1) {
MenuItem copyTextItem = mMenu.findItem(R.id.copy_to_sim);
if (copyTextItem != null) {
copyTextItem.setVisible(false);
}
}
}
private String getAllSMSBody() {
if (!getResources().getBoolean(R.bool.config_forwardconv)) {
//There should be only one messageItem if forwardconv config is disable.
return mMessageItems.get(0).mBody;
}
StringBuilder body = new StringBuilder();
for (MessageItem msgItem : mMessageItems) {
// if not the first append a new line
if (mMessageItems.indexOf(msgItem) != 0) {
body.append(LINE_BREAK);
}
if (TelephonyWrapper.Sms.isOutgoingFolder(msgItem.mBoxId)) {
body.append(msgItem.mContact + COLON + LINE_BREAK);
} else {
if (Contact.get(msgItem.mAddress, false).existsInDatabase()) {
body.append(msgItem.mContact + LEFT_PARENTHESES +
msgItem.mAddress + RIGHT_PARENTHESES + COLON + LINE_BREAK);
} else {
body.append(msgItem.mAddress + COLON + LINE_BREAK);
}
}
body.append(msgItem.mBody);
if (!TextUtils.isEmpty(msgItem.mTimestamp)) {
body.append(LINE_BREAK);
body.append(msgItem.mTimestamp);
}
}
return body.toString();
}
private int getMsgCount() {
return getListView().getCount();
}
private boolean forwardMessageCheck() {
int position = mSelectedPos.get(0).intValue();
MessageItem msgItem = getMessageItemByPos(position);
if (msgItem != null &&
msgItem.isMms() &&
!isAllowForwardMessage(msgItem)) {
Toast.makeText(ComposeMessageActivity.this,
R.string.forward_size_over,
Toast.LENGTH_SHORT).show();
return false;
}
forwardMessage();
return true;
}
public void saveAttachment() {
MessageItem messageItem = null;
try {
Cursor cursor = (Cursor) mMsgListAdapter.getItem(mSelectedPos.get(0));
messageItem = mMsgListAdapter.getCachedMessageItem(
cursor.getString(COLUMN_MSG_TYPE), cursor.getLong(COLUMN_ID), cursor);
} catch (Exception e) {
e.printStackTrace();
}
if (messageItem != null) {
saveAttachment(messageItem.mMsgId);
}
}
private void forwardMessage() {
mMessageItems.clear();
for (Integer pos : mSelectedPos) {
Cursor c = (Cursor) mMsgListAdapter.getItem(pos);
mMessageItems.add(mMsgListAdapter.getCachedMessageItem(
c.getString(COLUMN_MSG_TYPE), c.getLong(COLUMN_ID), c));
}
LogTag.debugD("clear DataSetChangedListener");
mMsgListAdapter.setOnDataSetChangedListener(null);
final MessageItem msgItem = mMessageItems.get(0);
getAsyncDialog().runAsync(new Runnable() {
@Override
public void run() {
// This runnable gets run in a background thread.
if (msgItem.mType.equals("mms")) {
SendReq sendReq = new SendReq();
String subject = getString(R.string.forward_prefix);
if (msgItem.mSubject != null) {
subject += msgItem.mSubject;
}
sendReq.setSubject(new EncodedStringValue(subject));
sendReq.setBody(msgItem.mSlideshow.makeCopy());
mTempMmsUri = null;
try {
PduPersister persister = PduPersister.getPduPersister(
ComposeMessageActivity.this);
// Copy the parts of the message here.
mTempMmsUri = persister
.persist(
sendReq,
Mms.Draft.CONTENT_URI,
true,
MessagingPreferenceActivity
.getIsGroupMmsEnabled(
ComposeMessageActivity.this), null);
mTempThreadId = MessagingNotification.getThreadId(
ComposeMessageActivity.this, mTempMmsUri);
} catch (MmsException e) {
Log.e(TAG, "Failed to copy message: "
+ msgItem.mMessageUri);
Toast.makeText(ComposeMessageActivity.this,
R.string.cannot_save_message,
Toast.LENGTH_SHORT).show();
return;
}
} else {
mBodyString = getAllSMSBody();
}
}
}, new Runnable() {
@Override
public void run() {
// Once the above background thread is complete, this
// runnable is run
// on the UI thread.
Intent intent = createIntent(ComposeMessageActivity.this, 0);
intent.putExtra(KEY_FORWARDED_MESSAGE, true);
if (mTempThreadId > 0) {
intent.putExtra(THREAD_ID, mTempThreadId);
}
if (msgItem.mType.equals("sms")) {
intent.putExtra("sms_body", mBodyString);
} else {
intent.putExtra("msg_uri", mTempMmsUri);
String subject = getString(R.string.forward_prefix);
if (msgItem.mSubject != null) {
subject += msgItem.mSubject;
}
intent.putExtra("subject", subject);
String[] numbers = mConversation.getRecipients()
.getNumbers();
if (numbers != null) {
intent.putExtra("msg_recipient", numbers);
}
}
intent.setClassName(ComposeMessageActivity.this,
"com.android.mms.ui.ForwardMessageActivity");
startActivity(intent);
}
}, R.string.building_slideshow_title);
}
@Override
public void onDestroyActionMode(ActionMode mode) {
updateColorPalette(mActionBarColor);
mSelectionMenu.dismiss();
}
private void updateUnlockedCount(int lock, boolean checked) {
if (lock != 1) {
if (checked) {
mUnlockedCount++;
} else {
mUnlockedCount--;
}
}
}
private void updateMmsSelected(boolean checked) {
if (checked) {
mMmsSelected++;
} else {
mMmsSelected--;
}
}
private void updateStatics(int pos, boolean checked) {
Cursor c = (Cursor) getListView().getAdapter().getItem(pos);
String type = c.getString(COLUMN_MSG_TYPE);
if (type.equals("mms")) {
int lock = c.getInt(COLUMN_MMS_LOCKED);
updateUnlockedCount(lock, checked);
updateMmsSelected(checked);
} else if (type.equals("sms")) {
int lock = c.getInt(COLUMN_SMS_LOCKED);
updateUnlockedCount(lock, checked);
}
}
private boolean isAttachmentSaveable(Cursor cursor) {
MessageItem messageItem = mMsgListAdapter.getCachedMessageItem(
cursor.getString(COLUMN_MSG_TYPE),
cursor.getLong(COLUMN_ID), cursor);
return messageItem != null && messageItem.hasAttachemntToSave();
}
private void customMenuVisibility(ActionMode mode, int checkedCount,
int position, boolean checked) {
mode.getMenu().findItem(R.id.lock).setVisible(false);
if (checkedCount > 1) {
// no detail
mode.getMenu().findItem(R.id.detail).setVisible(false);
// no delivery report
mode.getMenu().findItem(R.id.report).setVisible(false);
// no save attachment
mode.getMenu().findItem(R.id.save_attachment).setVisible(false);
// no resend
mode.getMenu().findItem(R.id.resend).setVisible(false);
// no copy text
mode.getMenu().findItem(R.id.copy_text).setVisible(false);
if (mMmsSelected > 0) {
mode.getMenu().findItem(R.id.forward).setVisible(false);
mode.getMenu().findItem(R.id.copy_to_sim).setVisible(false);
} else {
if (getResources().getBoolean(R.bool.config_forwardconv)) {
mode.getMenu().findItem(R.id.forward).setVisible(true);
} else {
mode.getMenu().findItem(R.id.forward).setVisible(false);
}
mode.getMenu().findItem(R.id.copy_to_sim).setVisible(true);
}
} else {
mode.getMenu().findItem(R.id.detail).setVisible(true);
mode.getMenu().findItem(R.id.save_attachment).setVisible(mMmsSelected > 0);
mode.getMenu().findItem(R.id.resend).setVisible(isFailedMessage(position));
mode.getMenu().findItem(R.id.forward).setVisible(isMessageForwardable(position));
if (mMmsSelected > 0) {
mode.getMenu().findItem(R.id.copy_to_sim).setVisible(false);
mode.getMenu().findItem(R.id.copy_text).setVisible(false);
} else {
mode.getMenu().findItem(R.id.copy_to_sim).setVisible(true);
mode.getMenu().findItem(R.id.copy_text).setVisible(true);
}
mode.getMenu().findItem(R.id.report).setVisible(isDeliveryReportMsg(position));
}
MenuItem moremenu = mode.getMenu().findItem(R.id.more);
boolean hasVisible = moremenu.getSubMenu().hasVisibleItems();
moremenu.setVisible(hasVisible);
}
private MessageItem getMessageItemByPos(int position) {
Cursor cursor = (Cursor) mMsgListAdapter.getItem(position);
if (cursor != null) {
return mMsgListAdapter.getCachedMessageItem(
cursor.getString(COLUMN_MSG_TYPE),
cursor.getLong(COLUMN_ID), cursor);
}
return null;
}
private boolean isDeliveryReportMsg(int position) {
MessageItem msgItem = getMessageItemByPos(position);
if (msgItem == null) {
return false;
}
if (msgItem.mDeliveryStatus != MessageItem.DeliveryStatus.NONE ||
msgItem.mReadReport) {
return true;
} else {
return false;
}
}
private boolean isMessageForwardable(int position) {
MessageItem msgItem = getMessageItemByPos(position);
if (msgItem == null) {
return false;
}
return msgItem.isSms() || (msgItem.isDownloaded() && msgItem.mIsForwardable);
}
private boolean isFailedMessage(int position) {
MessageItem msgItem = getMessageItemByPos(position);
if (msgItem == null) {
return false;
}
return msgItem.isFailedMessage();
}
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position,
long id, boolean checked) {
logMultiChoice("onItemCheckedStateChanged... position=" + position
+ ", checked=" + checked);
if (mMsgListAdapter == null || mMsgListAdapter.getCount() <= 0) {
logMultiChoice("onItemCheckedStateChanged list not ready!");
return;
}
mCheckedCount = getListView().getCheckedItemCount();
updateStatics(position, checked);
customMenuVisibility(mode, mCheckedCount, position, checked);
mSelectionMenu.setTitle(getApplicationContext().getString(
R.string.selected_count, mCheckedCount));
mSelectionMenu.updateSelectAllMode(getMsgCount() == mCheckedCount);
mSelectionMenu.updateCheckedCount();
}
private void confirmDeleteDialog(final DeleteMessagesListener listener,
boolean locked) {
View contents = View.inflate(ComposeMessageActivity.this,
R.layout.delete_thread_dialog_view, null);
TextView msg = (TextView) contents.findViewById(R.id.message);
msg.setText(getString(R.string.confirm_delete_selected_messages));
final CheckBox checkbox = (CheckBox) contents
.findViewById(R.id.delete_locked);
checkbox.setChecked(false);
if (mSelectedLockedMsg.size() == 0) {
checkbox.setVisibility(View.GONE);
} else {
listener.setDeleteLockedMessage(checkbox.isChecked());
checkbox.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
listener.setDeleteLockedMessage(checkbox.isChecked());
}
});
}
AlertDialog.Builder builder = new AlertDialog.Builder(
ComposeMessageActivity.this);
builder.setTitle(R.string.confirm_dialog_title);
builder.setIconAttribute(android.R.attr.alertDialogIcon);
builder.setCancelable(true);
builder.setView(contents);
builder.setPositiveButton(R.string.yes, listener);
builder.setNegativeButton(R.string.no, null);
builder.show();
}
}
private void handleUnsupportedTypeWarning(final Runnable runnable) {
if (runnable != null) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.restricted_format));
builder.setMessage(R.string.attach_anyway);
builder.setIcon(R.drawable.ic_sms_mms_not_delivered);
builder.setPositiveButton(R.string.yes,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
ContentRestrictionFactory
.initContentRestriction(
MessagingPreferenceActivity.CREATION_MODE_FREE);
runnable.run();
}
});
builder.setNegativeButton(R.string.no, null);
builder.create().show();
}
}
private void handleMessageSizeExceededWarning(final Runnable runnable) {
if (runnable != null) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle(getString(R.string.exceed_message_size_limitation));
builder.setMessage(R.string.attach_anyway);
builder.setIcon(R.drawable.ic_sms_mms_not_delivered);
builder.setPositiveButton(R.string.yes,
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface arg0, int arg1) {
ContentRestrictionFactory
.initContentRestriction(
MessagingPreferenceActivity.CREATION_MODE_FREE);
runnable.run();
}
});
builder.setNegativeButton(R.string.no, null);
builder.create().show();
}
}
private void setBackgroundWallpaper() {
SharedPreferences mPreferences = PreferenceManager
.getDefaultSharedPreferences(this);
String imageUri = mPreferences.getString(
MessagingPreferenceActivity.CHAT_WALLPAPER, null);
if (!TextUtils.isEmpty(imageUri)) {
Bitmap bitmap = BitmapFactory.decodeFile(mPreferences
.getString(
MessagingPreferenceActivity.CHAT_WALLPAPER, null));
if(bitmap != null) {
mMsgListView.setBackground(new BitmapDrawable(bitmap));
}
}
}
private void setTextFontsize() {
int size = MessageUtils.getFontSize();
if (mTextEditor != null) {
mTextEditor.setTextSize(size);
}
if (mMsgListAdapter != null) {
mMsgListAdapter.setTextSize(size);
}
if (mMsgListView != null
&& mMsgListView.getVisibility() == View.VISIBLE) {
int count = mMsgListView.getChildCount();
for (int i = 0; i < count; i++) {
MessageListItem item = (MessageListItem) mMsgListView
.getChildAt(i);
if (item != null) {
item.setBodyTextSize(size);
}
}
}
}
private void updateColorPalette(int color) {
MaterialPalette palette = determinePalette(color);
updateThemeColors(palette.mPrimaryColor, palette.mSecondaryColor);
mAccentColor = palette.mPrimaryColor;
mStatusBarColor = palette.mSecondaryColor;
mSendContactColor = mAccentColor;
}
private void updateThemeColors(int accentColor, int statusBarColor) {
final int ANIMATION_DURATION = 200;
final ColorDrawable background = new ColorDrawable();
final ObjectAnimator backgroundAnimation = ObjectAnimator.ofInt(background,
"color", mAccentColor, accentColor);
final ObjectAnimator statusBarAnimation = ObjectAnimator.ofInt(getWindow(),
"statusBarColor", mStatusBarColor, statusBarColor);
backgroundAnimation.setEvaluator(new ArgbEvaluator());
statusBarAnimation.setEvaluator(new ArgbEvaluator());
findViewById(R.id.header).setBackground(background);
final AnimatorSet animation = new AnimatorSet();
animation.playTogether(backgroundAnimation, statusBarAnimation);
animation.setDuration(isResumed() ? ANIMATION_DURATION : 0);
animation.start();
}
private MaterialPalette determinePalette(int color) {
final Resources res = ComposeMessageActivity.this.getResources();
if (color != 0) {
MaterialColorMapUtils mcmu = new MaterialColorMapUtils(res);
return mcmu.calculatePrimaryAndSecondaryColor(color);
}
return MaterialColorMapUtils.getDefaultPrimaryAndSecondaryColors(res);
}
private void setActionBarColor(int color) {
mActionBarColor = color;
}
public static int getSendContactColor() {
return mSendContactColor;
}
/* Following code used to support inline play Audio, while not
play audio by choosing another application */
public interface IMMSUpdateProgressBar {
void update(String time, int pos);
void reset();
}
public interface IMMSAudioPlayer {
void prepare(String path, ImageView view, int color);
void setUpdateCallback(IMMSUpdateProgressBar callback);
void pause();
void releaseMediaPlayer();
}
private IMMSAudioPlayer mMMSAudioPlayer = new MMSAudioPlayer();
private MediaPlayer mMediaPlayer;
private boolean mMediaPlayerPrepared = false;
private IMMSUpdateProgressBar mUpdateCallBack;
private String mCurPlayUrl;
private final int POST_DELAY = 100;
private final int MAX_DURATION = 100;
private class MMSAudioPlayer implements IMMSAudioPlayer {
private ImageView mPlayPause;
private Drawable mPlayPauseDrawable;
private int mColor;
/**
* Check current MediaPlayer status, according which to determine
* whether need init player or play/pause audio.
*/
@Override
public void prepare(String path, ImageView view, int color) {
ImageView oldPlayerView = mPlayPause;
mPlayPause = view;
mColor = color;
if (path == null) return;
if (mCurPlayUrl == null) {
mCurPlayUrl = path;
}
if (mMediaPlayerPrepared) {
if (path.equals(mCurPlayUrl)) {
if (mMediaPlayer.isPlaying()) {
pause();
} else {
play();
}
} else {
mPlayPauseDrawable = getResources().getDrawable(R.drawable.audio_play).mutate();
mPlayPauseDrawable.setTint(mColor);
if (null != oldPlayerView) {
oldPlayerView.setBackground(mPlayPauseDrawable);
}
releaseMediaPlayer();
initMediaPlayer(path);
}
} else {
initMediaPlayer(path);
}
mCurPlayUrl = path;
}
private void play() {
mMediaPlayer.start();
mHandler.post(mUpdateThread);
mPlayPauseDrawable = getResources().getDrawable(R.drawable.audio_pause).mutate();
mPlayPauseDrawable.setTint(mColor);
mPlayPause.setBackground(mPlayPauseDrawable);
}
@Override
public void pause() {
mMediaPlayer.pause();
mPlayPauseDrawable = getResources().getDrawable(R.drawable.audio_play).mutate();
mPlayPauseDrawable.setTint(mColor);
mPlayPause.setBackground(mPlayPauseDrawable);
}
@Override
public void setUpdateCallback(IMMSUpdateProgressBar callback) {
mUpdateCallBack = callback;
}
private void initMediaPlayer(final String path) {
try {
mMediaPlayer = new MediaPlayer();
mMediaPlayer.setDataSource(getApplicationContext(), Uri.parse(path));
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
mMediaPlayerPrepared = true;
play();
}
});
mMediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mUpdateCallBack.reset();
mPlayPauseDrawable = getResources().getDrawable(R.drawable.audio_play).mutate();
mPlayPauseDrawable.setTint(mColor);
mPlayPause.setBackground(mPlayPauseDrawable);
mHandler.removeCallbacks(mUpdateThread);
}
});
mMediaPlayer.prepareAsync();
} catch (Exception e) {
Log.w(TAG, "init MediaPlayer error:" + e);
}
}
@Override
public void releaseMediaPlayer() {
if (mMediaPlayer != null) {
mMediaPlayer.release();
mMediaPlayer = null;
mMediaPlayerPrepared = false;
}
}
}
private Runnable mUpdateThread = new Runnable() {
int pos;
int time;
@Override
public void run() {
if (mMediaPlayer == null) {
mHandler.removeCallbacks(mUpdateThread);
return;
}
pos = mMediaPlayer.getCurrentPosition();
time = mMediaPlayer.getDuration();
if (time != 0) {
mUpdateCallBack.update(MessageUtils.getDisplayTime(pos), pos * MAX_DURATION / time);
}
mHandler.postDelayed(mUpdateThread, POST_DELAY);
}
};
@Override
public void onRequestPermissionsResult(final int requestCode, final String permissions[],
final int[] grantResults) {
if (requestCode == SAVE_ATTACHMENT_PERMISSION_REQUEST_CODE) {
if (MessageUtils.hasStoragePermission()) {
mModeCallback.saveAttachment();
} else {
Toast.makeText(this, R.string.no_permission_save_attachment_to_storage, Toast.LENGTH_SHORT).show();
}
}
}
private ServiceConnection mConnection = new ServiceConnection() {
public void onServiceConnected(ComponentName className, IBinder service) {
if (DEBUG) Log.d(TAG, "PresenceService connected");
mService = IPresenceService.Stub.asInterface(service);
try {
mService.registerCallback(mCallback);
} catch (RemoteException e) {
Log.e(TAG, "PresenceService registerCallback error " + e);
}
}
public void onServiceDisconnected(ComponentName className) {
if (DEBUG) Log.d(TAG, "PresenceService disconnected");
mService = null;
}
};
private IPresenceServiceCB mCallback = new IPresenceServiceCB.Stub() {
public void setIMSEnabledCB() {
if (DEBUG) Log.d(TAG, "PresenceService setIMSEnabled callback");
}
};
private void bindService(Context context) {
if (DEBUG) Log.d(TAG, "PresenceService BindService");
Intent intent = new Intent(IPresenceService.class.getName());
intent.setClassName(PRESENCESERV, PRESENCESERV_PRESENCESERVICE);
mIsBound = context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
private void unbindService(Context context) {
if (DEBUG) Log.d(TAG, "PresenceService unbindService");
if (mService != null) {
try {
mService.unregisterCallback(mCallback);
} catch (RemoteException e) {
Log.e(TAG, "PresenceService unregister error " + e);
}
}
if (mIsBound) {
if (DEBUG) Log.d(TAG, "PresenceService unbind");
context.unbindService(mConnection);
mIsBound = false;
}
}
private boolean startAvailabilityFetch(String number){
if (DEBUG) Log.d(TAG, "startAvailabilityFetch number " + number);
if (null != mService) {
try {
boolean vt = false;
vt = mService.invokeAvailabilityFetch(number);
return vt;
} catch (Exception e) {
Log.d(TAG, "getVTCapOfContact ERROR " + e);
}
}
return false;
}
private boolean getVTCapability(String number) {
if (DEBUG) Log.d(TAG, "getVTCapability number " + number);
if (null != mService) {
try {
boolean vt = false;
vt = mService.hasVTCapability(number);
if (DEBUG) Log.d(TAG,
"getVTCapability success number " + number + " " + vt);
return vt;
} catch (Exception e) {
Log.d(TAG, "getVTCapability ERROR " + e);
}
}
return false;
}
}