1package eu.siacs.conversations.ui;
2
3import android.annotation.SuppressLint;
4import android.annotation.TargetApi;
5import android.app.ActionBar;
6import android.app.Activity;
7import android.app.AlertDialog;
8import android.app.AlertDialog.Builder;
9import android.app.PendingIntent;
10import android.content.ClipData;
11import android.content.ClipboardManager;
12import android.content.ComponentName;
13import android.content.Context;
14import android.content.DialogInterface;
15import android.content.DialogInterface.OnClickListener;
16import android.content.Intent;
17import android.content.IntentSender.SendIntentException;
18import android.content.ServiceConnection;
19import android.content.SharedPreferences;
20import android.content.pm.PackageManager;
21import android.content.pm.ResolveInfo;
22import android.content.res.Resources;
23import android.graphics.Bitmap;
24import android.graphics.Color;
25import android.graphics.Point;
26import android.graphics.drawable.BitmapDrawable;
27import android.graphics.drawable.Drawable;
28import android.net.Uri;
29import android.nfc.NdefMessage;
30import android.nfc.NdefRecord;
31import android.nfc.NfcAdapter;
32import android.nfc.NfcEvent;
33import android.os.AsyncTask;
34import android.os.Build;
35import android.os.Bundle;
36import android.os.Handler;
37import android.os.IBinder;
38import android.os.SystemClock;
39import android.preference.PreferenceManager;
40import android.text.InputType;
41import android.util.DisplayMetrics;
42import android.util.Log;
43import android.view.MenuItem;
44import android.view.View;
45import android.view.inputmethod.InputMethodManager;
46import android.widget.CompoundButton;
47import android.widget.EditText;
48import android.widget.ImageView;
49import android.widget.LinearLayout;
50import android.widget.TextView;
51import android.widget.Toast;
52
53import com.google.zxing.BarcodeFormat;
54import com.google.zxing.EncodeHintType;
55import com.google.zxing.WriterException;
56import com.google.zxing.common.BitMatrix;
57import com.google.zxing.qrcode.QRCodeWriter;
58import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
59
60import net.java.otr4j.session.SessionID;
61
62import java.io.FileNotFoundException;
63import java.lang.ref.WeakReference;
64import java.util.ArrayList;
65import java.util.Hashtable;
66import java.util.List;
67import java.util.concurrent.RejectedExecutionException;
68
69import eu.siacs.conversations.Config;
70import eu.siacs.conversations.R;
71import eu.siacs.conversations.crypto.axolotl.XmppAxolotlSession;
72import eu.siacs.conversations.entities.Account;
73import eu.siacs.conversations.entities.Contact;
74import eu.siacs.conversations.entities.Conversation;
75import eu.siacs.conversations.entities.Message;
76import eu.siacs.conversations.entities.MucOptions;
77import eu.siacs.conversations.entities.Presences;
78import eu.siacs.conversations.services.AvatarService;
79import eu.siacs.conversations.services.XmppConnectionService;
80import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
81import eu.siacs.conversations.ui.widget.Switch;
82import eu.siacs.conversations.utils.CryptoHelper;
83import eu.siacs.conversations.utils.ExceptionHelper;
84import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
85import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
86import eu.siacs.conversations.xmpp.jid.InvalidJidException;
87import eu.siacs.conversations.xmpp.jid.Jid;
88
89public abstract class XmppActivity extends Activity {
90
91 protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
92 protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
93 protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103;
94
95 public XmppConnectionService xmppConnectionService;
96 public boolean xmppConnectionServiceBound = false;
97 protected boolean registeredListeners = false;
98
99 protected int mPrimaryTextColor;
100 protected int mSecondaryTextColor;
101 protected int mTertiaryTextColor;
102 protected int mPrimaryBackgroundColor;
103 protected int mSecondaryBackgroundColor;
104 protected int mColorRed;
105 protected int mColorOrange;
106 protected int mColorGreen;
107 protected int mPrimaryColor;
108
109 protected boolean mUseSubject = true;
110
111 private DisplayMetrics metrics;
112 protected int mTheme;
113 protected boolean mUsingEnterKey = false;
114
115 private long mLastUiRefresh = 0;
116 private Handler mRefreshUiHandler = new Handler();
117 private Runnable mRefreshUiRunnable = new Runnable() {
118 @Override
119 public void run() {
120 mLastUiRefresh = SystemClock.elapsedRealtime();
121 refreshUiReal();
122 }
123 };
124
125 protected ConferenceInvite mPendingConferenceInvite = null;
126
127
128 protected final void refreshUi() {
129 final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh;
130 if (diff > Config.REFRESH_UI_INTERVAL) {
131 mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
132 runOnUiThread(mRefreshUiRunnable);
133 } else {
134 final long next = Config.REFRESH_UI_INTERVAL - diff;
135 mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
136 mRefreshUiHandler.postDelayed(mRefreshUiRunnable,next);
137 }
138 }
139
140 abstract protected void refreshUiReal();
141
142 protected interface OnValueEdited {
143 public void onValueEdited(String value);
144 }
145
146 public interface OnPresenceSelected {
147 public void onPresenceSelected();
148 }
149
150 protected ServiceConnection mConnection = new ServiceConnection() {
151
152 @Override
153 public void onServiceConnected(ComponentName className, IBinder service) {
154 XmppConnectionBinder binder = (XmppConnectionBinder) service;
155 xmppConnectionService = binder.getService();
156 xmppConnectionServiceBound = true;
157 if (!registeredListeners && shouldRegisterListeners()) {
158 registerListeners();
159 registeredListeners = true;
160 }
161 onBackendConnected();
162 }
163
164 @Override
165 public void onServiceDisconnected(ComponentName arg0) {
166 xmppConnectionServiceBound = false;
167 }
168 };
169
170 @Override
171 protected void onStart() {
172 super.onStart();
173 if (!xmppConnectionServiceBound) {
174 connectToBackend();
175 } else {
176 if (!registeredListeners) {
177 this.registerListeners();
178 this.registeredListeners = true;
179 }
180 this.onBackendConnected();
181 }
182 }
183
184 @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
185 protected boolean shouldRegisterListeners() {
186 if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
187 return !isDestroyed() && !isFinishing();
188 } else {
189 return !isFinishing();
190 }
191 }
192
193 public void connectToBackend() {
194 Intent intent = new Intent(this, XmppConnectionService.class);
195 intent.setAction("ui");
196 startService(intent);
197 bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
198 }
199
200 @Override
201 protected void onStop() {
202 super.onStop();
203 if (xmppConnectionServiceBound) {
204 if (registeredListeners) {
205 this.unregisterListeners();
206 this.registeredListeners = false;
207 }
208 unbindService(mConnection);
209 xmppConnectionServiceBound = false;
210 }
211 }
212
213 protected void hideKeyboard() {
214 InputMethodManager inputManager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
215
216 View focus = getCurrentFocus();
217
218 if (focus != null) {
219
220 inputManager.hideSoftInputFromWindow(focus.getWindowToken(),
221 InputMethodManager.HIDE_NOT_ALWAYS);
222 }
223 }
224
225 public boolean hasPgp() {
226 return xmppConnectionService.getPgpEngine() != null;
227 }
228
229 public void showInstallPgpDialog() {
230 Builder builder = new AlertDialog.Builder(this);
231 builder.setTitle(getString(R.string.openkeychain_required));
232 builder.setIconAttribute(android.R.attr.alertDialogIcon);
233 builder.setMessage(getText(R.string.openkeychain_required_long));
234 builder.setNegativeButton(getString(R.string.cancel), null);
235 builder.setNeutralButton(getString(R.string.restart),
236 new OnClickListener() {
237
238 @Override
239 public void onClick(DialogInterface dialog, int which) {
240 if (xmppConnectionServiceBound) {
241 unbindService(mConnection);
242 xmppConnectionServiceBound = false;
243 }
244 stopService(new Intent(XmppActivity.this,
245 XmppConnectionService.class));
246 finish();
247 }
248 });
249 builder.setPositiveButton(getString(R.string.install),
250 new OnClickListener() {
251
252 @Override
253 public void onClick(DialogInterface dialog, int which) {
254 Uri uri = Uri
255 .parse("market://details?id=org.sufficientlysecure.keychain");
256 Intent marketIntent = new Intent(Intent.ACTION_VIEW,
257 uri);
258 PackageManager manager = getApplicationContext()
259 .getPackageManager();
260 List<ResolveInfo> infos = manager
261 .queryIntentActivities(marketIntent, 0);
262 if (infos.size() > 0) {
263 startActivity(marketIntent);
264 } else {
265 uri = Uri.parse("http://www.openkeychain.org/");
266 Intent browserIntent = new Intent(
267 Intent.ACTION_VIEW, uri);
268 startActivity(browserIntent);
269 }
270 finish();
271 }
272 });
273 builder.create().show();
274 }
275
276 abstract void onBackendConnected();
277
278 protected void registerListeners() {
279 if (this instanceof XmppConnectionService.OnConversationUpdate) {
280 this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this);
281 }
282 if (this instanceof XmppConnectionService.OnAccountUpdate) {
283 this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
284 }
285 if (this instanceof XmppConnectionService.OnCaptchaRequested) {
286 this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
287 }
288 if (this instanceof XmppConnectionService.OnRosterUpdate) {
289 this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
290 }
291 if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
292 this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this);
293 }
294 if (this instanceof OnUpdateBlocklist) {
295 this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this);
296 }
297 if (this instanceof XmppConnectionService.OnShowErrorToast) {
298 this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
299 }
300 if (this instanceof OnKeyStatusUpdated) {
301 this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this);
302 }
303 }
304
305 protected void unregisterListeners() {
306 if (this instanceof XmppConnectionService.OnConversationUpdate) {
307 this.xmppConnectionService.removeOnConversationListChangedListener();
308 }
309 if (this instanceof XmppConnectionService.OnAccountUpdate) {
310 this.xmppConnectionService.removeOnAccountListChangedListener();
311 }
312 if (this instanceof XmppConnectionService.OnCaptchaRequested) {
313 this.xmppConnectionService.removeOnCaptchaRequestedListener();
314 }
315 if (this instanceof XmppConnectionService.OnRosterUpdate) {
316 this.xmppConnectionService.removeOnRosterUpdateListener();
317 }
318 if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
319 this.xmppConnectionService.removeOnMucRosterUpdateListener();
320 }
321 if (this instanceof OnUpdateBlocklist) {
322 this.xmppConnectionService.removeOnUpdateBlocklistListener();
323 }
324 if (this instanceof XmppConnectionService.OnShowErrorToast) {
325 this.xmppConnectionService.removeOnShowErrorToastListener();
326 }
327 if (this instanceof OnKeyStatusUpdated) {
328 this.xmppConnectionService.removeOnNewKeysAvailableListener();
329 }
330 }
331
332 @Override
333 public boolean onOptionsItemSelected(final MenuItem item) {
334 switch (item.getItemId()) {
335 case R.id.action_settings:
336 startActivity(new Intent(this, SettingsActivity.class));
337 break;
338 case R.id.action_accounts:
339 startActivity(new Intent(this, ManageAccountActivity.class));
340 break;
341 case android.R.id.home:
342 finish();
343 break;
344 case R.id.action_show_qr_code:
345 showQrCode();
346 break;
347 }
348 return super.onOptionsItemSelected(item);
349 }
350
351 @Override
352 protected void onCreate(Bundle savedInstanceState) {
353 super.onCreate(savedInstanceState);
354 metrics = getResources().getDisplayMetrics();
355 ExceptionHelper.init(getApplicationContext());
356 mPrimaryTextColor = getResources().getColor(R.color.black87);
357 mSecondaryTextColor = getResources().getColor(R.color.black54);
358 mTertiaryTextColor = getResources().getColor(R.color.black12);
359 mColorRed = getResources().getColor(R.color.red800);
360 mColorOrange = getResources().getColor(R.color.orange500);
361 mColorGreen = getResources().getColor(R.color.green500);
362 mPrimaryColor = getResources().getColor(R.color.primary);
363 mPrimaryBackgroundColor = getResources().getColor(R.color.grey50);
364 mSecondaryBackgroundColor = getResources().getColor(R.color.grey200);
365 this.mTheme = findTheme();
366 setTheme(this.mTheme);
367 this.mUsingEnterKey = usingEnterKey();
368 mUseSubject = getPreferences().getBoolean("use_subject", true);
369 final ActionBar ab = getActionBar();
370 if (ab!=null) {
371 ab.setDisplayHomeAsUpEnabled(true);
372 }
373 }
374
375 protected boolean usingEnterKey() {
376 return getPreferences().getBoolean("display_enter_key", false);
377 }
378
379 protected SharedPreferences getPreferences() {
380 return PreferenceManager
381 .getDefaultSharedPreferences(getApplicationContext());
382 }
383
384 public boolean useSubjectToIdentifyConference() {
385 return mUseSubject;
386 }
387
388 public void switchToConversation(Conversation conversation) {
389 switchToConversation(conversation, null, false);
390 }
391
392 public void switchToConversation(Conversation conversation, String text,
393 boolean newTask) {
394 switchToConversation(conversation,text,null,false,newTask);
395 }
396
397 public void highlightInMuc(Conversation conversation, String nick) {
398 switchToConversation(conversation, null, nick, false, false);
399 }
400
401 public void privateMsgInMuc(Conversation conversation, String nick) {
402 switchToConversation(conversation, null, nick, true, false);
403 }
404
405 private void switchToConversation(Conversation conversation, String text, String nick, boolean pm, boolean newTask) {
406 Intent viewConversationIntent = new Intent(this,
407 ConversationActivity.class);
408 viewConversationIntent.setAction(Intent.ACTION_VIEW);
409 viewConversationIntent.putExtra(ConversationActivity.CONVERSATION,
410 conversation.getUuid());
411 if (text != null) {
412 viewConversationIntent.putExtra(ConversationActivity.TEXT, text);
413 }
414 if (nick != null) {
415 viewConversationIntent.putExtra(ConversationActivity.NICK, nick);
416 viewConversationIntent.putExtra(ConversationActivity.PRIVATE_MESSAGE,pm);
417 }
418 viewConversationIntent.setType(ConversationActivity.VIEW_CONVERSATION);
419 if (newTask) {
420 viewConversationIntent.setFlags(viewConversationIntent.getFlags()
421 | Intent.FLAG_ACTIVITY_NEW_TASK
422 | Intent.FLAG_ACTIVITY_SINGLE_TOP);
423 } else {
424 viewConversationIntent.setFlags(viewConversationIntent.getFlags()
425 | Intent.FLAG_ACTIVITY_CLEAR_TOP);
426 }
427 startActivity(viewConversationIntent);
428 finish();
429 }
430
431 public void switchToContactDetails(Contact contact) {
432 switchToContactDetails(contact, null);
433 }
434
435 public void switchToContactDetails(Contact contact, String messageFingerprint) {
436 Intent intent = new Intent(this, ContactDetailsActivity.class);
437 intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
438 intent.putExtra("account", contact.getAccount().getJid().toBareJid().toString());
439 intent.putExtra("contact", contact.getJid().toString());
440 intent.putExtra("fingerprint", messageFingerprint);
441 startActivity(intent);
442 }
443
444 public void switchToAccount(Account account) {
445 switchToAccount(account, false);
446 }
447
448 public void switchToAccount(Account account, boolean init) {
449 Intent intent = new Intent(this, EditAccountActivity.class);
450 intent.putExtra("jid", account.getJid().toBareJid().toString());
451 intent.putExtra("init", init);
452 startActivity(intent);
453 }
454
455 protected void inviteToConversation(Conversation conversation) {
456 Intent intent = new Intent(getApplicationContext(),
457 ChooseContactActivity.class);
458 List<String> contacts = new ArrayList<>();
459 if (conversation.getMode() == Conversation.MODE_MULTI) {
460 for (MucOptions.User user : conversation.getMucOptions().getUsers()) {
461 Jid jid = user.getJid();
462 if (jid != null) {
463 contacts.add(jid.toBareJid().toString());
464 }
465 }
466 } else {
467 contacts.add(conversation.getJid().toBareJid().toString());
468 }
469 intent.putExtra("filter_contacts", contacts.toArray(new String[contacts.size()]));
470 intent.putExtra("conversation", conversation.getUuid());
471 intent.putExtra("multiple", true);
472 startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION);
473 }
474
475 protected void announcePgp(Account account, final Conversation conversation) {
476 if (account.getPgpId() == -1) {
477 choosePgpSignId(account);
478 } else {
479 xmppConnectionService.getPgpEngine().generateSignature(account, "", new UiCallback<Account>() {
480
481 @Override
482 public void userInputRequried(PendingIntent pi,
483 Account account) {
484 try {
485 startIntentSenderForResult(pi.getIntentSender(),
486 REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
487 } catch (final SendIntentException ignored) {
488 }
489 }
490
491 @Override
492 public void success(Account account) {
493 xmppConnectionService.databaseBackend.updateAccount(account);
494 xmppConnectionService.sendPresence(account);
495 if (conversation != null) {
496 conversation.setNextEncryption(Message.ENCRYPTION_PGP);
497 xmppConnectionService.databaseBackend.updateConversation(conversation);
498 }
499 }
500
501 @Override
502 public void error(int error, Account account) {
503 displayErrorDialog(error);
504 }
505 });
506 }
507 }
508
509 protected void choosePgpSignId(Account account) {
510 xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<Account>() {
511 @Override
512 public void success(Account account1) {
513 }
514
515 @Override
516 public void error(int errorCode, Account object) {
517
518 }
519
520 @Override
521 public void userInputRequried(PendingIntent pi, Account object) {
522 try {
523 startIntentSenderForResult(pi.getIntentSender(),
524 REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0);
525 } catch (final SendIntentException ignored) {
526 }
527 }
528 });
529 }
530
531 protected void displayErrorDialog(final int errorCode) {
532 runOnUiThread(new Runnable() {
533
534 @Override
535 public void run() {
536 AlertDialog.Builder builder = new AlertDialog.Builder(
537 XmppActivity.this);
538 builder.setIconAttribute(android.R.attr.alertDialogIcon);
539 builder.setTitle(getString(R.string.error));
540 builder.setMessage(errorCode);
541 builder.setNeutralButton(R.string.accept, null);
542 builder.create().show();
543 }
544 });
545
546 }
547
548 protected void showAddToRosterDialog(final Conversation conversation) {
549 showAddToRosterDialog(conversation.getContact());
550 }
551
552 protected void showAddToRosterDialog(final Contact contact) {
553 AlertDialog.Builder builder = new AlertDialog.Builder(this);
554 builder.setTitle(contact.getJid().toString());
555 builder.setMessage(getString(R.string.not_in_roster));
556 builder.setNegativeButton(getString(R.string.cancel), null);
557 builder.setPositiveButton(getString(R.string.add_contact),
558 new DialogInterface.OnClickListener() {
559
560 @Override
561 public void onClick(DialogInterface dialog, int which) {
562 final Jid jid = contact.getJid();
563 Account account = contact.getAccount();
564 Contact contact = account.getRoster().getContact(jid);
565 xmppConnectionService.createContact(contact);
566 }
567 });
568 builder.create().show();
569 }
570
571 private void showAskForPresenceDialog(final Contact contact) {
572 AlertDialog.Builder builder = new AlertDialog.Builder(this);
573 builder.setTitle(contact.getJid().toString());
574 builder.setMessage(R.string.request_presence_updates);
575 builder.setNegativeButton(R.string.cancel, null);
576 builder.setPositiveButton(R.string.request_now,
577 new DialogInterface.OnClickListener() {
578
579 @Override
580 public void onClick(DialogInterface dialog, int which) {
581 if (xmppConnectionServiceBound) {
582 xmppConnectionService.sendPresencePacket(contact
583 .getAccount(), xmppConnectionService
584 .getPresenceGenerator()
585 .requestPresenceUpdatesFrom(contact));
586 }
587 }
588 });
589 builder.create().show();
590 }
591
592 private void warnMutalPresenceSubscription(final Conversation conversation,
593 final OnPresenceSelected listener) {
594 AlertDialog.Builder builder = new AlertDialog.Builder(this);
595 builder.setTitle(conversation.getContact().getJid().toString());
596 builder.setMessage(R.string.without_mutual_presence_updates);
597 builder.setNegativeButton(R.string.cancel, null);
598 builder.setPositiveButton(R.string.ignore, new OnClickListener() {
599
600 @Override
601 public void onClick(DialogInterface dialog, int which) {
602 conversation.setNextCounterpart(null);
603 if (listener != null) {
604 listener.onPresenceSelected();
605 }
606 }
607 });
608 builder.create().show();
609 }
610
611 protected void quickEdit(String previousValue, OnValueEdited callback) {
612 quickEdit(previousValue, callback, false);
613 }
614
615 protected void quickPasswordEdit(String previousValue,
616 OnValueEdited callback) {
617 quickEdit(previousValue, callback, true);
618 }
619
620 @SuppressLint("InflateParams")
621 private void quickEdit(final String previousValue,
622 final OnValueEdited callback, boolean password) {
623 AlertDialog.Builder builder = new AlertDialog.Builder(this);
624 View view = getLayoutInflater().inflate(R.layout.quickedit, null);
625 final EditText editor = (EditText) view.findViewById(R.id.editor);
626 OnClickListener mClickListener = new OnClickListener() {
627
628 @Override
629 public void onClick(DialogInterface dialog, int which) {
630 String value = editor.getText().toString();
631 if (!previousValue.equals(value) && value.trim().length() > 0) {
632 callback.onValueEdited(value);
633 }
634 }
635 };
636 if (password) {
637 editor.setInputType(InputType.TYPE_CLASS_TEXT
638 | InputType.TYPE_TEXT_VARIATION_PASSWORD);
639 editor.setHint(R.string.password);
640 builder.setPositiveButton(R.string.accept, mClickListener);
641 } else {
642 builder.setPositiveButton(R.string.edit, mClickListener);
643 }
644 editor.requestFocus();
645 editor.setText(previousValue);
646 builder.setView(view);
647 builder.setNegativeButton(R.string.cancel, null);
648 builder.create().show();
649 }
650
651 protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight, View.OnClickListener onKeyClickedListener) {
652 final XmppAxolotlSession.Trust trust = account.getAxolotlService()
653 .getFingerprintTrust(fingerprint);
654 if (trust == null) {
655 return false;
656 }
657 return addFingerprintRowWithListeners(keys, account, fingerprint, highlight, trust, true,
658 new CompoundButton.OnCheckedChangeListener() {
659 @Override
660 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
661 account.getAxolotlService().setFingerprintTrust(fingerprint,
662 (isChecked) ? XmppAxolotlSession.Trust.TRUSTED :
663 XmppAxolotlSession.Trust.UNTRUSTED);
664 }
665 },
666 new View.OnClickListener() {
667 @Override
668 public void onClick(View v) {
669 account.getAxolotlService().setFingerprintTrust(fingerprint,
670 XmppAxolotlSession.Trust.UNTRUSTED);
671 v.setEnabled(true);
672 }
673 },
674 onKeyClickedListener
675
676 );
677 }
678
679 protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account,
680 final String fingerprint,
681 boolean highlight,
682 XmppAxolotlSession.Trust trust,
683 boolean showTag,
684 CompoundButton.OnCheckedChangeListener
685 onCheckedChangeListener,
686 View.OnClickListener onClickListener,
687 View.OnClickListener onKeyClickedListener) {
688 if (trust == XmppAxolotlSession.Trust.COMPROMISED) {
689 return false;
690 }
691 View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
692 TextView key = (TextView) view.findViewById(R.id.key);
693 key.setOnClickListener(onKeyClickedListener);
694 TextView keyType = (TextView) view.findViewById(R.id.key_type);
695 keyType.setOnClickListener(onKeyClickedListener);
696 Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
697 trustToggle.setVisibility(View.VISIBLE);
698 trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
699 trustToggle.setOnClickListener(onClickListener);
700 view.setOnLongClickListener(new View.OnLongClickListener() {
701 @Override
702 public boolean onLongClick(View v) {
703 showPurgeKeyDialog(account, fingerprint);
704 return true;
705 }
706 });
707 boolean x509 = trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509;
708 switch (trust) {
709 case UNTRUSTED:
710 case TRUSTED:
711 case TRUSTED_X509:
712 trustToggle.setChecked(trust.trusted(), false);
713 trustToggle.setEnabled(trust != XmppAxolotlSession.Trust.TRUSTED_X509);
714 if (trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
715 trustToggle.setOnClickListener(null);
716 }
717 key.setTextColor(getPrimaryTextColor());
718 keyType.setTextColor(getSecondaryTextColor());
719 break;
720 case UNDECIDED:
721 trustToggle.setChecked(false, false);
722 trustToggle.setEnabled(false);
723 key.setTextColor(getPrimaryTextColor());
724 keyType.setTextColor(getSecondaryTextColor());
725 break;
726 case INACTIVE_UNTRUSTED:
727 case INACTIVE_UNDECIDED:
728 trustToggle.setOnClickListener(null);
729 trustToggle.setChecked(false, false);
730 trustToggle.setEnabled(false);
731 key.setTextColor(getTertiaryTextColor());
732 keyType.setTextColor(getTertiaryTextColor());
733 break;
734 case INACTIVE_TRUSTED:
735 case INACTIVE_TRUSTED_X509:
736 trustToggle.setOnClickListener(null);
737 trustToggle.setChecked(true, false);
738 trustToggle.setEnabled(false);
739 key.setTextColor(getTertiaryTextColor());
740 keyType.setTextColor(getTertiaryTextColor());
741 break;
742 }
743
744 if (showTag) {
745 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
746 } else {
747 keyType.setVisibility(View.GONE);
748 }
749 if (highlight) {
750 keyType.setTextColor(getResources().getColor(R.color.accent));
751 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
752 } else {
753 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
754 }
755
756 key.setText(CryptoHelper.prettifyFingerprint(fingerprint));
757 keys.addView(view);
758 return true;
759 }
760
761 public void showPurgeKeyDialog(final Account account, final String fingerprint) {
762 Builder builder = new Builder(this);
763 builder.setTitle(getString(R.string.purge_key));
764 builder.setIconAttribute(android.R.attr.alertDialogIcon);
765 builder.setMessage(getString(R.string.purge_key_desc_part1)
766 + "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint)
767 + "\n\n" + getString(R.string.purge_key_desc_part2));
768 builder.setNegativeButton(getString(R.string.cancel), null);
769 builder.setPositiveButton(getString(R.string.accept),
770 new DialogInterface.OnClickListener() {
771 @Override
772 public void onClick(DialogInterface dialog, int which) {
773 account.getAxolotlService().purgeKey(fingerprint);
774 refreshUi();
775 }
776 });
777 builder.create().show();
778 }
779
780 public void selectPresence(final Conversation conversation,
781 final OnPresenceSelected listener) {
782 final Contact contact = conversation.getContact();
783 if (conversation.hasValidOtrSession()) {
784 SessionID id = conversation.getOtrSession().getSessionID();
785 Jid jid;
786 try {
787 jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID());
788 } catch (InvalidJidException e) {
789 jid = null;
790 }
791 conversation.setNextCounterpart(jid);
792 listener.onPresenceSelected();
793 } else if (!contact.showInRoster()) {
794 showAddToRosterDialog(conversation);
795 } else {
796 Presences presences = contact.getPresences();
797 if (presences.size() == 0) {
798 if (!contact.getOption(Contact.Options.TO)
799 && !contact.getOption(Contact.Options.ASKING)
800 && contact.getAccount().getStatus() == Account.State.ONLINE) {
801 showAskForPresenceDialog(contact);
802 } else if (!contact.getOption(Contact.Options.TO)
803 || !contact.getOption(Contact.Options.FROM)) {
804 warnMutalPresenceSubscription(conversation, listener);
805 } else {
806 conversation.setNextCounterpart(null);
807 listener.onPresenceSelected();
808 }
809 } else if (presences.size() == 1) {
810 String presence = presences.asStringArray()[0];
811 try {
812 conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence));
813 } catch (InvalidJidException e) {
814 conversation.setNextCounterpart(null);
815 }
816 listener.onPresenceSelected();
817 } else {
818 final StringBuilder presence = new StringBuilder();
819 AlertDialog.Builder builder = new AlertDialog.Builder(this);
820 builder.setTitle(getString(R.string.choose_presence));
821 final String[] presencesArray = presences.asStringArray();
822 int preselectedPresence = 0;
823 for (int i = 0; i < presencesArray.length; ++i) {
824 if (presencesArray[i].equals(contact.lastseen.presence)) {
825 preselectedPresence = i;
826 break;
827 }
828 }
829 presence.append(presencesArray[preselectedPresence]);
830 builder.setSingleChoiceItems(presencesArray,
831 preselectedPresence,
832 new DialogInterface.OnClickListener() {
833
834 @Override
835 public void onClick(DialogInterface dialog,
836 int which) {
837 presence.delete(0, presence.length());
838 presence.append(presencesArray[which]);
839 }
840 });
841 builder.setNegativeButton(R.string.cancel, null);
842 builder.setPositiveButton(R.string.ok, new OnClickListener() {
843
844 @Override
845 public void onClick(DialogInterface dialog, int which) {
846 try {
847 conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString()));
848 } catch (InvalidJidException e) {
849 conversation.setNextCounterpart(null);
850 }
851 listener.onPresenceSelected();
852 }
853 });
854 builder.create().show();
855 }
856 }
857 }
858
859 protected void onActivityResult(int requestCode, int resultCode,
860 final Intent data) {
861 super.onActivityResult(requestCode, resultCode, data);
862 if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
863 mPendingConferenceInvite = ConferenceInvite.parse(data);
864 if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
865 mPendingConferenceInvite.execute(this);
866 mPendingConferenceInvite = null;
867 }
868 }
869 }
870
871 private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
872 @Override
873 public void success(final Conversation conversation) {
874 switchToConversation(conversation);
875 runOnUiThread(new Runnable() {
876 @Override
877 public void run() {
878 Toast.makeText(XmppActivity.this,R.string.conference_created,Toast.LENGTH_LONG).show();
879 }
880 });
881 }
882
883 @Override
884 public void error(final int errorCode, Conversation object) {
885 runOnUiThread(new Runnable() {
886 @Override
887 public void run() {
888 Toast.makeText(XmppActivity.this,errorCode,Toast.LENGTH_LONG).show();
889 }
890 });
891 }
892
893 @Override
894 public void userInputRequried(PendingIntent pi, Conversation object) {
895
896 }
897 };
898
899 public int getTertiaryTextColor() {
900 return this.mTertiaryTextColor;
901 }
902
903 public int getSecondaryTextColor() {
904 return this.mSecondaryTextColor;
905 }
906
907 public int getPrimaryTextColor() {
908 return this.mPrimaryTextColor;
909 }
910
911 public int getWarningTextColor() {
912 return this.mColorRed;
913 }
914
915 public int getOnlineColor() {
916 return this.mColorGreen;
917 }
918
919 public int getPrimaryBackgroundColor() {
920 return this.mPrimaryBackgroundColor;
921 }
922
923 public int getSecondaryBackgroundColor() {
924 return this.mSecondaryBackgroundColor;
925 }
926
927 public int getPixel(int dp) {
928 DisplayMetrics metrics = getResources().getDisplayMetrics();
929 return ((int) (dp * metrics.density));
930 }
931
932 public boolean copyTextToClipboard(String text, int labelResId) {
933 ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
934 String label = getResources().getString(labelResId);
935 if (mClipBoardManager != null) {
936 ClipData mClipData = ClipData.newPlainText(label, text);
937 mClipBoardManager.setPrimaryClip(mClipData);
938 return true;
939 }
940 return false;
941 }
942
943 protected void registerNdefPushMessageCallback() {
944 NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
945 if (nfcAdapter != null && nfcAdapter.isEnabled()) {
946 nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
947 @Override
948 public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
949 return new NdefMessage(new NdefRecord[]{
950 NdefRecord.createUri(getShareableUri()),
951 NdefRecord.createApplicationRecord("eu.siacs.conversations")
952 });
953 }
954 }, this);
955 }
956 }
957
958 protected void unregisterNdefPushMessageCallback() {
959 NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
960 if (nfcAdapter != null && nfcAdapter.isEnabled()) {
961 nfcAdapter.setNdefPushMessageCallback(null,this);
962 }
963 }
964
965 protected String getShareableUri() {
966 return null;
967 }
968
969 @Override
970 public void onResume() {
971 super.onResume();
972 if (this.getShareableUri()!=null) {
973 this.registerNdefPushMessageCallback();
974 }
975 }
976
977 protected int findTheme() {
978 if (getPreferences().getBoolean("use_larger_font", false)) {
979 return R.style.ConversationsTheme_LargerText;
980 } else {
981 return R.style.ConversationsTheme;
982 }
983 }
984
985 @Override
986 public void onPause() {
987 super.onPause();
988 this.unregisterNdefPushMessageCallback();
989 }
990
991 protected void showQrCode() {
992 String uri = getShareableUri();
993 if (uri!=null) {
994 Point size = new Point();
995 getWindowManager().getDefaultDisplay().getSize(size);
996 final int width = (size.x < size.y ? size.x : size.y);
997 Bitmap bitmap = createQrCodeBitmap(uri, width);
998 ImageView view = new ImageView(this);
999 view.setImageBitmap(bitmap);
1000 AlertDialog.Builder builder = new AlertDialog.Builder(this);
1001 builder.setView(view);
1002 builder.create().show();
1003 }
1004 }
1005
1006 protected Bitmap createQrCodeBitmap(String input, int size) {
1007 Log.d(Config.LOGTAG,"qr code requested size: "+size);
1008 try {
1009 final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
1010 final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
1011 hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
1012 final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
1013 final int width = result.getWidth();
1014 final int height = result.getHeight();
1015 final int[] pixels = new int[width * height];
1016 for (int y = 0; y < height; y++) {
1017 final int offset = y * width;
1018 for (int x = 0; x < width; x++) {
1019 pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
1020 }
1021 }
1022 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1023 Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
1024 bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
1025 return bitmap;
1026 } catch (final WriterException e) {
1027 return null;
1028 }
1029 }
1030
1031 public static class ConferenceInvite {
1032 private String uuid;
1033 private List<Jid> jids = new ArrayList<>();
1034
1035 public static ConferenceInvite parse(Intent data) {
1036 ConferenceInvite invite = new ConferenceInvite();
1037 invite.uuid = data.getStringExtra("conversation");
1038 if (invite.uuid == null) {
1039 return null;
1040 }
1041 try {
1042 if (data.getBooleanExtra("multiple", false)) {
1043 String[] toAdd = data.getStringArrayExtra("contacts");
1044 for (String item : toAdd) {
1045 invite.jids.add(Jid.fromString(item));
1046 }
1047 } else {
1048 invite.jids.add(Jid.fromString(data.getStringExtra("contact")));
1049 }
1050 } catch (final InvalidJidException ignored) {
1051 return null;
1052 }
1053 return invite;
1054 }
1055
1056 public void execute(XmppActivity activity) {
1057 XmppConnectionService service = activity.xmppConnectionService;
1058 Conversation conversation = service.findConversationByUuid(this.uuid);
1059 if (conversation == null) {
1060 return;
1061 }
1062 if (conversation.getMode() == Conversation.MODE_MULTI) {
1063 for (Jid jid : jids) {
1064 service.invite(conversation, jid);
1065 }
1066 } else {
1067 jids.add(conversation.getJid().toBareJid());
1068 service.createAdhocConference(conversation.getAccount(), jids, activity.adhocCallback);
1069 }
1070 }
1071 }
1072
1073 public AvatarService avatarService() {
1074 return xmppConnectionService.getAvatarService();
1075 }
1076
1077 class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
1078 private final WeakReference<ImageView> imageViewReference;
1079 private Message message = null;
1080
1081 public BitmapWorkerTask(ImageView imageView) {
1082 imageViewReference = new WeakReference<>(imageView);
1083 }
1084
1085 @Override
1086 protected Bitmap doInBackground(Message... params) {
1087 message = params[0];
1088 try {
1089 return xmppConnectionService.getFileBackend().getThumbnail(
1090 message, (int) (metrics.density * 288), false);
1091 } catch (FileNotFoundException e) {
1092 return null;
1093 }
1094 }
1095
1096 @Override
1097 protected void onPostExecute(Bitmap bitmap) {
1098 if (bitmap != null) {
1099 final ImageView imageView = imageViewReference.get();
1100 if (imageView != null) {
1101 imageView.setImageBitmap(bitmap);
1102 imageView.setBackgroundColor(0x00000000);
1103 }
1104 }
1105 }
1106 }
1107
1108 public void loadBitmap(Message message, ImageView imageView) {
1109 Bitmap bm;
1110 try {
1111 bm = xmppConnectionService.getFileBackend().getThumbnail(message,
1112 (int) (metrics.density * 288), true);
1113 } catch (FileNotFoundException e) {
1114 bm = null;
1115 }
1116 if (bm != null) {
1117 imageView.setImageBitmap(bm);
1118 imageView.setBackgroundColor(0x00000000);
1119 } else {
1120 if (cancelPotentialWork(message, imageView)) {
1121 imageView.setBackgroundColor(0xff333333);
1122 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1123 final AsyncDrawable asyncDrawable = new AsyncDrawable(
1124 getResources(), null, task);
1125 imageView.setImageDrawable(asyncDrawable);
1126 try {
1127 task.execute(message);
1128 } catch (final RejectedExecutionException ignored) {
1129 }
1130 }
1131 }
1132 }
1133
1134 public static boolean cancelPotentialWork(Message message,
1135 ImageView imageView) {
1136 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
1137
1138 if (bitmapWorkerTask != null) {
1139 final Message oldMessage = bitmapWorkerTask.message;
1140 if (oldMessage == null || message != oldMessage) {
1141 bitmapWorkerTask.cancel(true);
1142 } else {
1143 return false;
1144 }
1145 }
1146 return true;
1147 }
1148
1149 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
1150 if (imageView != null) {
1151 final Drawable drawable = imageView.getDrawable();
1152 if (drawable instanceof AsyncDrawable) {
1153 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
1154 return asyncDrawable.getBitmapWorkerTask();
1155 }
1156 }
1157 return null;
1158 }
1159
1160 static class AsyncDrawable extends BitmapDrawable {
1161 private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1162
1163 public AsyncDrawable(Resources res, Bitmap bitmap,
1164 BitmapWorkerTask bitmapWorkerTask) {
1165 super(res, bitmap);
1166 bitmapWorkerTaskReference = new WeakReference<>(
1167 bitmapWorkerTask);
1168 }
1169
1170 public BitmapWorkerTask getBitmapWorkerTask() {
1171 return bitmapWorkerTaskReference.get();
1172 }
1173 }
1174}