1package eu.siacs.conversations.ui;
2
3import android.Manifest;
4import android.annotation.SuppressLint;
5import android.annotation.TargetApi;
6import android.app.PendingIntent;
7import android.content.ActivityNotFoundException;
8import android.content.ClipData;
9import android.content.ClipboardManager;
10import android.content.ComponentName;
11import android.content.Context;
12import android.content.ContextWrapper;
13import android.content.DialogInterface;
14import android.content.Intent;
15import android.content.IntentSender.SendIntentException;
16import android.content.ServiceConnection;
17import android.content.SharedPreferences;
18import android.content.pm.PackageManager;
19import android.content.pm.ResolveInfo;
20import android.content.res.Resources;
21import android.content.res.TypedArray;
22import android.graphics.Bitmap;
23import android.graphics.Color;
24import android.graphics.Point;
25import android.graphics.drawable.BitmapDrawable;
26import android.graphics.drawable.Drawable;
27import android.net.ConnectivityManager;
28import android.net.Uri;
29import android.os.AsyncTask;
30import android.os.Build;
31import android.os.Bundle;
32import android.os.Handler;
33import android.os.IBinder;
34import android.os.PowerManager;
35import android.os.SystemClock;
36import android.preference.PreferenceManager;
37import android.text.Html;
38import android.text.InputType;
39import android.util.DisplayMetrics;
40import android.util.Log;
41import android.view.Menu;
42import android.view.MenuItem;
43import android.view.View;
44import android.widget.ImageView;
45import android.widget.Toast;
46
47import androidx.annotation.BoolRes;
48import androidx.annotation.NonNull;
49import androidx.annotation.StringRes;
50import androidx.appcompat.app.AlertDialog;
51import androidx.appcompat.app.AlertDialog.Builder;
52import androidx.appcompat.app.AppCompatDelegate;
53import androidx.databinding.DataBindingUtil;
54
55import java.io.IOException;
56import java.lang.ref.WeakReference;
57import java.util.ArrayList;
58import java.util.List;
59import java.util.concurrent.RejectedExecutionException;
60
61import eu.siacs.conversations.Config;
62import eu.siacs.conversations.R;
63import eu.siacs.conversations.crypto.PgpEngine;
64import eu.siacs.conversations.databinding.DialogQuickeditBinding;
65import eu.siacs.conversations.entities.Account;
66import eu.siacs.conversations.entities.Contact;
67import eu.siacs.conversations.entities.Conversation;
68import eu.siacs.conversations.entities.Message;
69import eu.siacs.conversations.entities.Presences;
70import eu.siacs.conversations.services.AvatarService;
71import eu.siacs.conversations.services.BarcodeProvider;
72import eu.siacs.conversations.services.QuickConversationsService;
73import eu.siacs.conversations.services.XmppConnectionService;
74import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
75import eu.siacs.conversations.ui.service.EmojiService;
76import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
77import eu.siacs.conversations.ui.util.PresenceSelector;
78import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
79import eu.siacs.conversations.utils.AccountUtils;
80import eu.siacs.conversations.utils.ExceptionHelper;
81import eu.siacs.conversations.utils.ThemeHelper;
82import eu.siacs.conversations.xmpp.Jid;
83import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
84import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
85
86public abstract class XmppActivity extends ActionBarActivity {
87
88 public static final String EXTRA_ACCOUNT = "account";
89 protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
90 protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
91 protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103;
92 protected static final int REQUEST_BATTERY_OP = 0x49ff;
93 public XmppConnectionService xmppConnectionService;
94 public boolean xmppConnectionServiceBound = false;
95
96 protected static final String FRAGMENT_TAG_DIALOG = "dialog";
97
98 private boolean isCameraFeatureAvailable = false;
99
100 protected int mTheme;
101 protected boolean mUsingEnterKey = false;
102 protected boolean mUseTor = false;
103 protected Toast mToast;
104 public Runnable onOpenPGPKeyPublished = () -> Toast.makeText(XmppActivity.this, R.string.openpgp_has_been_published, Toast.LENGTH_SHORT).show();
105 protected ConferenceInvite mPendingConferenceInvite = null;
106 protected ServiceConnection mConnection = new ServiceConnection() {
107
108 @Override
109 public void onServiceConnected(ComponentName className, IBinder service) {
110 XmppConnectionBinder binder = (XmppConnectionBinder) service;
111 xmppConnectionService = binder.getService();
112 xmppConnectionServiceBound = true;
113 registerListeners();
114 onBackendConnected();
115 }
116
117 @Override
118 public void onServiceDisconnected(ComponentName arg0) {
119 xmppConnectionServiceBound = false;
120 }
121 };
122 private DisplayMetrics metrics;
123 private long mLastUiRefresh = 0;
124 private final Handler mRefreshUiHandler = new Handler();
125 private final Runnable mRefreshUiRunnable = () -> {
126 mLastUiRefresh = SystemClock.elapsedRealtime();
127 refreshUiReal();
128 };
129 private final UiCallback<Conversation> adhocCallback = new UiCallback<Conversation>() {
130 @Override
131 public void success(final Conversation conversation) {
132 runOnUiThread(() -> {
133 switchToConversation(conversation);
134 hideToast();
135 });
136 }
137
138 @Override
139 public void error(final int errorCode, Conversation object) {
140 runOnUiThread(() -> replaceToast(getString(errorCode)));
141 }
142
143 @Override
144 public void userInputRequired(PendingIntent pi, Conversation object) {
145
146 }
147 };
148 public boolean mSkipBackgroundBinding = false;
149
150 public static boolean cancelPotentialWork(Message message, ImageView imageView) {
151 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
152
153 if (bitmapWorkerTask != null) {
154 final Message oldMessage = bitmapWorkerTask.message;
155 if (oldMessage == null || message != oldMessage) {
156 bitmapWorkerTask.cancel(true);
157 } else {
158 return false;
159 }
160 }
161 return true;
162 }
163
164 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
165 if (imageView != null) {
166 final Drawable drawable = imageView.getDrawable();
167 if (drawable instanceof AsyncDrawable) {
168 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
169 return asyncDrawable.getBitmapWorkerTask();
170 }
171 }
172 return null;
173 }
174
175 protected void hideToast() {
176 if (mToast != null) {
177 mToast.cancel();
178 }
179 }
180
181 protected void replaceToast(String msg) {
182 replaceToast(msg, true);
183 }
184
185 protected void replaceToast(String msg, boolean showlong) {
186 hideToast();
187 mToast = Toast.makeText(this, msg, showlong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT);
188 mToast.show();
189 }
190
191 protected final void refreshUi() {
192 final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh;
193 if (diff > Config.REFRESH_UI_INTERVAL) {
194 mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
195 runOnUiThread(mRefreshUiRunnable);
196 } else {
197 final long next = Config.REFRESH_UI_INTERVAL - diff;
198 mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
199 mRefreshUiHandler.postDelayed(mRefreshUiRunnable, next);
200 }
201 }
202
203 abstract protected void refreshUiReal();
204
205 @Override
206 protected void onStart() {
207 super.onStart();
208 if (!xmppConnectionServiceBound) {
209 if (this.mSkipBackgroundBinding) {
210 Log.d(Config.LOGTAG, "skipping background binding");
211 } else {
212 connectToBackend();
213 }
214 } else {
215 this.registerListeners();
216 this.onBackendConnected();
217 }
218 this.mUsingEnterKey = usingEnterKey();
219 this.mUseTor = useTor();
220 }
221
222 public void connectToBackend() {
223 Intent intent = new Intent(this, XmppConnectionService.class);
224 intent.setAction("ui");
225 try {
226 startService(intent);
227 } catch (IllegalStateException e) {
228 Log.w(Config.LOGTAG, "unable to start service from " + getClass().getSimpleName());
229 }
230 bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
231 }
232
233 @Override
234 protected void onStop() {
235 super.onStop();
236 if (xmppConnectionServiceBound) {
237 this.unregisterListeners();
238 unbindService(mConnection);
239 xmppConnectionServiceBound = false;
240 }
241 }
242
243
244 public boolean hasPgp() {
245 return xmppConnectionService.getPgpEngine() != null;
246 }
247
248 public void showInstallPgpDialog() {
249 Builder builder = new AlertDialog.Builder(this);
250 builder.setTitle(getString(R.string.openkeychain_required));
251 builder.setIconAttribute(android.R.attr.alertDialogIcon);
252 builder.setMessage(Html.fromHtml(getString(R.string.openkeychain_required_long, getString(R.string.app_name))));
253 builder.setNegativeButton(getString(R.string.cancel), null);
254 builder.setNeutralButton(getString(R.string.restart),
255 (dialog, which) -> {
256 if (xmppConnectionServiceBound) {
257 unbindService(mConnection);
258 xmppConnectionServiceBound = false;
259 }
260 stopService(new Intent(XmppActivity.this,
261 XmppConnectionService.class));
262 finish();
263 });
264 builder.setPositiveButton(getString(R.string.install),
265 (dialog, which) -> {
266 Uri uri = Uri
267 .parse("market://details?id=org.sufficientlysecure.keychain");
268 Intent marketIntent = new Intent(Intent.ACTION_VIEW,
269 uri);
270 PackageManager manager = getApplicationContext()
271 .getPackageManager();
272 List<ResolveInfo> infos = manager
273 .queryIntentActivities(marketIntent, 0);
274 if (infos.size() > 0) {
275 startActivity(marketIntent);
276 } else {
277 uri = Uri.parse("http://www.openkeychain.org/");
278 Intent browserIntent = new Intent(
279 Intent.ACTION_VIEW, uri);
280 startActivity(browserIntent);
281 }
282 finish();
283 });
284 builder.create().show();
285 }
286
287 abstract void onBackendConnected();
288
289 protected void registerListeners() {
290 if (this instanceof XmppConnectionService.OnConversationUpdate) {
291 this.xmppConnectionService.setOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this);
292 }
293 if (this instanceof XmppConnectionService.OnAccountUpdate) {
294 this.xmppConnectionService.setOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
295 }
296 if (this instanceof XmppConnectionService.OnCaptchaRequested) {
297 this.xmppConnectionService.setOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
298 }
299 if (this instanceof XmppConnectionService.OnRosterUpdate) {
300 this.xmppConnectionService.setOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
301 }
302 if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
303 this.xmppConnectionService.setOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this);
304 }
305 if (this instanceof OnUpdateBlocklist) {
306 this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this);
307 }
308 if (this instanceof XmppConnectionService.OnShowErrorToast) {
309 this.xmppConnectionService.setOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
310 }
311 if (this instanceof OnKeyStatusUpdated) {
312 this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this);
313 }
314 if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) {
315 this.xmppConnectionService.setOnRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this);
316 }
317 }
318
319 protected void unregisterListeners() {
320 if (this instanceof XmppConnectionService.OnConversationUpdate) {
321 this.xmppConnectionService.removeOnConversationListChangedListener((XmppConnectionService.OnConversationUpdate) this);
322 }
323 if (this instanceof XmppConnectionService.OnAccountUpdate) {
324 this.xmppConnectionService.removeOnAccountListChangedListener((XmppConnectionService.OnAccountUpdate) this);
325 }
326 if (this instanceof XmppConnectionService.OnCaptchaRequested) {
327 this.xmppConnectionService.removeOnCaptchaRequestedListener((XmppConnectionService.OnCaptchaRequested) this);
328 }
329 if (this instanceof XmppConnectionService.OnRosterUpdate) {
330 this.xmppConnectionService.removeOnRosterUpdateListener((XmppConnectionService.OnRosterUpdate) this);
331 }
332 if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
333 this.xmppConnectionService.removeOnMucRosterUpdateListener((XmppConnectionService.OnMucRosterUpdate) this);
334 }
335 if (this instanceof OnUpdateBlocklist) {
336 this.xmppConnectionService.removeOnUpdateBlocklistListener((OnUpdateBlocklist) this);
337 }
338 if (this instanceof XmppConnectionService.OnShowErrorToast) {
339 this.xmppConnectionService.removeOnShowErrorToastListener((XmppConnectionService.OnShowErrorToast) this);
340 }
341 if (this instanceof OnKeyStatusUpdated) {
342 this.xmppConnectionService.removeOnNewKeysAvailableListener((OnKeyStatusUpdated) this);
343 }
344 if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) {
345 this.xmppConnectionService.removeRtpConnectionUpdateListener((XmppConnectionService.OnJingleRtpConnectionUpdate) this);
346 }
347 }
348
349 @Override
350 public boolean onOptionsItemSelected(final MenuItem item) {
351 switch (item.getItemId()) {
352 case R.id.action_settings:
353 startActivity(new Intent(this, SettingsActivity.class));
354 break;
355 case R.id.action_accounts:
356 AccountUtils.launchManageAccounts(this);
357 break;
358 case R.id.action_account:
359 AccountUtils.launchManageAccount(this);
360 break;
361 case android.R.id.home:
362 finish();
363 break;
364 case R.id.action_show_qr_code:
365 showQrCode();
366 break;
367 }
368 return super.onOptionsItemSelected(item);
369 }
370
371 public void selectPresence(final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
372 final Contact contact = conversation.getContact();
373 if (contact.showInRoster() || contact.isSelf()) {
374 final Presences presences = contact.getPresences();
375 if (presences.size() == 0) {
376 if (contact.isSelf()) {
377 conversation.setNextCounterpart(null);
378 listener.onPresenceSelected();
379 } else if (!contact.getOption(Contact.Options.TO)
380 && !contact.getOption(Contact.Options.ASKING)
381 && contact.getAccount().getStatus() == Account.State.ONLINE) {
382 showAskForPresenceDialog(contact);
383 } else if (!contact.getOption(Contact.Options.TO)
384 || !contact.getOption(Contact.Options.FROM)) {
385 PresenceSelector.warnMutualPresenceSubscription(this, conversation, listener);
386 } else {
387 conversation.setNextCounterpart(null);
388 listener.onPresenceSelected();
389 }
390 } else if (presences.size() == 1) {
391 final String presence = presences.toResourceArray()[0];
392 conversation.setNextCounterpart(PresenceSelector.getNextCounterpart(contact, presence));
393 listener.onPresenceSelected();
394 } else {
395 PresenceSelector.showPresenceSelectionDialog(this, conversation, listener);
396 }
397 } else {
398 showAddToRosterDialog(conversation.getContact());
399 }
400 }
401
402 @SuppressLint("UnsupportedChromeOsCameraSystemFeature")
403 @Override
404 protected void onCreate(Bundle savedInstanceState) {
405 super.onCreate(savedInstanceState);
406 metrics = getResources().getDisplayMetrics();
407 ExceptionHelper.init(getApplicationContext());
408 new EmojiService(this).init();
409 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
410 this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
411 } else {
412 this.isCameraFeatureAvailable = getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA);
413 }
414 this.mTheme = findTheme();
415 setTheme(this.mTheme);
416 }
417
418 protected boolean isCameraFeatureAvailable() {
419 return this.isCameraFeatureAvailable;
420 }
421
422 public boolean isDarkTheme() {
423 return ThemeHelper.isDark(mTheme);
424 }
425
426 public int getThemeResource(int r_attr_name, int r_drawable_def) {
427 int[] attrs = {r_attr_name};
428 TypedArray ta = this.getTheme().obtainStyledAttributes(attrs);
429
430 int res = ta.getResourceId(0, r_drawable_def);
431 ta.recycle();
432
433 return res;
434 }
435
436 protected boolean isOptimizingBattery() {
437 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
438 final PowerManager pm = (PowerManager) getSystemService(POWER_SERVICE);
439 return pm != null
440 && !pm.isIgnoringBatteryOptimizations(getPackageName());
441 } else {
442 return false;
443 }
444 }
445
446 protected boolean isAffectedByDataSaver() {
447 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
448 final ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
449 return cm != null
450 && cm.isActiveNetworkMetered()
451 && cm.getRestrictBackgroundStatus() == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
452 } else {
453 return false;
454 }
455 }
456
457 private boolean usingEnterKey() {
458 return getBooleanPreference("display_enter_key", R.bool.display_enter_key);
459 }
460
461 private boolean useTor() {
462 return QuickConversationsService.isConversations() && getBooleanPreference("use_tor", R.bool.use_tor);
463 }
464
465 protected SharedPreferences getPreferences() {
466 return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
467 }
468
469 protected boolean getBooleanPreference(String name, @BoolRes int res) {
470 return getPreferences().getBoolean(name, getResources().getBoolean(res));
471 }
472
473 public void switchToConversation(Conversation conversation) {
474 switchToConversation(conversation, null);
475 }
476
477 public void switchToConversationAndQuote(Conversation conversation, String text) {
478 switchToConversation(conversation, text, true, null, false, false);
479 }
480
481 public void switchToConversation(Conversation conversation, String text) {
482 switchToConversation(conversation, text, false, null, false, false);
483 }
484
485 public void switchToConversationDoNotAppend(Conversation conversation, String text) {
486 switchToConversation(conversation, text, false, null, false, true);
487 }
488
489 public void highlightInMuc(Conversation conversation, String nick) {
490 switchToConversation(conversation, null, false, nick, false, false);
491 }
492
493 public void privateMsgInMuc(Conversation conversation, String nick) {
494 switchToConversation(conversation, null, false, nick, true, false);
495 }
496
497 private void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend) {
498 Intent intent = new Intent(this, ConversationsActivity.class);
499 intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
500 intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
501 if (text != null) {
502 intent.putExtra(Intent.EXTRA_TEXT, text);
503 if (asQuote) {
504 intent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, true);
505 }
506 }
507 if (nick != null) {
508 intent.putExtra(ConversationsActivity.EXTRA_NICK, nick);
509 intent.putExtra(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, pm);
510 }
511 if (doNotAppend) {
512 intent.putExtra(ConversationsActivity.EXTRA_DO_NOT_APPEND, true);
513 }
514 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
515 startActivity(intent);
516 finish();
517 }
518
519 public void switchToContactDetails(Contact contact) {
520 switchToContactDetails(contact, null);
521 }
522
523 public void switchToContactDetails(Contact contact, String messageFingerprint) {
524 Intent intent = new Intent(this, ContactDetailsActivity.class);
525 intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
526 intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().asBareJid().toEscapedString());
527 intent.putExtra("contact", contact.getJid().toEscapedString());
528 intent.putExtra("fingerprint", messageFingerprint);
529 startActivity(intent);
530 }
531
532 public void switchToAccount(Account account, String fingerprint) {
533 switchToAccount(account, false, fingerprint);
534 }
535
536 public void switchToAccount(Account account) {
537 switchToAccount(account, false, null);
538 }
539
540 public void switchToAccount(Account account, boolean init, String fingerprint) {
541 Intent intent = new Intent(this, EditAccountActivity.class);
542 intent.putExtra("jid", account.getJid().asBareJid().toEscapedString());
543 intent.putExtra("init", init);
544 if (init) {
545 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_NO_ANIMATION);
546 }
547 if (fingerprint != null) {
548 intent.putExtra("fingerprint", fingerprint);
549 }
550 startActivity(intent);
551 if (init) {
552 overridePendingTransition(0, 0);
553 }
554 }
555
556 protected void delegateUriPermissionsToService(Uri uri) {
557 Intent intent = new Intent(this, XmppConnectionService.class);
558 intent.setAction(Intent.ACTION_SEND);
559 intent.setData(uri);
560 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
561 try {
562 startService(intent);
563 } catch (Exception e) {
564 Log.e(Config.LOGTAG, "unable to delegate uri permission", e);
565 }
566 }
567
568 protected void inviteToConversation(Conversation conversation) {
569 startActivityForResult(ChooseContactActivity.create(this, conversation), REQUEST_INVITE_TO_CONVERSATION);
570 }
571
572 protected void announcePgp(final Account account, final Conversation conversation, Intent intent, final Runnable onSuccess) {
573 if (account.getPgpId() == 0) {
574 choosePgpSignId(account);
575 } else {
576 String status = null;
577 if (manuallyChangePresence()) {
578 status = account.getPresenceStatusMessage();
579 }
580 if (status == null) {
581 status = "";
582 }
583 xmppConnectionService.getPgpEngine().generateSignature(intent, account, status, new UiCallback<String>() {
584
585 @Override
586 public void userInputRequired(PendingIntent pi, String signature) {
587 try {
588 startIntentSenderForResult(pi.getIntentSender(), REQUEST_ANNOUNCE_PGP, null, 0, 0, 0);
589 } catch (final SendIntentException ignored) {
590 }
591 }
592
593 @Override
594 public void success(String signature) {
595 account.setPgpSignature(signature);
596 xmppConnectionService.databaseBackend.updateAccount(account);
597 xmppConnectionService.sendPresence(account);
598 if (conversation != null) {
599 conversation.setNextEncryption(Message.ENCRYPTION_PGP);
600 xmppConnectionService.updateConversation(conversation);
601 refreshUi();
602 }
603 if (onSuccess != null) {
604 runOnUiThread(onSuccess);
605 }
606 }
607
608 @Override
609 public void error(int error, String signature) {
610 if (error == 0) {
611 account.setPgpSignId(0);
612 account.unsetPgpSignature();
613 xmppConnectionService.databaseBackend.updateAccount(account);
614 choosePgpSignId(account);
615 } else {
616 displayErrorDialog(error);
617 }
618 }
619 });
620 }
621 }
622
623 @SuppressWarnings("deprecation")
624 @TargetApi(Build.VERSION_CODES.JELLY_BEAN)
625 protected void setListItemBackgroundOnView(View view) {
626 int sdk = android.os.Build.VERSION.SDK_INT;
627 if (sdk < android.os.Build.VERSION_CODES.JELLY_BEAN) {
628 view.setBackgroundDrawable(getResources().getDrawable(R.drawable.greybackground));
629 } else {
630 view.setBackground(getResources().getDrawable(R.drawable.greybackground));
631 }
632 }
633
634 protected void choosePgpSignId(Account account) {
635 xmppConnectionService.getPgpEngine().chooseKey(account, new UiCallback<Account>() {
636 @Override
637 public void success(Account account1) {
638 }
639
640 @Override
641 public void error(int errorCode, Account object) {
642
643 }
644
645 @Override
646 public void userInputRequired(PendingIntent pi, Account object) {
647 try {
648 startIntentSenderForResult(pi.getIntentSender(),
649 REQUEST_CHOOSE_PGP_ID, null, 0, 0, 0);
650 } catch (final SendIntentException ignored) {
651 }
652 }
653 });
654 }
655
656 protected void displayErrorDialog(final int errorCode) {
657 runOnUiThread(() -> {
658 Builder builder = new Builder(XmppActivity.this);
659 builder.setIconAttribute(android.R.attr.alertDialogIcon);
660 builder.setTitle(getString(R.string.error));
661 builder.setMessage(errorCode);
662 builder.setNeutralButton(R.string.accept, null);
663 builder.create().show();
664 });
665
666 }
667
668 protected void showAddToRosterDialog(final Contact contact) {
669 AlertDialog.Builder builder = new AlertDialog.Builder(this);
670 builder.setTitle(contact.getJid().toString());
671 builder.setMessage(getString(R.string.not_in_roster));
672 builder.setNegativeButton(getString(R.string.cancel), null);
673 builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> xmppConnectionService.createContact(contact, true));
674 builder.create().show();
675 }
676
677 private void showAskForPresenceDialog(final Contact contact) {
678 AlertDialog.Builder builder = new AlertDialog.Builder(this);
679 builder.setTitle(contact.getJid().toString());
680 builder.setMessage(R.string.request_presence_updates);
681 builder.setNegativeButton(R.string.cancel, null);
682 builder.setPositiveButton(R.string.request_now,
683 (dialog, which) -> {
684 if (xmppConnectionServiceBound) {
685 xmppConnectionService.sendPresencePacket(contact
686 .getAccount(), xmppConnectionService
687 .getPresenceGenerator()
688 .requestPresenceUpdatesFrom(contact));
689 }
690 });
691 builder.create().show();
692 }
693
694 protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback) {
695 quickEdit(previousValue, callback, hint, false, false);
696 }
697
698 protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback, boolean permitEmpty) {
699 quickEdit(previousValue, callback, hint, false, permitEmpty);
700 }
701
702 protected void quickPasswordEdit(String previousValue, OnValueEdited callback) {
703 quickEdit(previousValue, callback, R.string.password, true, false);
704 }
705
706 @SuppressLint("InflateParams")
707 private void quickEdit(final String previousValue,
708 final OnValueEdited callback,
709 final @StringRes int hint,
710 boolean password,
711 boolean permitEmpty) {
712 AlertDialog.Builder builder = new AlertDialog.Builder(this);
713 DialogQuickeditBinding binding = DataBindingUtil.inflate(getLayoutInflater(), R.layout.dialog_quickedit, null, false);
714 if (password) {
715 binding.inputEditText.setInputType(InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
716 }
717 builder.setPositiveButton(R.string.accept, null);
718 if (hint != 0) {
719 binding.inputLayout.setHint(getString(hint));
720 }
721 binding.inputEditText.requestFocus();
722 if (previousValue != null) {
723 binding.inputEditText.getText().append(previousValue);
724 }
725 builder.setView(binding.getRoot());
726 builder.setNegativeButton(R.string.cancel, null);
727 final AlertDialog dialog = builder.create();
728 dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(binding.inputEditText));
729 dialog.show();
730 View.OnClickListener clickListener = v -> {
731 String value = binding.inputEditText.getText().toString();
732 if (!value.equals(previousValue) && (!value.trim().isEmpty() || permitEmpty)) {
733 String error = callback.onValueEdited(value);
734 if (error != null) {
735 binding.inputLayout.setError(error);
736 return;
737 }
738 }
739 SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
740 dialog.dismiss();
741 };
742 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
743 dialog.getButton(DialogInterface.BUTTON_NEGATIVE).setOnClickListener((v -> {
744 SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
745 dialog.dismiss();
746 }));
747 dialog.setCanceledOnTouchOutside(false);
748 dialog.setOnDismissListener(dialog1 -> {
749 SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
750 });
751 }
752
753 protected boolean hasStoragePermission(int requestCode) {
754 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
755 if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
756 requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
757 return false;
758 } else {
759 return true;
760 }
761 } else {
762 return true;
763 }
764 }
765
766 protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
767 super.onActivityResult(requestCode, resultCode, data);
768 if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
769 mPendingConferenceInvite = ConferenceInvite.parse(data);
770 if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
771 if (mPendingConferenceInvite.execute(this)) {
772 mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
773 mToast.show();
774 }
775 mPendingConferenceInvite = null;
776 }
777 }
778 }
779
780 public boolean copyTextToClipboard(String text, int labelResId) {
781 ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
782 String label = getResources().getString(labelResId);
783 if (mClipBoardManager != null) {
784 ClipData mClipData = ClipData.newPlainText(label, text);
785 mClipBoardManager.setPrimaryClip(mClipData);
786 return true;
787 }
788 return false;
789 }
790
791 protected boolean manuallyChangePresence() {
792 return getBooleanPreference(SettingsActivity.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
793 }
794
795 protected String getShareableUri() {
796 return getShareableUri(false);
797 }
798
799 protected String getShareableUri(boolean http) {
800 return null;
801 }
802
803 protected void shareLink(boolean http) {
804 String uri = getShareableUri(http);
805 if (uri == null || uri.isEmpty()) {
806 return;
807 }
808 Intent intent = new Intent(Intent.ACTION_SEND);
809 intent.setType("text/plain");
810 intent.putExtra(Intent.EXTRA_TEXT, getShareableUri(http));
811 try {
812 startActivity(Intent.createChooser(intent, getText(R.string.share_uri_with)));
813 } catch (ActivityNotFoundException e) {
814 Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
815 }
816 }
817
818 protected void launchOpenKeyChain(long keyId) {
819 PgpEngine pgp = XmppActivity.this.xmppConnectionService.getPgpEngine();
820 try {
821 startIntentSenderForResult(
822 pgp.getIntentForKey(keyId).getIntentSender(), 0, null, 0,
823 0, 0);
824 } catch (Throwable e) {
825 Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show();
826 }
827 }
828
829 @Override
830 public void onResume() {
831 super.onResume();
832 }
833
834 protected int findTheme() {
835 return ThemeHelper.find(this);
836 }
837
838 @Override
839 public void onPause() {
840 super.onPause();
841 }
842
843 @Override
844 public boolean onMenuOpened(int id, Menu menu) {
845 if (id == AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR && menu != null) {
846 MenuDoubleTabUtil.recordMenuOpen();
847 }
848 return super.onMenuOpened(id, menu);
849 }
850
851 protected void showQrCode() {
852 showQrCode(getShareableUri());
853 }
854
855 protected void showQrCode(final String uri) {
856 if (uri == null || uri.isEmpty()) {
857 return;
858 }
859 Point size = new Point();
860 getWindowManager().getDefaultDisplay().getSize(size);
861 final int width = (size.x < size.y ? size.x : size.y);
862 Bitmap bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width);
863 ImageView view = new ImageView(this);
864 view.setBackgroundColor(Color.WHITE);
865 view.setImageBitmap(bitmap);
866 AlertDialog.Builder builder = new AlertDialog.Builder(this);
867 builder.setView(view);
868 builder.create().show();
869 }
870
871 protected Account extractAccount(Intent intent) {
872 final String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
873 try {
874 return jid != null ? xmppConnectionService.findAccountByJid(Jid.ofEscaped(jid)) : null;
875 } catch (IllegalArgumentException e) {
876 return null;
877 }
878 }
879
880 public AvatarService avatarService() {
881 return xmppConnectionService.getAvatarService();
882 }
883
884 public void loadBitmap(Message message, ImageView imageView) {
885 Bitmap bm;
886 try {
887 bm = xmppConnectionService.getFileBackend().getThumbnail(message, (int) (metrics.density * 288), true);
888 } catch (IOException e) {
889 bm = null;
890 }
891 if (bm != null) {
892 cancelPotentialWork(message, imageView);
893 imageView.setImageBitmap(bm);
894 imageView.setBackgroundColor(0x00000000);
895 } else {
896 if (cancelPotentialWork(message, imageView)) {
897 imageView.setBackgroundColor(0xff333333);
898 imageView.setImageDrawable(null);
899 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
900 final AsyncDrawable asyncDrawable = new AsyncDrawable(
901 getResources(), null, task);
902 imageView.setImageDrawable(asyncDrawable);
903 try {
904 task.execute(message);
905 } catch (final RejectedExecutionException ignored) {
906 ignored.printStackTrace();
907 }
908 }
909 }
910 }
911
912 protected interface OnValueEdited {
913 String onValueEdited(String value);
914 }
915
916 public static class ConferenceInvite {
917 private String uuid;
918 private final List<Jid> jids = new ArrayList<>();
919
920 public static ConferenceInvite parse(Intent data) {
921 ConferenceInvite invite = new ConferenceInvite();
922 invite.uuid = data.getStringExtra(ChooseContactActivity.EXTRA_CONVERSATION);
923 if (invite.uuid == null) {
924 return null;
925 }
926 invite.jids.addAll(ChooseContactActivity.extractJabberIds(data));
927 return invite;
928 }
929
930 public boolean execute(XmppActivity activity) {
931 XmppConnectionService service = activity.xmppConnectionService;
932 Conversation conversation = service.findConversationByUuid(this.uuid);
933 if (conversation == null) {
934 return false;
935 }
936 if (conversation.getMode() == Conversation.MODE_MULTI) {
937 for (Jid jid : jids) {
938 service.invite(conversation, jid);
939 }
940 return false;
941 } else {
942 jids.add(conversation.getJid().asBareJid());
943 return service.createAdhocConference(conversation.getAccount(), null, jids, activity.adhocCallback);
944 }
945 }
946 }
947
948 static class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
949 private final WeakReference<ImageView> imageViewReference;
950 private Message message = null;
951
952 private BitmapWorkerTask(ImageView imageView) {
953 this.imageViewReference = new WeakReference<>(imageView);
954 }
955
956 @Override
957 protected Bitmap doInBackground(Message... params) {
958 if (isCancelled()) {
959 return null;
960 }
961 message = params[0];
962 try {
963 final XmppActivity activity = find(imageViewReference);
964 if (activity != null && activity.xmppConnectionService != null) {
965 return activity.xmppConnectionService.getFileBackend().getThumbnail(message, (int) (activity.metrics.density * 288), false);
966 } else {
967 return null;
968 }
969 } catch (IOException e) {
970 return null;
971 }
972 }
973
974 @Override
975 protected void onPostExecute(final Bitmap bitmap) {
976 if (!isCancelled()) {
977 final ImageView imageView = imageViewReference.get();
978 if (imageView != null) {
979 imageView.setImageBitmap(bitmap);
980 imageView.setBackgroundColor(bitmap == null ? 0xff333333 : 0x00000000);
981 }
982 }
983 }
984 }
985
986 private static class AsyncDrawable extends BitmapDrawable {
987 private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
988
989 private AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
990 super(res, bitmap);
991 bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
992 }
993
994 private BitmapWorkerTask getBitmapWorkerTask() {
995 return bitmapWorkerTaskReference.get();
996 }
997 }
998
999 public static XmppActivity find(@NonNull WeakReference<ImageView> viewWeakReference) {
1000 final View view = viewWeakReference.get();
1001 return view == null ? null : find(view);
1002 }
1003
1004 public static XmppActivity find(@NonNull final View view) {
1005 Context context = view.getContext();
1006 while (context instanceof ContextWrapper) {
1007 if (context instanceof XmppActivity) {
1008 return (XmppActivity) context;
1009 }
1010 context = ((ContextWrapper) context).getBaseContext();
1011 }
1012 return null;
1013 }
1014}