org.kontalk.ui.ComposeMessage.java Source code

Java tutorial

Introduction

Here is the source code for org.kontalk.ui.ComposeMessage.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.regex.Pattern;

import android.content.ContentUris;
import android.content.Context;
import android.content.Intent;
import android.graphics.Typeface;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.ActionBar;
import android.support.v7.widget.Toolbar;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.StyleSpan;
import android.view.View;
import android.widget.Toast;

import org.kontalk.Kontalk;
import org.kontalk.Log;
import org.kontalk.R;
import org.kontalk.authenticator.Authenticator;
import org.kontalk.client.NumberValidator;
import org.kontalk.data.Conversation;
import org.kontalk.message.TextComponent;
import org.kontalk.provider.MyMessages.Threads;
import org.kontalk.provider.MyMessages.Threads.Conversations;
import org.kontalk.util.MessageUtils;
import org.kontalk.util.SystemUtils;
import org.kontalk.util.XMPPUtils;

/**
 * Conversation writing activity.
 * @author Daniele Ricci
 */
public class ComposeMessage extends ToolbarActivity implements ComposeMessageParent {
    public static final String TAG = ComposeMessage.class.getSimpleName();

    private static final int REQUEST_CONTACT_PICKER = 9721;

    private static final StyleSpan sUpdatingTextSpan = new StyleSpan(Typeface.ITALIC);

    /** View conversation intent action. Just provide the threadId with this. */
    public static final String ACTION_VIEW_CONVERSATION = "org.kontalk.conversation.VIEW";
    /** View conversation with userId intent action. Just provide userId with this. */
    public static final String ACTION_VIEW_USERID = "org.kontalk.conversation.VIEW_USERID";

    /** Used with VIEW actions, scrolls to a specific message. */
    public static final String EXTRA_MESSAGE = "org.kontalk.conversation.MESSAGE";
    /** Used with VIEW actions, highlight a {@link Pattern} in messages. */
    public static final String EXTRA_HIGHLIGHT = "org.kontalk.conversation.HIGHLIGHT";
    /** Used internally when reloading: does not trigger scroll-to-match. */
    static final String EXTRA_RELOADING = "org.kontalk.conversation.RELOADING";
    /** Set to true for showing the group chat on creation disclaimer. */
    static final String EXTRA_CREATING_GROUP = "org.kontalk.CREATING_GROUP";

    /** The SEND intent. */
    private Intent sendIntent;

    AbstractComposeFragment mFragment;

