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 intent.putExtra("show_enter_jid", true);
473 intent.putExtra("account", conversation.getAccount().getJid().toBareJid().toString());
474 startActivityForResult(intent, REQUEST_INVITE_TO_CONVERSATION);
475 }
476
477 protected void announcePgp(Account account, final Conversation conversation) {
478 if (account.getPgpId() == -1) {
479 choosePgpSignId(account);
480 } else {
481 xmppConnectionService.getPgpEngine().generateSignature(account, "", new UiCallback<Account>() {
482
483 @Override
484 public void userInputRequried(PendingIntent pi,
485 Account account) {
486 try {
487 startIntentSenderForResult(pi.getIntentSender(),
488 REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
489 } catch (final SendIntentException ignored) {
490 }
491 }
492
493 @Override
494 public void success(Account account) {
495 xmppConnectionService.databaseBackend.updateAccount(account);
496 xmppConnectionService.sendPresence(account);
497 if (conversation != null) {
498 conversation.setNextEncryption(Message.ENCRYPTION_PGP);
499 xmppConnectionService.databaseBackend.updateConversation(conversation);
500 }
501 }
502
503 @Override
504 public void error(int error, Account account) {
505 displayErrorDialog(error);
506 }
507 });
508 }
509 }
510
511 protected void choosePgpSignId(Account account) {
512 xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<Account>() {
513 @Override
514 public void success(Account account1) {
515 }
516
517 @Override
518 public void error(int errorCode, Account object) {
519
520 }
521
522 @Override
523 public void userInputRequried(PendingIntent pi, Account object) {
524 try {
525 startIntentSenderForResult(pi.getIntentSender(),
526 REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0);
527 } catch (final SendIntentException ignored) {
528 }
529 }
530 });
531 }
532
533 protected void displayErrorDialog(final int errorCode) {
534 runOnUiThread(new Runnable() {
535
536 @Override
537 public void run() {
538 AlertDialog.Builder builder = new AlertDialog.Builder(
539 XmppActivity.this);
540 builder.setIconAttribute(android.R.attr.alertDialogIcon);
541 builder.setTitle(getString(R.string.error));
542 builder.setMessage(errorCode);
543 builder.setNeutralButton(R.string.accept, null);
544 builder.create().show();
545 }
546 });
547
548 }
549
550 protected void showAddToRosterDialog(final Conversation conversation) {
551 showAddToRosterDialog(conversation.getContact());
552 }
553
554 protected void showAddToRosterDialog(final Contact contact) {
555 AlertDialog.Builder builder = new AlertDialog.Builder(this);
556 builder.setTitle(contact.getJid().toString());
557 builder.setMessage(getString(R.string.not_in_roster));
558 builder.setNegativeButton(getString(R.string.cancel), null);
559 builder.setPositiveButton(getString(R.string.add_contact),
560 new DialogInterface.OnClickListener() {
561
562 @Override
563 public void onClick(DialogInterface dialog, int which) {
564 final Jid jid = contact.getJid();
565 Account account = contact.getAccount();
566 Contact contact = account.getRoster().getContact(jid);
567 xmppConnectionService.createContact(contact);
568 }
569 });
570 builder.create().show();
571 }
572
573 private void showAskForPresenceDialog(final Contact contact) {
574 AlertDialog.Builder builder = new AlertDialog.Builder(this);
575 builder.setTitle(contact.getJid().toString());
576 builder.setMessage(R.string.request_presence_updates);
577 builder.setNegativeButton(R.string.cancel, null);
578 builder.setPositiveButton(R.string.request_now,
579 new DialogInterface.OnClickListener() {
580
581 @Override
582 public void onClick(DialogInterface dialog, int which) {
583 if (xmppConnectionServiceBound) {
584 xmppConnectionService.sendPresencePacket(contact
585 .getAccount(), xmppConnectionService
586 .getPresenceGenerator()
587 .requestPresenceUpdatesFrom(contact));
588 }
589 }
590 });
591 builder.create().show();
592 }
593
594 private void warnMutalPresenceSubscription(final Conversation conversation,
595 final OnPresenceSelected listener) {
596 AlertDialog.Builder builder = new AlertDialog.Builder(this);
597 builder.setTitle(conversation.getContact().getJid().toString());
598 builder.setMessage(R.string.without_mutual_presence_updates);
599 builder.setNegativeButton(R.string.cancel, null);
600 builder.setPositiveButton(R.string.ignore, new OnClickListener() {
601
602 @Override
603 public void onClick(DialogInterface dialog, int which) {
604 conversation.setNextCounterpart(null);
605 if (listener != null) {
606 listener.onPresenceSelected();
607 }
608 }
609 });
610 builder.create().show();
611 }
612
613 protected void quickEdit(String previousValue, OnValueEdited callback) {
614 quickEdit(previousValue, callback, false);
615 }
616
617 protected void quickPasswordEdit(String previousValue,
618 OnValueEdited callback) {
619 quickEdit(previousValue, callback, true);
620 }
621
622 @SuppressLint("InflateParams")
623 private void quickEdit(final String previousValue,
624 final OnValueEdited callback, boolean password) {
625 AlertDialog.Builder builder = new AlertDialog.Builder(this);
626 View view = getLayoutInflater().inflate(R.layout.quickedit, null);
627 final EditText editor = (EditText) view.findViewById(R.id.editor);
628 OnClickListener mClickListener = new OnClickListener() {
629
630 @Override
631 public void onClick(DialogInterface dialog, int which) {
632 String value = editor.getText().toString();
633 if (!previousValue.equals(value) && value.trim().length() > 0) {
634 callback.onValueEdited(value);
635 }
636 }
637 };
638 if (password) {
639 editor.setInputType(InputType.TYPE_CLASS_TEXT
640 | InputType.TYPE_TEXT_VARIATION_PASSWORD);
641 editor.setHint(R.string.password);
642 builder.setPositiveButton(R.string.accept, mClickListener);
643 } else {
644 builder.setPositiveButton(R.string.edit, mClickListener);
645 }
646 editor.requestFocus();
647 editor.setText(previousValue);
648 builder.setView(view);
649 builder.setNegativeButton(R.string.cancel, null);
650 builder.create().show();
651 }
652
653 protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight, View.OnClickListener onKeyClickedListener) {
654 final XmppAxolotlSession.Trust trust = account.getAxolotlService()
655 .getFingerprintTrust(fingerprint);
656 if (trust == null) {
657 return false;
658 }
659 return addFingerprintRowWithListeners(keys, account, fingerprint, highlight, trust, true,
660 new CompoundButton.OnCheckedChangeListener() {
661 @Override
662 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
663 account.getAxolotlService().setFingerprintTrust(fingerprint,
664 (isChecked) ? XmppAxolotlSession.Trust.TRUSTED :
665 XmppAxolotlSession.Trust.UNTRUSTED);
666 }
667 },
668 new View.OnClickListener() {
669 @Override
670 public void onClick(View v) {
671 account.getAxolotlService().setFingerprintTrust(fingerprint,
672 XmppAxolotlSession.Trust.UNTRUSTED);
673 v.setEnabled(true);
674 }
675 },
676 onKeyClickedListener
677
678 );
679 }
680
681 protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account,
682 final String fingerprint,
683 boolean highlight,
684 XmppAxolotlSession.Trust trust,
685 boolean showTag,
686 CompoundButton.OnCheckedChangeListener
687 onCheckedChangeListener,
688 View.OnClickListener onClickListener,
689 View.OnClickListener onKeyClickedListener) {
690 if (trust == XmppAxolotlSession.Trust.COMPROMISED) {
691 return false;
692 }
693 View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
694 TextView key = (TextView) view.findViewById(R.id.key);
695 key.setOnClickListener(onKeyClickedListener);
696 TextView keyType = (TextView) view.findViewById(R.id.key_type);
697 keyType.setOnClickListener(onKeyClickedListener);
698 Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
699 trustToggle.setVisibility(View.VISIBLE);
700 trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
701 trustToggle.setOnClickListener(onClickListener);
702 final View.OnLongClickListener purge = new View.OnLongClickListener() {
703 @Override
704 public boolean onLongClick(View v) {
705 showPurgeKeyDialog(account, fingerprint);
706 return true;
707 }
708 };
709 view.setOnLongClickListener(purge);
710 key.setOnLongClickListener(purge);
711 keyType.setOnLongClickListener(purge);
712 boolean x509 = trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509;
713 switch (trust) {
714 case UNTRUSTED:
715 case TRUSTED:
716 case TRUSTED_X509:
717 trustToggle.setChecked(trust.trusted(), false);
718 trustToggle.setEnabled(trust != XmppAxolotlSession.Trust.TRUSTED_X509);
719 if (trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
720 trustToggle.setOnClickListener(null);
721 }
722 key.setTextColor(getPrimaryTextColor());
723 keyType.setTextColor(getSecondaryTextColor());
724 break;
725 case UNDECIDED:
726 trustToggle.setChecked(false, false);
727 trustToggle.setEnabled(false);
728 key.setTextColor(getPrimaryTextColor());
729 keyType.setTextColor(getSecondaryTextColor());
730 break;
731 case INACTIVE_UNTRUSTED:
732 case INACTIVE_UNDECIDED:
733 trustToggle.setOnClickListener(null);
734 trustToggle.setChecked(false, false);
735 trustToggle.setEnabled(false);
736 key.setTextColor(getTertiaryTextColor());
737 keyType.setTextColor(getTertiaryTextColor());
738 break;
739 case INACTIVE_TRUSTED:
740 case INACTIVE_TRUSTED_X509:
741 trustToggle.setOnClickListener(null);
742 trustToggle.setChecked(true, false);
743 trustToggle.setEnabled(false);
744 key.setTextColor(getTertiaryTextColor());
745 keyType.setTextColor(getTertiaryTextColor());
746 break;
747 }
748
749 if (showTag) {
750 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
751 } else {
752 keyType.setVisibility(View.GONE);
753 }
754 if (highlight) {
755 keyType.setTextColor(getResources().getColor(R.color.accent));
756 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
757 } else {
758 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
759 }
760
761 key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
762 keys.addView(view);
763 return true;
764 }
765
766 public void showPurgeKeyDialog(final Account account, final String fingerprint) {
767 Builder builder = new Builder(this);
768 builder.setTitle(getString(R.string.purge_key));
769 builder.setIconAttribute(android.R.attr.alertDialogIcon);
770 builder.setMessage(getString(R.string.purge_key_desc_part1)
771 + "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2))
772 + "\n\n" + getString(R.string.purge_key_desc_part2));
773 builder.setNegativeButton(getString(R.string.cancel), null);
774 builder.setPositiveButton(getString(R.string.accept),
775 new DialogInterface.OnClickListener() {
776 @Override
777 public void onClick(DialogInterface dialog, int which) {
778 account.getAxolotlService().purgeKey(fingerprint);
779 refreshUi();
780 }
781 });
782 builder.create().show();
783 }
784
785 public void selectPresence(final Conversation conversation,
786 final OnPresenceSelected listener) {
787 final Contact contact = conversation.getContact();
788 if (conversation.hasValidOtrSession()) {
789 SessionID id = conversation.getOtrSession().getSessionID();
790 Jid jid;
791 try {
792 jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID());
793 } catch (InvalidJidException e) {
794 jid = null;
795 }
796 conversation.setNextCounterpart(jid);
797 listener.onPresenceSelected();
798 } else if (!contact.showInRoster()) {
799 showAddToRosterDialog(conversation);
800 } else {
801 Presences presences = contact.getPresences();
802 if (presences.size() == 0) {
803 if (!contact.getOption(Contact.Options.TO)
804 && !contact.getOption(Contact.Options.ASKING)
805 && contact.getAccount().getStatus() == Account.State.ONLINE) {
806 showAskForPresenceDialog(contact);
807 } else if (!contact.getOption(Contact.Options.TO)
808 || !contact.getOption(Contact.Options.FROM)) {
809 warnMutalPresenceSubscription(conversation, listener);
810 } else {
811 conversation.setNextCounterpart(null);
812 listener.onPresenceSelected();
813 }
814 } else if (presences.size() == 1) {
815 String presence = presences.asStringArray()[0];
816 try {
817 conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence));
818 } catch (InvalidJidException e) {
819 conversation.setNextCounterpart(null);
820 }
821 listener.onPresenceSelected();
822 } else {
823 final StringBuilder presence = new StringBuilder();
824 AlertDialog.Builder builder = new AlertDialog.Builder(this);
825 builder.setTitle(getString(R.string.choose_presence));
826 final String[] presencesArray = presences.asStringArray();
827 int preselectedPresence = 0;
828 for (int i = 0; i < presencesArray.length; ++i) {
829 if (presencesArray[i].equals(contact.lastseen.presence)) {
830 preselectedPresence = i;
831 break;
832 }
833 }
834 presence.append(presencesArray[preselectedPresence]);
835 builder.setSingleChoiceItems(presencesArray,
836 preselectedPresence,
837 new DialogInterface.OnClickListener() {
838
839 @Override
840 public void onClick(DialogInterface dialog,
841 int which) {
842 presence.delete(0, presence.length());
843 presence.append(presencesArray[which]);
844 }
845 });
846 builder.setNegativeButton(R.string.cancel, null);
847 builder.setPositiveButton(R.string.ok, new OnClickListener() {
848
849 @Override
850 public void onClick(DialogInterface dialog, int which) {
851 try {
852 conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString()));
853 } catch (InvalidJidException e) {
854 conversation.setNextCounterpart(null);
855 }
856 listener.onPresenceSelected();
857 }
858 });
859 builder.create().show();
860 }
861 }
862 }
863
864 protected void onActivityResult(int requestCode, int resultCode,
865 final Intent data) {
866 super.onActivityResult(requestCode, resultCode, data);
867 if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
868 mPendingConferenceInvite = ConferenceInvite.parse(data);
869 if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
870 mPendingConferenceInvite.execute(this);
871 mPendingConferenceInvite = null;
872 }
873 }
874 }
875
876 private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
877 @Override
878 public void success(final Conversation conversation) {
879 switchToConversation(conversation);
880 runOnUiThread(new Runnable() {
881 @Override
882 public void run() {
883 Toast.makeText(XmppActivity.this,R.string.conference_created,Toast.LENGTH_LONG).show();
884 }
885 });
886 }
887
888 @Override
889 public void error(final int errorCode, Conversation object) {
890 runOnUiThread(new Runnable() {
891 @Override
892 public void run() {
893 Toast.makeText(XmppActivity.this,errorCode,Toast.LENGTH_LONG).show();
894 }
895 });
896 }
897
898 @Override
899 public void userInputRequried(PendingIntent pi, Conversation object) {
900
901 }
902 };
903
904 public int getTertiaryTextColor() {
905 return this.mTertiaryTextColor;
906 }
907
908 public int getSecondaryTextColor() {
909 return this.mSecondaryTextColor;
910 }
911
912 public int getPrimaryTextColor() {
913 return this.mPrimaryTextColor;
914 }
915
916 public int getWarningTextColor() {
917 return this.mColorRed;
918 }
919
920 public int getOnlineColor() {
921 return this.mColorGreen;
922 }
923
924 public int getPrimaryBackgroundColor() {
925 return this.mPrimaryBackgroundColor;
926 }
927
928 public int getSecondaryBackgroundColor() {
929 return this.mSecondaryBackgroundColor;
930 }
931
932 public int getPixel(int dp) {
933 DisplayMetrics metrics = getResources().getDisplayMetrics();
934 return ((int) (dp * metrics.density));
935 }
936
937 public boolean copyTextToClipboard(String text, int labelResId) {
938 ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
939 String label = getResources().getString(labelResId);
940 if (mClipBoardManager != null) {
941 ClipData mClipData = ClipData.newPlainText(label, text);
942 mClipBoardManager.setPrimaryClip(mClipData);
943 return true;
944 }
945 return false;
946 }
947
948 protected void registerNdefPushMessageCallback() {
949 NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
950 if (nfcAdapter != null && nfcAdapter.isEnabled()) {
951 nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
952 @Override
953 public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
954 return new NdefMessage(new NdefRecord[]{
955 NdefRecord.createUri(getShareableUri()),
956 NdefRecord.createApplicationRecord("eu.siacs.conversations")
957 });
958 }
959 }, this);
960 }
961 }
962
963 protected void unregisterNdefPushMessageCallback() {
964 NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
965 if (nfcAdapter != null && nfcAdapter.isEnabled()) {
966 nfcAdapter.setNdefPushMessageCallback(null,this);
967 }
968 }
969
970 protected String getShareableUri() {
971 return null;
972 }
973
974 @Override
975 public void onResume() {
976 super.onResume();
977 if (this.getShareableUri()!=null) {
978 this.registerNdefPushMessageCallback();
979 }
980 }
981
982 protected int findTheme() {
983 if (getPreferences().getBoolean("use_larger_font", false)) {
984 return R.style.ConversationsTheme_LargerText;
985 } else {
986 return R.style.ConversationsTheme;
987 }
988 }
989
990 @Override
991 public void onPause() {
992 super.onPause();
993 this.unregisterNdefPushMessageCallback();
994 }
995
996 protected void showQrCode() {
997 String uri = getShareableUri();
998 if (uri!=null) {
999 Point size = new Point();
1000 getWindowManager().getDefaultDisplay().getSize(size);
1001 final int width = (size.x < size.y ? size.x : size.y);
1002 Bitmap bitmap = createQrCodeBitmap(uri, width);
1003 ImageView view = new ImageView(this);
1004 view.setImageBitmap(bitmap);
1005 AlertDialog.Builder builder = new AlertDialog.Builder(this);
1006 builder.setView(view);
1007 builder.create().show();
1008 }
1009 }
1010
1011 protected Bitmap createQrCodeBitmap(String input, int size) {
1012 Log.d(Config.LOGTAG,"qr code requested size: "+size);
1013 try {
1014 final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
1015 final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
1016 hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
1017 final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
1018 final int width = result.getWidth();
1019 final int height = result.getHeight();
1020 final int[] pixels = new int[width * height];
1021 for (int y = 0; y < height; y++) {
1022 final int offset = y * width;
1023 for (int x = 0; x < width; x++) {
1024 pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
1025 }
1026 }
1027 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1028 Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
1029 bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
1030 return bitmap;
1031 } catch (final WriterException e) {
1032 return null;
1033 }
1034 }
1035
1036 public static class ConferenceInvite {
1037 private String uuid;
1038 private List<Jid> jids = new ArrayList<>();
1039
1040 public static ConferenceInvite parse(Intent data) {
1041 ConferenceInvite invite = new ConferenceInvite();
1042 invite.uuid = data.getStringExtra("conversation");
1043 if (invite.uuid == null) {
1044 return null;
1045 }
1046 try {
1047 if (data.getBooleanExtra("multiple", false)) {
1048 String[] toAdd = data.getStringArrayExtra("contacts");
1049 for (String item : toAdd) {
1050 invite.jids.add(Jid.fromString(item));
1051 }
1052 } else {
1053 invite.jids.add(Jid.fromString(data.getStringExtra("contact")));
1054 }
1055 } catch (final InvalidJidException ignored) {
1056 return null;
1057 }
1058 return invite;
1059 }
1060
1061 public void execute(XmppActivity activity) {
1062 XmppConnectionService service = activity.xmppConnectionService;
1063 Conversation conversation = service.findConversationByUuid(this.uuid);
1064 if (conversation == null) {
1065 return;
1066 }
1067 if (conversation.getMode() == Conversation.MODE_MULTI) {
1068 for (Jid jid : jids) {
1069 service.invite(conversation, jid);
1070 }
1071 } else {
1072 jids.add(conversation.getJid().toBareJid());
1073 service.createAdhocConference(conversation.getAccount(), jids, activity.adhocCallback);
1074 }
1075 }
1076 }
1077
1078 public AvatarService avatarService() {
1079 return xmppConnectionService.getAvatarService();
1080 }
1081
1082 class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
1083 private final WeakReference<ImageView> imageViewReference;
1084 private Message message = null;
1085
1086 public BitmapWorkerTask(ImageView imageView) {
1087 imageViewReference = new WeakReference<>(imageView);
1088 }
1089
1090 @Override
1091 protected Bitmap doInBackground(Message... params) {
1092 message = params[0];
1093 try {
1094 return xmppConnectionService.getFileBackend().getThumbnail(
1095 message, (int) (metrics.density * 288), false);
1096 } catch (FileNotFoundException e) {
1097 return null;
1098 }
1099 }
1100
1101 @Override
1102 protected void onPostExecute(Bitmap bitmap) {
1103 if (bitmap != null) {
1104 final ImageView imageView = imageViewReference.get();
1105 if (imageView != null) {
1106 imageView.setImageBitmap(bitmap);
1107 imageView.setBackgroundColor(0x00000000);
1108 }
1109 }
1110 }
1111 }
1112
1113 public void loadBitmap(Message message, ImageView imageView) {
1114 Bitmap bm;
1115 try {
1116 bm = xmppConnectionService.getFileBackend().getThumbnail(message,
1117 (int) (metrics.density * 288), true);
1118 } catch (FileNotFoundException e) {
1119 bm = null;
1120 }
1121 if (bm != null) {
1122 imageView.setImageBitmap(bm);
1123 imageView.setBackgroundColor(0x00000000);
1124 } else {
1125 if (cancelPotentialWork(message, imageView)) {
1126 imageView.setBackgroundColor(0xff333333);
1127 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1128 final AsyncDrawable asyncDrawable = new AsyncDrawable(
1129 getResources(), null, task);
1130 imageView.setImageDrawable(asyncDrawable);
1131 try {
1132 task.execute(message);
1133 } catch (final RejectedExecutionException ignored) {
1134 }
1135 }
1136 }
1137 }
1138
1139 public static boolean cancelPotentialWork(Message message,
1140 ImageView imageView) {
1141 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
1142
1143 if (bitmapWorkerTask != null) {
1144 final Message oldMessage = bitmapWorkerTask.message;
1145 if (oldMessage == null || message != oldMessage) {
1146 bitmapWorkerTask.cancel(true);
1147 } else {
1148 return false;
1149 }
1150 }
1151 return true;
1152 }
1153
1154 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
1155 if (imageView != null) {
1156 final Drawable drawable = imageView.getDrawable();
1157 if (drawable instanceof AsyncDrawable) {
1158 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
1159 return asyncDrawable.getBitmapWorkerTask();
1160 }
1161 }
1162 return null;
1163 }
1164
1165 static class AsyncDrawable extends BitmapDrawable {
1166 private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1167
1168 public AsyncDrawable(Resources res, Bitmap bitmap,
1169 BitmapWorkerTask bitmapWorkerTask) {
1170 super(res, bitmap);
1171 bitmapWorkerTaskReference = new WeakReference<>(
1172 bitmapWorkerTask);
1173 }
1174
1175 public BitmapWorkerTask getBitmapWorkerTask() {
1176 return bitmapWorkerTaskReference.get();
1177 }
1178 }
1179}