org.kontalk.ui.ConversationsActivity.java Source code

Java tutorial

Introduction

Here is the source code for org.kontalk.ui.ConversationsActivity.java

Source

/*
 * 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);
    }

}