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
 32
 33import static eu.siacs.conversations.ui.ConversationFragment.REQUEST_DECRYPT_PGP;
 34
 35import android.Manifest;
 36import android.annotation.SuppressLint;
 37import android.app.Activity;
 38import android.app.Fragment;
 39import android.app.FragmentManager;
 40import android.app.FragmentTransaction;
 41import android.content.ActivityNotFoundException;
 42import android.content.ComponentName;
 43import android.content.Context;
 44import android.content.Intent;
 45import android.content.pm.PackageManager;
 46import android.net.Uri;
 47import android.os.Build;
 48import android.os.Bundle;
 49import android.provider.Settings;
 50import android.util.Log;
 51import android.util.Pair;
 52import android.view.KeyEvent;
 53import android.view.Menu;
 54import android.view.MenuItem;
 55import android.widget.Toast;
 56
 57import androidx.annotation.IdRes;
 58import androidx.annotation.NonNull;
 59import androidx.appcompat.app.ActionBar;
 60import androidx.appcompat.app.AlertDialog;
 61import androidx.core.app.ActivityCompat;
 62import androidx.core.content.ContextCompat;
 63import androidx.databinding.DataBindingUtil;
 64
 65import com.cheogram.android.DownloadDefaultStickers;
 66import com.cheogram.android.FinishOnboarding;
 67
 68import com.google.common.collect.ImmutableList;
 69
 70import io.michaelrocks.libphonenumber.android.NumberParseException;
 71import com.google.android.material.dialog.MaterialAlertDialogBuilder;
 72
 73import org.openintents.openpgp.util.OpenPgpApi;
 74
 75import java.util.Arrays;
 76import java.util.HashSet;
 77import java.util.List;
 78import java.util.Objects;
 79import java.util.Set;
 80import java.util.concurrent.atomic.AtomicBoolean;
 81
 82import eu.siacs.conversations.Config;
 83import eu.siacs.conversations.R;
 84import eu.siacs.conversations.crypto.OmemoSetting;
 85import eu.siacs.conversations.databinding.ActivityConversationsBinding;
 86import eu.siacs.conversations.entities.Account;
 87import eu.siacs.conversations.entities.Contact;
 88import eu.siacs.conversations.entities.Conversation;
 89import eu.siacs.conversations.entities.Conversational;
 90import eu.siacs.conversations.services.XmppConnectionService;
 91import eu.siacs.conversations.ui.interfaces.OnBackendConnected;
 92import eu.siacs.conversations.ui.interfaces.OnConversationArchived;
 93import eu.siacs.conversations.ui.interfaces.OnConversationRead;
 94import eu.siacs.conversations.ui.interfaces.OnConversationSelected;
 95import eu.siacs.conversations.ui.interfaces.OnConversationsListItemUpdated;
 96import eu.siacs.conversations.ui.util.ActivityResult;
 97import eu.siacs.conversations.ui.util.ConversationMenuConfigurator;
 98import eu.siacs.conversations.ui.util.MenuDoubleTabUtil;
 99import eu.siacs.conversations.ui.util.PendingItem;
