ContactDetailsActivity.java

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