content: package me.tagavari.airmessage.activity;
  
  import android.app.Application;
  import android.app.NotificationManager;
  import android.content.ActivityNotFoundException;
  import android.content.Context;
  import android.content.Intent;
  import android.content.pm.PackageManager;
  import android.net.Uri;
  import android.os.Build;
  import android.os.Bundle;
  import android.os.Handler;
  import android.os.Looper;
  import android.provider.Settings;
  import android.service.notification.StatusBarNotification;
  import android.text.Editable;
  import android.text.Spannable;
  import android.text.SpannableString;
  import android.text.TextWatcher;
  import android.text.style.ForegroundColorSpan;
  import android.text.style.TypefaceSpan;
  import android.view.*;
  import android.view.inputmethod.InputMethodManager;
  import android.widget.*;
  import androidx.annotation.NonNull;
  import androidx.annotation.Nullable;
  import androidx.appcompat.view.ActionMode;
  import androidx.appcompat.widget.Toolbar;
  import androidx.lifecycle.AndroidViewModel;
  import androidx.lifecycle.MutableLiveData;
  import androidx.lifecycle.ViewModel;
  import androidx.lifecycle.ViewModelProvider;
  import androidx.recyclerview.widget.LinearLayoutManager;
  import androidx.recyclerview.widget.RecyclerView;
  import com.google.android.material.appbar.AppBarLayout;
  import com.google.android.material.dialog.MaterialAlertDialogBuilder;
  import com.google.android.material.floatingactionbutton.FloatingActionButton;
  import com.google.android.material.progressindicator.LinearProgressIndicator;
  import com.google.android.material.snackbar.Snackbar;
  import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
  import io.reactivex.rxjava3.core.Observable;
  import io.reactivex.rxjava3.core.Single;
  import io.reactivex.rxjava3.core.SingleEmitter;
  import io.reactivex.rxjava3.disposables.CompositeDisposable;
  import io.reactivex.rxjava3.disposables.Disposable;
  import io.reactivex.rxjava3.schedulers.Schedulers;
  import kotlin.Pair;
  import me.tagavari.airmessage.BuildConfig;
  import me.tagavari.airmessage.MainApplication;
  import me.tagavari.airmessage.R;
  import me.tagavari.airmessage.composite.AppCompatCompositeActivity;
  import me.tagavari.airmessage.compositeplugin.*;
  import me.tagavari.airmessage.connection.ConnectionManager;
  import me.tagavari.airmessage.constants.ColorConstants;
  import me.tagavari.airmessage.constants.ExternalLinkConstants;
  import me.tagavari.airmessage.constants.VersionConstants;
  import me.tagavari.airmessage.data.DatabaseManager;
  import me.tagavari.airmessage.data.SharedPreferencesManager;
  import me.tagavari.airmessage.enums.ConnectionErrorCode;
  import me.tagavari.airmessage.enums.ConnectionState;
  import me.tagavari.airmessage.enums.ProxyType;
  import me.tagavari.airmessage.enums.ServiceHandler;
  import me.tagavari.airmessage.flavor.PlaySecurityBridge;
  import me.tagavari.airmessage.fragment.FragmentSync;
  import me.tagavari.airmessage.helper.*;
  import me.tagavari.airmessage.messaging.*;
  import me.tagavari.airmessage.messaging.viewbinder.VBConversation;
  import me.tagavari.airmessage.messaging.viewholder.VHConversationDetailed;
  import me.tagavari.airmessage.redux.*;
  import me.tagavari.airmessage.task.ConversationActionTask;
  import me.tagavari.airmessage.util.DisposableViewHolder;
  import me.tagavari.airmessage.util.ReplaceInsertResult;
  import me.tagavari.airmessage.util.ServerUpdateData;
  import me.tagavari.airmessage.util.TransferredConversation;
  
  import java.lang.ref.WeakReference;
  import java.util.*;
  import java.util.function.ObjIntConsumer;
  import java.util.regex.Pattern;
  import java.util.stream.Collectors;
  import java.util.stream.IntStream;
  import java.util.stream.Stream;
  
  public class Conversations extends AppCompatCompositeActivity {
  	//Creating the constants
  	public static final String intentExtraArchived = "archived";
  	
  	private static final String savedInstanceStateArchived = "archived";
  	
  	private static final int permissionRequestContacts = 0;
  	private static final String keyFragmentSync = "fragment_sync";
  	
  	private static final int activityResultPlayServices = 0;
  	
  	private static final int conversationPayloadPreview = 0;
  	private static final int conversationPayloadTitle = 1;
  	private static final int conversationPayloadMember = 2;
  	private static final int conversationPayloadMuted = 3;
  	private static final int conversationPayloadUnread = 4;
  	private static final int conversationPayloadSelection = 5;
  	
  	private static final long timeUpdateHandlerDelay = 60 * 1000; //1 minute
  	
  	//Creating the static values
  	private static final List<WeakReference<Conversations>> foregroundActivities = new ArrayList<>();
  	
  	//Creating the parameter values
  	private boolean isViewArchived;
  	
  	//Creating the plugin values
  	private final PluginMessageBar pluginMessageBar;
  	private final PluginRXDisposable pluginRXD;
  	private final PluginConnectionService pluginCS;
  	
  	//Creating the view model and info bar values
  	private ActivityViewModel viewModel;
  	private PluginMessageBar.InfoBar infoBarConnection, infoBarContacts, infoBarServerUpdate, infoBarServerUpdateRequired, infoBarSecurityUpdate;
  	
  	//Creating the menu values
  	private MenuItem menuItemMarkAllRead = null;
  
  	//Creating the view values
  	private AppBarLayout viewAppBar;
  	private Toolbar viewToolbar;
  	private FloatingActionButton viewFAB;
  	private RecyclerView viewMainList;
  	private View viewLoading;
  	private ViewGroup viewGroupBlank;
  	
  	private ViewGroup viewGroupSearch;
  	private RecyclerView viewSearchList;
  	private ViewGroup viewGroupToolbarSearch;
  	private EditText viewSearchField;
  	private ImageButton viewSearchClear;
  	
  	private ViewGroup viewGroupSync;
  	private LinearProgressIndicator progressBarSync;
  	
  	private ViewGroup viewGroupError;
  	
  	private ConversationRecyclerAdapter conversationRecyclerAdapter = null;
  	private SearchRecyclerAdapter searchRecyclerAdapter = null;
  	
  	private ActionMode actionMode = null;
  	private final CountingActionModeCallback actionModeCallbacks = new CountingActionModeCallback();
  	
  	private final TextWatcher searchTextWatcher = new TextWatcher() {
  		@Override
  		public void beforeTextChanged(CharSequence s, int start, int count, int after) {
  		
  		}
  		
  		@Override
  		public void onTextChanged(CharSequence s, int start, int before, int count) {
  			//Setting the clear button state
  			viewSearchClear.setVisibility(s.length() > 0 ? View.VISIBLE : View.GONE);
  			
  			//Updating the search filter
  			updateSearchFilter(s.toString());
  		}
  		
  		@Override
  		public void afterTextChanged(Editable s) {
  		
  		}
  	};
  	
  	//Creating the timer values
  	private final Handler timeUpdateHandler = new Handler(Looper.getMainLooper());
  	private Runnable timeUpdateHandlerRunnable = new Runnable() {
  		@Override
  		public void run() {
  			if(viewModel.stateLD.getValue() == ActivityViewModel.stateReady && conversationRecyclerAdapter != null) {
  				conversationRecyclerAdapter.notifyItemRangeChanged(0, viewModel.conversationList.size(), conversationPayloadPreview);
  			}
  			
  			//Running again
  			timeUpdateHandler.postDelayed(this, timeUpdateHandlerDelay);
  		}
  	};
  	
  	//Creating the state values
  	private int currentState = -1;
  	private ServerUpdateData currentServerUpdateData = null;
  	
  	public Conversations() {
  		//Setting the plugins
  		addPlugin(pluginMessageBar = new PluginMessageBar());
  		addPlugin(pluginRXD = new PluginRXDisposable());
  		addPlugin(pluginCS = new PluginConnectionService());
  		addPlugin(new PluginQNavigation());
  		addPlugin(new PluginThemeUpdater());
  	}
  	
  	@Override
  	public void onCreate(Bundle savedInstanceState) {
  		//Calling the super method
  		super.onCreate(savedInstanceState);
  		
  		//Checking if the user needs to configure their server connection
  		if(!SharedPreferencesManager.isConnectionConfigured(this)) {
  			//Creating the intent
  			Intent launchOnboarding = new Intent(this, Onboarding.class);
  			
  			//Launching the intent
  			startActivity(launchOnboarding);
  			
  			//Finishing the current activity
  			finish();
  			
  			//Returning
  			return;
  		}
  		
  		//Reading the archived state
  		isViewArchived = getIntent().getBooleanExtra(intentExtraArchived, false) || (savedInstanceState != null && savedInstanceState.getBoolean(savedInstanceStateArchived));
  		
  		//Setting the content view
  		setContentView(R.layout.activity_conversations);
  		
  		//Enabling the toolbar
  		setSupportActionBar(findViewById(R.id.toolbar));
  		
  		//Getting the view model
  		viewModel = new ViewModelProvider(this, new ViewModelProvider.Factory() {
  			@NonNull
  			@Override
  			public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
  				return (T) new ActivityViewModel(getApplication(), isViewArchived);
  			}
  		}).get(ActivityViewModel.class);
  		
  		//Getting the views
  		viewAppBar = findViewById(R.id.appbar);
  		viewToolbar = findViewById(R.id.toolbar);
  		viewFAB = findViewById(R.id.fab);
  		viewMainList = findViewById(R.id.list);
  		viewLoading = findViewById(R.id.loading_text);
  		viewGroupBlank = findViewById(R.id.blankview);
  		
  		viewGroupSearch = findViewById(R.id.viewgroup_search);
  		viewSearchList = findViewById(R.id.list_search);
  		viewGroupToolbarSearch = findViewById(R.id.layout_search);
  		viewSearchField = findViewById(R.id.search_edittext);
  		viewSearchClear = findViewById(R.id.search_buttonclear);
  		
  		viewGroupSync = findViewById(R.id.syncview);
  		progressBarSync = findViewById(R.id.syncview_progress);
  		
  		viewGroupError = findViewById(R.id.errorview);
  		Button buttonError = findViewById(R.id.button_error);
  		
  		pluginMessageBar.setParentView(findViewById(R.id.infobar_container));
  		
  		//Setting the list padding
  		PluginQNavigation.setViewForInsets(viewMainList, viewMainList, viewSearchList);
  		
  		viewMainList.post(() -> {
  			//Calculating the required padding
  			int padding = WindowHelper.calculatePaddingContentWidth(getResources(), viewMainList);
  			
  			//Applying the padding to the main list
  			{
  				ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) viewMainList.getLayoutParams();
  				layoutParams.leftMargin = padding;
  				layoutParams.rightMargin = padding;
  				viewMainList.setLayoutParams(layoutParams);
  			}
  			
  			//Applying the padding to the search results list
  			{
  				ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) viewSearchList.getLayoutParams();
  				layoutParams.leftMargin = padding;
  				layoutParams.rightMargin = padding;
  				viewSearchList.setLayoutParams(layoutParams);
  			}
  		});
  		
  		//Configuring the AMOLED theme
  		if(ThemeHelper.shouldUseAMOLED(this)) setDarkAMOLED();
  		
  		//Setting the status bar color
  		PlatformHelper.updateChromeOSStatusBar(this);
  		
  		//Configuring the list
  		//conversationsBasePlugin.listView.setOnItemClickListener(onListItemClickListener);
  		//conversationsBasePlugin.listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
  		//conversationsBasePlugin.listView.setMultiChoiceModeListener(listMultiChoiceModeListener);
  		
  		//Setting the listeners
  		viewFAB.setOnClickListener(view -> startActivity(new Intent(Conversations.this, NewMessage.class)));
  		viewSearchField.addTextChangedListener(searchTextWatcher);
  		viewSearchClear.setOnClickListener(view -> viewSearchField.setText(""));
  		buttonError.setOnClickListener(view -> viewModel.loadConversations());
  		
  		//Configuring the app bar
  		viewAppBar.setLiftable(false);
  		viewMainList.addOnScrollListener(new RecyclerView.OnScrollListener() {
  			@Override
  			public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
  				viewAppBar.setLifted(((LinearLayoutManager) viewMainList.getLayoutManager()).findFirstCompletelyVisibleItemPosition() > 0);
  			}
  		});
  		
  		//Creating the banners
  		infoBarConnection = pluginMessageBar.create(R.drawable.disconnection, null);
  		infoBarContacts = pluginMessageBar.create(R.drawable.contacts, getResources().getString(R.string.message_permissiondetails_contacts_listing));
  		infoBarContacts.setButton(R.string.action_enable, view -> requestPermissions(new String[]{android.Manifest.permission.READ_CONTACTS}, permissionRequestContacts));
  		infoBarServerUpdate = pluginMessageBar.create(R.drawable.update, getResources().getString(R.string.message_serverupdate));
  		infoBarServerUpdate.setButton(R.string.action_details, view -> startUpdateActivity());
  		infoBarServerUpdateRequired = pluginMessageBar.create(R.drawable.sync_problem, getResources().getString(R.string.message_serverupdaterequired));
  		infoBarServerUpdateRequired.setButton(R.string.action_details, view -> showServerUpdateRequiredDialog());
  		infoBarSecurityUpdate = pluginMessageBar.create(R.drawable.lock_alert, getResources().getString(R.string.message_securityupdate));
  		infoBarSecurityUpdate.setButton(R.string.action_resolve, view -> PlaySecurityBridge.showDialog(this, viewModel.playServicesErrorCode.getValue(), activityResultPlayServices));
  		
  		//Configuring the normal / archived view
  		if(isViewArchived) {
  			getSupportActionBar().setTitle(getArchivedSpannable());
  			getSupportActionBar().setDisplayHomeAsUpEnabled(true);
  			
  			//Setting the blank state text
  			((TextView) findViewById(R.id.blankview_title)).setText(R.string.message_blankstate_conversations_archived);
  			((TextView) findViewById(R.id.blankview_description)).setText(R.string.message_blankstate_conversations_archived_description);
  			
  			//Hiding the FAB
  			viewFAB.setVisibility(View.GONE);
  		} else {
  			getSupportActionBar().setTitle(getTitleSpannable());
  		}
  		
  		//Subscribing to load state updates
  		viewModel.stateLD.observe(this, this::updateUI);
  		
  		//Subscribing to mass retrieval updates
  		viewModel.massRetrievalProgressLD.observe(this, progress -> {
  			progressBarSync.setProgressCompat(progress, true);
  		});
  		viewModel.massRetrievalTotalLD.observe(this, total -> {
  			progressBarSync.setMax(total);
  		});
  		
  		//Subscribing to security provider updates
  		viewModel.playServicesErrorCode.observe(this, error -> {
  			if(error == null) infoBarSecurityUpdate.hide();
  			else infoBarSecurityUpdate.show();
  		});
  		
  		pluginRXD.activity().addAll(
  				ReduxEmitterNetwork.getMessageUpdateSubject().subscribe(this::updateConversationList), //Subscribing to messaging updates
  				ReduxEmitterNetwork.getTextImportUpdateSubject().subscribe(this::updateStateTextImport) //Subscribing to message import updates
  		);
  	}
  	
  	@Override
  	public void onStart() {
  		//Calling the super method
  		super.onStart();
  		
  		pluginRXD.ui().addAll(
  				ReduxEmitterNetwork.getConnectionStateSubject().subscribe(this::updateStateConnection), //Subscribing to connection state updates
  				ReduxEmitterNetwork.getMassRetrievalUpdateSubject().subscribe(this::updateStateMassRetrieval), //Subscribing to mass retrieval updates
  				ReduxEmitterNetwork.getRemoteUpdateSubject().subscribe(this::updateStateRemoteUpdate) //Subscribing to remote update updates
  		);
  	}
  	
  	@Override
  	public void onResume() {
  		//Calling the super method
  		super.onResume();
  		
  		//Adding the phantom reference
  		foregroundActivities.add(new WeakReference<>(this));
  		
  		//Checking if a sync is due
  		if(pluginCS.isServiceBound()) {
  			ConnectionManager connectionManager = pluginCS.getConnectionManager();
  			if(connectionManager.isConnected() && connectionManager.isPendingSync()) {
  				//Showing the sync screen
  				promptSync();
  			}
  		}
  		
  		//Updating the contacts info bar
  		if(MainApplication.canUseContacts(this)) infoBarContacts.hide();
  		else infoBarContacts.show();
  		
  		//Updating the "mark as read" control
  		updateMarkAllRead();
  		
  		//Clearing all message notifications
  		NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
  		for(StatusBarNotification notification : notificationManager.getActiveNotifications()) {
  			//Cancelling notifications with the 'notification' tag
  			if(NotificationHelper.notificationTagMessage.equals(notification.getTag()) || NotificationHelper.notificationIDMessageSummary == notification.getId()) {
  				notificationManager.cancel(notification.getTag(), notification.getId());
  			}
  		}
  		/* if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  			for(StatusBarNotification notification : notificationManager.getActiveNotifications()) {
  				if(!MainApplication.notificationChannelMessage.equals(notification.getNotification().getChannelId())) continue;
  				notificationManager.cancel(notification.getId());
  			}
  		} */
  		
  		//Starting the time update handler
  		timeUpdateHandlerRunnable.run();
  	}
  	
  	@Override
  	protected void onPause() {
  		super.onPause();
  		
  		//Iterating over the foreground conversations
  		for(Iterator<WeakReference<Conversations>> iterator = foregroundActivities.iterator(); iterator.hasNext();) {
  			//Getting the referenced activity
  			Conversations activity = iterator.next().get();
  			
  			//Removing the reference if it is invalid
  			if(activity == null) {
  				iterator.remove();
  				continue;
  			}
  			//Skipping the remainder of the iteration if the activity isn't this one
  			else if(activity != this) continue;
  			
  			//Removing the reference (to this activity)
  			iterator.remove();
  			
  			//Breaking from the loop
  			break;
  		}
  		
  		//Stopping the time update handler
  		timeUpdateHandler.removeCallbacks(timeUpdateHandlerRunnable);
  	}
  	
  	@Override
  	protected void onDestroy() {
  		super.onDestroy();
  		
  		if(viewMainList != null) viewMainList.setAdapter(null);
  		if(viewSearchList != null) viewSearchList.setAdapter(null);
  	}
  	
  	@Override
  	protected void onSaveInstanceState(@NonNull Bundle savedInstanceState) {
  		super.onSaveInstanceState(savedInstanceState);
  		
  		savedInstanceState.putBoolean(savedInstanceStateArchived, isViewArchived);
  	}
  	
  	@Override
  	public boolean onCreateOptionsMenu(Menu menu) {
  		//Don't create an options menu if we're on the archived screen
  		if(isViewArchived) {
  			return true;
  		}
  		
  		//Inflating the menu resource
  		getMenuInflater().inflate(R.menu.menu_conversations, menu);
  		
  		//Updating the "mark all as read" option
  		menuItemMarkAllRead = menu.findItem(R.id.action_markallread);
  		updateMarkAllRead();
  
  		//Getting the FaceTime option and subscribing to updates
  		MenuItem menuItemFaceTime = menu.findItem(R.id.action_facetime);
  		pluginRXD.activity().add(
  				ReduxEmitterNetwork.getServerFaceTimeSupportSubject()
  						.subscribe(menuItemFaceTime::setVisible)
  		);
  		
  		return true;
  	}
  	
  	@Override
  	protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
  		super.onActivityResult(requestCode, resultCode, data);
  		
  		if(requestCode == activityResultPlayServices) {
  			//Refreshing the security provider
  			viewModel.updateSecurityProvider();
  		}
  	}
  	
  	@Override
  	public void onRequestPermissionsResult(final int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
  		super.onRequestPermissionsResult(requestCode, permissions, grantResults);
  		
  		//Returning if there are no results
  		if(grantResults.length == 0) return;
  		
  		//Checking if the request code is contacts access
  		if(requestCode == permissionRequestContacts) {
  			//Checking if the result is a success
  			if(grantResults[0] == PackageManager.PERMISSION_GRANTED) {
  				//Hiding the contact request info bar
  				infoBarContacts.hide();
  				
  				//Updating the display
  				if(viewModel.stateLD.getValue() == ActivityViewModel.stateReady) {
  					conversationRecyclerAdapter.notifyDataSetChanged();
  				}
  				
  				//Starting the update listener
  				MainApplication.getInstance().registerContactsListener();
  			}
  			//Otherwise checking if the result is a denial
  			else if(grantResults[0] == PackageManager.PERMISSION_DENIED) {
  				//Showing a snackbar
  				Snackbar.make(findViewById(R.id.root), R.string.message_permissionrejected, Snackbar.LENGTH_LONG)
  					.setAction(R.string.screen_settings, view -> {
  						//Opening the application settings
  						Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
  						intent.setData(Uri.parse("package:" + getPackageName()));
  						startActivity(intent);
  					})
  					.show();
  			}
  		}
  	}
  	
  	@Override
  	public boolean onOptionsItemSelected(MenuItem item) {
  		int itemID = item.getItemId();
  		if(itemID == android.R.id.home) { //Up button
  			finish();
  			
  			return true;
  		} else if(itemID == R.id.action_search) { //Search
  			if(viewModel.stateLD.getValue() != ActivityViewModel.stateReady) return true;
  			
  			//Setting the state to search
  			setSearchState(true, true);
  
  			return true;
  		} else if(itemID == R.id.action_facetime) { //FaceTime
  			//Starting the FaceTime call history activity
  			startActivity(new Intent(this, CallHistory.class));
  
  			return true;
  		} else if(itemID == R.id.action_archived) { //Archived conversations
  			//Starting the archived conversations activity
  			startActivity(new Intent(this, Conversations.class).putExtra(intentExtraArchived, true));
  			
  			return true;
  		} else if(itemID == R.id.action_markallread) { //Mark all as read
  			if(viewModel.stateLD.getValue() != ActivityViewModel.stateReady) return true;
  			
  			//Marking all items as read
  			List<ConversationInfo> unreadConversationList = viewModel.conversationList.stream().filter(conversation -> conversation.getUnreadMessageCount() > 0).collect(Collectors.toList());
  			ConversationActionTask.unreadConversations(unreadConversationList, 0).subscribe();
  		/* } else if(itemID == R.id.action_blocked) { //Blocked contacts
  			return true; */
  		} else if(itemID == R.id.action_settings) { //Settings
  			//Starting the settings activity
  			startActivity(new Intent(this, Preferences.class));
  			
  			//Returning true
  			return true;
  		} else if(itemID == R.id.action_feedback) { //Send feedback
  			String currentCommunicationsVersion;
  			String serverSystemVersion;
  			String serverSoftwareVersion;
  			String proxyType;
  			
  			if(pluginCS.isServiceBound()) {
  				List<Integer> communicationsVersion = pluginCS.getConnectionManager().getCommunicationsVersion();
  				currentCommunicationsVersion = communicationsVersion != null ? communicationsVersion.stream().map(String::valueOf).collect(Collectors.joining(".")) : "(none)";
  				serverSystemVersion = StringHelper.defaultEmptyString(pluginCS.getConnectionManager().getServerSystemVersion(), "(none)");
  				serverSoftwareVersion = StringHelper.defaultEmptyString(pluginCS.getConnectionManager().getServerSoftwareVersion(), "(none)");
  			} else {
  				currentCommunicationsVersion = "(none)";
  				serverSystemVersion = "(none)";
  				serverSoftwareVersion = "(none)";
  			}
  			
  			if(SharedPreferencesManager.isConnectionConfigured(this)) {
  				if(SharedPreferencesManager.getProxyType(this) == ProxyType.direct) {
  					proxyType = "Direct";
  				} else {
  					proxyType = "Connect";
  				}
  			} else {
  				proxyType = "(none)";
  			}
  			
  			//Showing a dialog
  			new MaterialAlertDialogBuilder(this)
  					.setTitle(R.string.action_sendfeedback)
  					.setMessage(R.string.dialog_feedback_message)
  					.setPositiveButton(R.string.dialog_feedback_email, (dialog, which) -> {
  						//Creating the intent
  						Intent intent = new Intent(Intent.ACTION_SENDTO);
  						intent.setData(Uri.parse("mailto:"));
  						intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  						intent.putExtra(Intent.EXTRA_EMAIL, new String[]{ExternalLinkConstants.feedbackEmail});
  						intent.putExtra(Intent.EXTRA_SUBJECT, "AirMessage feedback");
  						intent.putExtra(Intent.EXTRA_TEXT, "\r\n\r\n" +
  								"---------- DEVICE INFORMATION ----------" + "\r\n" +
  								"Device model: " + Build.MODEL + "\r\n" +
  								"Android version: " + Build.VERSION.RELEASE + "\r\n" +
  								"Client version: " + BuildConfig.VERSION_NAME + "\r\n" +
  								"Build flavor: " + BuildConfig.FLAVOR + "\r\n" +
  								"Communications version: " + currentCommunicationsVersion + " (target " + VersionConstants.getLatestCommVerString() + ")" + "\r\n" +
  								"Proxy type: " + proxyType + "\r\n" +
  								"Server system version: " + serverSystemVersion + "\r\n" +
  								"Server software version: " + serverSoftwareVersion);
  						
  						//Launching the intent
  						try {
  							startActivity(intent);
  						} catch(ActivityNotFoundException exception) {
  							Toast.makeText(this, R.string.message_intenterror_email, Toast.LENGTH_SHORT).show();
  						}
  					})
  					.setNegativeButton(R.string.dialog_feedback_discord, (dialog, which) -> {
  						//Creating the intent
  						Intent intent = new Intent(Intent.ACTION_VIEW, ExternalLinkConstants.discordAddress);
  						intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  						
  						//Launching the intent
  						try {
  							startActivity(intent);
  						} catch(ActivityNotFoundException exception) {
  							Toast.makeText(this, R.string.message_intenterror_browser, Toast.LENGTH_SHORT).show();
  						}
  					})
  					.setNeutralButton(R.string.dialog_feedback_reddit, (dialog, which) -> {
  						//Creating the intent
  						Intent intent = new Intent(Intent.ACTION_VIEW, ExternalLinkConstants.redditAddress);
  						intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
  						
  						//Launching the intent
  						try {
  							startActivity(intent);
  						} catch(ActivityNotFoundException exception) {
  							Toast.makeText(this, R.string.message_intenterror_browser, Toast.LENGTH_SHORT).show();
  						}
  					})
  					.create().show();
  			
  			return true;
  		}
  		
  		return false;
  	}
  	
  	@Override
  	public void onBackPressed() {
  		//Exiting out of a search
  		if(viewModel.isSearching) {
  			setSearchState(false, true);
  		}
  		//Default behavior
  		else {
  			super.onBackPressed();
  		}
  	}
  	
  	/**
  	 * Updates the UI view in response to a state change
  	 */
  	private void updateUI(int state) {
  		//Ignoring if the state hasn't changed
  		if(currentState == state) return;
  		
  		if(state == ActivityViewModel.stateReady) {
  			//Setting the list adapters
  			viewMainList.setAdapter(conversationRecyclerAdapter = new ConversationRecyclerAdapter(viewModel.conversationList));
  			viewSearchList.setAdapter(searchRecyclerAdapter = new SearchRecyclerAdapter(viewModel.conversationList));
  			
  			//Restoring the action mode
  			restoreActionMode();
  			
  			//Prompting the user to update their messages
  			promptSync();
  		} else {
  			//Closing search
  			setSearchState(false, true);
  			
  			//Closing action mode
  			if(actionMode != null) actionMode.finish();
  		}
  		
  		//Disabling the old state
  		switch(currentState) {
  			case ActivityViewModel.stateLoading:
  				viewLoading.animate()
  						.alpha(0)
  						.withEndAction(() -> viewLoading.setVisibility(View.GONE));
  				break;
  			case ActivityViewModel.stateSyncing:
  				viewGroupSync.animate()
  						.alpha(0)
  						.withEndAction(() -> viewGroupSync.setVisibility(View.GONE));
  				break;
  			case ActivityViewModel.stateReady:
  				viewMainList.animate()
  						.alpha(0)
  						.withEndAction(() -> viewMainList.setVisibility(View.GONE));
  				
  				if(viewModel.conversationList.isEmpty()) {
  					viewGroupBlank.animate()
  							.alpha(0)
  							.withEndAction(() -> viewGroupBlank.setVisibility(View.GONE));
  				}
  				break;
  			case ActivityViewModel.stateError:
  				viewGroupError.animate()
  						.alpha(0)
  						.withEndAction(() -> viewGroupError.setVisibility(View.GONE));
  				break;
  		}
  		
  		//Setting the new state
  		currentState = state;
  		
  		//Enabling the new state
  		switch(state) {
  			case ActivityViewModel.stateLoading:
  				viewLoading.animate().withStartAction(() -> viewLoading.setVisibility(View.VISIBLE)).alpha(1);
  				break;
  			case ActivityViewModel.stateSyncing:
  				viewGroupSync.animate().withStartAction(() -> viewGroupSync.setVisibility(View.VISIBLE)).alpha(1);
  				progressBarSync.setIndeterminate(true);
  				break;
  			case ActivityViewModel.stateReady:
  				viewMainList.animate().withStartAction(() -> viewMainList.setVisibility(View.VISIBLE)).alpha(1);
  				if(viewModel.conversationList.isEmpty()) {
  					viewGroupBlank.animate().withStartAction(() -> viewGroupBlank.setVisibility(View.VISIBLE)).alpha(1);
  				}
  				break;
  			case ActivityViewModel.stateError:
  				viewGroupError.animate().withStartAction(() -> viewGroupError.setVisibility(View.VISIBLE)).alpha(1);
  				break;
  		}
  	}
  	
  	/**
  	 * Updates the activity based on a new connection event
  	 */
  	private void updateStateConnection(ReduxEventConnection event) {
  		if(event.getState() == ConnectionState.connected) {
  			//Prompting the user to sync their messages
  			promptSync();
  			
  			//Showing the server update required bar if the user's server is too old
  			/* if(pluginCS.isServiceBound()) {
  				List<Integer> version = pluginCS.getConnectionManager().getCommunicationsVersion();
  				if(version != null && version.size() == 2 && VersionHelper.INSTANCE.compareVersions(version, Arrays.asList(5, 4)) < 0) {
  					infoBarServerUpdateRequired.show();
  				}
  			} */
  		} else {
  			//Dismissing the server update required bar
  			infoBarServerUpdateRequired.hide();
  		}
  		
  		//Updating the connection warning banner
  		if(event.getState() == ConnectionState.disconnected) {
  			showServerWarning(((ReduxEventConnection.Disconnected) event).getCode());
  		} else {
  			hideServerWarning();
  		}
  	}
  	
  	/**
  	 * Updates the state from a mass retrieval event
  	 */
  	private void updateStateMassRetrieval(ReduxEventMassRetrieval event) {
  		if(event instanceof ReduxEventMassRetrieval.Complete) {
  			viewModel.finishStateSyncing();
  		} else if(event instanceof ReduxEventMassRetrieval.Error) {
  			viewModel.finishStateSyncing();
  			
  			//Showing a snackbar
  			if(event.getRequestID() != viewModel.lastMassRetrievalErrorID) {
  				Snackbar.make(findViewById(R.id.root), getResources().getString(R.string.message_syncerror, ((ReduxEventMassRetrieval.Error) event).getCode()), Snackbar.LENGTH_LONG).show();
  				viewModel.lastMassRetrievalErrorID = event.getRequestID();
  			}
  		} else {
  			viewModel.setStateSyncing();
  			
  			if(event instanceof ReduxEventMassRetrieval.Start) {
  				ReduxEventMassRetrieval.Start startEvent = (ReduxEventMassRetrieval.Start) event;
  				viewModel.massRetrievalTotalLD.setValue(startEvent.getMessageCount());
  			} else if(event instanceof ReduxEventMassRetrieval.Progress) {
  				ReduxEventMassRetrieval.Progress progressEvent = (ReduxEventMassRetrieval.Progress) event;
  				viewModel.massRetrievalTotalLD.setValue(progressEvent.getTotalItems());
  				viewModel.massRetrievalProgressLD.setValue(progressEvent.getReceivedItems());
  			}
  		}
  	}
  
  	private void updateStateRemoteUpdate(Optional<ServerUpdateData> updateData) {
  		if(updateData.isPresent()) {
  			currentServerUpdateData = updateData.get();
  			infoBarServerUpdate.show();
  		} else {
  			infoBarServerUpdate.hide();
  			currentServerUpdateData = null;
  		}
  	}
  	
  	/**
  	 * Updates the state from a text message import event
  	 */
  	private void updateStateTextImport(ReduxEventTextImport event) {
  		if(event instanceof ReduxEventTextImport.Complete) {
  			//Ignoring if there is no change in state
  			if(!viewModel.isStateTextImport()) return;
  			
  			//Updating the state
  			viewModel.setStateTextImport(false);
  			
  			//Adding the imported conversations
  			for(ConversationInfo conversationInfo : ((ReduxEventTextImport.Complete) event).getConversations()) {
  				int insertIndex = ConversationHelper.findInsertionIndex(conversationInfo, viewModel.conversationList);
  				viewModel.conversationList.add(insertIndex, conversationInfo);
  				conversationRecyclerAdapter.notifyItemInserted(insertIndex);
  			}
  		} else {
  			//Updating the state
  			viewModel.setStateTextImport(true);
  		}
  	}
  	
  	/**
  	 * Shows the connection warning banner with the provided {@link ConnectionErrorCode}
  	 */
  	private void showServerWarning(@ConnectionErrorCode int reason) {
  		//Getting the error details
  		ErrorDetailsHelper.ErrorDetails details = ErrorDetailsHelper.getErrorDetails(reason, false);
  		ErrorDetailsHelper.ErrorDetails.Button button = details.getButton();
  		
  		//Applying the error details to the info bar
  		infoBarConnection.setText(getResources().getString(R.string.message_serverstatus_limitedfunctionality, getResources().getString(details.getLabel())));
  		if(button == null) {
  			infoBarConnection.removeButton();
  		} else {
  			infoBarConnection.setButton(getResources().getString(button.getLabel()), view -> button.getClickListener().invoke(this, getSupportFragmentManager(), pluginCS.getConnectionManager()));
  		}
  		
  		//Showing the info bar
  		infoBarConnection.show();
  	}
  	
  	/**
  	 * Hides the connection warning banner
  	 */
  	private void hideServerWarning() {
  		infoBarConnection.hide();
  	}
  	
  	/**
  	 * Shows a dialog that warns the user to update their server
  	 */
  	private void showServerUpdateRequiredDialog() {
  		new MaterialAlertDialogBuilder(this)
  				.setTitle(R.string.message_serverupdaterequired)
  				.setMessage(R.string.message_serverupdaterequired_desc)
  				.setPositiveButton(android.R.string.ok, (dialog, which) -> dialog.dismiss())
  				.create()
  				.show();
  	}
  	
  	/**
  	 * Updates the conversation list in response to a messaging event
  	 */
  	private void updateConversationList(ReduxEventMessaging event) {
  		//Ignoring if messages aren't loaded
  		if(viewModel.stateLD.getValue() != ActivityViewModel.stateReady || conversationRecyclerAdapter == null) return;
  		
  		if(event instanceof ReduxEventMessaging.Message) {
  			updateMessageListMessageUpdate((ReduxEventMessaging.Message) event);
  		} else if(event instanceof ReduxEventMessaging.ConversationUpdate) {
  			updateMessageListConversationUpdate((ReduxEventMessaging.ConversationUpdate) event);
  		} else if(event instanceof ReduxEventMessaging.ConversationUnread) {
  			ReduxEventMessaging.ConversationUnread unreadEvent = (ReduxEventMessaging.ConversationUnread) event;
  			
  			getConversationFromAction(unreadEvent, (conversation, i) -> {
  				conversation.setUnreadMessageCount(unreadEvent.getUnreadCount());
  				conversationRecyclerAdapter.notifyItemChanged(i, conversationPayloadUnread);
  			});
  			
  			updateMarkAllRead();
  		} else if(event instanceof ReduxEventMessaging.ConversationMember) {
  			ReduxEventMessaging.ConversationMember memberEvent = (ReduxEventMessaging.ConversationMember) event;
  			
  			getConversationFromAction(memberEvent, (conversation, i) -> {
  				if(memberEvent.isJoin()) conversation.getMembers().add(memberEvent.getMember().clone());
  				else conversation.getMembers().removeIf(member -> member.getAddress().equals(memberEvent.getMember().getAddress()));
  				conversationRecyclerAdapter.notifyItemChanged(i, conversationPayloadMember);
  			});
  		} else if(event instanceof ReduxEventMessaging.ConversationMute) {
  			ReduxEventMessaging.ConversationMute muteEvent = (ReduxEventMessaging.ConversationMute) event;
  			
  			getConversationFromAction(muteEvent, (conversation, i) -> {
  				conversation.setMuted(muteEvent.isMuted());
  				conversationRecyclerAdapter.notifyItemChanged(i, conversationPayloadMuted);
  			});
  		} else if(event instanceof ReduxEventMessaging.ConversationArchive) {
  			ReduxEventMessaging.ConversationArchive archiveEvent = (ReduxEventMessaging.ConversationArchive) event;
  			
  			//If this conversation is to be added to the list
  			if(isViewArchived == archiveEvent.isArchived()) {
  				//Hiding the blank state view
  				if(viewModel.conversationList.isEmpty()) {
  					viewGroupBlank.animate().alpha(0).withEndAction(() -> viewGroupBlank.setVisibility(View.GONE));
  				}
  				
  				//Getting the conversation
  				ConversationInfo conversationInfo = archiveEvent.getConversationInfo().clone();
  				conversationInfo.setArchived(archiveEvent.isArchived());
  				
  				//Adding the conversation
  				int index = ConversationHelper.findInsertionIndex(conversationInfo, viewModel.conversationList);
  				viewModel.conversationList.add(index, conversationInfo);
  				conversationRecyclerAdapter.notifyItemInserted(index);
  			} else { //This conversation is to be removed from the list
  				getConversationFromAction(archiveEvent, (conversation, i) -> {
  							viewModel.conversationList.remove(i);
  							conversationRecyclerAdapter.notifyItemRemoved(i);
  						});
  				
  				//Showing the blank state view if there are no more conversations
  				if(viewModel.conversationList.isEmpty()) {
  					viewGroupBlank.animate().withStartAction(() -> viewGroupBlank.setVisibility(View.VISIBLE)).alpha(1);
  				}
  			}
  		} else if(event instanceof ReduxEventMessaging.ConversationDelete) {
  			ReduxEventMessaging.ConversationDelete deleteEvent = (ReduxEventMessaging.ConversationDelete) event;
  			
  			//Removing the conversation
  			getConversationFromAction(deleteEvent, (conversation, i) -> {
  				viewModel.conversationList.remove(i);
  				conversationRecyclerAdapter.notifyItemRemoved(i);
  			});
  			
  			//Showing the blank state view if there are no more conversations
  			if(viewModel.conversationList.isEmpty()) {
  				viewGroupBlank.animate().withStartAction(() -> viewGroupBlank.setVisibility(View.VISIBLE)).alpha(1);
  			}
  		} else if(event instanceof ReduxEventMessaging.ConversationServiceHandlerDelete) {
  			ReduxEventMessaging.ConversationServiceHandlerDelete deleteEvent = (ReduxEventMessaging.ConversationServiceHandlerDelete) event;
  			
  			//Removing any matching conversations
  			for(ListIterator<ConversationInfo> iterator = viewModel.conversationList.listIterator(); iterator.hasNext();) {
  				int i = iterator.nextIndex();
  				ConversationInfo conversationInfo = iterator.next();
  				if(conversationInfo.getServiceHandler() != deleteEvent.getServiceHandler()) continue;
  				
  				iterator.remove();
  				conversationRecyclerAdapter.notifyItemRemoved(i);
  			}
  			
  			//Showing the blank state view if there are no more conversations
  			if(viewModel.conversationList.isEmpty()) {
  				viewGroupBlank.animate().withStartAction(() -> viewGroupBlank.setVisibility(View.VISIBLE)).alpha(1);
  			}
  		} else if(event instanceof ReduxEventMessaging.ConversationTitle) {
  			ReduxEventMessaging.ConversationTitle titleEvent = (ReduxEventMessaging.ConversationTitle) event;
  			
  			getConversationFromAction(titleEvent, (conversation, i) -> {
  						conversation.setTitle(titleEvent.getTitle());
  						conversationRecyclerAdapter.notifyItemChanged(i, conversationPayloadTitle);
  					});
  		} else if(event instanceof ReduxEventMessaging.ConversationDraftMessageUpdate) {
  			ReduxEventMessaging.ConversationDraftMessageUpdate draftMessageEvent = (ReduxEventMessaging.ConversationDraftMessageUpdate) event;
  			
  			getConversationFromAction(draftMessageEvent, (conversation, i) -> {
  				//Updating the conversation
  				conversation.setDraftMessage(draftMessageEvent.getDraftMessage());
  				conversation.setDraftUpdateTime(draftMessageEvent.getUpdateTime());
  				conversationRecyclerAdapter.notifyItemChanged(i, conversationPayloadPreview);
  				
  				//Re-sorting the conversation
  				int conversationIndex = viewModel.conversationList.indexOf(conversation);
  				int insertionIndex = ConversationHelper.findReinsertionIndex(conversation, viewModel.conversationList);
  				if(conversationIndex != insertionIndex) {
  					viewModel.conversationList.remove(conversation);
  					viewModel.conversationList.add(insertionIndex, conversation);
  					conversationRecyclerAdapter.notifyItemMoved(conversationIndex, insertionIndex);
  				}
  			});
  		} else if(event instanceof ReduxEventMessaging.ConversationDraftFileUpdate) {
  			ReduxEventMessaging.ConversationDraftFileUpdate draftFileEvent = (ReduxEventMessaging.ConversationDraftFileUpdate) event;
  			
  			getConversationFromAction(draftFileEvent, (conversation, i) -> {
  				//Updating the conversation
  				if(draftFileEvent.isAddition()) conversation.getDraftFiles().add(draftFileEvent.getDraft());
  				else conversation.getDraftFiles().removeIf(draft -> draft.getLocalID() == draftFileEvent.getDraft().getLocalID());
  				conversation.setDraftUpdateTime(draftFileEvent.getUpdateTime());
  				conversationRecyclerAdapter.notifyItemChanged(i, conversationPayloadPreview);
  				
  				//Re-sorting the conversation
  				int conversationIndex = viewModel.conversationList.indexOf(conversation);
  				int insertionIndex = ConversationHelper.findReinsertionIndex(conversation, viewModel.conversationList);
  				if(conversationIndex != insertionIndex) {
  					viewModel.conversationList.remove(conversation);
  					viewModel.conversationList.add(insertionIndex, conversation);
  					conversationRecyclerAdapter.notifyItemMoved(conversationIndex, insertionIndex);
  				}
  			});
  		} else if(event instanceof ReduxEventMessaging.ConversationDraftFileClear) {
  			ReduxEventMessaging.ConversationDraftFileClear draftClearEvent = (ReduxEventMessaging.ConversationDraftFileClear) event;
  			
  			getConversationFromAction(draftClearEvent, (conversation, i) -> {
  				//Updating the conversation
  				conversation.getDraftFiles().clear();
  				conversation.setDraftUpdateTime(System.currentTimeMillis());
  				conversationRecyclerAdapter.notifyItemChanged(i, conversationPayloadPreview);
  				
  				//Re-sorting the conversation
  				int conversationIndex = viewModel.conversationList.indexOf(conversation);
  				int insertionIndex = ConversationHelper.findReinsertionIndex(conversation, viewModel.conversationList);
  				if(conversationIndex != insertionIndex) {
  					viewModel.conversationList.remove(conversation);
  					viewModel.conversationList.add(insertionIndex, conversation);
  					conversationRecyclerAdapter.notifyItemMoved(conversationIndex, insertionIndex);
  				}
  			});
  		}
  		
  		//Updating the search list adapter
  		if(viewModel.isSearching) searchRecyclerAdapter.updateAndFilter();
  		
  		//Updating the "mark all as read" menu option
  		updateMarkAllRead();
  	}
  	
  	/**
  	 * Runs the provided consumer if the conversation of the provided action is present
  	 */
  	private void getConversationFromAction(ReduxEventMessaging.ReduxConversationAction action, ObjIntConsumer<ConversationInfo> consumer) {
  		IntStream.range(0, viewModel.conversationList.size())
  				.filter(i -> viewModel.conversationList.get(i).getLocalID() == action.getConversationInfo().getLocalID())
  				.findAny()
  				.ifPresent(i -> consumer.accept(viewModel.conversationList.get(i), i));
  	}
  	
  	/**
  	 * Updates a conversation's preview, position in the list, title, and members based on a collection of new items
  	 * @param conversation The conversation to update
  	 * @param newMessages The collection of new messages under this conversation
  	 */
  	private void updateConversationDetails(ConversationInfo conversation, Collection<ConversationItem> newMessages) {
  		//Getting the conversation preview
  		ConversationPreview conversationPreview = ConversationPreviewHelper.latestItemToPreview(newMessages);
  		if(conversationPreview == null) return;
  		
  		//Checking if this new preview can replace the old one
  		if((conversation.getMessagePreview() == null || conversation.getMessagePreview().getDate() < conversationPreview.getDate())) {
  			//Updating the message preview
  			conversation.setMessagePreview(conversationPreview);
  			
  			//Re-sorting the conversation
  			int conversationIndex = viewModel.conversationList.indexOf(conversation);
  			int insertionIndex = ConversationHelper.findReinsertionIndex(conversation, viewModel.conversationList);
  			if(conversationIndex != insertionIndex) {
  				viewModel.conversationList.remove(conversation);
  				viewModel.conversationList.add(insertionIndex, conversation);
  				conversationRecyclerAdapter.notifyItemMoved(conversationIndex, insertionIndex);
  			}
  			
  			//Updating the conversation preview
  			conversationRecyclerAdapter.notifyItemChanged(insertionIndex, conversationPayloadPreview);
  		}
  	}
  	
  	/**
  	 * Updates the message list in response to a message update
  	 */
  	private void updateMessageListMessageUpdate(ReduxEventMessaging.Message event) {
  		List<ConversationInfo> conversationList = viewModel.conversationList;
  		
  		for(Pair<ConversationInfo, List<ReplaceInsertResult>> entry : event.getConversationItems()) {
  			//Finding the existing conversation index
  			ConversationInfo conversationInfo = conversationList.stream().filter(conversation -> entry.getFirst().getLocalID() == conversation.getLocalID()).findAny().orElse(null);
  			if(conversationInfo == null) continue;
  			
  			//Updating the conversation's preview
  			updateConversationDetails(conversationInfo, entry.getSecond().stream().map(ReplaceInsertResult::getTargetItem).collect(Collectors.toList()));
  		}
  	}
  	
  	/**
  	 * Updates the message list in response to a conversation update
  	 */
  	private void updateMessageListConversationUpdate(ReduxEventMessaging.ConversationUpdate event) {
  		List<ConversationInfo> conversationList = viewModel.conversationList;
  		
  		//Copying the list of new conversations
  		Map<ConversationInfo, Collection<ConversationItem>> newConversations = new HashMap<>(event.getNewConversations());
  		
  		//Handling transferred conversations
  		for(TransferredConversation transferredConversation : event.getTransferredConversations()) {
  			//Finding the existing conversation
  			ConversationInfo conversationInfo = conversationList.stream()
  					.filter((conversation) -> conversation.getLocalID() == transferredConversation.getClientConversation().getLocalID())
  					.findAny().orElse(null);
  			
  			//Converting the merge result to just the target items
  			List<ConversationItem> conversationItems = transferredConversation.getServerConversationItems().stream().map(ReplaceInsertResult::getTargetItem).collect(Collectors.toList());
  			
  			//If we couldn't find a matching conversation, add it as a new conversation
  			if(conversationInfo == null) {
  				newConversations.put(transferredConversation.getClientConversation(), conversationItems);
  				continue;
  			}
  			
  			//Updating the conversation's preview
  			updateConversationDetails(conversationInfo, conversationItems);
  		}
  		
  		//Hiding the blank state view
  		if(!newConversations.isEmpty() && viewModel.conversationList.isEmpty()) {
  			viewGroupBlank.animate().alpha(0).withEndAction(() -> viewGroupBlank.setVisibility(View.GONE));
  		}
  		
  		//Handling new conversations
  		for(Map.Entry<ConversationInfo, Collection<ConversationItem>> entry : newConversations.entrySet()) {
  			//Getting the conversation
  			ConversationInfo conversationInfo = entry.getKey().clone();
  			
  			//Getting and updating the conversation's preview
  			ConversationPreview conversationPreview = ConversationPreviewHelper.latestItemToPreview(entry.getValue());
  			if(conversationPreview != null) conversationInfo.setMessagePreview(conversationPreview);
  			conversationInfo.setUnreadMessageCount(entry.getValue().size());
  			
  			//Finding the index to insert the conversation
  			int insertionIndex = ConversationHelper.findInsertionIndex(conversationInfo, conversationList);
  			
  			//Inserting the conversation
  			conversationList.add(insertionIndex, conversationInfo);
  			conversationRecyclerAdapter.notifyItemInserted(insertionIndex);
  		}
  	}
  	
  	void startActionMode() {
  		//Starting action mode
  		actionMode = startSupportActionMode(actionModeCallbacks);
  	}
  	
  	void restoreActionMode() {
  		//Returning if the action mode is already set up or there are no items selected
  		if(actionMode != null || viewModel.actionModeSelections.isEmpty()) return;
  		
  		//Calculating the values
  		for(ConversationInfo conversationInfo : viewModel.conversationList) {
  			//Skipping the remainder of the iteration if the conversation is not selected
  			if(!viewModel.actionModeSelections.contains(conversationInfo.getLocalID())) continue;
  			
  			//Adding to the values
  			actionModeCallbacks.selectedCount++;
  			if(conversationInfo.isMuted()) actionModeCallbacks.mutedCount++;
  			else actionModeCallbacks.unmutedCount++;
  			if(conversationInfo.isArchived()) actionModeCallbacks.archivedCount++;
  			else actionModeCallbacks.unarchivedCount++;
  		}
  		
  		//Starting the action mode
  		startActionMode();
  		actionModeCallbacks.updateActionModeContext();
  	}
  	
  	void setSearchState(boolean enabled, boolean updateContext) {
  		//Returning if the requested state matches the current state
  		if(viewModel.isSearching == enabled) return;
  		
  		//Setting the search state
  		viewModel.isSearching = enabled;
  		
  		long duration = getResources().getInteger(android.R.integer.config_shortAnimTime);
  		if(enabled) {
  			//Hiding the toolbar
  			if(updateContext) viewToolbar.animate().alpha(0).setDuration(duration).withEndAction(() -> viewToolbar.setVisibility(View.GONE));
  			
  			//Showing the search group
  			viewGroupToolbarSearch.animate().alpha(1).setDuration(duration).withStartAction(() -> {
  				viewGroupToolbarSearch.setVisibility(View.VISIBLE);
  				
  				//Clearing the text field
  				if(viewSearchField.getText().length() > 0) viewSearchField.setText("");
  				
  				//Opening the keyboard
  				viewSearchField.requestFocus();
  				((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)).showSoftInput(viewSearchField, InputMethodManager.SHOW_IMPLICIT);
  			}).withEndAction(() -> viewGroupToolbarSearch.setClickable(true));
  			
  			//Hiding the FAB
  			if(updateContext) updateFAB();
  		} else {
  			//Showing the toolbar
  			if(updateContext) viewToolbar.animate().alpha(1).setDuration(duration).withStartAction(() -> viewToolbar.setVisibility(View.VISIBLE));
  			
  			//Hiding the search group
  			viewGroupToolbarSearch.animate().alpha(0).setDuration(duration).withStartAction(() -> {
  				viewGroupToolbarSearch.setClickable(false);
  				
  				//Closing the keyboard
  				((InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE)).hideSoftInputFromWindow(viewSearchField.getWindowToken(), 0);
  			}).withEndAction(() -> viewGroupToolbarSearch.setVisibility(View.GONE));
  			
  			//Showing the FAB
  			if(updateContext) updateFAB();
  			
  			//Clearing the search query
  			updateSearchFilter("");
  		}
  	}
  	
  	/**
  	 * Updates the "mark all as read" menu option item, showing or hiding it based on if there are any conversations with unread messages
  	 */
  	private void updateMarkAllRead() {
  		if(menuItemMarkAllRead == null || viewModel.stateLD.getValue() != ActivityViewModel.stateReady) return;
  		
  		menuItemMarkAllRead.setVisible(viewModel.conversationList.stream().anyMatch(conversation -> conversation.getUnreadMessageCount() > 0));
  	}
  	
  	/**
  	 * Updates the FAB display, based on if the user is searching or the activity is displaying archived conversations
  	 */
  	private void updateFAB() {
  		if(viewModel.isSearching || isViewArchived) viewFAB.hide();
  		else viewFAB.show();
  	}
  	
  	/**
  	 * Opens the sync dialog to prompt the user to update their messages from their server
  	 */
  	private void promptSync() {
  		//Ignoring if we aren't connected to the service
  		if(!pluginCS.isServiceBound()) return;
  		
  		//Returning if no sync is pending, conversations aren't loaded, or the fragment already exists
  		if(!pluginCS.getConnectionManager().isPendingSync() || viewModel.stateLD.getValue() != ActivityViewModel.stateReady || getSupportFragmentManager().findFragmentByTag(keyFragmentSync) != null) return;
  		
  		//Creating and showing a new fragment
  		FragmentSync fragmentSync = new FragmentSync(pluginCS.getConnectionManager().getServerDeviceName(), pluginCS.getConnectionManager().getServerInstallationID(), viewModel.conversationList.stream().anyMatch(chat -> chat.getServiceHandler() == ServiceHandler.appleBridge));
  		fragmentSync.setCancelable(false);
  		fragmentSync.show(getSupportFragmentManager(), keyFragmentSync);
  	}
  
  	private void startUpdateActivity() {
  		Intent intent = new Intent(this, ServerUpdate.class);
  		intent.putExtra(ServerUpdate.PARAM_UPDATE, currentServerUpdateData);
  		if(pluginCS.isServiceBound()) {
  			intent.putExtra(ServerUpdate.PARAM_SERVERVERSION, pluginCS.getConnectionManager().getServerSoftwareVersion());
  			intent.putExtra(ServerUpdate.PARAM_SERVERNAME, pluginCS.getConnectionManager().getServerDeviceName());
  		}
  		startActivity(intent);
  	}
  	
  	public void onCloseSearchClicked(View view) {
  		setSearchState(false, true);
  	}
  	
  	private void updateSearchFilter(String query) {
  		boolean queryAvailable = !query.isEmpty();
  		
  		//Returning if the search is not active
  		if(queryAvailable && !viewModel.isSearching) return;
  		
  		//Updating the recycler adapter
  		if(searchRecyclerAdapter != null) searchRecyclerAdapter.updateFilterText(query);
  		
  		//Setting the search group state
  		viewGroupSearch.setVisibility(queryAvailable ? View.VISIBLE : View.GONE);
  		viewMainList.setVisibility(queryAvailable ? View.INVISIBLE : View.VISIBLE);
  	}
  	
  	/**
  	 * Gets a spannable to use for the action bar title when displaying the conversation list
  	 */
  	private Spannable getTitleSpannable() {
  		Spannable text = new SpannableString(getResources().getString(R.string.app_name));
  		text.setSpan(new ForegroundColorSpan(getResources().getColor(R.color.colorPrimary, null)), 0, text.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
  		text.setSpan(new TypefaceSpan("sans-serif-medium"), 0, text.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
  		
  		return text;
  	}
  	
  	/**
  	 * Gets a spannable to use for the action bar title when displaying archived conversations
  	 */
  	private Spannable getArchivedSpannable() {
  		Spannable text = new SpannableString(getResources().getString(R.string.screen_archived));
  		//text.setSpan(new ForegroundColorSpan(getResources().getServiceColor(R.color.colorArchived, null)), 0, text.length(), Spannable.SPAN_INCLUSIVE_INCLUSIVE);
  		return text;
  	}
  	
  	void setDarkAMOLED() {
  		ThemeHelper.setActivityAMOLEDBase(this);
  		findViewById(R.id.appbar).setBackgroundColor(ColorConstants.colorAMOLED);
  		findViewById(R.id.viewgroup_search).setBackgroundColor(ColorConstants.colorAMOLED);
  	}
  	
  	/**
  	 * Returns true if there is a foreground instance of this activity running
  	 */
  	public static boolean isForeground() {
  		return !foregroundActivities.isEmpty();
  	}
  	
  	public static class ActivityViewModel extends AndroidViewModel {
  		static final int stateIdle = 0;
  		static final int stateLoading = 1;
  		static final int stateSyncing = 2;
  		static final int stateReady = 3;
  		static final int stateError = 4;
  		
  		private final CompositeDisposable compositeDisposable = new CompositeDisposable();
  		private Disposable conversationLoadDisposable;
  		
  		//Parameters
  		final boolean isViewArchived;
  		
  		//Data
  		List<ConversationInfo> conversationList;
  		
  		//State
  		final MutableLiveData<Integer> stateLD = new MutableLiveData<>(stateIdle);
  		final MutableLiveData<Integer> massRetrievalProgressLD = new MutableLiveData<>();
  		final MutableLiveData<Integer> massRetrievalTotalLD = new MutableLiveData<>();
  		final MutableLiveData<Boolean> textImportLD = new MutableLiveData<>(false);
  		final MutableLiveData<Integer> playServicesErrorCode = new MutableLiveData<>(null);
  		final List<Long> actionModeSelections = new ArrayList<>();
  		boolean isSearching = false;
  		short lastMassRetrievalErrorID = -1;
  		
  		public ActivityViewModel(@NonNull Application application, boolean isViewArchived) {
  			super(application);
  			
  			//Setting the parameters
  			this.isViewArchived = isViewArchived;
  			
  			//Loading conversations
  			loadConversations();
  			
  			//Updating the security provider
  			if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
  				updateSecurityProvider();
  			}
  		}
  		
  		@Override
  		protected void onCleared() {
  			//Clearing task subscriptions
  			compositeDisposable.clear();
  		}
  		
  		/**
  		 * Loads conversations from the database
  		 */
  		private void loadConversations() {
  			//Ignoring if the state is already loading
  			if(stateLD.getValue() == stateLoading) return;
  			
  			//Updating the state
  			stateLD.setValue(stateLoading);
  			
  			//Loading the conversations
  			compositeDisposable.add(conversationLoadDisposable = Single.create((SingleEmitter<List<ConversationInfo>> emitter) -> {
  				//Loading the conversations
  				emitter.onSuccess(DatabaseManager.getInstance().fetchSummaryConversations(getApplication(), isViewArchived));
  			}).subscribeOn(Schedulers.single())
  					.observeOn(AndroidSchedulers.mainThread())
  					.subscribe((conversations) -> {
  						//Setting the conversation list
  						conversationList = conversations;
  						
  						//Setting the state
  						stateLD.setValue(stateReady);
  					}));
  		}
  		
  		/**
  		 * Updates the state in response to a sync event
  		 */
  		public void setStateSyncing() {
  			//Ignoring if the state is already syncing
  			if(stateLD.getValue() == stateSyncing) return;
  			
  			//Cancelling the existing conversation load task if it's running
  			if(conversationLoadDisposable != null && !conversationLoadDisposable.isDisposed()) {
  				conversationLoadDisposable.dispose();
  			}
  			
  			//Updating the state
  			stateLD.setValue(stateSyncing);
  		}
  		
  		/**
  		 * Updates the state in response to the completion of a sync event
  		 * @return Whether a sync was taking place
  		 */
  		public boolean finishStateSyncing() {
  			//Ignoring if the state is not syncing
  			if(stateLD.getValue() != stateSyncing) return false;
  			
  			//Reloading the conversations
  			loadConversations();
  			
  			//Returning true
  			return true;
  		}
  		
  		/**
  		 * Sets whether a text message import is in progress
  		 */
  		public void setStateTextImport(boolean inProgress) {
  			//Updating the state value
  			textImportLD.setValue(inProgress);
  		}
  		
  		/**
  		 * Gets whether a text message import is in progress
  		 */
  		public boolean isStateTextImport() {
  			return textImportLD.getValue();
  		}
  		
  		public void updateSecurityProvider() {
  			playServicesErrorCode.setValue(null);
  			PlaySecurityBridge.update(getApplication(), playServicesErrorCode::setValue);
  		}
  	}
  	
  	private class ConversationRecyclerAdapter extends RecyclerView.Adapter<VHConversationDetailed> {
  		//Creating the list values
  		private final List<ConversationInfo> items;
  		
  		ConversationRecyclerAdapter(List<ConversationInfo> items) {
  			//Setting the items
  			this.items = items;
  			
  			//Enabling stable IDs
  			setHasStableIds(true);
  		}
  		
  		@Override
  		@NonNull
  		public VHConversationDetailed onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
  			View view = getLayoutInflater().inflate(R.layout.listitem_conversation, parent, false);
  			return new VHConversationDetailed(view,
  					view.findViewById(R.id.conversationicon),
  					view.findViewById(R.id.title),
  					view.findViewById(R.id.selected),
  					view.findViewById(R.id.selectionhighlight),
  					view.findViewById(R.id.message),
  					view.findViewById(R.id.time),
  					view.findViewById(R.id.unread),
  					view.findViewById(R.id.flag_muted),
  					view.findViewById(R.id.flag_draft)
  			);
  		}
  		
  		@Override
  		public void onBindViewHolder(@NonNull VHConversationDetailed holder, int position) {
  			//Getting the conversation info
  			ConversationInfo conversationInfo = items.get(position);
  			
  			//Binding the title and icon
  			holder.getCompositeDisposable().addAll(
  					VBConversation.bindTitle(Conversations.this, holder.getConversationTitle(), conversationInfo).subscribe(),
  					VBConversation.bindUsers(Conversations.this, holder.getIconGroup(), conversationInfo).subscribe()
  			);
  			
  			//Binding the conversation preview
  			VBConversation.bindPreview(Conversations.this, holder.getLabelMessage(), holder.getLabelStatus(), conversationInfo.getDynamicPreview());
  			
  			//Binding the unread status
  			VBConversation.bindUnreadStatus(Conversations.this, holder.getConversationTitle(), holder.getLabelMessage(), holder.getLabelUnread(), conversationInfo.getUnreadMessageCount());
  			
  			//Binding the flags
  			holder.getFlagMuted().setVisibility(conversationInfo.isMuted() ? View.VISIBLE : View.GONE);
  			holder.getFlagDraft().setVisibility(conversationInfo.getDraftMessage() != null || !conversationInfo.getDraftFiles().isEmpty() ? View.VISIBLE : View.GONE);
  			
  			//Binding the selection indicator
  			VBConversation.bindSelectionIndicator(holder.itemView, holder.getIconGroup(), holder.getSelectionIndicator(), holder.getSelectionHighlight(), viewModel.actionModeSelections.contains(conversationInfo.getLocalID()), false);
  			
  			//Setting the view's click listeners
  			holder.itemView.setOnClickListener(view -> {
  				//Checking if action mode is active
  				if(actionMode != null) {
  					//Toggling the item's checked state
  					actionModeCallbacks.onItemCheckedStateToggled(conversationInfo);
  					
  					return;
  				}
  				
  				//Updating the conversation's shortcut usage
  				if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
  					ShortcutHelper.reportShortcutUsed(getApplication(), conversationInfo.getLocalID());
  				}
  				
  				//Launching the conversation activity
  				startActivity(new Intent(Conversations.this, Messaging.class).putExtra(Messaging.intentParamTargetID, conversationInfo.getLocalID()));
  			});
  			holder.itemView.setOnLongClickListener(view -> {
  				//Starting action mode if it isn't running
  				if(actionMode == null) startActionMode();
  				
  				//Toggling the item's checked state
  				actionModeCallbacks.onItemCheckedStateToggled(conversationInfo);
  				
  				return true;
  			});
  		}
  		
  		@Override
  		public void onBindViewHolder(@NonNull VHConversationDetailed holder, int position, @NonNull List<Object> payloads) {
  			if(payloads.isEmpty()) {
  				onBindViewHolder(holder, position);
  			} else {
  				//Getting the conversation info
  				ConversationInfo conversationInfo = items.get(position);
  				
  				for(Object payload : payloads) {
  					switch((int) payload) {
  						case conversationPayloadPreview:
  							VBConversation.bindPreview(Conversations.this, holder.getLabelMessage(), holder.getLabelStatus(), conversationInfo.getDynamicPreview());
  							holder.getFlagDraft().setVisibility(conversationInfo.getDraftMessage() != null || !conversationInfo.getDraftFiles().isEmpty() ? View.VISIBLE : View.GONE);
  							break;
  						case conversationPayloadTitle:
  							holder.getCompositeDisposable().add(VBConversation.bindTitle(Conversations.this, holder.getConversationTitle(), conversationInfo).subscribe());
  							break;
  						case conversationPayloadMember:
  							holder.getCompositeDisposable().add(VBConversation.bindUsers(Conversations.this, holder.getIconGroup(), conversationInfo).subscribe());
  							break;
  						case conversationPayloadMuted:
  							holder.getFlagMuted().setVisibility(conversationInfo.isMuted() ? View.VISIBLE : View.GONE);
  							break;
  						case conversationPayloadUnread:
  							VBConversation.bindUnreadStatus(Conversations.this, holder.getConversationTitle(), holder.getLabelMessage(), holder.getLabelUnread(), conversationInfo.getUnreadMessageCount());
  							break;
  						case conversationPayloadSelection:
  							VBConversation.bindSelectionIndicator(holder.itemView, holder.getIconGroup(), holder.getSelectionIndicator(), holder.getSelectionHighlight(), viewModel.actionModeSelections.contains(conversationInfo.getLocalID()), true);
  							break;
  					}
  				}
  			}
  		}
  		
  		@Override
  		public void onViewRecycled(@NonNull VHConversationDetailed holder) {
  			holder.getCompositeDisposable().clear();
  		}
  		
  		@Override
  		public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
  			//Cancelling all view holder tasks
  			LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
  			int firstVisibleIndex = layoutManager.findFirstVisibleItemPosition();
  			int lastVisibleIndex = layoutManager.findLastVisibleItemPosition();
  			
  			for(int i = firstVisibleIndex; i <= lastVisibleIndex; i++) {
  				DisposableViewHolder holder = (DisposableViewHolder) recyclerView.findViewHolderForLayoutPosition(i);
  				if(holder != null) holder.getCompositeDisposable().clear();
  			}
  		}
  		
  		@Override
  		public int getItemCount() {
  			return items.size();
  		}
  		
  		@Override
  		public long getItemId(int position) {
  			return items.get(position).getLocalID();
  		}
  	}
  	
  	private class SearchRecyclerAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
  		//Creating the reference values
  		private static final int itemTypeSubheader = -1;
  		private static final int itemTypeConversation = 0;
  		private static final int itemTypeMessage = 1;
  		
  		//Creating the list values
  		private final List<ConversationInfo> conversationSourceList;
  		
  		private final List<ConversationInfo> conversationFilterList = new ArrayList<>();
  		private final List<MessageInfo> messageFilterList = new ArrayList<>();
  		
  		private Disposable searchTask = null;
  		
  		SearchRecyclerAdapter(List<ConversationInfo> conversationList) {
  			//Setting the list
  			conversationSourceList = conversationList;
  		}
  		
  		private class SubheaderViewHolder extends RecyclerView.ViewHolder {
  			final TextView label;
  			
  			private SubheaderViewHolder(View itemView) {
  				super(itemView);
  				label = (TextView) itemView;
  			}
  		}
  		
  		void updateAndFilter() {
  			updateFilterText(lastFilterText);
  		}
  		
  		private String lastFilterText = "";
  		void updateFilterText(String text) {
  			//Cancelling the current subscription
  			if(searchTask != null && !searchTask.isDisposed()) searchTask.dispose();
  			
  			//Setting the last filter text
  			lastFilterText = text;
  			
  			//Clearing the filter lists
  			conversationFilterList.clear();
  			messageFilterList.clear();
  			
  			//Returning if there is no filter text
  			if(text.isEmpty()) {
  				notifyDataSetChanged();
  				return;
  			}
  			
  			//Iterating over the conversations
  			List<ConversationInfo> asyncSearchList = new ArrayList<>();
  			conversationLoop:
  			for(ConversationInfo conversationInfo : conversationSourceList) {
  				//Filtering the conversation based on its static name
  				if(conversationInfo.getTitle() != null && searchString(conversationInfo.getTitle(), text)) {
  					conversationFilterList.add(conversationInfo);
  					continue conversationLoop;
  				}
  				
  				//Filtering the conversation based on its members
  				for(MemberInfo member : conversationInfo.getMembers()) {
  					if(searchString(AddressHelper.normalizeAddress(member.getAddress()), text)) {
  						conversationFilterList.add(conversationInfo);
  						continue conversationLoop;
  					}
  					asyncSearchList.add(conversationInfo);
  				}
  			}
  			
  			//Updating the list
  			notifyDataSetChanged();
  			
  			if(!asyncSearchList.isEmpty()) {
  				//Starting a search for member names
  				searchTask = Observable.fromIterable(asyncSearchList).flatMapMaybe(conversation ->
  						Observable.fromIterable(conversation.getMembers())
  								//Map each member to their user info
  								.flatMapSingle(member -> MainApplication.getInstance().getUserCacheHelper().getUserInfo(Conversations.this, member.getAddress()))
  								//Ignore errors
  								.onErrorResumeNext(error -> Observable.empty())
  								//Try to match any member to the search query
  								.any(user -> searchString(user.getContactName(), lastFilterText))
  								//Filter out non-matches
  								.filter(isMatch -> isMatch)
  								//Map back to return the conversation
  								.map(isMatch -> conversation)
  				).subscribe((conversation) -> {
  					//Adding the conversation
  					conversationFilterList.add(conversation);
  					
  					//Updating the list
  					notifyItemInserted(conversationFilterList.size() - 1);
  				});
  			}
  		}
  		
  		private boolean searchString(String target, String query) {
  			return Pattern.compile(Pattern.quote(query), Pattern.CASE_INSENSITIVE).matcher(target).find();
  		}
  		
  		@NonNull
  		@Override
  		public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
  			switch(viewType) {
  				case itemTypeSubheader:
  					return new SubheaderViewHolder(getLayoutInflater().inflate(R.layout.listitem_subheader, parent, false));
  				case itemTypeConversation: {
  					View view = getLayoutInflater().inflate(R.layout.listitem_conversation, parent, false);
  					return new VHConversationDetailed(view,
  							view.findViewById(R.id.conversationicon),
  							view.findViewById(R.id.title),
  							view.findViewById(R.id.selected),
  							view.findViewById(R.id.selectionhighlight),
  							view.findViewById(R.id.message),
  							view.findViewById(R.id.time),
  							view.findViewById(R.id.unread),
  							view.findViewById(R.id.flag_muted),
  							view.findViewById(R.id.flag_draft)
  					);
  				}
  				case itemTypeMessage:
  					return null;
  				default:
  					throw new IllegalArgumentException("Invalid view type requested: " + viewType);
  			}
  		}
  		
  		@Override
  		public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
  			switch(getItemViewType(position)) {
  				case itemTypeSubheader:
  					//Setting the header label text
  					if(position == 0 && !conversationFilterList.isEmpty()) ((SubheaderViewHolder) holder).label.setText(R.string.part_conversations);
  					else ((SubheaderViewHolder) holder).label.setText(R.string.part_messages);
  					break;
  				case itemTypeConversation: {
  					//Getting the data
  					VHConversationDetailed viewHolder = (VHConversationDetailed) holder;
  					ConversationInfo conversation = conversationFilterList.get(position - 1);
  					
  					//Binding the title and icon
  					viewHolder.getCompositeDisposable().addAll(
  							VBConversation.bindTitle(Conversations.this, viewHolder.getConversationTitle(), conversation).subscribe(),
  							VBConversation.bindUsers(Conversations.this, viewHolder.getIconGroup(), conversation).subscribe()
  					);
  					
  					//Binding the conversation preview
  					VBConversation.bindPreview(Conversations.this, viewHolder.getLabelMessage(), viewHolder.getLabelStatus(), conversation.getDynamicPreview());
  					
  					//Binding the unread status
  					VBConversation.bindUnreadStatus(Conversations.this, viewHolder.getConversationTitle(), viewHolder.getLabelMessage(), viewHolder.getLabelUnread(), conversation.getUnreadMessageCount());
  					
  					//Binding the flags
  					viewHolder.getFlagMuted().setVisibility(conversation.isMuted() ? View.VISIBLE : View.GONE);
  					viewHolder.getFlagDraft().setVisibility(conversation.getDraftMessage() != null || !conversation.getDraftFiles().isEmpty() ? View.VISIBLE : View.GONE);
  					
  					//Setting the view's click listener
  					holder.itemView.setOnClickListener(view -> {
  						//Updating the conversation's shortcut usage
  						if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
  							ShortcutHelper.reportShortcutUsed(getApplication(), conversation.getLocalID());
  						}
  						
  						//Opening the conversation
  						startActivity(new Intent(Conversations.this, Messaging.class).putExtra(Messaging.intentParamTargetID, conversation.getLocalID()));
  					});
  					
  					break;
  				}
  				case itemTypeMessage:
  					break;
  			}
  		}
  		
  		@Override
  		public void onViewRecycled(@NonNull RecyclerView.ViewHolder holder) {
  			if(holder instanceof DisposableViewHolder) {
  				((DisposableViewHolder) holder).getCompositeDisposable().clear();
  			}
  		}
  		
  		@Override
  		public void onDetachedFromRecyclerView(@NonNull RecyclerView recyclerView) {
  			//Cancelling all view holder tasks
  			LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
  			int firstVisibleIndex = layoutManager.findFirstVisibleItemPosition();
  			int lastVisibleIndex = layoutManager.findLastVisibleItemPosition();
  			
  			for(int i = firstVisibleIndex; i <= lastVisibleIndex; i++) {
  				RecyclerView.ViewHolder holder = recyclerView.findViewHolderForLayoutPosition(i);
  				if(holder instanceof DisposableViewHolder) {
  					((DisposableViewHolder) holder).getCompositeDisposable().clear();
  				}
  			}
  		}
  		
  		@Override
  		public int getItemCount() {
  			int count = 0;
  			if(!conversationFilterList.isEmpty()) count += conversationFilterList.size() + 1;
  			if(!messageFilterList.isEmpty()) count += messageFilterList.size() + 1;
  			return count;
  		}
  		
  		@Override
  		public int getItemViewType(int position) {
  			if(position == 0) return itemTypeSubheader; //The first position will always be a header
  			if(conversationFilterList.isEmpty()) return itemTypeMessage; //If there are no conversations listed and the position is not 0, the item must be a message
  			if(position - 1 < conversationFilterList.size()) return itemTypeConversation; //If the position is not 0 and is less than the size of the conversation list, it must be a conversation
  			if(position - 1 == conversationFilterList.size()) return itemTypeSubheader; //If the position is the same size as the conversation list, it reaches the first entry of of the "messages" section, and is therefore the messages section subheader
  			return itemTypeMessage; //Otherwise, the item is a message
  		}
  	}
  	
  	private class CountingActionModeCallback implements ActionMode.Callback {
  		//Creating the selected conversations variable
  		int selectedCount = 0;
  		int mutedCount = 0;
  		int unmutedCount = 0;
  		int archivedCount = 0;
  		int unarchivedCount = 0;
  		
  		@Override
  		public boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
  			//Inflating the menu
  			MenuInflater inflater = actionMode.getMenuInflater();
  			inflater.inflate(R.menu.menu_conversation_actionmode, menu);
  			
  			//Closing the search
  			setSearchState(false, false);
  			
  			//Hiding the toolbar
  			viewToolbar.animate().alpha(0).withEndAction(() -> viewToolbar.setVisibility(View.INVISIBLE));
  			
  			//Animating the app bar
  			/* int toolbarColor = getResources().getServiceColor(R.color.colorContextualAppBar, null);
  			int statusBarColor = getResources().getServiceColor(R.color.colorContextualAppBarDark, null);
  			animateAppBarColor(toolbarColor, statusBarColor, getResources().getInteger(android.R.integer.config_mediumAnimTime)); */
  			
  			//Hiding the FAB
  			updateFAB();
  			
  			//Returning true
  			return true;
  		}
  		
  		@Override
  		public void onDestroyActionMode(ActionMode actionMode) {
  			//Invalidating the action mode
  			Conversations.this.actionMode = null;
  			
  			//Copying the selected items
  			List<Long> selectedItems = new ArrayList<>(viewModel.actionModeSelections);
  			
  			//Deselecting all items
  			viewModel.actionModeSelections.clear();
  			
  			//Updating the list
  			for(long selectedItemID : selectedItems) {
  				ConversationInfo selectedItem = viewModel.conversationList.stream().filter(item -> item.getLocalID() == selectedItemID).findAny().orElse(null);
  				if(selectedItem != null) {
  					conversationRecyclerAdapter.notifyItemChanged(viewModel.conversationList.indexOf(selectedItem), conversationPayloadSelection);
  				}
  			}
  			
  			//Resetting the selection counts
  			selectedCount = mutedCount = unmutedCount = archivedCount = unarchivedCount = 0;
  			
  			//Showing the toolbar
  			viewToolbar.setVisibility(View.VISIBLE);
  			viewToolbar.animate().setStartDelay(50).alpha(1);
  			
  			//Showing the FAB
  			updateFAB();
  		}
  		
  		@Override
  		public boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
  			return false;
  		}
  		
  		@Override
  		public boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
  			//Collecting the selected conversations
  			Stream<ConversationInfo> selectionStream = viewModel.actionModeSelections.stream().map(conversationID ->
  					viewModel.conversationList.stream().filter(conversation -> conversation.getLocalID() == conversationID).findAny())
  					.filter(Optional::isPresent)
  					.map(Optional::get);
  			
  			//Mute conversations
  			if(menuItem.getItemId() == R.id.action_mute) {
  				ConversationActionTask.muteConversations(selectionStream.filter(conversation -> !conversation.isMuted()).collect(Collectors.toList()), true).subscribe();
  				
  				actionMode.finish();
  				return true;
  			}
  			//Unmute conversations
  			else if(menuItem.getItemId() == R.id.action_unmute) {
  				ConversationActionTask.muteConversations(selectionStream.filter(ConversationInfo::isMuted).collect(Collectors.toList()), false).subscribe();
  				
  				actionMode.finish();
  				return true;
  			}
  			//Archive conversations
  			else if(menuItem.getItemId() == R.id.action_archive) {
  				//Getting the conversation list
  				final List<ConversationInfo> selectedConversations = selectionStream.collect(Collectors.toList());
  				
  				//Archiving the conversations
  				pluginRXD.activity().add(ConversationActionTask.archiveConversations(selectedConversations, true)
  						.subscribe(() -> {
  							//Creating a snackbar
  							int affectedCount = selectedConversations.size();
  							Snackbar.make(findViewById(R.id.root), getResources().getQuantityString(R.plurals.message_conversationarchived, affectedCount, affectedCount), Snackbar.LENGTH_LONG).setAction(R.string.action_undo, view -> {
  								//Unarchiving the conversations
  								ConversationActionTask.archiveConversations(selectedConversations, false).subscribe();
  							}).show();
  						}));
  				
  				//Finishing the action mode
  				actionMode.finish();
  				
  				//Returning true
  				return true;
  			}
  			//Unarchive conversations
  			else if(menuItem.getItemId() == R.id.action_unarchive) {
  				//Getting the conversation list
  				final List<ConversationInfo> selectedConversations = selectionStream.collect(Collectors.toList());
  				
  				//Unarchiving the conversations
  				pluginRXD.activity().add(ConversationActionTask.archiveConversations(selectedConversations, false)
  						.subscribe(() -> {
  							//Creating a snackbar
  							int affectedCount = selectedConversations.size();
  							Snackbar.make(findViewById(R.id.root), getResources().getQuantityString(R.plurals.message_conversationunarchived, affectedCount, affectedCount), Snackbar.LENGTH_LONG).setAction(R.string.action_undo, view -> {
  								//Re-archiving the conversations
  								ConversationActionTask.archiveConversations(selectedConversations, true).subscribe();
  							}).show();
  						}));
  				
  				//Finishing the action mode
  				actionMode.finish();
  				
  				//Returning true
  				return true;
  			}
  			//Delete conversations
  			else if(menuItem.getItemId() == R.id.action_delete) {
  				//Displaying a dialog
  				new MaterialAlertDialogBuilder(Conversations.this)
  						//Setting the message
  						.setMessage(getResources().getQuantityString(R.plurals.message_confirm_deleteconversation, selectedCount))
  						//Setting the button
  						.setNegativeButton(android.R.string.cancel, (dialog, which) -> dialog.dismiss())
  						.setPositiveButton(R.string.action_delete, (dialog, which) -> {
  							//Deleting the conversations
  							ConversationActionTask.deleteConversations(Conversations.this, selectionStream.collect(Collectors.toList())).subscribe();
  							
  							//Dismissing the dialog
  							dialog.dismiss();
  							
  							//Finishing the action mode
  							actionMode.finish();
  						})
  						//Creating the dialog
  						.create()
  						//Showing the dialog
  						.show();
  				
  				//Returning true
  				return true;
  			}
  			
  			//Returning false
  			return false;
  		}
  		
  		void onItemCheckedStateToggled(ConversationInfo item) {
  			boolean newSelectionState = !viewModel.actionModeSelections.contains(item.getLocalID());
  			
  			//Setting the item's checked state
  			if(newSelectionState) viewModel.actionModeSelections.add(item.getLocalID());
  			else viewModel.actionModeSelections.remove(item.getLocalID());
  			
  			//Updating the view
  			conversationRecyclerAdapter.notifyItemChanged(viewModel.conversationList.indexOf(item), conversationPayloadSelection);
  			
  			//Updating the selected conversations
  			int value = newSelectionState ? 1 : -1;
  			selectedCount += value;
  			if(item.isMuted()) mutedCount += value;
  			else unmutedCount += value;
  			if(item.isArchived()) archivedCount += value;
  			else unarchivedCount += value;
  			
  			//Updating the context
  			updateActionModeContext();
  			
  			//Finishing the action mode if there are no more items selected
  			if(selectedCount == 0) actionMode.finish();
  		}
  		
  		void updateActionModeContext() {
  			//Updating the title
  			actionMode.setTitle(getResources().getQuantityString(R.plurals.message_selectioncount, selectedCount, selectedCount));
  			
  			//Showing or hiding the mute / unmute buttons
  			actionMode.getMenu().findItem(R.id.action_unmute).setVisible(mutedCount > 0);
  			actionMode.getMenu().findItem(R.id.action_mute).setVisible(unmutedCount > 0);
  			
  			//Showing or hiding the archive / unarchive buttons
  			actionMode.getMenu().findItem(R.id.action_unarchive).setVisible(archivedCount > 0);
  			actionMode.getMenu().findItem(R.id.action_archive).setVisible(unarchivedCount > 0);
  		}
  	}
  }