Created
August 4, 2014 08:03
-
-
Save acappelli/b8446f18c82d0f6752ae to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Kontalk Android client | |
* Copyright (C) 2014 Kontalk Devteam <devteam@kontalk.org> | |
* This program is free software: you can redistribute it and/or modify | |
* it under the terms of the GNU General Public License as published by | |
* the Free Software Foundation, either version 3 of the License, or | |
* (at your option) any later version. | |
* This program is distributed in the hope that it will be useful, | |
* but WITHOUT ANY WARRANTY; without even the implied warranty of | |
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
* GNU General Public License for more details. | |
* You should have received a copy of the GNU General Public License | |
* along with this program. If not, see <http://www.gnu.org/licenses/>. | |
*/ | |
package org.kontalk.ui; | |
import static android.content.res.Configuration.KEYBOARDHIDDEN_NO; | |
import static org.kontalk.service.msgcenter.MessageCenterService.PRIVACY_ACCEPT; | |
import static org.kontalk.service.msgcenter.MessageCenterService.PRIVACY_BLOCK; | |
import static org.kontalk.service.msgcenter.MessageCenterService.PRIVACY_UNBLOCK; | |
import java.io.File; | |
import java.io.IOException; | |
import java.util.Date; | |
import java.util.HashSet; | |
import java.util.List; | |
import java.util.Random; | |
import java.util.Set; | |
import java.util.regex.Pattern; | |
import org.jivesoftware.smack.packet.Presence; | |
import org.jivesoftware.smackx.chatstates.ChatState; | |
import org.kontalk.R; | |
import org.kontalk.authenticator.Authenticator; | |
import org.kontalk.client.EndpointServer; | |
import org.kontalk.crypto.Coder; | |
import org.kontalk.crypto.PGP; | |
import org.kontalk.data.Contact; | |
import org.kontalk.data.Conversation; | |
import org.kontalk.message.AttachmentComponent; | |
import org.kontalk.message.AudioComponent; | |
import org.kontalk.message.CompositeMessage; | |
import org.kontalk.message.ImageComponent; | |
import org.kontalk.message.MessageComponent; | |
import org.kontalk.message.TextComponent; | |
import org.kontalk.message.VCardComponent; | |
import org.kontalk.provider.MessagesProvider; | |
import org.kontalk.provider.MyMessages.CommonColumns; | |
import org.kontalk.provider.MyMessages.Messages; | |
import org.kontalk.provider.MyMessages.Threads; | |
import org.kontalk.provider.MyMessages.Threads.Conversations; | |
import org.kontalk.provider.MyMessages.Threads.Requests; | |
import org.kontalk.provider.UsersProvider; | |
import org.kontalk.service.DownloadService; | |
import org.kontalk.service.msgcenter.MessageCenterService; | |
import org.kontalk.sync.Syncer; | |
import org.kontalk.ui.AudioDialog.OnAudioDialogResult; | |
import org.kontalk.ui.IconContextMenu.IconContextMenuOnClickListener; | |
import org.kontalk.util.MediaStorage; | |
import org.kontalk.util.MessageUtils; | |
import org.kontalk.util.MessageUtils.SmileyImageSpan; | |
import org.kontalk.util.Preferences; | |
import org.spongycastle.openpgp.PGPPublicKey; | |
import org.spongycastle.openpgp.PGPPublicKeyRing; | |
import android.annotation.TargetApi; | |
import android.app.Activity; | |
import android.app.AlertDialog; | |
import android.content.AsyncQueryHandler; | |
import android.content.BroadcastReceiver; | |
import android.content.ContentResolver; | |
import android.content.ContentUris; | |
import android.content.ContentValues; | |
import android.content.Context; | |
import android.content.DialogInterface; | |
import android.content.Intent; | |
import android.content.IntentFilter; | |
import android.content.pm.PackageManager; | |
import android.content.pm.ResolveInfo; | |
import android.content.res.Configuration; | |
import android.database.ContentObserver; | |
import android.database.Cursor; | |
import android.database.sqlite.SQLiteException; | |
import android.graphics.Color; | |
import android.graphics.drawable.Drawable; | |
import android.media.AudioManager; | |
import android.media.MediaPlayer; | |
import android.net.Uri; | |
import android.os.Build; | |
import android.os.Bundle; | |
import android.os.Handler; | |
import android.provider.ContactsContract.Contacts; | |
import android.provider.MediaStore; | |
import android.support.v4.app.ListFragment; | |
import android.support.v4.content.LocalBroadcastManager; | |
import android.text.ClipboardManager; | |
import android.text.Editable; | |
import android.text.Html; | |
import android.text.InputType; | |
import android.text.TextUtils; | |
import android.text.TextWatcher; | |
import android.util.Log; | |
import android.view.ContextMenu; | |
import android.view.ContextMenu.ContextMenuInfo; | |
import android.view.KeyEvent; | |
import android.view.LayoutInflater; | |
import android.view.Menu; | |
import android.view.MenuInflater; | |
import android.view.MenuItem; | |
import android.view.View; | |
import android.view.ViewGroup; | |
import android.view.inputmethod.EditorInfo; | |
import android.view.inputmethod.InputMethodManager; | |
import android.widget.AdapterView; | |
import android.widget.AdapterView.AdapterContextMenuInfo; | |
import android.widget.EditText; | |
import android.widget.ImageButton; | |
import android.widget.ListView; | |
import android.widget.SeekBar; | |
import android.widget.TextView; | |
import android.widget.TextView.OnEditorActionListener; | |
import android.widget.Toast; | |
/** | |
* The composer fragment. | |
* @author Daniele Ricci | |
*/ | |
public class ComposeMessageFragment extends ListFragment implements | |
View.OnLongClickListener, IconContextMenuOnClickListener, OnAudioDialogResult, AudioContentView.AudioPlayerControl { | |
private static final String TAG = ComposeMessage.TAG; | |
private static final int MESSAGE_LIST_QUERY_TOKEN = 8720; | |
private static final int CONVERSATION_QUERY_TOKEN = 8721; | |
private static final int SELECT_ATTACHMENT_OPENABLE = Activity.RESULT_FIRST_USER + 1; | |
private static final int SELECT_ATTACHMENT_CONTACT = Activity.RESULT_FIRST_USER + 2; | |
/** Context menu group ID for this fragment. */ | |
private static final int CONTEXT_MENU_GROUP_ID = 2; | |
/* Attachment chooser stuff. */ | |
private static final int CONTEXT_MENU_ATTACHMENT = 1; | |
private static final int ATTACHMENT_ACTION_PICTURE = 1; | |
private static final int ATTACHMENT_ACTION_CONTACT = 2; | |
private static final int ATTACHMENT_ACTION_AUDIO = 3; | |
private IconContextMenu attachmentMenu; | |
private MessageListQueryHandler mQueryHandler; | |
private MessageListAdapter mListAdapter; | |
private EditText mTextEntry; | |
private View mSendButton; | |
private ViewGroup mInvitationBar; | |
private MenuItem mDeleteThreadMenu; | |
private MenuItem mViewContactMenu; | |
private MenuItem mCallMenu; | |
private MenuItem mBlockMenu; | |
private MenuItem mUnblockMenu; | |
/** The thread id. */ | |
private long threadId = -1; | |
private Conversation mConversation; | |
private Bundle mArguments; | |
/** The user we are talking to. */ | |
private String userId; | |
private String userName; | |
private String userPhone; | |
/** Presence probe packet id. */ | |
private String mPresenceId; | |
/** Last most available stanza. */ | |
private PresenceData mMostAvailable; | |
/** Available resources. */ | |
private Set<String> mAvailableResources = new HashSet<String>(); | |
/** MediaPlayer */ | |
private MediaPlayer mPlayer; | |
private PeerObserver mPeerObserver; | |
private File mCurrentPhoto; | |
private LocalBroadcastManager mLocalBroadcastManager; | |
private BroadcastReceiver mPresenceReceiver; | |
private BroadcastReceiver mPrivacyListener; | |
private QuickAction mSmileyPopup; | |
private boolean mOfflineModeWarned; | |
private boolean mComposeSent; | |
private boolean mIsTyping; | |
private CharSequence mCurrentStatus; | |
private TextWatcher mChatStateListener; | |
private AdapterView.OnItemClickListener mSmileySelectListener; | |
private static final class PresenceData { | |
public String status; | |
public int priority; | |
public Date stamp; | |
} | |
/** Returns a new fragment instance from a picked contact. */ | |
public static ComposeMessageFragment fromUserId(Context context, String userId) { | |
ComposeMessageFragment f = new ComposeMessageFragment(); | |
Conversation conv = Conversation.loadFromUserId(context, userId); | |
// not found - create new | |
if (conv == null) { | |
Bundle args = new Bundle(); | |
args.putString("action", ComposeMessage.ACTION_VIEW_USERID); | |
args.putParcelable("data", Threads.getUri(userId)); | |
f.setArguments(args); | |
return f; | |
} | |
return fromConversation(context, conv); | |
} | |
/** Returns a new fragment instance from a {@link Conversation} instance. */ | |
public static ComposeMessageFragment fromConversation(Context context, | |
Conversation conv) { | |
return fromConversation(context, conv.getThreadId()); | |
} | |
/** Returns a new fragment instance from a thread ID. */ | |
public static ComposeMessageFragment fromConversation(Context context, | |
long threadId) { | |
ComposeMessageFragment f = new ComposeMessageFragment(); | |
Bundle args = new Bundle(); | |
args.putString("action", ComposeMessage.ACTION_VIEW_CONVERSATION); | |
args.putParcelable("data", | |
ContentUris.withAppendedId(Conversations.CONTENT_URI, threadId)); | |
f.setArguments(args); | |
return f; | |
} | |
@Override | |
public void onActivityCreated(Bundle savedInstanceState) { | |
super.onActivityCreated(savedInstanceState); | |
// setListAdapter() is post-poned | |
ListView list = getListView(); | |
list.setFastScrollEnabled(true); | |
registerForContextMenu(list); | |
// set custom background (if any) | |
Drawable bg = Preferences.getConversationBackground(getActivity()); | |
if (bg != null) { | |
list.setCacheColorHint(Color.TRANSPARENT); | |
list.setBackgroundDrawable(bg); | |
} | |
mTextEntry = (EditText) getView().findViewById(R.id.text_editor); | |
// enter key flag | |
int inputTypeFlags = Preferences.getEnterKeyEnabled(getActivity()) ? | |
InputType.TYPE_TEXT_VARIATION_LONG_MESSAGE : | |
InputType.TYPE_TEXT_VARIATION_SHORT_MESSAGE; | |
mTextEntry.setInputType(mTextEntry.getInputType() | inputTypeFlags); | |
mTextEntry.addTextChangedListener(new TextWatcher() { | |
@Override | |
public void onTextChanged(CharSequence s, int start, int before, int count) { | |
} | |
@Override | |
public void beforeTextChanged(CharSequence s, int start, int count, int after) { | |
} | |
@Override | |
public void afterTextChanged(Editable s) { | |
// convert smiley codes | |
mTextEntry.removeTextChangedListener(this); | |
MessageUtils.convertSmileys(getActivity(), s, SmileyImageSpan.SIZE_EDITABLE); | |
mTextEntry.addTextChangedListener(this); | |
// enable the send button if there is something to send | |
mSendButton.setEnabled(s.length() > 0); | |
} | |
}); | |
mTextEntry.setOnEditorActionListener(new OnEditorActionListener() { | |
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { | |
if (actionId == EditorInfo.IME_ACTION_SEND) { | |
InputMethodManager imm = (InputMethodManager) getActivity() | |
.getSystemService(Context.INPUT_METHOD_SERVICE); | |
imm.hideSoftInputFromWindow(v.getApplicationWindowToken(), 0); | |
submitSend(); | |
return true; | |
} | |
return false; | |
} | |
}); | |
mChatStateListener = new TextWatcher() { | |
public void onTextChanged(CharSequence s, int start, int before, int count) { | |
if (Preferences.getSendTyping(getActivity())) { | |
// send typing notification if necessary | |
if (!mComposeSent && mAvailableResources.size() > 0) { | |
MessageCenterService.sendChatState(getActivity(), userId, ChatState.composing); | |
mComposeSent = true; | |
} | |
} | |
} | |
public void beforeTextChanged(CharSequence s, int start, int count, int after) { | |
} | |
@Override | |
public void afterTextChanged(Editable s) { | |
} | |
}; | |
mSendButton = getView().findViewById(R.id.send_button); | |
mSendButton.setEnabled(mTextEntry.length() > 0); | |
mSendButton.setOnClickListener(new View.OnClickListener() { | |
@Override | |
public void onClick(View v) { | |
submitSend(); | |
} | |
}); | |
mSmileySelectListener = new AdapterView.OnItemClickListener() { | |
public void onItemClick(AdapterView<?> parent, View view, int position, long id) { | |
Editable text = mTextEntry.getText(); | |
int startPos = mTextEntry.getSelectionStart(); | |
int endPos = mTextEntry.getSelectionEnd(); | |
if (startPos < 0) startPos = text.length(); | |
if (endPos < 0) endPos = startPos; | |
int startMin = Math.min(startPos, endPos); | |
// add unicode emoji | |
char[] value = Character.toChars((int) id); | |
text.replace(startMin, Math.max(startPos, endPos), | |
String.valueOf(value), 0, value.length); | |
// textview change listener will do the rest | |
// dismiss smileys popup | |
// TEST mSmileyPopup.dismiss(); | |
} | |
}; | |
ImageButton smileyButton = (ImageButton) getView().findViewById(R.id.smiley_button); | |
smileyButton.setOnClickListener(new View.OnClickListener() { | |
@Override | |
public void onClick(View v) { | |
showSmileysPopup(v); | |
} | |
}); | |
Configuration config = getResources().getConfiguration(); | |
onKeyboardStateChanged(config.keyboardHidden == KEYBOARDHIDDEN_NO); | |
mLocalBroadcastManager = LocalBroadcastManager.getInstance(getActivity()); | |
processArguments(savedInstanceState); | |
} | |
@Override | |
public void onConfigurationChanged(Configuration newConfig) { | |
super.onConfigurationChanged(newConfig); | |
onKeyboardStateChanged(newConfig.keyboardHidden == KEYBOARDHIDDEN_NO); | |
} | |
public void reload() { | |
processArguments(null); | |
} | |
@Override | |
public View onCreateView(LayoutInflater inflater, ViewGroup container, | |
Bundle savedInstanceState) { | |
return inflater.inflate(R.layout.compose_message, container, false); | |
} | |
private final MessageListAdapter.OnContentChangedListener mContentChangedListener = new MessageListAdapter.OnContentChangedListener() { | |
public void onContentChanged(MessageListAdapter adapter) { | |
if (isVisible()) | |
startQuery(true, false); | |
} | |
}; | |
@Override | |
public void onCreate(Bundle savedInstanceState) { | |
super.onCreate(savedInstanceState); | |
setHasOptionsMenu(true); | |
mQueryHandler = new MessageListQueryHandler(); | |
// list adapter creation is post-poned | |
} | |
private void submitSend() { | |
mTextEntry.removeTextChangedListener(mChatStateListener); | |
// send message | |
sendTextMessage(null, true); | |
// reset compose sent flag | |
mComposeSent = false; | |
mTextEntry.addTextChangedListener(mChatStateListener); | |
} | |
/** Sends out a binary message. */ | |
public void sendBinaryMessage(Uri uri, String mime, boolean media, | |
Class<? extends MessageComponent<?>> klass) { | |
Log.v(TAG, "sending binary content: " + uri); | |
Uri newMsg = null; | |
File previewFile = null; | |
long length = -1; | |
try { | |
// TODO convert to thread (?) | |
offlineModeWarning(); | |
String msgId = "draft" + (new Random().nextInt()); | |
// generate thumbnail | |
// FIXME this is blocking!!!! | |
if (media && klass == ImageComponent.class) { | |
// FIXME hard-coded to ImageComponent | |
String filename = ImageComponent.buildMediaFilename(msgId, MediaStorage.THUMBNAIL_MIME); | |
previewFile = MediaStorage.cacheThumbnail(getActivity(), uri, | |
filename); | |
} | |
length = MediaStorage.getLength(getActivity(), uri); | |
// save to database | |
ContentValues values = new ContentValues(); | |
// must supply a message ID... | |
values.put(Messages.MESSAGE_ID, msgId); | |
values.put(Messages.PEER, userId); | |
/* TODO ask for a text to send with the image | |
values.put(Messages.BODY_MIME, TextComponent.MIME_TYPE); | |
values.put(Messages.BODY_CONTENT, content.getBytes()); | |
values.put(Messages.BODY_LENGTH, content.length()); | |
*/ | |
values.put(Messages.UNREAD, false); | |
values.put(Messages.ENCRYPTED, false); | |
values.put(Messages.DIRECTION, Messages.DIRECTION_OUT); | |
values.put(Messages.TIMESTAMP, System.currentTimeMillis()); | |
values.put(Messages.STATUS, Messages.STATUS_SENDING); | |
if (previewFile != null) | |
values.put(Messages.ATTACHMENT_PREVIEW_PATH, previewFile.getAbsolutePath()); | |
values.put(Messages.ATTACHMENT_MIME, mime); | |
values.put(Messages.ATTACHMENT_LOCAL_URI, uri.toString()); | |
values.put(Messages.ATTACHMENT_LENGTH, length); | |
newMsg = getActivity().getContentResolver().insert( | |
Messages.CONTENT_URI, values); | |
} | |
catch (Exception e) { | |
Log.e(TAG, "unable to store media", e); | |
} | |
if (newMsg != null) { | |
// update thread id from the inserted message | |
if (threadId <= 0) { | |
Cursor c = getActivity().getContentResolver().query(newMsg, | |
new String[] { Messages.THREAD_ID }, null, null, null); | |
if (c.moveToFirst()) { | |
threadId = c.getLong(0); | |
mConversation = null; | |
startQuery(true, false); | |
} | |
else { | |
Log.v(TAG, "no data - cannot start query for this composer"); | |
} | |
c.close(); | |
} | |
// send message! | |
// FIXME do not encrypt binary messages for now | |
String previewPath = (previewFile != null) ? previewFile.getAbsolutePath() : null; | |
MessageCenterService.sendBinaryMessage(getActivity(), | |
userId, mime, uri, length, previewPath, ContentUris.parseId(newMsg)); | |
} | |
else { | |
getActivity().runOnUiThread(new Runnable() { | |
public void run() { | |
Toast.makeText(getActivity(), | |
R.string.err_store_message_failed, | |
Toast.LENGTH_LONG).show(); | |
} | |
}); | |
} | |
} | |
private final class TextMessageThread extends Thread { | |
private final String mText; | |
TextMessageThread(String text) { | |
mText = text; | |
} | |
@Override | |
public void run() { | |
try { | |
boolean encrypted = Preferences.getEncryptionEnabled(getActivity()); | |
/* TODO maybe this hack could work...? | |
MessageListItem v = (MessageListItem) LayoutInflater.from(getActivity()) | |
.inflate(R.layout.message_list_item, getListView(), false); | |
v.bind(getActivity(), msg, contact, null); | |
getListView().addFooterView(v); | |
*/ | |
byte[] bytes = mText.getBytes(); | |
// save to local storage | |
ContentValues values = new ContentValues(); | |
// must supply a message ID... | |
values.put(Messages.MESSAGE_ID, "draft" + (new Random().nextInt())); | |
values.put(Messages.PEER, userId); | |
values.put(Messages.BODY_MIME, TextComponent.MIME_TYPE); | |
values.put(Messages.BODY_CONTENT, bytes); | |
values.put(Messages.BODY_LENGTH, bytes.length); | |
values.put(Messages.UNREAD, false); | |
values.put(Messages.DIRECTION, Messages.DIRECTION_OUT); | |
values.put(Messages.TIMESTAMP, System.currentTimeMillis()); | |
values.put(Messages.STATUS, Messages.STATUS_SENDING); | |
// of course outgoing messages are not encrypted in database | |
values.put(Messages.ENCRYPTED, false); | |
values.put(Messages.SECURITY_FLAGS, encrypted ? Coder.SECURITY_BASIC : Coder.SECURITY_CLEARTEXT); | |
Uri newMsg = getActivity().getContentResolver().insert( | |
Messages.CONTENT_URI, values); | |
if (newMsg != null) { | |
// update thread id from the inserted message | |
if (threadId <= 0) { | |
Cursor c = getActivity().getContentResolver().query(newMsg, | |
new String[] { Messages.THREAD_ID }, null, null, | |
null); | |
if (c.moveToFirst()) { | |
threadId = c.getLong(0); | |
mConversation = null; | |
// we can run it here because progress=false | |
startQuery(true, false); | |
} | |
else { | |
Log.v(TAG, "no data - cannot start query for this composer"); | |
} | |
c.close(); | |
} | |
// send message! | |
MessageCenterService.sendTextMessage(getActivity(), | |
userId, mText, Preferences | |
.getEncryptionEnabled(getActivity()), | |
ContentUris.parseId(newMsg)); | |
} | |
else { | |
getActivity().runOnUiThread(new Runnable() { | |
@Override | |
public void run() { | |
Toast.makeText(getActivity(), R.string.error_store_outbox, | |
Toast.LENGTH_LONG).show(); | |
} | |
}); | |
} | |
} | |
catch (Exception e) { | |
// whatever | |
Log.d(TAG, "broken message thread", e); | |
} | |
} | |
} | |
/** Sends out the text message in the composing entry. */ | |
public void sendTextMessage(String text, boolean fromTextEntry) { | |
if (fromTextEntry) | |
text = mTextEntry.getText().toString(); | |
if (!TextUtils.isEmpty(text)) { | |
/* | |
* TODO show an animation to warn the user that the message | |
* is being sent (actually stored). | |
*/ | |
offlineModeWarning(); | |
// start thread | |
new TextMessageThread(text).start(); | |
if (fromTextEntry) { | |
// empty text | |
mTextEntry.setText(""); | |
// hide softkeyboard | |
InputMethodManager imm = (InputMethodManager) getActivity() | |
.getSystemService(Context.INPUT_METHOD_SERVICE); | |
imm.hideSoftInputFromWindow(mTextEntry.getWindowToken(), | |
InputMethodManager.HIDE_IMPLICIT_ONLY); | |
} | |
} | |
} | |
@Override | |
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { | |
inflater.inflate(R.menu.compose_message_menu, menu); | |
mDeleteThreadMenu = menu.findItem(R.id.delete_thread); | |
mViewContactMenu = menu.findItem(R.id.view_contact); | |
mCallMenu = menu.findItem(R.id.call_contact); | |
mBlockMenu = menu.findItem(R.id.block_user); | |
mUnblockMenu = menu.findItem(R.id.unblock_user); | |
} | |
@Override | |
public boolean onOptionsItemSelected(MenuItem item) { | |
switch (item.getItemId()) { | |
case R.id.call_contact: | |
startActivity(new Intent(Intent.ACTION_CALL, Uri.parse("tel:" | |
+ userPhone))); | |
return true; | |
case R.id.view_contact: | |
viewContact(); | |
return true; | |
case R.id.menu_attachment: | |
selectAttachment(); | |
return true; | |
case R.id.delete_thread: | |
if (threadId > 0) | |
deleteThread(); | |
return true; | |
case R.id.block_user: | |
blockUser(); | |
return true; | |
case R.id.unblock_user: | |
unblockUser(); | |
return true; | |
} | |
return super.onOptionsItemSelected(item); | |
} | |
@Override | |
public void onListItemClick(ListView listView, View view, int position, long id) { | |
MessageListItem item = (MessageListItem) view; | |
final CompositeMessage msg = item.getMessage(); | |
AttachmentComponent attachment = (AttachmentComponent) msg | |
.getComponent(AttachmentComponent.class); | |
if (attachment != null && (attachment.getFetchUrl() != null || attachment.getLocalUri() != null)) { | |
// outgoing message or already fetched | |
if (attachment.getLocalUri() != null) { | |
// open file | |
openFile(msg); | |
} | |
else { | |
// info & download dialog | |
CharSequence message = MessageUtils | |
.getFileInfoMessage(getActivity(), msg, | |
userPhone != null ? userPhone : userId); | |
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) | |
.setTitle(R.string.title_file_info) | |
.setMessage(message) | |
.setNegativeButton(android.R.string.cancel, null) | |
.setCancelable(true); | |
if (!DownloadService.isQueued(attachment.getFetchUrl())) { | |
DialogInterface.OnClickListener startDL = new DialogInterface.OnClickListener() { | |
public void onClick(DialogInterface dialog, int which) { | |
// start file download | |
startDownload(msg); | |
} | |
}; | |
builder.setPositiveButton(R.string.download, startDL); | |
} | |
else { | |
DialogInterface.OnClickListener stopDL = new DialogInterface.OnClickListener() { | |
public void onClick(DialogInterface dialog, int which) { | |
// cancel file download | |
stopDownload(msg); | |
} | |
}; | |
builder.setPositiveButton(R.string.download_cancel, stopDL); | |
} | |
builder.show(); | |
} | |
} | |
} | |
private void startDownload(CompositeMessage msg) { | |
AttachmentComponent attachment = (AttachmentComponent) msg | |
.getComponent(AttachmentComponent.class); | |
if (attachment != null && attachment.getFetchUrl() != null) { | |
Intent i = new Intent(getActivity(), DownloadService.class); | |
i.setAction(DownloadService.ACTION_DOWNLOAD_URL); | |
i.putExtra(CompositeMessage.MSG_ID, msg.getId()); | |
i.putExtra(CompositeMessage.MSG_SENDER, msg.getSender()); | |
i.setData(Uri.parse(attachment.getFetchUrl())); | |
getActivity().startService(i); | |
} | |
else { | |
// corrupted message :( | |
Toast.makeText(getActivity(), R.string.err_attachment_corrupted, | |
Toast.LENGTH_LONG).show(); | |
} | |
} | |
private void stopDownload(CompositeMessage msg) { | |
AttachmentComponent attachment = (AttachmentComponent) msg | |
.getComponent(AttachmentComponent.class); | |
if (attachment != null && attachment.getFetchUrl() != null) { | |
Intent i = new Intent(getActivity(), DownloadService.class); | |
i.setAction(DownloadService.ACTION_DOWNLOAD_ABORT); | |
i.setData(Uri.parse(attachment.getFetchUrl())); | |
getActivity().startService(i); | |
} | |
} | |
private void openFile(CompositeMessage msg) { | |
AttachmentComponent attachment = (AttachmentComponent) msg | |
.getComponent(AttachmentComponent.class); | |
if (attachment != null && !(attachment instanceof AudioComponent)) { | |
Intent i = new Intent(Intent.ACTION_VIEW); | |
i.setDataAndType(attachment.getLocalUri(), attachment.getMime()); | |
startActivity(i); | |
} | |
} | |
private void openAudio(CompositeMessage msg) { | |
AttachmentComponent attachment = (AttachmentComponent) msg | |
.getComponent(AttachmentComponent.class); | |
if (attachment != null) { | |
Intent i = new Intent(Intent.ACTION_VIEW); | |
i.setDataAndType(attachment.getLocalUri(), attachment.getMime()); | |
startActivity(i); | |
} | |
} | |
/** Listener for attachment type chooser. */ | |
@Override | |
public void onClick(int id) { | |
switch (id) { | |
case ATTACHMENT_ACTION_PICTURE: | |
selectImageAttachment(); | |
break; | |
case ATTACHMENT_ACTION_CONTACT: | |
selectContactAttachment(); | |
break; | |
case ATTACHMENT_ACTION_AUDIO: | |
selectAudioAttachment(); | |
break; | |
} | |
} | |
public void viewContact() { | |
if (mConversation != null) { | |
Contact contact = mConversation.getContact(); | |
if (contact != null) | |
startActivity(new Intent(Intent.ACTION_VIEW, | |
contact.getUri())); | |
} | |
} | |
/** Starts dialog for attachment selection. */ | |
public void selectAttachment() { | |
if (attachmentMenu == null) { | |
attachmentMenu = new IconContextMenu(getActivity(), CONTEXT_MENU_ATTACHMENT); | |
attachmentMenu.addItem(getResources(), R.string.attachment_picture, R.drawable.ic_launcher_gallery, ATTACHMENT_ACTION_PICTURE); | |
attachmentMenu.addItem(getResources(), R.string.attachment_contact, R.drawable.ic_launcher_contacts, ATTACHMENT_ACTION_CONTACT); | |
attachmentMenu.addItem(getResources(), R.string.attachment_audio, R.drawable.ic_launcher_audio, ATTACHMENT_ACTION_AUDIO); | |
attachmentMenu.setOnClickListener(this); | |
} | |
attachmentMenu.createMenu(getString(R.string.menu_attachment)).show(); | |
} | |
/** Starts activity for an image attachment. */ | |
@TargetApi(Build.VERSION_CODES.KITKAT) | |
private void selectImageAttachment() { | |
Intent pictureIntent; | |
if (!MediaStorage.isStorageAccessFrameworkAvailable()) { | |
pictureIntent = new Intent(Intent.ACTION_GET_CONTENT); | |
} | |
else { | |
pictureIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT); | |
} | |
pictureIntent | |
.addCategory(Intent.CATEGORY_OPENABLE) | |
.setType("image/*"); | |
Intent chooser = null; | |
try { | |
// check if camera is available | |
final PackageManager packageManager = getActivity().getPackageManager(); | |
final Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); | |
List<ResolveInfo> list = | |
packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); | |
if (list.size() <= 0) throw new UnsupportedOperationException(); | |
mCurrentPhoto = MediaStorage.getTempImage(getActivity()); | |
Intent take = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); | |
take.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(mCurrentPhoto)); | |
chooser = Intent.createChooser(pictureIntent, getString(R.string.chooser_send_picture)); | |
chooser.putExtra(Intent.EXTRA_INITIAL_INTENTS, new Intent[] { take }); | |
} | |
catch (UnsupportedOperationException ue) { | |
Log.d(TAG, "no camera app or no camera present", ue); | |
} | |
catch (IOException e) { | |
Log.e(TAG, "error creating temp file", e); | |
Toast.makeText(getActivity(), R.string.chooser_error_no_camera, | |
Toast.LENGTH_LONG).show(); | |
} | |
if (chooser == null) chooser = pictureIntent; | |
startActivityForResult(chooser, SELECT_ATTACHMENT_OPENABLE); | |
} | |
/** Starts activity for a vCard attachment from a contact. */ | |
private void selectContactAttachment() { | |
Intent i = new Intent(Intent.ACTION_PICK, Contacts.CONTENT_URI); | |
startActivityForResult(i, SELECT_ATTACHMENT_CONTACT); | |
} | |
private void selectAudioAttachment() { | |
new AudioDialog(getActivity(), this).show(); | |
} | |
private void showSmileysPopup(View anchor) { | |
if (mSmileyPopup == null) | |
mSmileyPopup = MessageUtils.smileysPopup(getActivity(), mSmileySelectListener); | |
mSmileyPopup.show(anchor); | |
} | |
private void deleteThread() { | |
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); | |
builder.setTitle(R.string.confirm_delete_thread); | |
builder.setIcon(android.R.drawable.ic_dialog_alert); | |
builder.setMessage(R.string.confirm_will_delete_thread); | |
builder.setPositiveButton(android.R.string.ok, | |
new DialogInterface.OnClickListener() { | |
@Override | |
public void onClick(DialogInterface dialog, int which) { | |
mTextEntry.setText(""); | |
MessagesProvider.deleteThread(getActivity(), threadId); | |
} | |
}); | |
builder.setNegativeButton(android.R.string.cancel, null); | |
builder.create().show(); | |
} | |
private void deleteMessage(final long id) { | |
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); | |
builder.setTitle(R.string.confirm_delete_message); | |
builder.setIcon(android.R.drawable.ic_dialog_alert); | |
builder.setMessage(R.string.confirm_will_delete_message); | |
builder.setPositiveButton(android.R.string.ok, | |
new DialogInterface.OnClickListener() { | |
@Override | |
public void onClick(DialogInterface dialog, int which) { | |
getActivity().getContentResolver().delete( | |
ContentUris.withAppendedId( | |
Messages.CONTENT_URI, id), null, null); | |
} | |
}); | |
builder.setNegativeButton(android.R.string.cancel, null); | |
builder.create().show(); | |
} | |
private void blockUser() { | |
new AlertDialog.Builder(getActivity()) | |
.setTitle(R.string.menu_block_user) | |
.setMessage(Html.fromHtml(getString(R.string.msg_block_user_warning))) | |
.setPositiveButton(R.string.menu_block_user, new DialogInterface.OnClickListener() { | |
public void onClick(DialogInterface dialog, int which) { | |
setPrivacy(PRIVACY_BLOCK); | |
} | |
}) | |
.setNegativeButton(android.R.string.cancel, null) | |
.show(); | |
} | |
private void unblockUser() { | |
new AlertDialog.Builder(getActivity()) | |
.setTitle(R.string.menu_unblock_user) | |
.setMessage(Html.fromHtml(getString(R.string.msg_unblock_user_warning))) | |
.setPositiveButton(R.string.menu_unblock_user, new DialogInterface.OnClickListener() { | |
public void onClick(DialogInterface dialog, int which) { | |
setPrivacy(PRIVACY_UNBLOCK); | |
} | |
}) | |
.setNegativeButton(android.R.string.cancel, null) | |
.show(); | |
} | |
private void decryptMessage(CompositeMessage msg) { | |
try { | |
Context ctx = getActivity(); | |
MessageUtils.decryptMessage(ctx, null, msg); | |
// write updated data to the database | |
ContentValues values = new ContentValues(); | |
MessageUtils.fillContentValues(values, msg); | |
ctx.getContentResolver().update(Messages.getUri(msg.getId()), | |
values, null, null); | |
} | |
catch (Exception e) { | |
Log.e(TAG, "decryption failed", e); | |
// TODO i18n | |
Toast.makeText(getActivity(), "Decryption failed!", | |
Toast.LENGTH_LONG).show(); | |
} | |
} | |
private void retryMessage(CompositeMessage msg) { | |
Intent i = new Intent(getActivity(), MessageCenterService.class); | |
i.setAction(MessageCenterService.ACTION_RETRY); | |
i.putExtra(MessageCenterService.EXTRA_MESSAGE, ContentUris.withAppendedId | |
(Messages.CONTENT_URI, msg.getDatabaseId())); | |
getActivity().startService(i); | |
} | |
private static final int MENU_RETRY = 1; | |
private static final int MENU_SHARE = 2; | |
private static final int MENU_COPY_TEXT = 3; | |
private static final int MENU_DECRYPT = 4; | |
private static final int MENU_OPEN = 5; | |
private static final int MENU_DOWNLOAD = 6; | |
private static final int MENU_CANCEL_DOWNLOAD = 7; | |
private static final int MENU_DETAILS = 8; | |
private static final int MENU_DELETE = 9; | |
@Override | |
public void onCreateContextMenu(ContextMenu menu, View v, | |
ContextMenuInfo menuInfo) { | |
AdapterContextMenuInfo info = (AdapterContextMenuInfo) menuInfo; | |
MessageListItem vitem = (MessageListItem) info.targetView; | |
CompositeMessage msg = vitem.getMessage(); | |
menu.setHeaderTitle(R.string.title_message_options); | |
// message waiting for user review | |
if (msg.getStatus() == Messages.STATUS_PENDING) { | |
menu.add(CONTEXT_MENU_GROUP_ID, MENU_RETRY, MENU_RETRY, R.string.resend); | |
} | |
// some commands can be used only on unencrypted messages | |
if (!msg.isEncrypted()) { | |
AttachmentComponent attachment = (AttachmentComponent) msg | |
.getComponent(AttachmentComponent.class); | |
TextComponent text = (TextComponent) msg | |
.getComponent(TextComponent.class); | |
// sharing media messages has no purpose if media file hasn't been | |
// retrieved yet | |
if (text != null || (attachment != null ? attachment.getLocalUri() != null : true)) { | |
menu.add(CONTEXT_MENU_GROUP_ID, MENU_SHARE, MENU_SHARE, R.string.share); | |
} | |
// non-empty text: copy text to clipboard | |
if (text != null && !TextUtils.isEmpty(text.getContent())) { | |
menu.add(CONTEXT_MENU_GROUP_ID, MENU_COPY_TEXT, MENU_COPY_TEXT, | |
R.string.copy_message_text); | |
} | |
if (attachment != null) { | |
// message has a local uri - add open file entry | |
if (attachment.getLocalUri() != null) { | |
int resId; | |
if (attachment instanceof ImageComponent) | |
resId = R.string.view_image; | |
else if (attachment instanceof AudioComponent) | |
resId = R.string.open_audio; | |
else | |
resId = R.string.open_file; | |
menu.add(CONTEXT_MENU_GROUP_ID, MENU_OPEN, MENU_OPEN, resId); | |
} | |
// message has a fetch url - add download control entry | |
if (msg.getDirection() == Messages.DIRECTION_IN && attachment.getFetchUrl() != null) { | |
int id, string; | |
if (!DownloadService.isQueued(attachment.getFetchUrl())) { | |
// already fetched | |
if (attachment.getLocalUri() != null) | |
string = R.string.download_again; | |
else | |
string = R.string.download_file; | |
id = MENU_DOWNLOAD; | |
} | |
else { | |
string = R.string.download_cancel; | |
id = MENU_CANCEL_DOWNLOAD; | |
} | |
menu.add(CONTEXT_MENU_GROUP_ID, id, id, string); | |
} | |
} | |
} | |
else { | |
menu.add(CONTEXT_MENU_GROUP_ID, MENU_DECRYPT, MENU_DECRYPT, | |
R.string.decrypt_message); | |
} | |
menu.add(CONTEXT_MENU_GROUP_ID, MENU_DETAILS, MENU_DETAILS, R.string.menu_message_details); | |
menu.add(CONTEXT_MENU_GROUP_ID, MENU_DELETE, MENU_DELETE, R.string.delete_message); | |
} | |
@Override | |
public boolean onContextItemSelected(android.view.MenuItem item) { | |
// not our context | |
if (item.getGroupId() != CONTEXT_MENU_GROUP_ID) | |
return false; | |
AdapterContextMenuInfo info = (AdapterContextMenuInfo) item | |
.getMenuInfo(); | |
MessageListItem v = (MessageListItem) info.targetView; | |
CompositeMessage msg = v.getMessage(); | |
switch (item.getItemId()) { | |
case MENU_SHARE: { | |
Intent i = null; | |
AttachmentComponent attachment = (AttachmentComponent) msg | |
.getComponent(AttachmentComponent.class); | |
if (attachment != null) { | |
i = ComposeMessage.sendMediaMessage(attachment.getLocalUri(), | |
attachment.getMime()); | |
} | |
else { | |
TextComponent txt = (TextComponent) msg | |
.getComponent(TextComponent.class); | |
if (txt != null) | |
i = ComposeMessage.sendTextMessage(txt.getContent()); | |
} | |
if (i != null) | |
startActivity(i); | |
else | |
// TODO ehm... | |
Log.w(TAG, "error sharing message"); | |
return true; | |
} | |
case MENU_COPY_TEXT: { | |
TextComponent txt = (TextComponent) msg | |
.getComponent(TextComponent.class); | |
String text = (txt != null) ? txt.getContent() : ""; | |
ClipboardManager cpm = (ClipboardManager) getActivity() | |
.getSystemService(Context.CLIPBOARD_SERVICE); | |
cpm.setText(text); | |
Toast.makeText(getActivity(), R.string.message_text_copied, | |
Toast.LENGTH_SHORT).show(); | |
return true; | |
} | |
case MENU_DECRYPT: { | |
decryptMessage(msg); | |
return true; | |
} | |
case MENU_RETRY: { | |
retryMessage(msg); | |
return true; | |
} | |
case MENU_DOWNLOAD: { | |
startDownload(msg); | |
return true; | |
} | |
case MENU_CANCEL_DOWNLOAD: { | |
stopDownload(msg); | |
return true; | |
} | |
case MENU_DETAILS: { | |
CharSequence messageDetails = MessageUtils.getMessageDetails( | |
getActivity(), msg, userPhone != null ? userPhone : userId); | |
new AlertDialog.Builder(getActivity()) | |
.setTitle(R.string.title_message_details) | |
.setMessage(messageDetails) | |
.setPositiveButton(android.R.string.ok, null) | |
.setCancelable(true).show(); | |
return true; | |
} | |
case MENU_DELETE: { | |
deleteMessage(msg.getDatabaseId()); | |
return true; | |
} | |
case MENU_OPEN: { | |
AttachmentComponent attachment = (AttachmentComponent) msg | |
.getComponent(AttachmentComponent.class); | |
if (!(attachment instanceof AudioComponent)) | |
openFile(msg); | |
else | |
openAudio(msg); | |
return true; | |
} | |
} | |
return super.onContextItemSelected(item); | |
} | |
private void startQuery(boolean reloadConversation, boolean progress) { | |
try { | |
if (progress) | |
getActivity().setProgressBarIndeterminateVisibility(true); | |
CompositeMessage.startQuery(mQueryHandler, MESSAGE_LIST_QUERY_TOKEN, | |
threadId); | |
if (reloadConversation) | |
Conversation.startQuery(mQueryHandler, | |
CONVERSATION_QUERY_TOKEN, threadId); | |
} catch (SQLiteException e) { | |
Log.e(TAG, "query error", e); | |
} | |
} | |
private void loadConversationMetadata(Uri uri) { | |
threadId = ContentUris.parseId(uri); | |
mConversation = Conversation.loadFromId(getActivity(), threadId); | |
if (mConversation == null) { | |
Log.w(TAG, "conversation for thread " + threadId + " not found!"); | |
startActivity(new Intent(getActivity(), ConversationList.class)); | |
getActivity().finish(); | |
return; | |
} | |
userId = mConversation.getRecipient(); | |
Contact contact = mConversation.getContact(); | |
if (contact != null) { | |
userName = contact.getName(); | |
userPhone = contact.getNumber(); | |
} | |
else { | |
userName = userId; | |
} | |
} | |
private Bundle myArguments() { | |
return (mArguments != null) ? mArguments : getArguments(); | |
} | |
public void setMyArguments(Bundle args) { | |
mArguments = args; | |
} | |
@Override | |
public void onActivityResult(int requestCode, int resultCode, Intent data) { | |
if (requestCode == SELECT_ATTACHMENT_OPENABLE) { | |
if (resultCode == Activity.RESULT_OK) { | |
Uri uri = null; | |
String mime = null; | |
// returning from camera | |
if (data == null) { | |
/* | |
* FIXME picture taking should be done differently. | |
* Use a MediaStore-based uri and use a requestCode just | |
* for taking pictures. | |
*/ | |
if (mCurrentPhoto != null) { | |
uri = Uri.fromFile(mCurrentPhoto); | |
// notify media scanner | |
Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); | |
mediaScanIntent.setData(uri); | |
getActivity().sendBroadcast(mediaScanIntent); | |
mCurrentPhoto = null; | |
} | |
} | |
else { | |
if (mCurrentPhoto != null) { | |
mCurrentPhoto.delete(); | |
mCurrentPhoto = null; | |
} | |
uri = data.getData(); | |
mime = data.getType(); | |
// SAF available, request persistable permissions | |
if (MediaStorage.isStorageAccessFrameworkAvailable()) { | |
MediaStorage.requestPersistablePermissions(getActivity(), data); | |
} | |
} | |
if (uri != null) { | |
if (mime == null || mime.startsWith("*/") | |
|| mime.endsWith("/*")) { | |
mime = MediaStorage.getType(getActivity(), uri); | |
Log.v(TAG, "using detected mime type " + mime); | |
} | |
if (ImageComponent.supportsMimeType(mime)) | |
sendBinaryMessage(uri, mime, true, ImageComponent.class); | |
else if (VCardComponent.supportsMimeType(mime)) | |
sendBinaryMessage(uri, VCardComponent.MIME_TYPE, false, VCardComponent.class); | |
else | |
Toast.makeText(getActivity(), R.string.send_mime_not_supported, Toast.LENGTH_LONG) | |
.show(); | |
} | |
} | |
// operation aborted | |
else { | |
// delete photo :) | |
if (mCurrentPhoto != null) { | |
mCurrentPhoto.delete(); | |
mCurrentPhoto = null; | |
} | |
} | |
} | |
else if (requestCode == SELECT_ATTACHMENT_CONTACT) { | |
if (resultCode == Activity.RESULT_OK) { | |
Uri uri = data.getData(); | |
if (uri != null) { | |
// get lookup key | |
final Cursor c = getActivity().getContentResolver() | |
.query(uri, new String[] { Contacts.LOOKUP_KEY }, null, null, null); | |
if (c != null) { | |
try { | |
c.moveToFirst(); | |
String lookupKey = c.getString(0); | |
Uri vcardUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey); | |
sendBinaryMessage(vcardUri, VCardComponent.MIME_TYPE, false, VCardComponent.class); | |
} | |
finally { | |
c.close(); | |
} | |
} | |
} | |
} | |
} | |
} | |
private void onKeyboardStateChanged(boolean isKeyboardOpen) { | |
if (isKeyboardOpen) { | |
mTextEntry.setFocusableInTouchMode(true); | |
mTextEntry.setHint(R.string.hint_type_to_compose); | |
} | |
else { | |
mTextEntry.setFocusableInTouchMode(false); | |
mTextEntry.setHint(R.string.hint_open_kbd_to_compose); | |
} | |
} | |
@Override | |
public void onSaveInstanceState(Bundle out) { | |
super.onSaveInstanceState(out); | |
out.putParcelable(Uri.class.getName(), Threads.getUri(userId)); | |
} | |
private void processArguments(Bundle savedInstanceState) { | |
Bundle args = null; | |
if (savedInstanceState != null) { | |
Uri uri = savedInstanceState.getParcelable(Uri.class.getName()); | |
// threadId = ContentUris.parseId(uri); | |
args = new Bundle(); | |
args.putString("action", ComposeMessage.ACTION_VIEW_USERID); | |
args.putParcelable("data", uri); | |
} | |
else { | |
args = myArguments(); | |
} | |
if (args != null && args.size() > 0) { | |
final String action = args.getString("action"); | |
// view intent | |
if (Intent.ACTION_VIEW.equals(action)) { | |
Uri uri = args.getParcelable("data"); | |
ContentResolver cres = getActivity().getContentResolver(); | |
/* | |
* FIXME this will retrieve name directly from contacts, | |
* resulting in a possible discrepancy with users database | |
*/ | |
Cursor c = cres.query(uri, new String[] { | |
Syncer.DATA_COLUMN_DISPLAY_NAME, | |
Syncer.DATA_COLUMN_PHONE }, null, null, null); | |
if (c.moveToFirst()) { | |
userName = c.getString(0); | |
userPhone = c.getString(1); | |
// FIXME should it be retrieved from RawContacts.SYNC3 ?? | |
userId = MessageUtils.sha1(userPhone); | |
Cursor cp = cres.query(Messages.CONTENT_URI, | |
new String[] { Messages.THREAD_ID }, Messages.PEER | |
+ " = ?", new String[] { userId }, null); | |
if (cp.moveToFirst()) | |
threadId = cp.getLong(0); | |
cp.close(); | |
} | |
c.close(); | |
if (threadId > 0) { | |
mConversation = Conversation.loadFromId(getActivity(), | |
threadId); | |
} | |
else { | |
mConversation = Conversation.createNew(getActivity()); | |
mConversation.setRecipient(userId); | |
} | |
} | |
// view conversation - just threadId provided | |
else if (ComposeMessage.ACTION_VIEW_CONVERSATION.equals(action)) { | |
Uri uri = args.getParcelable("data"); | |
loadConversationMetadata(uri); | |
} | |
// view conversation - just userId provided | |
else if (ComposeMessage.ACTION_VIEW_USERID.equals(action)) { | |
Uri uri = args.getParcelable("data"); | |
userId = uri.getPathSegments().get(1); | |
mConversation = Conversation.loadFromUserId(getActivity(), | |
userId); | |
if (mConversation == null) { | |
mConversation = Conversation.createNew(getActivity()); | |
mConversation.setNumberHint(args.getString("number")); | |
mConversation.setRecipient(userId); | |
} | |
// this way avoid doing the users database query twice | |
else { | |
if (mConversation.getContact() == null) { | |
mConversation.setNumberHint(args.getString("number")); | |
mConversation.setRecipient(userId); | |
} | |
} | |
threadId = mConversation.getThreadId(); | |
Contact contact = mConversation.getContact(); | |
if (contact != null) { | |
userName = contact.getName(); | |
userPhone = contact.getNumber(); | |
} | |
else { | |
userName = userId; | |
} | |
} | |
} | |
// set title if we are autonomous | |
if (mArguments != null) { | |
String title = userName; | |
//if (userPhone != null) title += " <" + userPhone + ">"; | |
setActivityTitle(title, "", null); | |
} | |
// update conversation stuff | |
if (mConversation != null) | |
onConversationCreated(); | |
// non existant thread - check for not synced contact | |
if (threadId <= 0 && mConversation != null) { | |
Contact contact = mConversation.getContact(); | |
if (userPhone != null && contact != null ? !contact.isRegistered() : true) { | |
// ask user to send invitation | |
DialogInterface.OnClickListener noListener = new DialogInterface.OnClickListener() { | |
@Override | |
public void onClick(DialogInterface dialog, int which) { | |
// FIXME is this specific to sms app? | |
Intent i = new Intent(Intent.ACTION_SENDTO, | |
Uri.parse("smsto:" + userPhone)); | |
i.putExtra("sms_body", | |
getString(R.string.text_invite_message)); | |
startActivity(i); | |
getActivity().finish(); | |
} | |
}; | |
AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); | |
builder. | |
setTitle(R.string.title_user_not_found) | |
.setMessage(R.string.message_user_not_found) | |
// nothing happens if user chooses to contact the user anyway | |
.setPositiveButton(R.string.yes_user_not_found, null) | |
.setNegativeButton(R.string.no_user_not_found, noListener) | |
.show(); | |
} | |
} | |
} | |
public void setActivityTitle(CharSequence title, CharSequence status, Contact contact) { | |
Activity parent = getActivity(); | |
if (parent instanceof ComposeMessage) | |
((ComposeMessage) parent).setTitle(title, status, contact); | |
else if (title != null) | |
parent.setTitle(title); | |
} | |
public void setActivityStatusUpdating() { | |
Activity parent = getActivity(); | |
if (parent instanceof ComposeMessage) | |
((ComposeMessage) parent).setUpdatingSubtitle(); | |
} | |
public ComposeMessage getParentActivity() { | |
Activity _activity = getActivity(); | |
return (_activity instanceof ComposeMessage) ? (ComposeMessage) _activity | |
: null; | |
} | |
private void processStart(boolean resuming) { | |
ComposeMessage activity = getParentActivity(); | |
// opening for contact picker - do nothing | |
if (threadId < 0 && activity != null | |
&& activity.getSendIntent() != null) | |
return; | |
if (mListAdapter == null) { | |
Pattern highlight = null; | |
Bundle args = myArguments(); | |
if (args != null) { | |
String highlightString = args | |
.getString(ComposeMessage.EXTRA_HIGHLIGHT); | |
highlight = (highlightString == null) ? null : Pattern.compile( | |
"\\b" + Pattern.quote(highlightString), | |
Pattern.CASE_INSENSITIVE); | |
} | |
mListAdapter = new MessageListAdapter(getActivity(), null, | |
highlight, getListView(), this); | |
mListAdapter.setOnContentChangedListener(mContentChangedListener); | |
setListAdapter(mListAdapter); | |
} | |
if (threadId > 0) { | |
// always reload conversation | |
startQuery(true, resuming); | |
} | |
else { | |
// HACK this is for crappy honeycomb :) | |
getActivity().setProgressBarIndeterminateVisibility(false); | |
mConversation = Conversation.createNew(getActivity()); | |
mConversation.setRecipient(userId); | |
onConversationCreated(); | |
} | |
} | |
/** Called when the {@link Conversation} object has been created. */ | |
private void onConversationCreated() { | |
// subscribe to presence notifications | |
subscribePresence(); | |
mTextEntry.removeTextChangedListener(mChatStateListener); | |
// restore draft (if any and only if user hasn't inserted text) | |
if (mTextEntry.getText().length() == 0) { | |
String draft = mConversation.getDraft(); | |
if (draft != null) { | |
mTextEntry.setText(draft); | |
// move cursor to end | |
mTextEntry.setSelection(mTextEntry.getText().length()); | |
} | |
} | |
mTextEntry.addTextChangedListener(mChatStateListener); | |
if (mConversation.getThreadId() > 0 && mConversation.getUnreadCount() > 0) { | |
/* | |
* FIXME this has the usual issue about resuming while screen is | |
* still locked, having focus and so on... | |
* See issue #28. | |
*/ | |
mConversation.markAsRead(); | |
} | |
else { | |
// new conversation -- observe peer Uri | |
registerPeerObserver(); | |
} | |
// update contact icon | |
setActivityTitle(null, null, mConversation.getContact()); | |
// setup invitation bar | |
boolean visible = (mConversation.getRequestStatus() == Threads.REQUEST_WAITING); | |
if (visible) { | |
if (mInvitationBar == null) { | |
mInvitationBar = (ViewGroup) getView().findViewById(R.id.invitation_bar); | |
// setup listeners and show button bar | |
View.OnClickListener listener = new View.OnClickListener() { | |
public void onClick(View v) { | |
mInvitationBar.setVisibility(View.GONE); | |
int action; | |
if (v.getId() == R.id.button_accept) | |
action = PRIVACY_ACCEPT; | |
else | |
action = PRIVACY_BLOCK; | |
setPrivacy(action); | |
} | |
}; | |
mInvitationBar.findViewById(R.id.button_accept) | |
.setOnClickListener(listener); | |
mInvitationBar.findViewById(R.id.button_block) | |
.setOnClickListener(listener); | |
// identity button has its own listener | |
mInvitationBar.findViewById(R.id.button_identity) | |
.setOnClickListener(new View.OnClickListener() { | |
public void onClick(View v) { | |
showIdentityDialog(); | |
} | |
} | |
); | |
} | |
} | |
if (mInvitationBar != null) | |
mInvitationBar.setVisibility(visible ? View.VISIBLE : View.GONE); | |
updateUI(); | |
} | |
private void setPrivacy(int action) { | |
int status; | |
switch (action) { | |
case PRIVACY_ACCEPT: | |
status = Threads.REQUEST_REPLY_PENDING_ACCEPT; | |
break; | |
case PRIVACY_BLOCK: | |
status = Threads.REQUEST_REPLY_PENDING_BLOCK; | |
break; | |
case PRIVACY_UNBLOCK: | |
status = Threads.REQUEST_REPLY_PENDING_UNBLOCK; | |
break; | |
default: | |
return; | |
} | |
Context ctx = getActivity(); | |
// mark request as pending accepted | |
ContentValues values = new ContentValues(1); | |
values.put(Threads.REQUEST_STATUS, status); | |
// FIXME this won't work on new threads | |
ctx.getContentResolver().update(Requests.CONTENT_URI, | |
values, CommonColumns.PEER + "=?", | |
new String[] { userId }); | |
// setup broadcast receiver for block/unblock reply | |
if (action == PRIVACY_BLOCK || action == PRIVACY_UNBLOCK) { | |
if (mPrivacyListener == null) { | |
mPrivacyListener = new BroadcastReceiver() { | |
public void onReceive(Context context, Intent intent) { | |
String from = intent.getStringExtra(MessageCenterService.EXTRA_FROM_USERID); | |
if (userId.equals(from)) { | |
// this will trigger a Contact reload | |
mConversation.setRecipient(userId); | |
// this will update block/unblock menu items | |
updateUI(); | |
// request presence subscription if unblocking | |
if (MessageCenterService.ACTION_UNBLOCKED.equals(intent.getAction())) { | |
Toast.makeText(getActivity(), | |
R.string.msg_user_unblocked, | |
Toast.LENGTH_LONG).show(); | |
presenceSubscribe(); | |
} | |
else { | |
Toast.makeText(getActivity(), | |
R.string.msg_user_blocked, | |
Toast.LENGTH_LONG).show(); | |
} | |
// we don't need this receiver anymore | |
mLocalBroadcastManager.unregisterReceiver(this); | |
} | |
} | |
}; | |
} | |
IntentFilter filter = new IntentFilter(MessageCenterService.ACTION_BLOCKED); | |
filter.addAction(MessageCenterService.ACTION_UNBLOCKED); | |
mLocalBroadcastManager.registerReceiver(mPrivacyListener, filter); | |
} | |
// send command to message center | |
MessageCenterService.replySubscription(ctx, userId, action); | |
} | |
private void showIdentityDialog() { | |
String fingerprint; | |
String uid; | |
PGPPublicKeyRing publicKey = UsersProvider.getPublicKey(getActivity(), userId); | |
if (publicKey != null) { | |
PGPPublicKey pk = PGP.getMasterKey(publicKey); | |
fingerprint = PGP.getFingerprint(pk); | |
uid = PGP.getUserId(pk, null); // TODO server!!! | |
} | |
else { | |
// FIXME using another string | |
fingerprint = uid = getString(R.string.peer_unknown); | |
} | |
String text; | |
Contact c = mConversation.getContact(); | |
if (c != null) | |
text = getString(R.string.text_invitation_known, | |
c.getName(), | |
c.getNumber(), | |
uid, fingerprint); | |
else | |
text = getString(R.string.text_invitation_unknown, | |
uid, fingerprint); | |
/* | |
* TODO include an "Open" button on the dialog to ignore the request | |
* and go on with the compose window. | |
*/ | |
new AlertDialog.Builder(getActivity()) | |
.setPositiveButton(android.R.string.ok, null) | |
.setTitle(R.string.title_invitation) | |
.setMessage(text) | |
.show(); | |
} | |
/* | |
private final class UserPresenceBroadcastReceiver extends BroadcastReceiver { | |
@Override | |
public void onReceive(Context context, Intent intent) { | |
String action = intent.getAction(); | |
if (MessageCenterServiceLegacy.ACTION_USER_PRESENCE.equals(action)) { | |
int event = intent.getIntExtra("org.kontalk.presence.event", 0); | |
CharSequence text = null; | |
if (event == UserEvent.EVENT_OFFLINE_VALUE) { | |
text = buildLastSeenText(getResources().getString(R.string.seen_moment_ago_label)); | |
} | |
else if (event == UserEvent.EVENT_ONLINE_VALUE) { | |
text = getResources().getString(R.string.seen_online_label); | |
} | |
else if (event == UserEvent.EVENT_STATUS_CHANGED_VALUE) { | |
// update users table | |
ContentValues values = new ContentValues(1); | |
values.put(Users.STATUS, intent.getStringExtra("org.kontalk.presence.status")); | |
context.getContentResolver().update( | |
Users.CONTENT_URI, values, | |
Users.HASH + "=?", new String[] { userId }); | |
// time to invalidate cache | |
// TODO this should be done by cursor notification | |
Contact.invalidate(userId); | |
} | |
if (text != null) { | |
try { | |
setStatusText(text); | |
} | |
catch (Exception e) { | |
// something could happen in the mean time - e.g. fragment destruction | |
} | |
} | |
} | |
else if (MessageCenterServiceLegacy.ACTION_CONNECTED.equals(action)) { | |
// request user lookup | |
PresenceServiceConnection conn = new PresenceServiceConnection(userId, true); | |
getActivity().bindService( | |
new Intent(getActivity().getApplicationContext(), | |
MessageCenterServiceLegacy.class), conn, | |
Context.BIND_AUTO_CREATE); | |
} | |
} | |
} | |
*/ | |
private void subscribePresence() { | |
if (mPresenceReceiver == null) { | |
mPresenceReceiver = new BroadcastReceiver() { | |
public void onReceive(Context context, Intent intent) { | |
String action = intent.getAction(); | |
if (MessageCenterService.ACTION_PRESENCE.equals(action)) { | |
// we handle only (un)available presence stanzas | |
String type = intent.getStringExtra(MessageCenterService.EXTRA_TYPE); | |
if (Presence.Type.available.name().equals(type) || Presence.Type.unavailable.name().equals(type)) { | |
CharSequence statusText = null; | |
String groupId = intent.getStringExtra(MessageCenterService.EXTRA_GROUP_ID); | |
String from = intent.getStringExtra(MessageCenterService.EXTRA_FROM_USERID); | |
// we are receiving a presence from our peer, upgrade available resources | |
if (from != null && from.substring(0, CompositeMessage.USERID_LENGTH).equals(userId)) { | |
// our presence!!! | |
if (Presence.Type.available.toString().equals(type)) { | |
mAvailableResources.add(from); | |
mCurrentStatus = getString(R.string.seen_online_label); | |
if (!mIsTyping) | |
setStatusText(mCurrentStatus); | |
// abort presence probe if non-group stanza | |
if (groupId == null) mPresenceId = null; | |
} | |
else if (Presence.Type.unavailable.toString().equals(type)) { | |
mAvailableResources.remove(from); | |
/* | |
* All available resources have gone. If we are | |
* not waiting for presence probe response, mark | |
* the user as offline immediately and use the | |
* timestamp provided with the stanza. | |
*/ | |
/* | |
* FIXME this part has a serious bug. | |
* Client might receive a certain set of | |
* presence stanzas (e.g. while syncer is | |
* running) which will empty mAvailableResources, | |
* thus taking the latest stanza (this one) as | |
* reference for last presence indication. | |
* In fact, the most important presence is | |
* always the most available or the most recent | |
* one. | |
* Anyway, this method is not reliable either | |
* because of presence information not being | |
* accounted for from the beginning. Therefore, | |
* we don't know when a presence informs us | |
* about a user being unavailable in that moment | |
* or because a probe has been requested. | |
*/ | |
if (mAvailableResources.size() == 0 && mPresenceId == null) { | |
// an offline user can't be typing | |
mIsTyping = false; | |
// user offline | |
long stamp = intent.getLongExtra(MessageCenterService.EXTRA_STAMP, -1); | |
if (stamp >= 0) { | |
statusText = MessageUtils.formatRelativeTimeSpan(context, stamp); | |
} | |
else { | |
statusText = getString(R.string.seen_moment_ago_label); | |
} | |
} | |
} | |
} | |
// we have a presence group | |
if (mPresenceId != null && mPresenceId.equals(groupId)) { | |
if (mMostAvailable == null) | |
mMostAvailable = new PresenceData(); | |
boolean take = false; | |
boolean available = (type == null || Presence.Type.available.toString().equals(type)); | |
long stamp = intent.getLongExtra(MessageCenterService.EXTRA_STAMP, -1); | |
int priority = intent.getIntExtra(MessageCenterService.EXTRA_PRIORITY, 0); | |
if (available) { | |
// take if higher priority | |
if (priority >= mMostAvailable.priority) | |
take = true; | |
} | |
else { | |
// take if most recent | |
long old = mMostAvailable.stamp != null ? mMostAvailable.stamp.getTime() : -1; | |
if (stamp >= old) | |
take = true; | |
} | |
if (take) { | |
// available stanza - null stamp | |
if (available) { | |
mMostAvailable.stamp = null; | |
} | |
// unavailable stanza - update stamp | |
else { | |
if (mMostAvailable.stamp == null) | |
mMostAvailable.stamp = new Date(stamp); | |
else | |
mMostAvailable.stamp.setTime(stamp); | |
} | |
mMostAvailable.status = intent.getStringExtra(MessageCenterService.EXTRA_STATUS); | |
mMostAvailable.priority = priority; | |
} | |
int count = intent.getIntExtra(MessageCenterService.EXTRA_GROUP_COUNT, 0); | |
if (count <= 1 || mPresenceId == null) { | |
// we got all presence stanzas | |
Log.v(TAG, "got all presence stanzas or available stanza found (stamp=" + mMostAvailable.stamp + | |
", status=" + mMostAvailable.status + ")"); | |
// stop receiving presence probes | |
mPresenceId = null; | |
/* | |
* TODO if we receive a presence unavailable stanza | |
* we shall consider it only if there is no other | |
* available resource. So we shall keep a reference | |
* to all available resources and sync them | |
* whenever a presence stanza is received. | |
*/ | |
if (mAvailableResources.size() == 0) { | |
if (mMostAvailable.stamp != null) { | |
statusText = MessageUtils.formatRelativeTimeSpan(context, | |
mMostAvailable.stamp.getTime()); | |
} | |
} | |
} | |
} | |
if (statusText != null) { | |
mCurrentStatus = statusText; | |
if (!mIsTyping) | |
setStatusText(statusText); | |
} | |
} | |
// subscription accepted, send presence probe | |
else if (Presence.Type.subscribed.name().equals(type)) { | |
mPresenceId = intent.getStringExtra(MessageCenterService.EXTRA_PACKET_ID); | |
} | |
} | |
else if (MessageCenterService.ACTION_CONNECTED.equals(action)) { | |
// reset compose sent flag | |
mComposeSent = false; | |
// send subscription request | |
presenceSubscribe(); | |
} | |
else if (MessageCenterService.ACTION_MESSAGE.equals(action)) { | |
String from = intent.getStringExtra(MessageCenterService.EXTRA_FROM_USERID); | |
String chatState = intent.getStringExtra("org.kontalk.message.chatState"); | |
// we are receiving a composing notification from our peer | |
if (from != null && from.substring(0, CompositeMessage.USERID_LENGTH).equals(userId)) { | |
if (chatState != null && ChatState.composing.toString().equals(chatState)) { | |
mIsTyping = true; | |
setStatusText(getString(R.string.seen_typing_label)); | |
} | |
else { | |
mIsTyping = false; | |
setStatusText(mCurrentStatus != null ? mCurrentStatus : ""); | |
} | |
} | |
} | |
} | |
}; | |
// listen for user presence, connection and incoming messages | |
IntentFilter filter = new IntentFilter(); | |
filter.addAction(MessageCenterService.ACTION_PRESENCE); | |
filter.addAction(MessageCenterService.ACTION_CONNECTED); | |
filter.addAction(MessageCenterService.ACTION_MESSAGE); | |
mLocalBroadcastManager.registerReceiver(mPresenceReceiver, filter); | |
// request connection status | |
MessageCenterService.requestConnectionStatus(getActivity()); | |
} | |
} | |
/** Sends a subscription request for the current peer. */ | |
private void presenceSubscribe() { | |
// all of this shall be done only if there isn't a request from the other contact | |
if (mConversation.getRequestStatus() != Threads.REQUEST_WAITING) { | |
Contact c = mConversation.getContact(); | |
// pre-approve our presence if we don't have contact's key | |
if (c == null || c.getPublicKeyRing() == null) { | |
Intent i = new Intent(getActivity(), MessageCenterService.class); | |
i.setAction(MessageCenterService.ACTION_PRESENCE); | |
i.putExtra(MessageCenterService.EXTRA_TO_USERID, userId); | |
i.putExtra(MessageCenterService.EXTRA_TYPE, Presence.Type.subscribed.name()); | |
getActivity().startService(i); | |
} | |
// send subscription request | |
Intent i = new Intent(getActivity(), MessageCenterService.class); | |
i.setAction(MessageCenterService.ACTION_PRESENCE); | |
i.putExtra(MessageCenterService.EXTRA_TO_USERID, userId); | |
i.putExtra(MessageCenterService.EXTRA_TYPE, Presence.Type.subscribe.name()); | |
getActivity().startService(i); | |
} | |
} | |
private void unsubcribePresence() { | |
if (mPresenceReceiver != null) { | |
mLocalBroadcastManager.unregisterReceiver(mPresenceReceiver); | |
mPresenceReceiver = null; | |
} | |
// send unsubscription request | |
Intent i = new Intent(getActivity(), MessageCenterService.class); | |
i.setAction(MessageCenterService.ACTION_PRESENCE); | |
i.putExtra(MessageCenterService.EXTRA_TO_USERID, userId); | |
i.putExtra(MessageCenterService.EXTRA_TYPE, "unsubscribe"); | |
getActivity().startService(i); | |
} | |
/* | |
@Override | |
public boolean tx(ClientConnection connection, String txId, MessageLite pack) { | |
if (pack instanceof UserLookupResponse) { | |
UserLookupResponse _pack = (UserLookupResponse) pack; | |
if (_pack.getEntryCount() > 0) { | |
UserLookupResponse.Entry res = _pack.getEntry(0); | |
CharSequence text = null; | |
try { | |
Activity context = getActivity(); | |
if (context != null) { | |
if (res.hasTimediff()) { | |
long diff = res.getTimediff(); | |
if (diff == 0) { | |
text = getResources().getString(R.string.seen_online_label); | |
} | |
else if (diff <= 10) { | |
text = buildLastSeenText(getResources().getString(R.string.seen_moment_ago_label)); | |
} | |
} | |
String afterText = null; | |
// update UsersProvider if necessary | |
ContentValues values = new ContentValues(2); | |
if (res.hasTimestamp()) | |
values.put(Users.LAST_SEEN, res.getTimestamp()); | |
if (res.hasStatus()) { | |
afterText = res.getStatus(); | |
if (!TextUtils.isEmpty(afterText)) { | |
Contact c = getContact(); | |
if (c != null) | |
afterText = Preferences | |
.decryptUserdata(getActivity(), afterText, c.getNumber()); | |
} | |
values.put(Users.STATUS, afterText); | |
} | |
else { | |
values.putNull(Users.STATUS); | |
} | |
context.getContentResolver().update( | |
Users.CONTENT_URI, values, | |
Users.HASH + "=?", new String[] { userId }); | |
// time to invalidate cache | |
// TODO this should be done by cursor notification | |
Contact.invalidate(userId); | |
if (text == null && res.hasTimestamp()) { | |
long time = res.getTimestamp(); | |
if (time > 0) { | |
text = buildLastSeenText(MessageUtils.formatRelativeTimeSpan(context, time * 1000)); | |
} | |
} | |
if (text != null) { | |
final CharSequence banner = text; | |
// show last seen banner | |
context.runOnUiThread(new Runnable() { | |
public void run() { | |
try { | |
setStatusText(banner); | |
} | |
catch (Exception e) { | |
// something could happen in the meanwhile e.g. fragment destruction | |
} | |
} | |
}); | |
} | |
} | |
} | |
catch (Exception e) { | |
// what here? | |
Log.e(TAG, "user lookup response error!", e); | |
} | |
} | |
} | |
return false; | |
} | |
*/ | |
private void setStatusText(CharSequence text) { | |
setActivityTitle(null, text, null); | |
} | |
private synchronized void registerPeerObserver() { | |
if (mPeerObserver == null) { | |
Uri uri = Threads.getUri(mConversation.getRecipient()); | |
mPeerObserver = new PeerObserver(getActivity(), mQueryHandler); | |
getActivity().getContentResolver().registerContentObserver(uri, | |
false, mPeerObserver); | |
} | |
} | |
private synchronized void unregisterPeerObserver() { | |
if (mPeerObserver != null) { | |
getActivity().getContentResolver().unregisterContentObserver( | |
mPeerObserver); | |
mPeerObserver = null; | |
} | |
} | |
private final class PeerObserver extends ContentObserver { | |
private final Context mContext; | |
public PeerObserver(Context context, Handler handler) { | |
super(handler); | |
mContext = context; | |
} | |
@Override | |
public void onChange(boolean selfChange) { | |
Conversation conv = Conversation.loadFromUserId(mContext, userId); | |
if (conv != null) { | |
mConversation = conv; | |
threadId = mConversation.getThreadId(); | |
// auto-unregister | |
unregisterPeerObserver(); | |
} | |
// fire cursor update | |
processStart(false); | |
} | |
@Override | |
public boolean deliverSelfNotifications() { | |
return false; | |
} | |
} | |
@Override | |
public void onResume() { | |
super.onResume(); | |
if (Authenticator.getDefaultAccount(getActivity()) == null) { | |
NumberValidation.startValidation(getActivity()); | |
getActivity().finish(); | |
return; | |
} | |
// hold message center | |
MessageCenterService.hold(getActivity()); | |
ComposeMessage activity = getParentActivity(); | |
if (activity == null || !activity.hasLostFocus() || activity.hasWindowFocus()) { | |
onFocus(); | |
} | |
} | |
public void onFocus() { | |
// resume content watcher | |
resumeContentListener(); | |
// we are updating the status now | |
setActivityStatusUpdating(); | |
// cursor was previously destroyed -- reload everything | |
// mConversation = null; | |
processStart(true); | |
if (userId != null) { | |
// TODO use some method to generate the JID | |
EndpointServer server = Preferences.getEndpointServer(getActivity()); | |
String jid = userId + '@' + server.getNetwork(); | |
// set notifications on pause | |
MessagingNotification.setPaused(jid); | |
// clear chat invitation (if any) | |
// TODO use jid here | |
MessagingNotification.clearChatInvitation(getActivity(), userId); | |
} | |
} | |
@Override | |
public void onPause() { | |
super.onPause(); | |
// pause content watcher | |
pauseContentListener(); | |
// notify parent of pausing | |
ComposeMessage parent = getParentActivity(); | |
if (parent != null) | |
parent.fragmentLostFocus(); | |
CharSequence text = mTextEntry.getText(); | |
int len = text.length(); | |
// resume notifications | |
MessagingNotification.setPaused(null); | |
// save last message as draft | |
if (threadId > 0) { | |
// no draft and no messages - delete conversation | |
if (len == 0 && mConversation.getMessageCount() == 0 && | |
mConversation.getRequestStatus() != Threads.REQUEST_WAITING) { | |
// FIXME shouldn't be faster to just delete the thread? | |
MessagesProvider.deleteThread(getActivity(), threadId); | |
} | |
// update draft | |
else { | |
ContentValues values = new ContentValues(1); | |
values.put(Threads.DRAFT, (len > 0) ? text.toString() : null); | |
getActivity().getContentResolver().update( | |
ContentUris.withAppendedId(Threads.CONTENT_URI, threadId), | |
values, null, null); | |
} | |
} | |
// new thread, create empty conversation | |
else { | |
if (len > 0) { | |
// save to local storage | |
ContentValues values = new ContentValues(); | |
// must supply a message ID... | |
values.put(Messages.MESSAGE_ID, | |
"draft" + (new Random().nextInt())); | |
values.put(Messages.PEER, userId); | |
values.put(Messages.BODY_CONTENT, new byte[0]); | |
values.put(Messages.BODY_LENGTH, 0); | |
values.put(Messages.BODY_MIME, TextComponent.MIME_TYPE); | |
values.put(Messages.DIRECTION, Messages.DIRECTION_OUT); | |
values.put(Messages.TIMESTAMP, System.currentTimeMillis()); | |
values.put(Messages.ENCRYPTED, false); | |
values.put(Threads.DRAFT, text.toString()); | |
getActivity().getContentResolver().insert(Messages.CONTENT_URI, | |
values); | |
} | |
} | |
if (len > 0) { | |
Toast.makeText(getActivity(), R.string.msg_draft_saved, | |
Toast.LENGTH_LONG).show(); | |
} | |
if (Preferences.getSendTyping(getActivity())) { | |
// send inactive state notification | |
if (mAvailableResources.size() > 0) | |
MessageCenterService.sendChatState(getActivity(), userId, ChatState.inactive); | |
mComposeSent = false; | |
} | |
// unsubcribe presence notifications | |
unsubcribePresence(); | |
// release message center | |
MessageCenterService.release(getActivity()); | |
} | |
@Override | |
public void onStop() { | |
super.onStop(); | |
unregisterPeerObserver(); | |
if (mListAdapter != null) | |
mListAdapter.changeCursor(null); | |
// be sure to cancel all queries | |
mQueryHandler.cancelOperation(MESSAGE_LIST_QUERY_TOKEN); | |
mQueryHandler.cancelOperation(CONVERSATION_QUERY_TOKEN); | |
} | |
@Override | |
public void onDestroy() { | |
super.onDestroy(); | |
if (mTextEntry != null) { | |
mTextEntry.removeTextChangedListener(mChatStateListener); | |
mTextEntry.setText(""); | |
} | |
} | |
private void pauseContentListener() { | |
if (mListAdapter != null) | |
mListAdapter.setOnContentChangedListener(null); | |
} | |
private void resumeContentListener() { | |
if (mListAdapter != null) | |
mListAdapter.setOnContentChangedListener(mContentChangedListener); | |
} | |
public final boolean isFinishing() { | |
return (getActivity() == null || (getActivity() != null && getActivity() | |
.isFinishing())) || isRemoving(); | |
} | |
private void updateUI() { | |
Contact contact = (mConversation != null) ? mConversation | |
.getContact() : null; | |
boolean contactEnabled = contact != null && contact.getId() > 0; | |
boolean threadEnabled = (threadId > 0); | |
if (mCallMenu != null) { | |
// FIXME what about VoIP? | |
if (!getActivity().getPackageManager().hasSystemFeature( | |
PackageManager.FEATURE_TELEPHONY)) { | |
mCallMenu.setVisible(false).setEnabled(false); | |
} | |
else { | |
mCallMenu.setVisible(true).setEnabled(true); | |
mCallMenu.setEnabled(contactEnabled); | |
} | |
mViewContactMenu.setEnabled(contactEnabled); | |
mDeleteThreadMenu.setEnabled(threadEnabled); | |
} | |
if (mBlockMenu != null) { | |
if (Authenticator.isSelfUserId(getActivity(), userId)) { | |
mBlockMenu.setVisible(false).setEnabled(false); | |
mUnblockMenu.setVisible(false).setEnabled(false); | |
} | |
else if (contact != null) { | |
// block/unblock | |
boolean blocked = contact.isBlocked(); | |
mBlockMenu.setVisible(!blocked).setEnabled(!blocked); | |
mUnblockMenu.setVisible(blocked).setEnabled(blocked); | |
} | |
else { | |
mBlockMenu.setVisible(true).setEnabled(true); | |
mUnblockMenu.setVisible(true).setEnabled(true); | |
} | |
} | |
} | |
/** The conversation list query handler. */ | |
// TODO convert to static class and use a weak reference to the context | |
private final class MessageListQueryHandler extends AsyncQueryHandler { | |
public MessageListQueryHandler() { | |
super(getActivity().getApplicationContext().getContentResolver()); | |
} | |
@Override | |
protected synchronized void onQueryComplete(int token, Object cookie, | |
Cursor cursor) { | |
if (cursor == null || isFinishing()) { | |
// close cursor - if any | |
if (cursor != null) | |
cursor.close(); | |
Log.e(TAG, "query aborted or error!"); | |
unregisterPeerObserver(); | |
mListAdapter.changeCursor(null); | |
return; | |
} | |
switch (token) { | |
case MESSAGE_LIST_QUERY_TOKEN: | |
// no messages to show - exit | |
if (cursor.getCount() == 0 | |
&& (mConversation == null || | |
// no draft | |
(mConversation.getDraft() == null && | |
// no subscription request | |
mConversation.getRequestStatus() != Threads.REQUEST_WAITING && | |
// no text in compose entry | |
mTextEntry.getText().length() == 0))) { | |
Log.i(TAG, "no data to view - exit"); | |
// close conversation | |
closeConversation(); | |
} | |
else { | |
// see if we have to scroll to a specific message | |
int newSelectionPos = -1; | |
Bundle args = myArguments(); | |
if (args != null) { | |
long msgId = args.getLong(ComposeMessage.EXTRA_MESSAGE, | |
-1); | |
if (msgId > 0) { | |
cursor.moveToPosition(-1); | |
while (cursor.moveToNext()) { | |
long curId = cursor.getLong(CompositeMessage.COLUMN_ID); | |
if (curId == msgId) { | |
newSelectionPos = cursor.getPosition(); | |
break; | |
} | |
} | |
} | |
} | |
mListAdapter.changeCursor(cursor); | |
if (newSelectionPos > 0) | |
getListView().setSelection(newSelectionPos); | |
getActivity().setProgressBarIndeterminateVisibility(false); | |
updateUI(); | |
} | |
break; | |
case CONVERSATION_QUERY_TOKEN: | |
if (cursor.moveToFirst()) { | |
mConversation = Conversation.createFromCursor( | |
getActivity(), cursor); | |
onConversationCreated(); | |
} | |
cursor.close(); | |
break; | |
default: | |
Log.e(TAG, "onQueryComplete called with unknown token " + token); | |
} | |
} | |
} | |
public Conversation getConversation() { | |
return mConversation; | |
} | |
public Contact getContact() { | |
return (mConversation != null) ? mConversation.getContact() : null; | |
} | |
public long getThreadId() { | |
return threadId; | |
} | |
public String getUserId() { | |
return userId; | |
} | |
public void setTextEntry(CharSequence text) { | |
mTextEntry.setText(text); | |
} | |
@Override | |
public boolean onLongClick(View v) { | |
// this seems to be necessary... | |
return false; | |
} | |
public void closeConversation() { | |
// main activity | |
if (getParentActivity() != null) { | |
getActivity().finish(); | |
} | |
// using fragments... | |
else { | |
ConversationList activity = (ConversationList) getActivity(); | |
activity.getListFragment().endConversation(this); | |
} | |
} | |
private void offlineModeWarning() { | |
if (Preferences.getOfflineMode(getActivity()) && !mOfflineModeWarned) { | |
mOfflineModeWarned = true; | |
Toast.makeText(getActivity(), R.string.warning_offline_mode, | |
Toast.LENGTH_LONG).show(); | |
} | |
} | |
@Override | |
public void onResult(String path) { | |
if (path != null) | |
sendBinaryMessage(Uri.fromFile(new File(path)), AudioDialog.DEFAULT_MIME, true, AudioComponent.class); | |
} | |
@Override | |
public void prepareAudio() { | |
Toast.makeText(getActivity(),"TEST",Toast.LENGTH_SHORT).show(); | |
} | |
@Override | |
public void playAudio(SeekBar seekBar) { | |
} | |
@Override | |
public void pauseAudio(SeekBar seekBar) { | |
mPlayer.pause(); | |
} | |
@Override | |
public void releaseAudio(SeekBar seekBar) { | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment