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