/* * 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.HashSet; import java.util.Set; import com.afollestad.materialdialogs.DialogAction; import com.afollestad.materialdialogs.MaterialDialog; import org.jivesoftware.smack.packet.IQ; import org.jivesoftware.smack.packet.Presence; import org.jivesoftware.smack.util.StringUtils; import org.jivesoftware.smackx.chatstates.ChatState; import org.jxmpp.util.XmppStringUtils; import org.spongycastle.openpgp.PGPPublicKey; import org.spongycastle.openpgp.PGPPublicKeyRing; import android.app.Activity; import android.content.BroadcastReceiver; import android.content.ContentResolver; import android.content.ContentUris; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.content.pm.PackageManager; import android.database.Cursor; import android.database.sqlite.SQLiteDiskIOException; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; 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.PGP; import org.kontalk.data.Contact; import org.kontalk.data.Conversation; import org.kontalk.message.CompositeMessage; import org.kontalk.provider.Keyring; import org.kontalk.provider.KontalkGroupCommands; import org.kontalk.provider.MyMessages; import org.kontalk.provider.MyMessages.Threads; import org.kontalk.provider.MyUsers; import org.kontalk.provider.UsersProvider; import org.kontalk.service.msgcenter.MessageCenterService; import org.kontalk.sync.Syncer; import org.kontalk.util.MessageUtils; import org.kontalk.util.Preferences; import org.kontalk.util.SystemUtils; import org.kontalk.util.XMPPUtils; import static org.kontalk.service.msgcenter.MessageCenterService.PRIVACY_ACCEPT; import static org.kontalk.service.msgcenter.MessageCenterService.PRIVACY_BLOCK; import static org.kontalk.service.msgcenter.MessageCenterService.PRIVACY_REJECT; import static org.kontalk.service.msgcenter.MessageCenterService.PRIVACY_UNBLOCK; /** * The composer fragment. * @author Daniele Ricci * @author Andrea Cappelli */ public class ComposeMessageFragment extends AbstractComposeFragment { private static final String TAG = ComposeMessage.TAG; ViewGroup mInvitationBar; private MenuItem mViewContactMenu; private MenuItem mCallMenu; private MenuItem mBlockMenu; private MenuItem mUnblockMenu; /** The user we are talking to. */ String mUserJID; private String mUserPhone; String mLastActivityRequestId; String mVersionRequestId; String mKeyRequestId; private boolean mIsTyping; private BroadcastReceiver mBroadcastReceiver; @Override protected void onInflateOptionsMenu(Menu menu, MenuInflater inflater) { inflater.inflate(R.menu.compose_message_menu, menu); mViewContactMenu = menu.findItem(R.id.view_contact); mCallMenu = menu.findItem(R.id.call_contact); mBlockMenu = menu.findItem(R.id.block_user); mUnblockMenu = menu.findItem(R.id.unblock_user); } @Override public boolean onOptionsItemSelected(MenuItem item) { if (super.onOptionsItemSelected(item)) return true; switch (item.getItemId()) { case R.id.call_contact: startActivity(SystemUtils.externalIntent(Intent.ACTION_CALL, Uri.parse("tel:" + mUserPhone))); return true; case R.id.view_contact: viewContact(); return true; case R.id.block_user: blockUser(); return true; case R.id.unblock_user: unblockUser(); return true; } return false; } @Override public void onPause() { super.onPause(); if (mLocalBroadcastManager != null && mBroadcastReceiver != null) { mLocalBroadcastManager.unregisterReceiver(mBroadcastReceiver); } } public void viewContact() { if (mConversation != null) { Contact contact = mConversation.getContact(); if (contact != null) { Uri uri = contact.getUri(); if (uri != null) { Intent i = SystemUtils.externalIntent(Intent.ACTION_VIEW, uri); if (i.resolveActivity(getActivity().getPackageManager()) != null) { startActivity(i); } else { // no contacts app found (crap device eh?) Toast.makeText(getActivity(), R.string.err_no_contacts_app, Toast.LENGTH_LONG).show(); } } else { // no contact found Toast.makeText(getActivity(), R.string.err_no_contact, Toast.LENGTH_SHORT).show(); } } } } private void blockUser() { new MaterialDialog.Builder(getActivity()) .title(R.string.title_block_user_warning) .content(R.string.msg_block_user_warning) .positiveText(R.string.menu_block_user) .positiveColorRes(R.color.button_danger) .negativeText(android.R.string.cancel) .onPositive(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(@NonNull MaterialDialog materialDialog, @NonNull DialogAction dialogAction) { setPrivacy(PRIVACY_BLOCK); } }) .show(); } private void unblockUser() { new MaterialDialog.Builder(getActivity()) .title(R.string.title_unblock_user_warning) .content(R.string.msg_unblock_user_warning) .positiveText(R.string.menu_unblock_user) .positiveColorRes(R.color.button_danger) .negativeText(android.R.string.cancel) .onPositive(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(@NonNull MaterialDialog materialDialog, @NonNull DialogAction dialogAction) { setPrivacy(PRIVACY_UNBLOCK); } }) .show(); } @Override protected void loadConversationMetadata(Uri uri) { super.loadConversationMetadata(uri); if (mConversation != null) { mUserJID = mConversation.getRecipient(); Contact contact = mConversation.getContact(); if (contact != null) { mUserName = contact.getDisplayName(); mUserPhone = contact.getNumber(); } else { mUserName = mUserJID; } } } @Override protected void handleActionView(Uri uri) { long threadId = 0; ContentResolver cres = getContext().getContentResolver(); /* * FIXME this will retrieve name directly from contacts, * resulting in a possible discrepancy with users database */ Cursor c = cres.query(uri, new String[] { Syncer.DATA_COLUMN_DISPLAY_NAME, Syncer.DATA_COLUMN_PHONE }, null, null, null); if (c.moveToFirst()) { mUserName = c.getString(0); mUserPhone = c.getString(1); // FIXME should it be retrieved from RawContacts.SYNC3 ?? mUserJID = XMPPUtils.createLocalJID(getActivity(), MessageUtils.sha1(mUserPhone)); Cursor cp = cres.query(MyMessages.Messages.CONTENT_URI, new String[] { MyMessages.Messages.THREAD_ID }, MyMessages.Messages.PEER + " = ?", new String[] { mUserJID }, null); if (cp.moveToFirst()) threadId = cp.getLong(0); cp.close(); } c.close(); if (threadId > 0) { mConversation = Conversation.loadFromId(getActivity(), threadId); setThreadId(threadId); } else if (mUserJID == null) { Toast.makeText(getActivity(), R.string.err_no_contact, Toast.LENGTH_LONG).show(); closeConversation(); } else { mConversation = Conversation.createNew(getActivity()); mConversation.setRecipient(mUserJID); } } @Override protected boolean handleActionViewConversation(Uri uri, Bundle args) { mUserJID = uri.getPathSegments().get(1); mConversation = Conversation.loadFromUserId(getActivity(), mUserJID); if (mConversation == null) { mConversation = Conversation.createNew(getActivity()); mConversation.setNumberHint(args.getString("number")); mConversation.setRecipient(mUserJID); } // this way avoid doing the users database query twice else { if (mConversation.getContact() == null) { mConversation.setNumberHint(args.getString("number")); mConversation.setRecipient(mUserJID); } } setThreadId(mConversation.getThreadId()); Contact contact = mConversation.getContact(); if (contact != null) { mUserName = contact.getDisplayName(); mUserPhone = contact.getNumber(); } else { mUserName = mUserJID; mUserPhone = null; } return true; } @Override protected void onArgumentsProcessed() { // non existant thread - check for not synced contact if (getThreadId() <= 0 && mConversation != null && mUserJID != null) { Contact contact = mConversation.getContact(); if (!(mUserPhone != null && contact != null) || !contact.isRegistered()) { // ask user to send invitation new MaterialDialog.Builder(getActivity()) .title(R.string.title_user_not_found) .content(R.string.message_user_not_found) // nothing happens if user chooses to contact the user anyway .positiveText(R.string.yes_user_not_found) .negativeText(R.string.no_user_not_found) .onNegative(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { sendInvitation(); } }) .show(); } } } @Override public boolean sendTyping() { if (mAvailableResources.size() > 0) { MessageCenterService.sendChatState(getContext(), mUserJID, ChatState.composing); return true; } return false; } @Override public boolean sendInactive() { if (mAvailableResources.size() > 0) { MessageCenterService.sendChatState(getActivity(), mUserJID, ChatState.inactive); return true; } return false; } @Override protected void deleteConversation() { try { // delete all mConversation.delete(true); } catch (SQLiteDiskIOException e) { Log.w(TAG, "error deleting thread"); Toast.makeText(getActivity(), R.string.error_delete_thread, Toast.LENGTH_LONG).show(); } } @Override protected boolean isUserId(String jid) { return XMPPUtils.equalsBareJID(jid, mUserJID); } @Override protected void onConnected() { // reset any pending request mLastActivityRequestId = null; mVersionRequestId = null; mKeyRequestId = null; } @Override protected void onRosterLoaded() { // probe presence requestPresence(); } @Override protected void onStartTyping(String jid) { mIsTyping = true; setStatusText(getString(R.string.seen_typing_label)); } @Override protected void onStopTyping(String jid) { mIsTyping = false; setStatusText(mCurrentStatus != null ? mCurrentStatus : ""); } @Override protected void onPresence(String jid, Presence.Type type, boolean removed, Presence.Mode mode, String fingerprint) { final Context context = getContext(); if (context == null) return; if (type == null) { // no roster entry found, request subscription // pre-approve our presence if we don't have contact's key Intent i = new Intent(context, MessageCenterService.class); i.setAction(MessageCenterService.ACTION_PRESENCE); i.putExtra(MessageCenterService.EXTRA_TO, mUserJID); i.putExtra(MessageCenterService.EXTRA_TYPE, Presence.Type.subscribed.name()); context.startService(i); // request subscription i = new Intent(context, MessageCenterService.class); i.setAction(MessageCenterService.ACTION_PRESENCE); i.putExtra(MessageCenterService.EXTRA_TO, mUserJID); i.putExtra(MessageCenterService.EXTRA_TYPE, Presence.Type.subscribe.name()); context.startService(i); setStatusText(context.getString(R.string.invitation_sent_label)); } // (un)available presence else if (type == Presence.Type.available || type == Presence.Type.unavailable) { CharSequence statusText = null; // really not much sense in requesting the key for a non-existing contact Contact contact = getContact(); if (contact != null) { // if this is null, we are accepting the key for the first time PGPPublicKeyRing trustedPublicKey = contact.getTrustedPublicKeyRing(); // request the key if we don't have a trusted one and of course if the user has a key boolean unknownKey = (trustedPublicKey == null && contact.getFingerprint() != null); boolean changedKey = false; // check if fingerprint changed if (trustedPublicKey != null && fingerprint != null) { String oldFingerprint = PGP.getFingerprint(PGP.getMasterKey(trustedPublicKey)); if (!fingerprint.equalsIgnoreCase(oldFingerprint)) { // fingerprint has changed since last time changedKey = true; } } // user has no key or it couldn't be found // request it else if (trustedPublicKey == null && fingerprint == null) { requestPublicKey(jid); } if (changedKey) { // warn user that public key is changed showKeyChangedWarning(fingerprint); } else if (unknownKey) { // warn user that public key is unknown showKeyUnknownWarning(fingerprint); } } if (type == Presence.Type.available) { mIsTyping = mIsTyping || Contact.isTyping(jid); if (mIsTyping) { setStatusText(context.getString(R.string.seen_typing_label)); } /* * FIXME using mode this way has several flaws. * 1. it doesn't take multiple resources into account * 2. it doesn't account for away status duration (we don't have this information at all) */ boolean isAway = (mode == Presence.Mode.away); if (isAway) { statusText = context.getString(R.string.seen_away_label); } else { statusText = context.getString(R.string.seen_online_label); } String version = Contact.getVersion(jid); // do not request version info if already requested before if (!isAway && version == null && mVersionRequestId == null) { requestVersion(jid); } } else if (type == Presence.Type.unavailable) { /* * All available resources have gone. Mark * the user as offline immediately and use the * timestamp provided with the stanza (if any). */ if (mAvailableResources.size() == 0) { // an offline user can't be typing mIsTyping = false; if (removed) { // resource was removed now, mark as just offline statusText = context.getText(R.string.seen_moment_ago_label); } else { // resource is offline, request last activity if (contact != null && contact.getLastSeen() > 0) { setLastSeenTimestamp(context, contact.getLastSeen()); } else if (mLastActivityRequestId == null) { mLastActivityRequestId = StringUtils.randomString(6); MessageCenterService.requestLastActivity(context, XmppStringUtils.parseBareJid(jid), mLastActivityRequestId); } } } } if (statusText != null) { setCurrentStatusText(statusText); } } } /** Sends a subscription request for the current peer. */ void requestPresence() { // do not request presence for domain JIDs if (!XMPPUtils.isDomainJID(mUserJID)) { Context context = getContext(); if (context != null) { // all of this shall be done only if there isn't a request from the other contact // FIXME when accepting an invitation, the if below could be // false because of delay or no reloading of mConversation at all // thus skipping the presence request. // An automatic solution could be to emit a ROSTER_LOADED whenever the roster changes // still this _if_ should go away in favour of the message center checking for subscription status // and the UI reacting to a pending request status (i.e. "pending_in") if (mConversation.getRequestStatus() != Threads.REQUEST_WAITING) { // request last presence MessageCenterService.requestPresence(context, mUserJID); } } } } void sendInvitation() { // FIXME is this specific to sms app? Intent i = SystemUtils.externalIntent(Intent.ACTION_SENDTO, Uri.parse("smsto:" + mUserPhone)); i.putExtra("sms_body", getString(R.string.text_invite_message)); startActivity(i); getActivity().finish(); } /** Called when the {@link Conversation} object has been created. */ @Override protected void onConversationCreated() { super.onConversationCreated(); // setup broadcast receiver if (mBroadcastReceiver == null) { mBroadcastReceiver = new BroadcastReceiver() { public void onReceive(Context context, Intent intent) { String from = XmppStringUtils.parseBareJid(intent .getStringExtra(MessageCenterService.EXTRA_FROM)); if (!mUserJID.equals(from)) { // not for us return; } String action = intent.getAction(); if (MessageCenterService.ACTION_LAST_ACTIVITY.equals(action)) { String id = intent.getStringExtra(MessageCenterService.EXTRA_PACKET_ID); if (id != null && id.equals(mLastActivityRequestId)) { mLastActivityRequestId = null; // ignore last activity if we had an available presence in the meantime if (mAvailableResources.size() == 0) { String type = intent.getStringExtra(MessageCenterService.EXTRA_TYPE); if (type == null || !type.equalsIgnoreCase(IQ.Type.error.toString())) { long seconds = intent.getLongExtra(MessageCenterService.EXTRA_SECONDS, -1); setLastSeenSeconds(context, seconds); } } } } else if (MessageCenterService.ACTION_VERSION.equals(action)) { // compare version and show warning if needed String id = intent.getStringExtra(MessageCenterService.EXTRA_PACKET_ID); if (id != null && id.equals(mVersionRequestId)) { mVersionRequestId = null; String name = intent.getStringExtra(MessageCenterService.EXTRA_VERSION_NAME); if (name != null && name.equalsIgnoreCase(context.getString(R.string.app_name))) { String version = intent.getStringExtra(MessageCenterService.EXTRA_VERSION_NUMBER); if (version != null) { // cache the version String fullFrom = intent.getStringExtra(MessageCenterService.EXTRA_FROM); Contact.setVersion(fullFrom, version); setVersionInfo(context, version); } } } } else if (MessageCenterService.ACTION_PUBLICKEY.equals(action)) { String id = intent.getStringExtra(MessageCenterService.EXTRA_PACKET_ID); if (id != null && id.equals(mKeyRequestId)) { mKeyRequestId = null; // reload contact invalidateContact(); // request presence again requestPresence(); } } else if (MessageCenterService.ACTION_BLOCKED.equals(intent.getAction())) { // reload contact reloadContact(); // this will update block/unblock menu items updateUI(); Toast.makeText(context, R.string.msg_user_blocked, Toast.LENGTH_LONG).show(); } else if (MessageCenterService.ACTION_UNBLOCKED.equals(intent.getAction())) { // reload contact reloadContact(); // this will update block/unblock menu items updateUI(); // hide any block warning // a new warning will be issued for the key if needed hideWarning(); // request presence subscription when unblocking requestPresence(); Toast.makeText(context, R.string.msg_user_unblocked, Toast.LENGTH_LONG).show(); } else if (MessageCenterService.ACTION_SUBSCRIBED.equals(intent.getAction())) { // reload contact invalidateContact(); // request presence requestPresence(); } } }; // listen for some stuff we need IntentFilter filter = new IntentFilter(); filter.addAction(MessageCenterService.ACTION_LAST_ACTIVITY); filter.addAction(MessageCenterService.ACTION_VERSION); filter.addAction(MessageCenterService.ACTION_PUBLICKEY); filter.addAction(MessageCenterService.ACTION_BLOCKED); filter.addAction(MessageCenterService.ACTION_UNBLOCKED); filter.addAction(MessageCenterService.ACTION_SUBSCRIBED); mLocalBroadcastManager.registerReceiver(mBroadcastReceiver, filter); } // setup invitation bar boolean visible = (mConversation.getRequestStatus() == Threads.REQUEST_WAITING); if (visible) { if (mInvitationBar == null) { mInvitationBar = (ViewGroup) getView().findViewById(R.id.invitation_bar); // setup listeners and show button bar View.OnClickListener listener = new View.OnClickListener() { public void onClick(View v) { mInvitationBar.setVisibility(View.GONE); int action; if (v.getId() == R.id.button_accept) action = PRIVACY_ACCEPT; else action = PRIVACY_REJECT; setPrivacy(action); } }; mInvitationBar.findViewById(R.id.button_accept) .setOnClickListener(listener); mInvitationBar.findViewById(R.id.button_block) .setOnClickListener(listener); // identity button has its own listener mInvitationBar.findViewById(R.id.button_identity) .setOnClickListener(new View.OnClickListener() { public void onClick(View v) { showIdentityDialog(true, R.string.title_invitation); } } ); } } if (mInvitationBar != null) mInvitationBar.setVisibility(visible ? View.VISIBLE : View.GONE); } void setPrivacy(int action) { int status; switch (action) { case PRIVACY_ACCEPT: status = Threads.REQUEST_REPLY_PENDING_ACCEPT; break; case PRIVACY_BLOCK: case PRIVACY_REJECT: status = Threads.REQUEST_REPLY_PENDING_BLOCK; break; case PRIVACY_UNBLOCK: status = Threads.REQUEST_REPLY_PENDING_UNBLOCK; break; default: return; } Context ctx = getActivity(); // temporarly disable peer observer because the next call will write to the threads table unregisterPeerObserver(); // mark request as pending accepted UsersProvider.setRequestStatus(ctx, mUserJID, status); // accept invitation if (action == PRIVACY_ACCEPT) { // trust the key Kontalk.getMessagesController(getContext()) .setTrustLevelAndRetryMessages(ctx, mUserJID, getContact().getFingerprint(), MyUsers.Keys.TRUST_VERIFIED); } // reload contact invalidateContact(); // send command to message center MessageCenterService.replySubscription(ctx, mUserJID, action); // reload manually mConversation = Conversation.loadFromUserId(ctx, mUserJID); if (mConversation == null) { // threads was deleted (it was a request thread) threadId = 0; } processStart(); if (threadId == 0) { // no thread means no peer observer will be invoked // we need to manually trigger this MessageCenterService.requestConnectionStatus(ctx); MessageCenterService.requestRosterStatus(ctx); } } void invalidateContact() { Contact.invalidate(mUserJID); reloadContact(); } void reloadContact() { if (mConversation != null) { // this will trigger contact reload mConversation.setRecipient(mUserJID); } } @Override protected void addUsers(String[] members) { String selfJid = Authenticator.getSelfJID(getContext()); String groupId = StringUtils.randomString(20); String groupJid = KontalkGroupCommands.createGroupJid(groupId, selfJid); // ensure no duplicates Set<String> usersList = new HashSet<>(); String userId = getUserId(); if (!userId.equalsIgnoreCase(selfJid)) usersList.add(userId); for (String member : members) { // 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(getContext()) .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; String[] users = usersList.toArray(new String[usersList.size()]); long groupThreadId = Conversation.initGroupChat(getContext(), groupJid, title, users, mComposer.getText().toString()); // store create group command to outbox // NOTE: group chats can currently only be created with chat encryption enabled boolean encrypted = Preferences.getEncryptionEnabled(getContext()); String msgId = MessageCenterService.messageId(); Uri cmdMsg = KontalkGroupCommands.createGroup(getContext(), groupThreadId, groupJid, users, msgId, encrypted); // TODO check for null // send create group command now MessageCenterService.createGroup(getContext(), groupJid, title, users, encrypted, ContentUris.parseId(cmdMsg), msgId); // open the new conversation ((ComposeMessageParent) getActivity()).loadConversation(groupThreadId, true); } }) .inputRange(0, MyMessages.Groups.GROUP_SUBJECT_MAX_LENGTH) .show(); } void showIdentityDialog(boolean informationOnly, int titleId) { String fingerprint; String uid; PGPPublicKeyRing publicKey = Keyring.getPublicKey(getActivity(), mUserJID, MyUsers.Keys.TRUST_UNKNOWN); if (publicKey != null) { PGPPublicKey pk = PGP.getMasterKey(publicKey); fingerprint = PGP.formatFingerprint(PGP.getFingerprint(pk)); uid = PGP.getUserId(pk, XmppStringUtils.parseDomain(mUserJID)); } else { // FIXME using another string fingerprint = uid = getString(R.string.peer_unknown); } SpannableStringBuilder text = new SpannableStringBuilder(); text.append(getString(R.string.text_invitation1)) .append('\n'); Contact c = mConversation.getContact(); if (c != null && c.getName() != null && c.getNumber() != null) { text.append(c.getName()) .append(" <") .append(c.getNumber()) .append('>'); } else { int start = text.length() - 1; text.append(uid); text.setSpan(MessageUtils.STYLE_BOLD, start, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } text.append('\n') .append(getString(R.string.text_invitation2)) .append('\n'); int start = text.length() - 1; text.append(fingerprint); text.setSpan(MessageUtils.STYLE_BOLD, start, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); MaterialDialog.Builder builder = new MaterialDialog .Builder(getActivity()) .content(text); if (informationOnly) { builder.title(titleId); } else { builder.title(titleId) .positiveText(R.string.button_accept) .positiveColorRes(R.color.button_success) .negativeText(R.string.button_block) .negativeColorRes(R.color.button_danger) .onAny(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { // hide warning bar hideWarning(); switch (which) { case POSITIVE: // trust new key trustKeyChange(null); break; case NEGATIVE: // block user immediately setPrivacy(PRIVACY_BLOCK); break; } } }); } builder.show(); } void trustKeyChange(String fingerprint) { // mark current key as trusted if (fingerprint == null) fingerprint = getContact().getFingerprint(); Kontalk.getMessagesController(getContext()) .setTrustLevelAndRetryMessages(getContext(), mUserJID, fingerprint, MyUsers.Keys.TRUST_VERIFIED); // reload contact invalidateContact(); } private void showKeyWarning(int textId, final int dialogTitleId, final int dialogMessageId, final Object... data) { final Activity context = getActivity(); if (context != null) { showWarning(context.getText(textId), new View.OnClickListener() { @Override public void onClick(View v) { new MaterialDialog.Builder(context) .title(dialogTitleId) .content(dialogMessageId) .positiveText(R.string.button_accept) .positiveColorRes(R.color.button_success) .neutralText(R.string.button_identity) .negativeText(R.string.button_block) .negativeColorRes(R.color.button_danger) .onAny(new MaterialDialog.SingleButtonCallback() { @Override public void onClick(@NonNull MaterialDialog dialog, @NonNull DialogAction which) { switch (which) { case POSITIVE: // hide warning bar hideWarning(); // trust new key trustKeyChange((String) data[0]); break; case NEUTRAL: showIdentityDialog(false, dialogTitleId); break; case NEGATIVE: // hide warning bar hideWarning(); // block user immediately setPrivacy(PRIVACY_BLOCK); break; } } }) .show(); } }, WarningType.FATAL); } } private void showKeyUnknownWarning(String fingerprint) { showKeyWarning(R.string.warning_public_key_unknown, R.string.title_public_key_unknown_warning, R.string.msg_public_key_unknown_warning, fingerprint); } private void showKeyChangedWarning(String newFingerprint) { showKeyWarning(R.string.warning_public_key_changed, R.string.title_public_key_changed_warning, R.string.msg_public_key_changed_warning, newFingerprint); } void setVersionInfo(Context context, String version) { if (SystemUtils.isOlderVersion(context, version)) { showWarning(context.getText(R.string.warning_older_version), null, WarningType.WARNING); } } private void setLastSeenTimestamp(Context context, long stamp) { setCurrentStatusText(MessageUtils.formatRelativeTimeSpan(context, stamp)); } void setLastSeenSeconds(Context context, long seconds) { CharSequence statusText = null; if (seconds == 0) { // it's improbable, but whatever... statusText = context.getText(R.string.seen_moment_ago_label); } else if (seconds > 0) { long stamp = System.currentTimeMillis() - (seconds * 1000); Contact contact = getContact(); if (contact != null) { contact.setLastSeen(stamp); } // seconds ago relative to our time statusText = MessageUtils.formatRelativeTimeSpan(context, stamp); } if (statusText != null) { setCurrentStatusText(statusText); } } private void setCurrentStatusText(CharSequence statusText) { mCurrentStatus = statusText; if (!mIsTyping) setStatusText(statusText); } private void requestVersion(String jid) { Context context = getActivity(); if (context != null) { mVersionRequestId = StringUtils.randomString(6); MessageCenterService.requestVersionInfo(context, jid, mVersionRequestId); } } private void requestPublicKey(String jid) { Context context = getActivity(); if (context != null) { mKeyRequestId = StringUtils.randomString(6); MessageCenterService.requestPublicKey(context, jid, mKeyRequestId); } } public void onFocus() { super.onFocus(); if (mUserJID != null) { // clear chat invitation (if any) MessagingNotification.clearChatInvitation(getActivity(), mUserJID); } } protected void updateUI() { super.updateUI(); Contact contact = (mConversation != null) ? mConversation .getContact() : null; boolean contactEnabled = contact != null && contact.getId() > 0; if (mCallMenu != null) { Context context = getContext(); // FIXME what about VoIP? if (context != null && !context.getPackageManager().hasSystemFeature( PackageManager.FEATURE_TELEPHONY)) { mCallMenu.setVisible(false).setEnabled(false); } else { mCallMenu.setVisible(true).setEnabled(true); mCallMenu.setEnabled(contactEnabled); } mViewContactMenu.setEnabled(contactEnabled); } if (mBlockMenu != null) { Context context = getContext(); if (context != null) { if (Authenticator.isSelfJID(context, mUserJID)) { mBlockMenu.setVisible(false).setEnabled(false); mUnblockMenu.setVisible(false).setEnabled(false); } else if (contact != null) { // block/unblock boolean blocked = contact.isBlocked(); if (blocked) // show warning if blocked showWarning(context.getText(R.string.warning_user_blocked), null, WarningType.WARNING); mBlockMenu.setVisible(!blocked).setEnabled(!blocked); mUnblockMenu.setVisible(blocked).setEnabled(blocked); } else { mBlockMenu.setVisible(true).setEnabled(true); mUnblockMenu.setVisible(true).setEnabled(true); } } } } @Override public String getUserId() { return mUserJID; } @Override protected String getDecodedPeer(CompositeMessage msg) { return mUserPhone != null ? mUserPhone : mUserJID; } @Override protected String getDecodedName(CompositeMessage msg) { Contact c = getContact(); return (c != null) ? c.getName() : null; } }