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 java.io.IOException;
89import java.lang.ref.WeakReference;
90import java.util.ArrayList;
91import java.util.Collection;
92import java.util.List;
93import java.util.concurrent.RejectedExecutionException;
94import java.util.function.Consumer;
95
96public abstract class XmppActivity extends ActionBarActivity {
97
98 public static final String EXTRA_ACCOUNT = "account";
99 protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
100 protected static final int REQUEST_INVITE_TO_CONVERSATION = 0x0102;
101 protected static final int REQUEST_CHOOSE_PGP_ID = 0x0103;
102 protected static final int REQUEST_BATTERY_OP = 0x49ff;
103 protected static final int REQUEST_POST_NOTIFICATION = 0x50ff;
104 public XmppConnectionService xmppConnectionService;
105 public boolean xmppConnectionServiceBound = false;
106
107 protected static final String FRAGMENT_TAG_DIALOG = "dialog";
108
109 private boolean isCameraFeatureAvailable = false;
110
111 protected boolean mUsingEnterKey = false;
112 protected boolean mUseTor = false;
113 protected Toast mToast;
114 public Runnable onOpenPGPKeyPublished =
115 () ->
116 Toast.makeText(
117 XmppActivity.this,
118 R.string.openpgp_has_been_published,
119 Toast.LENGTH_SHORT)
120 .show();
121 protected ConferenceInvite mPendingConferenceInvite = null;
122 protected ServiceConnection mConnection =
123 new ServiceConnection() {
124
125 @Override
126 public void onServiceConnected(ComponentName className, IBinder service) {
127 XmppConnectionBinder binder = (XmppConnectionBinder) service;
128 xmppConnectionService = binder.getService();
129 xmppConnectionServiceBound = true;
130 registerListeners();
131 onBackendConnected();
132 }
133
134 @Override
135 public void onServiceDisconnected(ComponentName arg0) {
136 xmppConnectionServiceBound = false;
137 }
138 };
139 private DisplayMetrics metrics;
140 private long mLastUiRefresh = 0;
141 private final Handler mRefreshUiHandler = new Handler();
142 private final Runnable mRefreshUiRunnable =
143 () -> {
144 mLastUiRefresh = SystemClock.elapsedRealtime();
145 refreshUiReal();
146 };
147 private final UiCallback<Conversation> adhocCallback =
148 new UiCallback<Conversation>() {
149 @Override
150 public void success(final Conversation conversation) {
151 runOnUiThread(
152 () -> {
153 switchToConversation(conversation);
154 hideToast();
155 });
156 }
157
158 @Override
159 public void error(final int errorCode, Conversation object) {
160 runOnUiThread(() -> replaceToast(getString(errorCode)));
161 }
162
163 @Override
164 public void userInputRequired(PendingIntent pi, Conversation object) {}
165 };
166
167 public static boolean cancelPotentialWork(Message message, ImageView imageView) {
168 final BitmapWorkerTask bitmapWorkerTask = getBitmapWorkerTask(imageView);
169
170 if (bitmapWorkerTask != null) {
171 final Message oldMessage = bitmapWorkerTask.message;
172 if (oldMessage == null || message != oldMessage) {
173 bitmapWorkerTask.cancel(true);
174 } else {
175 return false;
176 }
177 }
178 return true;
179 }
180
181 private static BitmapWorkerTask getBitmapWorkerTask(ImageView imageView) {
182 if (imageView != null) {
183 final Drawable drawable = imageView.getDrawable();
184 if (drawable instanceof AsyncDrawable) {
185 final AsyncDrawable asyncDrawable = (AsyncDrawable) drawable;
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().toEscapedString());
737 intent.putExtra("contact", contact.getJid().toEscapedString());
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().toEscapedString());
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, true));
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 if (xmppConnectionServiceBound) {
911 xmppConnectionService.sendPresencePacket(
912 contact.getAccount(),
913 xmppConnectionService
914 .getPresenceGenerator()
915 .requestPresenceUpdatesFrom(contact));
916 }
917 });
918 builder.create().show();
919 }
920
921 protected void quickEdit(String previousValue, @StringRes int hint, OnValueEdited callback) {
922 quickEdit(previousValue, callback, hint, false, false);
923 }
924
925 protected void quickEdit(
926 String previousValue,
927 @StringRes int hint,
928 OnValueEdited callback,
929 boolean permitEmpty) {
930 quickEdit(previousValue, callback, hint, false, permitEmpty);
931 }
932
933 protected void quickPasswordEdit(String previousValue, OnValueEdited callback) {
934 quickEdit(previousValue, callback, R.string.password, true, false);
935 }
936
937 @SuppressLint("InflateParams")
938 private void quickEdit(
939 final String previousValue,
940 final OnValueEdited callback,
941 final @StringRes int hint,
942 boolean password,
943 boolean permitEmpty) {
944 final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
945 final DialogQuickeditBinding binding =
946 DataBindingUtil.inflate(
947 getLayoutInflater(), R.layout.dialog_quickedit, null, false);
948 if (password) {
949 binding.inputEditText.setInputType(
950 InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_PASSWORD);
951 }
952 builder.setPositiveButton(R.string.accept, null);
953 if (hint != 0) {
954 binding.inputLayout.setHint(getString(hint));
955 }
956 binding.inputEditText.requestFocus();
957 if (previousValue != null) {
958 binding.inputEditText.getText().append(previousValue);
959 }
960 builder.setView(binding.getRoot());
961 builder.setNegativeButton(R.string.cancel, null);
962 final AlertDialog dialog = builder.create();
963 dialog.setOnShowListener(d -> SoftKeyboardUtils.showKeyboard(binding.inputEditText));
964 dialog.show();
965 View.OnClickListener clickListener =
966 v -> {
967 String value = binding.inputEditText.getText().toString();
968 if (!value.equals(previousValue) && (!value.trim().isEmpty() || permitEmpty)) {
969 String error = callback.onValueEdited(value);
970 if (error != null) {
971 binding.inputLayout.setError(error);
972 return;
973 }
974 }
975 SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
976 dialog.dismiss();
977 };
978 dialog.getButton(DialogInterface.BUTTON_POSITIVE).setOnClickListener(clickListener);
979 dialog.getButton(DialogInterface.BUTTON_NEGATIVE)
980 .setOnClickListener(
981 (v -> {
982 SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
983 dialog.dismiss();
984 }));
985 dialog.setCanceledOnTouchOutside(false);
986 dialog.setOnDismissListener(
987 dialog1 -> {
988 SoftKeyboardUtils.hideSoftKeyboard(binding.inputEditText);
989 });
990 }
991
992 protected boolean hasStoragePermission(int requestCode) {
993 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
994 if (checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE)
995 != PackageManager.PERMISSION_GRANTED) {
996 requestPermissions(
997 new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, requestCode);
998 return false;
999 } else {
1000 return true;
1001 }
1002 } else {
1003 return true;
1004 }
1005 }
1006
1007 protected void onActivityResult(int requestCode, int resultCode, final Intent data) {
1008 super.onActivityResult(requestCode, resultCode, data);
1009 if (requestCode == REQUEST_INVITE_TO_CONVERSATION && resultCode == RESULT_OK) {
1010 mPendingConferenceInvite = ConferenceInvite.parse(data);
1011 if (xmppConnectionServiceBound && mPendingConferenceInvite != null) {
1012 if (mPendingConferenceInvite.execute(this)) {
1013 mToast = Toast.makeText(this, R.string.creating_conference, Toast.LENGTH_LONG);
1014 mToast.show();
1015 }
1016 mPendingConferenceInvite = null;
1017 }
1018 }
1019 }
1020
1021 public boolean copyTextToClipboard(String text, int labelResId) {
1022 ClipboardManager mClipBoardManager = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
1023 String label = getResources().getString(labelResId);
1024 if (mClipBoardManager != null) {
1025 ClipData mClipData = ClipData.newPlainText(label, text);
1026 mClipBoardManager.setPrimaryClip(mClipData);
1027 return true;
1028 }
1029 return false;
1030 }
1031
1032 protected boolean manuallyChangePresence() {
1033 return getBooleanPreference(
1034 AppSettings.MANUALLY_CHANGE_PRESENCE, R.bool.manually_change_presence);
1035 }
1036
1037 protected String getShareableUri() {
1038 return getShareableUri(false);
1039 }
1040
1041 protected String getShareableUri(boolean http) {
1042 return null;
1043 }
1044
1045 protected void shareLink(boolean http) {
1046 String uri = getShareableUri(http);
1047 if (uri == null || uri.isEmpty()) {
1048 return;
1049 }
1050 Intent intent = new Intent(Intent.ACTION_SEND);
1051 intent.setType("text/plain");
1052 intent.putExtra(Intent.EXTRA_TEXT, getShareableUri(http));
1053 try {
1054 startActivity(Intent.createChooser(intent, getText(R.string.share_uri_with)));
1055 } catch (ActivityNotFoundException e) {
1056 Toast.makeText(this, R.string.no_application_to_share_uri, Toast.LENGTH_SHORT).show();
1057 }
1058 }
1059
1060 protected void launchOpenKeyChain(long keyId) {
1061 PgpEngine pgp = XmppActivity.this.xmppConnectionService.getPgpEngine();
1062 try {
1063 startIntentSenderForResult(
1064 pgp.getIntentForKey(keyId).getIntentSender(),
1065 0,
1066 null,
1067 0,
1068 0,
1069 0,
1070 Compatibility.pgpStartIntentSenderOptions());
1071 } catch (final Throwable e) {
1072 Log.d(Config.LOGTAG, "could not launch OpenKeyChain", e);
1073 Toast.makeText(XmppActivity.this, R.string.openpgp_error, Toast.LENGTH_SHORT).show();
1074 }
1075 }
1076
1077 @Override
1078 protected void onResume() {
1079 super.onResume();
1080 SettingsUtils.applyScreenshotSetting(this);
1081 }
1082
1083 @Override
1084 public void onPause() {
1085 super.onPause();
1086 }
1087
1088 @Override
1089 public boolean onMenuOpened(int id, Menu menu) {
1090 if (id == AppCompatDelegate.FEATURE_SUPPORT_ACTION_BAR && menu != null) {
1091 MenuDoubleTabUtil.recordMenuOpen();
1092 }
1093 return super.onMenuOpened(id, menu);
1094 }
1095
1096 protected void showQrCode() {
1097 showQrCode(getShareableUri());
1098 }
1099
1100 protected void showQrCode(final String uri) {
1101 if (uri == null || uri.isEmpty()) {
1102 return;
1103 }
1104 final Point size = new Point();
1105 getWindowManager().getDefaultDisplay().getSize(size);
1106 final int width = Math.min(size.x, size.y);
1107 final int black;
1108 final int white;
1109 if (Activities.isNightMode(this)) {
1110 black =
1111 MaterialColors.getColor(
1112 this,
1113 com.google.android.material.R.attr.colorSurfaceContainerHighest,
1114 "No surface color configured");
1115 white =
1116 MaterialColors.getColor(
1117 this,
1118 com.google.android.material.R.attr.colorSurfaceInverse,
1119 "No inverse surface color configured");
1120 } else {
1121 black =
1122 MaterialColors.getColor(
1123 this,
1124 com.google.android.material.R.attr.colorSurfaceInverse,
1125 "No inverse surface color configured");
1126 white =
1127 MaterialColors.getColor(
1128 this,
1129 com.google.android.material.R.attr.colorSurfaceContainerHighest,
1130 "No surface color configured");
1131 }
1132 final var bitmap = BarcodeProvider.create2dBarcodeBitmap(uri, width, black, white);
1133 final ImageView view = new ImageView(this);
1134 view.setBackgroundColor(white);
1135 view.setImageBitmap(bitmap);
1136 MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
1137 builder.setView(view);
1138 builder.create().show();
1139 }
1140
1141 protected Account extractAccount(Intent intent) {
1142 final String jid = intent != null ? intent.getStringExtra(EXTRA_ACCOUNT) : null;
1143 try {
1144 return jid != null ? xmppConnectionService.findAccountByJid(Jid.ofEscaped(jid)) : null;
1145 } catch (IllegalArgumentException e) {
1146 return null;
1147 }
1148 }
1149
1150 public AvatarService avatarService() {
1151 return xmppConnectionService.getAvatarService();
1152 }
1153
1154 public void loadBitmap(Message message, ImageView imageView) {
1155 Bitmap bm;
1156 try {
1157 bm =
1158 xmppConnectionService
1159 .getFileBackend()
1160 .getThumbnail(message, (int) (metrics.density * 288), true);
1161 } catch (IOException e) {
1162 bm = null;
1163 }
1164 if (bm != null) {
1165 cancelPotentialWork(message, imageView);
1166 imageView.setImageBitmap(bm);
1167 imageView.setBackgroundColor(0x00000000);
1168 } else {
1169 if (cancelPotentialWork(message, imageView)) {
1170 imageView.setBackgroundColor(0xff333333);
1171 imageView.setImageDrawable(null);
1172 final BitmapWorkerTask task = new BitmapWorkerTask(imageView);
1173 final AsyncDrawable asyncDrawable = new AsyncDrawable(getResources(), null, task);
1174 imageView.setImageDrawable(asyncDrawable);
1175 try {
1176 task.execute(message);
1177 } catch (final RejectedExecutionException ignored) {
1178 ignored.printStackTrace();
1179 }
1180 }
1181 }
1182 }
1183
1184 protected interface OnValueEdited {
1185 String onValueEdited(String value);
1186 }
1187
1188 public static class ConferenceInvite {
1189 private String uuid;
1190 private final List<Jid> jids = new ArrayList<>();
1191
1192 public static ConferenceInvite parse(Intent data) {
1193 ConferenceInvite invite = new ConferenceInvite();
1194 invite.uuid = data.getStringExtra(ChooseContactActivity.EXTRA_CONVERSATION);
1195 if (invite.uuid == null) {
1196 return null;
1197 }
1198 invite.jids.addAll(ChooseContactActivity.extractJabberIds(data));
1199 return invite;
1200 }
1201
1202 public boolean execute(final XmppActivity activity) {
1203 final XmppConnectionService service = activity.xmppConnectionService;
1204 final Conversation conversation = service.findConversationByUuid(this.uuid);
1205 if (conversation == null) {
1206 return false;
1207 }
1208 if (conversation.getMode() == Conversation.MODE_MULTI) {
1209 for (final Jid jid : jids) {
1210 // TODO use direct invites for public conferences
1211 service.invite(conversation, jid);
1212 }
1213 return false;
1214 } else {
1215 jids.add(conversation.getJid().asBareJid());
1216 return service.createAdhocConference(
1217 conversation.getAccount(), null, jids, activity.adhocCallback);
1218 }
1219 }
1220 }
1221
1222 static class BitmapWorkerTask extends AsyncTask<Message, Void, Bitmap> {
1223 private final WeakReference<ImageView> imageViewReference;
1224 private Message message = null;
1225
1226 private BitmapWorkerTask(ImageView imageView) {
1227 this.imageViewReference = new WeakReference<>(imageView);
1228 }
1229
1230 @Override
1231 protected Bitmap doInBackground(Message... params) {
1232 if (isCancelled()) {
1233 return null;
1234 }
1235 message = params[0];
1236 try {
1237 final XmppActivity activity = find(imageViewReference);
1238 if (activity != null && activity.xmppConnectionService != null) {
1239 return activity.xmppConnectionService
1240 .getFileBackend()
1241 .getThumbnail(message, (int) (activity.metrics.density * 288), false);
1242 } else {
1243 return null;
1244 }
1245 } catch (IOException e) {
1246 return null;
1247 }
1248 }
1249
1250 @Override
1251 protected void onPostExecute(final Bitmap bitmap) {
1252 if (!isCancelled()) {
1253 final ImageView imageView = imageViewReference.get();
1254 if (imageView != null) {
1255 imageView.setImageBitmap(bitmap);
1256 imageView.setBackgroundColor(bitmap == null ? 0xff333333 : 0x00000000);
1257 }
1258 }
1259 }
1260 }
1261
1262 private static class AsyncDrawable extends BitmapDrawable {
1263 private final WeakReference<BitmapWorkerTask> bitmapWorkerTaskReference;
1264
1265 private AsyncDrawable(Resources res, Bitmap bitmap, BitmapWorkerTask bitmapWorkerTask) {
1266 super(res, bitmap);
1267 bitmapWorkerTaskReference = new WeakReference<>(bitmapWorkerTask);
1268 }
1269
1270 private BitmapWorkerTask getBitmapWorkerTask() {
1271 return bitmapWorkerTaskReference.get();
1272 }
1273 }
1274
1275 public static XmppActivity find(@NonNull WeakReference<ImageView> viewWeakReference) {
1276 final View view = viewWeakReference.get();
1277 return view == null ? null : find(view);
1278 }
1279
1280 public static XmppActivity find(@NonNull final View view) {
1281 Context context = view.getContext();
1282 while (context instanceof ContextWrapper) {
1283 if (context instanceof XmppActivity) {
1284 return (XmppActivity) context;
1285 }
1286 context = ((ContextWrapper) context).getBaseContext();
1287 }
1288 return null;
1289 }
1290}