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