100import eu.siacs.conversations.ui.util.ToolbarUtils;
101import eu.siacs.conversations.utils.ExceptionHelper;
102import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
103import eu.siacs.conversations.utils.SignupUtils;
104import eu.siacs.conversations.utils.ThemeHelper;
105import eu.siacs.conversations.utils.XmppUri;
106import eu.siacs.conversations.xmpp.Jid;
107import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
108
109public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged {
110
111    public static final String ACTION_VIEW_CONVERSATION = "eu.siacs.conversations.action.VIEW";
112    public static final String EXTRA_CONVERSATION = "conversationUuid";
113    public static final String EXTRA_DOWNLOAD_UUID = "eu.siacs.conversations.download_uuid";
114    public static final String EXTRA_AS_QUOTE = "eu.siacs.conversations.as_quote";
115    public static final String EXTRA_NICK = "nick";
116    public static final String EXTRA_IS_PRIVATE_MESSAGE = "pm";
117    public static final String EXTRA_DO_NOT_APPEND = "do_not_append";
118    public static final String EXTRA_POST_INIT_ACTION = "post_init_action";
119    public static final String POST_ACTION_RECORD_VOICE = "record_voice";
120    public static final String EXTRA_THREAD = "threadId";
121    public static final String EXTRA_TYPE = "type";
122    public static final String EXTRA_NODE = "node";
123    public static final String EXTRA_JID = "jid";
124
125    private static final List<String> VIEW_AND_SHARE_ACTIONS = Arrays.asList(
126            ACTION_VIEW_CONVERSATION,
127            Intent.ACTION_SEND,
128            Intent.ACTION_SEND_MULTIPLE
129    );
130
131    public static final int REQUEST_OPEN_MESSAGE = 0x9876;
132    public static final int REQUEST_PLAY_PAUSE = 0x5432;
133    public static final int REQUEST_MICROPHONE = 0x5432f;
134    public static final int DIALLER_INTEGRATION = 0x5432ff;
135    public static final int REQUEST_DOWNLOAD_STICKERS = 0xbf8702;
136
137
138    //secondary fragment (when holding the conversation, must be initialized before refreshing the overview fragment
139    private static final @IdRes
140    int[] FRAGMENT_ID_NOTIFICATION_ORDER = {R.id.secondary_fragment, R.id.main_fragment};
141    private final PendingItem<Intent> pendingViewIntent = new PendingItem<>();
142    private final PendingItem<ActivityResult> postponedActivityResult = new PendingItem<>();
143    private ActivityConversationsBinding binding;
144    private boolean mActivityPaused = true;
145    private final AtomicBoolean mRedirectInProcess = new AtomicBoolean(false);
146    private boolean refreshForNewCaps = false;
147    private Set<Jid> newCapsJids = new HashSet<>();
148    private int mRequestCode = -1;
149
150    private static boolean isViewOrShareIntent(Intent i) {
151        Log.d(Config.LOGTAG, "action: " + (i == null ? null : i.getAction()));
152        return i != null && VIEW_AND_SHARE_ACTIONS.contains(i.getAction()) && i.hasExtra(EXTRA_CONVERSATION);
153    }
154
155    private static Intent createLauncherIntent(Context context) {
156        final Intent intent = new Intent(context, ConversationsActivity.class);
157        intent.setAction(Intent.ACTION_MAIN);
158        intent.addCategory(Intent.CATEGORY_LAUNCHER);
159        return intent;
160    }
161
162    @Override
163    protected void refreshUiReal() {
164        invalidateOptionsMenu();
165        for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) {
166            refreshFragment(id);
167        }
168        refreshForNewCaps = false;
169        newCapsJids.clear();
170    }
171
172    @Override
173    protected void onBackendConnected() {
174        if (performRedirectIfNecessary(true)) {
175            return;
176        }
177        xmppConnectionService.getNotificationService().setIsInForeground(true);
178        final Intent intent = pendingViewIntent.pop();
179        if (intent != null) {
180            if (processViewIntent(intent)) {
181                if (binding.secondaryFragment != null) {
182                    notifyFragmentOfBackendConnected(R.id.main_fragment);
183                }
184                invalidateActionBarTitle();
185                return;
186            }
187        }
188        for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) {
189            notifyFragmentOfBackendConnected(id);
190        }
191
192        final ActivityResult activityResult = postponedActivityResult.pop();
193        if (activityResult != null) {
194            handleActivityResult(activityResult);
195        }
196
197        invalidateActionBarTitle();
198        if (binding.secondaryFragment != null && ConversationFragment.getConversation(this) == null) {
199            Conversation conversation = ConversationsOverviewFragment.getSuggestion(this);
200            if (conversation != null) {
201                openConversation(conversation, null);
202            }
203        }
204        showDialogsIfMainIsOverview();
205    }
206
207    private boolean performRedirectIfNecessary(boolean noAnimation) {
208        return performRedirectIfNecessary(null, noAnimation);
209    }
210
211    private boolean performRedirectIfNecessary(final Conversation ignore, final boolean noAnimation) {
212        if (xmppConnectionService == null) {
213            return false;
214        }
215
216        boolean isConversationsListEmpty = xmppConnectionService.isConversationsListEmpty(ignore);
217        if (isConversationsListEmpty && mRedirectInProcess.compareAndSet(false, true)) {
218            final Intent intent = SignupUtils.getRedirectionIntent(this);
219            if (noAnimation) {
220                intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
221            }
222            runOnUiThread(() -> {
223                startActivity(intent);
224                if (noAnimation) {
225                    overridePendingTransition(0, 0);
226                }
227            });
228        }
229        return mRedirectInProcess.get();
230    }
231
232    private void showDialogsIfMainIsOverview() {
233        Pair<Account, Account> incomplete = null;
234        if (xmppConnectionService != null && (incomplete = xmppConnectionService.onboardingIncomplete()) != null) {
235            FinishOnboarding.finish(xmppConnectionService, this, incomplete.first, incomplete.second);
236        }
237        if (xmppConnectionService == null || xmppConnectionService.isOnboarding()) {
238            return;
239        }
240        final Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
241        if (fragment instanceof ConversationsOverviewFragment) {
242            if (ExceptionHelper.checkForCrash(this)) return;
243            if (offerToSetupDiallerIntegration()) return;
244            if (offerToDownloadStickers()) return;
245            if (openBatteryOptimizationDialogIfNeeded()) return;
246            requestNotificationPermissionIfNeeded();
247            xmppConnectionService.rescanStickers();
248        }
249    }
250
251    private String getBatteryOptimizationPreferenceKey() {
252        @SuppressLint("HardwareIds") String device = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID);
253        return "show_battery_optimization" + (device == null ? "" : device);
254    }
255
256    private void setNeverAskForBatteryOptimizationsAgain() {
257        getPreferences().edit().putBoolean(getBatteryOptimizationPreferenceKey(), false).apply();
258    }
259
260    private boolean openBatteryOptimizationDialogIfNeeded() {
261        if (isOptimizingBattery() && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) {
262            final MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(this);
263            builder.setTitle(R.string.battery_optimizations_enabled);
264            builder.setMessage(getString(R.string.battery_optimizations_enabled_dialog, getString(R.string.app_name)));
265            builder.setPositiveButton(R.string.next, (dialog, which) -> {
266                final Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
267                final Uri uri = Uri.parse("package:" + getPackageName());
268                intent.setData(uri);
269                try {
270                    startActivityForResult(intent, REQUEST_BATTERY_OP);
271                } catch (final ActivityNotFoundException e) {
272                    Toast.makeText(this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show();
273                }
274            });
275            builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain());
276            final AlertDialog dialog = builder.create();
277            dialog.setCanceledOnTouchOutside(false);
278            dialog.show();
279            return true;
280        }
281        return false;
282    }
283
284    private void requestNotificationPermissionIfNeeded() {
285        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && ActivityCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
286            requestPermissions(new String[]{Manifest.permission.POST_NOTIFICATIONS}, REQUEST_POST_NOTIFICATION);
287        }
288    }
289
290    private boolean offerToDownloadStickers() {
291        int offered = getPreferences().getInt("default_stickers_offered", 0);
292        if (offered > 0) return false;
293        getPreferences().edit().putInt("default_stickers_offered", 1).apply();
294
295        AlertDialog.Builder builder = new AlertDialog.Builder(this);
296        builder.setTitle("Download Stickers?");
297        builder.setMessage("Would you like to download some default sticker packs?");
298        builder.setPositiveButton(R.string.yes, (dialog, which) -> {
299            if (hasStoragePermission(REQUEST_DOWNLOAD_STICKERS)) {
300                downloadStickers();
301            }
302        });
303        builder.setNegativeButton(R.string.no, (dialog, which) -> {
304            showDialogsIfMainIsOverview();
305        });
306        final AlertDialog dialog = builder.create();
307        dialog.setCanceledOnTouchOutside(false);
308        dialog.show();
309        return true;
310    }
311
312    private boolean offerToSetupDiallerIntegration() {
313        if (mRequestCode == DIALLER_INTEGRATION) {
314            mRequestCode = -1;
315            return true;
316        }
317        if (Build.VERSION.SDK_INT < 23) return false;
318        if (Build.VERSION.SDK_INT >= 33) {
319            if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_TELECOM) && !getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return false;
320        } else {
321            if (!getPackageManager().hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE)) return false;
322        }
323
324        Set<String> pstnGateways = new HashSet<>();
325        for (Account account : xmppConnectionService.getAccounts()) {
326            for (Contact contact : account.getRoster().getContacts()) {
327                if (contact.getPresences().anyIdentity("gateway", "pstn")) {
328                    pstnGateways.add(contact.getJid().asBareJid().toEscapedString());
329                }
330            }
331        }
332
333        if (pstnGateways.size() < 1) return false;
334        Set<String> fromPrefs = getPreferences().getStringSet("pstn_gateways", Set.of("UPGRADE"));
335        getPreferences().edit().putStringSet("pstn_gateways", pstnGateways).apply();
336        pstnGateways.removeAll(fromPrefs);
337        if (pstnGateways.size() < 1) return false;
338
339        if (fromPrefs.contains("UPGRADE")) return false;
340
341        AlertDialog.Builder builder = new AlertDialog.Builder(this);
342        builder.setTitle("Dialler Integration");
343        builder.setMessage("Cheogram Android is able to integrate with your system's dialler app to allow dialling calls via your configured gateway " + String.join(", ", pstnGateways) + ".\n\nEnabling this integration will require granting microphone permission to the app.  Would you like to enable it now?");
344        builder.setPositiveButton(R.string.yes, (dialog, which) -> {
345            final String[] permissions;
346            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
347                permissions = new String[]{Manifest.permission.RECORD_AUDIO, Manifest.permission.BLUETOOTH_CONNECT};
348            } else {
349                permissions = new String[]{Manifest.permission.RECORD_AUDIO};
350            }
351            requestPermissions(permissions, REQUEST_MICROPHONE);
352        });
353        builder.setNegativeButton(R.string.no, (dialog, which) -> {
354            showDialogsIfMainIsOverview();
355        });
356        final AlertDialog dialog = builder.create();
357        dialog.setCanceledOnTouchOutside(false);
358        dialog.show();
359        return true;
360    }
361
362    private void notifyFragmentOfBackendConnected(@IdRes int id) {
363        final Fragment fragment = getFragmentManager().findFragmentById(id);
364        if (fragment instanceof OnBackendConnected callback) {
365            callback.onBackendConnected();
366        }
367    }
368
369    private void refreshFragment(@IdRes int id) {
370        final Fragment fragment = getFragmentManager().findFragmentById(id);
371        if (fragment instanceof XmppFragment xmppFragment) {
372            xmppFragment.refresh();
373            if (refreshForNewCaps) xmppFragment.refreshForNewCaps(newCapsJids);
374        }
375    }
376
377    private boolean processViewIntent(Intent intent) {
378        final String uuid = intent.getStringExtra(EXTRA_CONVERSATION);
379        final Conversation conversation = uuid != null ? xmppConnectionService.findConversationByUuid(uuid) : null;
380        if (conversation == null) {
381            Log.d(Config.LOGTAG, "unable to view conversation with uuid:" + uuid);
382            return false;
383        }
384        openConversation(conversation, intent.getExtras());
385        return true;
386    }
387
388    @Override
389    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
390        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
391        UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults);
392        if (grantResults.length > 0) {
393            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
394                switch (requestCode) {
395                    case REQUEST_OPEN_MESSAGE:
396                        refreshUiReal();
397                        ConversationFragment.openPendingMessage(this);
398                        break;
399                    case REQUEST_PLAY_PAUSE:
400                        ConversationFragment.startStopPending(this);
401                        break;
402                    case REQUEST_MICROPHONE:
403                        Intent intent = new Intent();
404                        intent.setComponent(new ComponentName("com.android.server.telecom",
405                            "com.android.server.telecom.settings.EnableAccountPreferenceActivity"));
406                        try {
407                            startActivityForResult(intent, DIALLER_INTEGRATION);
408                        } catch (ActivityNotFoundException e) {
409                            displayToast("Dialler integration not available on your OS");
410                        }
411                        break;
412                    case REQUEST_DOWNLOAD_STICKERS:
413                        downloadStickers();
414                        break;
415                }
416            } else {
417                showDialogsIfMainIsOverview();
418            }
419        } else {
420            showDialogsIfMainIsOverview();
421        }
422    }
423
424    private void downloadStickers() {
425        Intent intent = new Intent(this, DownloadDefaultStickers.class);
426        intent.putExtra("tor", xmppConnectionService.useTorToConnect());
427        ContextCompat.startForegroundService(this, intent);
428        displayToast("Sticker download started");
429        showDialogsIfMainIsOverview();
430    }
431
432    @Override
433    public void onActivityResult(int requestCode, int resultCode, final Intent data) {
434        super.onActivityResult(requestCode, resultCode, data);
435
436        if (requestCode == DIALLER_INTEGRATION) {
437            mRequestCode = requestCode;
438            try {
439                startActivity(new Intent(android.telecom.TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS));
440            } catch (ActivityNotFoundException e) {
441                displayToast("Dialler integration not available on your OS");
442            }
443            return;
444        }
445
446        ActivityResult activityResult = ActivityResult.of(requestCode, resultCode, data);
447        if (xmppConnectionService != null) {
448            handleActivityResult(activityResult);
449        } else {
450            this.postponedActivityResult.push(activityResult);
451        }
452    }
453
454    private void handleActivityResult(final ActivityResult activityResult) {
455        if (activityResult.resultCode == Activity.RESULT_OK) {
456            handlePositiveActivityResult(activityResult.requestCode, activityResult.data);
457        } else {
458            handleNegativeActivityResult(activityResult.requestCode);
459        }
460        if (activityResult.requestCode == REQUEST_BATTERY_OP) {
461            // the result code is always 0 even when battery permission were granted
462            requestNotificationPermissionIfNeeded();
463            XmppConnectionService.toggleForegroundService(xmppConnectionService);
464        }
465    }
466
467    private void handleNegativeActivityResult(int requestCode) {
468        Conversation conversation = ConversationFragment.getConversationReliable(this);
469        switch (requestCode) {
470            case REQUEST_DECRYPT_PGP:
471                if (conversation == null) {
472                    break;
473                }
474                conversation.getAccount().getPgpDecryptionService().giveUpCurrentDecryption();
475                break;
476            case REQUEST_BATTERY_OP:
477                setNeverAskForBatteryOptimizationsAgain();
478                break;
479        }
480    }
481
482    private void handlePositiveActivityResult(int requestCode, final Intent data) {
483        Conversation conversation = ConversationFragment.getConversationReliable(this);
484        if (conversation == null) {
485            Log.d(Config.LOGTAG, "conversation not found");
486            return;
487        }
488        switch (requestCode) {
489            case REQUEST_DECRYPT_PGP:
490                conversation.getAccount().getPgpDecryptionService().continueDecryption(data);
491                break;
492            case REQUEST_CHOOSE_PGP_ID:
493                long id = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, 0);
494                if (id != 0) {
495                    conversation.getAccount().setPgpSignId(id);
496                    announcePgp(conversation.getAccount(), null, null, onOpenPGPKeyPublished);
497                } else {
498                    choosePgpSignId(conversation.getAccount());
499                }
500                break;
501            case REQUEST_ANNOUNCE_PGP:
502                announcePgp(conversation.getAccount(), conversation, data, onOpenPGPKeyPublished);
503                break;
504        }
505    }
506
507    @Override
508    protected void onCreate(final Bundle savedInstanceState) {
509        super.onCreate(savedInstanceState);
510        ConversationMenuConfigurator.reloadFeatures(this);
511        OmemoSetting.load(this);
512        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_conversations);
513        Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
514        setSupportActionBar(binding.toolbar);
515        configureActionBar(getSupportActionBar());
516        this.getFragmentManager().addOnBackStackChangedListener(this::invalidateActionBarTitle);
517        this.getFragmentManager().addOnBackStackChangedListener(this::showDialogsIfMainIsOverview);
518        this.initializeFragments();
519        this.invalidateActionBarTitle();
520        final Intent intent;
521        if (savedInstanceState == null) {
522            intent = getIntent();
523        } else {
524            intent = savedInstanceState.getParcelable("intent");
525        }
526        if (isViewOrShareIntent(intent)) {
527            pendingViewIntent.push(intent);
528            setIntent(createLauncherIntent(this));
529        }
530    }
531
532    @Override
533    public boolean onCreateOptionsMenu(Menu menu) {
534        getMenuInflater().inflate(R.menu.activity_conversations, menu);
535        final MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code);
536        if (qrCodeScanMenuItem != null) {
537            if (isCameraFeatureAvailable() && (xmppConnectionService == null || !xmppConnectionService.isOnboarding())) {
538                Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
539                boolean visible = getResources().getBoolean(R.bool.show_qr_code_scan)
540                        && fragment instanceof ConversationsOverviewFragment;
541                qrCodeScanMenuItem.setVisible(visible);
542            } else {
543                qrCodeScanMenuItem.setVisible(false);
544            }
545        }
546        return super.onCreateOptionsMenu(menu);
547    }
548
549    @Override
550    public void onConversationSelected(Conversation conversation) {
551        clearPendingViewIntent();
552        if (ConversationFragment.getConversation(this) == conversation) {
553            Log.d(Config.LOGTAG, "ignore onConversationSelected() because conversation is already open");
554            return;
555        }
556        openConversation(conversation, null);
557    }
558
559    public void clearPendingViewIntent() {
560        if (pendingViewIntent.clear()) {
561            Log.e(Config.LOGTAG, "cleared pending view intent");
562        }
563    }
564
565    private void displayToast(final String msg) {
566        runOnUiThread(() -> Toast.makeText(ConversationsActivity.this, msg, Toast.LENGTH_SHORT).show());
567    }
568
569    @Override
570    public void onAffiliationChangedSuccessful(Jid jid) {
571
572    }
573
574    @Override
575    public void onAffiliationChangeFailed(Jid jid, int resId) {
576        displayToast(getString(resId, jid.asBareJid().toString()));
577    }
578
579    private void openConversation(Conversation conversation, Bundle extras) {
580        final FragmentManager fragmentManager = getFragmentManager();
581        executePendingTransactions(fragmentManager);
582        ConversationFragment conversationFragment = (ConversationFragment) fragmentManager.findFragmentById(R.id.secondary_fragment);
583        final boolean mainNeedsRefresh;
584        if (conversationFragment == null) {
585            mainNeedsRefresh = false;
586            final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
587            if (mainFragment instanceof ConversationFragment) {
588                conversationFragment = (ConversationFragment) mainFragment;
589            } else {
590                conversationFragment = new ConversationFragment();
591                FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();
592                fragmentTransaction.replace(R.id.main_fragment, conversationFragment);
593                fragmentTransaction.addToBackStack(null);
594                try {
595                    fragmentTransaction.commit();
596                } catch (IllegalStateException e) {
597                    Log.w(Config.LOGTAG, "sate loss while opening conversation", e);
598                    //allowing state loss is probably fine since view intents et all are already stored and a click can probably be 'ignored'
599                    return;
600                }
601            }
602        } else {
603            mainNeedsRefresh = true;
604        }
605        conversationFragment.reInit(conversation, extras == null ? new Bundle() : extras);
606        if (mainNeedsRefresh) {
607            refreshFragment(R.id.main_fragment);
608        }
609        invalidateActionBarTitle();
610    }
611
612    private static void executePendingTransactions(final FragmentManager fragmentManager) {
613        try {
614            fragmentManager.executePendingTransactions();
615        } catch (final Exception e) {
616            Log.e(Config.LOGTAG,"unable to execute pending fragment transactions");
617        }
618    }
619
620    public boolean onXmppUriClicked(Uri uri) {
621        XmppUri xmppUri = new XmppUri(uri);
622        if (xmppUri.isValidJid() && !xmppUri.hasFingerprints()) {
623            final Conversation conversation = xmppConnectionService.findUniqueConversationByJid(xmppUri);
624            if (conversation != null) {
625                if (xmppUri.getParameter("password") != null) {
626                    xmppConnectionService.providePasswordForMuc(conversation, xmppUri.getParameter("password"));
627                }
628                if (xmppUri.isAction("command")) {
629                    startCommand(conversation.getAccount(), xmppUri.getJid(), xmppUri.getParameter("node"));
630                } else {
631                    Bundle extras = new Bundle();
632                    extras.putString(Intent.EXTRA_TEXT, xmppUri.getBody());
633                    if (xmppUri.isAction("message")) extras.putString(EXTRA_POST_INIT_ACTION, "message");
634                    openConversation(conversation, extras);
635                }
636                return true;
637            }
638        }
639        return false;
640    }
641
642    public boolean onTelUriClicked(Uri uri, Account acct) {
643        final String tel;
644        try {
645            tel = PhoneNumberUtilWrapper.normalize(this, uri.getSchemeSpecificPart());
646        } catch (final IllegalArgumentException | NumberParseException | NullPointerException e) {
647            return false;
648        }
649
650        Set<String> gateways = new HashSet<>();
651        for (Account account : (acct == null ? xmppConnectionService.getAccounts() : List.of(acct))) {
652            for (Contact contact : account.getRoster().getContacts()) {
653                if (contact.getPresences().anyIdentity("gateway", "pstn") || contact.getPresences().anyIdentity("gateway", "sms")) {
654                    if (acct == null) acct = account;
655                    gateways.add(contact.getJid().asBareJid().toEscapedString());
656                }
657            }
658        }
659
660        for (String gateway : gateways) {
661            if (onXmppUriClicked(Uri.parse("xmpp:" + tel + "@" + gateway))) return true;
662        }
663
664        if (gateways.size() == 1 && acct != null) {
665            openConversation(xmppConnectionService.findOrCreateConversation(acct, Jid.ofLocalAndDomain(tel, gateways.iterator().next()), false, true), null);
666            return true;
667        }
668
669        return false;
670    }
671
672    @Override
673    public boolean onOptionsItemSelected(MenuItem item) {
674        if (MenuDoubleTabUtil.shouldIgnoreTap()) {
675            return false;
676        }
677        switch (item.getItemId()) {
678            case android.R.id.home:
679                FragmentManager fm = getFragmentManager();
680                if (android.os.Build.VERSION.SDK_INT >= 26) {
681                    Fragment f = fm.getFragments().get(fm.getFragments().size() - 1);
682                    if (f != null && f instanceof ConversationFragment) {
683                        if (((ConversationFragment) f).onBackPressed()) {
684                            return true;
685                        }
686                    }
687                }
688                if (fm.getBackStackEntryCount() > 0) {
689                    try {
690                        fm.popBackStack();
691                    } catch (IllegalStateException e) {
692                        Log.w(Config.LOGTAG, "Unable to pop back stack after pressing home button");
693                    }
694                    return true;
695                }
696                break;
697            case R.id.action_scan_qr_code:
698                UriHandlerActivity.scan(this);
699                return true;
700            case R.id.action_search_all_conversations:
701                startActivity(new Intent(this, SearchActivity.class));
702                return true;
703            case R.id.action_search_this_conversation:
704                final Conversation conversation = ConversationFragment.getConversation(this);
705                if (conversation == null) {
706                    return true;
707                }
708                final Intent intent = new Intent(this, SearchActivity.class);
709                intent.putExtra(SearchActivity.EXTRA_CONVERSATION_UUID, conversation.getUuid());
710                startActivity(intent);
711                return true;
712        }
713        return super.onOptionsItemSelected(item);
714    }
715
716    @Override
717    public boolean onKeyDown(final int keyCode, final KeyEvent keyEvent) {
718        if (keyCode == KeyEvent.KEYCODE_DPAD_UP && keyEvent.isCtrlPressed()) {
719            final ConversationFragment conversationFragment = ConversationFragment.get(this);
720            if (conversationFragment != null && conversationFragment.onArrowUpCtrlPressed()) {
721                return true;
722            }
723        }
724        return super.onKeyDown(keyCode, keyEvent);
725    }
726
727    @Override
728    public void onSaveInstanceState(final Bundle savedInstanceState) {
729        final Intent pendingIntent = pendingViewIntent.peek();
730        savedInstanceState.putParcelable("intent", pendingIntent != null ? pendingIntent : getIntent());
731        super.onSaveInstanceState(savedInstanceState);
732    }
733
734    @Override
735    public void onStart() {
736        super.onStart();
737        mRedirectInProcess.set(false);
738    }
739
740    @Override
741    protected void onNewIntent(final Intent intent) {
742        super.onNewIntent(intent);
743        if (isViewOrShareIntent(intent)) {
744            if (xmppConnectionService != null) {
745                clearPendingViewIntent();
746                processViewIntent(intent);
747            } else {
748                pendingViewIntent.push(intent);
749            }
750        }
751        setIntent(createLauncherIntent(this));
752    }
753
754    @Override
755    public void onPause() {
756        this.mActivityPaused = true;
757        super.onPause();
758    }
759
760    @Override
761    public void onResume() {
762        super.onResume();
763        this.mActivityPaused = false;
764    }
765
766    private void initializeFragments() {
767        final FragmentManager fragmentManager = getFragmentManager();
768        FragmentTransaction transaction = fragmentManager.beginTransaction();
769        final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
770        final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
771        if (mainFragment != null) {
772            if (binding.secondaryFragment != null) {
773                if (mainFragment instanceof ConversationFragment) {
774                    getFragmentManager().popBackStack();
775                    transaction.remove(mainFragment);
776                    transaction.commit();
777                    fragmentManager.executePendingTransactions();
778                    transaction = fragmentManager.beginTransaction();
779                    transaction.replace(R.id.secondary_fragment, mainFragment);
780                    transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
781                    transaction.commit();
782                    return;
783                }
784            } else {
785                if (secondaryFragment instanceof ConversationFragment) {
786                    transaction.remove(secondaryFragment);
787                    transaction.commit();
788                    getFragmentManager().executePendingTransactions();
789                    transaction = fragmentManager.beginTransaction();
790                    transaction.replace(R.id.main_fragment, secondaryFragment);
791                    transaction.addToBackStack(null);
792                    transaction.commit();
793                    return;
794                }
795            }
796        } else {
797            transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment());
798        }
799        if (binding.secondaryFragment != null && secondaryFragment == null) {
800            transaction.replace(R.id.secondary_fragment, new ConversationFragment());
801        }
802        transaction.commit();
803    }
804
805    private void invalidateActionBarTitle() {
806        final ActionBar actionBar = getSupportActionBar();
807        if (actionBar == null) {
808            return;
809        }
810        final FragmentManager fragmentManager = getFragmentManager();
811        final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
812        if (mainFragment instanceof ConversationFragment conversationFragment) {
813            final Conversation conversation = conversationFragment.getConversation();
814            if (conversation != null) {
815                actionBar.setTitle(conversation.getName());
816                actionBar.setDisplayHomeAsUpEnabled(!xmppConnectionService.isOnboarding() || !conversation.getJid().equals(Jid.of("cheogram.com")));
817                ToolbarUtils.setActionBarOnClickListener(
818                        binding.toolbar,
819                        (v) -> { if(!xmppConnectionService.isOnboarding()) openConversationDetails(conversation); }
820                );
821                return;
822            }
823        }
824        final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
825        if (secondaryFragment instanceof ConversationFragment conversationFragment) {
826            final Conversation conversation = conversationFragment.getConversation();
827            if (conversation != null) {
828                actionBar.setTitle(conversation.getName());
829            } else {
830                actionBar.setTitle(R.string.app_name);
831            }
832        } else {
833            actionBar.setTitle(R.string.app_name);
834        }
835        actionBar.setDisplayHomeAsUpEnabled(false);
836        ToolbarUtils.resetActionBarOnClickListeners(binding.toolbar);
837    }
838
839    private void openConversationDetails(final Conversation conversation) {
840        if (conversation.getMode() == Conversational.MODE_MULTI) {
841            ConferenceDetailsActivity.open(this, conversation);
842        } else {
843            final Contact contact = conversation.getContact();
844            if (contact.isSelf()) {
845                switchToAccount(conversation.getAccount());
846            } else {
847                switchToContactDetails(contact);
848            }
849        }
850    }
851
852    @Override
853    public void onConversationArchived(Conversation conversation) {
854        if (performRedirectIfNecessary(conversation, false)) {
855            return;
856        }
857        final FragmentManager fragmentManager = getFragmentManager();
858        final Fragment mainFragment = fragmentManager.findFragmentById(R.id.main_fragment);
859        if (mainFragment instanceof ConversationFragment) {
860            try {
861                fragmentManager.popBackStack();
862            } catch (final IllegalStateException e) {
863                Log.w(Config.LOGTAG, "state loss while popping back state after archiving conversation", e);
864                //this usually means activity is no longer active; meaning on the next open we will run through this again
865            }
866            return;
867        }
868        final Fragment secondaryFragment = fragmentManager.findFragmentById(R.id.secondary_fragment);
869        if (secondaryFragment instanceof ConversationFragment) {
870            if (((ConversationFragment) secondaryFragment).getConversation() == conversation) {
871                Conversation suggestion = ConversationsOverviewFragment.getSuggestion(this, conversation);
872                if (suggestion != null) {
873                    openConversation(suggestion, null);
874                }
875            }
876        }
877    }
878
879    @Override
880    public void onConversationsListItemUpdated() {
881        Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment);
882        if (fragment instanceof ConversationsOverviewFragment) {
883            ((ConversationsOverviewFragment) fragment).refresh();
884        }
885    }
886
887    @Override
888    public void switchToConversation(Conversation conversation) {
889        Log.d(Config.LOGTAG, "override");
890        openConversation(conversation, null);
891    }
892
893    @Override
894    public void onConversationRead(Conversation conversation, String upToUuid) {
895        if (!mActivityPaused && pendingViewIntent.peek() == null) {
896            xmppConnectionService.sendReadMarker(conversation, upToUuid);
897        } else {
898            Log.d(Config.LOGTAG, "ignoring read callback. mActivityPaused=" + mActivityPaused);
899        }
900    }
901
902    @Override
903    public void onAccountUpdate() {
904        this.refreshUi();
905    }
906
907    @Override
908    public void onConversationUpdate(boolean newCaps) {
909        if (performRedirectIfNecessary(false)) {
910            return;
911        }
912        refreshForNewCaps = newCaps;
913        this.refreshUi();
914    }
915
916    @Override
917    public void onRosterUpdate(final XmppConnectionService.UpdateRosterReason reason, final Contact contact) {
918        if (reason != XmppConnectionService.UpdateRosterReason.AVATAR) {
919            refreshForNewCaps = true;
920            if (contact != null) newCapsJids.add(contact.getJid().asBareJid());
921        }
922        this.refreshUi();
923    }
924
925    @Override
926    public void OnUpdateBlocklist(OnUpdateBlocklist.Status status) {
927        this.refreshUi();
928    }
929
930    @Override
931    public void onShowErrorToast(int resId) {
932        runOnUiThread(() -> Toast.makeText(this, resId, Toast.LENGTH_SHORT).show());
933    }
934}