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