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