/*
* Copyright (C) 2012 The CyanogenMod Project (DvTonder)
*
* 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.quickmessage;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.KeyguardManager;
import android.app.LoaderManager;
import android.app.NotificationManager;
import android.content.Context;
import android.content.CursorLoader;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.Loader;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.database.Cursor;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcelable;
import android.os.PowerManager;
import android.preference.PreferenceManager;
import android.provider.ContactsContract.Profile;
import android.support.v4.view.PagerAdapter;
import android.support.v4.view.ViewPager;
import android.text.InputFilter;
import android.text.InputFilter.LengthFilter;
import android.text.InputType;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.KeyEvent;
import android.view.LayoutInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.view.Window;
import android.view.WindowManager;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemClickListener;
import android.widget.AdapterView.OnItemLongClickListener;
import android.widget.Button;
import android.widget.CursorAdapter;
import android.widget.EditText;
import android.widget.GridView;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.QuickContactBadge;
import android.widget.SimpleAdapter;
import android.widget.SimpleCursorAdapter;
import android.widget.TextView;
import android.widget.TextView.OnEditorActionListener;
import android.widget.Toast;
import com.android.mms.MmsConfig;
import com.android.mms.R;
import com.android.mms.data.Contact;
import com.android.mms.data.Conversation;
import com.android.mms.templates.TemplatesProvider.Template;
import com.android.mms.transaction.MessagingNotification;
import com.android.mms.transaction.MessagingNotification.NotificationInfo;
import com.android.mms.transaction.SmsMessageSender;
import com.android.mms.ui.ImageAdapter;
import com.android.mms.ui.MessageUtils;
import com.android.mms.ui.MessagingPreferenceActivity;
import com.android.mms.util.EmojiParser;
import com.android.mms.util.SmileyParser;
import com.google.android.mms.MmsException;
import java.nio.charset.Charset;
import java.nio.charset.CharsetEncoder;
import java.text.Normalizer;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;
public class QuickMessagePopup extends Activity implements
LoaderManager.LoaderCallbacks<Cursor> {
private static final String LOG_TAG = "QuickMessagePopup";
private boolean DEBUG = false;
// Intent bungle fields
public static final String SMS_FROM_NAME_EXTRA =
"com.android.mms.SMS_FROM_NAME";
public static final String SMS_FROM_NUMBER_EXTRA =
"com.android.mms.SMS_FROM_NUMBER";
public static final String SMS_NOTIFICATION_OBJECT_EXTRA =
"com.android.mms.NOTIFICATION_OBJECT";
public static final String QR_SHOW_KEYBOARD_EXTRA =
"com.android.mms.QR_SHOW_KEYBOARD";
// Message removal
public static final String QR_REMOVE_MESSAGES_EXTRA =
"com.android.mms.QR_REMOVE_MESSAGES";
public static final String QR_THREAD_ID_EXTRA =
"com.android.mms.QR_THREAD_ID";
// Templates support
private static final int DIALOG_TEMPLATE_SELECT = 1;
private static final int DIALOG_TEMPLATE_NOT_AVAILABLE = 2;
private SimpleCursorAdapter mTemplatesCursorAdapter;
private int mNumTemplates = 0;
// View items
private ImageView mQmPagerArrow;
private TextView mQmMessageCounter;
private Button mCloseButton;
private Button mViewButton;
// General items
private Drawable mDefaultContactImage;
private Context mContext;
private boolean mScreenUnlocked = false;
private KeyguardManager mKeyguardManager = null;
private PowerManager mPowerManager;
// Message list items
private ArrayList<QuickMessage> mMessageList;
private QuickMessage mCurrentQm = null;
private int mCurrentPage = -1; // Set to an invalid index
// Configuration
private boolean mCloseClosesAll = false;
private boolean mWakeAndUnlock = false;
private boolean mDarkTheme = false;
private boolean mFullTimestamp = false;
private boolean mStripUnicode = false;
private boolean mEnableEmojis = false;
private int mInputMethod;
// Message pager
private ViewPager mMessagePager;
private MessagePagerAdapter mPagerAdapter;
// Options menu items
private static final int MENU_INSERT_SMILEY = 1;
private static final int MENU_INSERT_EMOJI = 3;
private static final int MENU_ADD_TEMPLATE = 2;
// Smiley and Emoji support
private AlertDialog mSmileyDialog;
private AlertDialog mEmojiDialog;
private View mEmojiView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Initialise the message list and other variables
mContext = this;
mMessageList = new ArrayList<QuickMessage>();
mDefaultContactImage = getResources().getDrawable(R.drawable.ic_contact_picture);
mNumTemplates = getTemplatesCount();
mPowerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE);
// Get the preferences
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
mFullTimestamp = prefs.getBoolean(MessagingPreferenceActivity.FULL_TIMESTAMP, false);
mCloseClosesAll = prefs.getBoolean(MessagingPreferenceActivity.QM_CLOSE_ALL_ENABLED, false);
mWakeAndUnlock = prefs.getBoolean(MessagingPreferenceActivity.QM_LOCKSCREEN_ENABLED, false);
mDarkTheme = prefs.getBoolean(MessagingPreferenceActivity.QM_DARK_THEME_ENABLED, false);
mStripUnicode = prefs.getBoolean(MessagingPreferenceActivity.STRIP_UNICODE, false);
mEnableEmojis = prefs.getBoolean(MessagingPreferenceActivity.ENABLE_EMOJIS, false);
mInputMethod = Integer.parseInt(prefs.getString(MessagingPreferenceActivity.INPUT_TYPE,
Integer.toString(InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE)));
// Set the window features and layout
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.dialog_quickmessage);
// Turn on the Options Menu
invalidateOptionsMenu();
// Load the views and Parse the intent to show the QuickMessage
setupViews();
parseIntent(getIntent().getExtras(), false);
}
private void setupViews() {
// Load the main views
mQmPagerArrow = (ImageView) findViewById(R.id.pager_arrow);
mQmMessageCounter = (TextView) findViewById(R.id.message_counter);
mCloseButton = (Button) findViewById(R.id.button_close);
mViewButton = (Button) findViewById(R.id.button_view);
// Set the theme color on the pager arrow
Resources res = getResources();
if (mDarkTheme) {
mQmPagerArrow.setBackgroundColor(res.getColor(R.color.quickmessage_body_dark_bg));
} else {
mQmPagerArrow.setBackgroundColor(res.getColor(R.color.quickmessage_body_light_bg));
}
// ViewPager Support
mPagerAdapter = new MessagePagerAdapter();
mMessagePager = (ViewPager) findViewById(R.id.message_pager);
mMessagePager.setAdapter(mPagerAdapter);
mMessagePager.setOnPageChangeListener(mPagerAdapter);
// Close button
mCloseButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// If not closing all, close the current QM and move on
int numMessages = mMessageList.size();
if (mCloseClosesAll || numMessages == 1) {
clearNotification(true);
finish();
} else {
// Dismiss the keyboard if it is shown
QuickMessage qm = mMessageList.get(mCurrentPage);
if (qm != null) {
dismissKeyboard(qm);
if (mCurrentPage < numMessages-1) {
showNextMessageWithRemove(qm);
} else {
showPreviousMessageWithRemove(qm);
}
}
}
}
});
// View button
mViewButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
// Override the re-lock if the screen was unlocked
if (mScreenUnlocked) {
// Cancel the receiver that will clear the wake locks
ClearAllReceiver.removeCancel(getApplicationContext());
ClearAllReceiver.clearAll(false);
mScreenUnlocked = false;
}
// Trigger the view intent
mCurrentQm = mMessageList.get(mCurrentPage);
Intent vi = mCurrentQm.getViewIntent();
if (vi != null) {
mCurrentQm.saveReplyText();
vi.putExtra("sms_body", mCurrentQm.getReplyText());
startActivity(vi);
}
clearNotification(false);
finish();
}
});
}
private void parseIntent(Bundle extras, boolean newMessage) {
if (extras == null) {
return;
}
// Check if we are being called to remove messages already showing
if (extras.getBoolean(QR_REMOVE_MESSAGES_EXTRA, false)) {
// Get the ID
long threadId = extras.getLong(QR_THREAD_ID_EXTRA, -1);
if (threadId != -1) {
removeMatchingMessages(threadId);
}
} else {
// Parse the intent and ensure we have a notification object to work with
NotificationInfo nm = (NotificationInfo) extras.getParcelable(SMS_NOTIFICATION_OBJECT_EXTRA);
if (nm != null) {
QuickMessage qm = new QuickMessage(extras.getString(SMS_FROM_NAME_EXTRA),
extras.getString(SMS_FROM_NUMBER_EXTRA), nm);
mMessageList.add(qm);
// If triggered from Quick Reply the keyboard should be visible immediately
if (extras.getBoolean(QR_SHOW_KEYBOARD_EXTRA, false)) {
getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_ALWAYS_VISIBLE);
}
if (newMessage && mCurrentPage != -1) {
// There is already a message showing
// Stay on the current message
mMessagePager.setCurrentItem(mCurrentPage);
} else {
// Set the current message to the last message received
mCurrentPage = mMessageList.size()-1;
mMessagePager.setCurrentItem(mCurrentPage);
}
if (DEBUG)
Log.d(LOG_TAG, "parseIntent(): New message from " + qm.getFromName().toString()
+ " added. Number of messages = " + mMessageList.size()
+ ". Displaying page #" + (mCurrentPage+1));
// Make sure the counter is accurate
updateMessageCounter();
}
}
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
if (DEBUG)
Log.d(LOG_TAG, "onNewIntent() called");
// Set new intent
setIntent(intent);
// Load and display the new message
parseIntent(intent.getExtras(), true);
unlockScreen();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
@Override
protected void onResume() {
super.onResume();
// Unlock the screen if needed
unlockScreen();
}
@Override
protected void onStop() {
super.onStop();
if (mScreenUnlocked) {
// Cancel the receiver that will clear the wake locks
ClearAllReceiver.removeCancel(getApplicationContext());
ClearAllReceiver.clearAll(true);
}
}
@Override
public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
super.onCreateContextMenu(menu, v, menuInfo);
menu.clear();
// Smileys menu item
menu.add(0, MENU_INSERT_SMILEY, 0, R.string.menu_insert_smiley)
.setIcon(R.drawable.ic_menu_emoticons);
// Emoji's menu item (if enabled)
if (mEnableEmojis) {
menu.add(0, MENU_INSERT_EMOJI, 0, R.string.menu_insert_emoji);
}
// Templates menu item, if there are defined templates
if (mNumTemplates > 0) {
menu.add(0, MENU_ADD_TEMPLATE, 0, R.string.template_insert)
.setIcon(android.R.drawable.ic_menu_add)
.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
}
}
@Override
public boolean onContextItemSelected(MenuItem item) {
switch (item.getItemId()) {
case MENU_INSERT_SMILEY:
showSmileyDialog();
return true;
case MENU_INSERT_EMOJI:
showEmojiDialog();
return true;
case MENU_ADD_TEMPLATE:
selectTemplate();
return true;
default:
return super.onContextItemSelected(item);
}
}
//==========================================================
// Utility methods
//==========================================================
/**
* Copied from ComposeMessageActivity.java, this method displays the available
* templates and allows the user to select and append it to the reply text. It
* has been modified to work with this class
*/
private void selectTemplate() {
getLoaderManager().restartLoader(0, null, this);
}
/**
* Copied from ComposeMessageActivity.java, this method displays the available
* smileys and allows the user to select and append it to the reply text. It
* has been modified to work with this class
*/
private void showSmileyDialog() {
if (mSmileyDialog == null) {
int[] icons = SmileyParser.DEFAULT_SMILEY_RES_IDS;
String[] names = getResources().getStringArray(
SmileyParser.DEFAULT_SMILEY_NAMES);
final String[] texts = getResources().getStringArray(
SmileyParser.DEFAULT_SMILEY_TEXTS);
final int N = names.length;
List<Map<String, ?>> entries = new ArrayList<Map<String, ?>>();
for (int i = 0; i < N; i++) {
// We might have different ASCII for the same icon, skip it if
// the icon is already added.
boolean added = false;
for (int j = 0; j < i; j++) {
if (icons[i] == icons[j]) {
added = true;
break;
}
}
if (!added) {
HashMap<String, Object> entry = new HashMap<String, Object>();
entry. put("icon", icons[i]);
entry. put("name", names[i]);
entry.put("text", texts[i]);
entries.add(entry);
}
}
final SimpleAdapter a = new SimpleAdapter(
this,
entries,
R.layout.smiley_menu_item,
new String[] {"icon", "name", "text"},
new int[] {R.id.smiley_icon, R.id.smiley_name, R.id.smiley_text});
SimpleAdapter.ViewBinder viewBinder = new SimpleAdapter.ViewBinder() {
@Override
public boolean setViewValue(View view, Object data, String textRepresentation) {
if (view instanceof ImageView) {
Drawable img = getResources().getDrawable((Integer)data);
((ImageView)view).setImageDrawable(img);
return true;
}
return false;
}
};
a.setViewBinder(viewBinder);
AlertDialog.Builder b = new AlertDialog.Builder(this);
b.setTitle(getString(R.string.menu_insert_smiley));
b.setCancelable(true);
b.setAdapter(a, new DialogInterface.OnClickListener() {
@Override
@SuppressWarnings("unchecked")
public final void onClick(DialogInterface dialog, int which) {
HashMap<String, Object> item = (HashMap<String, Object>) a.getItem(which);
String smiley = (String)item.get("text");
// Get the currently visible message and append the smiley
QuickMessage qm = mMessageList.get(mCurrentPage);
if (qm != null) {
// add the smiley at the cursor location or replace selected
int start = qm.getEditText().getSelectionStart();
int end = qm.getEditText().getSelectionEnd();
qm.getEditText().getText().replace(Math.min(start, end),
Math.max(start, end), smiley);
}
dialog.dismiss();
}
});
mSmileyDialog = b.create();
}
mSmileyDialog.show();
}
/**
* Copied from ComposeMessageActivity.java, this method displays the available
* emoji's and allows the user to select and insert one or more into a emoji text
* string which can then me appended to the the reply text. It has been modified
* to work with this class
*/
private void showEmojiDialog() {
if (mEmojiDialog == null) {
int[] icons = EmojiParser.DEFAULT_EMOJI_RES_IDS;
int layout = R.layout.emoji_insert_view;
mEmojiView = getLayoutInflater().inflate(layout, null);
final GridView gridView = (GridView) mEmojiView.findViewById(R.id.emoji_grid_view);
gridView.setAdapter(new ImageAdapter(this, icons));
final EditText editText = (EditText) mEmojiView.findViewById(R.id.emoji_edit_text);
final Button button = (Button) mEmojiView.findViewById(R.id.emoji_button);
gridView.setOnItemClickListener(new OnItemClickListener() {
public void onItemClick(AdapterView<?> parent, View v, int position, long id) {
// We use the new unified Unicode 6.1 emoji code points
CharSequence emoji = EmojiParser.getInstance().addEmojiSpans(EmojiParser.mEmojiTexts[position]);
editText.append(emoji);
}
});
gridView.setOnItemLongClickListener(new OnItemLongClickListener() {
@Override
public boolean onItemLongClick(AdapterView<?> parent, View view, int position,
long id) {
// We use the new unified Unicode 6.1 emoji code points
CharSequence emoji = EmojiParser.getInstance().addEmojiSpans(EmojiParser.mEmojiTexts[position]);
// Get the currently visible message and append the emoji
QuickMessage qm = mMessageList.get(mCurrentPage);
if (qm != null) {
// add the emoji at the cursor location or replace selected
int start = qm.getEditText().getSelectionStart();
int end = qm.getEditText().getSelectionEnd();
qm.getEditText().getText().replace(Math.min(start, end),
Math.max(start, end), emoji);
}
mEmojiDialog.dismiss();
return true;
}
});
button.setOnClickListener(new android.view.View.OnClickListener() {
@Override
public void onClick(View v) {
// Get the currently visible message and append the emoji
QuickMessage qm = mMessageList.get(mCurrentPage);
if (qm != null) {
// add the emoji at the cursor location or replace selected
int start = qm.getEditText().getSelectionStart();
int end = qm.getEditText().getSelectionEnd();
qm.getEditText().getText().replace(Math.min(start, end),
Math.max(start, end), editText.getText());
}
mEmojiDialog.dismiss();
}
});
AlertDialog.Builder b = new AlertDialog.Builder(this);
b.setTitle(getString(R.string.menu_insert_emoji));
b.setCancelable(true);
b.setView(mEmojiView);
mEmojiDialog = b.create();
}
final EditText editText = (EditText) mEmojiView.findViewById(R.id.emoji_edit_text);
editText.setText("");
mEmojiDialog.show();
}
/**
* This method dismisses the on screen keyboard if it is visible for the supplied qm
*
* @param qm - qm to check against
*/
private void dismissKeyboard(QuickMessage qm) {
if (qm != null) {
EditText editView = qm.getEditText();
if (editView != null) {
InputMethodManager imm = (InputMethodManager)getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(editView.getApplicationWindowToken(), 0);
}
}
}
/**
* If 'Wake and unlock' is enabled, this method will unlock the screen
*/
private void unlockScreen() {
// See if the lock screen should be disabled
if (!mWakeAndUnlock) {
return;
}
// See if the screen is locked or if no lock set and the screen is off
// and get the wake lock to turn on the screen.
boolean isScreenOn = mPowerManager.isScreenOn();
boolean inKeyguardRestrictedInputMode = mKeyguardManager.inKeyguardRestrictedInputMode();
if (inKeyguardRestrictedInputMode || ((!inKeyguardRestrictedInputMode) && !isScreenOn)) {
ManageWakeLock.acquireFull(mContext);
mScreenUnlocked = true;
}
}
/**
* Update the page indicator counter to show the currently selected visible page number
*/
public void updateMessageCounter() {
String separator = mContext.getString(R.string.message_counter_separator);
mQmMessageCounter.setText((mCurrentPage + 1) + " " + separator + " " + mMessageList.size());
if (DEBUG)
Log.d(LOG_TAG, "updateMessageCounter() called, counter text set to " + (mCurrentPage + 1)
+ " of " + mMessageList.size());
}
/**
* Remove the supplied qm from the ViewPager and show the previous/older message
*
* @param qm
*/
public void showPreviousMessageWithRemove(QuickMessage qm) {
if (qm != null) {
if (DEBUG)
Log.d(LOG_TAG, "showPreviousMessageWithRemove()");
markCurrentMessageRead(qm);
if (mCurrentPage > 0) {
updatePages(mCurrentPage-1, qm);
}
}
}
/**
* Remove the supplied qm from the ViewPager and show the next/newer message
*
* @param qm
*/
public void showNextMessageWithRemove(QuickMessage qm) {
if (qm != null) {
if (DEBUG)
Log.d(LOG_TAG, "showNextMessageWithRemove()");
markCurrentMessageRead(qm);
if (mCurrentPage < (mMessageList.size() - 1)) {
updatePages(mCurrentPage, qm);
}
}
}
/**
* Handle qm removal and the move to and display of the appropriate page
*
* @param gotoPage - page number to display after the removal
* @param removeMsg - qm to remove from ViewPager
*/
private void updatePages(int gotoPage, QuickMessage removeMsg) {
mMessageList.remove(removeMsg);
mPagerAdapter.notifyDataSetChanged();
mMessagePager.setCurrentItem(gotoPage);
updateMessageCounter();
if (DEBUG)
Log.d(LOG_TAG, "updatePages(): Removed message " + removeMsg.getThreadId()
+ " and changed to page #" + (gotoPage+1) + ". Remaining messages = "
+ mMessageList.size());
}
/**
* Remove all matching quickmessages for the supplied thread id
*
* @param threadId
*/
public void removeMatchingMessages(long threadId) {
if (DEBUG)
Log.d(LOG_TAG, "removeMatchingMessages() looking for match with threadID = " + threadId);
Iterator<QuickMessage> itr = mMessageList.iterator();
QuickMessage qmElement = null;
// Iterate through the list and remove the messages that match
while(itr.hasNext()){
qmElement = itr.next();
if(qmElement.getThreadId() == threadId) {
itr.remove();
}
}
// See if there are any remaining messages and update the pager
if (mMessageList.size() > 0) {
mPagerAdapter.notifyDataSetChanged();
mMessagePager.setCurrentItem(1); // First message
updateMessageCounter();
} else {
// we are done
finish();
}
}
/**
* Marks the supplied qm as read
*
* @param qm
*/
private void markCurrentMessageRead(QuickMessage qm) {
if (qm != null) {
Conversation con = Conversation.get(mContext, qm.getThreadId(), true);
if (con != null) {
con.markAsRead(false);
if (DEBUG)
Log.d(LOG_TAG, "markCurrentMessageRead(): Marked message " + qm.getThreadId()
+ " as read");
}
}
}
/**
* Marks all qm's in the message list as read
*/
private void markAllMessagesRead() {
// This iterates through our MessageList and marks the contained threads as read
for (QuickMessage qm : mMessageList) {
Conversation con = Conversation.get(mContext, qm.getThreadId(), true);
if (con != null) {
con.markAsRead(false);
if (DEBUG)
Log.d(LOG_TAG, "markAllMessagesRead(): Marked message " + qm.getThreadId()
+ " as read");
}
}
}
/**
* Show the appropriate image for the QuickContact badge
*
* @param badge
* @param addr
* @param isSelf
*/
private void updateContactBadge(QuickContactBadge badge, String addr, boolean isSelf) {
Drawable avatarDrawable;
if (isSelf || !TextUtils.isEmpty(addr)) {
Contact contact = isSelf ? Contact.getMe(false) : Contact.get(addr, false);
avatarDrawable = contact.getAvatar(mContext, mDefaultContactImage);
if (isSelf) {
badge.assignContactUri(Profile.CONTENT_URI);
} else {
if (contact.existsInDatabase()) {
badge.assignContactUri(contact.getUri());
} else {
badge.assignContactFromPhone(contact.getNumber(), true);
}
}
} else {
avatarDrawable = mDefaultContactImage;
}
badge.setImageDrawable(avatarDrawable);
}
/**
* Use standard api to send the supplied message
*
* @param message - message to send
* @param qm - qm to reply to (for sender details)
*/
private void sendQuickMessage(String message, QuickMessage qm) {
if (message != null && qm != null) {
long threadId = qm.getThreadId();
SmsMessageSender sender = new SmsMessageSender(getBaseContext(),
qm.getFromNumber(), message, threadId);
try {
if (DEBUG)
Log.d(LOG_TAG, "sendQuickMessage(): Sending message to " + qm.getFromName()
+ ", with threadID = " + threadId + ". Current page is #" + (mCurrentPage+1));
sender.sendMessage(threadId);
Toast.makeText(mContext, R.string.toast_sending_message, Toast.LENGTH_SHORT).show();
} catch (MmsException e) {
Log.e(LOG_TAG, "Error sending message to " + qm.getFromName());
}
}
}
/**
* Clears the status bar notification and, optionally, mark all messages as read
* This is used to clean up when we are done with all qm's
*
* @param markAsRead - should remaining qm's be maked as read?
*/
private void clearNotification(boolean markAsRead) {
// Dismiss the notification that brought us here.
NotificationManager notificationManager =
(NotificationManager)getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(MessagingNotification.NOTIFICATION_ID);
// Mark all contained conversations as seen
if (markAsRead) {
markAllMessagesRead();
}
// Clear the messages list
mMessageList.clear();
if (DEBUG)
Log.d(LOG_TAG, "clearNotification(): Message list cleared. Size = " + mMessageList.size());
}
/**
* This method formats the message text to include smiley and emoji graphics as appropriate
*
* @param message - message to format
* @return - formatted message
*/
private CharSequence formatMessage(String message) {
SpannableStringBuilder buf = new SpannableStringBuilder();
// Get the emojis preference
SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mContext);
boolean enableEmojis = prefs.getBoolean(MessagingPreferenceActivity.ENABLE_EMOJIS, false);
if (!TextUtils.isEmpty(message)) {
SmileyParser parser = SmileyParser.getInstance();
CharSequence smileyBody = parser.addSmileySpans(message);
if (enableEmojis) {
EmojiParser emojiParser = EmojiParser.getInstance();
smileyBody = emojiParser.addEmojiSpans(smileyBody);
}
buf.append(smileyBody);
}
return buf;
}
/**
* This method queries the Templates database and returns the count of templates
*
* @return - number of templates
*/
private int getTemplatesCount() {
Cursor cur = getContentResolver().query(Template.CONTENT_URI, null, null, null, null);
int numColumns = cur.getCount();
cur.close();
return numColumns;
}
/**
* Async data loader used for loading and displaying Templates
*/
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(this, Template.CONTENT_URI, null, null, null, null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
if(data != null && data.getCount() > 0){
showDialog(DIALOG_TEMPLATE_SELECT);
mTemplatesCursorAdapter.swapCursor(data);
} else {
showDialog(DIALOG_TEMPLATE_NOT_AVAILABLE);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
/**
* This displays the Templates selection dialog
*/
@Override
protected Dialog onCreateDialog(int id, Bundle args) {
AlertDialog.Builder builder = new AlertDialog.Builder(this);
switch (id) {
case DIALOG_TEMPLATE_NOT_AVAILABLE:
builder.setTitle(R.string.template_not_present_error_title);
builder.setMessage(R.string.template_not_present_error);
return builder.create();
case DIALOG_TEMPLATE_SELECT:
builder = new AlertDialog.Builder(this);
builder.setTitle(R.string.template_select);
mTemplatesCursorAdapter = new SimpleCursorAdapter(this,
android.R.layout.simple_list_item_1, null, new String[] {
Template.TEXT
}, new int[] {
android.R.id.text1
}, CursorAdapter.FLAG_REGISTER_CONTENT_OBSERVER);
builder.setAdapter(mTemplatesCursorAdapter, new DialogInterface.OnClickListener(){
@Override
public void onClick(DialogInterface dialog, int which) {
// Get the selected template and text
Cursor c = (Cursor) mTemplatesCursorAdapter.getItem(which);
String text = c.getString(c.getColumnIndex(Template.TEXT));
// Get the currently visible message and append text
QuickMessage qm = mMessageList.get(mCurrentPage);
if (qm != null) {
// insert the template text at the cursor location or replace selected
int start = qm.getEditText().getSelectionStart();
int end = qm.getEditText().getSelectionEnd();
qm.getEditText().getText().replace(Math.min(start, end),
Math.max(start, end), text);
}
}
});
return builder.create();
}
return super.onCreateDialog(id, args);
}
//==========================================================
// Inner classes
//==========================================================
/**
* Class copied from ComposeMessageActivity.java
* InputFilter which attempts to substitute characters that cannot be
* encoded in the limited GSM 03.38 character set. In many cases this will
* prevent the keyboards auto-correction feature from inserting characters
* that would switch the message from 7-bit GSM encoding (160 char limit)
* to 16-bit Unicode encoding (70 char limit).
*/
private class StripUnicode implements InputFilter {
private CharsetEncoder gsm =
Charset.forName("gsm-03.38-2000").newEncoder();
private Pattern diacritics =
Pattern.compile("\\p{InCombiningDiacriticalMarks}");
public CharSequence filter(CharSequence source, int start, int end,
Spanned dest, int dstart, int dend) {
Boolean unfiltered = true;
StringBuilder output = new StringBuilder(end - start);
for (int i = start; i < end; i++) {
char c = source.charAt(i);
// Character is encodable by GSM, skip filtering
if (gsm.canEncode(c)) {
output.append(c);
}
// Character requires Unicode, try to replace it
else {
unfiltered = false;
String s = String.valueOf(c);
// Try normalizing the character into Unicode NFKD form and
// stripping out diacritic mark characters.
s = Normalizer.normalize(s, Normalizer.Form.NFKD);
s = diacritics.matcher(s).replaceAll("");
// Special case characters that don't get stripped by the
// above technique.
s = s.replace("Œ", "OE");
s = s.replace("œ", "oe");
s = s.replace("Ł", "L");
s = s.replace("ł", "l");
s = s.replace("Đ", "DJ");
s = s.replace("đ", "dj");
s = s.replace("Α", "A");
s = s.replace("Β", "B");
s = s.replace("Ε", "E");
s = s.replace("Ζ", "Z");
s = s.replace("Η", "H");
s = s.replace("Ι", "I");
s = s.replace("Κ", "K");
s = s.replace("Μ", "M");
s = s.replace("Ν", "N");
s = s.replace("Ο", "O");
s = s.replace("Ρ", "P");
s = s.replace("Τ", "T");
s = s.replace("Υ", "Y");
s = s.replace("Χ", "X");
s = s.replace("α", "A");
s = s.replace("β", "B");
s = s.replace("γ", "Γ");
s = s.replace("δ", "Δ");
s = s.replace("ε", "E");
s = s.replace("ζ", "Z");
s = s.replace("η", "H");
s = s.replace("θ", "Θ");
s = s.replace("ι", "I");
s = s.replace("κ", "K");
s = s.replace("λ", "Λ");
s = s.replace("μ", "M");
s = s.replace("ν", "N");
s = s.replace("ξ", "Ξ");
s = s.replace("ο", "O");
s = s.replace("π", "Π");
s = s.replace("ρ", "P");
s = s.replace("σ", "Σ");
s = s.replace("τ", "T");
s = s.replace("υ", "Y");
s = s.replace("φ", "Φ");
s = s.replace("χ", "X");
s = s.replace("ψ", "Ψ");
s = s.replace("ω", "Ω");
s = s.replace("ς", "Σ");
output.append(s);
}
}
// No changes were attempted, so don't return anything
if (unfiltered) {
return null;
}
// Source is a spanned string, so copy the spans from it
else if (source instanceof Spanned) {
SpannableString spannedoutput = new SpannableString(output);
TextUtils.copySpansFrom(
(Spanned) source, start, end, null, spannedoutput, 0);
return spannedoutput;
}
// Source is a vanilla charsequence, so return output as-is
else {
return output;
}
}
}
/**
* Message Pager class, used to display and navigate through the ViewPager pages
*/
private class MessagePagerAdapter extends PagerAdapter
implements ViewPager.OnPageChangeListener {
protected LinearLayout mCurrentPrimaryLayout = null;
@Override
public int getCount() {
return mMessageList.size();
}
@Override
public Object instantiateItem(View collection, int position) {
// Load the layout to be used
LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View layout;
if (mDarkTheme) {
layout = inflater.inflate(R.layout.quickmessage_content_dark, null);
} else {
layout = inflater.inflate(R.layout.quickmessage_content_light, null);
}
// Load the main views
EditText qmReplyText = (EditText) layout.findViewById(R.id.embedded_text_editor);
TextView qmTextCounter = (TextView) layout.findViewById(R.id.text_counter);
ImageButton qmSendButton = (ImageButton) layout.findViewById(R.id.send_button_sms);
ImageButton qmTemplatesButton = (ImageButton) layout.findViewById(R.id.templates_button);
TextView qmMessageText = (TextView) layout.findViewById(R.id.messageTextView);
TextView qmFromName = (TextView) layout.findViewById(R.id.fromTextView);
TextView qmTimestamp = (TextView) layout.findViewById(R.id.timestampTextView);
QuickContactBadge qmContactBadge = (QuickContactBadge) layout.findViewById(R.id.contactBadge);
// Retrieve the current message
QuickMessage qm = mMessageList.get(position);
if (qm != null) {
if (DEBUG)
Log.d(LOG_TAG, "instantiateItem(): Creating page #" + (position + 1) + " for message from "
+ qm.getFromName() + ". Number of pages to create = " + getCount());
// Set the general fields
qmFromName.setText(qm.getFromName());
qmTimestamp.setText(MessageUtils.formatTimeStampString(mContext, qm.getTimestamp(), mFullTimestamp));
updateContactBadge(qmContactBadge, qm.getFromNumber()[0], false);
qmMessageText.setText(formatMessage(qm.getMessageBody()));
if (!mDarkTheme) {
// We are using a holo.light background with a holo.dark activity theme
// Override the EditText background to use the holo.light theme
qmReplyText.setBackgroundResource(R.drawable.edit_text_holo_light);
}
// Set the remaining values
qmReplyText.setInputType(InputType.TYPE_CLASS_TEXT | mInputMethod
| InputType.TYPE_TEXT_FLAG_AUTO_CORRECT
| InputType.TYPE_TEXT_FLAG_CAP_SENTENCES
| InputType.TYPE_TEXT_FLAG_MULTI_LINE);
qmReplyText.setText(qm.getReplyText());
qmReplyText.setSelection(qm.getReplyText().length());
qmReplyText.addTextChangedListener(new QmTextWatcher(mContext, qmTextCounter, qmSendButton,
qmTemplatesButton, mNumTemplates));
qmReplyText.setOnEditorActionListener(new OnEditorActionListener() {
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (event != null) {
// event != null means enter key pressed
if (!event.isShiftPressed()) {
// if shift is not pressed then move focus to send button
if (v != null) {
View focusableView = v.focusSearch(View.FOCUS_RIGHT);
if (focusableView != null) {
focusableView.requestFocus();
return true;
}
}
}
return false;
}
if (actionId == EditorInfo.IME_ACTION_SEND) {
if (v != null) {
QuickMessage qm = mMessageList.get(mCurrentPage);
if (qm != null) {
sendMessageAndMoveOn(v.getText().toString(), qm);
}
}
return true;
}
return true;
}
});
LengthFilter lengthFilter = new LengthFilter(MmsConfig.getMaxTextLimit());
if (mStripUnicode) {
qmReplyText.setFilters(new InputFilter[] { new StripUnicode(), lengthFilter });
} else {
qmReplyText.setFilters(new InputFilter[] { lengthFilter });
}
QmTextWatcher.getQuickReplyCounterText(qmReplyText.getText().toString(),
qmTextCounter, qmSendButton, qmTemplatesButton, mNumTemplates);
// Add the context menu
registerForContextMenu(qmReplyText);
// Store the EditText object for future use
qm.setEditText(qmReplyText);
// Templates button
qmTemplatesButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
selectTemplate();
}
});
// Send button
qmSendButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
QuickMessage qm = mMessageList.get(mCurrentPage);
if (qm != null) {
EditText editView = qm.getEditText();
if (editView != null) {
sendMessageAndMoveOn(editView.getText().toString(), qm);
}
}
}
});
// Add the layout to the viewpager
((ViewPager) collection).addView(layout);
}
return layout;
}
/**
* This method sends the supplied message in reply to the supplied qm and then
* moves to the next or previous message as appropriate. If this is the last qm
* in the MessageList, we end by clearing the notification and calling finish()
*
* @param message - message to send
* @param qm - qm we are replying to (for sender details)
*/
private void sendMessageAndMoveOn(String message, QuickMessage qm) {
sendQuickMessage(message, qm);
// Close the current QM and move on
int numMessages = mMessageList.size();
if (numMessages == 1) {
// No more messages
clearNotification(true);
finish();
} else {
// Dismiss the keyboard if it is shown
dismissKeyboard(qm);
if (mCurrentPage < numMessages-1) {
showNextMessageWithRemove(qm);
} else {
showPreviousMessageWithRemove(qm);
}
}
}
@Override
public void setPrimaryItem(ViewGroup container, int position, Object object) {
LinearLayout view = ((LinearLayout)object);
if (view != mCurrentPrimaryLayout) {
mCurrentPrimaryLayout = view;
}
}
@Override
public void onPageSelected(int position) {
// The user had scrolled to a new message
if (mCurrentQm != null) {
mCurrentQm.saveReplyText();
}
// Set the new 'active' QuickMessage
mCurrentPage = position;
mCurrentQm = mMessageList.get(position);
if (DEBUG)
Log.d(LOG_TAG, "onPageSelected(): Current page is #" + (position+1)
+ " of " + getCount() + " pages. Currenty visible message is from "
+ mCurrentQm.getFromName());
updateMessageCounter();
}
@Override
public int getItemPosition(Object object) {
// This is needed to force notifyDatasetChanged() to rebuild the pages
return PagerAdapter.POSITION_NONE;
}
@Override
public void destroyItem(View collection, int position, Object view) {
((ViewPager) collection).removeView((LinearLayout) view);
}
@Override
public boolean isViewFromObject(View view, Object object) {
return view == ((LinearLayout)object);
}
@Override
public void finishUpdate(View arg0) {}
@Override
public void restoreState(Parcelable arg0, ClassLoader arg1) {}
@Override
public Parcelable saveState() {
return null;
}
@Override
public void startUpdate(View arg0) {}
@Override
public void onPageScrollStateChanged(int arg0) {}
@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {}
}
}