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