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