ContactDetailsActivity.java

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