ContactDetailsActivity.java

  1package eu.siacs.conversations.ui;
  2
  3import android.content.DialogInterface;
  4import android.content.Intent;
  5import android.content.SharedPreferences;
  6import android.databinding.DataBindingUtil;
  7import android.net.Uri;
  8import android.os.Bundle;
  9import android.preference.PreferenceManager;
 10import android.provider.ContactsContract.CommonDataKinds;
 11import android.provider.ContactsContract.Contacts;
 12import android.provider.ContactsContract.Intents;
 13import android.support.v7.app.AlertDialog;
 14import android.support.v7.widget.Toolbar;
 15import android.view.LayoutInflater;
 16import android.view.Menu;
 17import android.view.MenuItem;
 18import android.view.View;
 19import android.view.View.OnClickListener;
 20import android.widget.CompoundButton;
 21import android.widget.CompoundButton.OnCheckedChangeListener;
 22import android.widget.TextView;
 23import android.widget.Toast;
 24
 25import org.openintents.openpgp.util.OpenPgpUtils;
 26
 27import java.util.List;
 28
 29import eu.siacs.conversations.Config;
 30import eu.siacs.conversations.R;
 31import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 32import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 33import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
 34import eu.siacs.conversations.databinding.ActivityContactDetailsBinding;
 35import eu.siacs.conversations.entities.Account;
 36import eu.siacs.conversations.entities.Contact;
 37import eu.siacs.conversations.entities.ListItem;
 38import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
 39import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
 40import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 41import eu.siacs.conversations.utils.IrregularUnicodeDetector;
 42import eu.siacs.conversations.utils.UIHelper;
 43import eu.siacs.conversations.utils.XmppUri;
 44import eu.siacs.conversations.xml.Namespace;
 45import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 46import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 47import eu.siacs.conversations.xmpp.XmppConnection;
 48import rocks.xmpp.addr.Jid;
 49
 50public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated {
 51	public static final String ACTION_VIEW_CONTACT = "view_contact";
 52	ActivityContactDetailsBinding binding;
 53	private Contact contact;
 54	private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() {
 55
 56		@Override
 57		public void onClick(DialogInterface dialog, int which) {
 58			xmppConnectionService.deleteContactOnServer(contact);
 59		}
 60	};
 61	private OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() {
 62
 63		@Override
 64		public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
 65			if (isChecked) {
 66				if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
 67					xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().sendPresenceUpdatesTo(contact));
 68				} else {
 69					contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
 70				}
 71			} else {
 72				contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
 73				xmppConnectionService.sendPresencePacket(contact.getAccount(),xmppConnectionService.getPresenceGenerator().stopPresenceUpdatesTo(contact));
 74			}
 75		}
 76	};
 77	private OnCheckedChangeListener mOnReceiveCheckedChange = new OnCheckedChangeListener() {
 78
 79		@Override
 80		public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
 81			if (isChecked) {
 82				xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().requestPresenceUpdatesFrom(contact));
 83			} else {
 84				xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().stopPresenceUpdatesFrom(contact));
 85			}
 86		}
 87	};
 88	private Jid accountJid;
 89	private Jid contactJid;
 90	private boolean showDynamicTags = false;
 91	private boolean showLastSeen = false;
 92	private boolean showInactiveOmemo = false;
 93	private String messageFingerprint;
 94
 95	private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() {
 96
 97		@Override
 98		public void onClick(DialogInterface dialog, int which) {
 99			Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
100			intent.setType(Contacts.CONTENT_ITEM_TYPE);
101			intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid().toString());
102			intent.putExtra(Intents.Insert.IM_PROTOCOL,
103					CommonDataKinds.Im.PROTOCOL_JABBER);
104			intent.putExtra("finishActivityOnSaveCompleted", true);
105			ContactDetailsActivity.this.startActivityForResult(intent, 0);
106		}
107	};
108
109	private OnClickListener onBadgeClick = new OnClickListener() {
110
111		@Override
112		public void onClick(View v) {
113			Uri systemAccount = contact.getSystemAccount();
114			if (systemAccount == null) {
115				AlertDialog.Builder builder = new AlertDialog.Builder(
116						ContactDetailsActivity.this);
117				builder.setTitle(getString(R.string.action_add_phone_book));
118				builder.setMessage(getString(R.string.add_phone_book_text, contact.getJid().toString()));
119				builder.setNegativeButton(getString(R.string.cancel), null);
120				builder.setPositiveButton(getString(R.string.add), addToPhonebook);
121				builder.create().show();
122			} else {
123				Intent intent = new Intent(Intent.ACTION_VIEW);
124				intent.setData(systemAccount);
125				startActivity(intent);
126			}
127		}
128	};
129
130	@Override
131	public void onRosterUpdate() {
132		refreshUi();
133	}
134
135	@Override
136	public void onAccountUpdate() {
137		refreshUi();
138	}
139
140	@Override
141	public void OnUpdateBlocklist(final Status status) {
142		refreshUi();
143	}
144
145	@Override
146	protected void refreshUiReal() {
147		invalidateOptionsMenu();
148		populateView();
149	}
150
151	@Override
152	protected String getShareableUri(boolean http) {
153		if (http) {
154			return "https://conversations.im/j/" + XmppUri.lameUrlEncode(contact.getJid().asBareJid().toEscapedString());
155		} else {
156			return "xmpp:" + contact.getJid().asBareJid().toEscapedString();
157		}
158	}
159
160	@Override
161	protected void onCreate(final Bundle savedInstanceState) {
162		super.onCreate(savedInstanceState);
163		showInactiveOmemo = savedInstanceState != null && savedInstanceState.getBoolean("show_inactive_omemo", false);
164		if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
165			try {
166				this.accountJid = Jid.of(getIntent().getExtras().getString(EXTRA_ACCOUNT));
167			} catch (final IllegalArgumentException ignored) {
168			}
169			try {
170				this.contactJid = Jid.of(getIntent().getExtras().getString("contact"));
171			} catch (final IllegalArgumentException ignored) {
172			}
173		}
174		this.messageFingerprint = getIntent().getStringExtra("fingerprint");
175		this.binding = DataBindingUtil.setContentView(this, R.layout.activity_contact_details);
176
177		setSupportActionBar((Toolbar) binding.toolbar);
178		configureActionBar(getSupportActionBar());
179		binding.showInactiveDevices.setOnClickListener(v -> {
180			showInactiveOmemo = !showInactiveOmemo;
181			populateView();
182		});
183		binding.addContactButton.setOnClickListener(v -> showAddToRosterDialog(contact));
184	}
185
186	@Override
187	public void onSaveInstanceState(final Bundle savedInstanceState) {
188		savedInstanceState.putBoolean("show_inactive_omemo", showInactiveOmemo);
189		super.onSaveInstanceState(savedInstanceState);
190	}
191
192	@Override
193	public void onStart() {
194		super.onStart();
195		final int theme = findTheme();
196		if (this.mTheme != theme) {
197			recreate();
198		} else {
199			final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
200			this.showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, false);
201			this.showLastSeen = preferences.getBoolean("last_activity", false);
202		}
203	}
204
205	@Override
206	public boolean onOptionsItemSelected(final MenuItem menuItem) {
207		if (MenuDoubleTabUtil.shouldIgnoreTap()) {
208			return false;
209		}
210		final AlertDialog.Builder builder = new AlertDialog.Builder(this);
211		builder.setNegativeButton(getString(R.string.cancel), null);
212		switch (menuItem.getItemId()) {
213			case android.R.id.home:
214				finish();
215				break;
216			case R.id.action_share_http:
217				shareLink(true);
218				break;
219			case R.id.action_share_uri:
220				shareLink(false);
221				break;
222			case R.id.action_delete_contact:
223				builder.setTitle(getString(R.string.action_delete_contact))
224						.setMessage(getString(R.string.remove_contact_text, contact.getJid().toString()))
225						.setPositiveButton(getString(R.string.delete),
226								removeFromRoster).create().show();
227				break;
228			case R.id.action_edit_contact:
229				Uri systemAccount = contact.getSystemAccount();
230				if (systemAccount == null) {
231					quickEdit(contact.getServerName(), R.string.contact_name, value -> {
232						contact.setServerName(value);
233						ContactDetailsActivity.this.xmppConnectionService.pushContactToServer(contact);
234						populateView();
235						return null;
236					}, true);
237				} else {
238					Intent intent = new Intent(Intent.ACTION_EDIT);
239					intent.setDataAndType(systemAccount, Contacts.CONTENT_ITEM_TYPE);
240					intent.putExtra("finishActivityOnSaveCompleted", true);
241					startActivity(intent);
242				}
243				break;
244			case R.id.action_block:
245				BlockContactDialog.show(this, contact);
246				break;
247			case R.id.action_unblock:
248				BlockContactDialog.show(this, contact);
249				break;
250		}
251		return super.onOptionsItemSelected(menuItem);
252	}
253
254	@Override
255	public boolean onCreateOptionsMenu(final Menu menu) {
256		getMenuInflater().inflate(R.menu.contact_details, menu);
257		MenuItem block = menu.findItem(R.id.action_block);
258		MenuItem unblock = menu.findItem(R.id.action_unblock);
259		MenuItem edit = menu.findItem(R.id.action_edit_contact);
260		MenuItem delete = menu.findItem(R.id.action_delete_contact);
261		if (contact == null) {
262			return true;
263		}
264		final XmppConnection connection = contact.getAccount().getXmppConnection();
265		if (connection != null && connection.getFeatures().blocking()) {
266			if (this.contact.isBlocked()) {
267				block.setVisible(false);
268			} else {
269				unblock.setVisible(false);
270			}
271		} else {
272			unblock.setVisible(false);
273			block.setVisible(false);
274		}
275		if (!contact.showInRoster()) {
276			edit.setVisible(false);
277			delete.setVisible(false);
278		}
279		return super.onCreateOptionsMenu(menu);
280	}
281
282	private void populateView() {
283		if (contact == null) {
284			return;
285		}
286		invalidateOptionsMenu();
287		setTitle(contact.getDisplayName());
288		if (contact.showInRoster()) {
289			binding.detailsSendPresence.setVisibility(View.VISIBLE);
290			binding.detailsReceivePresence.setVisibility(View.VISIBLE);
291			binding.addContactButton.setVisibility(View.GONE);
292			binding.detailsSendPresence.setOnCheckedChangeListener(null);
293			binding.detailsReceivePresence.setOnCheckedChangeListener(null);
294
295			List<String> statusMessages = contact.getPresences().getStatusMessages();
296			if (statusMessages.size() == 0) {
297				binding.statusMessage.setVisibility(View.GONE);
298			} else {
299				StringBuilder builder = new StringBuilder();
300				binding.statusMessage.setVisibility(View.VISIBLE);
301				int s = statusMessages.size();
302				for (int i = 0; i < s; ++i) {
303					if (s > 1) {
304						builder.append("");
305					}
306					builder.append(statusMessages.get(i));
307					if (i < s - 1) {
308						builder.append("\n");
309					}
310				}
311				binding.statusMessage.setText(builder);
312			}
313
314			if (contact.getOption(Contact.Options.FROM)) {
315				binding.detailsSendPresence.setText(R.string.send_presence_updates);
316				binding.detailsSendPresence.setChecked(true);
317			} else if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
318				binding.detailsSendPresence.setChecked(false);
319				binding.detailsSendPresence.setText(R.string.send_presence_updates);
320			} else {
321				binding.detailsSendPresence.setText(R.string.preemptively_grant);
322				if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
323					binding.detailsSendPresence.setChecked(true);
324				} else {
325					binding.detailsSendPresence.setChecked(false);
326				}
327			}
328			if (contact.getOption(Contact.Options.TO)) {
329				binding.detailsReceivePresence.setText(R.string.receive_presence_updates);
330				binding.detailsReceivePresence.setChecked(true);
331			} else {
332				binding.detailsReceivePresence.setText(R.string.ask_for_presence_updates);
333				if (contact.getOption(Contact.Options.ASKING)) {
334					binding.detailsReceivePresence.setChecked(true);
335				} else {
336					binding.detailsReceivePresence.setChecked(false);
337				}
338			}
339			if (contact.getAccount().isOnlineAndConnected()) {
340				binding.detailsReceivePresence.setEnabled(true);
341				binding.detailsSendPresence.setEnabled(true);
342			} else {
343				binding.detailsReceivePresence.setEnabled(false);
344				binding.detailsSendPresence.setEnabled(false);
345			}
346			binding.detailsSendPresence.setOnCheckedChangeListener(this.mOnSendCheckedChange);
347			binding.detailsReceivePresence.setOnCheckedChangeListener(this.mOnReceiveCheckedChange);
348		} else {
349			binding.addContactButton.setVisibility(View.VISIBLE);
350			binding.detailsSendPresence.setVisibility(View.GONE);
351			binding.detailsReceivePresence.setVisibility(View.GONE);
352			binding.statusMessage.setVisibility(View.GONE);
353		}
354
355		if (contact.isBlocked() && !this.showDynamicTags) {
356			binding.detailsLastseen.setVisibility(View.VISIBLE);
357			binding.detailsLastseen.setText(R.string.contact_blocked);
358		} else {
359			if (showLastSeen
360					&& contact.getLastseen() > 0
361					&& contact.getPresences().allOrNonSupport(Namespace.IDLE)) {
362				binding.detailsLastseen.setVisibility(View.VISIBLE);
363				binding.detailsLastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen()));
364			} else {
365				binding.detailsLastseen.setVisibility(View.GONE);
366			}
367		}
368
369		binding.detailsContactjid.setText(IrregularUnicodeDetector.style(this, contact.getJid()));
370		String account;
371		if (Config.DOMAIN_LOCK != null) {
372			account = contact.getAccount().getJid().getLocal();
373		} else {
374			account = contact.getAccount().getJid().asBareJid().toString();
375		}
376		binding.detailsAccount.setText(getString(R.string.using_account, account));
377		binding.detailsContactBadge.setImageBitmap(avatarService().get(contact, (int) getResources().getDimension(R.dimen.avatar_on_details_screen_size)));
378		binding.detailsContactBadge.setOnClickListener(this.onBadgeClick);
379
380		binding.detailsContactKeys.removeAllViews();
381		boolean hasKeys = false;
382		final LayoutInflater inflater = getLayoutInflater();
383		final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
384		if (Config.supportOmemo() && axolotlService != null) {
385			boolean skippedInactive = false;
386			boolean showsInactive = false;
387			for (final XmppAxolotlSession session : axolotlService.findSessionsForContact(contact)) {
388				final FingerprintStatus trust = session.getTrust();
389				hasKeys |= !trust.isCompromised();
390				if (!trust.isActive()) {
391					if (showInactiveOmemo) {
392						showsInactive = true;
393					} else {
394						skippedInactive = true;
395						continue;
396					}
397				}
398				if (!trust.isCompromised()) {
399					boolean highlight = session.getFingerprint().equals(messageFingerprint);
400					addFingerprintRow(binding.detailsContactKeys, session, highlight);
401				}
402			}
403			if (showsInactive || skippedInactive) {
404				binding.showInactiveDevices.setText(showsInactive ? R.string.hide_inactive_devices : R.string.show_inactive_devices);
405				binding.showInactiveDevices.setVisibility(View.VISIBLE);
406			} else {
407				binding.showInactiveDevices.setVisibility(View.GONE);
408			}
409		} else {
410			binding.showInactiveDevices.setVisibility(View.GONE);
411		}
412		binding.scanButton.setVisibility(hasKeys && isCameraFeatureAvailable() ? View.VISIBLE : View.GONE);
413		if (hasKeys) {
414			binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this));
415		}
416		if (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) {
417			hasKeys = true;
418			View view = inflater.inflate(R.layout.contact_key, binding.detailsContactKeys, false);
419			TextView key = (TextView) view.findViewById(R.id.key);
420			TextView keyType = (TextView) view.findViewById(R.id.key_type);
421			keyType.setText(R.string.openpgp_key_id);
422			if ("pgp".equals(messageFingerprint)) {
423				keyType.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption_Highlight);
424			}
425			key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId()));
426			final OnClickListener openKey = v -> launchOpenKeyChain(contact.getPgpKeyId());
427			view.setOnClickListener(openKey);
428			key.setOnClickListener(openKey);
429			keyType.setOnClickListener(openKey);
430			binding.detailsContactKeys.addView(view);
431		}
432		binding.keysWrapper.setVisibility(hasKeys ? View.VISIBLE : View.GONE);
433
434		List<ListItem.Tag> tagList = contact.getTags(this);
435		if (tagList.size() == 0 || !this.showDynamicTags) {
436			binding.tags.setVisibility(View.GONE);
437		} else {
438			binding.tags.setVisibility(View.VISIBLE);
439			binding.tags.removeAllViewsInLayout();
440			for (final ListItem.Tag tag : tagList) {
441				final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, binding.tags, false);
442				tv.setText(tag.getName());
443				tv.setBackgroundColor(tag.getColor());
444				binding.tags.addView(tv);
445			}
446		}
447	}
448
449	public void onBackendConnected() {
450		if (accountJid != null && contactJid != null) {
451			Account account = xmppConnectionService.findAccountByJid(accountJid);
452			if (account == null) {
453				return;
454			}
455			this.contact = account.getRoster().getContact(contactJid);
456			if (mPendingFingerprintVerificationUri != null) {
457				processFingerprintVerification(mPendingFingerprintVerificationUri);
458				mPendingFingerprintVerificationUri = null;
459			}
460			populateView();
461		}
462	}
463
464	@Override
465	public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
466		refreshUi();
467	}
468
469	@Override
470	protected void processFingerprintVerification(XmppUri uri) {
471		if (contact != null && contact.getJid().asBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
472			if (xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints())) {
473				Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
474			}
475		} else {
476			Toast.makeText(this, R.string.invalid_barcode, Toast.LENGTH_SHORT).show();
477		}
478	}
479}