1package eu.siacs.conversations.ui;
2
3import android.telephony.TelephonyManager;
4
5import android.Manifest;
6import android.annotation.SuppressLint;
7import android.app.NotificationManager;
8import android.app.PendingIntent;
9import android.content.ActivityNotFoundException;
10import android.content.ClipData;
11import android.content.ClipboardManager;
12import android.content.ComponentName;
13import android.content.Context;
14import android.content.ContextWrapper;
15import android.content.DialogInterface;
16import android.content.Intent;
17import android.content.IntentSender.SendIntentException;
18import android.content.ServiceConnection;
19import android.content.SharedPreferences;
20import android.content.pm.PackageManager;
21import android.content.pm.ResolveInfo;
22import android.content.res.Configuration;
23import android.content.res.Resources;
24import android.graphics.Bitmap;
25import android.graphics.Point;
26import android.graphics.drawable.AnimatedImageDrawable;
27import android.graphics.drawable.BitmapDrawable;
28import android.graphics.drawable.Drawable;
29import android.net.ConnectivityManager;
30import android.net.Uri;
31import android.os.AsyncTask;
32import android.os.Build;
33import android.os.Bundle;
34import android.os.Handler;
35import android.os.IBinder;
36import android.os.Looper;
37import android.os.PowerManager;
38import android.os.SystemClock;
39import android.preference.PreferenceManager;
40import android.provider.Settings;
41import android.text.Editable;
42import android.text.Html;
43import android.text.InputType;
44import android.text.Spannable;
45import android.util.DisplayMetrics;
46import android.util.Log;
47import android.util.Pair;
48import android.view.Menu;
49import android.view.MenuItem;
50import android.view.View;
51import android.webkit.ValueCallback;
52import android.widget.Button;
53import android.widget.CheckBox;
54import android.widget.ImageView;
55import android.widget.Toast;
56import androidx.annotation.BoolRes;
57import androidx.annotation.NonNull;
58import androidx.annotation.RequiresApi;
59import androidx.annotation.StringRes;
60import androidx.appcompat.app.AlertDialog;
61import androidx.appcompat.app.AppCompatDelegate;
62import androidx.core.content.pm.ShortcutInfoCompat;
63import androidx.core.content.pm.ShortcutManagerCompat;
64import androidx.databinding.DataBindingUtil;
65import androidx.recyclerview.widget.RecyclerView.Adapter;
66import com.google.android.material.color.MaterialColors;
67import com.google.android.material.dialog.MaterialAlertDialogBuilder;
68import com.google.common.base.Strings;
69import com.google.common.collect.Collections2;
70import com.google.common.collect.ImmutableSet;
71
72import com.cheogram.android.EmojiSearch;
73
74import com.otaliastudios.autocomplete.Autocomplete;
75import com.otaliastudios.autocomplete.AutocompleteCallback;
76import com.otaliastudios.autocomplete.AutocompletePolicy;
77import com.otaliastudios.autocomplete.AutocompletePresenter;
78import com.otaliastudios.autocomplete.RecyclerViewPresenter;
79
80import java.io.IOException;
81import java.lang.ref.WeakReference;
82import java.util.ArrayList;
83import java.util.HashMap;
84import java.util.List;
85import java.util.PriorityQueue;
86import java.util.concurrent.atomic.AtomicReference;
87import java.util.concurrent.RejectedExecutionException;
88
89import eu.siacs.conversations.AppSettings;
90import eu.siacs.conversations.BuildConfig;
91import eu.siacs.conversations.Config;
92import eu.siacs.conversations.R;
93import eu.siacs.conversations.crypto.PgpEngine;
94import eu.siacs.conversations.databinding.DialogAddReactionBinding;
95import eu.siacs.conversations.databinding.DialogQuickeditBinding;
96import eu.siacs.conversations.entities.Account;
97import eu.siacs.conversations.entities.Contact;
98import eu.siacs.conversations.entities.Conversation;
99import eu.siacs.conversations.entities.Message;
100import eu.siacs.conversations.entities.Presences;
101import eu.siacs.conversations.entities.Reaction;
102import eu.siacs.conversations.services.AvatarService;
103import eu.siacs.conversations.services.BarcodeProvider;
104import eu.siacs.conversations.services.NotificationService;
105import eu.siacs.conversations.services.QuickConversationsService;
106import eu.siacs.conversations.services.XmppConnectionService;
107import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
108import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
109import eu.siacs.conversations.ui.util.PresenceSelector;
110import eu.siacs.conversations.ui.util.SettingsUtils;
111import eu.siacs.conversations.ui.util.SoftKeyboardUtils;
112import eu.siacs.conversations.utils.AccountUtils;
113import eu.siacs.conversations.utils.Compatibility;
114import eu.siacs.conversations.utils.SignupUtils;
115import eu.siacs.conversations.utils.ThemeHelper;
116import eu.siacs.conversations.xmpp.Jid;
117import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
118import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
119import java.io.IOException;
120import java.lang.ref.WeakReference;
121import java.util.ArrayList;
122import java.util.Collection;
123import java.util.List;
124import java.util.concurrent.RejectedExecutionException;
125import java.util.function.Consumer;
126
127public abstract class XmppActivity extends ActionBarActivity {
128
129 public static final String EXTRA_ACCOUNT = "account";
130 protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
131 protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
132 protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103;
133 protected static final int REQUEST_BATTERY_OP = 0x49ff;
134 protected static final int REQUEST_POST_NOTIFICATION = 0x50ff;
135 public XmppConnectionService xmppConnectionService;
136 public boolean xmppConnectionServiceBound = false;
137
138 protected static final String FRAGMENT_TAG_DIALOG = "dialog";
139
140 private boolean isCameraFeatureAvailable = false;
141
142 protected int mTheme;
143 protected HashMap<Integer,Integer> mCustomColors;
144 protected boolean mUsingEnterKey = false;
145 protected boolean mUseTor = false;
146 protected Toast mToast;
147 public Runnable onOpenPGPKeyPublished =
148 () ->
149 Toast.makeText(
150 XmppActivity.this,
151 R.string.openpgp_has_been_published,
152 Toast.LENGTH_SHORT)
153 .show();
154 protected ConferenceInvite mPendingConferenceInvite = null;
155 protected PriorityQueue<Pair<Integer, ValueCallback<Uri[]>>> activityCallbacks =
156 Build.VERSION.SDK_INT >= 24 ? new PriorityQueue<>((x, y) -> y.first.compareTo(x.first)) : new PriorityQueue<>();
157 protected ServiceConnection mConnection =
158 new ServiceConnection() {
159
160 @Override
161 public void onServiceConnected(ComponentName className, IBinder service) {
162 XmppConnectionBinder binder = (XmppConnectionBinder) service;
163 xmppConnectionService = binder.getService();
164 xmppConnectionServiceBound = true;
165 registerListeners();
166 onBackendConnected();
167 }
168
169 @Override
170 public void onServiceDisconnected(ComponentName arg0) {
171 xmppConnectionServiceBound = false;
172 }
173 };
174 private DisplayMetrics metrics;
175 private long mLastUiRefresh = 0;
176 private final Handler mRefreshUiHandler = new Handler();
177 private final Runnable mRefreshUiRunnable =
178 () -> {
179 mLastUiRefresh = SystemClock.elapsedRealtime();
180 refreshUiReal();
181 };
182 private final UiCallback<Conversation> adhocCallback =
183 new UiCallback<Conversation>() {
184 @Override
185 public void success(final Conversation conversation) {
186 runOnUiThread(
187 () -> {
188 switchToConversation(conversation);
189 hideToast();
190 });
191 }
192
193 @Override
194 public void error(final int errorCode, Conversation object) {
195 runOnUiThread(() -> replaceToast(getString(errorCode)));
196 }
197
198 @Override
199 public void userInputRequired(PendingIntent pi, Conversation object) {}
200 };
201
202 public static boolean cancelPotentialWork(Message message, ImageView imageView) {
203 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
204
205 if (bitmapWorkerTask != null) {
206 final Message oldMessage = bitmapWorkerTask.message;
207 if (oldMessage == null || message != oldMessage) {
208 bitmapWorkerTask.cancel(true);
209 } else {
210 return false;
211 }
212 }
213 return true;
214 }
215
216 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
217 if (imageView != null) {
218 final Drawable drawable = imageView.getDrawable();
219 if (drawable instanceof AsyncDrawable) {
220 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
221 return asyncDrawable.getBitmapWorkerTask();
222 }
223 }
224 return null;
225 }
226
227 protected void hideToast() {
228 final var toast = this.mToast;
229 if (toast == null) {
230 return;
231 }
232 toast.cancel();
233 }
234
235 protected void replaceToast(String msg) {
236 replaceToast(msg, true);
237 }
238
239 protected void replaceToast(String msg, boolean showlong) {
240 hideToast();
241 mToast = Toast.makeText(this, msg, showlong ? Toast.LENGTH_LONG : Toast.LENGTH_SHORT);
242 mToast.show();
243 }
244
245 public final void refreshUi() {
246 final long diff = SystemClock.elapsedRealtime() - mLastUiRefresh;
247 if (diff > Config.REFRESH_UI_INTERVAL) {
248 mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
249 mRefreshUiHandler.postDelayed(mRefreshUiRunnable, 1);
250 } else {
251 final long next = Config.REFRESH_UI_INTERVAL - diff;
252 mRefreshUiHandler.removeCallbacks(mRefreshUiRunnable);
253 mRefreshUiHandler.postDelayed(mRefreshUiRunnable, next);
254 }
255 }
256
257 protected abstract void refreshUiReal();
258
259 @Override
260 public void onStart() {
261 super.onStart();
262 if (!this.mCustomColors.equals(ThemeHelper.applyCustomColors(this))) {
263 recreate();
264 }
265 if (!xmppConnectionServiceBound) {
266 connectToBackend();
267 } else {
268 this.registerListeners();
269 this.onBackendConnected();
270 }
271 this.mUsingEnterKey = usingEnterKey();
272 this.mUseTor = useTor();
273 }
274
275 public void connectToBackend() {
276 Intent intent = new Intent(this, XmppConnectionService.class);
277 intent.setAction("ui");
278 try {
279 startService(intent);
280 } catch (IllegalStateException e) {
281 Log.w(Config.LOGTAG, "unable to start service from " + getClass().getSimpleName());
282 }
283 bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
284 }
285
286 @Override
287 protected void onStop() {
288 super.onStop();
289 if (xmppConnectionServiceBound) {
290 this.unregisterListeners();
291 unbindService(mConnection);
292 xmppConnectionServiceBound = false;
293 }
294 }
295
296 @RequiresApi(api = Build.VERSION_CODES.R)
297 protected void configureCustomNotification(final ShortcutInfoCompat shortcut) {
298 final var notificationManager = getSystemService(NotificationManager.class);
299 final var channel =
300 notificationManager.getNotificationChannel(
301 NotificationService.MESSAGES_NOTIFICATION_CHANNEL, shortcut.getId());
302 if (channel != null && channel.getConversationId() != null) {
303 ShortcutManagerCompat.pushDynamicShortcut(this, shortcut);
304 openNotificationSettings(shortcut);
305 } else {
306 NotificationService.createConversationChannel(this, shortcut);
307 ShortcutManagerCompat.pushDynamicShortcut(this, shortcut);
308 openNotificationSettings(shortcut);
309 }
310 }
311
312 @RequiresApi(api = Build.VERSION_CODES.R)
313 protected void openNotificationSettings(final ShortcutInfoCompat shortcut) {
314 final var intent = new Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS);
315 intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
316 intent.putExtra(
317 Settings.EXTRA_CHANNEL_ID, NotificationService.MESSAGES_NOTIFICATION_CHANNEL);
318 intent.putExtra(Settings.EXTRA_CONVERSATION_ID, shortcut.getId());
319 startActivity(intent);
320 }
321
322 public boolean hasPgp() {
323 return xmppConnectionService.getPgpEngine() != null;
324 }
325
326 public void showInstallPgpDialog() {
327 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
328 builder.setTitle(getString(R.string.openkeychain_required));
329 builder.setIconAttribute(android.R.attr.alertDialogIcon);
330 builder.setMessage(
331 Html.fromHtml(
332 getString(
333 R.string.openkeychain_required_long,
334 getString(R.string.app_name))));
335 builder.setNegativeButton(getString(R.string.cancel), null);
336 builder.setNeutralButton(
337 getString(R.string.restart),
338 (dialog, which) -> {
339 if (xmppConnectionServiceBound) {
340 unbindService(mConnection);
341 xmppConnectionServiceBound = false;
342 }
343 stopService(new Intent(XmppActivity.this, XmppConnectionService.class));
344 finish();
345 });
346 builder.setPositiveButton(
347 getString(R.string.install),
348 (dialog, which) -> {
349 final Uri uri =
350 Uri.parse("market://details?id=org.sufficientlysecure.keychain");
351 Intent marketIntent = new Intent(Intent.ACTION_VIEW, uri);
352 PackageManager manager = getApplicationContext().getPackageManager();
353 final var infos = manager.queryIntentActivities(marketIntent, 0);
354 if (infos.isEmpty()) {
355 final var website = Uri.parse("http://www.openkeychain.org/");
356 final Intent browserIntent = new Intent(Intent.ACTION_VIEW, website);
357 try {
358 startActivity(browserIntent);
359 } catch (final ActivityNotFoundException e) {
360 Toast.makeText(
361 this,
362 R.string.application_found_to_open_website,
363 Toast.LENGTH_LONG)
364 .show();
365 }
366 } else {
367 startActivity(marketIntent);
368 }
369 finish();
370 });
371 builder.create().show();
372 }
373
374 public void addReaction(Consumer<EmojiSearch.Emoji> callback) {
375 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
376 final var layoutInflater = this.getLayoutInflater();
377 final DialogAddReactionBinding viewBinding =
378 DataBindingUtil.inflate(layoutInflater, R.layout.dialog_add_reaction, null, false);
379 builder.setView(viewBinding.getRoot());
380 final var dialog = builder.create();
381 for (final String emoji : Reaction.SUGGESTIONS) {
382 final Button button =
383 (Button)
384 layoutInflater.inflate(
385 R.layout.item_emoji_button, viewBinding.emojis, false);
386 viewBinding.emojis.addView(button);
387 button.setText(emoji);
388 button.setOnClickListener(
389 v -> {
390 callback.accept(new EmojiSearch.Emoji(emoji, 0));
391 dialog.dismiss();
392 });
393 }
394
395 final var emojiDebounce = new Handler(Looper.getMainLooper());
396 final var emojiSearch = xmppConnectionService.emojiSearch();
397 final var autocomplete = Autocomplete.<EmojiSearch.Emoji>on(viewBinding.search)
398 .with(getDrawable(R.drawable.background_message_bubble))
399 .with(new AutocompletePolicy() {
400 @Override
401 public boolean shouldShowPopup(@NonNull Spannable text, int cursorPos) { return true; }
402
403 @Override
404 public boolean shouldDismissPopup(@NonNull Spannable text, int cursorPos) { return false; }
405
406 @Override
407 public CharSequence getQuery(@NonNull Spannable text) { return text; }
408
409 @Override
410 public void onDismiss(@NonNull Spannable text) { }
411 })
412 .with(new RecyclerViewPresenter<EmojiSearch.Emoji>(this) {
413 protected EmojiSearch.EmojiSearchAdapter adapter;
414
415 @Override
416 protected Adapter instantiateAdapter() {
417 adapter = emojiSearch.makeAdapter(item -> dispatchClick(item));
418 return adapter;
419 }
420
421 @Override
422 protected PopupDimensions getPopupDimensions() {
423 final var dims = super.getPopupDimensions();
424 final var available = new android.widget.PopupWindow().getMaxAvailableHeight(viewBinding.search);
425 dims.maxHeight = available / 4;
426 return dims;
427 }
428
429 @Override
430 protected void onViewHidden() {
431 if (getRecyclerView() == null) return;
432 super.onViewHidden();
433 }
434
435 @Override
436 protected void onQuery(CharSequence query) {
437 emojiDebounce.removeCallbacksAndMessages(null);
438 emojiDebounce.postDelayed(() -> {
439 if (getRecyclerView() == null) return;
440 getRecyclerView().setItemAnimator(null);
441 adapter.search(XmppActivity.this, getRecyclerView(), query.toString());
442 }, 100L);
443 }
444 })
445 .with(new AutocompleteCallback<EmojiSearch.Emoji>() {
446 @Override
447 public boolean onPopupItemClicked(Editable editable, EmojiSearch.Emoji emoji) {
448 callback.accept(emoji);
449 dialog.dismiss();
450 return true;
451 }
452
453 @Override
454 public void onPopupVisibilityChanged(boolean shown) {}
455 }).build();
456
457 dialog.show();
458 viewBinding.search.setOnFocusChangeListener((v, hasFocus) -> {
459 if (hasFocus) autocomplete.showPopup(viewBinding.search.getText());
460 });
461 }
462
463 protected void deleteAccount(final Account account) {
464 this.deleteAccount(account, null);
465 }
466
467 protected void deleteAccount(final Account account, final Runnable postDelete) {
468 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
469 final View dialogView = getLayoutInflater().inflate(R.layout.dialog_delete_account, null);
470 final CheckBox deleteFromServer = dialogView.findViewById(R.id.delete_from_server);
471 builder.setView(dialogView);
472 builder.setTitle(R.string.mgmt_account_delete);
473 builder.setPositiveButton(getString(R.string.delete), null);
474 builder.setNegativeButton(getString(R.string.cancel), null);
475 final AlertDialog dialog = builder.create();
476 dialog.setOnShowListener(
477 dialogInterface -> {
478 final Button button = dialog.getButton(AlertDialog.BUTTON_POSITIVE);
479 button.setOnClickListener(
480 v -> {
481 final boolean unregister = deleteFromServer.isChecked();
482 if (unregister) {
483 if (account.isOnlineAndConnected()) {
484 deleteFromServer.setEnabled(false);
485 button.setText(R.string.please_wait);
486 button.setEnabled(false);
487 xmppConnectionService.unregisterAccount(
488 account,
489 result -> {
490 runOnUiThread(
491 () -> {
492 if (result) {
493 dialog.dismiss();
494 if (postDelete != null) {
495 postDelete.run();
496 }
497 if (xmppConnectionService
498 .getAccounts()
499 .size()
500 == 0
501 && Config
502 .MAGIC_CREATE_DOMAIN
503 != null) {
504 final Intent intent =
505 SignupUtils
506 .getSignUpIntent(
507 this);
508 intent.setFlags(
509 Intent
510 .FLAG_ACTIVITY_NEW_TASK
511 | Intent
512 .FLAG_ACTIVITY_CLEAR_TASK);
513 startActivity(intent);
514 }
515 } else {
516 deleteFromServer.setEnabled(
517 true);
518 button.setText(R.string.delete);
519 button.setEnabled(true);
520 Toast.makeText(
521 this,
522 R.string
523 .could_not_delete_account_from_server,
524 Toast
525 .LENGTH_LONG)
526 .show();
527 }
528 });
529 });
530 } else {
531 Toast.makeText(
532 this,
533 R.string.not_connected_try_again,
534 Toast.LENGTH_LONG)
535 .show();
536 }
537 } else {
538 xmppConnectionService.deleteAccount(account);
539 dialog.dismiss();
540 if (xmppConnectionService.getAccounts().size() == 0
541 && Config.MAGIC_CREATE_DOMAIN != null) {
542 final Intent intent = SignupUtils.getSignUpIntent(this);
543 intent.setFlags(
544 Intent.FLAG_ACTIVITY_NEW_TASK
545 | Intent.FLAG_ACTIVITY_CLEAR_TASK);
546 startActivity(intent);
547 } else if (postDelete != null) {
548 postDelete.run();
549 }
550 }
551 });
552 });
553 dialog.show();
554 }
555
556 protected abstract void onBackendConnected();
557
558 protected void registerListeners() {
559 if (this instanceof XmppConnectionService.OnConversationUpdate) {
560 this.xmppConnectionService.setOnConversationListChangedListener(
561 (XmppConnectionService.OnConversationUpdate) this);
562 }
563 if (this instanceof XmppConnectionService.OnAccountUpdate) {
564 this.xmppConnectionService.setOnAccountListChangedListener(
565 (XmppConnectionService.OnAccountUpdate) this);
566 }
567 if (this instanceof XmppConnectionService.OnCaptchaRequested) {
568 this.xmppConnectionService.setOnCaptchaRequestedListener(
569 (XmppConnectionService.OnCaptchaRequested) this);
570 }
571 if (this instanceof XmppConnectionService.OnRosterUpdate) {
572 this.xmppConnectionService.setOnRosterUpdateListener(
573 (XmppConnectionService.OnRosterUpdate) this);
574 }
575 if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
576 this.xmppConnectionService.setOnMucRosterUpdateListener(
577 (XmppConnectionService.OnMucRosterUpdate) this);
578 }
579 if (this instanceof OnUpdateBlocklist) {
580 this.xmppConnectionService.setOnUpdateBlocklistListener((OnUpdateBlocklist) this);
581 }
582 if (this instanceof XmppConnectionService.OnShowErrorToast) {
583 this.xmppConnectionService.setOnShowErrorToastListener(
584 (XmppConnectionService.OnShowErrorToast) this);
585 }
586 if (this instanceof OnKeyStatusUpdated) {
587 this.xmppConnectionService.setOnKeyStatusUpdatedListener((OnKeyStatusUpdated) this);
588 }
589 if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) {
590 this.xmppConnectionService.setOnRtpConnectionUpdateListener(
591 (XmppConnectionService.OnJingleRtpConnectionUpdate) this);
592 }
593 }
594
595 protected void unregisterListeners() {
596 if (this instanceof XmppConnectionService.OnConversationUpdate) {
597 this.xmppConnectionService.removeOnConversationListChangedListener(
598 (XmppConnectionService.OnConversationUpdate) this);
599 }
600 if (this instanceof XmppConnectionService.OnAccountUpdate) {
601 this.xmppConnectionService.removeOnAccountListChangedListener(
602 (XmppConnectionService.OnAccountUpdate) this);
603 }
604 if (this instanceof XmppConnectionService.OnCaptchaRequested) {
605 this.xmppConnectionService.removeOnCaptchaRequestedListener(
606 (XmppConnectionService.OnCaptchaRequested) this);
607 }
608 if (this instanceof XmppConnectionService.OnRosterUpdate) {
609 this.xmppConnectionService.removeOnRosterUpdateListener(
610 (XmppConnectionService.OnRosterUpdate) this);
611 }
612 if (this instanceof XmppConnectionService.OnMucRosterUpdate) {
613 this.xmppConnectionService.removeOnMucRosterUpdateListener(
614 (XmppConnectionService.OnMucRosterUpdate) this);
615 }
616 if (this instanceof OnUpdateBlocklist) {
617 this.xmppConnectionService.removeOnUpdateBlocklistListener((OnUpdateBlocklist) this);
618 }
619 if (this instanceof XmppConnectionService.OnShowErrorToast) {
620 this.xmppConnectionService.removeOnShowErrorToastListener(
621 (XmppConnectionService.OnShowErrorToast) this);
622 }
623 if (this instanceof OnKeyStatusUpdated) {
624 this.xmppConnectionService.removeOnNewKeysAvailableListener((OnKeyStatusUpdated) this);
625 }
626 if (this instanceof XmppConnectionService.OnJingleRtpConnectionUpdate) {
627 this.xmppConnectionService.removeRtpConnectionUpdateListener(
628 (XmppConnectionService.OnJingleRtpConnectionUpdate) this);
629 }
630 }
631
632 @Override
633 public boolean onOptionsItemSelected(final MenuItem item) {
634 switch (item.getItemId()) {
635 case R.id.action_settings:
636 startActivity(
637 new Intent(
638 this, eu.siacs.conversations.ui.activity.SettingsActivity.class));
639 break;
640 case R.id.action_privacy_policy:
641 openPrivacyPolicy();
642 break;
643 case R.id.action_accounts:
644 AccountUtils.launchManageAccounts(this);
645 break;
646 case R.id.action_account:
647 AccountUtils.launchManageAccount(this);
648 break;
649 case android.R.id.home:
650 finish();
651 break;
652 case R.id.action_show_qr_code:
653 showQrCode();
654 break;
655 }
656 return super.onOptionsItemSelected(item);
657 }
658
659 private void openPrivacyPolicy() {
660 if (BuildConfig.PRIVACY_POLICY == null) {
661 return;
662 }
663 final var viewPolicyIntent = new Intent(Intent.ACTION_VIEW);
664 viewPolicyIntent.setData(Uri.parse(BuildConfig.PRIVACY_POLICY));
665 try {
666 startActivity(viewPolicyIntent);
667 } catch (final ActivityNotFoundException e) {
668 Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT)
669 .show();
670 }
671 }
672
673 public void selectPresence(
674 final Conversation conversation, final PresenceSelector.OnPresenceSelected listener) {
675 final Contact contact = conversation.getContact();
676 if (contact.showInRoster() || contact.isSelf()) {
677 final Presences presences = contact.getPresences();
678 if (presences.size() == 0) {
679 if (contact.isSelf()) {
680 conversation.setNextCounterpart(null);
681 listener.onPresenceSelected();
682 } else if (!contact.getOption(Contact.Options.TO)
683 && !contact.getOption(Contact.Options.ASKING)
684 && contact.getAccount().getStatus() == Account.State.ONLINE) {
685 showAskForPresenceDialog(contact);
686 } else if (!contact.getOption(Contact.Options.TO)
687 || !contact.getOption(Contact.Options.FROM)) {
688 PresenceSelector.warnMutualPresenceSubscription(this, conversation, listener);
689 } else {
690 conversation.setNextCounterpart(null);
691 listener.onPresenceSelected();
692 }
693 } else if (presences.size() == 1) {
694 final String presence = presences.toResourceArray()[0];
695 conversation.setNextCounterpart(
696 PresenceSelector.getNextCounterpart(contact, presence));
697 listener.onPresenceSelected();
698 } else {
699 PresenceSelector.showPresenceSelectionDialog(this, conversation, listener);
700 }
701 } else {
702 showAddToRosterDialog(conversation.getContact());
703 }
704 }
705
706 @SuppressLint("UnsupportedChromeOsCameraSystemFeature")
707 @Override
708 protected void onCreate(Bundle savedInstanceState) {
709 super.onCreate(savedInstanceState);
710 metrics = getResources().getDisplayMetrics();
711 this.isCameraFeatureAvailable =
712 getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY);
713 this.mCustomColors = ThemeHelper.applyCustomColors(this);
714 }
715
716 protected boolean isCameraFeatureAvailable() {
717 return this.isCameraFeatureAvailable;
718 }
719
720 protected boolean isOptimizingBattery() {
721 final PowerManager pm = getSystemService(PowerManager.class);
722 return !pm.isIgnoringBatteryOptimizations(getPackageName());
723 }
724
725 protected boolean isAffectedByDataSaver() {
726 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
727 final ConnectivityManager cm =
728 (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
729 return cm != null
730 && cm.isActiveNetworkMetered()
731 && Compatibility.getRestrictBackgroundStatus(cm)
732 == ConnectivityManager.RESTRICT_BACKGROUND_STATUS_ENABLED;
733 } else {
734 return false;
735 }
736 }
737
738 private boolean usingEnterKey() {
739 return getBooleanPreference("display_enter_key", R.bool.display_enter_key);
740 }
741
742 private boolean useTor() {
743 return getBooleanPreference("use_tor", R.bool.use_tor);
744 }
745
746 protected SharedPreferences getPreferences() {
747 return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
748 }
749
750 protected boolean getBooleanPreference(String name, @BoolRes int res) {
751 return getPreferences().getBoolean(name, getResources().getBoolean(res));
752 }
753
754 public void startCommand(final Account account, final Jid jid, final String node) {
755 Intent intent = new Intent(this, ConversationsActivity.class);
756 intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
757 intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, xmppConnectionService.findOrCreateConversation(account, jid, false, false).getUuid());
758 intent.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, "command");
759 intent.putExtra(ConversationsActivity.EXTRA_NODE, node);
760 intent.putExtra(ConversationsActivity.EXTRA_JID, (CharSequence) jid);
761 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
762 startActivity(intent);
763 }
764
765 public boolean colorCodeAccounts() {
766 return xmppConnectionService.getAccounts().size() > 1;
767 }
768
769 public void populateWithOrderedConversations(List<Conversation> list) {
770 xmppConnectionService.populateWithOrderedConversations(list);
771 }
772
773 public void launchStartConversation() {
774 StartConversationActivity.launch(this);
775 }
776
777 public void switchToConversation(Conversation conversation) {
778 switchToConversation(conversation, null);
779 }
780
781 public void switchToConversationAndQuote(Conversation conversation, String text) {
782 switchToConversation(conversation, text, true, null, false, false);
783 }
784
785 public void switchToConversation(Conversation conversation, String text) {
786 switchToConversation(conversation, text, false, null, false, false);
787 }
788
789 public void switchToConversationDoNotAppend(Conversation conversation, String text) {
790 switchToConversation(conversation, text, false, null, false, true);
791 }
792
793 public void highlightInMuc(Conversation conversation, String nick) {
794 switchToConversation(conversation, null, false, nick, false, false);
795 }
796
797 public void privateMsgInMuc(Conversation conversation, String nick) {
798 switchToConversation(conversation, null, false, nick, true, false);
799 }
800
801 public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend) {
802 switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, null);
803 }
804
805 public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend, String postInit) {
806 switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, postInit, null);
807 }
808
809 public void switchToConversation(
810 Conversation conversation,
811 String text,
812 boolean asQuote,
813 String nick,
814 boolean pm,
815 boolean doNotAppend,
816 String postInit,
817 String thread) {
818 if (conversation == null) return;
819 Intent intent = new Intent(this, ConversationsActivity.class);
820 intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
821 intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
822 intent.putExtra(ConversationsActivity.EXTRA_THREAD, thread);
823 if (text != null) {
824 intent.putExtra(Intent.EXTRA_TEXT, text);
825 if (asQuote) {
826 intent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, true);
827 }
828 }
829 if (nick != null) {
830 intent.putExtra(ConversationsActivity.EXTRA_NICK, nick);
831 intent.putExtra(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, pm);
832 }
833 if (doNotAppend) {
834 intent.putExtra(ConversationsActivity.EXTRA_DO_NOT_APPEND, true);
835 }
836 intent.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, postInit);
837 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
838 startActivity(intent);
839 finish();
840 }
841
842 public void switchToContactDetails(Contact contact) {
843 switchToContactDetails(contact, null);
844 }
845
846 public void switchToContactDetails(Contact contact, String messageFingerprint) {
847 Intent intent = new Intent(this, ContactDetailsActivity.class);
848 intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
849 intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().asBareJid().toString());
850 intent.putExtra("contact", contact.getJid().toString());
851 intent.putExtra("fingerprint", messageFingerprint);
852 startActivity(intent);
853 }
854
855 public void switchToAccount(Account account, String fingerprint) {
856 switchToAccount(account, false, fingerprint);
857 }
858
859 public void switchToAccount(Account account) {
860 switchToAccount(account, false, null);
861 }
862
863 public void switchToAccount(Account account, boolean init, String fingerprint) {
864 Intent intent = new Intent(this, EditAccountActivity.class);
865 intent.putExtra("jid", account.getJid().asBareJid().toString());
866 intent.putExtra("init", init);
867 if (init) {
868 intent.setFlags(
869 Intent.FLAG_ACTIVITY_NEW_TASK
870 | Intent.FLAG_ACTIVITY_CLEAR_TASK
871 | Intent.FLAG_ACTIVITY_NO_ANIMATION);
872 }
873 if (fingerprint != null) {
874 intent.putExtra("fingerprint", fingerprint);
875 }
876 startActivity(intent);
877 if (init) {
878 overridePendingTransition(0, 0);
879 }
880 }
881
882 protected void delegateUriPermissionsToService(Uri uri) {
883 Intent intent = new Intent(this, XmppConnectionService.class);
884 intent.setAction(Intent.ACTION_SEND);
885 intent.setData(uri);
886 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
887 try {
888 startService(intent);
889 } catch (Exception e) {
890 Log.e(Config.LOGTAG, "unable to delegate uri permission", e);
891 }
892 }
893
894 protected void inviteToConversation(Conversation conversation) {
895 startActivityForResult(
896 ChooseContactActivity.create(this, conversation), REQUEST_INVITE_TO_CONVERSATION);
897 }
898
899 protected void announcePgp(
900 final Account account,
901 final Conversation conversation,
902 Intent intent,
903 final Runnable onSuccess) {
904 if (account.getPgpId() == 0) {
905 choosePgpSignId(account);
906 } else {
907 final String status = Strings.nullToEmpty(account.getPresenceStatusMessage());
908 xmppConnectionService
909 .getPgpEngine()
910 .generateSignature(
911 intent,
912 account,
913 status,
914 new UiCallback<String>() {
915
916 @Override
917 public void userInputRequired(
918 final PendingIntent pi, final String signature) {
919 try {
920 startIntentSenderForResult(
921 pi.getIntentSender(),
922 REQUEST_ANNOUNCE_PGP,
923 null,
924 0,
925 0,
926 0,
927 Compatibility.pgpStartIntentSenderOptions());
928 } catch (final SendIntentException ignored) {
929 }
930 }
931
932 @Override
933 public void success(String signature) {
934 account.setPgpSignature(signature);
935 xmppConnectionService.databaseBackend.updateAccount(account);
936 xmppConnectionService.sendPresence(account);
937 if (conversation != null) {
938 conversation.setNextEncryption(Message.ENCRYPTION_PGP);
939 xmppConnectionService.updateConversation(conversation);
940 refreshUi();
941 }
942 if (onSuccess != null) {
943 runOnUiThread(onSuccess);
944 }
945 }
946
947 @Override
948 public void error(int error, String signature) {
949 if (error == 0) {
950 account.setPgpSignId(0);
951 account.unsetPgpSignature();
952 xmppConnectionService.databaseBackend.updateAccount(
953 account);
954 choosePgpSignId(account);
955 } else {
956 displayErrorDialog(error);
957 }
958 }
959 });
960 }
961 }
962
963 protected void choosePgpSignId(final Account account) {
964 xmppConnectionService
965 .getPgpEngine()
966 .chooseKey(
967 account,
968 new UiCallback<>() {
969 @Override
970 public void success(final Account a) {}
971
972 @Override
973 public void error(int errorCode, Account object) {}
974
975 @Override
976 public void userInputRequired(PendingIntent pi, Account object) {
977 try {
978 startIntentSenderForResult(
979 pi.getIntentSender(),
980 REQUEST_CHOOSE_PGP_ID,
981 null,
982 0,
983 0,
984 0,
985 Compatibility.pgpStartIntentSenderOptions());
986 } catch (final SendIntentException ignored) {
987 }
988 }
989 });
990 }
991
992 protected void displayErrorDialog(final int errorCode) {
993 runOnUiThread(
994 () -> {
995 final MaterialAlertDialogBuilder builder =
996 new MaterialAlertDialogBuilder(XmppActivity.this);
997 builder.setTitle(getString(R.string.error));
998 builder.setMessage(errorCode);
999 builder.setNeutralButton(R.string.accept, null);
1000 builder.create().show();
1001 });
1002 }
1003
1004 protected void showAddToRosterDialog(final Contact contact) {
1005 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1006 builder.setTitle(contact.getJid().toString());
1007 builder.setMessage(getString(R.string.not_in_roster));
1008 builder.setNegativeButton(getString(R.string.cancel), null);
1009 builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> {
1010 contact.copySystemTagsToGroups();
1011 xmppConnectionService.createContact(contact, true);
1012 });
1013 builder.create().show();
1014 }
1015
1016 private void showAskForPresenceDialog(final Contact contact) {
1017 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1018 builder.setTitle(contact.getJid().toString());
1019 builder.setMessage(R.string.request_presence_updates);
1020 builder.setNegativeButton(R.string.cancel, null);
1021 builder.setPositiveButton(
1022 R.string.request_now,
1023 (dialog, which) -> {
1024 if (xmppConnectionServiceBound) {
1025 xmppConnectionService.sendPresencePacket(
1026 contact.getAccount(),
1027 xmppConnectionService
1028 .getPresenceGenerator()
1029 .requestPresenceUpdatesFrom(contact));
1030 }
1031 });
1032 builder.create().show();
1033 }
1034
1035 protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback) {
1036 quickEdit(previousValue, callback, hint, false, false);
1037 }
1038
1039 protected void quickEdit(
1040 String previousValue,
1041 @StringRes int hint,
1042 OnValueEdited callback,
1043 boolean permitEmpty) {
1044 quickEdit(previousValue, callback, hint, false, permitEmpty);
1045 }
1046
1047 protected void quickPasswordEdit(String previousValue, OnValueEdited callback) {
1048 quickEdit(previousValue, callback, R.string.password, true, false);
1049 }
1050
1051 protected void quickEdit(final String previousValue, final OnValueEdited callback, final @StringRes int hint, boolean password, boolean permitEmpty) {
1052 quickEdit(previousValue, callback, hint, password, permitEmpty, false);
1053 }
1054
1055 protected void quickEdit(final String previousValue, final OnValueEdited callback, final @StringRes int hint, boolean password, boolean permitEmpty, boolean alwaysCallback) {
1056 quickEdit(previousValue, callback, hint, password, permitEmpty, alwaysCallback, false);
1057 }
1058
1059 @SuppressLint("InflateParams")
1060 protected void quickEdit(final String previousValue,
1061 final OnValueEdited callback,
1062 final @StringRes int hint,
1063 boolean password,
1064 boolean permitEmpty,
1065 boolean alwaysCallback,
1066 boolean startSelected) {
1067 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1068 final DialogQuickeditBinding binding =
1069 DataBindingUtil.inflate(
1070 getLayoutInflater(), R.layout.dialog_quickedit, null, false);
1071 if (password) {
1072 binding.inputEditText.setInputType(
1073 InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
1074 }
1075 builder.setPositiveButton(R.string.accept, null);
1076 if (hint != 0) {
1077 binding.inputLayout.setHint(getString(hint));
1078 }
1079 binding.inputEditText.requestFocus();
1080 if (previousValue != null) {
1081 binding.inputEditText.getText().append(previousValue);
1082 }
1083 builder.setView(binding.getRoot());
1084 builder.setNegativeButton(R.string.cancel, null);
1085 final AlertDialog dialog = builder.create();
1086 dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(binding.inputEditText));
1087 dialog.show();
1088 if (startSelected) {
1089 binding.inputEditText.selectAll();
1090 }
1091 View.OnClickListener clickListener =
1092 v -> {
1093 String value = binding.inputEditText.getText().toString();
1094 if ((alwaysCallback || !value.equals(previousValue)) && (!value.trim().isEmpty() || permitEmpty)) {
1095 String error = callback.onValueEdited(value);
1096 if (error != null) {
1097 binding.inputLayout.setError(error);
1098 return;
1099 }
1100 }
1101 SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
1102 dialog.dismiss();
1103 };
1104 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
1105 dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
1106 .setOnClickListener(
1107 (v -> {
1108 SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
1109 dialog.dismiss();
1110 }));
1111 dialog.setCanceledOnTouchOutside(false);
1112 dialog.setOnDismissListener(
1113 dialog1 -> {
1114 SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
1115 });
1116 }
1117
1118 protected boolean hasStoragePermission(int requestCode) {
1119 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
1120 if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
1121 != PackageManager.PERMISSION_GRANTED) {
1122 requestPermissions(
1123 new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
1124 return false;
1125 } else {
1126 return true;
1127 }
1128 } else {
1129 return true;
1130 }
1131 }
1132
1133 public synchronized void startActivityWithCallback(Intent intent, ValueCallback<Uri[]> cb) {
1134 Pair<Integer, ValueCallback<Uri[]>> peek = activityCallbacks.peek();
1135 int index = peek == null ? 1 : peek.first + 1;
1136 activityCallbacks.add(new Pair<>(index, cb));
1137 startActivityForResult(intent, index);
1138 }
1139
1140 protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
1141 super.onActivityResult(requestCode, resultCode, data);
1142 if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
1143 mPendingConferenceInvite = ConferenceInvite.parse(data);
1144 if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
1145 if (mPendingConferenceInvite.execute(this)) {
1146 mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
1147 mToast.show();
1148 }
1149 mPendingConferenceInvite = null;
1150 }
1151 } else if (resultCode == RESULT_OK) {
1152 for (Pair<Integer, ValueCallback<Uri[]>> cb : new ArrayList<>(activityCallbacks)) {
1153 if (cb.first == requestCode) {
1154 activityCallbacks.remove(cb);
1155 ArrayList<Uri> dataUris = new ArrayList<>();
1156 if (data.getDataString() != null) {
1157 dataUris.add(Uri.parse(data.getDataString()));
1158 } else if (data.getClipData() != null) {
1159 for (int i = 0; i < data.getClipData().getItemCount(); i++) {
1160 dataUris.add(data.getClipData().getItemAt(i).getUri());
1161 }
1162 }
1163 cb.second.onReceiveValue(dataUris.toArray(new Uri[0]));
1164 }
1165 }
1166 }
1167 }
1168
1169 public boolean copyTextToClipboard(String text, int labelResId) {
1170 ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
1171 String label = getResources().getString(labelResId);
1172 if (mClipBoardManager != null) {
1173 ClipData mClipData = ClipData.newPlainText(label, text);
1174 mClipBoardManager.setPrimaryClip(mClipData);
1175 return true;
1176 }
1177 return false;
1178 }
1179
1180 protected boolean manuallyChangePresence() {
1181 return getBooleanPreference(
1182 AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
1183 }
1184
1185 protected String getShareableUri() {
1186 return getShareableUri(false);
1187 }
1188
1189 protected String getShareableUri(boolean http) {
1190 return null;
1191 }
1192
1193 protected void shareLink(boolean http) {
1194 String uri = getShareableUri(http);
1195 if (uri == null || uri.isEmpty()) {
1196 return;
1197 }
1198 Intent intent = new Intent(Intent.ACTION_SEND);
1199 intent.setType("text/plain");
1200 intent.putExtra(Intent.EXTRA_TEXT, getShareableUri(http));
1201 try {
1202 startActivity(Intent.createChooser(intent, getText(R.string.share_uri_with)));
1203 } catch (ActivityNotFoundException e) {
1204 Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
1205 }
1206 }
1207
1208 protected void launchOpenKeyChain(long keyId) {
1209 PgpEngine pgp = XmppActivity.this.xmppConnectionService.getPgpEngine();
1210 try {
1211 startIntentSenderForResult(
1212 pgp.getIntentForKey(keyId).getIntentSender(),
1213 0,
1214 null,
1215 0,
1216 0,
1217 0,
1218 Compatibility.pgpStartIntentSenderOptions());
1219 } catch (final Throwable e) {
1220 Log.d(Config.LOGTAG, "could not launch OpenKeyChain", e);
1221 Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show();
1222 }
1223 }
1224
1225 @Override
1226 protected void onResume() {
1227 super.onResume();
1228 SettingsUtils.applyScreenshotSetting(this);
1229 }
1230
1231 @Override
1232 public void onPause() {
1233 super.onPause();
1234 }
1235
1236 @Override
1237 public boolean onMenuOpened(int id, Menu menu) {
1238 if (id == AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR && menu != null) {
1239 MenuDoubleTabUtil.recordMenuOpen();
1240 }
1241 return super.onMenuOpened(id, menu);
1242 }
1243
1244 protected void showQrCode() {
1245 final var uri = getShareableUri();
1246 if (uri != null) {
1247 showQrCode(uri);
1248 return;
1249 }
1250
1251 final var accounts = xmppConnectionService.getAccounts();
1252 if (accounts.size() < 1) return;
1253
1254 if (accounts.size() == 1) {
1255 showQrCode(accounts.get(0).getShareableUri());
1256 return;
1257 }
1258
1259 final AtomicReference<Account> selectedAccount = new AtomicReference<>(accounts.get(0));
1260 final MaterialAlertDialogBuilder alertDialogBuilder = new MaterialAlertDialogBuilder(this);
1261 alertDialogBuilder.setTitle(R.string.choose_account);
1262 final String[] asStrings = Collections2.transform(accounts, a -> a.getJid().asBareJid().toString()).toArray(new String[0]);
1263 alertDialogBuilder.setSingleChoiceItems(asStrings, 0, (dialog, which) -> selectedAccount.set(accounts.get(which)));
1264 alertDialogBuilder.setNegativeButton(R.string.cancel, null);
1265 alertDialogBuilder.setPositiveButton(R.string.ok, (dialog, which) -> showQrCode(selectedAccount.get().getShareableUri()));
1266 alertDialogBuilder.create().show();
1267 }
1268
1269 protected void showQrCode(final String uri) {
1270 if (uri == null || uri.isEmpty()) {
1271 return;
1272 }
1273 final Point size = new Point();
1274 getWindowManager().getDefaultDisplay().getSize(size);
1275 final int width = Math.min(size.x, size.y);
1276 final int black;
1277 final int white;
1278 if (Activities.isNightMode(this)) {
1279 black =
1280 MaterialColors.getColor(
1281 this,
1282 com.google.android.material.R.attr.colorSurfaceContainerHighest,
1283 "No surface color configured");
1284 white =
1285 MaterialColors.getColor(
1286 this,
1287 com.google.android.material.R.attr.colorSurfaceInverse,
1288 "No inverse surface color configured");
1289 } else {
1290 black =
1291 MaterialColors.getColor(
1292 this,
1293 com.google.android.material.R.attr.colorSurfaceInverse,
1294 "No inverse surface color configured");
1295 white =
1296 MaterialColors.getColor(
1297 this,
1298 com.google.android.material.R.attr.colorSurfaceContainerHighest,
1299 "No surface color configured");
1300 }
1301 final var bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width, black, white);
1302 final ImageView view = new ImageView(this);
1303 view.setBackgroundColor(white);
1304 view.setImageBitmap(bitmap);
1305 MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1306 builder.setView(view);
1307 builder.create().show();
1308 }
1309
1310 protected Account extractAccount(Intent intent) {
1311 final String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
1312 try {
1313 return jid != null ? xmppConnectionService.findAccountByJid(Jid.of(jid)) : null;
1314 } catch (IllegalArgumentException e) {
1315 return null;
1316 }
1317 }
1318
1319 public AvatarService avatarService() {
1320 return xmppConnectionService.getAvatarService();
1321 }
1322
1323 public void loadBitmap(Message message, ImageView imageView) {
1324 Drawable bm;
1325 try {
1326 bm =
1327 xmppConnectionService
1328 .getFileBackend()
1329 .getThumbnail(message, getResources(), (int) (metrics.density * 288), true);
1330 } catch (IOException e) {
1331 bm = null;
1332 }
1333 if (bm != null) {
1334 cancelPotentialWork(message, imageView);
1335 imageView.setImageDrawable(bm);
1336 imageView.setBackgroundColor(0x00000000);
1337 if (Build.VERSION.SDK_INT >= 28 && bm instanceof AnimatedImageDrawable) {
1338 ((AnimatedImageDrawable) bm).start();
1339 }
1340 } else {
1341 if (cancelPotentialWork(message, imageView)) {
1342 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1343 final BitmapDrawable fallbackThumb = xmppConnectionService.getFileBackend().getFallbackThumbnail(message, (int) (metrics.density * 288), true);
1344 imageView.setBackgroundColor(fallbackThumb == null ? 0xff333333 : 0x00000000);
1345 final AsyncDrawable asyncDrawable = new AsyncDrawable(
1346 getResources(), fallbackThumb != null ? fallbackThumb.getBitmap() : null, task);
1347 imageView.setImageDrawable(asyncDrawable);
1348 try {
1349 task.execute(message);
1350 } catch (final RejectedExecutionException ignored) {
1351 ignored.printStackTrace();
1352 }
1353 }
1354 }
1355 }
1356
1357 protected interface OnValueEdited {
1358 String onValueEdited(String value);
1359 }
1360
1361 public static class ConferenceInvite {
1362 private String uuid;
1363 private final List<Jid> jids = new ArrayList<>();
1364
1365 public static ConferenceInvite parse(Intent data) {
1366 ConferenceInvite invite = new ConferenceInvite();
1367 invite.uuid = data.getStringExtra(ChooseContactActivity.EXTRA_CONVERSATION);
1368 if (invite.uuid == null) {
1369 return null;
1370 }
1371 invite.jids.addAll(ChooseContactActivity.extractJabberIds(data));
1372 return invite;
1373 }
1374
1375 public boolean execute(XmppActivity activity) {
1376 XmppConnectionService service = activity.xmppConnectionService;
1377 Conversation conversation = service.findConversationByUuid(this.uuid);
1378 if (conversation == null) {
1379 return false;
1380 }
1381 if (conversation.getMode() == Conversation.MODE_MULTI) {
1382 for (Jid jid : jids) {
1383 service.invite(conversation, jid);
1384 }
1385 return false;
1386 } else {
1387 jids.add(conversation.getJid().asBareJid());
1388 return service.createAdhocConference(
1389 conversation.getAccount(), null, jids, activity.adhocCallback);
1390 }
1391 }
1392 }
1393
1394 static class BitmapWorkerTask extends AsyncTask<Message, Void, Drawable> {
1395 private final WeakReference<ImageView> imageViewReference;
1396 private Message message = null;
1397
1398 private BitmapWorkerTask(ImageView imageView) {
1399 this.imageViewReference = new WeakReference<>(imageView);
1400 }
1401
1402 @Override
1403 protected Drawable doInBackground(Message... params) {
1404 if (isCancelled()) {
1405 return null;
1406 }
1407 final XmppActivity activity = find(imageViewReference);
1408 Drawable d = null;
1409 message = params[0];
1410 try {
1411 if (activity != null && activity.xmppConnectionService != null) {
1412 d = activity.xmppConnectionService.getFileBackend().getThumbnail(message, imageViewReference.get().getContext().getResources(), (int) (activity.metrics.density * 288), false);
1413 }
1414 } catch (IOException e) { e.printStackTrace(); }
1415 final ImageView imageView = imageViewReference.get();
1416 if (d == null && activity != null && activity.xmppConnectionService != null && imageView != null && imageView.getDrawable() instanceof AsyncDrawable && ((AsyncDrawable) imageView.getDrawable()).getBitmap() == null) {
1417 d = activity.xmppConnectionService.getFileBackend().getFallbackThumbnail(message, (int) (activity.metrics.density * 288), false);
1418 }
1419 return d;
1420 }
1421
1422 @Override
1423 protected void onPostExecute(final Drawable drawable) {
1424 if (!isCancelled()) {
1425 final ImageView imageView = imageViewReference.get();
1426 if (imageView != null) {
1427 Drawable old = imageView.getDrawable();
1428 if (old instanceof AsyncDrawable) {
1429 ((AsyncDrawable) old).clearTask();
1430 }
1431 if (drawable != null) {
1432 imageView.setImageDrawable(drawable);
1433 }
1434 imageView.setBackgroundColor(drawable == null ? 0xff333333 : 0x00000000);
1435 if (Build.VERSION.SDK_INT >= 28 && drawable instanceof AnimatedImageDrawable) {
1436 ((AnimatedImageDrawable) drawable).start();
1437 }
1438 }
1439 }
1440 }
1441 }
1442
1443 private static class AsyncDrawable extends BitmapDrawable {
1444 private WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1445
1446 private AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
1447 super(res, bitmap);
1448 bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
1449 }
1450
1451 private synchronized BitmapWorkerTask getBitmapWorkerTask() {
1452 if (bitmapWorkerTaskReference == null) return null;
1453
1454 return bitmapWorkerTaskReference.get();
1455 }
1456
1457 public synchronized void clearTask() {
1458 bitmapWorkerTaskReference = null;
1459 }
1460 }
1461
1462 public static XmppActivity find(@NonNull WeakReference<ImageView> viewWeakReference) {
1463 final View view = viewWeakReference.get();
1464 return view == null ? null : find(view);
1465 }
1466
1467 public static XmppActivity find(@NonNull final View view) {
1468 Context context = view.getContext();
1469 while (context instanceof ContextWrapper) {
1470 if (context instanceof XmppActivity) {
1471 return (XmppActivity) context;
1472 }
1473 context = ((ContextWrapper) context).getBaseContext();
1474 }
1475 return null;
1476 }
1477
1478 public boolean isDark() {
1479 int nightModeFlags = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
1480 return nightModeFlags == Configuration.UI_MODE_NIGHT_YES;
1481 }
1482}