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