ContactDetailsActivity.java

  1package eu.siacs.conversations.ui;
  2
  3import android.app.AlertDialog;
  4import android.app.PendingIntent;
  5import android.content.Context;
  6import android.content.DialogInterface;
  7import android.content.Intent;
  8import android.content.IntentSender.SendIntentException;
  9import android.content.SharedPreferences;
 10import android.net.Uri;
 11import android.os.Bundle;
 12import android.preference.PreferenceManager;
 13import android.provider.ContactsContract.CommonDataKinds;
 14import android.provider.ContactsContract.Contacts;
 15import android.provider.ContactsContract.Intents;
 16import android.view.LayoutInflater;
 17import android.view.Menu;
 18import android.view.MenuItem;
 19import android.view.View;
 20import android.view.View.OnClickListener;
 21import android.widget.Button;
 22import android.widget.CheckBox;
 23import android.widget.CompoundButton;
 24import android.widget.CompoundButton.OnCheckedChangeListener;
 25import android.widget.ImageButton;
 26import android.widget.LinearLayout;
 27import android.widget.QuickContactBadge;
 28import android.widget.TextView;
 29import android.widget.Toast;
 30
 31import com.wefika.flowlayout.FlowLayout;
 32
 33import org.openintents.openpgp.util.OpenPgpUtils;
 34
 35import java.util.List;
 36
 37import eu.siacs.conversations.Config;
 38import eu.siacs.conversations.R;
 39import eu.siacs.conversations.crypto.PgpEngine;
 40import eu.siacs.conversations.crypto.axolotl.AxolotlService;
 41import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
 42import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
 43import eu.siacs.conversations.entities.Account;
 44import eu.siacs.conversations.entities.Contact;
 45import eu.siacs.conversations.entities.ListItem;
 46import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
 47import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
 48import eu.siacs.conversations.utils.CryptoHelper;
 49import eu.siacs.conversations.utils.UIHelper;
 50import eu.siacs.conversations.utils.XmppUri;
 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, xmppConnectionService, contact);
