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 final View.OnLongClickListener purge = new View.OnLongClickListener() {
701 @Override
702 public boolean onLongClick(View v) {
703 showPurgeKeyDialog(account, fingerprint);
704 return true;
705 }
706 };
707 view.setOnLongClickListener(purge);
708 key.setOnLongClickListener(purge);
709 keyType.setOnLongClickListener(purge);
710 boolean x509 = trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509;
711 switch (trust) {
712 case UNTRUSTED:
713 case TRUSTED:
714 case TRUSTED_X509:
715 trustToggle.setChecked(trust.trusted(), false);
716 trustToggle.setEnabled(trust != XmppAxolotlSession.Trust.TRUSTED_X509);
717 if (trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
718 trustToggle.setOnClickListener(null);
719 }
720 key.setTextColor(getPrimaryTextColor());
721 keyType.setTextColor(getSecondaryTextColor());
722 break;
723 case UNDECIDED:
724 trustToggle.setChecked(false, false);
725 trustToggle.setEnabled(false);
726 key.setTextColor(getPrimaryTextColor());
727 keyType.setTextColor(getSecondaryTextColor());
728 break;
729 case INACTIVE_UNTRUSTED:
730 case INACTIVE_UNDECIDED:
731 trustToggle.setOnClickListener(null);
732 trustToggle.setChecked(false, false);
733 trustToggle.setEnabled(false);
734 key.setTextColor(getTertiaryTextColor());
735 keyType.setTextColor(getTertiaryTextColor());
736 break;
737 case INACTIVE_TRUSTED:
738 case INACTIVE_TRUSTED_X509:
739 trustToggle.setOnClickListener(null);
740 trustToggle.setChecked(true, false);
741 trustToggle.setEnabled(false);
742 key.setTextColor(getTertiaryTextColor());
743 keyType.setTextColor(getTertiaryTextColor());
744 break;
745 }
746
747 if (showTag) {
748 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
749 } else {
750 keyType.setVisibility(View.GONE);
751 }
752 if (highlight) {
753 keyType.setTextColor(getResources().getColor(R.color.accent));
754 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
755 } else {
756 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
757 }
758
759 key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
760 keys.addView(view);
761 return true;
762 }
763
764 public void showPurgeKeyDialog(final Account account, final String fingerprint) {
765 Builder builder = new Builder(this);
766 builder.setTitle(getString(R.string.purge_key));
767 builder.setIconAttribute(android.R.attr.alertDialogIcon);
768 builder.setMessage(getString(R.string.purge_key_desc_part1)
769 + "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2))
770 + "\n\n" + getString(R.string.purge_key_desc_part2));
771 builder.setNegativeButton(getString(R.string.cancel), null);
772 builder.setPositiveButton(getString(R.string.accept),
773 new DialogInterface.OnClickListener() {
774 @Override
775 public void onClick(DialogInterface dialog, int which) {
776 account.getAxolotlService().purgeKey(fingerprint);
777 refreshUi();
778 }
779 });
780 builder.create().show();
781 }
782
783 public void selectPresence(final Conversation conversation,
784 final OnPresenceSelected listener) {
785 final Contact contact = conversation.getContact();
786 if (conversation.hasValidOtrSession()) {
787 SessionID id = conversation.getOtrSession().getSessionID();
788 Jid jid;
789 try {
790 jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID());
791 } catch (InvalidJidException e) {
792 jid = null;
793 }
794 conversation.setNextCounterpart(jid);
795 listener.onPresenceSelected();
796 } else if (!contact.showInRoster()) {
797 showAddToRosterDialog(conversation);
798 } else {
799 Presences presences = contact.getPresences();
800 if (presences.size() == 0) {
801 if (!contact.getOption(Contact.Options.TO)
802 && !contact.getOption(Contact.Options.ASKING)
803 && contact.getAccount().getStatus() == Account.State.ONLINE) {
804 showAskForPresenceDialog(contact);
805 } else if (!contact.getOption(Contact.Options.TO)
806 || !contact.getOption(Contact.Options.FROM)) {
807 warnMutalPresenceSubscription(conversation, listener);
808 } else {
809 conversation.setNextCounterpart(null);
810 listener.onPresenceSelected();
811 }
812 } else if (presences.size() == 1) {
813 String presence = presences.asStringArray()[0];
814 try {
815 conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence));
816 } catch (InvalidJidException e) {
817 conversation.setNextCounterpart(null);
818 }
819 listener.onPresenceSelected();
820 } else {
821 final StringBuilder presence = new StringBuilder();
822 AlertDialog.Builder builder = new AlertDialog.Builder(this);
823 builder.setTitle(getString(R.string.choose_presence));
824 final String[] presencesArray = presences.asStringArray();
825 int preselectedPresence = 0;
826 for (int i = 0; i < presencesArray.length; ++i) {
827 if (presencesArray[i].equals(contact.lastseen.presence)) {
828 preselectedPresence = i;
829 break;
830 }
831 }
832 presence.append(presencesArray[preselectedPresence]);
833 builder.setSingleChoiceItems(presencesArray,
834 preselectedPresence,
835 new DialogInterface.OnClickListener() {
836
837 @Override
838 public void onClick(DialogInterface dialog,
839 int which) {
840 presence.delete(0, presence.length());
841 presence.append(presencesArray[which]);
842 }
843 });
844 builder.setNegativeButton(R.string.cancel, null);
845 builder.setPositiveButton(R.string.ok, new OnClickListener() {
846
847 @Override
848 public void onClick(DialogInterface dialog, int which) {
849 try {
850 conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString()));
851 } catch (InvalidJidException e) {
852 conversation.setNextCounterpart(null);
853 }
854 listener.onPresenceSelected();
855 }
856 });
857 builder.create().show();
858 }
859 }
860 }
861
862 protected void onActivityResult(int requestCode, int resultCode,
863 final Intent data) {
864 super.onActivityResult(requestCode, resultCode, data);
865 if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
866 mPendingConferenceInvite = ConferenceInvite.parse(data);
867 if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
868 mPendingConferenceInvite.execute(this);
869 mPendingConferenceInvite = null;
870 }
871 }
872 }
873
874 private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
875 @Override
876 public void success(final Conversation conversation) {
877 switchToConversation(conversation);
878 runOnUiThread(new Runnable() {
879 @Override
880 public void run() {
881 Toast.makeText(XmppActivity.this,R.string.conference_created,Toast.LENGTH_LONG).show();
882 }
883 });
884 }
885
886 @Override
887 public void error(final int errorCode, Conversation object) {
888 runOnUiThread(new Runnable() {
889 @Override
890 public void run() {
891 Toast.makeText(XmppActivity.this,errorCode,Toast.LENGTH_LONG).show();
892 }
893 });
894 }
895
896 @Override
897 public void userInputRequried(PendingIntent pi, Conversation object) {
898
899 }
900 };
901
902 public int getTertiaryTextColor() {
903 return this.mTertiaryTextColor;
904 }
905
906 public int getSecondaryTextColor() {
907 return this.mSecondaryTextColor;
908 }
909
910 public int getPrimaryTextColor() {
911 return this.mPrimaryTextColor;
912 }
913
914 public int getWarningTextColor() {
915 return this.mColorRed;
916 }
917
918 public int getOnlineColor() {
919 return this.mColorGreen;
920 }
921
922 public int getPrimaryBackgroundColor() {
923 return this.mPrimaryBackgroundColor;
924 }
925
926 public int getSecondaryBackgroundColor() {
927 return this.mSecondaryBackgroundColor;
928 }
929
930 public int getPixel(int dp) {
931 DisplayMetrics metrics = getResources().getDisplayMetrics();
932 return ((int) (dp * metrics.density));
933 }
934
935 public boolean copyTextToClipboard(String text, int labelResId) {
936 ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
937 String label = getResources().getString(labelResId);
938 if (mClipBoardManager != null) {
939 ClipData mClipData = ClipData.newPlainText(label, text);
940 mClipBoardManager.setPrimaryClip(mClipData);
941 return true;
942 }
943 return false;
944 }
945
946 protected void registerNdefPushMessageCallback() {
947 NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
948 if (nfcAdapter != null && nfcAdapter.isEnabled()) {
949 nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
950 @Override
951 public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
952 return new NdefMessage(new NdefRecord[]{
953 NdefRecord.createUri(getShareableUri()),
954 NdefRecord.createApplicationRecord("eu.siacs.conversations")
955 });
956 }
957 }, this);
958 }
959 }
960
961 protected void unregisterNdefPushMessageCallback() {
962 NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
963 if (nfcAdapter != null && nfcAdapter.isEnabled()) {
964 nfcAdapter.setNdefPushMessageCallback(null,this);
965 }
966 }
967
968 protected String getShareableUri() {
969 return null;
970 }
971
972 @Override
973 public void onResume() {
974 super.onResume();
975 if (this.getShareableUri()!=null) {
976 this.registerNdefPushMessageCallback();
977 }
978 }
979
980 protected int findTheme() {
981 if (getPreferences().getBoolean("use_larger_font", false)) {
982 return R.style.ConversationsTheme_LargerText;
983 } else {
984 return R.style.ConversationsTheme;
985 }
986 }
987
988 @Override
989 public void onPause() {
990 super.onPause();
991 this.unregisterNdefPushMessageCallback();
992 }
993
994 protected void showQrCode() {
995 String uri = getShareableUri();
996 if (uri!=null) {
997 Point size = new Point();
998 getWindowManager().getDefaultDisplay().getSize(size);
999 final int width = (size.x < size.y ? size.x : size.y);
1000 Bitmap bitmap = createQrCodeBitmap(uri, width);
1001 ImageView view = new ImageView(this);
1002 view.setImageBitmap(bitmap);
1003 AlertDialog.Builder builder = new AlertDialog.Builder(this);
1004 builder.setView(view);
1005 builder.create().show();
1006 }
1007 }
1008
1009 protected Bitmap createQrCodeBitmap(String input, int size) {
1010 Log.d(Config.LOGTAG,"qr code requested size: "+size);
1011 try {
1012 final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
1013 final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
1014 hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
1015 final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
1016 final int width = result.getWidth();
1017 final int height = result.getHeight();
1018 final int[] pixels = new int[width * height];
1019 for (int y = 0; y < height; y++) {
1020 final int offset = y * width;
1021 for (int x = 0; x < width; x++) {
1022 pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
1023 }
1024 }
1025 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1026 Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
1027 bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
1028 return bitmap;
1029 } catch (final WriterException e) {
1030 return null;
1031 }
1032 }
1033
1034 public static class ConferenceInvite {
1035 private String uuid;
1036 private List<Jid> jids = new ArrayList<>();
1037
1038 public static ConferenceInvite parse(Intent data) {
1039 ConferenceInvite invite = new ConferenceInvite();
1040 invite.uuid = data.getStringExtra("conversation");
1041 if (invite.uuid == null) {
1042 return null;
1043 }
1044 try {
1045 if (data.getBooleanExtra("multiple", false)) {
1046 String[] toAdd = data.getStringArrayExtra("contacts");
1047 for (String item : toAdd) {
1048 invite.jids.add(Jid.fromString(item));
1049 }
1050 } else {
1051 invite.jids.add(Jid.fromString(data.getStringExtra("contact")));
1052 }
1053 } catch (final InvalidJidException ignored) {
1054 return null;
1055 }
1056 return invite;
1057 }
1058
1059 public void execute(XmppActivity activity) {
1060 XmppConnectionService service = activity.xmppConnectionService;
1061 Conversation conversation = service.findConversationByUuid(this.uuid);
1062 if (conversation == null) {
1063 return;
1064 }
1065 if (conversation.getMode() == Conversation.MODE_MULTI) {
1066 for (Jid jid : jids) {
1067 service.invite(conversation, jid);
1068 }
1069 } else {
1070 jids.add(conversation.getJid().toBareJid());
1071 service.createAdhocConference(conversation.getAccount(), jids, activity.adhocCallback);
1072 }
1073 }
1074 }
1075
1076 public AvatarService avatarService() {
1077 return xmppConnectionService.getAvatarService();
1078 }
1079
1080 class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
1081 private final WeakReference<ImageView> imageViewReference;
1082 private Message message = null;
1083
1084 public BitmapWorkerTask(ImageView imageView) {
1085 imageViewReference = new WeakReference<>(imageView);
1086 }
1087
1088 @Override
1089 protected Bitmap doInBackground(Message... params) {
1090 message = params[0];
1091 try {
1092 return xmppConnectionService.getFileBackend().getThumbnail(
1093 message, (int) (metrics.density * 288), false);
1094 } catch (FileNotFoundException e) {
1095 return null;
1096 }
1097 }
1098
1099 @Override
1100 protected void onPostExecute(Bitmap bitmap) {
1101 if (bitmap != null) {
1102 final ImageView imageView = imageViewReference.get();
1103 if (imageView != null) {
1104 imageView.setImageBitmap(bitmap);
1105 imageView.setBackgroundColor(0x00000000);
1106 }
1107 }
1108 }
1109 }
1110
1111 public void loadBitmap(Message message, ImageView imageView) {
1112 Bitmap bm;
1113 try {
1114 bm = xmppConnectionService.getFileBackend().getThumbnail(message,
1115 (int) (metrics.density * 288), true);
1116 } catch (FileNotFoundException e) {
1117 bm = null;
1118 }
1119 if (bm != null) {
1120 imageView.setImageBitmap(bm);
1121 imageView.setBackgroundColor(0x00000000);
1122 } else {
1123 if (cancelPotentialWork(message, imageView)) {
1124 imageView.setBackgroundColor(0xff333333);
1125 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1126 final AsyncDrawable asyncDrawable = new AsyncDrawable(
1127 getResources(), null, task);
1128 imageView.setImageDrawable(asyncDrawable);
1129 try {
1130 task.execute(message);
1131 } catch (final RejectedExecutionException ignored) {
1132 }
1133 }
1134 }
1135 }
1136
1137 public static boolean cancelPotentialWork(Message message,
1138 ImageView imageView) {
1139 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
1140
1141 if (bitmapWorkerTask != null) {
1142 final Message oldMessage = bitmapWorkerTask.message;
1143 if (oldMessage == null || message != oldMessage) {
1144 bitmapWorkerTask.cancel(true);
1145 } else {
1146 return false;
1147 }
1148 }
1149 return true;
1150 }
1151
1152 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
1153 if (imageView != null) {
1154 final Drawable drawable = imageView.getDrawable();
1155 if (drawable instanceof AsyncDrawable) {
1156 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
1157 return asyncDrawable.getBitmapWorkerTask();
1158 }
1159 }
1160 return null;
1161 }
1162
1163 static class AsyncDrawable extends BitmapDrawable {
1164 private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1165
1166 public AsyncDrawable(Resources res, Bitmap bitmap,
1167 BitmapWorkerTask bitmapWorkerTask) {
1168 super(res, bitmap);
1169 bitmapWorkerTaskReference = new WeakReference<>(
1170 bitmapWorkerTask);
1171 }
1172
1173 public BitmapWorkerTask getBitmapWorkerTask() {
1174 return bitmapWorkerTaskReference.get();
1175 }
1176 }
1177}