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