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()) {
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, OnValueEdited callback) {
666 quickEdit(previousValue, callback, false);
667 }
668
669 protected void quickPasswordEdit(String previousValue,
670 OnValueEdited callback) {
671 quickEdit(previousValue, callback, true);
672 }
673
674 @SuppressLint("InflateParams")
675 private void quickEdit(final String previousValue,
676 final OnValueEdited callback, boolean password) {
677 AlertDialog.Builder builder = new AlertDialog.Builder(this);
678 View view = getLayoutInflater().inflate(R.layout.quickedit, null);
679 final EditText editor = (EditText) view.findViewById(R.id.editor);
680 OnClickListener mClickListener = new OnClickListener() {
681
682 @Override
683 public void onClick(DialogInterface dialog, int which) {
684 String value = editor.getText().toString();
685 if (!previousValue.equals(value) && value.trim().length() > 0) {
686 callback.onValueEdited(value);
687 }
688 }
689 };
690 if (password) {
691 editor.setInputType(InputType.TYPE_CLASS_TEXT
692 | InputType.TYPE_TEXT_VARIATION_PASSWORD);
693 editor.setHint(R.string.password);
694 builder.setPositiveButton(R.string.accept, mClickListener);
695 } else {
696 builder.setPositiveButton(R.string.edit, mClickListener);
697 }
698 editor.requestFocus();
699 editor.setText(previousValue);
700 builder.setView(view);
701 builder.setNegativeButton(R.string.cancel, null);
702 builder.create().show();
703 }
704
705 protected boolean addFingerprintRow(LinearLayout keys, final Account account, final String fingerprint, boolean highlight, View.OnClickListener onKeyClickedListener) {
706 final XmppAxolotlSession.Trust trust = account.getAxolotlService()
707 .getFingerprintTrust(fingerprint);
708 if (trust == null) {
709 return false;
710 }
711 return addFingerprintRowWithListeners(keys, account, fingerprint, highlight, trust, true,
712 new CompoundButton.OnCheckedChangeListener() {
713 @Override
714 public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
715 account.getAxolotlService().setFingerprintTrust(fingerprint,
716 (isChecked) ? XmppAxolotlSession.Trust.TRUSTED :
717 XmppAxolotlSession.Trust.UNTRUSTED);
718 }
719 },
720 new View.OnClickListener() {
721 @Override
722 public void onClick(View v) {
723 account.getAxolotlService().setFingerprintTrust(fingerprint,
724 XmppAxolotlSession.Trust.UNTRUSTED);
725 v.setEnabled(true);
726 }
727 },
728 onKeyClickedListener
729
730 );
731 }
732
733 protected boolean addFingerprintRowWithListeners(LinearLayout keys, final Account account,
734 final String fingerprint,
735 boolean highlight,
736 XmppAxolotlSession.Trust trust,
737 boolean showTag,
738 CompoundButton.OnCheckedChangeListener
739 onCheckedChangeListener,
740 View.OnClickListener onClickListener,
741 View.OnClickListener onKeyClickedListener) {
742 if (trust == XmppAxolotlSession.Trust.COMPROMISED) {
743 return false;
744 }
745 View view = getLayoutInflater().inflate(R.layout.contact_key, keys, false);
746 TextView key = (TextView) view.findViewById(R.id.key);
747 key.setOnClickListener(onKeyClickedListener);
748 TextView keyType = (TextView) view.findViewById(R.id.key_type);
749 keyType.setOnClickListener(onKeyClickedListener);
750 Switch trustToggle = (Switch) view.findViewById(R.id.tgl_trust);
751 trustToggle.setVisibility(View.VISIBLE);
752 trustToggle.setOnCheckedChangeListener(onCheckedChangeListener);
753 trustToggle.setOnClickListener(onClickListener);
754 final View.OnLongClickListener purge = new View.OnLongClickListener() {
755 @Override
756 public boolean onLongClick(View v) {
757 showPurgeKeyDialog(account, fingerprint);
758 return true;
759 }
760 };
761 view.setOnLongClickListener(purge);
762 key.setOnLongClickListener(purge);
763 keyType.setOnLongClickListener(purge);
764 boolean x509 = Config.X509_VERIFICATION
765 && (trust == XmppAxolotlSession.Trust.TRUSTED_X509 || trust == XmppAxolotlSession.Trust.INACTIVE_TRUSTED_X509);
766 switch (trust) {
767 case UNTRUSTED:
768 case TRUSTED:
769 case TRUSTED_X509:
770 trustToggle.setChecked(trust.trusted(), false);
771 trustToggle.setEnabled(!Config.X509_VERIFICATION || trust != XmppAxolotlSession.Trust.TRUSTED_X509);
772 if (Config.X509_VERIFICATION && trust == XmppAxolotlSession.Trust.TRUSTED_X509) {
773 trustToggle.setOnClickListener(null);
774 }
775 key.setTextColor(getPrimaryTextColor());
776 keyType.setTextColor(getSecondaryTextColor());
777 break;
778 case UNDECIDED:
779 trustToggle.setChecked(false, false);
780 trustToggle.setEnabled(false);
781 key.setTextColor(getPrimaryTextColor());
782 keyType.setTextColor(getSecondaryTextColor());
783 break;
784 case INACTIVE_UNTRUSTED:
785 case INACTIVE_UNDECIDED:
786 trustToggle.setOnClickListener(null);
787 trustToggle.setChecked(false, false);
788 trustToggle.setEnabled(false);
789 key.setTextColor(getTertiaryTextColor());
790 keyType.setTextColor(getTertiaryTextColor());
791 break;
792 case INACTIVE_TRUSTED:
793 case INACTIVE_TRUSTED_X509:
794 trustToggle.setOnClickListener(null);
795 trustToggle.setChecked(true, false);
796 trustToggle.setEnabled(false);
797 key.setTextColor(getTertiaryTextColor());
798 keyType.setTextColor(getTertiaryTextColor());
799 break;
800 }
801
802 if (showTag) {
803 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
804 } else {
805 keyType.setVisibility(View.GONE);
806 }
807 if (highlight) {
808 keyType.setTextColor(getResources().getColor(R.color.accent));
809 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509_selected_message : R.string.omemo_fingerprint_selected_message));
810 } else {
811 keyType.setText(getString(x509 ? R.string.omemo_fingerprint_x509 : R.string.omemo_fingerprint));
812 }
813
814 key.setText(CryptoHelper.prettifyFingerprint(fingerprint.substring(2)));
815 keys.addView(view);
816 return true;
817 }
818
819 public void showPurgeKeyDialog(final Account account, final String fingerprint) {
820 Builder builder = new Builder(this);
821 builder.setTitle(getString(R.string.purge_key));
822 builder.setIconAttribute(android.R.attr.alertDialogIcon);
823 builder.setMessage(getString(R.string.purge_key_desc_part1)
824 + "\n\n" + CryptoHelper.prettifyFingerprint(fingerprint.substring(2))
825 + "\n\n" + getString(R.string.purge_key_desc_part2));
826 builder.setNegativeButton(getString(R.string.cancel), null);
827 builder.setPositiveButton(getString(R.string.purge_key),
828 new DialogInterface.OnClickListener() {
829 @Override
830 public void onClick(DialogInterface dialog, int which) {
831 account.getAxolotlService().purgeKey(fingerprint);
832 refreshUi();
833 }
834 });
835 builder.create().show();
836 }
837
838 public boolean hasStoragePermission(int requestCode) {
839 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
840 if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
841 requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
842 return false;
843 } else {
844 return true;
845 }
846 } else {
847 return true;
848 }
849 }
850
851 public void selectPresence(final Conversation conversation,
852 final OnPresenceSelected listener) {
853 final Contact contact = conversation.getContact();
854 if (conversation.hasValidOtrSession()) {
855 SessionID id = conversation.getOtrSession().getSessionID();
856 Jid jid;
857 try {
858 jid = Jid.fromString(id.getAccountID() + "/" + id.getUserID());
859 } catch (InvalidJidException e) {
860 jid = null;
861 }
862 conversation.setNextCounterpart(jid);
863 listener.onPresenceSelected();
864 } else if (!contact.showInRoster()) {
865 showAddToRosterDialog(conversation);
866 } else {
867 Presences presences = contact.getPresences();
868 if (presences.size() == 0) {
869 if (!contact.getOption(Contact.Options.TO)
870 && !contact.getOption(Contact.Options.ASKING)
871 && contact.getAccount().getStatus() == Account.State.ONLINE) {
872 showAskForPresenceDialog(contact);
873 } else if (!contact.getOption(Contact.Options.TO)
874 || !contact.getOption(Contact.Options.FROM)) {
875 warnMutalPresenceSubscription(conversation, listener);
876 } else {
877 conversation.setNextCounterpart(null);
878 listener.onPresenceSelected();
879 }
880 } else if (presences.size() == 1) {
881 String presence = presences.asStringArray()[0];
882 try {
883 conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence));
884 } catch (InvalidJidException e) {
885 conversation.setNextCounterpart(null);
886 }
887 listener.onPresenceSelected();
888 } else {
889 final StringBuilder presence = new StringBuilder();
890 AlertDialog.Builder builder = new AlertDialog.Builder(this);
891 builder.setTitle(getString(R.string.choose_presence));
892 final String[] presencesArray = presences.asStringArray();
893 int preselectedPresence = 0;
894 for (int i = 0; i < presencesArray.length; ++i) {
895 if (presencesArray[i].equals(contact.lastseen.presence)) {
896 preselectedPresence = i;
897 break;
898 }
899 }
900 presence.append(presencesArray[preselectedPresence]);
901 builder.setSingleChoiceItems(presencesArray,
902 preselectedPresence,
903 new DialogInterface.OnClickListener() {
904
905 @Override
906 public void onClick(DialogInterface dialog,
907 int which) {
908 presence.delete(0, presence.length());
909 presence.append(presencesArray[which]);
910 }
911 });
912 builder.setNegativeButton(R.string.cancel, null);
913 builder.setPositiveButton(R.string.ok, new OnClickListener() {
914
915 @Override
916 public void onClick(DialogInterface dialog, int which) {
917 try {
918 conversation.setNextCounterpart(Jid.fromParts(contact.getJid().getLocalpart(),contact.getJid().getDomainpart(),presence.toString()));
919 } catch (InvalidJidException e) {
920 conversation.setNextCounterpart(null);
921 }
922 listener.onPresenceSelected();
923 }
924 });
925 builder.create().show();
926 }
927 }
928 }
929
930 protected void onActivityResult(int requestCode, int resultCode,
931 final Intent data) {
932 super.onActivityResult(requestCode, resultCode, data);
933 if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
934 mPendingConferenceInvite = ConferenceInvite.parse(data);
935 if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
936 mPendingConferenceInvite.execute(this);
937 mPendingConferenceInvite = null;
938 }
939 }
940 }
941
942 private UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
943 @Override
944 public void success(final Conversation conversation) {
945 switchToConversation(conversation);
946 runOnUiThread(new Runnable() {
947 @Override
948 public void run() {
949 Toast.makeText(XmppActivity.this,R.string.conference_created,Toast.LENGTH_LONG).show();
950 }
951 });
952 }
953
954 @Override
955 public void error(final int errorCode, Conversation object) {
956 runOnUiThread(new Runnable() {
957 @Override
958 public void run() {
959 Toast.makeText(XmppActivity.this,errorCode,Toast.LENGTH_LONG).show();
960 }
961 });
962 }
963
964 @Override
965 public void userInputRequried(PendingIntent pi, Conversation object) {
966
967 }
968 };
969
970 public int getTertiaryTextColor() {
971 return this.mTertiaryTextColor;
972 }
973
974 public int getSecondaryTextColor() {
975 return this.mSecondaryTextColor;
976 }
977
978 public int getPrimaryTextColor() {
979 return this.mPrimaryTextColor;
980 }
981
982 public int getWarningTextColor() {
983 return this.mColorRed;
984 }
985
986 public int getOnlineColor() {
987 return this.mColorGreen;
988 }
989
990 public int getPrimaryBackgroundColor() {
991 return this.mPrimaryBackgroundColor;
992 }
993
994 public int getSecondaryBackgroundColor() {
995 return this.mSecondaryBackgroundColor;
996 }
997
998 public int getPixel(int dp) {
999 DisplayMetrics metrics = getResources().getDisplayMetrics();
1000 return ((int) (dp * metrics.density));
1001 }
1002
1003 public boolean copyTextToClipboard(String text, int labelResId) {
1004 ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
1005 String label = getResources().getString(labelResId);
1006 if (mClipBoardManager != null) {
1007 ClipData mClipData = ClipData.newPlainText(label, text);
1008 mClipBoardManager.setPrimaryClip(mClipData);
1009 return true;
1010 }
1011 return false;
1012 }
1013
1014 protected void registerNdefPushMessageCallback() {
1015 NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
1016 if (nfcAdapter != null && nfcAdapter.isEnabled()) {
1017 nfcAdapter.setNdefPushMessageCallback(new NfcAdapter.CreateNdefMessageCallback() {
1018 @Override
1019 public NdefMessage createNdefMessage(NfcEvent nfcEvent) {
1020 return new NdefMessage(new NdefRecord[]{
1021 NdefRecord.createUri(getShareableUri()),
1022 NdefRecord.createApplicationRecord("eu.siacs.conversations")
1023 });
1024 }
1025 }, this);
1026 }
1027 }
1028
1029 protected boolean neverCompressPictures() {
1030 return getPreferences().getString("picture_compression", "auto").equals("never");
1031 }
1032
1033 protected boolean manuallyChangePresence() {
1034 return getPreferences().getBoolean("manually_change_presence", false);
1035 }
1036
1037 protected void unregisterNdefPushMessageCallback() {
1038 NfcAdapter nfcAdapter = NfcAdapter.getDefaultAdapter(this);
1039 if (nfcAdapter != null && nfcAdapter.isEnabled()) {
1040 nfcAdapter.setNdefPushMessageCallback(null,this);
1041 }
1042 }
1043
1044 protected String getShareableUri() {
1045 return null;
1046 }
1047
1048 @Override
1049 public void onResume() {
1050 super.onResume();
1051 if (this.getShareableUri()!=null) {
1052 this.registerNdefPushMessageCallback();
1053 }
1054 }
1055
1056 protected int findTheme() {
1057 if (getPreferences().getBoolean("use_larger_font", false)) {
1058 return R.style.ConversationsTheme_LargerText;
1059 } else {
1060 return R.style.ConversationsTheme;
1061 }
1062 }
1063
1064 @Override
1065 public void onPause() {
1066 super.onPause();
1067 this.unregisterNdefPushMessageCallback();
1068 }
1069
1070 protected void showQrCode() {
1071 String uri = getShareableUri();
1072 if (uri!=null) {
1073 Point size = new Point();
1074 getWindowManager().getDefaultDisplay().getSize(size);
1075 final int width = (size.x < size.y ? size.x : size.y);
1076 Bitmap bitmap = createQrCodeBitmap(uri, width);
1077 ImageView view = new ImageView(this);
1078 view.setImageBitmap(bitmap);
1079 AlertDialog.Builder builder = new AlertDialog.Builder(this);
1080 builder.setView(view);
1081 builder.create().show();
1082 }
1083 }
1084
1085 protected Bitmap createQrCodeBitmap(String input, int size) {
1086 Log.d(Config.LOGTAG,"qr code requested size: "+size);
1087 try {
1088 final QRCodeWriter QR_CODE_WRITER = new QRCodeWriter();
1089 final Hashtable<EncodeHintType, Object> hints = new Hashtable<>();
1090 hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
1091 final BitMatrix result = QR_CODE_WRITER.encode(input, BarcodeFormat.QR_CODE, size, size, hints);
1092 final int width = result.getWidth();
1093 final int height = result.getHeight();
1094 final int[] pixels = new int[width * height];
1095 for (int y = 0; y < height; y++) {
1096 final int offset = y * width;
1097 for (int x = 0; x < width; x++) {
1098 pixels[offset + x] = result.get(x, y) ? Color.BLACK : Color.TRANSPARENT;
1099 }
1100 }
1101 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
1102 Log.d(Config.LOGTAG,"output size: "+width+"x"+height);
1103 bitmap.setPixels(pixels, 0, width, 0, 0, width, height);
1104 return bitmap;
1105 } catch (final WriterException e) {
1106 return null;
1107 }
1108 }
1109
1110 protected Account extractAccount(Intent intent) {
1111 String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
1112 try {
1113 return jid != null ? xmppConnectionService.findAccountByJid(Jid.fromString(jid)) : null;
1114 } catch (InvalidJidException e) {
1115 return null;
1116 }
1117 }
1118
1119 public static class ConferenceInvite {
1120 private String uuid;
1121 private List<Jid> jids = new ArrayList<>();
1122
1123 public static ConferenceInvite parse(Intent data) {
1124 ConferenceInvite invite = new ConferenceInvite();
1125 invite.uuid = data.getStringExtra("conversation");
1126 if (invite.uuid == null) {
1127 return null;
1128 }
1129 try {
1130 if (data.getBooleanExtra("multiple", false)) {
1131 String[] toAdd = data.getStringArrayExtra("contacts");
1132 for (String item : toAdd) {
1133 invite.jids.add(Jid.fromString(item));
1134 }
1135 } else {
1136 invite.jids.add(Jid.fromString(data.getStringExtra("contact")));
1137 }
1138 } catch (final InvalidJidException ignored) {
1139 return null;
1140 }
1141 return invite;
1142 }
1143
1144 public void execute(XmppActivity activity) {
1145 XmppConnectionService service = activity.xmppConnectionService;
1146 Conversation conversation = service.findConversationByUuid(this.uuid);
1147 if (conversation == null) {
1148 return;
1149 }
1150 if (conversation.getMode() == Conversation.MODE_MULTI) {
1151 for (Jid jid : jids) {
1152 service.invite(conversation, jid);
1153 }
1154 } else {
1155 jids.add(conversation.getJid().toBareJid());
1156 service.createAdhocConference(conversation.getAccount(), jids, activity.adhocCallback);
1157 }
1158 }
1159 }
1160
1161 public AvatarService avatarService() {
1162 return xmppConnectionService.getAvatarService();
1163 }
1164
1165 class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
1166 private final WeakReference<ImageView> imageViewReference;
1167 private Message message = null;
1168
1169 public BitmapWorkerTask(ImageView imageView) {
1170 imageViewReference = new WeakReference<>(imageView);
1171 }
1172
1173 @Override
1174 protected Bitmap doInBackground(Message... params) {
1175 if (isCancelled()) {
1176 return null;
1177 }
1178 message = params[0];
1179 try {
1180 return xmppConnectionService.getFileBackend().getThumbnail(
1181 message, (int) (metrics.density * 288), false);
1182 } catch (FileNotFoundException e) {
1183 return null;
1184 }
1185 }
1186
1187 @Override
1188 protected void onPostExecute(Bitmap bitmap) {
1189 if (bitmap != null && !isCancelled()) {
1190 final ImageView imageView = imageViewReference.get();
1191 if (imageView != null) {
1192 imageView.setImageBitmap(bitmap);
1193 imageView.setBackgroundColor(0x00000000);
1194 }
1195 }
1196 }
1197 }
1198
1199 public void loadBitmap(Message message, ImageView imageView) {
1200 Bitmap bm;
1201 try {
1202 bm = xmppConnectionService.getFileBackend().getThumbnail(message,
1203 (int) (metrics.density * 288), true);
1204 } catch (FileNotFoundException e) {
1205 bm = null;
1206 }
1207 if (bm != null) {
1208 cancelPotentialWork(message, imageView);
1209 imageView.setImageBitmap(bm);
1210 imageView.setBackgroundColor(0x00000000);
1211 } else {
1212 if (cancelPotentialWork(message, imageView)) {
1213 imageView.setBackgroundColor(0xff333333);
1214 imageView.setImageDrawable(null);
1215 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1216 final AsyncDrawable asyncDrawable = new AsyncDrawable(
1217 getResources(), null, task);
1218 imageView.setImageDrawable(asyncDrawable);
1219 try {
1220 task.execute(message);
1221 } catch (final RejectedExecutionException ignored) {
1222 ignored.printStackTrace();
1223 }
1224 }
1225 }
1226 }
1227
1228 public static boolean cancelPotentialWork(Message message, ImageView imageView) {
1229 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
1230
1231 if (bitmapWorkerTask != null) {
1232 final Message oldMessage = bitmapWorkerTask.message;
1233 if (oldMessage == null || message != oldMessage) {
1234 bitmapWorkerTask.cancel(true);
1235 } else {
1236 return false;
1237 }
1238 }
1239 return true;
1240 }
1241
1242 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
1243 if (imageView != null) {
1244 final Drawable drawable = imageView.getDrawable();
1245 if (drawable instanceof AsyncDrawable) {
1246 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
1247 return asyncDrawable.getBitmapWorkerTask();
1248 }
1249 }
1250 return null;
1251 }
1252
1253 static class AsyncDrawable extends BitmapDrawable {
1254 private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1255
1256 public AsyncDrawable(Resources res, Bitmap bitmap,
1257 BitmapWorkerTask bitmapWorkerTask) {
1258 super(res, bitmap);
1259 bitmapWorkerTaskReference = new WeakReference<>(
1260 bitmapWorkerTask);
1261 }
1262
1263 public BitmapWorkerTask getBitmapWorkerTask() {
1264 return bitmapWorkerTaskReference.get();
1265 }
1266 }
1267}