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 switchToConversationOnMessage(Conversation conversation, String messageUuid) {
797 switchToConversation(conversation, null, false, null, false, false, null, null, messageUuid);
798 }
799
800 public void switchToConversationAndQuote(Conversation conversation, String text) {
801 switchToConversation(conversation, text, true, null, false, false);
802 }
803
804 public void switchToConversation(Conversation conversation, String text) {
805 switchToConversation(conversation, text, false, null, false, false);
806 }
807
808 public void switchToConversationDoNotAppend(Conversation conversation, String text) {
809 switchToConversation(conversation, text, false, null, false, true);
810 }
811
812 public void highlightInMuc(Conversation conversation, String nick) {
813 switchToConversation(conversation, null, false, nick, false, false);
814 }
815
816 public void privateMsgInMuc(Conversation conversation, String nick) {
817 switchToConversation(conversation, null, false, nick, true, false);
818 }
819
820 public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend) {
821 switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, null);
822 }
823
824 public void switchToConversation(Conversation conversation, String text, boolean asQuote, String nick, boolean pm, boolean doNotAppend, String postInit) {
825 switchToConversation(conversation, text, asQuote, nick, pm, doNotAppend, postInit, null, null);
826 }
827
828 public void switchToConversation(
829 Conversation conversation,
830 String text,
831 boolean asQuote,
832 String nick,
833 boolean pm,
834 boolean doNotAppend,
835 String postInit,
836 String thread,
837 String messageUuid) {
838 if (conversation == null) return;
839 Intent intent = new Intent(this, ConversationsActivity.class);
840 intent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
841 intent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
842 intent.putExtra(ConversationsActivity.EXTRA_THREAD, thread);
843 if (text != null) {
844 intent.putExtra(Intent.EXTRA_TEXT, text);
845 if (asQuote) {
846 intent.putExtra(ConversationsActivity.EXTRA_AS_QUOTE, true);
847 }
848 }
849 if (nick != null) {
850 intent.putExtra(ConversationsActivity.EXTRA_NICK, nick);
851 intent.putExtra(ConversationsActivity.EXTRA_IS_PRIVATE_MESSAGE, pm);
852 }
853 if (doNotAppend) {
854 intent.putExtra(ConversationsActivity.EXTRA_DO_NOT_APPEND, true);
855 }
856 if (messageUuid != null) {
857 intent.putExtra(ConversationsActivity.EXTRA_MESSAGE_UUID, messageUuid);
858 }
859 intent.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, postInit);
860 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
861 startActivity(intent);
862 finish();
863 }
864
865 public void switchToContactDetails(Contact contact) {
866 switchToContactDetails(contact, null);
867 }
868
869 public void switchToContactDetails(Contact contact, String messageFingerprint) {
870 Intent intent = new Intent(this, ContactDetailsActivity.class);
871 intent.setAction(ContactDetailsActivity.ACTION_VIEW_CONTACT);
872 intent.putExtra(EXTRA_ACCOUNT, contact.getAccount().getJid().asBareJid().toString());
873 intent.putExtra("contact", contact.getJid().toString());
874 intent.putExtra("fingerprint", messageFingerprint);
875 startActivity(intent);
876 }
877
878 public void switchToAccount(Account account, String fingerprint) {
879 switchToAccount(account, false, fingerprint);
880 }
881
882 public void switchToAccount(Account account) {
883 switchToAccount(account, false, null);
884 }
885
886 public void switchToAccount(Account account, boolean init, String fingerprint) {
887 Intent intent = new Intent(this, EditAccountActivity.class);
888 intent.putExtra("jid", account.getJid().asBareJid().toString());
889 intent.putExtra("init", init);
890 if (init) {
891 intent.setFlags(
892 Intent.FLAG_ACTIVITY_NEW_TASK
893 | Intent.FLAG_ACTIVITY_CLEAR_TASK
894 | Intent.FLAG_ACTIVITY_NO_ANIMATION);
895 }
896 if (fingerprint != null) {
897 intent.putExtra("fingerprint", fingerprint);
898 }
899 startActivity(intent);
900 if (init) {
901 overridePendingTransition(0, 0);
902 }
903 }
904
905 protected void delegateUriPermissionsToService(Uri uri) {
906 Intent intent = new Intent(this, XmppConnectionService.class);
907 intent.setAction(Intent.ACTION_SEND);
908 intent.setData(uri);
909 intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
910 try {
911 startService(intent);
912 } catch (Exception e) {
913 Log.e(Config.LOGTAG, "unable to delegate uri permission", e);
914 }
915 }
916
917 protected void inviteToConversation(Conversation conversation) {
918 startActivityForResult(
919 ChooseContactActivity.create(this, conversation), REQUEST_INVITE_TO_CONVERSATION);
920 }
921
922 protected void announcePgp(
923 final Account account,
924 final Conversation conversation,
925 Intent intent,
926 final Runnable onSuccess) {
927 if (account.getPgpId() == 0) {
928 choosePgpSignId(account);
929 } else {
930 final String status = Strings.nullToEmpty(account.getPresenceStatusMessage());
931 xmppConnectionService
932 .getPgpEngine()
933 .generateSignature(
934 intent,
935 account,
936 status,
937 new UiCallback<String>() {
938
939 @Override
940 public void userInputRequired(
941 final PendingIntent pi, final String signature) {
942 try {
943 startIntentSenderForResult(
944 pi.getIntentSender(),
945 REQUEST_ANNOUNCE_PGP,
946 null,
947 0,
948 0,
949 0,
950 Compatibility.pgpStartIntentSenderOptions());
951 } catch (final SendIntentException ignored) {
952 }
953 }
954
955 @Override
956 public void success(String signature) {
957 account.setPgpSignature(signature);
958 xmppConnectionService.databaseBackend.updateAccount(account);
959 account.getXmppConnection()
960 .getManager(PresenceManager.class)
961 .available();
962 if (conversation != null) {
963 conversation.setNextEncryption(Message.ENCRYPTION_PGP);
964 xmppConnectionService.updateConversation(conversation);
965 refreshUi();
966 }
967 if (onSuccess != null) {
968 runOnUiThread(onSuccess);
969 }
970 }
971
972 @Override
973 public void error(int error, String signature) {
974 if (error == 0) {
975 account.setPgpSignId(0);
976 account.unsetPgpSignature();
977 xmppConnectionService.databaseBackend.updateAccount(
978 account);
979 choosePgpSignId(account);
980 } else {
981 displayErrorDialog(error);
982 }
983 }
984 });
985 }
986 }
987
988 protected void choosePgpSignId(final Account account) {
989 xmppConnectionService
990 .getPgpEngine()
991 .chooseKey(
992 account,
993 new UiCallback<>() {
994 @Override
995 public void success(final Account a) {}
996
997 @Override
998 public void error(int errorCode, Account object) {}
999
1000 @Override
1001 public void userInputRequired(PendingIntent pi, Account object) {
1002 try {
1003 startIntentSenderForResult(
1004 pi.getIntentSender(),
1005 REQUEST_CHOOSE_PGP_ID,
1006 null,
1007 0,
1008 0,
1009 0,
1010 Compatibility.pgpStartIntentSenderOptions());
1011 } catch (final SendIntentException ignored) {
1012 }
1013 }
1014 });
1015 }
1016
1017 protected void displayErrorDialog(final int errorCode) {
1018 runOnUiThread(
1019 () -> {
1020 final MaterialAlertDialogBuilder builder =
1021 new MaterialAlertDialogBuilder(XmppActivity.this);
1022 builder.setTitle(getString(R.string.error));
1023 builder.setMessage(errorCode);
1024 builder.setNeutralButton(R.string.accept, null);
1025 builder.create().show();
1026 });
1027 }
1028
1029 protected void showAddToRosterDialog(final Contact contact) {
1030 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1031 builder.setTitle(contact.getJid().toString());
1032 builder.setMessage(getString(R.string.not_in_roster));
1033 builder.setNegativeButton(getString(R.string.cancel), null);
1034 builder.setPositiveButton(getString(R.string.add_contact), (dialog, which) -> {
1035 contact.copySystemTagsToGroups();
1036 xmppConnectionService.createContact(contact);
1037 });
1038 builder.create().show();
1039 }
1040
1041 private void showAskForPresenceDialog(final Contact contact) {
1042 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1043 builder.setTitle(contact.getJid().toString());
1044 builder.setMessage(R.string.request_presence_updates);
1045 builder.setNegativeButton(R.string.cancel, null);
1046 builder.setPositiveButton(
1047 R.string.request_now,
1048 (dialog, which) -> {
1049 final var connection = contact.getAccount().getXmppConnection();
1050 connection
1051 .getManager(PresenceManager.class)
1052 .subscribe(contact.getJid().asBareJid());
1053 });
1054 builder.create().show();
1055 }
1056
1057 protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback) {
1058 quickEdit(previousValue, callback, hint, false, false);
1059 }
1060
1061 protected void quickEdit(
1062 String previousValue,
1063 @StringRes int hint,
1064 OnValueEdited callback,
1065 boolean permitEmpty) {
1066 quickEdit(previousValue, callback, hint, false, permitEmpty);
1067 }
1068
1069 protected void quickPasswordEdit(String previousValue, OnValueEdited callback) {
1070 quickEdit(previousValue, callback, R.string.password, true, false);
1071 }
1072
1073 protected void quickEdit(final String previousValue, final OnValueEdited callback, final @StringRes int hint, boolean password, boolean permitEmpty) {
1074 quickEdit(previousValue, callback, hint, password, permitEmpty, false);
1075 }
1076
1077 protected void quickEdit(final String previousValue, final OnValueEdited callback, final @StringRes int hint, boolean password, boolean permitEmpty, boolean alwaysCallback) {
1078 quickEdit(previousValue, callback, hint, password, permitEmpty, alwaysCallback, false);
1079 }
1080
1081 @SuppressLint("InflateParams")
1082 protected void quickEdit(final String previousValue,
1083 final OnValueEdited callback,
1084 final @StringRes int hint,
1085 boolean password,
1086 boolean permitEmpty,
1087 boolean alwaysCallback,
1088 boolean startSelected) {
1089 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1090 final DialogQuickeditBinding binding =
1091 DataBindingUtil.inflate(
1092 getLayoutInflater(), R.layout.dialog_quickedit, null, false);
1093 if (password) {
1094 binding.inputEditText.setInputType(
1095 InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
1096 }
1097 builder.setPositiveButton(R.string.accept, null);
1098 if (hint != 0) {
1099 binding.inputLayout.setHint(getString(hint));
1100 }
1101 binding.inputEditText.requestFocus();
1102 if (previousValue != null) {
1103 binding.inputEditText.getText().append(previousValue);
1104 }
1105 builder.setView(binding.getRoot());
1106 builder.setNegativeButton(R.string.cancel, null);
1107 final AlertDialog dialog = builder.create();
1108 dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(binding.inputEditText));
1109 dialog.show();
1110 if (startSelected) {
1111 binding.inputEditText.selectAll();
1112 }
1113 View.OnClickListener clickListener =
1114 v -> {
1115 String value = binding.inputEditText.getText().toString();
1116 if ((alwaysCallback || !value.equals(previousValue)) && (!value.trim().isEmpty() || permitEmpty)) {
1117 String error = callback.onValueEdited(value);
1118 if (error != null) {
1119 binding.inputLayout.setError(error);
1120 return;
1121 }
1122 }
1123 SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
1124 dialog.dismiss();
1125 };
1126 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
1127 dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
1128 .setOnClickListener(
1129 (v -> {
1130 SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
1131 dialog.dismiss();
1132 }));
1133 dialog.setCanceledOnTouchOutside(false);
1134 dialog.setOnDismissListener(
1135 dialog1 -> {
1136 SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
1137 });
1138 }
1139
1140 protected boolean hasStoragePermission(int requestCode) {
1141 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
1142 if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
1143 != PackageManager.PERMISSION_GRANTED) {
1144 requestPermissions(
1145 new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
1146 return false;
1147 } else {
1148 return true;
1149 }
1150 } else {
1151 return true;
1152 }
1153 }
1154
1155 public synchronized void startActivityWithCallback(Intent intent, ValueCallback<Uri[]> cb) {
1156 Pair<Integer, ValueCallback<Uri[]>> peek = activityCallbacks.peek();
1157 int index = peek == null ? 1 : peek.first + 1;
1158 activityCallbacks.add(new Pair<>(index, cb));
1159 startActivityForResult(intent, index);
1160 }
1161
1162 protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
1163 super.onActivityResult(requestCode, resultCode, data);
1164 if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
1165 mPendingConferenceInvite = ConferenceInvite.parse(data);
1166 if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
1167 if (mPendingConferenceInvite.execute(this)) {
1168 mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
1169 mToast.show();
1170 }
1171 mPendingConferenceInvite = null;
1172 }
1173 } else if (resultCode == RESULT_OK) {
1174 for (Pair<Integer, ValueCallback<Uri[]>> cb : new ArrayList<>(activityCallbacks)) {
1175 if (cb.first == requestCode) {
1176 activityCallbacks.remove(cb);
1177 ArrayList<Uri> dataUris = new ArrayList<>();
1178 if (data.getDataString() != null) {
1179 dataUris.add(Uri.parse(data.getDataString()));
1180 } else if (data.getClipData() != null) {
1181 for (int i = 0; i < data.getClipData().getItemCount(); i++) {
1182 dataUris.add(data.getClipData().getItemAt(i).getUri());
1183 }
1184 }
1185 cb.second.onReceiveValue(dataUris.toArray(new Uri[0]));
1186 }
1187 }
1188 }
1189 }
1190
1191 public boolean copyTextToClipboard(String text, int labelResId) {
1192 ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
1193 String label = getResources().getString(labelResId);
1194 if (mClipBoardManager != null) {
1195 ClipData mClipData = ClipData.newPlainText(label, text);
1196 mClipBoardManager.setPrimaryClip(mClipData);
1197 return true;
1198 }
1199 return false;
1200 }
1201
1202 protected boolean manuallyChangePresence() {
1203 return getBooleanPreference(
1204 AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
1205 }
1206
1207 protected String getShareableUri() {
1208 return getShareableUri(false);
1209 }
1210
1211 protected String getShareableUri(boolean http) {
1212 return null;
1213 }
1214
1215 protected void shareLink(boolean http) {
1216 String uri = getShareableUri(http);
1217 if (uri == null || uri.isEmpty()) {
1218 return;
1219 }
1220 Intent intent = new Intent(Intent.ACTION_SEND);
1221 intent.setType("text/plain");
1222 intent.putExtra(Intent.EXTRA_TEXT, getShareableUri(http));
1223 try {
1224 startActivity(Intent.createChooser(intent, getText(R.string.share_uri_with)));
1225 } catch (ActivityNotFoundException e) {
1226 Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
1227 }
1228 }
1229
1230 protected void launchOpenKeyChain(long keyId) {
1231 PgpEngine pgp = XmppActivity.this.xmppConnectionService.getPgpEngine();
1232 try {
1233 startIntentSenderForResult(
1234 pgp.getIntentForKey(keyId).getIntentSender(),
1235 0,
1236 null,
1237 0,
1238 0,
1239 0,
1240 Compatibility.pgpStartIntentSenderOptions());
1241 } catch (final Throwable e) {
1242 Log.d(Config.LOGTAG, "could not launch OpenKeyChain", e);
1243 Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show();
1244 }
1245 }
1246
1247 @Override
1248 protected void onResume() {
1249 super.onResume();
1250 SettingsUtils.applyScreenshotSetting(this);
1251 }
1252
1253 @Override
1254 public void onPause() {
1255 super.onPause();
1256 }
1257
1258 @Override
1259 public boolean onMenuOpened(int id, Menu menu) {
1260 if (id == AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR && menu != null) {
1261 MenuDoubleTabUtil.recordMenuOpen();
1262 }
1263 return super.onMenuOpened(id, menu);
1264 }
1265
1266 protected void showQrCode() {
1267 final var uri = getShareableUri();
1268 if (uri != null) {
1269 showQrCode(uri);
1270 return;
1271 }
1272
1273 final var accounts = xmppConnectionService.getAccounts();
1274 if (accounts.size() < 1) return;
1275
1276 if (accounts.size() == 1) {
1277 showQrCode(accounts.get(0).getShareableUri());
1278 return;
1279 }
1280
1281 final AtomicReference<Account> selectedAccount = new AtomicReference<>(accounts.get(0));
1282 final MaterialAlertDialogBuilder alertDialogBuilder = new MaterialAlertDialogBuilder(this);
1283 alertDialogBuilder.setTitle(R.string.choose_account);
1284 final String[] asStrings = Collections2.transform(accounts, a -> a.getJid().asBareJid().toString()).toArray(new String[0]);
1285 alertDialogBuilder.setSingleChoiceItems(asStrings, 0, (dialog, which) -> selectedAccount.set(accounts.get(which)));
1286 alertDialogBuilder.setNegativeButton(R.string.cancel, null);
1287 alertDialogBuilder.setPositiveButton(R.string.ok, (dialog, which) -> showQrCode(selectedAccount.get().getShareableUri()));
1288 alertDialogBuilder.create().show();
1289 }
1290
1291 protected void showQrCode(final String uri) {
1292 if (uri == null || uri.isEmpty()) {
1293 return;
1294 }
1295 final Point size = new Point();
1296 getWindowManager().getDefaultDisplay().getSize(size);
1297 final int width = Math.min(size.x, size.y);
1298 final int black;
1299 final int white;
1300 if (Activities.isNightMode(this)) {
1301 black =
1302 MaterialColors.getColor(
1303 this,
1304 com.google.android.material.R.attr.colorSurfaceContainerHighest,
1305 "No surface color configured");
1306 white =
1307 MaterialColors.getColor(
1308 this,
1309 com.google.android.material.R.attr.colorSurfaceInverse,
1310 "No inverse surface color configured");
1311 } else {
1312 black =
1313 MaterialColors.getColor(
1314 this,
1315 com.google.android.material.R.attr.colorSurfaceInverse,
1316 "No inverse surface color configured");
1317 white =
1318 MaterialColors.getColor(
1319 this,
1320 com.google.android.material.R.attr.colorSurfaceContainerHighest,
1321 "No surface color configured");
1322 }
1323 final var bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width, black, white);
1324 final ImageView view = new ImageView(this);
1325 view.setBackgroundColor(white);
1326 view.setImageBitmap(bitmap);
1327 MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1328 builder.setView(view);
1329 builder.create().show();
1330 }
1331
1332 protected Account extractAccount(Intent intent) {
1333 final String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
1334 try {
1335 return jid != null ? xmppConnectionService.findAccountByJid(Jid.of(jid)) : null;
1336 } catch (IllegalArgumentException e) {
1337 return null;
1338 }
1339 }
1340
1341 public AvatarService avatarService() {
1342 return xmppConnectionService.getAvatarService();
1343 }
1344
1345 public void loadBitmap(Message message, ImageView imageView) {
1346 Drawable bm;
1347 try {
1348 bm =
1349 xmppConnectionService
1350 .getFileBackend()
1351 .getThumbnail(message, getResources(), (int) (metrics.density * 288), true);
1352 } catch (IOException e) {
1353 bm = null;
1354 }
1355 if (bm != null) {
1356 cancelPotentialWork(message, imageView);
1357 imageView.setImageDrawable(bm);
1358 imageView.setBackgroundColor(0x00000000);
1359 if (Build.VERSION.SDK_INT >= 28 && bm instanceof AnimatedImageDrawable) {
1360 ((AnimatedImageDrawable) bm).start();
1361 }
1362 } else {
1363 if (cancelPotentialWork(message, imageView)) {
1364 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1365 final BitmapDrawable fallbackThumb = xmppConnectionService.getFileBackend().getFallbackThumbnail(message, (int) (metrics.density * 288), true);
1366 imageView.setBackgroundColor(fallbackThumb == null ? 0xff333333 : 0x00000000);
1367 final AsyncDrawable asyncDrawable = new AsyncDrawable(
1368 getResources(), fallbackThumb != null ? fallbackThumb.getBitmap() : null, task);
1369 imageView.setImageDrawable(asyncDrawable);
1370 try {
1371 task.execute(message);
1372 } catch (final RejectedExecutionException ignored) {
1373 ignored.printStackTrace();
1374 }
1375 }
1376 }
1377 }
1378
1379 protected interface OnValueEdited {
1380 String onValueEdited(String value);
1381 }
1382
1383 public static class ConferenceInvite {
1384 private String uuid;
1385 private final List<Jid> jids = new ArrayList<>();
1386
1387 public static ConferenceInvite parse(Intent data) {
1388 ConferenceInvite invite = new ConferenceInvite();
1389 invite.uuid = data.getStringExtra(ChooseContactActivity.EXTRA_CONVERSATION);
1390 if (invite.uuid == null) {
1391 return null;
1392 }
1393 invite.jids.addAll(ChooseContactActivity.extractJabberIds(data));
1394 return invite;
1395 }
1396
1397 public boolean execute(XmppActivity activity) {
1398 XmppConnectionService service = activity.xmppConnectionService;
1399 Conversation conversation = service.findConversationByUuid(this.uuid);
1400 if (conversation == null) {
1401 return false;
1402 }
1403 if (conversation.getMode() == Conversation.MODE_MULTI) {
1404 for (Jid jid : jids) {
1405 service.invite(conversation, jid);
1406 }
1407 return false;
1408 } else {
1409 jids.add(conversation.getJid().asBareJid());
1410 return service.createAdhocConference(
1411 conversation.getAccount(), null, jids, activity.adhocCallback);
1412 }
1413 }
1414 }
1415
1416 static class BitmapWorkerTask extends AsyncTask<Message, Void, Drawable> {
1417 private final WeakReference<ImageView> imageViewReference;
1418 private Message message = null;
1419
1420 private BitmapWorkerTask(ImageView imageView) {
1421 this.imageViewReference = new WeakReference<>(imageView);
1422 }
1423
1424 @Override
1425 protected Drawable doInBackground(Message... params) {
1426 if (isCancelled()) {
1427 return null;
1428 }
1429 final XmppActivity activity = find(imageViewReference);
1430 Drawable d = null;
1431 message = params[0];
1432 try {
1433 if (activity != null && activity.xmppConnectionService != null) {
1434 d = activity.xmppConnectionService.getFileBackend().getThumbnail(message, imageViewReference.get().getContext().getResources(), (int) (activity.metrics.density * 288), false);
1435 }
1436 } catch (IOException e) { e.printStackTrace(); }
1437 final ImageView imageView = imageViewReference.get();
1438 if (d == null && activity != null && activity.xmppConnectionService != null && imageView != null && imageView.getDrawable() instanceof AsyncDrawable && ((AsyncDrawable) imageView.getDrawable()).getBitmap() == null) {
1439 d = activity.xmppConnectionService.getFileBackend().getFallbackThumbnail(message, (int) (activity.metrics.density * 288), false);
1440 }
1441 return d;
1442 }
1443
1444 @Override
1445 protected void onPostExecute(final Drawable drawable) {
1446 if (!isCancelled()) {
1447 final ImageView imageView = imageViewReference.get();
1448 if (imageView != null) {
1449 Drawable old = imageView.getDrawable();
1450 if (old instanceof AsyncDrawable) {
1451 ((AsyncDrawable) old).clearTask();
1452 }
1453 if (drawable != null) {
1454 imageView.setImageDrawable(drawable);
1455 }
1456 imageView.setBackgroundColor(drawable == null ? 0xff333333 : 0x00000000);
1457 if (Build.VERSION.SDK_INT >= 28 && drawable instanceof AnimatedImageDrawable) {
1458 ((AnimatedImageDrawable) drawable).start();
1459 }
1460 }
1461 }
1462 }
1463 }
1464
1465 private static class AsyncDrawable extends BitmapDrawable {
1466 private WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1467
1468 private AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
1469 super(res, bitmap);
1470 bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
1471 }
1472
1473 private synchronized BitmapWorkerTask getBitmapWorkerTask() {
1474 if (bitmapWorkerTaskReference == null) return null;
1475
1476 return bitmapWorkerTaskReference.get();
1477 }
1478
1479 public synchronized void clearTask() {
1480 bitmapWorkerTaskReference = null;
1481 }
1482 }
1483
1484 public static XmppActivity find(@NonNull WeakReference<ImageView> viewWeakReference) {
1485 final View view = viewWeakReference.get();
1486 return view == null ? null : find(view);
1487 }
1488
1489 public static XmppActivity find(@NonNull final View view) {
1490 Context context = view.getContext();
1491 while (context instanceof ContextWrapper) {
1492 if (context instanceof XmppActivity) {
1493 return (XmppActivity) context;
1494 }
1495 context = ((ContextWrapper) context).getBaseContext();
1496 }
1497 return null;
1498 }
1499
1500 public boolean isDark() {
1501 int nightModeFlags = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK;
1502 return nightModeFlags == Configuration.UI_MODE_NIGHT_YES;
1503 }
1504}