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