ConversationsActivity.java

  1/*
  2 * Copyright (c) 2018, Daniel Gultsch All rights reserved.
  3 *
  4 * Redistribution and use in source and binary forms, with or without modification,
  5 * are permitted provided that the following conditions are met:
  6 *
  7 * 1. Redistributions of source code must retain the above copyright notice, this
  8 * list of conditions and the following disclaimer.
  9 *
 10 * 2. Redistributions in binary form must reproduce the above copyright notice,
 11 * this list of conditions and the following disclaimer in the documentation and/or
 12 * other materials provided with the distribution.
 13 *
 14 * 3. Neither the name of the copyright holder nor the names of its contributors
 15 * may be used to endorse or promote products derived from this software without
 16 * specific prior written permission.
 17 *
 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
 19 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 21 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
 22 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
 25 * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 27 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 28 */
 29
 30package eu.siacs.conversations.ui;
 31
 32import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP;
 33
 34import android.Manifest;
 35import android.annotation.SuppressLint;
 36import android.app.Activity;
 37import android.app.Fragment;
 38import android.app.FragmentManager;
 39import android.app.FragmentTransaction;
 40import android.content.ActivityNotFoundException;
 41import android.content.Context;
 42import android.content.Intent;
 43import android.content.pm.PackageManager;
 44import android.net.Uri;
 45import android.os.Build;
 46import android.os.Bundle;
 47import android.provider.Settings;
 48import android.util.Log;
 49import android.view.KeyEvent;
 50import android.view.Menu;
 51import android.view.MenuItem;
 52import android.widget.Toast;
 53import androidx.annotation.IdRes;
 54import androidx.annotation.NonNull;
 55import androidx.appcompat.app.ActionBar;
 56import androidx.appcompat.app.AlertDialog;
 57import androidx.core.app.ActivityCompat;
 58import androidx.databinding.DataBindingUtil;
 59import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 60import eu.siacs.conversations.Config;
 61import eu.siacs.conversations.R;
 62import eu.siacs.conversations.crypto.OmemoSetting;
 63import eu.siacs.conversations.databinding.ActivityConversationsBinding;
 64import eu.siacs.conversations.entities.Contact;
 65import eu.siacs.conversations.entities.Conversation;
 66import eu.siacs.conversations.entities.Conversational;
 67import eu.siacs.conversations.services.XmppConnectionService;
 68import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
 69import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
 70import eu.siacs.conversations.ui.interfaces.OnConversationRead;
 71import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
 72import eu.siacs.conversations.ui.interfaces.OnConversationsListItemUpdated;
 73import eu.siacs.conversations.ui.util.ActivityResult;
 74import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
 75import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 76import eu.siacs.conversations.ui.util.PendingItem;
 77import eu.siacs.conversations.ui.util.ToolbarUtils;
 78import eu.siacs.conversations.utils.ExceptionHelper;
 79import eu.siacs.conversations.utils.SignupUtils;
 80import eu.siacs.conversations.utils.XmppUri;
 81import eu.siacs.conversations.xmpp.Jid;
 82import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 83import java.util.Arrays;
 84import java.util.List;
 85import java.util.concurrent.atomic.AtomicBoolean;
 86import org.openintents.openpgp.util.OpenPgpApi;
 87
 88public class ConversationsActivity extends XmppActivity
 89        implements OnConversationSelected,
 90                OnConversationArchived,
 91                OnConversationsListItemUpdated,
 92                OnConversationRead,
 93                XmppConnectionService.OnAccountUpdate,
 94                XmppConnectionService.OnConversationUpdate,
 95                XmppConnectionService.OnRosterUpdate,
 96                OnUpdateBlocklist,
 97                XmppConnectionService.OnShowErrorToast,
 98                XmppConnectionService.OnAffiliationChanged {
 99
100    public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW";
101    public static final String EXTRA_CONVERSATION = "conversationUuid";
102    public static final String EXTRA_DOWNLOAD_UUID = "eu.siacs.conversations.download_uuid";
103    public static final String EXTRA_AS_QUOTE = "eu.siacs.conversations.as_quote";
104    public static final String EXTRA_NICK = "nick";
105    public static final String EXTRA_IS_PRIVATE_MESSAGE = "pm";
106    public static final String EXTRA_DO_NOT_APPEND = "do_not_append";
107    public static final String EXTRA_POST_INIT_ACTION = "post_init_action";
108    public static final String POST_ACTION_RECORD_VOICE = "record_voice";
109    public static final String EXTRA_TYPE = "type";
110
111    private static final List<String> VIEW_AND_SHARE_ACTIONS =
112            Arrays.asList(
113                    ACTION_VIEW_CONVERSATION, Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE);
114
115    public static final int REQUEST_OPEN_MESSAGE = 0x9876;
116    public static final int REQUEST_PLAY_PAUSE = 0x5432;
117
118    // secondary fragment (when holding the conversation, must be initialized before refreshing the
119    // overview fragment
120    private static final @IdRes int[] FRAGMENT_ID_NOTIFICATION_ORDER = {
121        R.id.secondary_fragment, R.id.main_fragment
122    };
123    private final PendingItem<Intent> pendingViewIntent = new PendingItem<>();
124    private final PendingItem<ActivityResult> postponedActivityResult = new PendingItem<>();
125    private ActivityConversationsBinding binding;
126    private boolean mActivityPaused = true;
127    private final AtomicBoolean mRedirectInProcess = new AtomicBoolean(false);
128
129    private static boolean isViewOrShareIntent(Intent i) {
130        Log.d(Config.LOGTAG, "action: " + (i == null ? null : i.getAction()));
131        return i != null
132                && VIEW_AND_SHARE_ACTIONS.contains(i.getAction())
133                && i.hasExtra(EXTRA_CONVERSATION);
134    }
135
136    private static Intent createLauncherIntent(Context context) {
137        final Intent intent = new Intent(context, ConversationsActivity.class);
138        intent.setAction(Intent.ACTION_MAIN);
139        intent.addCategory(Intent.CATEGORY_LAUNCHER);
140        return intent;
141    }
142
143    @Override
144    protected void refreshUiReal() {
145        invalidateOptionsMenu();
146        for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) {
147            refreshFragment(id);
148        }
149    }
150
151    @Override
152    protected void onBackendConnected() {
153        if (performRedirectIfNecessary(true)) {
154            return;
155        }
156        xmppConnectionService.getNotificationService().setIsInForeground(true);
157        final Intent intent = pendingViewIntent.pop();
158        if (intent != null) {
159            if (processViewIntent(intent)) {
160                if (binding.secondaryFragment != null) {
161                    notifyFragmentOfBackendConnected(R.id.main_fragment);
162                }
163                invalidateActionBarTitle();
164                return;
165            }
166        }
167        for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) {
168            notifyFragmentOfBackendConnected(id);
169        }
170
171        final ActivityResult activityResult = postponedActivityResult.pop();
172        if (activityResult != null) {
173            handleActivityResult(activityResult);
174        }
175
176        invalidateActionBarTitle();
177        if (binding.secondaryFragment != null
178                && ConversationFragment.getConversation(this) == null) {
179            Conversation conversation = ConversationsOverviewFragment.getSuggestion(this);
180            if (conversation != null) {
181                openConversation(conversation, null);
182            }
183        }
184        showDialogsIfMainIsOverview();
185    }
186
187    private boolean performRedirectIfNecessary(boolean noAnimation) {
188        return performRedirectIfNecessary(null, noAnimation);
189    }
190
191    private boolean performRedirectIfNecessary(
192            final Conversation ignore, final boolean noAnimation) {
193        if (xmppConnectionService == null) {
194            return false;
195        }
196        boolean isConversationsListEmpty = xmppConnectionService.isConversationsListEmpty(ignore);
197        if (isConversationsListEmpty && mRedirectInProcess.compareAndSet(false, true)) {
198            final Intent intent = SignupUtils.getRedirectionIntent(this);
199            if (noAnimation) {
200                intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
201            }
202            runOnUiThread(
203                    () -> {
204                        startActivity(intent);
205                        if (noAnimation) {
206                            overridePendingTransition(0, 0);
207                        }
208                    });
209        }
210        return mRedirectInProcess.get();
211    }
212
213    private void showDialogsIfMainIsOverview() {
214        if (xmppConnectionService == null) {
215            return;
216        }
217        final Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
218        if (fragment instanceof ConversationsOverviewFragment) {
219            if (ExceptionHelper.checkForCrash(this)) {
220                return;
221            }
222            if (openBatteryOptimizationDialogIfNeeded()) {
223                return;
224            }
225            requestNotificationPermissionIfNeeded();
226        }
227    }
228
229    private String getBatteryOptimizationPreferenceKey() {
230        @SuppressLint("HardwareIds")
231        String device = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
232        return "show_battery_optimization" + (device == null ? "" : device);
233    }
234
235    private void setNeverAskForBatteryOptimizationsAgain() {
236        getPreferences().edit().putBoolean(getBatteryOptimizationPreferenceKey(), false).apply();
237    }
238
239    private boolean openBatteryOptimizationDialogIfNeeded() {
240        if (isOptimizingBattery()
241                && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) {
242            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
243            builder.setTitle(R.string.battery_optimizations_enabled);
244            builder.setMessage(
245                    getString(
246                            R.string.battery_optimizations_enabled_dialog,
247                            getString(R.string.app_name)));
248            builder.setPositiveButton(
249                    R.string.next,
250                    (dialog, which) -> {
251                        final Intent intent =
252                                new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
253                        final Uri uri = Uri.parse("package:" + getPackageName());
254                        intent.setData(uri);
255                        try {
256                            startActivityForResult(intent, REQUEST_BATTERY_OP);
257                        } catch (final ActivityNotFoundException e) {
258                            Toast.makeText(
259                                            this,
260                                            R.string.device_does_not_support_battery_op,
261                                            Toast.LENGTH_SHORT)
262                                    .show();
263                        }
264                    });
265            builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain());
266            final AlertDialog dialog = builder.create();
267            dialog.setCanceledOnTouchOutside(false);
268            dialog.show();
269            return true;
270        }
271        return false;
272    }
273
274    private void requestNotificationPermissionIfNeeded() {
275        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
276                && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
277                        != PackageManager.PERMISSION_GRANTED) {
278            requestPermissions(
279                    new String[] {Manifest.permission.POST_NOTIFICATIONS},
280                    REQUEST_POST_NOTIFICATION);
281        }
282    }
283
284    private void notifyFragmentOfBackendConnected(@IdRes int id) {
285        final Fragment fragment = getFragmentManager().findFragmentById(id);
286        if (fragment instanceof OnBackendConnected callback) {
287            callback.onBackendConnected();
288        }
289    }
290
291    private void refreshFragment(@IdRes int id) {
292        final Fragment fragment = getFragmentManager().findFragmentById(id);
293        if (fragment instanceof XmppFragment xmppFragment) {
294            xmppFragment.refresh();
295        }
296    }
297
298    private boolean processViewIntent(final Intent intent) {
299        final String uuid = intent.getStringExtra(EXTRA_CONVERSATION);
300        final Conversation conversation =
301                uuid != null ? xmppConnectionService.findConversationByUuidReliable(uuid) : null;
302        if (conversation == null) {
303            Log.d(Config.LOGTAG, "unable to view conversation with uuid:" + uuid);
304            return false;
305        }
306        openConversation(conversation, intent.getExtras());
307        return true;
308    }
309
310    @Override
311    public void onRequestPermissionsResult(
312            int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
313        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
314        UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
315        if (grantResults.length > 0) {
316            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
317                switch (requestCode) {
318                    case REQUEST_OPEN_MESSAGE:
319                        refreshUiReal();
320                        ConversationFragment.openPendingMessage(this);
321                        break;
322                    case REQUEST_PLAY_PAUSE:
323                        ConversationFragment.startStopPending(this);
324                        break;
325                }
326            }
327        }
328    }
329
330    @Override
331    public void onActivityResult(int requestCode, int resultCode, final Intent data) {
332        super.onActivityResult(requestCode, resultCode, data);
333        ActivityResult activityResult = ActivityResult.of(requestCode, resultCode, data);
334        if (xmppConnectionService != null) {
335            handleActivityResult(activityResult);
336        } else {
337            this.postponedActivityResult.push(activityResult);
338        }
339    }
340
341    private void handleActivityResult(final ActivityResult activityResult) {
342        if (activityResult.resultCode == Activity.RESULT_OK) {
343            handlePositiveActivityResult(activityResult.requestCode, activityResult.data);
344        } else {
345            handleNegativeActivityResult(activityResult.requestCode);
346        }
347        if (activityResult.requestCode == REQUEST_BATTERY_OP) {
348            // the result code is always 0 even when battery permission were granted
349            requestNotificationPermissionIfNeeded();
350            XmppConnectionService.toggleForegroundService(xmppConnectionService);
351        }
352    }
353
354    private void handleNegativeActivityResult(int requestCode) {
355        Conversation conversation = ConversationFragment.getConversationReliable(this);
356        switch (requestCode) {
357            case REQUEST_DECRYPT_PGP:
358                if (conversation == null) {
359                    break;
360                }
361                conversation.getAccount().getPgpDecryptionService().giveUpCurrentDecryption();
362                break;
363            case REQUEST_BATTERY_OP:
364                setNeverAskForBatteryOptimizationsAgain();
365                break;
366        }
367    }
368
369    private void handlePositiveActivityResult(int requestCode, final Intent data) {
370        Conversation conversation = ConversationFragment.getConversationReliable(this);
371        if (conversation == null) {
372            Log.d(Config.LOGTAG, "conversation not found");
373            return;
374        }
375        switch (requestCode) {
376            case REQUEST_DECRYPT_PGP:
377                conversation.getAccount().getPgpDecryptionService().continueDecryption(data);
378                break;
379            case REQUEST_CHOOSE_PGP_ID:
380                long id = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, 0);
381                if (id != 0) {
382                    conversation.getAccount().setPgpSignId(id);
383                    announcePgp(conversation.getAccount(), null, null, onOpenPGPKeyPublished);
384                } else {
385                    choosePgpSignId(conversation.getAccount());
386                }
387                break;
388            case REQUEST_ANNOUNCE_PGP:
389                announcePgp(conversation.getAccount(), conversation, data, onOpenPGPKeyPublished);
390                break;
391        }
392    }
393
394    @Override
395    protected void onCreate(final Bundle savedInstanceState) {
396        super.onCreate(savedInstanceState);
397        ConversationMenuConfigurator.reloadFeatures(this);
398        OmemoSetting.load(this);
399        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_conversations);
400        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
401        setSupportActionBar(binding.toolbar);
402        configureActionBar(getSupportActionBar());
403        this.getFragmentManager().addOnBackStackChangedListener(this::invalidateActionBarTitle);
404        this.getFragmentManager().addOnBackStackChangedListener(this::showDialogsIfMainIsOverview);
405        this.initializeFragments();
406        this.invalidateActionBarTitle();
407        final Intent intent;
408        if (savedInstanceState == null) {
409            intent = getIntent();
410        } else {
411            intent = savedInstanceState.getParcelable("intent");
412        }
413        if (isViewOrShareIntent(intent)) {
414            pendingViewIntent.push(intent);
415            setIntent(createLauncherIntent(this));
416        }
417    }
418
419    @Override
420    public boolean onCreateOptionsMenu(Menu menu) {
421        getMenuInflater().inflate(R.menu.activity_conversations, menu);
422        final MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
423        if (qrCodeScanMenuItem != null) {
424            if (isCameraFeatureAvailable()) {
425                Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
426                boolean visible =
427                        getResources().getBoolean(R.bool.show_qr_code_scan)
428                                && fragment instanceof ConversationsOverviewFragment;
429                qrCodeScanMenuItem.setVisible(visible);
430            } else {
431                qrCodeScanMenuItem.setVisible(false);
432            }
433        }
434        return super.onCreateOptionsMenu(menu);
435    }
436
437    @Override
438    public void onConversationSelected(Conversation conversation) {
439        clearPendingViewIntent();
440        if (ConversationFragment.getConversation(this) == conversation) {
441            Log.d(
442                    Config.LOGTAG,
443                    "ignore onConversationSelected() because conversation is already open");
444            return;
445        }
446        openConversation(conversation, null);
447    }
448
449    public void clearPendingViewIntent() {
450        if (pendingViewIntent.clear()) {
451            Log.e(Config.LOGTAG, "cleared pending view intent");
452        }
453    }
454
455    private void displayToast(final String msg) {
456        runOnUiThread(
457                () -> Toast.makeText(ConversationsActivity.this, msg, Toast.LENGTH_SHORT).show());
458    }
459
460    @Override
461    public void onAffiliationChangedSuccessful(Jid jid) {}
462
463    @Override
464    public void onAffiliationChangeFailed(Jid jid, int resId) {
465        displayToast(getString(resId, jid.asBareJid().toString()));
466    }
467
468    private void openConversation(Conversation conversation, Bundle extras) {
469        final FragmentManager fragmentManager = getFragmentManager();
470        executePendingTransactions(fragmentManager);
471        ConversationFragment conversationFragment =
472                (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment);
473        final boolean mainNeedsRefresh;
474        if (conversationFragment == null) {
475            mainNeedsRefresh = false;
476            final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
477            if (mainFragment instanceof ConversationFragment) {
478                conversationFragment = (ConversationFragment) mainFragment;
479            } else {
480                conversationFragment = new ConversationFragment();
481                FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
482                fragmentTransaction.replace(R.id.main_fragment, conversationFragment);
483                fragmentTransaction.addToBackStack(null);
484                try {
485                    fragmentTransaction.commit();
486                } catch (IllegalStateException e) {
487                    Log.w(Config.LOGTAG, "sate loss while opening conversation", e);
488                    // allowing state loss is probably fine since view intents et all are already
489                    // stored and a click can probably be 'ignored'
490                    return;
491                }
492            }
493        } else {
494            mainNeedsRefresh = true;
495        }
496        conversationFragment.reInit(conversation, extras == null ? new Bundle() : extras);
497        if (mainNeedsRefresh) {
498            refreshFragment(R.id.main_fragment);
499        }
500        invalidateActionBarTitle();
501    }
502
503    private static void executePendingTransactions(final FragmentManager fragmentManager) {
504        try {
505            fragmentManager.executePendingTransactions();
506        } catch (final Exception e) {
507            Log.e(Config.LOGTAG, "unable to execute pending fragment transactions");
508        }
509    }
510
511    public boolean onXmppUriClicked(Uri uri) {
512        XmppUri xmppUri = new XmppUri(uri);
513        if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) {
514            final Conversation conversation =
515                    xmppConnectionService.findUniqueConversationByJid(xmppUri);
516            if (conversation != null) {
517                openConversation(conversation, null);
518                return true;
519            }
520        }
521        return false;
522    }
523
524    @Override
525    public boolean onOptionsItemSelected(MenuItem item) {
526        if (MenuDoubleTabUtil.shouldIgnoreTap()) {
527            return false;
528        }
529        switch (item.getItemId()) {
530            case android.R.id.home:
531                FragmentManager fm = getFragmentManager();
532                if (fm.getBackStackEntryCount() > 0) {
533                    try {
534                        fm.popBackStack();
535                    } catch (IllegalStateException e) {
536                        Log.w(Config.LOGTAG, "Unable to pop back stack after pressing home button");
537                    }
538                    return true;
539                }
540                break;
541            case R.id.action_scan_qr_code:
542                UriHandlerActivity.scan(this);
543                return true;
544            case R.id.action_search_all_conversations:
545                startActivity(new Intent(this, SearchActivity.class));
546                return true;
547            case R.id.action_search_this_conversation:
548                final Conversation conversation = ConversationFragment.getConversation(this);
549                if (conversation == null) {
550                    return true;
551                }
552                final Intent intent = new Intent(this, SearchActivity.class);
553                intent.putExtra(SearchActivity.EXTRA_CONVERSATION_UUID, conversation.getUuid());
554                startActivity(intent);
555                return true;
556        }
557        return super.onOptionsItemSelected(item);
558    }
559
560    @Override
561    public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
562        if (keyCode == KeyEvent.KEYCODE_DPAD_UP && keyEvent.isCtrlPressed()) {
563            final ConversationFragment conversationFragment = ConversationFragment.get(this);
564            if (conversationFragment != null && conversationFragment.onArrowUpCtrlPressed()) {
565                return true;
566            }
567        }
568        return super.onKeyDown(keyCode, keyEvent);
569    }
570
571    @Override
572    public void onSaveInstanceState(final Bundle savedInstanceState) {
573        final Intent pendingIntent = pendingViewIntent.peek();
574        savedInstanceState.putParcelable(
575                "intent", pendingIntent != null ? pendingIntent : getIntent());
576        super.onSaveInstanceState(savedInstanceState);
577    }
578
579    @Override
580    public void onStart() {
581        super.onStart();
582        mRedirectInProcess.set(false);
583    }
584
585    @Override
586    protected void onNewIntent(final Intent intent) {
587        super.onNewIntent(intent);
588        if (isViewOrShareIntent(intent)) {
589            if (xmppConnectionService != null) {
590                clearPendingViewIntent();
591                processViewIntent(intent);
592            } else {
593                pendingViewIntent.push(intent);
594            }
595        }
596        setIntent(createLauncherIntent(this));
597    }
598
599    @Override
600    public void onPause() {
601        this.mActivityPaused = true;
602        super.onPause();
603    }
604
605    @Override
606    public void onResume() {
607        super.onResume();
608        this.mActivityPaused = false;
609    }
610
611    private void initializeFragments() {
612        final FragmentManager fragmentManager = getFragmentManager();
613        FragmentTransaction transaction = fragmentManager.beginTransaction();
614        final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
615        final Fragment secondaryFragment =
616                fragmentManager.findFragmentById(R.id.secondary_fragment);
617        if (mainFragment != null) {
618            if (binding.secondaryFragment != null) {
619                if (mainFragment instanceof ConversationFragment) {
620                    getFragmentManager().popBackStack();
621                    transaction.remove(mainFragment);
622                    transaction.commit();
623                    fragmentManager.executePendingTransactions();
624                    transaction = fragmentManager.beginTransaction();
625                    transaction.replace(R.id.secondary_fragment, mainFragment);
626                    transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
627                    transaction.commit();
628                    return;
629                }
630            } else {
631                if (secondaryFragment instanceof ConversationFragment) {
632                    transaction.remove(secondaryFragment);
633                    transaction.commit();
634                    getFragmentManager().executePendingTransactions();
635                    transaction = fragmentManager.beginTransaction();
636                    transaction.replace(R.id.main_fragment, secondaryFragment);
637                    transaction.addToBackStack(null);
638                    transaction.commit();
639                    return;
640                }
641            }
642        } else {
643            transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
644        }
645        if (binding.secondaryFragment != null && secondaryFragment == null) {
646            transaction.replace(R.id.secondary_fragment, new ConversationFragment());
647        }
648        transaction.commit();
649    }
650
651    private void invalidateActionBarTitle() {
652        final ActionBar actionBar = getSupportActionBar();
653        if (actionBar == null) {
654            return;
655        }
656        final FragmentManager fragmentManager = getFragmentManager();
657        final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
658        if (mainFragment instanceof ConversationFragment conversationFragment) {
659            final Conversation conversation = conversationFragment.getConversation();
660            if (conversation != null) {
661                actionBar.setTitle(conversation.getName());
662                actionBar.setDisplayHomeAsUpEnabled(true);
663                ToolbarUtils.setActionBarOnClickListener(
664                        binding.toolbar, (v) -> openConversationDetails(conversation));
665                return;
666            }
667        }
668        final Fragment secondaryFragment =
669                fragmentManager.findFragmentById(R.id.secondary_fragment);
670        if (secondaryFragment instanceof ConversationFragment conversationFragment) {
671            final Conversation conversation = conversationFragment.getConversation();
672            if (conversation != null) {
673                actionBar.setTitle(conversation.getName());
674            } else {
675                actionBar.setTitle(R.string.app_name);
676            }
677        } else {
678            actionBar.setTitle(R.string.app_name);
679        }
680        actionBar.setDisplayHomeAsUpEnabled(false);
681        ToolbarUtils.resetActionBarOnClickListeners(binding.toolbar);
682    }
683
684    private void openConversationDetails(final Conversation conversation) {
685        if (conversation.getMode() == Conversational.MODE_MULTI) {
686            ConferenceDetailsActivity.open(this, conversation);
687        } else {
688            final Contact contact = conversation.getContact();
689            if (contact.isSelf()) {
690                switchToAccount(conversation.getAccount());
691            } else {
692                switchToContactDetails(contact);
693            }
694        }
695    }
696
697    @Override
698    public void onConversationArchived(Conversation conversation) {
699        if (performRedirectIfNecessary(conversation, false)) {
700            return;
701        }
702        final FragmentManager fragmentManager = getFragmentManager();
703        final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
704        if (mainFragment instanceof ConversationFragment) {
705            try {
706                fragmentManager.popBackStack();
707            } catch (final IllegalStateException e) {
708                Log.w(
709                        Config.LOGTAG,
710                        "state loss while popping back state after archiving conversation",
711                        e);
712                // this usually means activity is no longer active; meaning on the next open we will
713                // run through this again
714            }
715            return;
716        }
717        final Fragment secondaryFragment =
718                fragmentManager.findFragmentById(R.id.secondary_fragment);
719        if (secondaryFragment instanceof ConversationFragment) {
720            if (((ConversationFragment) secondaryFragment).getConversation() == conversation) {
721                Conversation suggestion =
722                        ConversationsOverviewFragment.getSuggestion(this, conversation);
723                if (suggestion != null) {
724                    openConversation(suggestion, null);
725                }
726            }
727        }
728    }
729
730    @Override
731    public void onConversationsListItemUpdated() {
732        Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
733        if (fragment instanceof ConversationsOverviewFragment) {
734            ((ConversationsOverviewFragment) fragment).refresh();
735        }
736    }
737
738    @Override
739    public void switchToConversation(Conversation conversation) {
740        Log.d(Config.LOGTAG, "override");
741        openConversation(conversation, null);
742    }
743
744    @Override
745    public void onConversationRead(Conversation conversation, String upToUuid) {
746        if (!mActivityPaused && pendingViewIntent.peek() == null) {
747            xmppConnectionService.sendReadMarker(conversation, upToUuid);
748        } else {
749            Log.d(Config.LOGTAG, "ignoring read callback. mActivityPaused=" + mActivityPaused);
750        }
751    }
752
753    @Override
754    public void onAccountUpdate() {
755        this.refreshUi();
756    }
757
758    @Override
759    public void onConversationUpdate() {
760        if (performRedirectIfNecessary(false)) {
761            return;
762        }
763        this.refreshUi();
764    }
765
766    @Override
767    public void onRosterUpdate() {
768        this.refreshUi();
769    }
770
771    @Override
772    public void OnUpdateBlocklist(OnUpdateBlocklist.Status status) {
773        this.refreshUi();
774    }
775
776    @Override
777    public void onShowErrorToast(int resId) {
778        runOnUiThread(() -> Toast.makeText(this, resId, Toast.LENGTH_SHORT).show());
779    }
780}