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.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.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 QuickContactBadge badge;
116	private LinearLayout keys;
117	private FlowLayout tags;
118	private boolean showDynamicTags = false;
119	private boolean showLastSeen = false;
120	private String messageFingerprint;
121
122	private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() {
123
124		@Override
125		public void onClick(DialogInterface dialog, int which) {
126			Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
127			intent.setType(Contacts.CONTENT_ITEM_TYPE);
128			intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid().toString());
129			intent.putExtra(Intents.Insert.IM_PROTOCOL,
130					CommonDataKinds.Im.PROTOCOL_JABBER);
131			intent.putExtra("finishActivityOnSaveCompleted", true);
132			ContactDetailsActivity.this.startActivityForResult(intent, 0);
133		}
134	};
135
136	private OnClickListener onBadgeClick = new OnClickListener() {
137
138		@Override
139		public void onClick(View v) {
140			Uri systemAccount = contact.getSystemAccount();
141			if (systemAccount == null) {
142				AlertDialog.Builder builder = new AlertDialog.Builder(
143						ContactDetailsActivity.this);
144				builder.setTitle(getString(R.string.action_add_phone_book));
145				builder.setMessage(getString(R.string.add_phone_book_text,
146						contact.getDisplayJid()));
147				builder.setNegativeButton(getString(R.string.cancel), null);
148				builder.setPositiveButton(getString(R.string.add), addToPhonebook);
149				builder.create().show();
150			} else {
151				Intent intent = new Intent(Intent.ACTION_VIEW);
152				intent.setData(systemAccount);
153				startActivity(intent);
154			}
155		}
156	};
157
158	@Override
159	public void onRosterUpdate() {
160		refreshUi();
161	}
162
163	@Override
164	public void onAccountUpdate() {
165		refreshUi();
166	}
167
168	@Override
169	public void OnUpdateBlocklist(final Status status) {
170		refreshUi();
171	}
172
173	@Override
174	protected void refreshUiReal() {
175		invalidateOptionsMenu();
176		populateView();
177	}
178
179	@Override
180	protected String getShareableUri() {
181		if (contact != null) {
182			return "xmpp:"+contact.getJid().toBareJid().toString();
183		} else {
184			return "";
185		}
186	}
187
188	@Override
189	protected void onCreate(final Bundle savedInstanceState) {
190		super.onCreate(savedInstanceState);
191		if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
192			try {
193				this.accountJid = Jid.fromString(getIntent().getExtras().getString(EXTRA_ACCOUNT));
194			} catch (final InvalidJidException ignored) {
195			}
196			try {
197				this.contactJid = Jid.fromString(getIntent().getExtras().getString("contact"));
198			} catch (final InvalidJidException ignored) {
199			}
200		}
201		this.messageFingerprint = getIntent().getStringExtra("fingerprint");
202		setContentView(R.layout.activity_contact_details);
203
204		contactJidTv = (TextView) findViewById(R.id.details_contactjid);
205		accountJidTv = (TextView) findViewById(R.id.details_account);
206		lastseen = (TextView) findViewById(R.id.details_lastseen);
207		statusMessage = (TextView) findViewById(R.id.status_message);
208		send = (CheckBox) findViewById(R.id.details_send_presence);
209		receive = (CheckBox) findViewById(R.id.details_receive_presence);
210		badge = (QuickContactBadge) findViewById(R.id.details_contact_badge);
211		addContactButton = (Button) findViewById(R.id.add_contact_button);
212		addContactButton.setOnClickListener(new OnClickListener() {
213			@Override
214			public void onClick(View view) {
215				showAddToRosterDialog(contact);
216			}
217		});
218		keys = (LinearLayout) findViewById(R.id.details_contact_keys);
219		tags = (FlowLayout) findViewById(R.id.tags);
220		if (getActionBar() != null) {
221			getActionBar().setHomeButtonEnabled(true);
222			getActionBar().setDisplayHomeAsUpEnabled(true);
223		}
224	}
225
226	@Override
227	public void onStart() {
228		super.onStart();
229		final int theme = findTheme();
230		if (this.mTheme != theme) {
231			recreate();
232		} else {
233			final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
234			this.showDynamicTags = preferences.getBoolean("show_dynamic_tags", false);
235			this.showLastSeen = preferences.getBoolean("last_activity", false);
236		}
237	}
238
239	@Override
240	public boolean onOptionsItemSelected(final MenuItem menuItem) {
241		final AlertDialog.Builder builder = new AlertDialog.Builder(this);
242		builder.setNegativeButton(getString(R.string.cancel), null);
243		switch (menuItem.getItemId()) {
244			case android.R.id.home:
245				finish();
246				break;
247			case R.id.action_share:
248				shareUri();
249				break;
250			case R.id.action_delete_contact:
251				builder.setTitle(getString(R.string.action_delete_contact))
252					.setMessage(
253							getString(R.string.remove_contact_text,
254								contact.getDisplayJid()))
255					.setPositiveButton(getString(R.string.delete),
256							removeFromRoster).create().show();
257				break;
258			case R.id.action_edit_contact:
259				Uri systemAccount = contact.getSystemAccount();
260				if (systemAccount == null) {
261					quickEdit(contact.getDisplayName(), 0, new OnValueEdited() {
262
263						@Override
264						public void onValueEdited(String value) {
265							contact.setServerName(value);
266							ContactDetailsActivity.this.xmppConnectionService
267								.pushContactToServer(contact);
268							populateView();
269						}
270					});
271				} else {
272					Intent intent = new Intent(Intent.ACTION_EDIT);
273					intent.setDataAndType(systemAccount, Contacts.CONTENT_ITEM_TYPE);
274					intent.putExtra("finishActivityOnSaveCompleted", true);
275					startActivity(intent);
276				}
277				break;
278			case R.id.action_block:
279				BlockContactDialog.show(this, xmppConnectionService, contact);
280				break;
281			case R.id.action_unblock:
282				BlockContactDialog.show(this, xmppConnectionService, contact);
283				break;
284		}
285		return super.onOptionsItemSelected(menuItem);
286	}
287
288	@Override
289	public boolean onCreateOptionsMenu(final Menu menu) {
290		getMenuInflater().inflate(R.menu.contact_details, menu);
291		MenuItem block = menu.findItem(R.id.action_block);
292		MenuItem unblock = menu.findItem(R.id.action_unblock);
293		MenuItem edit = menu.findItem(R.id.action_edit_contact);
294		MenuItem delete = menu.findItem(R.id.action_delete_contact);
295		if (contact == null) {
296			return true;
297		}
298		final XmppConnection connection = contact.getAccount().getXmppConnection();
299		if (connection != null && connection.getFeatures().blocking()) {
300			if (this.contact.isBlocked()) {
301				block.setVisible(false);
302			} else {
303				unblock.setVisible(false);
304			}
305		} else {
306			unblock.setVisible(false);
307			block.setVisible(false);
308		}
309		if (!contact.showInRoster()) {
310			edit.setVisible(false);
311			delete.setVisible(false);
312		}
313		return super.onCreateOptionsMenu(menu);
314	}
315
316	private void populateView() {
317		if (contact == null) {
318			return;
319		}
320		invalidateOptionsMenu();
321		setTitle(contact.getDisplayName());
322		if (contact.showInRoster()) {
323			send.setVisibility(View.VISIBLE);
324			receive.setVisibility(View.VISIBLE);
325			addContactButton.setVisibility(View.GONE);
326			send.setOnCheckedChangeListener(null);
327			receive.setOnCheckedChangeListener(null);
328
329			List<String> statusMessages = contact.getPresences().getStatusMessages();
330			if (statusMessages.size() == 0) {
331				statusMessage.setVisibility(View.GONE);
332			} else {
333				StringBuilder builder = new StringBuilder();
334				statusMessage.setVisibility(View.VISIBLE);
335				int s = statusMessages.size();
336				for(int i = 0; i < s; ++i) {
337					if (s > 1) {
338						builder.append("");
339					}
340					builder.append(statusMessages.get(i));
341					if (i < s - 1) {
342						builder.append("\n");
343					}
344				}
345				statusMessage.setText(builder);
346			}
347
348			if (contact.getOption(Contact.Options.FROM)) {
349				send.setText(R.string.send_presence_updates);
350				send.setChecked(true);
351			} else if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
352				send.setChecked(false);
353				send.setText(R.string.send_presence_updates);
354			} else {
355				send.setText(R.string.preemptively_grant);
356				if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
357					send.setChecked(true);
358				} else {
359					send.setChecked(false);
360				}
361			}
362			if (contact.getOption(Contact.Options.TO)) {
363				receive.setText(R.string.receive_presence_updates);
364				receive.setChecked(true);
365			} else {
366				receive.setText(R.string.ask_for_presence_updates);
367				if (contact.getOption(Contact.Options.ASKING)) {
368					receive.setChecked(true);
369				} else {
370					receive.setChecked(false);
371				}
372			}
373			if (contact.getAccount().isOnlineAndConnected()) {
374				receive.setEnabled(true);
375				send.setEnabled(true);
376			} else {
377				receive.setEnabled(false);
378				send.setEnabled(false);
379			}
380			send.setOnCheckedChangeListener(this.mOnSendCheckedChange);
381			receive.setOnCheckedChangeListener(this.mOnReceiveCheckedChange);
382		} else {
383			addContactButton.setVisibility(View.VISIBLE);
384			send.setVisibility(View.GONE);
385			receive.setVisibility(View.GONE);
386			statusMessage.setVisibility(View.GONE);
387		}
388
389		if (contact.isBlocked() && !this.showDynamicTags) {
390			lastseen.setVisibility(View.VISIBLE);
391			lastseen.setText(R.string.contact_blocked);
392		} else {
393			if (showLastSeen && contact.getLastseen() > 0) {
394				lastseen.setVisibility(View.VISIBLE);
395				lastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen()));
396			} else {
397				lastseen.setVisibility(View.GONE);
398			}
399		}
400
401		if (contact.getPresences().size() > 1) {
402			contactJidTv.setText(contact.getDisplayJid() + " ("
403					+ contact.getPresences().size() + ")");
404		} else {
405			contactJidTv.setText(contact.getDisplayJid());
406		}
407		String account;
408		if (Config.DOMAIN_LOCK != null) {
409			account = contact.getAccount().getJid().getLocalpart();
410		} else {
411			account = contact.getAccount().getJid().toBareJid().toString();
412		}
413		accountJidTv.setText(getString(R.string.using_account, account));
414		badge.setImageBitmap(avatarService().get(contact, getPixel(72)));
415		badge.setOnClickListener(this.onBadgeClick);
416
417		keys.removeAllViews();
418		boolean hasKeys = false;
419		LayoutInflater inflater = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
420		if (Config.supportOtr()) {
421			for (final String otrFingerprint : contact.getOtrFingerprints()) {
422				hasKeys = true;
423				View view = inflater.inflate(R.layout.contact_key, keys, false);
424				TextView key = (TextView) view.findViewById(R.id.key);
425				TextView keyType = (TextView) view.findViewById(R.id.key_type);
426				ImageButton removeButton = (ImageButton) view
427						.findViewById(R.id.button_remove);
428				removeButton.setVisibility(View.VISIBLE);
429				key.setText(CryptoHelper.prettifyFingerprint(otrFingerprint));
430				if (otrFingerprint != null && otrFingerprint.equals(messageFingerprint)) {
431					keyType.setText(R.string.otr_fingerprint_selected_message);
432					keyType.setTextColor(getResources().getColor(R.color.accent));
433				} else {
434					keyType.setText(R.string.otr_fingerprint);
435				}
436				keys.addView(view);
437				removeButton.setOnClickListener(new OnClickListener() {
438
439					@Override
440					public void onClick(View v) {
441						confirmToDeleteFingerprint(otrFingerprint);
442					}
443				});
444			}
445		}
446		if (Config.supportOmemo()) {
447			for (final XmppAxolotlSession session : contact.getAccount().getAxolotlService().findSessionsForContact(contact)) {
448				if (!session.getTrust().isCompromised()) {
449					boolean highlight = session.getFingerprint().equals(messageFingerprint);
450					hasKeys = true;
451					addFingerprintRow(keys, session, highlight);
452				}
453			}
454		}
455		if (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) {
456			hasKeys = true;
457			View view = inflater.inflate(R.layout.contact_key, keys, false);
458			TextView key = (TextView) view.findViewById(R.id.key);
459			TextView keyType = (TextView) view.findViewById(R.id.key_type);
460			keyType.setText(R.string.openpgp_key_id);
461			if ("pgp".equals(messageFingerprint)) {
462				keyType.setTextColor(getResources().getColor(R.color.accent));
463			}
464			key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId()));
465			view.setOnClickListener(new OnClickListener() {
466
467				@Override
468				public void onClick(View v) {
469					PgpEngine pgp = ContactDetailsActivity.this.xmppConnectionService
470						.getPgpEngine();
471					if (pgp != null) {
472						PendingIntent intent = pgp.getIntentForKey(contact);
473						if (intent != null) {
474							try {
475								startIntentSenderForResult(
476										intent.getIntentSender(), 0, null, 0,
477										0, 0);
478							} catch (SendIntentException e) {
479
480							}
481						}
482					}
483				}
484			});
485			keys.addView(view);
486		}
487		if (hasKeys) {
488			keys.setVisibility(View.VISIBLE);
489		} else {
490			keys.setVisibility(View.GONE);
491		}
492
493		List<ListItem.Tag> tagList = contact.getTags(this);
494		if (tagList.size() == 0 || !this.showDynamicTags) {
495			tags.setVisibility(View.GONE);
496		} else {
497			tags.setVisibility(View.VISIBLE);
498			tags.removeAllViewsInLayout();
499			for(final ListItem.Tag tag : tagList) {
500				final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag,tags,false);
501				tv.setText(tag.getName());
502				tv.setBackgroundColor(tag.getColor());
503				tags.addView(tv);
504			}
505		}
506	}
507
508	protected void confirmToDeleteFingerprint(final String fingerprint) {
509		AlertDialog.Builder builder = new AlertDialog.Builder(this);
510		builder.setTitle(R.string.delete_fingerprint);
511		builder.setMessage(R.string.sure_delete_fingerprint);
512		builder.setNegativeButton(R.string.cancel, null);
513		builder.setPositiveButton(R.string.delete,
514				new android.content.DialogInterface.OnClickListener() {
515
516					@Override
517					public void onClick(DialogInterface dialog, int which) {
518						if (contact.deleteOtrFingerprint(fingerprint)) {
519							populateView();
520							xmppConnectionService.syncRosterToDisk(contact.getAccount());
521						}
522					}
523
524				});
525		builder.create().show();
526	}
527
528	public void onBackendConnected() {
529		if (accountJid != null && contactJid != null) {
530			Account account = xmppConnectionService.findAccountByJid(accountJid);
531			if (account == null) {
532				return;
533			}
534			this.contact = account.getRoster().getContact(contactJid);
535			if (mPendingFingerprintVerificationUri != null) {
536				processFingerprintVerification(mPendingFingerprintVerificationUri);
537				mPendingFingerprintVerificationUri = null;
538			}
539			populateView();
540		}
541	}
542
543	@Override
544	public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
545		refreshUi();
546	}
547
548	@Override
549	protected void processFingerprintVerification(XmppUri uri) {
550		if (contact != null && contact.getJid().toBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
551			if (xmppConnectionService.verifyFingerprints(contact,uri.getFingerprints())) {
552				Toast.makeText(this,R.string.verified_fingerprints,Toast.LENGTH_SHORT).show();
553			}
554		} else {
555			Toast.makeText(this,R.string.invalid_barcode,Toast.LENGTH_SHORT).show();
556		}
557	}
558}