Java tutorial
/* * Kontalk Android client * Copyright (C) 2015 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 java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import java.util.HashSet; import java.util.List; import java.util.Random; import java.util.Set; import java.util.regex.Pattern; import com.afollestad.materialdialogs.AlertDialogWrapper; import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import com.akalipetis.fragment.ActionModeListFragment; import com.akalipetis.fragment.MultiChoiceModeListener; import com.nispok.snackbar.Snackbar; import com.nispok.snackbar.SnackbarManager; import com.nispok.snackbar.enums.SnackbarType; import com.nispok.snackbar.listeners.ActionClickListener; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.chatstates.ChatState; import org.jxmpp.util.XmppStringUtils; import org.spongycastle.openpgp.PGPPublicKey; import org.spongycastle.openpgp.PGPPublicKeyRing; import android.annotation.TargetApi; import android.app.Activity; import android.content.AsyncQueryHandler; import android.content.BroadcastReceiver; import android.content.ClipData; 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.MergeCursor; import android.database.sqlite.SQLiteDiskIOException; 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.annotation.NonNull; import android.support.v4.app.FragmentManager; import android.support.v4.content.LocalBroadcastManager; import android.support.v7.view.ActionMode; import android.text.ClipboardManager; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.util.Log; import android.util.SparseBooleanArray; 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.animation.AccelerateDecelerateInterpolator; import android.widget.ImageView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import io.codetail.animation.SupportAnimator; import io.codetail.animation.ViewAnimationUtils; import org.kontalk.R; import org.kontalk.authenticator.Authenticator; 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.adapter.MessageListAdapter; import org.kontalk.ui.view.AudioContentView; import org.kontalk.ui.view.AudioContentViewControl; import org.kontalk.ui.view.AudioPlayerControl; import org.kontalk.ui.view.ComposerBar; import org.kontalk.ui.view.ComposerListener; import org.kontalk.ui.view.MessageListItem; import org.kontalk.util.MediaStorage; import org.kontalk.util.MessageUtils; import org.kontalk.util.Preferences; import org.kontalk.util.SystemUtils; import org.kontalk.util.XMPPUtils; 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_REJECT; import static org.kontalk.service.msgcenter.MessageCenterService.PRIVACY_UNBLOCK; /** * The composer fragment. * @author Daniele Ricci * @author Andrea Cappelli */ public class ComposeMessageFragment extends ActionModeListFragment implements ComposerListener, View.OnLongClickListener, // TODO these two interfaces should be handled by an inner class AudioDialog.AudioDialogListener, AudioPlayerControl, MultiChoiceModeListener { 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 MESSAGE_PAGE_QUERY_TOKEN = 8723; /** How many messages to load per page. */ private static final int MESSAGE_PAGE_SIZE = 1000; private static final int SELECT_ATTACHMENT_OPENABLE = Activity.RESULT_FIRST_USER + 1; private static final int SELECT_ATTACHMENT_CONTACT = Activity.RESULT_FIRST_USER + 2; private static final int SELECT_ATTACHMENT_PHOTO = Activity.RESULT_FIRST_USER + 3; private enum WarningType { SUCCESS(0), // not implemented INFO(1), // not implemented WARNING(2), FATAL(3); private final int value; WarningType(int value) { this.value = value; } public int getValue() { return value; } } /* Attachment chooser stuff. */ private SupportAnimator mAttachAnimator; private View mAttachmentCard; private View mAttachmentContainer; private ComposerBar mComposer; private MessageListQueryHandler mQueryHandler; private MessageListAdapter mListAdapter; /** Header view for the list view: "previous messages" button. */ private View mHeaderView; private View mNextPageButton; private TextView mStatusText; 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 mUserJID; private String mUserName; private String mUserPhone; /** Available resources. */ private Set<String> mAvailableResources = new HashSet<String>(); private String mLastActivityRequestId; private String mVersionRequestId; /** Media player stuff. */ private int mMediaPlayerStatus = AudioContentView.STATUS_IDLE; private Handler mHandler; private Runnable mMediaPlayerUpdater; private AudioContentViewControl mAudioControl; /** Audio recording dialog. */ private AudioDialog mAudioDialog; private PeerObserver mPeerObserver; private File mCurrentPhoto; private LocalBroadcastManager mLocalBroadcastManager; private BroadcastReceiver mPresenceReceiver; private BroadcastReceiver mPrivacyListener; private boolean mOfflineModeWarned; private CharSequence mCurrentStatus; private boolean mIsTyping; private int mCheckedItemCount; /** 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); setMultiChoiceModeListener(this); // add header view (this must be done before setting the adapter) mHeaderView = LayoutInflater.from(getActivity()).inflate(R.layout.message_list_header, list, false); mNextPageButton = mHeaderView.findViewById(R.id.load_next_page); mNextPageButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { // disable button in the meantime enableHeaderView(false); // start query for the next page startMessagesQuery(mQueryHandler.getLastId()); } }); list.addHeaderView(mHeaderView); // set custom background (if any) ImageView background = (ImageView) getView().findViewById(R.id.background); Drawable bg = Preferences.getConversationBackground(getActivity()); if (bg != null) { background.setScaleType(ImageView.ScaleType.CENTER_CROP); background.setImageDrawable(bg); } else { background.setScaleType(ImageView.ScaleType.FIT_XY); background.setImageResource(R.drawable.app_background_tile); } processArguments(savedInstanceState); initAttachmentView(); } @Override public void onAttach(Activity activity) { super.onAttach(activity); mLocalBroadcastManager = LocalBroadcastManager.getInstance(activity); } @Override public void onDetach() { super.onDetach(); mLocalBroadcastManager = null; } @Override public void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); mComposer.onKeyboardStateChanged(newConfig.keyboardHidden == KEYBOARDHIDDEN_NO); } public void reload() { // be sure to cancel all queries stopQuery(); // hide the warning bar hideWarning(); // reload data processArguments(null); onFocus(false); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.compose_message, container, false); mComposer = (ComposerBar) view.findViewById(R.id.composer_bar); mComposer.setComposerListener(this); // footer (for tablet presence status) mStatusText = (TextView) view.findViewById(R.id.status_text); mComposer.setRootView(view); Configuration config = getResources().getConfiguration(); mComposer.onKeyboardStateChanged(config.keyboardHidden == KEYBOARDHIDDEN_NO); return view; } private final MessageListAdapter.OnContentChangedListener mContentChangedListener = new MessageListAdapter.OnContentChangedListener() { public void onContentChanged(MessageListAdapter adapter) { if (isVisible()) startQuery(false); } }; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setHasOptionsMenu(true); mQueryHandler = new MessageListQueryHandler(this); mHandler = new Handler(); // list adapter creation is post-poned } public boolean isActionModeActive() { return mCheckedItemCount > 0; } @Override public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) { if (checked) mCheckedItemCount++; else mCheckedItemCount--; mode.setTitle( getResources().getQuantityString(R.plurals.context_selected, mCheckedItemCount, mCheckedItemCount)); mode.invalidate(); } @Override public boolean onCreateActionMode(ActionMode mode, Menu menu) { MenuInflater inflater = mode.getMenuInflater(); inflater.inflate(R.menu.compose_message_ctx, menu); return true; } @Override public boolean onPrepareActionMode(ActionMode mode, Menu menu) { MenuItem retryMenu = menu.findItem(R.id.menu_retry); MenuItem shareMenu = menu.findItem(R.id.menu_share); MenuItem copyTextMenu = menu.findItem(R.id.menu_copy_text); MenuItem detailsMenu = menu.findItem(R.id.menu_details); MenuItem openMenu = menu.findItem(R.id.menu_open); MenuItem dlMenu = menu.findItem(R.id.menu_download); MenuItem cancelDlMenu = menu.findItem(R.id.menu_cancel_download); MenuItem decryptMenu = menu.findItem(R.id.menu_decrypt); // initial status retryMenu.setVisible(false); shareMenu.setVisible(false); copyTextMenu.setVisible(false); detailsMenu.setVisible(false); openMenu.setVisible(false); dlMenu.setVisible(false); cancelDlMenu.setVisible(false); decryptMenu.setVisible(false); boolean singleItem = (mCheckedItemCount == 1); if (singleItem) { CompositeMessage msg = getCheckedItem(); // message waiting for user review or not delivered if (msg.getStatus() == Messages.STATUS_PENDING || msg.getStatus() == Messages.STATUS_NOTDELIVERED) { retryMenu.setVisible(true); } // 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) shareMenu.setVisible(true); // non-empty text: copy text to clipboard if (text != null && !TextUtils.isEmpty(text.getContent())) copyTextMenu.setVisible(true); 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; openMenu.setTitle(resId); openMenu.setVisible(true); } // message has a fetch url - add download control entry if (msg.getDirection() == Messages.DIRECTION_IN && attachment.getFetchUrl() != null) { if (!DownloadService.isQueued(attachment.getFetchUrl())) { int string; // already fetched if (attachment.getLocalUri() != null) string = R.string.download_again; else string = R.string.download_file; dlMenu.setTitle(string); dlMenu.setVisible(true); } else { cancelDlMenu.setVisible(true); } } } } else { decryptMenu.setVisible(true); } detailsMenu.setVisible(true); } return true; } @Override public boolean onActionItemClicked(ActionMode mode, MenuItem item) { switch (item.getItemId()) { case R.id.menu_delete: { // using clone because listview returns its original copy deleteSelectedMessages(SystemUtils.cloneSparseBooleanArray(getListView().getCheckedItemPositions())); mode.finish(); return true; } case R.id.menu_retry: { CompositeMessage msg = getCheckedItem(); retryMessage(msg); mode.finish(); return true; } case R.id.menu_share: { CompositeMessage msg = getCheckedItem(); shareMessage(msg); mode.finish(); return true; } case R.id.menu_copy_text: { CompositeMessage msg = getCheckedItem(); 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(); mode.finish(); return true; } case R.id.menu_decrypt: { CompositeMessage msg = getCheckedItem(); decryptMessage(msg); mode.finish(); return true; } case R.id.menu_open: { CompositeMessage msg = getCheckedItem(); openFile(msg); mode.finish(); return true; } case R.id.menu_download: { CompositeMessage msg = getCheckedItem(); startDownload(msg); mode.finish(); return true; } case R.id.menu_cancel_download: { CompositeMessage msg = getCheckedItem(); stopDownload(msg); mode.finish(); return true; } case R.id.menu_details: { CompositeMessage msg = getCheckedItem(); showMessageDetails(msg); mode.finish(); return true; } } return false; } @Override public void onDestroyActionMode(ActionMode mode) { mCheckedItemCount = 0; getListView().clearChoices(); mListAdapter.notifyDataSetChanged(); } private CompositeMessage getCheckedItem() { if (mCheckedItemCount != 1) throw new IllegalStateException("checked items count must be exactly 1"); Cursor cursor = (Cursor) getListView().getItemAtPosition(getCheckedItemPosition()); return CompositeMessage.fromCursor(getActivity(), cursor); } private int getCheckedItemPosition() { SparseBooleanArray checked = getListView().getCheckedItemPositions(); return checked.keyAt(checked.indexOfValue(true)); } private void deleteSelectedMessages(final SparseBooleanArray checked) { new AlertDialogWrapper.Builder(getActivity()).setMessage(R.string.confirm_will_delete_messages) .setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { Context ctx = getActivity(); for (int i = 0, c = getListView().getCount() + getListView().getHeaderViewsCount(); i < c; ++i) { if (checked.get(i)) CompositeMessage.deleteFromCursor(ctx, (Cursor) getListView().getItemAtPosition(i)); } mListAdapter.notifyDataSetChanged(); } }).setNegativeButton(android.R.string.cancel, null).show(); } private void initAttachmentView() { View view = getView(); mAttachmentContainer = view.findViewById(R.id.attachment_container); mAttachmentCard = view.findViewById(R.id.circular_card); View.OnClickListener hideAttachmentListener = new View.OnClickListener() { @Override public void onClick(View v) { toggleAttachmentView(); } }; view.findViewById(R.id.attachment_overlay).setOnClickListener(hideAttachmentListener); view.findViewById(R.id.attach_hide).setOnClickListener(hideAttachmentListener); view.findViewById(R.id.attach_camera).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { selectPhotoAttachment(); toggleAttachmentView(); } }); view.findViewById(R.id.attach_gallery).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { selectGalleryAttachment(); toggleAttachmentView(); } }); view.findViewById(R.id.attach_video).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(getContext(), R.string.msg_not_implemented, Toast.LENGTH_SHORT).show(); } }); view.findViewById(R.id.attach_audio).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { selectAudioAttachment(); toggleAttachmentView(); } }); view.findViewById(R.id.attach_file).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(getContext(), R.string.msg_not_implemented, Toast.LENGTH_SHORT).show(); } }); view.findViewById(R.id.attach_vcard).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { selectContactAttachment(); toggleAttachmentView(); } }); view.findViewById(R.id.attach_location).setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(getContext(), R.string.msg_not_implemented, Toast.LENGTH_SHORT).show(); } }); } /** Sends out a binary message. */ @Override public void sendBinaryMessage(Uri uri, String mime, boolean media, Class<? extends MessageComponent<?>> klass) { Log.v(TAG, "sending binary content: " + uri); Uri newMsg = null; String msgId = null; File previewFile = null; long length = -1; boolean encrypted = Preferences.getEncryptionEnabled(getActivity()); int compress = 0; if (klass == ImageComponent.class) { compress = Preferences.getImageCompression(getActivity()); } try { // TODO convert to thread (?) offlineModeWarning(); msgId = MessageCenterService.messageId(); // generate thumbnail // FIXME this is blocking!!!! if (media && klass == ImageComponent.class) { // FIXME hard-coded to ImageComponent String filename = ImageComponent.buildMediaFilename(msgId, MediaStorage.THUMBNAIL_MIME_NETWORK); previewFile = MediaStorage.cacheThumbnail(getActivity(), uri, filename, true); } length = MediaStorage.getLength(getActivity(), uri); // save to database ContentValues values = new ContentValues(); values.put(Messages.MESSAGE_ID, msgId); values.put(Messages.PEER, mUserJID); /* 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); // 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); 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); values.put(Messages.ATTACHMENT_COMPRESS, compress); 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); startQuery(false); } else { Log.v(TAG, "no data - cannot start query for this composer"); } c.close(); } // send message! String previewPath = (previewFile != null) ? previewFile.getAbsolutePath() : null; MessageCenterService.sendBinaryMessage(getActivity(), mUserJID, mime, uri, length, previewPath, encrypted, compress, ContentUris.parseId(newMsg), msgId); } 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(); String msgId = MessageUtils.messageId(); // save to local storage ContentValues values = new ContentValues(); // must supply a message ID... values.put(Messages.MESSAGE_ID, msgId); values.put(Messages.PEER, mUserJID); 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); // we can run it here because progress=false startQuery(false); } else { Log.v(TAG, "no data - cannot start query for this composer"); } c.close(); } // send message! MessageCenterService.sendTextMessage(getActivity(), mUserJID, mText, encrypted, ContentUris.parseId(newMsg), msgId); } else { throw new SQLiteDiskIOException(); } } catch (SQLiteDiskIOException e) { getActivity().runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getActivity(), R.string.error_store_outbox, Toast.LENGTH_LONG).show(); } }); } catch (Exception e) { // TODO warn user Log.d(TAG, "broken message thread", e); } } } /** Sends out the text message in the composing entry. */ @Override public void sendTextMessage(String message) { if (!TextUtils.isEmpty(message)) { offlineModeWarning(); // start thread new TextMessageThread(message).start(); } } @Override public boolean sendTyping() { if (mAvailableResources.size() > 0) { MessageCenterService.sendChatState(getActivity(), mUserJID, ChatState.composing); return true; } return false; } @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); updateUI(); } @Override public boolean onOptionsItemSelected(MenuItem item) { // action mode is active - no processing if (isActionModeActive()) return true; switch (item.getItemId()) { case R.id.call_contact: startActivity(new Intent(Intent.ACTION_CALL, Uri.parse("tel:" + mUserPhone))); return true; case R.id.view_contact: viewContact(); return true; case R.id.menu_attachment: toggleAttachmentView(); 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) { int choiceMode = listView.getChoiceMode(); if (choiceMode == ListView.CHOICE_MODE_NONE || choiceMode == ListView.CHOICE_MODE_SINGLE) { 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, mUserPhone != null ? mUserPhone : mUserJID); AlertDialogWrapper.Builder builder = new AlertDialogWrapper.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(); } } else { item.onClick(); } } else { super.onListItemClick(listView, view, position, id); } } 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.getDatabaseId()); i.putExtra(CompositeMessage.MSG_SENDER, msg.getSender()); i.putExtra(CompositeMessage.MSG_TIMESTAMP, msg.getTimestamp()); i.putExtra(CompositeMessage.MSG_ENCRYPTED, attachment.getSecurityFlags() != Coder.SECURITY_CLEARTEXT); 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) { Intent i = new Intent(Intent.ACTION_VIEW); i.setDataAndType(attachment.getLocalUri(), attachment.getMime()); startActivity(i); } } public void viewContact() { if (mConversation != null) { Contact contact = mConversation.getContact(); if (contact != null) { Uri uri = contact.getUri(); if (uri != null) { Intent i = new Intent(Intent.ACTION_VIEW, uri); if (i.resolveActivity(getActivity().getPackageManager()) != null) { startActivity(i); } else { // no contacts app found (crap device eh?) Toast.makeText(getActivity(), R.string.err_no_contacts_app, Toast.LENGTH_LONG).show(); } } else { // no contact found Toast.makeText(getActivity(), R.string.err_no_contact, Toast.LENGTH_SHORT).show(); } } } } boolean tryHideAttachmentView() { if (isAttachmentViewVisible()) { setupAttachmentViewCloseAnimation(); startAttachmentViewAnimation(); return true; } return false; } private void setupAttachmentViewCloseAnimation() { if (mAttachAnimator != null && !mAttachAnimator.isRunning()) { // reverse the animation mAttachAnimator = mAttachAnimator.reverse(); mAttachAnimator.addListener(new SupportAnimator.AnimatorListener() { public void onAnimationCancel() { } public void onAnimationEnd() { mAttachmentContainer.setVisibility(View.INVISIBLE); mAttachAnimator = null; } public void onAnimationRepeat() { } public void onAnimationStart() { } }); } } private boolean isAttachmentViewVisible() { return mAttachmentContainer.getVisibility() != View.INVISIBLE || mAttachAnimator != null; } private void startAttachmentViewAnimation() { mAttachAnimator.setInterpolator(new AccelerateDecelerateInterpolator()); mAttachAnimator.setDuration(250); mAttachAnimator.start(); } /** Show or hide the attachment selector. */ public void toggleAttachmentView() { if (isAttachmentViewVisible()) { setupAttachmentViewCloseAnimation(); } else { mComposer.forceHideKeyboard(); mAttachmentContainer.setVisibility(View.VISIBLE); int right = mAttachmentCard.getRight(); int top = mAttachmentCard.getTop(); float f = (float) Math .sqrt(Math.pow(mAttachmentCard.getWidth(), 2D) + Math.pow(mAttachmentCard.getHeight(), 2D)); mAttachAnimator = ViewAnimationUtils.createCircularReveal(mAttachmentCard, right, top, 0, f); } startAttachmentViewAnimation(); } /** Starts an activity for shooting a picture. */ private void selectPhotoAttachment() { 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.getOutgoingImageFile(); Uri uri = Uri.fromFile(mCurrentPhoto); Intent take = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); take.putExtra(MediaStore.EXTRA_OUTPUT, uri); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { take.setClipData(ClipData.newUri(getContext().getContentResolver(), "Picture path", uri)); } startActivityForResult(take, SELECT_ATTACHMENT_PHOTO); } catch (UnsupportedOperationException ue) { Toast.makeText(getActivity(), R.string.chooser_error_no_camera_app, Toast.LENGTH_LONG).show(); } catch (IOException e) { Log.e(TAG, "error creating temp file", e); Toast.makeText(getActivity(), R.string.chooser_error_no_camera, Toast.LENGTH_LONG).show(); } } /** Starts an activity for picture attachment selection. */ @TargetApi(Build.VERSION_CODES.KITKAT) private void selectGalleryAttachment() { Intent pictureIntent; if (!MediaStorage.isStorageAccessFrameworkAvailable()) { pictureIntent = new Intent(Intent.ACTION_GET_CONTENT).putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); } else { pictureIntent = new Intent(Intent.ACTION_OPEN_DOCUMENT); } pictureIntent.addCategory(Intent.CATEGORY_OPENABLE).setType("image/*") .addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION).putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true); startActivityForResult(pictureIntent, 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() { // create audio fragment if needed AudioFragment audio = getAudioFragment(); // stop everything if (mAudioControl != null) { resetAudio(mAudioControl); } else { audio.resetPlayer(); audio.setMessageId(-1); } // show dialog mAudioDialog = new AudioDialog(getActivity(), audio, this); mAudioDialog.show(); } private AudioFragment getAudioFragment() { AudioFragment fragment = findAudioFragment(); if (fragment == null) { fragment = new AudioFragment(); FragmentManager fm = getActivity().getSupportFragmentManager(); fm.beginTransaction().add(fragment, "audio").commit(); // commit immediately please fm.executePendingTransactions(); } return fragment; } private AudioFragment findAudioFragment() { FragmentManager fm = getFragmentManager(); return (AudioFragment) fm.findFragmentByTag("audio"); } private void deleteThread() { AlertDialogWrapper.Builder builder = new AlertDialogWrapper.Builder(getActivity()); 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) { mComposer.setText(""); try { MessagesProvider.deleteThread(getActivity(), threadId); } catch (SQLiteDiskIOException e) { Log.w(TAG, "error deleting thread"); Toast.makeText(getActivity(), R.string.error_delete_thread, Toast.LENGTH_LONG).show(); } } }); builder.setNegativeButton(android.R.string.cancel, null); builder.create().show(); } private void blockUser() { new MaterialDialog.Builder(getActivity()).title(R.string.title_block_user_warning) .content(R.string.msg_block_user_warning).positiveText(R.string.menu_block_user) .positiveColorRes(R.color.button_danger).negativeText(android.R.string.cancel) .onPositive(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(@NonNull MaterialDialog materialDialog, @NonNull DialogAction dialogAction) { setPrivacy(PRIVACY_BLOCK); } }).show(); } private void unblockUser() { new MaterialDialog.Builder(getActivity()).title(R.string.title_unblock_user_warning) .content(R.string.msg_unblock_user_warning).positiveText(R.string.menu_unblock_user) .positiveColorRes(R.color.button_danger).negativeText(android.R.string.cancel) .onPositive(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(@NonNull MaterialDialog materialDialog, @NonNull DialogAction dialogAction) { setPrivacy(PRIVACY_UNBLOCK); } }).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 void scrollToPosition(int position) { getListView().setSelection(position); } private boolean isSearching() { Bundle args = myArguments(); return args != null && args.getLong(ComposeMessage.EXTRA_MESSAGE, -1) >= 0; } private synchronized void startQuery(boolean progress) { if (progress) getActivity().setProgressBarIndeterminateVisibility(true); Conversation.startQuery(mQueryHandler, CONVERSATION_QUERY_TOKEN, threadId); // message list query will be started by query handler } private void startMessagesQuery() { CompositeMessage.startQuery(mQueryHandler, MESSAGE_LIST_QUERY_TOKEN, threadId, isSearching() ? 0 : MESSAGE_PAGE_SIZE, 0); } private void startMessagesQuery(long lastId) { CompositeMessage.startQuery(mQueryHandler, MESSAGE_PAGE_QUERY_TOKEN, threadId, isSearching() ? 0 : MESSAGE_PAGE_SIZE, lastId); } private void stopQuery() { hideHeaderView(); if (mListAdapter != null) mListAdapter.changeCursor(null); if (mQueryHandler != null) { // be sure to cancel all queries mQueryHandler.abort(); } } private void showMessageDetails(CompositeMessage msg) { MessageUtils.showMessageDetails(getActivity(), msg, mUserPhone != null ? mUserPhone : mUserJID); } private void shareMessage(CompositeMessage msg) { 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"); } 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(), ConversationsActivity.class)); getActivity().finish(); return; } mUserJID = mConversation.getRecipient(); Contact contact = mConversation.getContact(); if (contact != null) { mUserName = contact.getName(); mUserPhone = contact.getNumber(); } else { mUserName = mUserJID; } } 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) { // image from storage/picture from camera // since there are like up to 3 different ways of doing this... if (requestCode == SELECT_ATTACHMENT_OPENABLE || requestCode == SELECT_ATTACHMENT_PHOTO) { if (resultCode == Activity.RESULT_OK) { Uri[] uris = null; String[] mimes = null; // returning from camera if (data == null) { if (mCurrentPhoto != null) { Uri 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; uris = new Uri[] { uri }; } } else { if (mCurrentPhoto != null) { mCurrentPhoto.delete(); mCurrentPhoto = null; } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN && data.getClipData() != null) { ClipData cdata = data.getClipData(); uris = new Uri[cdata.getItemCount()]; for (int i = 0; i < uris.length; i++) { ClipData.Item item = cdata.getItemAt(i); uris[i] = item.getUri(); } } else { uris = new Uri[] { data.getData() }; mimes = new String[] { data.getType() }; } // SAF available, request persistable permissions if (MediaStorage.isStorageAccessFrameworkAvailable()) { for (Uri uri : uris) { if (uri != null && !"file".equals(uri.getScheme())) { MediaStorage.requestPersistablePermissions(getActivity(), uri); } } } } for (int i = 0; uris != null && i < uris.length; i++) { Uri uri = uris[i]; String mime = (mimes != null && mimes.length >= uris.length) ? mimes[i] : 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; } } } // contact card (vCard) 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(); } } } } } } @Override public void onSaveInstanceState(Bundle out) { super.onSaveInstanceState(out); out.putParcelable(Uri.class.getName(), Threads.getUri(mUserJID)); // save composer status if (mComposer != null) mComposer.onSaveInstanceState(out); // current photo being shot if (mCurrentPhoto != null) { out.putString("currentPhoto", mCurrentPhoto.toString()); } // audio dialog open if (mAudioDialog != null) { mAudioDialog.onSaveInstanceState(out); } // audio player stuff out.putInt("mediaPlayerStatus", mMediaPlayerStatus); } private void processArguments(Bundle savedInstanceState) { Bundle args; 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); String currentPhoto = savedInstanceState.getString("currentPhoto"); if (currentPhoto != null) { mCurrentPhoto = new File(currentPhoto); } mAudioDialog = AudioDialog.onRestoreInstanceState(getActivity(), savedInstanceState, getAudioFragment(), this); if (mAudioDialog != null) { Log.d(TAG, "recreating audio dialog"); mAudioDialog.show(); } } 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()) { mUserName = c.getString(0); mUserPhone = c.getString(1); // FIXME should it be retrieved from RawContacts.SYNC3 ?? mUserJID = XMPPUtils.createLocalJID(getActivity(), MessageUtils.sha1(mUserPhone)); Cursor cp = cres.query(Messages.CONTENT_URI, new String[] { Messages.THREAD_ID }, Messages.PEER + " = ?", new String[] { mUserJID }, 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(mUserJID); } } // 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"); mUserJID = uri.getPathSegments().get(1); mConversation = Conversation.loadFromUserId(getActivity(), mUserJID); if (mConversation == null) { mConversation = Conversation.createNew(getActivity()); mConversation.setNumberHint(args.getString("number")); mConversation.setRecipient(mUserJID); } // this way avoid doing the users database query twice else { if (mConversation.getContact() == null) { mConversation.setNumberHint(args.getString("number")); mConversation.setRecipient(mUserJID); } } threadId = mConversation.getThreadId(); Contact contact = mConversation.getContact(); if (contact != null) { mUserName = contact.getName(); mUserPhone = contact.getNumber(); } else { mUserName = mUserJID; mUserPhone = null; } } } // set title if we are autonomous if (mArguments != null) { String title = mUserName; //if (mUserPhone != null) title += " <" + mUserPhone + ">"; setActivityTitle(title, ""); } // 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 (mUserPhone != 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:" + mUserPhone)); i.putExtra("sms_body", getString(R.string.text_invite_message)); startActivity(i); getActivity().finish(); } }; AlertDialogWrapper.Builder builder = new AlertDialogWrapper.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) { if (mStatusText != null) { // tablet UI - ignore title mStatusText.setText(status); } else { ComposeMessageParent parent = (ComposeMessageParent) getActivity(); parent.setTitle(title, status); } } public void setActivityStatusUpdating() { if (mStatusText != null) { CharSequence text = mStatusText.getText(); if (text != null && text.length() > 0) { mStatusText.setText(ComposeMessage.applyUpdatingStyle(text)); } } else { ComposeMessageParent parent = (ComposeMessageParent) getActivity(); 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(resuming); } else { // HACK this is for crappy honeycomb :) getActivity().setProgressBarIndeterminateVisibility(false); mConversation = Conversation.createNew(getActivity()); mConversation.setRecipient(mUserJID); onConversationCreated(); } } /** Called when the {@link Conversation} object has been created. */ private void onConversationCreated() { // subscribe to presence notifications subscribePresence(); // restore any draft mComposer.restoreText(mConversation.getDraft()); 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. */ Log.v(TAG, "marking thread as read"); mConversation.markAsRead(); } else { // new conversation -- observe peer Uri registerPeerObserver(); } // 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_REJECT; 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(true, R.string.title_invitation); } }); } } 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: case PRIVACY_REJECT: 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[] { mUserJID }); // accept invitation if (action == PRIVACY_ACCEPT) { // trust the key UsersProvider.trustUserKey(ctx, mUserJID); // reload contact invalidateContact(); } // setup broadcast receiver for block/unblock reply else if (action == PRIVACY_REJECT || action == PRIVACY_BLOCK || action == PRIVACY_UNBLOCK) { if (mPrivacyListener == null) { mPrivacyListener = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String from = XmppStringUtils .parseBareJid(intent.getStringExtra(MessageCenterService.EXTRA_FROM)); if (mUserJID.equals(from)) { // reload contact reloadContact(); // 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(); // hide any block warning // a new warning will be issued for the key if needed hideWarning(); 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, mUserJID, action); } private void invalidateContact() { Contact.invalidate(mUserJID); reloadContact(); } private void reloadContact() { if (mConversation != null) { // this will trigger contact reload mConversation.setRecipient(mUserJID); } } private void showIdentityDialog(boolean informationOnly, int titleId) { String fingerprint; String uid; PGPPublicKeyRing publicKey = UsersProvider.getPublicKey(getActivity(), mUserJID, false); if (publicKey != null) { PGPPublicKey pk = PGP.getMasterKey(publicKey); fingerprint = PGP.formatFingerprint(PGP.getFingerprint(pk)); uid = PGP.getUserId(pk, null); // TODO server!!! } else { // FIXME using another string fingerprint = uid = getString(R.string.peer_unknown); } SpannableStringBuilder text = new SpannableStringBuilder(); text.append(getString(R.string.text_invitation1)).append('\n'); Contact c = mConversation.getContact(); if (c != null) { text.append(c.getName()).append(" <").append(c.getNumber()).append('>'); } else { int start = text.length() - 1; text.append(uid); text.setSpan(MessageUtils.STYLE_BOLD, start, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } text.append('\n').append(getString(R.string.text_invitation2)).append('\n'); int start = text.length() - 1; text.append(fingerprint); text.setSpan(MessageUtils.STYLE_BOLD, start, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); AlertDialogWrapper.Builder builder = new AlertDialogWrapper.Builder(getActivity()).setMessage(text); if (informationOnly) { builder.setTitle(titleId); } else { DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { // hide warning bar hideWarning(); switch (which) { case DialogInterface.BUTTON_POSITIVE: // trust new key trustKeyChange(); break; case DialogInterface.BUTTON_NEGATIVE: // block user immediately setPrivacy(PRIVACY_BLOCK); break; } } }; builder.setTitle(titleId).setPositiveButton(R.string.button_accept, listener) .setNegativeButton(R.string.button_block, listener); } builder.show(); } private void hideWarning() { SnackbarManager.dismiss(); } private void trustKeyChange() { // mark current key as trusted UsersProvider.trustUserKey(getActivity(), mUserJID); // reload contact invalidateContact(); } private void showKeyWarning(int textId, final int dialogTitleId, final int dialogMessageId) { Activity context = getActivity(); if (context != null) { showWarning(context.getText(textId), new View.OnClickListener() { @Override public void onClick(View v) { DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { switch (which) { case DialogInterface.BUTTON_POSITIVE: // hide warning bar hideWarning(); // trust new key trustKeyChange(); break; case DialogInterface.BUTTON_NEUTRAL: showIdentityDialog(false, dialogTitleId); break; case DialogInterface.BUTTON_NEGATIVE: // hide warning bar hideWarning(); // block user immediately setPrivacy(PRIVACY_BLOCK); break; } } }; new AlertDialogWrapper.Builder(getActivity()).setTitle(dialogTitleId) .setMessage(dialogMessageId).setPositiveButton(R.string.button_accept, listener) .setNeutralButton(R.string.button_identity, listener) .setNegativeButton(R.string.button_block, listener).show(); } }, WarningType.FATAL); } } private void showKeyUnknownWarning() { showKeyWarning(R.string.warning_public_key_unknown, R.string.title_public_key_unknown_warning, R.string.msg_public_key_unknown_warning); } private void showKeyChangedWarning() { showKeyWarning(R.string.warning_public_key_changed, R.string.title_public_key_changed_warning, R.string.msg_public_key_changed_warning); } private void showWarning(CharSequence text, final View.OnClickListener listener, WarningType type) { View view = getView(); Activity context = getActivity(); if (view == null || context == null) return; Snackbar bar = SnackbarManager.getCurrentSnackbar(); if (bar != null) { WarningType oldType = (WarningType) bar.getTag(); if (oldType != null && oldType.getValue() > type.getValue()) return; bar.dismiss(); } bar = Snackbar.with(context).type(SnackbarType.MULTI_LINE).text(text) .duration(Snackbar.SnackbarDuration.LENGTH_INDEFINITE).dismissOnActionClicked(false) .allowMultipleActionClicks(true); if (listener != null) { bar.swipeToDismiss(false).actionLabel(R.string.warning_button_details) .actionListener(new ActionClickListener() { @Override public void onActionClicked(Snackbar snackbar) { listener.onClick(null); } }); } else { bar.swipeToDismiss(true).animation(false); } int colorId = 0; int textColorId = 0; switch (type) { case FATAL: textColorId = R.color.warning_bar_text_fatal; colorId = R.color.warning_bar_background_fatal; break; case WARNING: textColorId = R.color.warning_bar_text_warning; colorId = R.color.warning_bar_background_warning; break; } bar.setTag(type); bar.color(getResources().getColor(colorId)).textColor(getResources().getColor(textColorId)); if (listener != null) { SnackbarManager.show(bar); } else { SnackbarManager.show(bar, (ViewGroup) view.findViewById(R.id.warning_bar)); } } private void subscribePresence() { // TODO this needs serious refactoring if (mPresenceReceiver == null) { mPresenceReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String action = intent.getAction(); if (MessageCenterService.ACTION_PRESENCE.equals(action)) { String from = intent.getStringExtra(MessageCenterService.EXTRA_FROM); String bareFrom = from != null ? XmppStringUtils.parseBareJid(from) : null; // we are receiving a presence from our peer if (from != null && bareFrom.equalsIgnoreCase(mUserJID)) { // we handle only (un)available presence stanzas String type = intent.getStringExtra(MessageCenterService.EXTRA_TYPE); if (type == null) { // no roster entry found, request subscription // pre-approve our presence if we don't have contact's key Intent i = new Intent(context, MessageCenterService.class); i.setAction(MessageCenterService.ACTION_PRESENCE); i.putExtra(MessageCenterService.EXTRA_TO, mUserJID); i.putExtra(MessageCenterService.EXTRA_TYPE, Presence.Type.subscribed.name()); context.startService(i); // request subscription i = new Intent(context, MessageCenterService.class); i.setAction(MessageCenterService.ACTION_PRESENCE); i.putExtra(MessageCenterService.EXTRA_TO, mUserJID); i.putExtra(MessageCenterService.EXTRA_TYPE, Presence.Type.subscribe.name()); context.startService(i); setStatusText(context.getString(R.string.invitation_sent_label)); } // (un)available presence else if (Presence.Type.available.name().equals(type) || Presence.Type.unavailable.name().equals(type)) { CharSequence statusText = null; // really not much sense in requesting the key for a non-existing contact Contact contact = getContact(); if (contact != null) { String newFingerprint = intent .getStringExtra(MessageCenterService.EXTRA_FINGERPRINT); // if this is null, we are accepting the key for the first time PGPPublicKeyRing trustedPublicKey = contact.getTrustedPublicKeyRing(); // request the key if we don't have a trusted one and of course if the user has a key boolean unknownKey = (trustedPublicKey == null && contact.getFingerprint() != null); boolean changedKey = false; // check if fingerprint changed if (trustedPublicKey != null && newFingerprint != null) { String oldFingerprint = PGP .getFingerprint(PGP.getMasterKey(trustedPublicKey)); if (!newFingerprint.equalsIgnoreCase(oldFingerprint)) { // fingerprint has changed since last time changedKey = true; } } if (changedKey) { // warn user that public key is changed showKeyChangedWarning(); } else if (unknownKey) { // warn user that public key is unknown showKeyUnknownWarning(); } } if (Presence.Type.available.toString().equals(type)) { mAvailableResources.add(from); /* * FIXME using mode this way has several flaws. * 1. it doesn't take multiple resources into account * 2. it doesn't account for away status duration (we don't have this information at all) */ String mode = intent.getStringExtra(MessageCenterService.EXTRA_SHOW); if (mode != null && mode.equals(Presence.Mode.away.toString())) { statusText = context.getString(R.string.seen_away_label); } else { statusText = context.getString(R.string.seen_online_label); } // request version information if (contact != null && contact.getVersion() != null) { setVersionInfo(context, contact.getVersion()); } else if (mVersionRequestId == null) { requestVersion(from); } } else if (Presence.Type.unavailable.toString().equals(type)) { boolean removed = mAvailableResources.remove(from); /* * All available resources have gone. Mark * the user as offline immediately and use the * timestamp provided with the stanza (if any). */ if (mAvailableResources.size() == 0) { // an offline user can't be typing mIsTyping = false; if (removed) { // resource was removed now, mark as just offline statusText = context.getText(R.string.seen_moment_ago_label); } else { // resource is offline, request last activity if (contact != null && contact.getLastSeen() > 0) { setLastSeenTimestamp(context, contact.getLastSeen()); } else if (mLastActivityRequestId == null) { mLastActivityRequestId = StringUtils.randomString(6); MessageCenterService.requestLastActivity(context, bareFrom, mLastActivityRequestId); } } } } if (statusText != null) { mCurrentStatus = statusText; if (!mIsTyping) setStatusText(statusText); } } // subscription accepted, probe presence else if (Presence.Type.subscribed.name().equals(type)) { presenceSubscribe(); } } } else if (MessageCenterService.ACTION_LAST_ACTIVITY.equals(action)) { String id = intent.getStringExtra(MessageCenterService.EXTRA_PACKET_ID); if (id != null && id.equals(mLastActivityRequestId)) { mLastActivityRequestId = null; // ignore last activity if we had an available presence in the meantime if (mAvailableResources.size() == 0) { String type = intent.getStringExtra(MessageCenterService.EXTRA_TYPE); if (type == null || !type.equalsIgnoreCase(IQ.Type.error.toString())) { long seconds = intent.getLongExtra(MessageCenterService.EXTRA_SECONDS, -1); setLastSeenSeconds(context, seconds); } } } } else if (MessageCenterService.ACTION_VERSION.equals(action)) { // compare version and show warning if needed String id = intent.getStringExtra(MessageCenterService.EXTRA_PACKET_ID); if (id != null && id.equals(mVersionRequestId)) { mVersionRequestId = null; String name = intent.getStringExtra(MessageCenterService.EXTRA_VERSION_NAME); if (name != null && name.equalsIgnoreCase(context.getString(R.string.app_name))) { String version = intent.getStringExtra(MessageCenterService.EXTRA_VERSION_NUMBER); if (version != null) { Contact contact = getContact(); if (contact != null) // cache the version contact.setVersion(version); setVersionInfo(context, version); } } } } else if (MessageCenterService.ACTION_CONNECTED.equals(action)) { // reset compose sent flag mComposer.resetCompose(); // reset available resources list mAvailableResources.clear(); // reset any pending request mLastActivityRequestId = null; mVersionRequestId = null; } else if (MessageCenterService.ACTION_ROSTER_LOADED.equals(action)) { // probe presence presenceSubscribe(); } else if (MessageCenterService.ACTION_MESSAGE.equals(action)) { String from = intent.getStringExtra(MessageCenterService.EXTRA_FROM); String chatState = intent.getStringExtra("org.kontalk.message.chatState"); // we are receiving a composing notification from our peer if (from != null && XMPPUtils.equalsBareJID(from, mUserJID)) { if (chatState != null && ChatState.composing.toString().equals(chatState)) { mIsTyping = true; setStatusText(context.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_ROSTER_LOADED); filter.addAction(MessageCenterService.ACTION_LAST_ACTIVITY); filter.addAction(MessageCenterService.ACTION_MESSAGE); filter.addAction(MessageCenterService.ACTION_VERSION); mLocalBroadcastManager.registerReceiver(mPresenceReceiver, filter); // request connection and roster load status Context ctx = getActivity(); if (ctx != null) { MessageCenterService.requestConnectionStatus(ctx); MessageCenterService.requestRosterStatus(ctx); } } } private void setVersionInfo(Context context, String version) { if (SystemUtils.isOlderVersion(context, version)) { showWarning(context.getText(R.string.warning_older_version), null, WarningType.WARNING); } } private void setLastSeenTimestamp(Context context, long stamp) { setCurrentStatusText(MessageUtils.formatRelativeTimeSpan(context, stamp)); } private void setLastSeenSeconds(Context context, long seconds) { CharSequence statusText = null; if (seconds == 0) { // it's improbable, but whatever... statusText = context.getText(R.string.seen_moment_ago_label); } else if (seconds > 0) { long stamp = System.currentTimeMillis() - (seconds * 1000); Contact contact = getContact(); if (contact != null) { contact.setLastSeen(stamp); } // seconds ago relative to our time statusText = MessageUtils.formatRelativeTimeSpan(context, stamp); } if (statusText != null) { setCurrentStatusText(statusText); } } private void setCurrentStatusText(CharSequence statusText) { mCurrentStatus = statusText; if (!mIsTyping) setStatusText(statusText); } private void requestVersion(String jid) { Context context = getActivity(); if (context != null) { mVersionRequestId = StringUtils.randomString(6); MessageCenterService.requestVersionInfo(context, jid, mVersionRequestId); } } /** Sends a subscription request for the current peer. */ private void presenceSubscribe() { Context context = getActivity(); if (context != null) { // all of this shall be done only if there isn't a request from the other contact if (mConversation.getRequestStatus() != Threads.REQUEST_WAITING) { // request last presence Intent i = new Intent(context, MessageCenterService.class); i.setAction(MessageCenterService.ACTION_PRESENCE); i.putExtra(MessageCenterService.EXTRA_TO, mUserJID); i.putExtra(MessageCenterService.EXTRA_TYPE, Presence.Type.probe.name()); context.startService(i); } } } private void unsubcribePresence() { if (mPresenceReceiver != null) { mLocalBroadcastManager.unregisterReceiver(mPresenceReceiver); mPresenceReceiver = null; } } private void setStatusText(CharSequence text) { ComposeMessageParent parent = (ComposeMessageParent) getActivity(); if (parent instanceof ComposeMessage) setActivityTitle(null, text); else { if (mStatusText != null) mStatusText.setText(text); } } 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, mUserJID); if (conv != null) { mConversation = conv; threadId = mConversation.getThreadId(); // auto-unregister unregisterPeerObserver(); } // fire cursor update Log.v(TAG, "peer observer active"); processStart(false); } @Override public boolean deliverSelfNotifications() { return false; } } @Override public void onResume() { super.onResume(); if (Authenticator.getDefaultAccount(getActivity()) == null) { NumberValidation.start(getActivity()); getActivity().finish(); return; } // hold message center MessageCenterService.hold(getActivity()); ComposeMessage activity = getParentActivity(); if (activity == null || !activity.hasLostFocus() || activity.hasWindowFocus()) { onFocus(true); } } public void onFocus(boolean resuming) { // resume content watcher resumeContentListener(); // we are updating the status now setActivityStatusUpdating(); // cursor was previously destroyed -- reload everything processStart(resuming); if (mUserJID != null) { // set notifications on pause MessagingNotification.setPaused(mUserJID); // clear chat invitation (if any) // TODO use jid here MessagingNotification.clearChatInvitation(getActivity(), mUserJID); } } @Override public void onPause() { super.onPause(); // notify composer bar mComposer.onPause(); // hide emoji drawer tryHideEmojiDrawer(); // pause content watcher pauseContentListener(); // notify parent of pausing ComposeMessage parent = getParentActivity(); if (parent != null) parent.fragmentLostFocus(); CharSequence text = mComposer.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); try { getActivity().getContentResolver() .update(ContentUris.withAppendedId(Threads.CONTENT_URI, threadId), values, null, null); } catch (SQLiteDiskIOException e) { // TODO warn user Log.w(TAG, "error saving draft", e); len = 0; } } } // 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, mUserJID); 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()); try { getActivity().getContentResolver().insert(Messages.CONTENT_URI, values); } catch (SQLiteDiskIOException e) { // TODO warn user Log.w(TAG, "error saving draft", e); len = 0; } } } if (len > 0) { Toast.makeText(getActivity(), R.string.msg_draft_saved, Toast.LENGTH_LONG).show(); } if (mComposer.isComposeSent()) { // send inactive state notification if (mAvailableResources.size() > 0) MessageCenterService.sendChatState(getActivity(), mUserJID, ChatState.inactive); mComposer.resetCompose(); } // unsubcribe presence notifications unsubcribePresence(); // release message center MessageCenterService.release(getActivity()); // release audio player AudioFragment audio = findAudioFragment(); if (audio != null) { stopMediaPlayerUpdater(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { if (!getActivity().isChangingConfigurations()) { audio.setMessageId(-1); audio.finish(true); } } } } @Override public void onStop() { super.onStop(); unregisterPeerObserver(); stopQuery(); } @Override public void onDestroy() { super.onDestroy(); if (mComposer != null) { mComposer.onDestroy(); } if (mAudioDialog != null) { mAudioDialog.dismiss(); mAudioDialog = null; } } private void pauseContentListener() { if (mListAdapter != null) mListAdapter.setOnContentChangedListener(null); } private void resumeContentListener() { if (mListAdapter != null) mListAdapter.setOnContentChangedListener(mContentChangedListener); } public final boolean isFinishing() { Activity activity = getActivity(); return (activity == null || activity.isFinishing()) || isRemoving(); } private void showHeaderView() { mHeaderView.setVisibility(View.VISIBLE); } private void hideHeaderView() { mHeaderView.setVisibility(View.GONE); } private void enableHeaderView(boolean enabled) { mNextPageButton.setEnabled(enabled); } private void updateUI() { Contact contact = (mConversation != null) ? mConversation.getContact() : null; boolean contactEnabled = contact != null && contact.getId() > 0; boolean threadEnabled = (threadId > 0); if (mCallMenu != null) { Context context = getActivity(); // FIXME what about VoIP? if (context != null && !context.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) { Context context = getActivity(); if (context != null && Authenticator.isSelfJID(context, mUserJID)) { mBlockMenu.setVisible(false).setEnabled(false); mUnblockMenu.setVisible(false).setEnabled(false); } else if (contact != null) { // block/unblock boolean blocked = contact.isBlocked(); if (blocked) // show warning if blocked showWarning(getText(R.string.warning_user_blocked), null, WarningType.WARNING); mBlockMenu.setVisible(!blocked).setEnabled(!blocked); mUnblockMenu.setVisible(blocked).setEnabled(blocked); } else { mBlockMenu.setVisible(true).setEnabled(true); mUnblockMenu.setVisible(true).setEnabled(true); } } } boolean tryHideEmojiDrawer() { if (mComposer.isEmojiVisible()) { mComposer.hideEmojiDrawer(false); return true; } return false; } public Conversation getConversation() { return mConversation; } public Contact getContact() { return (mConversation != null) ? mConversation.getContact() : null; } public long getThreadId() { return threadId; } public String getUserId() { return mUserJID; } public void setTextEntry(CharSequence text) { mComposer.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 { ConversationsActivity activity = (ConversationsActivity) 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 textChanged(CharSequence text) { Snackbar bar = SnackbarManager.getCurrentSnackbar(); if (bar != null) { WarningType type = (WarningType) bar.getTag(); if (type != null && type.getValue() < WarningType.FATAL.getValue()) { bar.dismiss(); } } } @Override public void onRecordingSuccessful(File file) { if (file != null) sendBinaryMessage(Uri.fromFile(file), AudioDialog.DEFAULT_MIME, true, AudioComponent.class); } @Override public void onRecordingCancel() { mAudioDialog = null; } @Override public void buttonClick(File audioFile, AudioContentViewControl view, long messageId) { AudioFragment audio = getAudioFragment(); if (audio.getMessageId() == messageId) { switch (mMediaPlayerStatus) { case AudioContentView.STATUS_PLAYING: pauseAudio(view); break; case AudioContentView.STATUS_PAUSED: case AudioContentView.STATUS_ENDED: playAudio(view, messageId); break; } } else { switch (mMediaPlayerStatus) { case AudioContentView.STATUS_IDLE: if (prepareAudio(audioFile, view, messageId)) playAudio(view, messageId); break; case AudioContentView.STATUS_ENDED: case AudioContentView.STATUS_PLAYING: case AudioContentView.STATUS_PAUSED: resetAudio(mAudioControl); if (prepareAudio(audioFile, view, messageId)) playAudio(view, messageId); break; } } } private boolean prepareAudio(File audioFile, final AudioContentViewControl view, final long messageId) { stopMediaPlayerUpdater(); try { AudioFragment audio = getAudioFragment(); final MediaPlayer player = audio.getPlayer(); player.setAudioStreamType(AudioManager.STREAM_MUSIC); player.setDataSource(audioFile.getAbsolutePath()); player.prepare(); // prepare was successful audio.setMessageId(messageId); mAudioControl = view; view.prepare(player.getDuration()); player.seekTo(view.getPosition()); view.setProgressChangeListener(true); player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { stopMediaPlayerUpdater(); view.end(); AudioFragment audio = findAudioFragment(); if (audio != null) audio.seekPlayerTo(0); setAudioStatus(AudioContentView.STATUS_ENDED); } }); return true; } catch (IOException e) { Toast.makeText(getActivity(), R.string.err_file_not_found, Toast.LENGTH_SHORT).show(); return false; } } @Override public void playAudio(AudioContentViewControl view, long messageId) { view.play(); findAudioFragment().getPlayer().start(); setAudioStatus(AudioContentView.STATUS_PLAYING); startMediaPlayerUpdater(view); } private void updatePosition(AudioContentViewControl view) { view.updatePosition(findAudioFragment().getPlayer().getCurrentPosition()); } @Override public void pauseAudio(AudioContentViewControl view) { view.pause(); findAudioFragment().getPlayer().pause(); stopMediaPlayerUpdater(); setAudioStatus(AudioContentView.STATUS_PAUSED); } private void resetAudio(AudioContentViewControl view) { if (view != null) { stopMediaPlayerUpdater(); view.end(); } AudioFragment audio = findAudioFragment(); if (audio != null) { audio.resetPlayer(); audio.setMessageId(-1); } } private void setAudioStatus(int audioStatus) { mMediaPlayerStatus = audioStatus; } @Override public void stopAllSounds() { resetAudio(mAudioControl); } @Override public void onBind(long messageId, final AudioContentViewControl view) { final AudioFragment audio = findAudioFragment(); if (audio != null && audio.getMessageId() == messageId) { mAudioControl = view; audio.getPlayer().setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { stopMediaPlayerUpdater(); view.end(); audio.seekPlayerTo(0); setAudioStatus(AudioContentView.STATUS_ENDED); } }); view.setProgressChangeListener(true); view.prepare(audio.getPlayer().getDuration()); if (audio.isPlaying()) { startMediaPlayerUpdater(view); view.play(); } else { view.pause(); } } } @Override public void onUnbind(long messageId, AudioContentViewControl view) { AudioFragment audio = findAudioFragment(); if (audio != null && audio.getMessageId() == messageId) { mAudioControl = null; MediaPlayer player = audio.getPlayer(); player.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { getAudioFragment().seekPlayerTo(0); setAudioStatus(AudioContentView.STATUS_ENDED); } }); view.setProgressChangeListener(false); if (!MessagesProvider.exists(getActivity(), messageId)) { resetAudio(view); } else { stopMediaPlayerUpdater(); } } } @Override public boolean isPlaying() { AudioFragment audio = findAudioFragment(); return audio != null && audio.isPlaying(); } @Override public void seekTo(int position) { AudioFragment audio = findAudioFragment(); if (audio != null) audio.seekPlayerTo(position); } private void startMediaPlayerUpdater(final AudioContentViewControl view) { updatePosition(view); mMediaPlayerUpdater = new Runnable() { @Override public void run() { updatePosition(view); mHandler.postDelayed(this, 100); } }; mHandler.postDelayed(mMediaPlayerUpdater, 100); } private void stopMediaPlayerUpdater() { if (mMediaPlayerUpdater != null) { mHandler.removeCallbacks(mMediaPlayerUpdater); mMediaPlayerUpdater = null; } } /** The conversation list query handler. */ private static final class MessageListQueryHandler extends AsyncQueryHandler { private WeakReference<ComposeMessageFragment> mParent; private boolean mCancel; private long mLastId; public MessageListQueryHandler(ComposeMessageFragment parent) { super(parent.getActivity().getApplicationContext().getContentResolver()); mParent = new WeakReference<>(parent); } @Override public synchronized void startQuery(int token, Object cookie, Uri uri, String[] projection, String selection, String[] selectionArgs, String orderBy) { mCancel = false; super.startQuery(token, cookie, uri, projection, selection, selectionArgs, orderBy); } @Override protected synchronized void onQueryComplete(int token, Object cookie, Cursor cursor) { final ComposeMessageFragment parent = mParent.get(); if (parent == null || cursor == null || parent.isFinishing() || mCancel) { // close cursor - if any if (cursor != null) cursor.close(); mCancel = false; if (parent != null) { parent.unregisterPeerObserver(); parent.mListAdapter.changeCursor(null); } return; } switch (token) { case MESSAGE_LIST_QUERY_TOKEN: // no messages to show - exit if (cursor.getCount() == 0 && (parent.mConversation == null || // no draft (parent.mConversation.getDraft() == null && // no subscription request parent.mConversation.getRequestStatus() != Threads.REQUEST_WAITING && // no text in compose entry parent.mComposer.getText().length() == 0))) { Log.i(TAG, "no data to view - exit"); // close conversation parent.closeConversation(); } else { // first query - use last id of this new cursor if (cursor.getCount() > 0) { cursor.moveToFirst(); mLastId = Conversation.getMessageId(cursor); } // save reloading status for next time Bundle args = parent.myArguments(); // see if we have to scroll to a specific message int newSelectionPos = -1; if (args != null && !args.getBoolean(ComposeMessage.EXTRA_RELOADING)) { 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; } } } args.putBoolean(ComposeMessage.EXTRA_RELOADING, true); } parent.mListAdapter.changeCursor(cursor); if (newSelectionPos >= 0) { // +1 is for the header view final int pos = newSelectionPos + 1; parent.getListView().post(new Runnable() { @Override public void run() { parent.scrollToPosition(pos); } }); } if (newSelectionPos < 0 && cursor.getCount() >= MESSAGE_PAGE_SIZE) parent.showHeaderView(); parent.getActivity().setProgressBarIndeterminateVisibility(false); parent.updateUI(); } break; case MESSAGE_PAGE_QUERY_TOKEN: if (cursor.getCount() > 0) { int newSelectionPos = -1; // there is no more data after this page if (cursor.getCount() < MESSAGE_PAGE_SIZE) parent.hideHeaderView(); // save last id of this new cursor cursor.moveToFirst(); mLastId = Conversation.getMessageId(cursor); // join with the old cursor (if any) Cursor oldCursor = parent.mListAdapter.getCursor(); if (oldCursor != null) { // the new selection will be the next item after this new cursor newSelectionPos = cursor.getCount(); cursor = new MergeCursor(new Cursor[] { cursor, oldCursor }); } parent.mListAdapter.swapCursor(cursor); if (newSelectionPos >= 0) parent.getListView().setSelection(newSelectionPos); parent.getActivity().setProgressBarIndeterminateVisibility(false); parent.updateUI(); } else { // this happens when the first page is exactly PAGE_SIZE big parent.hideHeaderView(); } parent.enableHeaderView(true); break; case CONVERSATION_QUERY_TOKEN: if (cursor.moveToFirst()) { parent.mConversation = Conversation.createFromCursor(parent.getActivity(), cursor); parent.onConversationCreated(); } cursor.close(); parent.startMessagesQuery(); break; default: Log.e(TAG, "onQueryComplete called with unknown token " + token); } } public synchronized void abort() { mCancel = true; mLastId = 0; cancelOperation(MESSAGE_LIST_QUERY_TOKEN); cancelOperation(CONVERSATION_QUERY_TOKEN); cancelOperation(MESSAGE_PAGE_QUERY_TOKEN); } public long getLastId() { return mLastId; } } }