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