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