300				break;
301			case R.id.action_unblock:
302				BlockContactDialog.show(this, xmppConnectionService, 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 && contact.getLastseen() > 0) {
414				lastseen.setVisibility(View.VISIBLE);
415				lastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen()));
416			} else {
417				lastseen.setVisibility(View.GONE);
418			}
419		}
420
421		if (contact.getPresences().size() > 1) {
422			contactJidTv.setText(contact.getDisplayJid() + " ("
423					+ contact.getPresences().size() + ")");
424		} else {
425			contactJidTv.setText(contact.getDisplayJid());
426		}
427		String account;
428		if (Config.DOMAIN_LOCK != null) {
429			account = contact.getAccount().getJid().getLocalpart();
430		} else {
431			account = contact.getAccount().getJid().toBareJid().toString();
432		}
433		accountJidTv.setText(getString(R.string.using_account, account));
434		badge.setImageBitmap(avatarService().get(contact, getPixel(72)));
435		badge.setOnClickListener(this.onBadgeClick);
436
437		keys.removeAllViews();
438		boolean hasKeys = false;
439		LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
440		if (Config.supportOtr()) {
441			for (final String otrFingerprint : contact.getOtrFingerprints()) {
442				hasKeys = true;
443				View view = inflater.inflate(R.layout.contact_key, keys, false);
444				TextView key = (TextView) view.findViewById(R.id.key);
445				TextView keyType = (TextView) view.findViewById(R.id.key_type);
446				ImageButton removeButton = (ImageButton) view
447						.findViewById(R.id.button_remove);
448				removeButton.setVisibility(View.VISIBLE);
449				key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
450				if (otrFingerprint != null && otrFingerprint.equals(messageFingerprint)) {
451					keyType.setText(R.string.otr_fingerprint_selected_message);
452					keyType.setTextColor(getResources().getColor(R.color.accent));
453				} else {
454					keyType.setText(R.string.otr_fingerprint);
455				}
456				keys.addView(view);
457				removeButton.setOnClickListener(new OnClickListener() {
458
459					@Override
460					public void onClick(View v) {
461						confirmToDeleteFingerprint(otrFingerprint);
462					}
463				});
464			}
465		}
466		if (Config.supportOmemo()) {
467			boolean skippedInactive = false;
468			boolean showsInactive = false;
469			for (final XmppAxolotlSession session : contact.getAccount().getAxolotlService().findSessionsForContact(contact)) {
470				final FingerprintStatus trust = session.getTrust();
471				if (!trust.isActive()) {
472					if (showInactiveOmemo) {
473						showsInactive = true;
474					} else {
475						skippedInactive = true;
476						continue;
477					}
478				}
479				if (!trust.isCompromised()) {
480					boolean highlight = session.getFingerprint().equals(messageFingerprint);
481					hasKeys = true;
482					addFingerprintRow(keys, session, highlight);
483				}
484			}
485			if (showsInactive || skippedInactive) {
486				mShowInactiveDevicesButton.setText(showsInactive ? R.string.hide_inactive_devices : R.string.show_inactive_devices);
487				mShowInactiveDevicesButton.setVisibility(View.VISIBLE);
488			} else {
489				mShowInactiveDevicesButton.setVisibility(View.GONE);
490			}
491		} else {
492			mShowInactiveDevicesButton.setVisibility(View.GONE);
493		}
494		if (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) {
495			hasKeys = true;
496			View view = inflater.inflate(R.layout.contact_key, keys, false);
497			TextView key = (TextView) view.findViewById(R.id.key);
498			TextView keyType = (TextView) view.findViewById(R.id.key_type);
499			keyType.setText(R.string.openpgp_key_id);
500			if ("pgp".equals(messageFingerprint)) {
501				keyType.setTextColor(getResources().getColor(R.color.accent));
502			}
503			key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId()));
504			view.setOnClickListener(new OnClickListener() {
505
506				@Override
507				public void onClick(View v) {
508					PgpEngine pgp = ContactDetailsActivity.this.xmppConnectionService
509						.getPgpEngine();
510					if (pgp != null) {
511						PendingIntent intent = pgp.getIntentForKey(contact);
512						if (intent != null) {
513							try {
514								startIntentSenderForResult(
515										intent.getIntentSender(), 0, null, 0,
516										0, 0);
517							} catch (SendIntentException e) {
518
519							}
520						}
521					}
522				}
523			});
524			keys.addView(view);
525		}
526		keysWrapper.setVisibility(hasKeys ? View.VISIBLE : View.GONE);
527
528		List<ListItem.Tag> tagList = contact.getTags(this);
529		if (tagList.size() == 0 || !this.showDynamicTags) {
530			tags.setVisibility(View.GONE);
531		} else {
532			tags.setVisibility(View.VISIBLE);
533			tags.removeAllViewsInLayout();
534			for(final ListItem.Tag tag : tagList) {
535				final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag,tags,false);
536				tv.setText(tag.getName());
537				tv.setBackgroundColor(tag.getColor());
538				tags.addView(tv);
539			}
540		}
541	}
542
543	protected void confirmToDeleteFingerprint(final String fingerprint) {
544		AlertDialog.Builder builder = new AlertDialog.Builder(this);
545		builder.setTitle(R.string.delete_fingerprint);
546		builder.setMessage(R.string.sure_delete_fingerprint);
547		builder.setNegativeButton(R.string.cancel, null);
548		builder.setPositiveButton(R.string.delete,
549				new android.content.DialogInterface.OnClickListener() {
550
551					@Override
552					public void onClick(DialogInterface dialog, int which) {
553						if (contact.deleteOtrFingerprint(fingerprint)) {
554							populateView();
555							xmppConnectionService.syncRosterToDisk(contact.getAccount());
556						}
557					}
558
559				});
560		builder.create().show();
561	}
562
563	public void onBackendConnected() {
564		if (accountJid != null && contactJid != null) {
565			Account account = xmppConnectionService.findAccountByJid(accountJid);
566			if (account == null) {
567				return;
568			}
569			this.contact = account.getRoster().getContact(contactJid);
570			if (mPendingFingerprintVerificationUri != null) {
571				processFingerprintVerification(mPendingFingerprintVerificationUri);
572				mPendingFingerprintVerificationUri = null;
573			}
574			populateView();
575		}
576	}
577
578	@Override
579	public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
580		refreshUi();
581	}
582
583	@Override
584	protected void processFingerprintVerification(XmppUri uri) {
585		if (contact != null && contact.getJid().toBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
586			if (xmppConnectionService.verifyFingerprints(contact,uri.getFingerprints())) {
587				Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
588			}
589		} else {
590			Toast.makeText(this,R.string.invalid_barcode,Toast.LENGTH_SHORT).show();
591		}
592	}
593}