ContactDetailsActivity.java

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