    /**
     * True if the window has lost focus the last time
     * {@link #onWindowFocusChanged} was called. */
    private boolean mLostFocus;
    /**
     * This is set to true in {@link #onResume} and to false in {@link #onPause}.
     * It is checked in {@link #onWindowFocusChanged} to ensure that the activity is indeed
     * visible before granting focus capabilities.
     */
    private boolean mResumed;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.compose_message_screen);

        setupActionBar();

        if (savedInstanceState != null) {
            mFragment = (AbstractComposeFragment) getSupportFragmentManager()
                    .findFragmentById(R.id.fragment_compose_message);
        }

        if (mFragment == null) {
            // build chat fragment
            AbstractComposeFragment f = getComposeFragment(savedInstanceState);
            if (f != null) {
                // insert it into the activity
                setComposeFragment(f);
            }
        }
    }

    private void setupActionBar() {
        Toolbar toolbar = super.setupToolbar(true, true);
        // TODO find a way to use a colored selector
        toolbar.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mFragment == null || !mFragment.isActionModeActive())
                    onTitleClick();
            }
        });
    }

    @Override
    protected boolean isNormalUpNavigation() {
        return false;
    }

    private void setComposeFragment(@NonNull AbstractComposeFragment f) {
        mFragment = f;
        getSupportFragmentManager().beginTransaction().replace(R.id.fragment_compose_message, f)
                .setTransition(FragmentTransaction.TRANSIT_NONE).commitNowAllowingStateLoss();
    }

    @Override
    public void loadConversation(long threadId, boolean creatingGroup) {
        // create bootstrap intent
        setIntent(ComposeMessage.fromConversation(this, threadId, creatingGroup));
        loadConversation();
    }

    @Override
    public void loadConversation(Uri threadUri) {
        onNewIntent(fromThreadUri(this, threadUri));
    }

    public void loadConversation() {
        // build chat fragment
        AbstractComposeFragment f = getComposeFragment(null);
        if (f != null) {
            // insert it into the activity
            setComposeFragment(f);
        } else {
            // conversation disappeared
            finish();
        }
    }

    @Override
    public void setTitle(CharSequence title, CharSequence subtitle) {
        ActionBar bar = getSupportActionBar();
        if (title != null)
            bar.setTitle(title);
        if (subtitle != null)
            bar.setSubtitle(subtitle);
    }

    @Override
    public void setUpdatingSubtitle() {
        ActionBar bar = getSupportActionBar();
        CharSequence current = bar.getSubtitle();
        // no need to set updating status if no text is displayed
        if (current != null && current.length() > 0) {
            bar.setSubtitle(applyUpdatingStyle(current));
        }
    }

    static CharSequence applyUpdatingStyle(CharSequence text) {
        // we call toString() to strip any existing span
        SpannableString status = new SpannableString(text.toString());
        status.setSpan(sUpdatingTextSpan, 0, status.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
        return status;
    }

    @Override
    public void onBackPressed() {
        if (mFragment == null || (!mFragment.tryHideAttachmentView() && !mFragment.tryHideEmojiDrawer()))
            super.onBackPressed();
    }

    public void onTitleClick() {
        if (mFragment instanceof ComposeMessageFragment)
            ((ComposeMessageFragment) mFragment).viewContact();
        else if (mFragment instanceof GroupMessageFragment)
            ((GroupMessageFragment) mFragment).viewGroupInfo();
    }

    private AbstractComposeFragment getComposeFragment(Bundle savedInstanceState) {
        Bundle args = processIntent(savedInstanceState);
        if (args != null) {
            AbstractComposeFragment f = null;
            Uri threadUri = args.getParcelable("data");
            String action = args.getString("action");
            if (ACTION_VIEW_CONVERSATION.equals(action)) {
                long threadId = ContentUris.parseId(threadUri);
                Conversation conv = Conversation.loadFromId(this, threadId);
                if (conv != null) {
                    f = conv.isGroupChat() ? new GroupMessageFragment() : new ComposeMessageFragment();
                }
            } else if (ACTION_VIEW_USERID.equals(action)) {
                String userId = threadUri.getLastPathSegment();
                Conversation conv = Conversation.loadFromUserId(this, userId);
                f = conv != null && conv.isGroupChat() ? new GroupMessageFragment() : new ComposeMessageFragment();
            } else {
                // default to a single user chat
                f = new ComposeMessageFragment();
            }

            if (f != null)
                f.setArguments(args);
            return f;
        }

        return null;
    }

    private Bundle processIntent(Bundle savedInstanceState) {
        Intent intent;
        if (savedInstanceState != null) {
            mLostFocus = savedInstanceState.getBoolean("lostFocus");

            Uri uri = savedInstanceState.getParcelable(Uri.class.getName());
            if (uri == null) {
                Log.d(TAG, "restoring non-loaded conversation, aborting");
                finish();
                return null;
            }
            intent = new Intent(ACTION_VIEW_USERID, uri);
        } else {
            intent = getIntent();
        }

        if (intent != null) {
            final String action = intent.getAction();
            Bundle args = null;

            // view intent
            // view conversation - just threadId provided
            // view conversation - just userId provided
            if (Intent.ACTION_VIEW.equals(action) || ACTION_VIEW_CONVERSATION.equals(action)
                    || ACTION_VIEW_USERID.equals(action)) {
                Uri uri = intent.getData();

                // two-panes UI: start conversation list
                if (Kontalk.hasTwoPanesUI(this)) {
                    Intent startIntent = new Intent(action, uri, getApplicationContext(),
                            ConversationsActivity.class);
                    startActivity(startIntent);
                    // no need to go further
                    finish();
                    return null;
                }
                // single-pane UI: start normally
                else {
                    args = new Bundle();
                    args.putString("action", action);
                    args.putParcelable("data", uri);
                    args.putLong(EXTRA_MESSAGE, intent.getLongExtra(EXTRA_MESSAGE, -1));
                    args.putString(EXTRA_HIGHLIGHT, intent.getStringExtra(EXTRA_HIGHLIGHT));
                    args.putBoolean(EXTRA_CREATING_GROUP, intent.getBooleanExtra(EXTRA_CREATING_GROUP, false));
                }
            }

            // send external content
            else if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
                sendIntent = intent;
                String mime = intent.getType();

                Log.i(TAG, "sending data to someone: " + mime);
                chooseContact();

                // onActivityResult will handle the rest
                return null;
            }

            // send to someone
            else if (Intent.ACTION_SENDTO.equals(action)) {
                try {
                    Uri uri = intent.getData();
                    // a phone number should come here...
                    String number = NumberValidator.fixNumber(this, uri.getSchemeSpecificPart(),
                            Authenticator.getDefaultAccountName(this), 0);
                    // compute hash and open conversation
                    String jid = XMPPUtils.createLocalJID(this, MessageUtils.sha1(number));

                    // two-panes UI: start conversation list
                    if (Kontalk.hasTwoPanesUI(this)) {
                        Intent startIntent = new Intent(getApplicationContext(), ConversationsActivity.class);
                        startIntent.setAction(ACTION_VIEW_USERID);
                        startIntent.setData(Threads.getUri(jid));
                        startActivity(startIntent);
                        // no need to go further
                        finish();
                        return null;
                    }
                    // single-pane UI: start normally
                    else {
                        args = new Bundle();
                        args.putString("action", ComposeMessage.ACTION_VIEW_USERID);
                        args.putParcelable("data", Threads.getUri(jid));
                        args.putString("number", number);
                    }
                } catch (Exception e) {
                    Log.e(TAG, "invalid intent", e);
                    finish();
                }
            }

            return args;
        }

        return null;
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == REQUEST_CONTACT_PICKER) {
            if (resultCode == RESULT_OK) {
                Uri threadUri = data.getData();
                if (threadUri != null) {
                    Log.i(TAG, "composing message for conversation: " + threadUri);
                    String userId = threadUri.getLastPathSegment();
                    Intent i = fromUserId(this, userId);
                    if (i != null) {
                        if (Kontalk.hasTwoPanesUI(this)) {
                            // we need to go back to the main activity
                            Intent startIntent = new Intent(getApplicationContext(), ConversationsActivity.class);
                            startIntent.setAction(ACTION_VIEW_USERID);
                            startIntent.setData(threadUri);
                            startIntent.putExtra(ConversationsActivity.EXTRA_SEND_INTENT, sendIntent);
                            startActivity(startIntent);
                            finish();
                        } else {
                            onNewIntent(i);

                            // process SEND intent if necessary
                            if (sendIntent != null)
                                processSendIntent();
                        }
                    } else {
                        Toast.makeText(this, R.string.contact_not_registered, Toast.LENGTH_LONG).show();
                        finish();
                    }
                }
            } else {
                // no contact chosen or other problems - quit
                finish();
            }
        } else {
            super.onActivityResult(requestCode, resultCode, data);
        }
    }

    public static Intent fromUserId(Context context, String userId) {
        return fromUserId(context, userId, false);
    }

    public static Intent fromUserId(Context context, String userId, boolean creatingGroup) {
        Conversation conv = Conversation.loadFromUserId(context, userId);
        // not found - create new
        if (conv == null) {
            Intent ni = new Intent(context, ComposeMessage.class);
            ni.setAction(ComposeMessage.ACTION_VIEW_USERID);
            ni.setData(Threads.getUri(userId));
            return ni;
        }

        return fromConversation(context, conv, creatingGroup);
    }

    public static Intent fromThreadUri(Context context, Uri threadUri) {
        String userId = threadUri.getLastPathSegment();
        Conversation conv = Conversation.loadFromUserId(context, userId);
        // not found - create new
        if (conv == null) {
            Intent ni = new Intent(context, ComposeMessage.class);
            ni.setAction(ComposeMessage.ACTION_VIEW_USERID);
            ni.setData(threadUri);
            return ni;
        }

        return fromConversation(context, conv);
    }

    /** Creates an {@link Intent} for launching the composer for a given {@link Conversation}. */
    public static Intent fromConversation(Context context, Conversation conv) {
        return fromConversation(context, conv, false);
    }

    /** Creates an {@link Intent} for launching the composer for a given {@link Conversation}. */
    public static Intent fromConversation(Context context, Conversation conv, boolean creatingGroup) {
        return fromConversation(context, conv.getThreadId(), creatingGroup);
    }

    /** Creates an {@link Intent} for launching the composer for a given thread Id. */
    public static Intent fromConversation(Context context, long threadId) {
        return fromConversation(context, threadId, false);
    }

    /** Creates an {@link Intent} for launching the composer for a given thread Id. */
    public static Intent fromConversation(Context context, long threadId, boolean creatingGroup) {
        Intent i = new Intent(ComposeMessage.ACTION_VIEW_CONVERSATION,
                ContentUris.withAppendedId(Conversations.CONTENT_URI, threadId), context, ComposeMessage.class);
        i.putExtra(EXTRA_CREATING_GROUP, creatingGroup);
        return i;
    }

    /** Creates an {@link Intent} for sending a text message. */
    public static Intent sendTextMessage(String text) {
        Intent i = SystemUtils.externalIntent(Intent.ACTION_SEND);
        i.setType(TextComponent.MIME_TYPE);
        i.putExtra(Intent.EXTRA_TEXT, text);
        return i;
    }

    public static Intent sendMediaMessage(Uri uri, String mime) {
        Intent i = SystemUtils.externalIntent(Intent.ACTION_SEND);
        i.setType(mime);
        i.putExtra(Intent.EXTRA_STREAM, uri);
        return i;
    }

    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(this, ContactsListActivity.class);
        startActivityForResult(i, REQUEST_CONTACT_PICKER);
    }

    private void processSendIntent() {
        SendIntentReceiver.processSendIntent(this, sendIntent, mFragment);
        sendIntent = null;
    }

    @Override
    protected void onNewIntent(Intent intent) {
        setIntent(intent);
        loadConversation();
    }

    @Override
    protected void onSaveInstanceState(Bundle out) {
        super.onSaveInstanceState(out);
        if (mFragment != null)
            out.putParcelable(Uri.class.getName(), Threads.getUri(mFragment.getUserId()));
        out.putBoolean("lostFocus", mLostFocus);
    }

    @Override
    protected void onResume() {
        super.onResume();
        mResumed = true;
    }

    @Override
    protected void onPause() {
        super.onPause();
        mResumed = false;
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);

        if (hasFocus && mResumed) {
            if (mLostFocus) {
                mFragment.onFocus();
                mLostFocus = false;
            }
        }
    }

    public void fragmentLostFocus() {
        mLostFocus = true;
    }

    public boolean hasLostFocus() {
        return mLostFocus;
    }

    public Intent getSendIntent() {
        return sendIntent;
    }
}