/*
* 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.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.afollestad.materialdialogs.DialogAction;
import com.afollestad.materialdialogs.MaterialDialog;
import org.jivesoftware.smack.util.StringUtils;
import android.app.Activity;
import android.app.SearchManager;
import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.FragmentTransaction;
import android.support.v4.view.MenuItemCompat;
import android.support.v7.widget.SearchView;
import android.text.TextUtils;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.ListAdapter;
import android.widget.Toast;
import org.kontalk.R;
import org.kontalk.authenticator.Authenticator;
import org.kontalk.data.Conversation;
import org.kontalk.provider.KontalkGroupCommands;
import org.kontalk.provider.MessagesProvider;
import org.kontalk.provider.MyMessages;
import org.kontalk.provider.MyMessages.Threads;
import org.kontalk.service.msgcenter.MessageCenterService;
import org.kontalk.sync.Syncer;
import org.kontalk.ui.adapter.ConversationListAdapter;
import org.kontalk.ui.prefs.HelpPreference;
import org.kontalk.ui.prefs.PreferencesActivity;
import org.kontalk.util.MessageUtils;
import org.kontalk.util.Preferences;
import org.kontalk.util.XMPPUtils;
/**
* The conversations list activity.
*
* Layout is a sliding pane holding the conversation list as primary view and the contact list as
* browser side view.
*
* @author Daniele Ricci
*/
public class ConversationsActivity extends MainActivity
implements ComposeMessageParent {
public static final String TAG = ConversationsActivity.class.getSimpleName();
/** An intent extra for storing an ACTION_SEND intent from {@link ComposeMessage}. */
public static final String EXTRA_SEND_INTENT = "org.kontalk.SEND_INTENT";
private ConversationListFragment mFragment;
/** Search menu item. */
private MenuItem mSearchMenu;
private MenuItem mDeleteAllMenu;
/** Offline mode menu item. */
private MenuItem mOfflineMenu;
private static final int REQUEST_CONTACT_PICKER = 7720;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.conversations_screen);
setupToolbar(false, false);
mFragment = (ConversationListFragment) getSupportFragmentManager()
.findFragmentById(R.id.fragment_conversation_list);
if (!afterOnCreate())
handleIntent(getIntent());
}
/** Called when a new intent is sent to the activity (if already started). */
@Override
protected void onNewIntent(Intent intent) {
handleIntent(intent);
ConversationListFragment fragment = getListFragment();
fragment.startQuery();
}
protected boolean handleIntent(Intent intent) {
if (super.handleIntent(intent))
return true;
if (intent != null) {
String action = intent.getAction();
// this is for intents coming from the world, forwarded by ComposeMessage
boolean actionView = Intent.ACTION_VIEW.equals(action);
if (actionView || ComposeMessage.ACTION_VIEW_USERID.equals(action)) {
Uri uri = null;
if (actionView) {
Cursor c = getContentResolver().query(intent.getData(),
new String[]{Syncer.DATA_COLUMN_PHONE},
null, null, null);
if (c.moveToFirst()) {
String phone = c.getString(0);
String userJID = XMPPUtils.createLocalJID(this,
MessageUtils.sha1(phone));
uri = Threads.getUri(userJID);
}
c.close();
}
else {
uri = intent.getData();
}
if (uri != null)
openConversation(uri);
}
else if (ComposeMessage.ACTION_VIEW_CONVERSATION.equals(action)) {
Uri uri = intent.getData();
if (uri != null) {
long threadId = ContentUris.parseId(uri);
if (threadId >= 0)
openConversation(threadId);
}
}
}
return true;
}
private void processSendIntent(Intent sendIntent) {
AbstractComposeFragment f = getCurrentConversation();
SendIntentReceiver.processSendIntent(this, sendIntent, f);
}
@Override
public boolean onSearchRequested() {
ConversationListFragment fragment = getListFragment();
ListAdapter list = fragment.getListAdapter();
// no data found
if (list == null || list.getCount() == 0)
return false;
toggleSearch();
return false;
}
private void toggleSearch() {
if (mSearchMenu != null) {
if (MenuItemCompat.isActionViewExpanded(mSearchMenu))
MenuItemCompat.collapseActionView(mSearchMenu);
else
MenuItemCompat.expandActionView(mSearchMenu);
}
}
@Override
public void onBackPressed() {
if (mFragment != null && mFragment.isActionMenuOpen()) {
mFragment.closeActionMenu();
return;
}
AbstractComposeFragment f = getCurrentConversation();
if (f == null || !f.tryHideEmojiDrawer()) {
super.onBackPressed();
}
}
@Override
public void onResume() {
super.onResume();
// set title for offline mode
setOfflineModeTitle();
final Context context = getApplicationContext();
new Thread(new Runnable() {
@Override
public void run() {
// mark all messages as old
MessagesProvider.markAllThreadsAsOld(context);
// update notification
MessagingNotification.updateMessagesNotification(context, false);
}
}).start();
if (Authenticator.getDefaultAccount(this) == null) {
NumberValidation.start(this);
finish();
}
else {
// hold message center
MessageCenterService.hold(this, true);
// since we have the conversation list open, we're going to disable notifications
// no need to notify the user twice
MessagingNotification.disable();
}
updateOffline();
}
@Override
protected void onResumeFragments() {
super.onResumeFragments();
Intent intent = getIntent();
if (intent != null) {
Intent sendIntent = getIntent().getParcelableExtra(EXTRA_SEND_INTENT);
if (sendIntent != null) {
// handle the share intent sent from ComposeMessage
processSendIntent(sendIntent);
// clear the intent
intent.removeExtra(EXTRA_SEND_INTENT);
}
}
}
@Override
protected void onPause() {
super.onPause();
// release message center
MessageCenterService.release(this);
// enable notifications again
MessagingNotification.enable();
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
// contact chooser
if (requestCode == REQUEST_CONTACT_PICKER) {
if (resultCode == Activity.RESULT_OK) {
ArrayList<Uri> uris;
Uri uri = data.getData();
if (uri != null) {
openConversation(uri);
}
else if ((uris = data.getParcelableArrayListExtra("org.kontalk.contacts")) != null) {
startGroupChat(uris);
}
}
}
else {
super.onActivityResult(requestCode, resultCode, data);
}
}
private AbstractComposeFragment getCurrentConversation() {
return (AbstractComposeFragment) getSupportFragmentManager()
.findFragmentById(R.id.fragment_compose_message);
}
private void startGroupChat(List<Uri> users) {
String selfJid = Authenticator.getSelfJID(this);
String groupId = StringUtils.randomString(20);
String groupJid = KontalkGroupCommands.createGroupJid(groupId, selfJid);
// ensure no duplicates
Set<String> usersList = new HashSet<>();
for (Uri uri : users) {
String member = uri.getLastPathSegment();
// exclude ourselves
if (!member.equalsIgnoreCase(selfJid))
usersList.add(member);
}
if (usersList.size() > 0) {
askGroupSubject(usersList, groupJid);
}
}
private void askGroupSubject(final Set<String> usersList, final String groupJid) {
new MaterialDialog.Builder(this)
.title(R.string.title_group_subject)
.positiveText(android.R.string.ok)
.negativeText(android.R.string.cancel)
.input(null, null, true, new MaterialDialog.InputCallback() {
@Override
public void onInput(@NonNull MaterialDialog dialog, CharSequence input) {
String title = !TextUtils.isEmpty(input) ? input.toString() : null;
Context ctx = ConversationsActivity.this;
String[] users = usersList.toArray(new String[usersList.size()]);
long groupThreadId = Conversation.initGroupChat(ctx,
groupJid, title, users, "");
// store create group command to outbox
// NOTE: group chats can currently only be created with chat encryption enabled
boolean encrypted = Preferences.getEncryptionEnabled(ctx);
String msgId = MessageCenterService.messageId();
Uri cmdMsg = KontalkGroupCommands.createGroup(ctx,
groupThreadId, groupJid, users, msgId, encrypted);
// TODO check for null
// send create group command now
MessageCenterService.createGroup(ConversationsActivity.this, groupJid, title,
users, encrypted, ContentUris.parseId(cmdMsg), msgId);
// load the new conversation
openConversation(Threads.getUri(groupJid), true);
}
})
.inputRange(0, MyMessages.Groups.GROUP_SUBJECT_MAX_LENGTH)
.show();
}
public void setOfflineModeTitle() {
setTitle(MessageCenterService.isOfflineMode(this));
}
public void setTitle(boolean offline) {
setTitle(offline ? R.string.app_name_offline : R.string.app_name);
}
public ConversationListFragment getListFragment() {
return mFragment;
}
public boolean isDualPane() {
return findViewById(R.id.fragment_compose_message) != null;
}
public void showContactPicker(boolean multiselect) {
// TODO one day it will be like this
// Intent i = new Intent(Intent.ACTION_PICK, Users.CONTENT_URI);
Intent i = new Intent(this, ContactsListActivity.class);
i.putExtra(ContactsListActivity.MODE_MULTI_SELECT, multiselect);
startActivityForResult(i, REQUEST_CONTACT_PICKER);
}
@Override
public void setTitle(CharSequence title, CharSequence subtitle) {
// nothing
}
@Override
public void setUpdatingSubtitle() {
// nothing
}
/** For tablets. */
@Override
public void loadConversation(long threadId, boolean creatingGroup) {
openConversation(threadId, creatingGroup);
}
/** For tablets. */
@Override
public void loadConversation(Uri threadUri) {
openConversation(threadUri, false);
}
public void openConversation(Conversation conv, int position) {
if (isDualPane()) {
mFragment.getListView().setItemChecked(position, true);
// get the old fragment
AbstractComposeFragment f = getCurrentConversation();
// check if we are replacing the same fragment
Conversation oldConv = (f != null ? f.getConversation() : null);
if (oldConv == null || !oldConv.getRecipient().equals(conv.getRecipient())) {
f = AbstractComposeFragment.fromConversation(this, conv, false);
// Execute a transaction, replacing any existing fragment
// with this one inside the frame.
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.fragment_compose_message, f);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
ft.commit();
}
} else {
Intent i = ComposeMessage.fromConversation(this, conv);
startActivity(i);
}
}
void openConversation(Uri threadUri) {
openConversation(threadUri, false);
}
void openConversation(Uri threadUri, boolean creatingGroup) {
if (isDualPane()) {
// load conversation
String userId = threadUri.getLastPathSegment();
Conversation conv = Conversation.loadFromUserId(this, userId);
// get the old fragment
AbstractComposeFragment f = getCurrentConversation();
// check if we are replacing the same fragment
Conversation oldConv = (f != null ? f.getConversation() : null);
if (oldConv == null || conv == null || !oldConv.getRecipient().equals(conv.getRecipient())) {
if (conv == null)
f = AbstractComposeFragment.fromUserId(this, userId, creatingGroup);
else
f = AbstractComposeFragment.fromConversation(this, conv, creatingGroup);
// Execute a transaction, replacing any existing fragment
// with this one inside the frame.
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.fragment_compose_message, f);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
ft.commitAllowingStateLoss();
}
}
else {
Intent i = ComposeMessage.fromUserId(this, threadUri.getLastPathSegment(), creatingGroup);
if (i != null)
startActivity(i);
else
Toast.makeText(this, R.string.contact_not_registered, Toast.LENGTH_LONG)
.show();
}
}
private void openConversation(long threadId) {
openConversation(threadId, false);
}
private void openConversation(long threadId, boolean creatingGroup) {
if (isDualPane()) {
// load conversation
Conversation conv = Conversation.loadFromId(this, threadId);
if (conv == null)
return;
// get the old fragment
AbstractComposeFragment f = getCurrentConversation();
// check if we are replacing the same fragment
Conversation oldConv = (f != null ? f.getConversation() : null);
if (oldConv == null || !oldConv.getRecipient().equals(conv.getRecipient())) {
f = AbstractComposeFragment.fromConversation(this, conv, creatingGroup);
// Execute a transaction, replacing any existing fragment
// with this one inside the frame.
FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
ft.replace(R.id.fragment_compose_message, f);
ft.setTransition(FragmentTransaction.TRANSIT_FRAGMENT_OPEN);
ft.commitAllowingStateLoss();
}
}
else {
startActivity(ComposeMessage.fromConversation(this, threadId, creatingGroup));
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.conversation_list_menu, menu);
// search
mSearchMenu = menu.findItem(R.id.menu_search);
SearchView searchView = (SearchView) MenuItemCompat.getActionView(mSearchMenu);
SearchManager searchManager = (SearchManager) getSystemService(Context.SEARCH_SERVICE);
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
// LayoutParams.MATCH_PARENT does not work, use a big value instead
searchView.setMaxWidth(1000000);
mDeleteAllMenu = menu.findItem(R.id.menu_delete_all);
// offline mode
mOfflineMenu = menu.findItem(R.id.menu_offline);
// trigger manually
onDatabaseChanged();
updateOffline();
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch(item.getItemId()) {
case android.R.id.home:
// TODO @deprecated
onBackPressed();
return true;
case R.id.menu_status:
StatusActivity.start(this);
return true;
case R.id.menu_offline:
final Context ctx = this;
final boolean currentMode = Preferences.getOfflineMode();
if (!currentMode && !Preferences.getOfflineModeUsed()) {
// show offline mode warning
new MaterialDialog.Builder(ctx)
.content(R.string.message_offline_mode_warning)
.positiveText(android.R.string.ok)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
Preferences.setOfflineModeUsed();
switchOfflineMode();
}
})
.negativeText(android.R.string.cancel)
.show();
}
else {
switchOfflineMode();
}
return true;
case R.id.menu_delete_all:
deleteAll();
return true;
case R.id.menu_mykey:
launchMyKey();
return true;
case R.id.menu_donate:
launchDonate();
return true;
case R.id.menu_help:
launchHelp();
return true;
case R.id.menu_settings: {
PreferencesActivity.start(this);
return true;
}
}
return super.onOptionsItemSelected(item);
}
/** Updates various UI elements after a database change. */
void onDatabaseChanged() {
boolean visible = mFragment.hasListItems();
if (mSearchMenu != null) {
mSearchMenu.setEnabled(visible).setVisible(visible);
}
// if it's null it hasn't gone through onCreateOptionsMenu() yet
if (mSearchMenu != null) {
mSearchMenu.setEnabled(visible).setVisible(visible);
mDeleteAllMenu.setEnabled(visible).setVisible(visible);
}
// for tablet interface
// select the current conversation item
AbstractComposeFragment f = getCurrentConversation();
if (f != null) {
int position = ((ConversationListAdapter) mFragment.getListAdapter())
.getItemPosition(f.getUserId());
mFragment.getListView().setItemChecked(position, true);
}
}
/** Updates offline mode menu. */
private void updateOffline() {
if (mOfflineMenu != null) {
boolean offlineMode = Preferences.getOfflineMode();
// set menu
int icon = (offlineMode) ? R.drawable.ic_menu_online :
R.drawable.ic_menu_offline;
int title = (offlineMode) ? R.string.menu_online : R.string.menu_offline;
mOfflineMenu.setIcon(icon);
mOfflineMenu.setTitle(title);
// set window title
setTitle(offlineMode);
}
}
void switchOfflineMode() {
boolean currentMode = Preferences.getOfflineMode();
Preferences.switchOfflineMode(this);
updateOffline();
// notify the user about the change
int text = (currentMode) ? R.string.going_online : R.string.going_offline;
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
}
private void deleteAll() {
new MaterialDialog.Builder(this)
.content(R.string.confirm_will_delete_all)
.positiveText(android.R.string.ok)
.positiveColorRes(R.color.button_danger)
.onPositive(new MaterialDialog.SingleButtonCallback() {
@Override
public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) {
Conversation.deleteAll(ConversationsActivity.this, dialog.isPromptCheckBoxChecked());
MessagingNotification.updateMessagesNotification(getApplicationContext(), false);
}
})
.checkBoxPromptRes(R.string.delete_threads_leave_any_groups, false, null)
.negativeText(android.R.string.cancel)
.show();
}
private void launchDonate() {
Intent i = new Intent(this, AboutActivity.class);
i.setAction(AboutActivity.ACTION_DONATION);
startActivity(i);
}
private void launchMyKey() {
Intent i = new Intent(this, MyKeyActivity.class);
startActivity(i);
}
private void launchHelp() {
HelpPreference.openHelp(this);
}
}