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