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