1package eu.siacs.conversations.ui;
2
3import android.content.DialogInterface;
4import android.content.Intent;
5import android.content.SharedPreferences;
6import android.databinding.DataBindingUtil;
7import android.net.Uri;
8import android.os.Bundle;
9import android.preference.PreferenceManager;
10import android.provider.ContactsContract.CommonDataKinds;
11import android.provider.ContactsContract.Contacts;
12import android.provider.ContactsContract.Intents;
13import android.support.v7.app.AlertDialog;
14import android.support.v7.widget.Toolbar;
15import android.view.LayoutInflater;
16import android.view.Menu;
17import android.view.MenuItem;
18import android.view.View;
19import android.view.View.OnClickListener;
20import android.widget.CompoundButton;
21import android.widget.CompoundButton.OnCheckedChangeListener;
22import android.widget.TextView;
23import android.widget.Toast;
24
25import org.openintents.openpgp.util.OpenPgpUtils;
26
27import java.util.List;
28
29import eu.siacs.conversations.Config;
30import eu.siacs.conversations.R;
31import eu.siacs.conversations.crypto.axolotl.AxolotlService;
32import eu.siacs.conversations.crypto.axolotl.FingerprintStatus;
33import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
34import eu.siacs.conversations.databinding.ActivityContactDetailsBinding;
35import eu.siacs.conversations.entities.Account;
36import eu.siacs.conversations.entities.Contact;
37import eu.siacs.conversations.entities.ListItem;
38import eu.siacs.conversations.services.XmppConnectionService.OnAccountUpdate;
39import eu.siacs.conversations.services.XmppConnectionService.OnRosterUpdate;
40import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
41import eu.siacs.conversations.utils.IrregularUnicodeDetector;
42import eu.siacs.conversations.utils.UIHelper;
43import eu.siacs.conversations.utils.XmppUri;
44import eu.siacs.conversations.xml.Namespace;
45import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
46import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
47import eu.siacs.conversations.xmpp.XmppConnection;
48import rocks.xmpp.addr.Jid;
49
50public class ContactDetailsActivity extends OmemoActivity implements OnAccountUpdate, OnRosterUpdate, OnUpdateBlocklist, OnKeyStatusUpdated {
51 public static final String ACTION_VIEW_CONTACT = "view_contact";
52 ActivityContactDetailsBinding binding;
53 private Contact contact;
54 private DialogInterface.OnClickListener removeFromRoster = new DialogInterface.OnClickListener() {
55
56 @Override
57 public void onClick(DialogInterface dialog, int which) {
58 xmppConnectionService.deleteContactOnServer(contact);
59 }
60 };
61 private OnCheckedChangeListener mOnSendCheckedChange = new OnCheckedChangeListener() {
62
63 @Override
64 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
65 if (isChecked) {
66 if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
67 xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().sendPresenceUpdatesTo(contact));
68 } else {
69 contact.setOption(Contact.Options.PREEMPTIVE_GRANT);
70 }
71 } else {
72 contact.resetOption(Contact.Options.PREEMPTIVE_GRANT);
73 xmppConnectionService.sendPresencePacket(contact.getAccount(),xmppConnectionService.getPresenceGenerator().stopPresenceUpdatesTo(contact));
74 }
75 }
76 };
77 private OnCheckedChangeListener mOnReceiveCheckedChange = new OnCheckedChangeListener() {
78
79 @Override
80 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
81 if (isChecked) {
82 xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().requestPresenceUpdatesFrom(contact));
83 } else {
84 xmppConnectionService.sendPresencePacket(contact.getAccount(), xmppConnectionService.getPresenceGenerator().stopPresenceUpdatesFrom(contact));
85 }
86 }
87 };
88 private Jid accountJid;
89 private Jid contactJid;
90 private boolean showDynamicTags = false;
91 private boolean showLastSeen = false;
92 private boolean showInactiveOmemo = false;
93 private String messageFingerprint;
94
95 private DialogInterface.OnClickListener addToPhonebook = new DialogInterface.OnClickListener() {
96
97 @Override
98 public void onClick(DialogInterface dialog, int which) {
99 Intent intent = new Intent(Intent.ACTION_INSERT_OR_EDIT);
100 intent.setType(Contacts.CONTENT_ITEM_TYPE);
101 intent.putExtra(Intents.Insert.IM_HANDLE, contact.getJid().toString());
102 intent.putExtra(Intents.Insert.IM_PROTOCOL,
103 CommonDataKinds.Im.PROTOCOL_JABBER);
104 intent.putExtra("finishActivityOnSaveCompleted", true);
105 ContactDetailsActivity.this.startActivityForResult(intent, 0);
106 }
107 };
108
109 private OnClickListener onBadgeClick = new OnClickListener() {
110
111 @Override
112 public void onClick(View v) {
113 Uri systemAccount = contact.getSystemAccount();
114 if (systemAccount == null) {
115 AlertDialog.Builder builder = new AlertDialog.Builder(
116 ContactDetailsActivity.this);
117 builder.setTitle(getString(R.string.action_add_phone_book));
118 builder.setMessage(getString(R.string.add_phone_book_text, contact.getJid().toString()));
119 builder.setNegativeButton(getString(R.string.cancel), null);
120 builder.setPositiveButton(getString(R.string.add), addToPhonebook);
121 builder.create().show();
122 } else {
123 Intent intent = new Intent(Intent.ACTION_VIEW);
124 intent.setData(systemAccount);
125 startActivity(intent);
126 }
127 }
128 };
129
130 @Override
131 public void onRosterUpdate() {
132 refreshUi();
133 }
134
135 @Override
136 public void onAccountUpdate() {
137 refreshUi();
138 }
139
140 @Override
141 public void OnUpdateBlocklist(final Status status) {
142 refreshUi();
143 }
144
145 @Override
146 protected void refreshUiReal() {
147 invalidateOptionsMenu();
148 populateView();
149 }
150
151 @Override
152 protected String getShareableUri(boolean http) {
153 if (http) {
154 return "https://conversations.im/j/" + XmppUri.lameUrlEncode(contact.getJid().asBareJid().toEscapedString());
155 } else {
156 return "xmpp:" + contact.getJid().asBareJid().toEscapedString();
157 }
158 }
159
160 @Override
161 protected void onCreate(final Bundle savedInstanceState) {
162 super.onCreate(savedInstanceState);
163 showInactiveOmemo = savedInstanceState != null && savedInstanceState.getBoolean("show_inactive_omemo", false);
164 if (getIntent().getAction().equals(ACTION_VIEW_CONTACT)) {
165 try {
166 this.accountJid = Jid.of(getIntent().getExtras().getString(EXTRA_ACCOUNT));
167 } catch (final IllegalArgumentException ignored) {
168 }
169 try {
170 this.contactJid = Jid.of(getIntent().getExtras().getString("contact"));
171 } catch (final IllegalArgumentException ignored) {
172 }
173 }
174 this.messageFingerprint = getIntent().getStringExtra("fingerprint");
175 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_contact_details);
176
177 setSupportActionBar((Toolbar) binding.toolbar);
178 configureActionBar(getSupportActionBar());
179 binding.showInactiveDevices.setOnClickListener(v -> {
180 showInactiveOmemo = !showInactiveOmemo;
181 populateView();
182 });
183 binding.addContactButton.setOnClickListener(v -> showAddToRosterDialog(contact));
184 }
185
186 @Override
187 public void onSaveInstanceState(final Bundle savedInstanceState) {
188 savedInstanceState.putBoolean("show_inactive_omemo", showInactiveOmemo);
189 super.onSaveInstanceState(savedInstanceState);
190 }
191
192 @Override
193 public void onStart() {
194 super.onStart();
195 final int theme = findTheme();
196 if (this.mTheme != theme) {
197 recreate();
198 } else {
199 final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
200 this.showDynamicTags = preferences.getBoolean(SettingsActivity.SHOW_DYNAMIC_TAGS, false);
201 this.showLastSeen = preferences.getBoolean("last_activity", false);
202 }
203 }
204
205 @Override
206 public boolean onOptionsItemSelected(final MenuItem menuItem) {
207 if (MenuDoubleTabUtil.shouldIgnoreTap()) {
208 return false;
209 }
210 final AlertDialog.Builder builder = new AlertDialog.Builder(this);
211 builder.setNegativeButton(getString(R.string.cancel), null);
212 switch (menuItem.getItemId()) {
213 case android.R.id.home:
214 finish();
215 break;
216 case R.id.action_share_http:
217 shareLink(true);
218 break;
219 case R.id.action_share_uri:
220 shareLink(false);
221 break;
222 case R.id.action_delete_contact:
223 builder.setTitle(getString(R.string.action_delete_contact))
224 .setMessage(getString(R.string.remove_contact_text, contact.getJid().toString()))
225 .setPositiveButton(getString(R.string.delete),
226 removeFromRoster).create().show();
227 break;
228 case R.id.action_edit_contact:
229 Uri systemAccount = contact.getSystemAccount();
230 if (systemAccount == null) {
231 quickEdit(contact.getDisplayName(), 0, value -> {
232 contact.setServerName(value);
233 ContactDetailsActivity.this.xmppConnectionService.pushContactToServer(contact);
234 populateView();
235 return null;
236 }, true);
237 } else {
238 Intent intent = new Intent(Intent.ACTION_EDIT);
239 intent.setDataAndType(systemAccount, Contacts.CONTENT_ITEM_TYPE);
240 intent.putExtra("finishActivityOnSaveCompleted", true);
241 startActivity(intent);
242 }
243 break;
244 case R.id.action_block:
245 BlockContactDialog.show(this, contact);
246 break;
247 case R.id.action_unblock:
248 BlockContactDialog.show(this, contact);
249 break;
250 }
251 return super.onOptionsItemSelected(menuItem);
252 }
253
254 @Override
255 public boolean onCreateOptionsMenu(final Menu menu) {
256 getMenuInflater().inflate(R.menu.contact_details, menu);
257 MenuItem block = menu.findItem(R.id.action_block);
258 MenuItem unblock = menu.findItem(R.id.action_unblock);
259 MenuItem edit = menu.findItem(R.id.action_edit_contact);
260 MenuItem delete = menu.findItem(R.id.action_delete_contact);
261 if (contact == null) {
262 return true;
263 }
264 final XmppConnection connection = contact.getAccount().getXmppConnection();
265 if (connection != null && connection.getFeatures().blocking()) {
266 if (this.contact.isBlocked()) {
267 block.setVisible(false);
268 } else {
269 unblock.setVisible(false);
270 }
271 } else {
272 unblock.setVisible(false);
273 block.setVisible(false);
274 }
275 if (!contact.showInRoster()) {
276 edit.setVisible(false);
277 delete.setVisible(false);
278 }
279 return super.onCreateOptionsMenu(menu);
280 }
281
282 private void populateView() {
283 if (contact == null) {
284 return;
285 }
286 invalidateOptionsMenu();
287 setTitle(contact.getDisplayName());
288 if (contact.showInRoster()) {
289 binding.detailsSendPresence.setVisibility(View.VISIBLE);
290 binding.detailsReceivePresence.setVisibility(View.VISIBLE);
291 binding.addContactButton.setVisibility(View.GONE);
292 binding.detailsSendPresence.setOnCheckedChangeListener(null);
293 binding.detailsReceivePresence.setOnCheckedChangeListener(null);
294
295 List<String> statusMessages = contact.getPresences().getStatusMessages();
296 if (statusMessages.size() == 0) {
297 binding.statusMessage.setVisibility(View.GONE);
298 } else {
299 StringBuilder builder = new StringBuilder();
300 binding.statusMessage.setVisibility(View.VISIBLE);
301 int s = statusMessages.size();
302 for (int i = 0; i < s; ++i) {
303 if (s > 1) {
304 builder.append("• ");
305 }
306 builder.append(statusMessages.get(i));
307 if (i < s - 1) {
308 builder.append("\n");
309 }
310 }
311 binding.statusMessage.setText(builder);
312 }
313
314 if (contact.getOption(Contact.Options.FROM)) {
315 binding.detailsSendPresence.setText(R.string.send_presence_updates);
316 binding.detailsSendPresence.setChecked(true);
317 } else if (contact.getOption(Contact.Options.PENDING_SUBSCRIPTION_REQUEST)) {
318 binding.detailsSendPresence.setChecked(false);
319 binding.detailsSendPresence.setText(R.string.send_presence_updates);
320 } else {
321 binding.detailsSendPresence.setText(R.string.preemptively_grant);
322 if (contact.getOption(Contact.Options.PREEMPTIVE_GRANT)) {
323 binding.detailsSendPresence.setChecked(true);
324 } else {
325 binding.detailsSendPresence.setChecked(false);
326 }
327 }
328 if (contact.getOption(Contact.Options.TO)) {
329 binding.detailsReceivePresence.setText(R.string.receive_presence_updates);
330 binding.detailsReceivePresence.setChecked(true);
331 } else {
332 binding.detailsReceivePresence.setText(R.string.ask_for_presence_updates);
333 if (contact.getOption(Contact.Options.ASKING)) {
334 binding.detailsReceivePresence.setChecked(true);
335 } else {
336 binding.detailsReceivePresence.setChecked(false);
337 }
338 }
339 if (contact.getAccount().isOnlineAndConnected()) {
340 binding.detailsReceivePresence.setEnabled(true);
341 binding.detailsSendPresence.setEnabled(true);
342 } else {
343 binding.detailsReceivePresence.setEnabled(false);
344 binding.detailsSendPresence.setEnabled(false);
345 }
346 binding.detailsSendPresence.setOnCheckedChangeListener(this.mOnSendCheckedChange);
347 binding.detailsReceivePresence.setOnCheckedChangeListener(this.mOnReceiveCheckedChange);
348 } else {
349 binding.addContactButton.setVisibility(View.VISIBLE);
350 binding.detailsSendPresence.setVisibility(View.GONE);
351 binding.detailsReceivePresence.setVisibility(View.GONE);
352 binding.statusMessage.setVisibility(View.GONE);
353 }
354
355 if (contact.isBlocked() && !this.showDynamicTags) {
356 binding.detailsLastseen.setVisibility(View.VISIBLE);
357 binding.detailsLastseen.setText(R.string.contact_blocked);
358 } else {
359 if (showLastSeen
360 && contact.getLastseen() > 0
361 && contact.getPresences().allOrNonSupport(Namespace.IDLE)) {
362 binding.detailsLastseen.setVisibility(View.VISIBLE);
363 binding.detailsLastseen.setText(UIHelper.lastseen(getApplicationContext(), contact.isActive(), contact.getLastseen()));
364 } else {
365 binding.detailsLastseen.setVisibility(View.GONE);
366 }
367 }
368
369 binding.detailsContactjid.setText(IrregularUnicodeDetector.style(this, contact.getJid()));
370 String account;
371 if (Config.DOMAIN_LOCK != null) {
372 account = contact.getAccount().getJid().getLocal();
373 } else {
374 account = contact.getAccount().getJid().asBareJid().toString();
375 }
376 binding.detailsAccount.setText(getString(R.string.using_account, account));
377 binding.detailsContactBadge.setImageBitmap(avatarService().get(contact, getPixel(72)));
378 binding.detailsContactBadge.setOnClickListener(this.onBadgeClick);
379
380 binding.detailsContactKeys.removeAllViews();
381 boolean hasKeys = false;
382 final LayoutInflater inflater = getLayoutInflater();
383 final AxolotlService axolotlService = contact.getAccount().getAxolotlService();
384 if (Config.supportOmemo() && axolotlService != null) {
385 boolean skippedInactive = false;
386 boolean showsInactive = false;
387 for (final XmppAxolotlSession session : axolotlService.findSessionsForContact(contact)) {
388 final FingerprintStatus trust = session.getTrust();
389 hasKeys |= !trust.isCompromised();
390 if (!trust.isActive()) {
391 if (showInactiveOmemo) {
392 showsInactive = true;
393 } else {
394 skippedInactive = true;
395 continue;
396 }
397 }
398 if (!trust.isCompromised()) {
399 boolean highlight = session.getFingerprint().equals(messageFingerprint);
400 addFingerprintRow(binding.detailsContactKeys, session, highlight);
401 }
402 }
403 if (showsInactive || skippedInactive) {
404 binding.showInactiveDevices.setText(showsInactive ? R.string.hide_inactive_devices : R.string.show_inactive_devices);
405 binding.showInactiveDevices.setVisibility(View.VISIBLE);
406 } else {
407 binding.showInactiveDevices.setVisibility(View.GONE);
408 }
409 } else {
410 binding.showInactiveDevices.setVisibility(View.GONE);
411 }
412 binding.scanButton.setVisibility(hasKeys && isCameraFeatureAvailable() ? View.VISIBLE : View.GONE);
413 if (hasKeys) {
414 binding.scanButton.setOnClickListener((v) -> ScanActivity.scan(this));
415 }
416 if (Config.supportOpenPgp() && contact.getPgpKeyId() != 0) {
417 hasKeys = true;
418 View view = inflater.inflate(R.layout.contact_key, binding.detailsContactKeys, false);
419 TextView key = (TextView) view.findViewById(R.id.key);
420 TextView keyType = (TextView) view.findViewById(R.id.key_type);
421 keyType.setText(R.string.openpgp_key_id);
422 if ("pgp".equals(messageFingerprint)) {
423 keyType.setTextAppearance(this, R.style.TextAppearance_Conversations_Caption_Highlight);
424 }
425 key.setText(OpenPgpUtils.convertKeyIdToHex(contact.getPgpKeyId()));
426 final OnClickListener openKey = v -> launchOpenKeyChain(contact.getPgpKeyId());
427 view.setOnClickListener(openKey);
428 key.setOnClickListener(openKey);
429 keyType.setOnClickListener(openKey);
430 binding.detailsContactKeys.addView(view);
431 }
432 binding.keysWrapper.setVisibility(hasKeys ? View.VISIBLE : View.GONE);
433
434 List<ListItem.Tag> tagList = contact.getTags(this);
435 if (tagList.size() == 0 || !this.showDynamicTags) {
436 binding.tags.setVisibility(View.GONE);
437 } else {
438 binding.tags.setVisibility(View.VISIBLE);
439 binding.tags.removeAllViewsInLayout();
440 for (final ListItem.Tag tag : tagList) {
441 final TextView tv = (TextView) inflater.inflate(R.layout.list_item_tag, binding.tags, false);
442 tv.setText(tag.getName());
443 tv.setBackgroundColor(tag.getColor());
444 binding.tags.addView(tv);
445 }
446 }
447 }
448
449 public void onBackendConnected() {
450 if (accountJid != null && contactJid != null) {
451 Account account = xmppConnectionService.findAccountByJid(accountJid);
452 if (account == null) {
453 return;
454 }
455 this.contact = account.getRoster().getContact(contactJid);
456 if (mPendingFingerprintVerificationUri != null) {
457 processFingerprintVerification(mPendingFingerprintVerificationUri);
458 mPendingFingerprintVerificationUri = null;
459 }
460 populateView();
461 }
462 }
463
464 @Override
465 public void onKeyStatusUpdated(AxolotlService.FetchStatus report) {
466 refreshUi();
467 }
468
469 @Override
470 protected void processFingerprintVerification(XmppUri uri) {
471 if (contact != null && contact.getJid().asBareJid().equals(uri.getJid()) && uri.hasFingerprints()) {
472 if (xmppConnectionService.verifyFingerprints(contact, uri.getFingerprints())) {
473 Toast.makeText(this, R.string.verified_fingerprints, Toast.LENGTH_SHORT).show();
474 }
475 } else {
476 Toast.makeText(this, R.string.invalid_barcode, Toast.LENGTH_SHORT).show();
477 }
478 }
479}