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