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