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