/*
* Kontalk Android client
* Copyright (C) 2017 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.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Pattern;
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.Presence;
import org.jivesoftware.smackx.chatstates.ChatState;
import org.jxmpp.util.XmppStringUtils;
import android.annotation.TargetApi;
import android.app.Activity;
import android.content.ActivityNotFoundException;
import android.content.AsyncQueryHandler;
import android.content.BroadcastReceiver;
import android.content.ClipData;
import android.content.ContentUris;
import android.content.Context;
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.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.ContextCompat;
import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.view.ActionMode;
import android.text.ClipboardManager;
import android.text.TextUtils;
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.widget.ImageView;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import org.kontalk.Kontalk;
import org.kontalk.Log;
import org.kontalk.R;
import org.kontalk.authenticator.Authenticator;
import org.kontalk.crypto.Coder;
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.GroupCommandComponent;
import org.kontalk.message.ImageComponent;
import org.kontalk.message.MessageComponent;
import org.kontalk.message.TextComponent;
import org.kontalk.message.VCardComponent;
import org.kontalk.provider.MessagesProviderUtils;
import org.kontalk.provider.MyMessages.Messages;
import org.kontalk.provider.MyMessages.Threads;
import org.kontalk.provider.MyMessages.Threads.Conversations;
import org.kontalk.reporting.ReportingManager;
import org.kontalk.service.DownloadService;
import org.kontalk.service.msgcenter.MessageCenterService;
import org.kontalk.ui.adapter.MessageListAdapter;
import org.kontalk.ui.view.AttachmentRevealFrameLayout;
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 static android.content.res.Configuration.KEYBOARDHIDDEN_NO;
/**
* Abstract message composing fragment.
* @author Daniele Ricci
* @author Andrea Cappelli
*/
public abstract class AbstractComposeFragment extends ActionModeListFragment implements
ComposerListener, View.OnLongClickListener,
// TODO these two interfaces should be handled by an inner class
AudioDialog.AudioDialogListener, AudioPlayerControl,
MultiChoiceModeListener {
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 = 1;
private static final int SELECT_ATTACHMENT_CONTACT = 2;
private static final int SELECT_ATTACHMENT_PHOTO = 3;
private static final int REQUEST_INVITE_USERS = 4;
// use this as base for request codes for child classes
protected static final int REQUEST_FIRST_CHILD = 100;
protected 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 AttachmentRevealFrameLayout mAttachmentContainer;
protected ComposerBar mComposer;
MessageListQueryHandler mQueryHandler;
MessageListAdapter mListAdapter;
/** Header view for the list view: "previous messages" button. */
private View mHeaderView;
private View mNextPageButton;
private TextView mStatusText;
private MenuItem mDeleteThreadMenu;
private MenuItem mToggleEncryptionMenu;
/** The thread id. */
long threadId = -1;
protected Conversation mConversation;
protected String mUserName;
/** Available resources. */
protected Set<String> mAvailableResources = new HashSet<>();
/** Media player stuff. */
private int mMediaPlayerStatus = AudioContentView.STATUS_IDLE;
private Handler mHandler;
private Runnable mMediaPlayerUpdater;
private AudioContentViewControl mAudioControl;
private AudioFragment mAudioFragment;
/** Audio recording dialog. */
private AudioDialog mAudioDialog;
private PeerObserver mPeerObserver;
private File mCurrentPhoto;
protected LocalBroadcastManager mLocalBroadcastManager;
private BroadcastReceiver mPresenceReceiver;
private boolean mOfflineModeWarned;
protected CharSequence mCurrentStatus;
private int mCheckedItemCount;
/** Returns a new fragment instance from a picked contact. */
public static AbstractComposeFragment fromUserId(Context context, String userId, boolean creatingGroup) {
AbstractComposeFragment 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));
// non existing group threads can't exist, so no reason to use creatingGroup
f.setArguments(args);
return f;
}
return fromConversation(context, conv, creatingGroup);
}
/** Returns a new fragment instance from a {@link Conversation} instance. */
public static AbstractComposeFragment fromConversation(Context context,
Conversation conv, boolean creatingGroup) {
return fromConversation(context, conv.getThreadId(), conv.isGroupChat(), creatingGroup);
}
/** Returns a new fragment instance from a thread ID. */
private static AbstractComposeFragment fromConversation(Context context,
long threadId, boolean group, boolean creatingGroup) {
AbstractComposeFragment f = group ?
new GroupMessageFragment() : new ComposeMessageFragment();
Bundle args = new Bundle();
args.putString("action", ComposeMessage.ACTION_VIEW_CONVERSATION);
args.putParcelable("data",
ContentUris.withAppendedId(Conversations.CONTENT_URI, threadId));
args.putBoolean(ComposeMessage.EXTRA_CREATING_GROUP, creatingGroup);
f.setArguments(args);
return f;
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// setListAdapter() is post-poned
ListView list = getListView();
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, null, false);
// 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(Context context) {
super.onAttach(context);
mLocalBroadcastManager = LocalBroadcastManager.getInstance(context);
}
@Override
public void onDetach() {
super.onDetach();
mLocalBroadcastManager = null;
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
mComposer.onKeyboardStateChanged(newConfig.keyboardHidden == KEYBOARDHIDDEN_NO);
}
@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();
}
};
@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 deleteMenu = menu.findItem(R.id.menu_delete);
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);
// initial status
deleteMenu.setVisible(true);
retryMenu.setVisible(false);
shareMenu.setVisible(false);
copyTextMenu.setVisible(false);
detailsMenu.setVisible(false);
openMenu.setVisible(false);
dlMenu.setVisible(false);
cancelDlMenu.setVisible(false);
boolean singleItem = (mCheckedItemCount == 1);
if (singleItem) {
CompositeMessage msg = getCheckedItem();
// group command can't be deleted or have details
if (msg.hasComponent(GroupCommandComponent.class)) {
deleteMenu.setVisible(false);
}
else {
// 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 = msg.getComponent(AttachmentComponent.class);
TextComponent text = 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);
}
}
}
}
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 = 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_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 MaterialDialog.Builder(getActivity())
.content(R.string.confirm_will_delete_messages)
.positiveText(android.R.string.ok)
.positiveColorRes(R.color.button_danger)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
Context ctx = getActivity();
for (int i = 0, c = getListView().getCount()+getListView().getHeaderViewsCount(); i < c; ++i) {
if (checked.get(i)) {
Cursor cursor = (Cursor) getListView().getItemAtPosition(i);
// skip group command messages
if (!GroupCommandComponent.isCursor(cursor))
CompositeMessage.deleteFromCursor(ctx, cursor);
}
}
mListAdapter.notifyDataSetChanged();
}
})
.negativeText(android.R.string.cancel)
.show();
}
private void initAttachmentView()
{
View view = getView();
mAttachmentContainer = (AttachmentRevealFrameLayout) view.findViewById(R.id.attachment_container);
View.OnClickListener hideAttachmentListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
hideAttachmentView();
}
};
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();
hideAttachmentView();
}
});
view.findViewById(R.id.attach_gallery).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
selectGalleryAttachment();
hideAttachmentView();
}
});
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();
hideAttachmentView();
}
});
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();
hideAttachmentView();
}
});
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);
try {
// TODO convert to thread (?)
offlineModeWarning();
final Context context = getContext();
final Conversation conv = mConversation;
Uri newMsg = Kontalk.getMessagesController(context)
.sendBinaryMessage(conv, uri, mime, media, klass);
// update thread id from the inserted message
if (threadId <= 0) {
threadId = MessagesProviderUtils.getThreadByMessage(getContext(), newMsg);
if (threadId > 0) {
// we can run it here because progress=false
startQuery();
}
else {
Log.v(TAG, "no data - cannot start query for this composer");
}
}
}
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) {
ReportingManager.logException(e);
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 {
final Context context = getContext();
final Conversation conv = mConversation;
Uri newMsg = Kontalk.getMessagesController(context)
.sendTextMessage(conv, mText);
// update thread id from the inserted message
if (threadId <= 0) {
threadId = MessagesProviderUtils.getThreadByMessage(context, newMsg);
if (threadId > 0) {
startQuery();
}
else {
Log.v(TAG, "no data - cannot start query for this composer");
}
}
}
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) {
ReportingManager.logException(e);
getActivity().runOnUiThread(new Runnable() {
public void run() {
Toast.makeText(getActivity(),
R.string.err_store_message_failed,
Toast.LENGTH_LONG).show();
}
});
}
}
}
/** 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();
}
}
/** Sends an inactive chat state message. */
public abstract boolean sendInactive();
protected abstract void onInflateOptionsMenu(Menu menu, MenuInflater inflater);
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
onInflateOptionsMenu(menu, inflater);
mDeleteThreadMenu = menu.findItem(R.id.delete_thread);
mToggleEncryptionMenu = menu.findItem(R.id.toggle_encryption);
updateUI();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// action mode is active - no processing
if (isActionModeActive())
return true;
switch (item.getItemId()) {
case R.id.menu_attachment:
toggleAttachmentView();
return true;
case R.id.delete_thread:
if (threadId > 0)
deleteThread();
return true;
case R.id.invite_group:
addUsers();
return true;
case R.id.toggle_encryption:
toggleEncryption();
return true;
}
return super.onOptionsItemSelected(item);
}
private void toggleEncryption() {
if (mConversation.isEncryptionEnabled()) {
new MaterialDialog.Builder(getActivity())
.title(R.string.title_disable_encryption)
.content(R.string.msg_disable_encryption)
.positiveText(R.string.menu_disable_encryption)
.positiveColorRes(R.color.button_danger)
.negativeText(android.R.string.cancel)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog materialDialog, @NonNull DialogAction dialogAction) {
setEncryption(false);
updateUI();
}
})
.show();
}
else {
setEncryption(true);
updateUI();
}
}
void setEncryption(boolean encryption) {
if (mConversation != null)
mConversation.setEncryptionEnabled(encryption);
}
@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 = 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, getDecodedPeer(msg));
MaterialDialog.Builder builder = new MaterialDialog.Builder(getActivity())
.title(R.string.title_file_info)
.content(message)
.negativeText(android.R.string.cancel)
.cancelable(true);
if (!DownloadService.isQueued(attachment.getFetchUrl())) {
MaterialDialog.SingleButtonCallback startDL = new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
// start file download
startDownload(msg);
}
};
builder.positiveText(R.string.download)
.onPositive(startDL);
}
else {
MaterialDialog.SingleButtonCallback stopDL = new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
// cancel file download
stopDownload(msg);
}
};
builder.positiveText(R.string.download_cancel)
.onPositive(stopDL);
}
builder.show();
}
}
else {
item.onClick();
}
}
else {
super.onListItemClick(listView, view, position, id);
}
}
private void startDownload(CompositeMessage msg) {
AttachmentComponent attachment = msg
.getComponent(AttachmentComponent.class);
if (attachment != null && attachment.getFetchUrl() != null) {
DownloadService.start(getContext(), msg.getDatabaseId(),
msg.getSender(), attachment.getMime(), msg.getTimestamp(),
attachment.getSecurityFlags() != Coder.SECURITY_CLEARTEXT,
attachment.getFetchUrl());
}
else {
// corrupted message :(
Toast.makeText(getActivity(), R.string.err_attachment_corrupted,
Toast.LENGTH_LONG).show();
}
}
private void stopDownload(CompositeMessage msg) {
AttachmentComponent attachment = msg.getComponent(AttachmentComponent.class);
if (attachment != null && attachment.getFetchUrl() != null) {
DownloadService.abort(getContext(),
Uri.parse(attachment.getFetchUrl()));
}
}
private void openFile(CompositeMessage msg) {
AttachmentComponent attachment = msg.getComponent(AttachmentComponent.class);
if (attachment != null) {
Intent i = new Intent(Intent.ACTION_VIEW);
i.setDataAndType(attachment.getLocalUri(), attachment.getMime());
try {
startActivity(i);
}
catch (ActivityNotFoundException e) {
Toast.makeText(getActivity(), R.string.chooser_error_no_app,
Toast.LENGTH_LONG).show();
}
}
}
private void chooseContact() {
// TODO one day it will be like this
// Intent i = new Intent(Intent.ACTION_PICK, Users.CONTENT_URI);
Intent i = new Intent(getContext(), ContactsListActivity.class);
i.putExtra(ContactsListActivity.MODE_MULTI_SELECT, true);
i.putExtra(ContactsListActivity.MODE_ADD_USERS, true);
startActivityForResult(i, REQUEST_INVITE_USERS);
}
boolean tryHideAttachmentView() {
return tryHideAttachmentView(false);
}
boolean tryHideAttachmentView(boolean instant) {
if (isAttachmentViewVisible()) {
mAttachmentContainer.hide(instant);
return true;
}
return false;
}
private boolean isAttachmentViewVisible() {
return mAttachmentContainer.getVisibility() == View.VISIBLE &&
!mAttachmentContainer.isClosing();
}
void hideAttachmentView() {
mAttachmentContainer.hide();
}
/** Show or hide the attachment selector. */
private void toggleAttachmentView() {
mComposer.forceHideKeyboard();
mAttachmentContainer.toggle();
}
/** Starts an activity for shooting a picture. */
void selectPhotoAttachment() {
try {
// check if camera is available
final PackageManager packageManager = getActivity().getPackageManager();
final Intent intent = SystemUtils.externalIntent(MediaStore.ACTION_IMAGE_CAPTURE);
List<ResolveInfo> list =
packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
if (list.size() <= 0) throw new UnsupportedOperationException();
mCurrentPhoto = MediaStorage.getOutgoingPhotoFile();
Uri uri = Uri.fromFile(mCurrentPhoto);
Intent take = SystemUtils.externalIntent(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)
void selectGalleryAttachment() {
boolean useSAF = MediaStorage.isStorageAccessFrameworkAvailable();
Intent pictureIntent = createGalleryIntent(useSAF);
try {
startActivityForResult(pictureIntent, SELECT_ATTACHMENT_OPENABLE);
}
catch (ActivityNotFoundException e1) {
try {
if (useSAF) {
// try direct file system access
pictureIntent = createGalleryIntent(false);
startActivityForResult(pictureIntent, SELECT_ATTACHMENT_OPENABLE);
}
else {
// simulate error
throw new ActivityNotFoundException("gallery");
}
}
catch (ActivityNotFoundException e2) {
Toast.makeText(getActivity(), R.string.chooser_error_no_gallery_app,
Toast.LENGTH_LONG).show();
}
}
}
@TargetApi(Build.VERSION_CODES.KITKAT)
private Intent createGalleryIntent(boolean useSAF) {
Intent intent;
if (!useSAF) {
intent = SystemUtils.externalIntent(Intent.ACTION_GET_CONTENT)
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
}
else {
intent = SystemUtils.externalIntent(Intent.ACTION_OPEN_DOCUMENT);
}
return intent
.addCategory(Intent.CATEGORY_OPENABLE)
.setType("image/*")
.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
}
/** Starts activity for a vCard attachment from a contact. */
void selectContactAttachment() {
try {
Intent i = SystemUtils.externalIntent(Intent.ACTION_PICK, Contacts.CONTENT_URI);
startActivityForResult(i, SELECT_ATTACHMENT_CONTACT);
}
catch (ActivityNotFoundException e) {
// no contacts app found (crap device eh?)
Toast.makeText(getActivity(),
R.string.err_no_contacts_app,
Toast.LENGTH_LONG).show();
}
}
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() {
FragmentManager fm = getFragmentManager();
if (fm != null) {
AudioFragment found = (AudioFragment) fm.findFragmentByTag("audio");
if (found != null) {
mAudioFragment = found;
}
else {
mAudioFragment = new AudioFragment();
fm.beginTransaction()
.add(mAudioFragment, "audio")
.commit();
}
}
return mAudioFragment;
}
protected abstract void deleteConversation();
private void deleteThread() {
new MaterialDialog.Builder(getActivity())
.content(R.string.confirm_will_delete_thread)
.positiveText(android.R.string.ok)
.positiveColorRes(R.color.button_danger)
.negativeText(android.R.string.cancel)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
deleteConversation();
}
})
.show();
}
void addUsers() {
chooseContact();
}
protected abstract void addUsers(String[] members);
private void retryMessage(CompositeMessage msg) {
MessageCenterService.retryMessage(getContext(), ContentUris.withAppendedId
(Messages.CONTENT_URI, msg.getDatabaseId()), mConversation.isEncryptionEnabled());
}
void scrollToPosition(int position) {
getListView().setSelection(position);
}
private boolean isSearching() {
Bundle args = getArguments();
return args != null && args.getLong(ComposeMessage.EXTRA_MESSAGE, -1) >= 0;
}
protected synchronized void startQuery() {
Conversation.startQuery(mQueryHandler,
CONVERSATION_QUERY_TOKEN, threadId);
// message list query will be started by query handler
}
void startMessagesQuery() {
CompositeMessage.startQuery(mQueryHandler, MESSAGE_LIST_QUERY_TOKEN,
threadId, isSearching() ? 0 : MESSAGE_PAGE_SIZE, 0);
}
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, getDecodedPeer(msg), getDecodedName(msg));
}
/** Returns the phone number of the message sender, if available. */
protected abstract String getDecodedPeer(CompositeMessage msg);
/** Returns the display name of the message sender, if available. */
protected abstract String getDecodedName(CompositeMessage msg);
private void shareMessage(CompositeMessage msg) {
Intent i = null;
AttachmentComponent attachment = msg.getComponent(AttachmentComponent.class);
if (attachment != null) {
i = ComposeMessage.sendMediaMessage(attachment.getLocalUri(),
attachment.getMime());
}
else {
TextComponent txt = 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");
}
protected 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();
}
}
@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 (requestCode == SELECT_ATTACHMENT_PHOTO) {
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];
if (uri == null)
continue;
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) {
Uri vcardUri = null;
// get lookup key
final Cursor c = getContext().getContentResolver()
.query(uri, new String[] { Contacts.LOOKUP_KEY }, null, null, null);
if (c != null) {
try {
if (c.moveToFirst()) {
String lookupKey = c.getString(0);
vcardUri = Uri.withAppendedPath(Contacts.CONTENT_VCARD_URI, lookupKey);
}
}
catch (Exception e) {
Log.w(TAG, "unable to lookup selected contact. Did you grant me the permission?", e);
ReportingManager.logException(e);
}
finally {
c.close();
}
}
if (vcardUri != null) {
sendBinaryMessage(vcardUri, VCardComponent.MIME_TYPE, false, VCardComponent.class);
}
else {
Toast.makeText(getContext(), R.string.err_no_contact,
Toast.LENGTH_LONG).show();
}
}
}
}
// invite user
else if (requestCode == REQUEST_INVITE_USERS) {
if (resultCode == Activity.RESULT_OK) {
ArrayList<Uri> uris;
Uri threadUri = data.getData();
if (threadUri != null) {
String userId = threadUri.getLastPathSegment();
addUsers(new String[] { userId });
}
else if ((uris = data.getParcelableArrayListExtra("org.kontalk.contacts")) != null) {
String[] users = new String[uris.size()];
for (int i = 0; i < users.length; i++)
users[i] = uris.get(i).getLastPathSegment();
addUsers(users);
}
}
}
}
@Override
public void onSaveInstanceState(Bundle out) {
super.onSaveInstanceState(out);
out.putParcelable(Uri.class.getName(), Threads.getUri(getUserId()));
// 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);
}
/** Handles ACTION_VIEW intents. */
protected abstract void handleActionView(Uri uri);
/** Handles ACTION_VIEW_USERID intents: providing the user ID/JID. */
protected abstract boolean handleActionViewConversation(Uri uri, Bundle args);
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);
}
// audio playing
setAudioStatus(savedInstanceState.getInt("mediaPlayerStatus", AudioContentView.STATUS_IDLE));
// audio dialog stuff
mAudioDialog = AudioDialog.onRestoreInstanceState(getActivity(),
savedInstanceState, getAudioFragment(), this);
if (mAudioDialog != null) {
Log.d(TAG, "recreating audio dialog");
mAudioDialog.show();
}
}
else {
args = getArguments();
}
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");
handleActionView(uri);
}
// 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");
if (!handleActionViewConversation(uri, args)) {
getActivity().finish();
return;
}
}
}
// set title if we are autonomous
if (args != null) {
String title = mUserName;
//if (mUserPhone != null) title += " <" + mUserPhone + ">";
setActivityTitle(title, "");
}
// update conversation stuff
if (mConversation != null)
onConversationCreated();
onArgumentsProcessed();
}
protected abstract void onArgumentsProcessed();
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;
}
void processStart() {
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 = getArguments();
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();
}
else {
mConversation = Conversation.createNew(getActivity());
mConversation.setRecipient(getUserId());
onConversationCreated();
}
}
/** Called when the {@link Conversation} object has been created. */
protected void onConversationCreated() {
// restore any draft
mComposer.restoreText(mConversation.getDraft());
if (mConversation.getThreadId() > 0) {
if (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();
}
// subscribe to presence notifications
subscribePresence();
updateUI();
}
/** Called when a presence is received. */
protected abstract void onPresence(String jid, Presence.Type type,
boolean removed, Presence.Mode mode, String fingerprint);
protected abstract void onConnected();
/** Called when the roster has been loaded (ACTION_ROSTER). */
protected abstract void onRosterLoaded();
/** Called when the contact starts typing. */
protected abstract void onStartTyping(String jid);
/** Called when the contact stops typing. */
protected abstract void onStopTyping(String jid);
/** Should return true if the contact is a user ID in the current context. */
protected abstract boolean isUserId(String jid);
private void subscribePresence() {
// TODO this needs serious refactoring
if (mPresenceReceiver == null) {
mPresenceReceiver = new BroadcastReceiver() {
public void onReceive(Context context, Intent intent) {
// activity is terminating
if (getContext() == null)
return;
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 && isUserId(bareFrom)) {
// we handle only (un)available presence stanzas
String type = intent.getStringExtra(MessageCenterService.EXTRA_TYPE);
Presence.Type presenceType = (type != null) ? Presence.Type.fromString(type) : null;
String mode = intent.getStringExtra(MessageCenterService.EXTRA_SHOW);
Presence.Mode presenceMode = (mode != null) ? Presence.Mode.fromString(mode) : null;
String fingerprint = intent.getStringExtra(MessageCenterService.EXTRA_FINGERPRINT);
boolean removed = false;
if (presenceType == Presence.Type.available) {
mAvailableResources.add(from);
}
else if (presenceType == Presence.Type.unavailable) {
removed = mAvailableResources.remove(from);
}
onPresence(from, presenceType, removed, presenceMode, fingerprint);
}
}
else if (MessageCenterService.ACTION_CONNECTED.equals(action)) {
// reset compose sent flag
mComposer.resetCompose();
// reset available resources list
mAvailableResources.clear();
onConnected();
}
else if (MessageCenterService.ACTION_ROSTER_LOADED.equals(action)) {
onRosterLoaded();
}
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 && isUserId(from)) {
if (chatState != null && ChatState.composing.toString().equals(chatState)) {
onStartTyping(from);
}
else {
onStopTyping(from);
}
}
}
}
};
// 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_MESSAGE);
mLocalBroadcastManager.registerReceiver(mPresenceReceiver, filter);
// request connection and roster load status
Context ctx = getActivity();
if (ctx != null) {
MessageCenterService.requestConnectionStatus(ctx);
MessageCenterService.requestRosterStatus(ctx);
}
}
}
private void unsubscribePresence() {
if (mPresenceReceiver != null) {
mLocalBroadcastManager.unregisterReceiver(mPresenceReceiver);
mPresenceReceiver = null;
}
}
protected boolean isWarningVisible(WarningType type) {
Snackbar bar = SnackbarManager.getCurrentSnackbar();
if (bar != null) {
WarningType oldType = (WarningType) bar.getTag();
if (oldType != null && oldType == type)
return true;
}
return false;
}
protected void hideWarning() {
SnackbarManager.dismiss();
}
protected 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(ContextCompat.getColor(context, colorId))
.textColor(ContextCompat.getColor(context, textColorId));
if (listener != null) {
SnackbarManager.show(bar);
}
else {
SnackbarManager.show(bar, (ViewGroup) view.findViewById(R.id.warning_bar));
}
}
protected 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);
}
}
synchronized void unregisterPeerObserver() {
if (mPeerObserver != null) {
Context context = mPeerObserver.mContext;
context.getContentResolver().unregisterContentObserver(mPeerObserver);
mPeerObserver = null;
}
}
private final class PeerObserver extends ContentObserver {
final Context mContext;
PeerObserver(Context context, Handler handler) {
super(handler);
mContext = context;
}
@Override
public void onChange(boolean selfChange) {
Conversation conv = Conversation.loadFromUserId(mContext, getUserId());
if (conv != null) {
mConversation = conv;
threadId = mConversation.getThreadId();
// auto-unregister
unregisterPeerObserver();
}
// fire cursor update
Log.v(TAG, "peer observer active");
processStart();
}
@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(), true);
ComposeMessage activity = getParentActivity();
if (activity == null || !activity.hasLostFocus() || activity.hasWindowFocus()) {
onFocus();
}
}
public void onFocus() {
// resume content watcher
resumeContentListener();
// set notifications on pause
MessagingNotification.setPaused(getUserId());
// we are updating the status now
setActivityStatusUpdating();
// cursor was previously destroyed -- reload everything
processStart();
}
@Override
public void onPause() {
super.onPause();
// unsubcribe presence notifications
unsubscribePresence();
// notify composer bar
mComposer.onPause();
// hide attachment view
tryHideAttachmentView(true);
// 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 &&
!mConversation.isGroupChat()) {
mConversation.delete(false);
}
// update draft
else {
try {
MessagesProviderUtils.updateDraft(getContext(), threadId, text.toString());
}
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
try {
MessagesProviderUtils.insertEmptyThread(getActivity(), getUserId(), text.toString());
}
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
sendInactive();
mComposer.resetCompose();
}
// release message center
MessageCenterService.release(getActivity());
// release audio player
AudioFragment audio = getAudioFragment();
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();
}
void showHeaderView() {
mHeaderView.setVisibility(View.VISIBLE);
}
void hideHeaderView() {
mHeaderView.setVisibility(View.GONE);
}
void enableHeaderView(boolean enabled) {
mNextPageButton.setEnabled(enabled);
}
protected void updateUI() {
boolean threadEnabled = (threadId > 0);
if (mDeleteThreadMenu != null) {
mDeleteThreadMenu.setEnabled(threadEnabled);
}
if (mToggleEncryptionMenu != null) {
Context context = getActivity();
if (context != null) {
if (mConversation != null && Preferences.getEncryptionEnabled(context)) {
boolean encryption = mConversation.isEncryptionEnabled();
mToggleEncryptionMenu
.setVisible(true)
.setEnabled(true)
.setChecked(encryption);
}
else {
mToggleEncryptionMenu
.setVisible(false)
.setEnabled(false)
.setChecked(false);
}
}
}
}
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;
}
protected void setThreadId(long threadId) {
this.threadId = threadId;
}
/** Returns the user id of this conversation. */
public abstract String getUserId();
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() && !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, false, 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();
audio.preparePlayer(audioFile);
// prepare was successful
audio.setMessageId(messageId);
mAudioControl = view;
view.prepare(audio.getPlayerDuration());
audio.seekPlayerTo(view.getPosition());
view.setProgressChangeListener(true);
audio.setOnCompletionListener(new AudioFragment.OnCompletionListener() {
@Override
public void onCompletion(AudioFragment audio) {
stopMediaPlayerUpdater();
view.end();
// this is mainly to get the wake lock released
audio.pausePlaying();
audio.seekPlayerTo(0);
setAudioStatus(AudioContentView.STATUS_ENDED);
}
@Override
public void onAudioFocusLost(AudioFragment audio) {
stopAllSounds();
}
});
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) {
if (getAudioFragment().startPlaying()) {
view.play();
setAudioStatus(AudioContentView.STATUS_PLAYING);
startMediaPlayerUpdater(view);
}
}
private void updatePosition(AudioContentViewControl view) {
// we don't use getElapsedTime() here because it might get moved by seeking
view.updatePosition(getAudioFragment().getPlayerPosition());
}
@Override
public void pauseAudio(AudioContentViewControl view) {
view.pause();
getAudioFragment().pausePlaying();
stopMediaPlayerUpdater();
setAudioStatus(AudioContentView.STATUS_PAUSED);
}
private void resetAudio(AudioContentViewControl view) {
if (view != null) {
stopMediaPlayerUpdater();
view.end();
}
AudioFragment audio = getAudioFragment();
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 = getAudioFragment();
if (audio != null && audio.getMessageId() == messageId) {
mAudioControl = view;
audio.setOnCompletionListener(new AudioFragment.OnCompletionListener() {
@Override
public void onCompletion(AudioFragment audio) {
stopMediaPlayerUpdater();
view.end();
audio.seekPlayerTo(0);
setAudioStatus(AudioContentView.STATUS_ENDED);
}
@Override
public void onAudioFocusLost(AudioFragment audio) {
stopAllSounds();
}
});
view.setProgressChangeListener(true);
view.prepare(audio.getPlayerDuration());
if (audio.isPlaying()) {
startMediaPlayerUpdater(view);
view.play();
}
else {
view.pause();
}
}
}
@Override
public void onUnbind(long messageId, AudioContentViewControl view) {
AudioFragment audio = getAudioFragment();
if (audio != null && audio.getMessageId() == messageId) {
mAudioControl = null;
audio.setOnCompletionListener(new AudioFragment.OnCompletionListener() {
@Override
public void onCompletion(AudioFragment audio) {
audio.seekPlayerTo(0);
setAudioStatus(AudioContentView.STATUS_ENDED);
}
@Override
public void onAudioFocusLost(AudioFragment audio) {
stopAllSounds();
}
});
view.setProgressChangeListener(false);
if (!MessagesProviderUtils.exists(getActivity(), messageId)) {
resetAudio(view);
}
else {
stopMediaPlayerUpdater();
}
}
}
@Override
public boolean isPlaying() {
AudioFragment audio = getAudioFragment();
return audio != null && audio.isPlaying();
}
@Override
public void seekTo(int position) {
AudioFragment audio = getAudioFragment();
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<AbstractComposeFragment> mParent;
private boolean mCancel;
private long mLastId;
MessageListQueryHandler(AbstractComposeFragment 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 AbstractComposeFragment 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 &&
// no group chat
!parent.mConversation.isGroupChat()))) {
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.getArguments();
// 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.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.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;
}
}
}