/* * Copyright (c) 2018, Daniel Gultsch All rights reserved. * * Redistribution and use in source and binary forms, with or without modification, * are permitted provided that the following conditions are met: * * 1. Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation and/or * other materials provided with the distribution. * * 3. Neither the name of the copyright holder nor the names of its contributors * may be used to endorse or promote products derived from this software without * specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package de.pixart.messenger.ui; import android.annotation.SuppressLint; import android.app.Activity; import android.app.AlertDialog; import android.app.Fragment; import android.app.FragmentManager; import android.app.FragmentTransaction; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.pm.PackageManager; import android.databinding.DataBindingUtil; import android.graphics.Typeface; import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.preference.PreferenceManager; import android.provider.Settings; import android.support.annotation.IdRes; import android.support.annotation.NonNull; import android.support.v7.app.ActionBar; import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.widget.PopupMenu; import android.widget.TextView; import android.widget.Toast; import net.java.otr4j.session.SessionStatus; import org.openintents.openpgp.util.OpenPgpApi; import java.util.Arrays; import java.util.List; import java.util.concurrent.atomic.AtomicBoolean; import de.pixart.messenger.Config; import de.pixart.messenger.R; import de.pixart.messenger.crypto.OmemoSetting; import de.pixart.messenger.databinding.ActivityConversationsBinding; import de.pixart.messenger.entities.Account; import de.pixart.messenger.entities.Conversation; import de.pixart.messenger.entities.MucOptions; import de.pixart.messenger.entities.Presence; import de.pixart.messenger.services.XmppConnectionService; import de.pixart.messenger.ui.interfaces.OnBackendConnected; import de.pixart.messenger.ui.interfaces.OnConversationArchived; import de.pixart.messenger.ui.interfaces.OnConversationRead; import de.pixart.messenger.ui.interfaces.OnConversationSelected; import de.pixart.messenger.ui.interfaces.OnConversationsListItemUpdated; import de.pixart.messenger.ui.util.ActivityResult; import de.pixart.messenger.ui.util.ConversationMenuConfigurator; import de.pixart.messenger.ui.util.PendingItem; import de.pixart.messenger.utils.EmojiWrapper; import de.pixart.messenger.utils.ExceptionHelper; import de.pixart.messenger.utils.MenuDoubleTabUtil; import de.pixart.messenger.utils.SignupUtils; import de.pixart.messenger.utils.UIHelper; import de.pixart.messenger.utils.XmppUri; import de.pixart.messenger.xmpp.OnUpdateBlocklist; import de.pixart.messenger.xmpp.chatstate.ChatState; import rocks.xmpp.addr.Jid; import static de.pixart.messenger.ui.ConversationFragment.REQUEST_DECRYPT_PGP; import static de.pixart.messenger.ui.SettingsActivity.USE_INTERNAL_UPDATER; public class ConversationsActivity extends XmppActivity implements OnConversationSelected, OnConversationArchived, OnConversationsListItemUpdated, OnConversationRead, XmppConnectionService.OnAccountUpdate, XmppConnectionService.OnConversationUpdate, XmppConnectionService.OnRosterUpdate, OnUpdateBlocklist, XmppConnectionService.OnShowErrorToast, XmppConnectionService.OnAffiliationChanged, XmppConnectionService.OnRoomDestroy { public static final String ACTION_VIEW_CONVERSATION = "de.pixart.messenger.VIEW"; public static final String EXTRA_CONVERSATION = "conversationUuid"; public static final String EXTRA_DOWNLOAD_UUID = "de.pixart.messenger.download_uuid"; public static final String EXTRA_AS_QUOTE = "as_quote"; public static final String EXTRA_NICK = "nick"; public static final String EXTRA_IS_PRIVATE_MESSAGE = "pm"; public static final String EXTRA_DO_NOT_APPEND = "do_not_append"; public static final String ACTION_DESTROY_MUC = "de.pixart.messenger.DESTROY_MUC"; public static final int REQUEST_OPEN_MESSAGE = 0x9876; public static final int REQUEST_PLAY_PAUSE = 0x5432; private static List VIEW_AND_SHARE_ACTIONS = Arrays.asList( ACTION_VIEW_CONVERSATION, Intent.ACTION_SEND, Intent.ACTION_SEND_MULTIPLE ); private boolean showLastSeen = false; long FirstStartTime = -1; String PREF_FIRST_START = "FirstStart"; //secondary fragment (when holding the conversation, must be initialized before refreshing the overview fragment private static final @IdRes int[] FRAGMENT_ID_NOTIFICATION_ORDER = {R.id.secondary_fragment, R.id.main_fragment}; private final PendingItem pendingViewIntent = new PendingItem<>(); private final PendingItem postponedActivityResult = new PendingItem<>(); private ActivityConversationsBinding binding; private boolean mActivityPaused = true; private AtomicBoolean mRedirectInProcess = new AtomicBoolean(false); private static boolean isViewOrShareIntent(Intent i) { Log.d(Config.LOGTAG, "action: " + (i == null ? null : i.getAction())); return i != null && VIEW_AND_SHARE_ACTIONS.contains(i.getAction()) && i.hasExtra(EXTRA_CONVERSATION); } private static Intent createLauncherIntent(Context context) { final Intent intent = new Intent(context, ConversationsActivity.class); intent.setAction(Intent.ACTION_MAIN); intent.addCategory(Intent.CATEGORY_LAUNCHER); return intent; } @Override protected void refreshUiReal() { invalidateActionBarTitle(); for(@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) { refreshFragment(id); } } @Override void onBackendConnected() { if (performRedirectIfNecessary(true)) { return; } Log.d(Config.LOGTAG, "ConversationsActivity onBackendConnected(): setIsInForeground = true"); xmppConnectionService.getNotificationService().setIsInForeground(true); final Intent FirstStartIntent = getIntent(); final Bundle extras = FirstStartIntent.getExtras(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { if (extras != null && extras.containsKey(PREF_FIRST_START)) { FirstStartTime = extras.getLong(PREF_FIRST_START); Log.d(Config.LOGTAG, "Get first start time from StartUI: " + FirstStartTime); } } else { FirstStartTime = System.currentTimeMillis(); Log.d(Config.LOGTAG, "Device is running Android < SDK 23, no restart required: " + FirstStartTime); } Intent intent = pendingViewIntent.pop(); if (intent != null) { if (processViewIntent(intent)) { if (binding.secondaryFragment != null) { notifyFragmentOfBackendConnected(R.id.main_fragment); } return; } } if (FirstStartTime == 0) { Log.d(Config.LOGTAG, "First start time: " + FirstStartTime + ", restarting App"); //write first start timestamp to file FirstStartTime = System.currentTimeMillis(); SharedPreferences FirstStart = getApplicationContext().getSharedPreferences(PREF_FIRST_START, Context.MODE_PRIVATE); SharedPreferences.Editor editor = FirstStart.edit(); editor.putLong(PREF_FIRST_START, FirstStartTime); editor.commit(); // restart Intent restartintent = getBaseContext().getPackageManager().getLaunchIntentForPackage(getBaseContext().getPackageName()); restartintent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); restartintent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); startActivity(restartintent); overridePendingTransition(R.animator.fade_in, R.animator.fade_out); System.exit(0); } if (useInternalUpdater()) { if (xmppConnectionService.getAccounts().size() != 0) { if (xmppConnectionService.hasInternetConnection()) { if (xmppConnectionService.isWIFI() || (xmppConnectionService.isMobile() && !xmppConnectionService.isMobileRoaming())) { AppUpdate(xmppConnectionService.installedFrom()); } } } } for (@IdRes int id : FRAGMENT_ID_NOTIFICATION_ORDER) { notifyFragmentOfBackendConnected(id); } ActivityResult activityResult = postponedActivityResult.pop(); if (activityResult != null) { handleActivityResult(activityResult); } if (binding.secondaryFragment != null && ConversationFragment.getConversation(this) == null) { Conversation conversation = ConversationsOverviewFragment.getSuggestion(this); if (conversation != null) { openConversation(conversation, null); } } invalidateActionBarTitle(); showDialogsIfMainIsOverview(); } private boolean performRedirectIfNecessary(boolean noAnimation) { return performRedirectIfNecessary(null, noAnimation); } private boolean performRedirectIfNecessary(final Conversation ignore, final boolean noAnimation) { if (xmppConnectionService == null) { return false; } boolean isConversationsListEmpty = xmppConnectionService.isConversationsListEmpty(ignore); if (isConversationsListEmpty && mRedirectInProcess.compareAndSet(false, true)) { final Intent intent = SignupUtils.getRedirectionIntent(this); if (noAnimation) { intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); } runOnUiThread(() -> { startActivity(intent); overridePendingTransition(R.animator.fade_in, R.animator.fade_out); if (noAnimation) { overridePendingTransition(0, 0); } }); } return mRedirectInProcess.get(); } private void showDialogsIfMainIsOverview() { if (xmppConnectionService == null) { return; } final Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment); if (fragment instanceof ConversationsOverviewFragment) { if (ExceptionHelper.checkForCrash(this)) { return; } openBatteryOptimizationDialogIfNeeded(); } } private String getBatteryOptimizationPreferenceKey() { @SuppressLint("HardwareIds") String device = Settings.Secure.getString(getContentResolver(), Settings.Secure.ANDROID_ID); return "show_battery_optimization" + (device == null ? "" : device); } private void setNeverAskForBatteryOptimizationsAgain() { getPreferences().edit().putBoolean(getBatteryOptimizationPreferenceKey(), false).apply(); } private void openBatteryOptimizationDialogIfNeeded() { if (hasAccountWithoutPush() && isOptimizingBattery() && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M && getPreferences().getBoolean(getBatteryOptimizationPreferenceKey(), true)) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.battery_optimizations_enabled); builder.setMessage(R.string.battery_optimizations_enabled_dialog); builder.setPositiveButton(R.string.next, (dialog, which) -> { Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS); Uri uri = Uri.parse("package:" + getPackageName()); intent.setData(uri); try { startActivityForResult(intent, REQUEST_BATTERY_OP); } catch (ActivityNotFoundException e) { Toast.makeText(this, R.string.device_does_not_support_battery_op, Toast.LENGTH_SHORT).show(); } }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { builder.setOnDismissListener(dialog -> setNeverAskForBatteryOptimizationsAgain()); } final AlertDialog dialog = builder.create(); dialog.setCanceledOnTouchOutside(false); dialog.show(); } } private boolean hasAccountWithoutPush() { for (Account account : xmppConnectionService.getAccounts()) { if (account.getStatus() == Account.State.ONLINE && !xmppConnectionService.getPushManagementService().available(account)) { return true; } } return false; } private void notifyFragmentOfBackendConnected(@IdRes int id) { final Fragment fragment = getFragmentManager().findFragmentById(id); if (fragment instanceof OnBackendConnected) { ((OnBackendConnected) fragment).onBackendConnected(); } } private void refreshFragment(@IdRes int id) { final Fragment fragment = getFragmentManager().findFragmentById(id); if (fragment instanceof XmppFragment) { ((XmppFragment) fragment).refresh(); } } private boolean processViewIntent(Intent intent) { Log.d(Config.LOGTAG,"process view intent"); String uuid = intent.getStringExtra(EXTRA_CONVERSATION); Conversation conversation = uuid != null ? xmppConnectionService.findConversationByUuid(uuid) : null; if (conversation == null) { Log.d(Config.LOGTAG, "unable to view conversation with uuid:" + uuid); return false; } openConversation(conversation, intent.getExtras()); return true; } @Override public void onRequestPermissionsResult(int requestCode, @NonNull String permissions[], @NonNull int[] grantResults) { UriHandlerActivity.onRequestPermissionResult(this, requestCode, grantResults); if (grantResults.length > 0) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { switch (requestCode) { case REQUEST_OPEN_MESSAGE: refreshUiReal(); ConversationFragment.openPendingMessage(this); break; case REQUEST_PLAY_PAUSE: ConversationFragment.startStopPending(this); break; } } } } @Override public void onActivityResult(int requestCode, int resultCode, final Intent data) { super.onActivityResult(requestCode, resultCode, data); ActivityResult activityResult = ActivityResult.of(requestCode, resultCode, data); if (xmppConnectionService != null) { handleActivityResult(activityResult); } else { this.postponedActivityResult.push(activityResult); } } private void handleActivityResult(ActivityResult activityResult) { if (activityResult.resultCode == Activity.RESULT_OK) { handlePositiveActivityResult(activityResult.requestCode, activityResult.data); } else { handleNegativeActivityResult(activityResult.requestCode); } } private void handleNegativeActivityResult(int requestCode) { Conversation conversation = ConversationFragment.getConversationReliable(this); switch (requestCode) { case REQUEST_DECRYPT_PGP: if (conversation == null) { break; } conversation.getAccount().getPgpDecryptionService().giveUpCurrentDecryption(); break; case REQUEST_BATTERY_OP: setNeverAskForBatteryOptimizationsAgain(); break; } } private void handlePositiveActivityResult(int requestCode, final Intent data) { Log.d(Config.LOGTAG, "positive activity result"); Conversation conversation = ConversationFragment.getConversationReliable(this); if (conversation == null) { Log.d(Config.LOGTAG, "conversation not found"); return; } switch (requestCode) { case REQUEST_DECRYPT_PGP: conversation.getAccount().getPgpDecryptionService().continueDecryption(data); break; case REQUEST_CHOOSE_PGP_ID: long id = data.getLongExtra(OpenPgpApi.EXTRA_SIGN_KEY_ID, 0); if (id != 0) { conversation.getAccount().setPgpSignId(id); announcePgp(conversation.getAccount(), null, null, onOpenPGPKeyPublished); } else { choosePgpSignId(conversation.getAccount()); } break; case REQUEST_ANNOUNCE_PGP: announcePgp(conversation.getAccount(), conversation, data, onOpenPGPKeyPublished); break; } } @Override protected void onCreate(final Bundle savedInstanceState) { super.onCreate(savedInstanceState); ConversationMenuConfigurator.reloadFeatures(this); OmemoSetting.load(this); this.binding = DataBindingUtil.setContentView(this, R.layout.activity_conversations); setSupportActionBar((Toolbar) binding.toolbar); configureActionBar(getSupportActionBar()); this.getFragmentManager().addOnBackStackChangedListener(this::invalidateActionBarTitle); this.getFragmentManager().addOnBackStackChangedListener(this::showDialogsIfMainIsOverview); this.initializeFragments(); this.invalidateActionBarTitle(); final Intent intent; if (savedInstanceState == null) { intent = getIntent(); } else { intent = savedInstanceState.getParcelable("intent"); } if (isViewOrShareIntent(intent)) { pendingViewIntent.push(intent); setIntent(createLauncherIntent(this)); } } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.activity_conversations, menu); final MenuItem qrCodeScanMenuItem = menu.findItem(R.id.action_scan_qr_code); final MenuItem menuEditProfiles = menu.findItem(R.id.action_accounts); final MenuItem inviteUser = menu.findItem(R.id.action_invite_user); if (qrCodeScanMenuItem != null) { if (isCameraFeatureAvailable()) { Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment); boolean visible = getResources().getBoolean(R.bool.show_qr_code_scan) && fragment != null && fragment instanceof ConversationsOverviewFragment; qrCodeScanMenuItem.setVisible(visible); } else { qrCodeScanMenuItem.setVisible(false); } } if (xmppConnectionServiceBound && xmppConnectionService.getAccounts().size() == 1 && !xmppConnectionService.multipleAccounts()) { menuEditProfiles.setTitle(R.string.action_account); } else { menuEditProfiles.setTitle(R.string.action_accounts); } if (xmppConnectionServiceBound && xmppConnectionService.getAccounts().size() > 0) { inviteUser.setVisible(true); } else { inviteUser.setVisible(false); } return super.onCreateOptionsMenu(menu); } @Override public void onConversationSelected(Conversation conversation) { clearPendingViewIntent(); if (ConversationFragment.getConversation(this) == conversation) { Log.d(Config.LOGTAG, "ignore onConversationSelected() because conversation is already open"); return; } openConversation(conversation, null); } public void clearPendingViewIntent() { if (pendingViewIntent.clear()) { Log.e(Config.LOGTAG, "cleared pending view intent"); } } private void displayToast(final String msg) { runOnUiThread(() -> Toast.makeText(ConversationsActivity.this, msg, Toast.LENGTH_SHORT).show()); } @Override public void onAffiliationChangedSuccessful(Jid jid) { } @Override public void onAffiliationChangeFailed(Jid jid, int resId) { displayToast(getString(resId, jid.asBareJid().toString())); } private void openConversation(Conversation conversation, Bundle extras) { ConversationFragment conversationFragment = (ConversationFragment) getFragmentManager().findFragmentById(R.id.secondary_fragment); final boolean mainNeedsRefresh; if (conversationFragment == null) { mainNeedsRefresh = false; Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment); if (mainFragment instanceof ConversationFragment) { conversationFragment = (ConversationFragment) mainFragment; } else { conversationFragment = new ConversationFragment(); FragmentTransaction fragmentTransaction = getFragmentManager().beginTransaction(); fragmentTransaction.setCustomAnimations( R.animator.fade_right_in, R.animator.fade_right_out, R.animator.fade_right_in, R.animator.fade_right_out ); fragmentTransaction.replace(R.id.main_fragment, conversationFragment); fragmentTransaction.addToBackStack(null); try { fragmentTransaction.commit(); } catch (IllegalStateException e) { Log.w(Config.LOGTAG, "sate loss while opening conversation", e); //allowing state loss is probably fine since view intents et all are already stored and a click can probably be 'ignored' return; } } } else { mainNeedsRefresh = true; } conversationFragment.reInit(conversation, extras == null ? new Bundle() : extras); if (mainNeedsRefresh) { refreshFragment(R.id.main_fragment); } else { invalidateActionBarTitle(); } } public boolean onXmppUriClicked(Uri uri) { XmppUri xmppUri = new XmppUri(uri); if (xmppUri.isJidValid() && !xmppUri.hasFingerprints()) { final Conversation conversation = xmppConnectionService.findUniqueConversationByJid(xmppUri); if (conversation != null) { openConversation(conversation, null); return true; } } return false; } @Override public boolean onOptionsItemSelected(MenuItem item) { if (MenuDoubleTabUtil.shouldIgnoreTap()) { return false; } switch (item.getItemId()) { case android.R.id.home: FragmentManager fm = getFragmentManager(); if (fm.getBackStackEntryCount() > 0) { try { fm.popBackStack(); } catch (IllegalStateException e) { Log.w(Config.LOGTAG, "Unable to pop back stack after pressing home button"); } return true; } break; case R.id.action_scan_qr_code: UriHandlerActivity.scan(this); return true; case R.id.action_check_updates: if (xmppConnectionService.hasInternetConnection()) { openInstallFromUnknownSourcesDialogIfNeeded(true); } else { Toast.makeText(this, R.string.account_status_no_internet, Toast.LENGTH_LONG).show(); } break; case R.id.action_invite_user: inviteUser(); break; } return super.onOptionsItemSelected(item); } @Override public void onSaveInstanceState(Bundle savedInstanceState) { Intent pendingIntent = pendingViewIntent.peek(); savedInstanceState.putParcelable("intent", pendingIntent != null ? pendingIntent : getIntent()); super.onSaveInstanceState(savedInstanceState); } @Override protected void onStart() { final int theme = findTheme(); if (this.mTheme != theme) { this.mSkipBackgroundBinding = true; recreate(); } else { this.mSkipBackgroundBinding = false; } mRedirectInProcess.set(false); final SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this); this.showLastSeen = preferences.getBoolean("last_activity", false); super.onStart(); } @Override protected void onNewIntent(final Intent intent) { if (isViewOrShareIntent(intent)) { if (xmppConnectionService != null) { clearPendingViewIntent(); processViewIntent(intent); } else { pendingViewIntent.push(intent); } } else if (intent != null && ACTION_DESTROY_MUC.equals(intent.getAction())) { final Bundle extras = intent.getExtras(); if (extras != null && extras.containsKey("MUC_UUID")) { Log.d(Config.LOGTAG, "Get " + intent.getAction() + " intent for " + extras.getString("MUC_UUID")); Conversation conversation = xmppConnectionService.findConversationByUuid(extras.getString("MUC_UUID")); ConversationsActivity.this.xmppConnectionService.clearConversationHistory(conversation); xmppConnectionService.destroyRoom(conversation, ConversationsActivity.this); endConversation(conversation); } } setIntent(createLauncherIntent(this)); } public void endConversation(Conversation conversation) { xmppConnectionService.archiveConversation(conversation); onConversationArchived(conversation); } @Override public void onPause() { this.mActivityPaused = true; super.onPause(); } @Override public void onResume() { super.onResume(); invalidateActionBarTitle(); this.mActivityPaused = false; } private void initializeFragments() { FragmentTransaction transaction = getFragmentManager().beginTransaction(); Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment); Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment); if (mainFragment != null) { Log.d(Config.LOGTAG, "initializeFragment(). main fragment exists"); if (binding.secondaryFragment != null) { if (mainFragment instanceof ConversationFragment) { Log.d(Config.LOGTAG, "gained secondary fragment. moving..."); getFragmentManager().popBackStack(); transaction.remove(mainFragment); transaction.commit(); getFragmentManager().executePendingTransactions(); transaction = getFragmentManager().beginTransaction(); transaction.replace(R.id.secondary_fragment, mainFragment); transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment()); transaction.commit(); return; } } else { if (secondaryFragment instanceof ConversationFragment) { Log.d(Config.LOGTAG, "lost secondary fragment. moving..."); transaction.remove(secondaryFragment); transaction.commit(); getFragmentManager().executePendingTransactions(); transaction = getFragmentManager().beginTransaction(); transaction.replace(R.id.main_fragment, secondaryFragment); transaction.addToBackStack(null); transaction.commit(); return; } } } else { transaction.replace(R.id.main_fragment, new ConversationsOverviewFragment()); } if (binding.secondaryFragment != null && secondaryFragment == null) { transaction.replace(R.id.secondary_fragment, new ConversationFragment()); } transaction.commit(); } private void invalidateActionBarTitle() { final ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment); if (mainFragment instanceof ConversationFragment) { final Conversation conversation = ((ConversationFragment) mainFragment).getConversation(); if (conversation != null) { actionBar.setDisplayHomeAsUpEnabled(true); View view = getLayoutInflater().inflate(R.layout.ab_title, null); getSupportActionBar().setCustomView(view); actionBar.setDisplayShowTitleEnabled(false); actionBar.setDisplayShowCustomEnabled(true); TextView abtitle = findViewById(android.R.id.text1); TextView absubtitle = findViewById(android.R.id.text2); abtitle.setText(EmojiWrapper.transform(conversation.getName())); abtitle.setOnClickListener(view1 -> { if (conversation.getMode() == Conversation.MODE_SINGLE) { switchToContactDetails(conversation.getContact()); } else if (conversation.getMode() == Conversation.MODE_MULTI) { Intent intent = new Intent(ConversationsActivity.this, ConferenceDetailsActivity.class); intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); intent.putExtra("uuid", conversation.getUuid()); startActivity(intent); overridePendingTransition(R.animator.fade_in, R.animator.fade_out); } }); abtitle.setSelected(true); if (conversation.getMode() == Conversation.MODE_SINGLE && !conversation.withSelf()) { ChatState state = conversation.getIncomingChatState(); if (conversation.getContact().getShownStatus() == Presence.Status.OFFLINE) { absubtitle.setText(getString(R.string.account_status_offline)); absubtitle.setSelected(true); absubtitle.setOnClickListener(view12 -> { if (conversation.getMode() == Conversation.MODE_SINGLE) { switchToContactDetails(conversation.getContact()); } else if (conversation.getMode() == Conversation.MODE_MULTI) { Intent intent = new Intent(ConversationsActivity.this, ConferenceDetailsActivity.class); intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); intent.putExtra("uuid", conversation.getUuid()); startActivity(intent); overridePendingTransition(R.animator.fade_in, R.animator.fade_out); } }); } else { if (state == ChatState.COMPOSING) { absubtitle.setText(getString(R.string.is_typing)); absubtitle.setTypeface(null, Typeface.BOLD_ITALIC); absubtitle.setSelected(true); absubtitle.setOnClickListener(view13 -> { if (conversation.getMode() == Conversation.MODE_SINGLE) { switchToContactDetails(conversation.getContact()); } else if (conversation.getMode() == Conversation.MODE_MULTI) { Intent intent = new Intent(ConversationsActivity.this, ConferenceDetailsActivity.class); intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); intent.putExtra("uuid", conversation.getUuid()); startActivity(intent); overridePendingTransition(R.animator.fade_in, R.animator.fade_out); } }); } else { if (showLastSeen && conversation.getContact().getLastseen() > 0) { absubtitle.setText(UIHelper.lastseen(getApplicationContext(), conversation.getContact().isActive(), conversation.getContact().getLastseen())); } else { absubtitle.setText(getString(R.string.account_status_online)); } absubtitle.setSelected(true); absubtitle.setOnClickListener(view14 -> { if (conversation.getMode() == Conversation.MODE_SINGLE) { switchToContactDetails(conversation.getContact()); } else if (conversation.getMode() == Conversation.MODE_MULTI) { Intent intent = new Intent(ConversationsActivity.this, ConferenceDetailsActivity.class); intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); intent.putExtra("uuid", conversation.getUuid()); startActivity(intent); overridePendingTransition(R.animator.fade_in, R.animator.fade_out); } }); } } } else { ChatState state = ChatState.COMPOSING; List userWithChatStates = conversation.getMucOptions().getUsersWithChatState(state, 5); if (userWithChatStates.size() == 0) { state = ChatState.PAUSED; userWithChatStates = conversation.getMucOptions().getUsersWithChatState(state, 5); } List users = conversation.getMucOptions().getUsers(true); if (state == ChatState.COMPOSING) { if (userWithChatStates.size() > 0) { if (userWithChatStates.size() == 1) { MucOptions.User user = userWithChatStates.get(0); absubtitle.setText(EmojiWrapper.transform(getString(R.string.contact_is_typing, UIHelper.getDisplayName(user)))); } else { StringBuilder builder = new StringBuilder(); for (MucOptions.User user : userWithChatStates) { if (builder.length() != 0) { builder.append(", "); } builder.append(UIHelper.getDisplayName(user)); } absubtitle.setText(EmojiWrapper.transform(getString(R.string.contacts_are_typing, builder.toString()))); } } } else { if (users.size() == 0) { absubtitle.setText(getString(R.string.one_participant)); } else { int size = users.size() + 1; absubtitle.setText(getString(R.string.more_participants, size)); } } absubtitle.setSelected(true); absubtitle.setOnClickListener(view15 -> { if (conversation.getMode() == Conversation.MODE_SINGLE) { switchToContactDetails(conversation.getContact()); } else if (conversation.getMode() == Conversation.MODE_MULTI) { Intent intent = new Intent(ConversationsActivity.this, ConferenceDetailsActivity.class); intent.setAction(ConferenceDetailsActivity.ACTION_VIEW_MUC); intent.putExtra("uuid", conversation.getUuid()); startActivity(intent); overridePendingTransition(R.animator.fade_in, R.animator.fade_out); } }); } return; } } actionBar.setDisplayShowTitleEnabled(true); actionBar.setDisplayShowCustomEnabled(false); actionBar.setTitle(R.string.app_name); actionBar.setSubtitle(null); actionBar.setDisplayHomeAsUpEnabled(false); } } public void verifyOtrSessionDialog(final Conversation conversation, View view) { if (!conversation.hasValidOtrSession() || conversation.getOtrSession().getSessionStatus() != SessionStatus.ENCRYPTED) { Toast.makeText(this, R.string.otr_session_not_started, Toast.LENGTH_LONG).show(); return; } if (view == null) { return; } PopupMenu popup = new PopupMenu(this, view); popup.inflate(R.menu.verification_choices); popup.setOnMenuItemClickListener(menuItem -> { Intent intent = new Intent(ConversationsActivity.this, VerifyOTRActivity.class); intent.setAction(VerifyOTRActivity.ACTION_VERIFY_CONTACT); intent.putExtra("contact", conversation.getContact().getJid().asBareJid().toString()); intent.putExtra(EXTRA_ACCOUNT, conversation.getAccount().getJid().asBareJid().toString()); switch (menuItem.getItemId()) { case R.id.ask_question: intent.putExtra("mode", VerifyOTRActivity.MODE_ASK_QUESTION); break; } startActivity(intent); overridePendingTransition(R.animator.fade_in, R.animator.fade_out); return true; }); popup.show(); } @Override public void onConversationArchived(Conversation conversation) { if (performRedirectIfNecessary(conversation, false)) { return; } Fragment mainFragment = getFragmentManager().findFragmentById(R.id.main_fragment); if (mainFragment instanceof ConversationFragment) { try { getFragmentManager().popBackStack(); } catch (IllegalStateException e) { Log.w(Config.LOGTAG, "state loss while popping back state after archiving conversation", e); //this usually means activity is no longer active; meaning on the next open we will run through this again } return; } Fragment secondaryFragment = getFragmentManager().findFragmentById(R.id.secondary_fragment); if (secondaryFragment instanceof ConversationFragment) { if (((ConversationFragment) secondaryFragment).getConversation() == conversation) { Conversation suggestion = ConversationsOverviewFragment.getSuggestion(this, conversation); if (suggestion != null) { openConversation(suggestion, null); } } } } @Override public void onConversationsListItemUpdated() { Fragment fragment = getFragmentManager().findFragmentById(R.id.main_fragment); if (fragment instanceof ConversationsOverviewFragment) { ((ConversationsOverviewFragment) fragment).refresh(); } } @Override public void switchToConversation(Conversation conversation) { Log.d(Config.LOGTAG, "override"); openConversation(conversation, null); } @Override public void onConversationRead(Conversation conversation, String upToUuid) { if (!mActivityPaused && pendingViewIntent.peek() == null) { xmppConnectionService.sendReadMarker(conversation, upToUuid); } else { Log.d(Config.LOGTAG, "ignoring read callback. mActivityPaused=" + Boolean.toString(mActivityPaused)); } } @Override public void onAccountUpdate() { this.refreshUi(); } @Override public void onConversationUpdate() { if (performRedirectIfNecessary(false)) { return; } this.refreshUi(); } @Override public void onRosterUpdate() { this.refreshUi(); } @Override public void OnUpdateBlocklist(OnUpdateBlocklist.Status status) { this.refreshUi(); } @Override public void onShowErrorToast(int resId) { runOnUiThread(() -> Toast.makeText(this, resId, Toast.LENGTH_SHORT).show()); } protected void AppUpdate(String Store) { String PREFS_NAME = "UpdateTimeStamp"; SharedPreferences UpdateTimeStamp = getApplicationContext().getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); long lastUpdateTime = UpdateTimeStamp.getLong("lastUpdateTime", 0); Log.d(Config.LOGTAG, "AppUpdater: LastUpdateTime: " + lastUpdateTime); if ((lastUpdateTime + (Config.UPDATE_CHECK_TIMER * 1000)) < System.currentTimeMillis()) { lastUpdateTime = System.currentTimeMillis(); SharedPreferences.Editor editor = UpdateTimeStamp.edit(); editor.putLong("lastUpdateTime", lastUpdateTime); editor.apply(); Log.d(Config.LOGTAG, "AppUpdater: CurrentTime: " + lastUpdateTime); if (Store == null) { Log.d(Config.LOGTAG, "AppUpdater started"); openInstallFromUnknownSourcesDialogIfNeeded(false); } } else { Log.d(Config.LOGTAG, "AppUpdater stopped"); } } @Override public void onRoomDestroySucceeded() { Conversation conversation = ConversationFragment.getConversationReliable(this); final boolean groupChat = conversation != null && conversation.isPrivateAndNonAnonymous(); displayToast(getString(groupChat ? R.string.destroy_room_succeed : R.string.destroy_channel_succeed)); } @Override public void onRoomDestroyFailed() { Conversation conversation = ConversationFragment.getConversationReliable(this); final boolean groupChat = conversation != null && conversation.isPrivateAndNonAnonymous(); displayToast(getString(groupChat ? R.string.destroy_room_failed : R.string.destroy_channel_failed)); } }