diff --git a/AndroidManifest.xml b/AndroidManifest.xml
index ba3ff74d31a..e61597dbd74 100644
--- a/AndroidManifest.xml
+++ b/AndroidManifest.xml
@@ -122,11 +122,11 @@
+ android:value="org.thoughtcrime.securesms.MainActivity" />
-
-
-
@@ -214,24 +206,14 @@
-
-
-
-
+ android:parentActivityName=".MainActivity">
+ android:value="org.thoughtcrime.securesms.MainActivity" />
@@ -468,6 +450,10 @@
android:theme="@style/TextSecure.LightNoActionBar"
android:windowSoftInputMode="adjustResize"/>
+
+
diff --git a/res/layout/conversation_list_activity.xml b/res/layout/conversation_list_activity.xml
deleted file mode 100644
index 564696fa098..00000000000
--- a/res/layout/conversation_list_activity.xml
+++ /dev/null
@@ -1,90 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/res/layout/conversation_list_fragment.xml b/res/layout/conversation_list_fragment.xml
index 59251232fed..2a50709f907 100644
--- a/res/layout/conversation_list_fragment.xml
+++ b/res/layout/conversation_list_fragment.xml
@@ -1,96 +1,200 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
+ android:layout_gravity="center_horizontal"
+ tools:src="@drawable/conversation_list_empty_state" />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/res/layout/conversation_list_item_action.xml b/res/layout/conversation_list_item_action.xml
index 77d2bc83753..e0338db9397 100644
--- a/res/layout/conversation_list_item_action.xml
+++ b/res/layout/conversation_list_item_action.xml
@@ -1,5 +1,5 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/res/layout/conversation_list_item_inbox_zero.xml b/res/layout/conversation_list_item_inbox_zero.xml
index 149e45ad988..a9ff8b02d7d 100644
--- a/res/layout/conversation_list_item_inbox_zero.xml
+++ b/res/layout/conversation_list_item_inbox_zero.xml
@@ -1,5 +1,5 @@
-
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/res/layout/conversation_list_item_view.xml b/res/layout/conversation_list_item_view.xml
index a1591d8a93c..fdc983227a1 100644
--- a/res/layout/conversation_list_item_view.xml
+++ b/res/layout/conversation_list_item_view.xml
@@ -1,5 +1,5 @@
-
-
+
diff --git a/res/layout/fragment_search.xml b/res/layout/fragment_search.xml
deleted file mode 100644
index ae8401c6e8b..00000000000
--- a/res/layout/fragment_search.xml
+++ /dev/null
@@ -1,20 +0,0 @@
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/res/layout/main_activity.xml b/res/layout/main_activity.xml
new file mode 100644
index 00000000000..43d7aa44d11
--- /dev/null
+++ b/res/layout/main_activity.xml
@@ -0,0 +1,10 @@
+
+
+
+
\ No newline at end of file
diff --git a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java
index 483c6078e9f..c67cdda4ce7 100644
--- a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java
+++ b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java
@@ -24,7 +24,6 @@
import android.os.Bundle;
import androidx.annotation.Nullable;
-import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
@@ -110,7 +109,8 @@ public boolean onSupportNavigateUp() {
if (fragmentManager.getBackStackEntryCount() > 0) {
fragmentManager.popBackStack();
} else {
- Intent intent = new Intent(this, ConversationListActivity.class);
+ // TODO [greyson] Navigation
+ Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java
deleted file mode 100644
index db89d3a429f..00000000000
--- a/src/org/thoughtcrime/securesms/ConversationListActivity.java
+++ /dev/null
@@ -1,318 +0,0 @@
-/*
- * Copyright (C) 2014-2017 Open Whisper Systems
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.thoughtcrime.securesms;
-
-import android.Manifest;
-import android.annotation.SuppressLint;
-import android.content.ActivityNotFoundException;
-import android.content.Context;
-import android.content.Intent;
-import android.graphics.drawable.Drawable;
-import android.net.Uri;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import android.text.TextUtils;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.Toast;
-
-import androidx.annotation.NonNull;
-import androidx.appcompat.widget.Toolbar;
-import androidx.appcompat.widget.TooltipCompat;
-
-import com.bumptech.glide.load.engine.DiskCacheStrategy;
-
-import org.thoughtcrime.securesms.color.MaterialColor;
-import org.thoughtcrime.securesms.components.RatingManager;
-import org.thoughtcrime.securesms.components.SearchToolbar;
-import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
-import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
-import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
-import org.thoughtcrime.securesms.conversation.ConversationActivity;
-import org.thoughtcrime.securesms.database.DatabaseFactory;
-import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
-import org.thoughtcrime.securesms.insights.InsightsLauncher;
-import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
-import org.thoughtcrime.securesms.mms.GlideApp;
-import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
-import org.thoughtcrime.securesms.notifications.MessageNotifier;
-import org.thoughtcrime.securesms.permissions.Permissions;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.search.SearchFragment;
-import org.thoughtcrime.securesms.service.KeyCachingService;
-import org.thoughtcrime.securesms.usernames.ProfileEditActivityV2;
-import org.thoughtcrime.securesms.util.DynamicLanguage;
-import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
-import org.thoughtcrime.securesms.util.DynamicTheme;
-import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.thoughtcrime.securesms.util.ViewUtil;
-import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
-import org.whispersystems.libsignal.util.guava.Optional;
-
-import java.util.List;
-
-public class ConversationListActivity extends PassphraseRequiredActionBarActivity
- implements ConversationListFragment.Controller
-{
- @SuppressWarnings("unused")
- private static final String TAG = ConversationListActivity.class.getSimpleName();
-
- private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
- private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
-
- private ConversationListFragment conversationListFragment;
- private SearchFragment searchFragment;
- private SearchToolbar searchToolbar;
- private ImageView searchAction;
- private ViewGroup fragmentContainer;
- private View toolbarShadow;
-
- @Override
- protected void onPreCreate() {
- dynamicTheme.onCreate(this);
- dynamicLanguage.onCreate(this);
- }
-
- @Override
- protected void onCreate(Bundle icicle, boolean ready) {
- setContentView(R.layout.conversation_list_activity);
-
- Toolbar toolbar = findViewById(R.id.toolbar);
- setSupportActionBar(toolbar);
-
- searchToolbar = findViewById(R.id.search_toolbar);
- searchAction = findViewById(R.id.search_action);
- fragmentContainer = findViewById(R.id.fragment_container);
- toolbarShadow = findViewById(R.id.conversation_list_toolbar_shadow);
- conversationListFragment = initFragment(R.id.fragment_container, new ConversationListFragment(), dynamicLanguage.getCurrentLocale());
-
- initializeSearchListener();
-
- RatingManager.showRatingDialogIfNecessary(this);
- RegistrationLockDialog.showReminderIfNecessary(this);
-
- TooltipCompat.setTooltipText(searchAction, getText(R.string.SearchToolbar_search_for_conversations_contacts_and_messages));
- }
-
- @Override
- public void onResume() {
- super.onResume();
- dynamicTheme.onResume(this);
- dynamicLanguage.onResume(this);
-
- SimpleTask.run(getLifecycle(), Recipient::self, this::initializeProfileIcon);
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- }
-
- @Override
- public boolean onPrepareOptionsMenu(Menu menu) {
- MenuInflater inflater = this.getMenuInflater();
- menu.clear();
-
- inflater.inflate(R.menu.text_secure_normal, menu);
-
- menu.findItem(R.id.menu_insights).setVisible(TextSecurePreferences.isSmsEnabled(this));
- menu.findItem(R.id.menu_clear_passphrase).setVisible(!TextSecurePreferences.isPasswordDisabled(this));
-
- super.onPrepareOptionsMenu(menu);
- return true;
- }
-
- private void initializeSearchListener() {
- searchAction.setOnClickListener(v -> {
- Permissions.with(this)
- .request(Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)
- .ifNecessary()
- .onAllGranted(() -> searchToolbar.display(searchAction.getX() + (searchAction.getWidth() / 2),
- searchAction.getY() + (searchAction.getHeight() / 2)))
- .withPermanentDenialDialog(getString(R.string.ConversationListActivity_signal_needs_contacts_permission_in_order_to_search_your_contacts_but_it_has_been_permanently_denied))
- .execute();
- });
-
- searchToolbar.setListener(new SearchToolbar.SearchListener() {
- @Override
- public void onSearchTextChange(String text) {
- String trimmed = text.trim();
-
- if (trimmed.length() > 0) {
- if (searchFragment == null) {
- searchFragment = SearchFragment.newInstance(dynamicLanguage.getCurrentLocale());
- getSupportFragmentManager().beginTransaction()
- .add(R.id.fragment_container, searchFragment, null)
- .commit();
- }
- searchFragment.updateSearchQuery(trimmed);
- } else if (searchFragment != null) {
- getSupportFragmentManager().beginTransaction()
- .remove(searchFragment)
- .commit();
- searchFragment = null;
- }
- }
-
- @Override
- public void onSearchClosed() {
- if (searchFragment != null) {
- getSupportFragmentManager().beginTransaction()
- .remove(searchFragment)
- .commit();
- searchFragment = null;
- }
- }
- });
- }
-
- private void initializeProfileIcon(@NonNull Recipient recipient) {
- ImageView icon = findViewById(R.id.toolbar_icon);
- String name = Optional.fromNullable(recipient.getDisplayName(this)).or(Optional.fromNullable(TextSecurePreferences.getProfileName(this))).or("");
- MaterialColor fallbackColor = recipient.getColor();
-
- if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
- fallbackColor = ContactColors.generateFor(name);
- }
-
- Drawable fallback = new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40).asDrawable(this, fallbackColor.toAvatarColor(this));
-
- GlideApp.with(this)
- .load(new ProfileContactPhoto(recipient.getId(), String.valueOf(TextSecurePreferences.getProfileAvatarId(this))))
- .error(fallback)
- .circleCrop()
- .diskCacheStrategy(DiskCacheStrategy.ALL)
- .into(icon);
-
- icon.setOnClickListener(v -> handleDisplaySettings());
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- super.onOptionsItemSelected(item);
-
- switch (item.getItemId()) {
- case R.id.menu_new_group: createGroup(); return true;
- case R.id.menu_settings: handleDisplaySettings(); return true;
- case R.id.menu_clear_passphrase: handleClearPassphrase(); return true;
- case R.id.menu_mark_all_read: handleMarkAllRead(); return true;
- case R.id.menu_invite: handleInvite(); return true;
- case R.id.menu_insights: handleInsights(); return true;
- case R.id.menu_help: handleHelp(); return true;
- }
-
- return false;
- }
-
- @Override
- public void onCreateConversation(long threadId, Recipient recipient, int distributionType, long lastSeen) {
- openConversation(threadId, recipient, distributionType, lastSeen, -1);
- }
-
- public void openConversation(long threadId, Recipient recipient, int distributionType, long lastSeen, int startingPosition) {
- searchToolbar.clearFocus();
-
- Intent intent = new Intent(this, ConversationActivity.class);
- intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
- intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
- intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
- intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis());
- intent.putExtra(ConversationActivity.LAST_SEEN_EXTRA, lastSeen);
- intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, startingPosition);
-
- startActivity(intent);
- overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out);
- }
-
- @Override
- public void onSwitchToArchive() {
- Intent intent = new Intent(this, ConversationListArchiveActivity.class);
- startActivity(intent);
- }
-
- @Override
- public void onBackPressed() {
- if (searchToolbar.isVisible()) searchToolbar.collapse();
- else super.onBackPressed();
- }
-
- @Override
- public void onListScrolledToTop() {
- if (toolbarShadow.getVisibility() != View.GONE) {
- ViewUtil.fadeOut(toolbarShadow, 250);
- }
- }
-
- @Override
- public void onListScrolledAwayFromTop() {
- if (toolbarShadow.getVisibility() != View.VISIBLE) {
- ViewUtil.fadeIn(toolbarShadow, 250);
- }
- }
-
- private void createGroup() {
- Intent intent = new Intent(this, GroupCreateActivity.class);
- startActivity(intent);
- }
-
- private void handleDisplaySettings() {
- Intent preferencesIntent = new Intent(this, ApplicationPreferencesActivity.class);
- startActivity(preferencesIntent);
- }
-
- private void handleClearPassphrase() {
- Intent intent = new Intent(this, KeyCachingService.class);
- intent.setAction(KeyCachingService.CLEAR_KEY_ACTION);
- startService(intent);
- }
-
- @SuppressLint("StaticFieldLeak")
- private void handleMarkAllRead() {
- new AsyncTask() {
- @Override
- protected Void doInBackground(Void... params) {
- Context context = ConversationListActivity.this;
- List messageIds = DatabaseFactory.getThreadDatabase(context).setAllThreadsRead();
-
- MessageNotifier.updateNotification(context);
- MarkReadReceiver.process(context, messageIds);
-
- return null;
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
- private void handleInvite() {
- startActivity(new Intent(this, InviteActivity.class));
- }
-
- private void handleInsights() {
- InsightsLauncher.showInsightsDashboard(getSupportFragmentManager());
- }
-
- private void handleHelp() {
- try {
- startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://support.signal.org")));
- } catch (ActivityNotFoundException e) {
- Toast.makeText(this, R.string.ConversationListActivity_there_is_no_browser_installed_on_your_device, Toast.LENGTH_LONG).show();
- }
- }
-}
diff --git a/src/org/thoughtcrime/securesms/ConversationListArchiveActivity.java b/src/org/thoughtcrime/securesms/ConversationListArchiveActivity.java
deleted file mode 100644
index 3aa7da6444d..00000000000
--- a/src/org/thoughtcrime/securesms/ConversationListArchiveActivity.java
+++ /dev/null
@@ -1,81 +0,0 @@
-package org.thoughtcrime.securesms;
-
-import android.content.Intent;
-import android.os.Bundle;
-import android.view.MenuItem;
-
-import org.thoughtcrime.securesms.conversation.ConversationActivity;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.util.DynamicLanguage;
-import org.thoughtcrime.securesms.util.DynamicTheme;
-
-public class ConversationListArchiveActivity extends PassphraseRequiredActionBarActivity
- implements ConversationListFragment.Controller
-{
-
- private final DynamicTheme dynamicTheme = new DynamicTheme();
- private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
-
- @Override
- protected void onPreCreate() {
- dynamicTheme.onCreate(this);
- dynamicLanguage.onCreate(this);
- }
-
- @Override
- protected void onCreate(Bundle icicle, boolean ready) {
- getSupportActionBar().setDisplayHomeAsUpEnabled(true);
- getSupportActionBar().setTitle(R.string.AndroidManifest_archived_conversations);
-
- Bundle bundle = new Bundle();
- bundle.putBoolean(ConversationListFragment.ARCHIVE, true);
-
- initFragment(android.R.id.content, new ConversationListFragment(), dynamicLanguage.getCurrentLocale(), bundle);
- }
-
- @Override
- public void onResume() {
- super.onResume();
- dynamicTheme.onResume(this);
- dynamicLanguage.onResume(this);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- super.onOptionsItemSelected(item);
-
- switch (item.getItemId()) {
- case R.id.home: super.onBackPressed(); return true;
- }
-
- return false;
- }
-
- @Override
- public void onCreateConversation(long threadId, Recipient recipient, int distributionType, long lastSeenTime) {
- Intent intent = new Intent(this, ConversationActivity.class);
- intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
- intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
- intent.putExtra(ConversationActivity.IS_ARCHIVED_EXTRA, true);
- intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
- intent.putExtra(ConversationActivity.LAST_SEEN_EXTRA, lastSeenTime);
-
- startActivity(intent);
- overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out);
- }
-
- @Override
- public void onSwitchToArchive() {
- throw new AssertionError();
- }
-
- @Override
- public void onListScrolledToTop() {
-
- }
-
- @Override
- public void onListScrolledAwayFromTop() {
-
- }
-}
diff --git a/src/org/thoughtcrime/securesms/ConversationListFragment.java b/src/org/thoughtcrime/securesms/ConversationListFragment.java
deleted file mode 100644
index aea856c1386..00000000000
--- a/src/org/thoughtcrime/securesms/ConversationListFragment.java
+++ /dev/null
@@ -1,660 +0,0 @@
-/*
- * Copyright (C) 2015 Open Whisper Systems
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-package org.thoughtcrime.securesms;
-
-import android.Manifest;
-import android.annotation.SuppressLint;
-import android.app.ProgressDialog;
-import android.content.Context;
-import android.content.Intent;
-import android.content.res.TypedArray;
-import android.database.Cursor;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-import android.graphics.Canvas;
-import android.graphics.Color;
-import android.graphics.Paint;
-import android.os.AsyncTask;
-import android.os.Build;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import com.google.android.material.snackbar.Snackbar;
-import androidx.fragment.app.Fragment;
-import androidx.loader.app.LoaderManager;
-import androidx.loader.content.Loader;
-import androidx.appcompat.app.AlertDialog;
-import androidx.appcompat.app.AppCompatActivity;
-import androidx.appcompat.view.ActionMode;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import androidx.recyclerview.widget.ItemTouchHelper;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuInflater;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.ImageView;
-import android.widget.TextView;
-import android.widget.Toast;
-
-import org.greenrobot.eventbus.EventBus;
-import org.greenrobot.eventbus.Subscribe;
-import org.greenrobot.eventbus.ThreadMode;
-import org.thoughtcrime.securesms.ConversationListAdapter.ItemClickListener;
-import org.thoughtcrime.securesms.components.recyclerview.DeleteItemAnimator;
-import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
-import org.thoughtcrime.securesms.components.reminder.DefaultSmsReminder;
-import org.thoughtcrime.securesms.components.reminder.DozeReminder;
-import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
-import org.thoughtcrime.securesms.components.reminder.OutdatedBuildReminder;
-import org.thoughtcrime.securesms.components.reminder.PushRegistrationReminder;
-import org.thoughtcrime.securesms.components.reminder.Reminder;
-import org.thoughtcrime.securesms.components.reminder.ReminderView;
-import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
-import org.thoughtcrime.securesms.components.reminder.ShareReminder;
-import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder;
-import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
-import org.thoughtcrime.securesms.database.DatabaseFactory;
-import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
-import org.thoughtcrime.securesms.database.loaders.ConversationListLoader;
-import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
-import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
-import org.thoughtcrime.securesms.insights.InsightsLauncher;
-import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
-import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
-import org.thoughtcrime.securesms.mms.GlideApp;
-import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
-import org.thoughtcrime.securesms.notifications.MessageNotifier;
-import org.thoughtcrime.securesms.permissions.Permissions;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.thoughtcrime.securesms.util.Util;
-import org.thoughtcrime.securesms.util.ViewUtil;
-import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
-import org.whispersystems.libsignal.util.guava.Optional;
-
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Locale;
-import java.util.Set;
-
-
-public class ConversationListFragment extends Fragment
- implements LoaderManager.LoaderCallbacks, ActionMode.Callback, ItemClickListener
-{
- public static final String ARCHIVE = "archive";
-
- @SuppressWarnings("unused")
- private static final String TAG = ConversationListFragment.class.getSimpleName();
-
- private static final int[] EMPTY_IMAGES = new int[] { R.drawable.empty_inbox_1,
- R.drawable.empty_inbox_2,
- R.drawable.empty_inbox_3,
- R.drawable.empty_inbox_4,
- R.drawable.empty_inbox_5 };
-
- private ActionMode actionMode;
- private RecyclerView list;
- private ReminderView reminderView;
- private View emptyState;
- private ImageView emptyImage;
- private TextView emptySearch;
- private PulsingFloatingActionButton fab;
- private PulsingFloatingActionButton cameraFab;
- private Locale locale;
- private String queryFilter = "";
- private boolean archive;
-
- @Override
- public void onCreate(Bundle icicle) {
- super.onCreate(icicle);
- locale = (Locale) getArguments().getSerializable(PassphraseRequiredActionBarActivity.LOCALE_EXTRA);
- archive = getArguments().getBoolean(ARCHIVE, false);
- }
-
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
- final View view = inflater.inflate(R.layout.conversation_list_fragment, container, false);
-
- reminderView = ViewUtil.findById(view, R.id.reminder);
- list = ViewUtil.findById(view, R.id.list);
- fab = ViewUtil.findById(view, R.id.fab);
- cameraFab = ViewUtil.findById(view, R.id.camera_fab);
- emptyState = ViewUtil.findById(view, R.id.empty_state);
- emptyImage = ViewUtil.findById(view, R.id.empty);
- emptySearch = ViewUtil.findById(view, R.id.empty_search);
-
- if (archive) {
- fab.hide();
- cameraFab.hide();
- } else {
- fab.show();
- cameraFab.show();
- }
-
- reminderView.setOnDismissListener(() -> updateReminders(true));
-
- list.setHasFixedSize(true);
- list.setLayoutManager(new LinearLayoutManager(getActivity()));
- list.setItemAnimator(new DeleteItemAnimator());
- list.addOnScrollListener(new ScrollListener());
-
- new ItemTouchHelper(new ArchiveListenerCallback()).attachToRecyclerView(list);
-
- return view;
- }
-
- @Override
- public void onActivityCreated(Bundle bundle) {
- super.onActivityCreated(bundle);
-
- setHasOptionsMenu(true);
- fab.setOnClickListener(v -> startActivity(new Intent(getActivity(), NewConversationActivity.class)));
- cameraFab.setOnClickListener(v -> {
- Permissions.with(requireActivity())
- .request(Manifest.permission.CAMERA)
- .ifNecessary()
- .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_camera_solid_24)
- .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
- .onAllGranted(() -> startActivity(MediaSendActivity.buildCameraFirstIntent(requireActivity())))
- .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())
- .execute();
- });
- initializeListAdapter();
- initializeTypingObserver();
- }
-
- @Override
- public void onResume() {
- super.onResume();
-
- updateReminders(true);
- list.getAdapter().notifyDataSetChanged();
- EventBus.getDefault().register(this);
-
- if (TextSecurePreferences.isSmsEnabled(requireContext())) {
- InsightsLauncher.showInsightsModal(requireContext(), requireFragmentManager());
- }
- }
-
- @Override
- public void onPause() {
- super.onPause();
-
- fab.stopPulse();
- cameraFab.stopPulse();
- EventBus.getDefault().unregister(this);
- }
-
- public ConversationListAdapter getListAdapter() {
- return (ConversationListAdapter) list.getAdapter();
- }
-
- public void setQueryFilter(String query) {
- this.queryFilter = query;
- getLoaderManager().restartLoader(0, null, this);
- }
-
- public void resetQueryFilter() {
- if (!TextUtils.isEmpty(this.queryFilter)) {
- setQueryFilter("");
- }
- }
-
- @SuppressLint("StaticFieldLeak")
- private void updateReminders(boolean hide) {
- new AsyncTask>() {
- @Override
- protected Optional extends Reminder> doInBackground(Context... params) {
- final Context context = params[0];
- if (UnauthorizedReminder.isEligible(context)) {
- return Optional.of(new UnauthorizedReminder(context));
- } else if (ExpiredBuildReminder.isEligible()) {
- return Optional.of(new ExpiredBuildReminder(context));
- } else if (ServiceOutageReminder.isEligible(context)) {
- ApplicationDependencies.getJobManager().add(new ServiceOutageDetectionJob());
- return Optional.of(new ServiceOutageReminder(context));
- } else if (OutdatedBuildReminder.isEligible()) {
- return Optional.of(new OutdatedBuildReminder(context));
- } else if (DefaultSmsReminder.isEligible(context)) {
- return Optional.of(new DefaultSmsReminder(context));
- } else if (Util.isDefaultSmsProvider(context) && SystemSmsImportReminder.isEligible(context)) {
- return Optional.of((new SystemSmsImportReminder(context)));
- } else if (PushRegistrationReminder.isEligible(context)) {
- return Optional.of((new PushRegistrationReminder(context)));
- } else if (ShareReminder.isEligible(context)) {
- return Optional.of(new ShareReminder(context));
- } else if (DozeReminder.isEligible(context)) {
- return Optional.of(new DozeReminder(context));
- } else {
- return Optional.absent();
- }
- }
-
- @Override
- protected void onPostExecute(Optional extends Reminder> reminder) {
- if (reminder.isPresent() && getActivity() != null && !isRemoving()) {
- reminderView.showReminder(reminder.get());
- } else if (!reminder.isPresent()) {
- reminderView.hide();
- }
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, getActivity());
- }
-
- private void initializeListAdapter() {
- list.setAdapter(new ConversationListAdapter(getActivity(), GlideApp.with(this), locale, null, this));
- getLoaderManager().restartLoader(0, null, this);
- }
-
- private void initializeTypingObserver() {
- ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().getTypingThreads().observe(this, threadIds -> {
- if (threadIds == null) {
- threadIds = Collections.emptySet();
- }
-
- getListAdapter().setTypingThreads(threadIds);
- });
- }
-
- @SuppressLint("StaticFieldLeak")
- private void handleArchiveAllSelected() {
- final Set selectedConversations = new HashSet<>(getListAdapter().getBatchSelections());
- final boolean archive = this.archive;
-
- int snackBarTitleId;
-
- if (archive) snackBarTitleId = R.plurals.ConversationListFragment_moved_conversations_to_inbox;
- else snackBarTitleId = R.plurals.ConversationListFragment_conversations_archived;
-
- int count = selectedConversations.size();
- String snackBarTitle = getResources().getQuantityString(snackBarTitleId, count, count);
-
- new SnackbarAsyncTask(getView(), snackBarTitle,
- getString(R.string.ConversationListFragment_undo),
- getResources().getColor(R.color.amber_500),
- Snackbar.LENGTH_LONG, true)
- {
-
- @Override
- protected void onPostExecute(Void result) {
- super.onPostExecute(result);
-
- if (actionMode != null) {
- actionMode.finish();
- actionMode = null;
- }
- }
-
- @Override
- protected void executeAction(@Nullable Void parameter) {
- for (long threadId : selectedConversations) {
- if (!archive) DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId);
- else DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId);
- }
- }
-
- @Override
- protected void reverseAction(@Nullable Void parameter) {
- for (long threadId : selectedConversations) {
- if (!archive) DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId);
- else DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId);
- }
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
-
- @SuppressLint("StaticFieldLeak")
- private void handleDeleteAllSelected() {
- int conversationsCount = getListAdapter().getBatchSelections().size();
- AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
- alert.setIconAttribute(R.attr.dialog_alert_icon);
- alert.setTitle(getActivity().getResources().getQuantityString(R.plurals.ConversationListFragment_delete_selected_conversations,
- conversationsCount, conversationsCount));
- alert.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationListFragment_this_will_permanently_delete_all_n_selected_conversations,
- conversationsCount, conversationsCount));
- alert.setCancelable(true);
-
- alert.setPositiveButton(R.string.delete, (dialog, which) -> {
- final Set selectedConversations = (getListAdapter())
- .getBatchSelections();
-
- if (!selectedConversations.isEmpty()) {
- new AsyncTask() {
- private ProgressDialog dialog;
-
- @Override
- protected void onPreExecute() {
- dialog = ProgressDialog.show(getActivity(),
- getActivity().getString(R.string.ConversationListFragment_deleting),
- getActivity().getString(R.string.ConversationListFragment_deleting_selected_conversations),
- true, false);
- }
-
- @Override
- protected Void doInBackground(Void... params) {
- DatabaseFactory.getThreadDatabase(getActivity()).deleteConversations(selectedConversations);
- MessageNotifier.updateNotification(getActivity());
- return null;
- }
-
- @Override
- protected void onPostExecute(Void result) {
- dialog.dismiss();
- if (actionMode != null) {
- actionMode.finish();
- actionMode = null;
- }
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
- }
- });
-
- alert.setNegativeButton(android.R.string.cancel, null);
- alert.show();
- }
-
- private void handleSelectAllThreads() {
- getListAdapter().selectAllThreads();
- actionMode.setTitle(String.valueOf(getListAdapter().getBatchSelections().size()));
- }
-
- private void handleCreateConversation(long threadId, Recipient recipient, int distributionType, long lastSeen) {
- ((Controller)getActivity()).onCreateConversation(threadId, recipient, distributionType, lastSeen);
- }
-
- @Override
- public @NonNull Loader onCreateLoader(int arg0, Bundle arg1) {
- return new ConversationListLoader(getActivity(), queryFilter, archive);
- }
-
- @Override
- public void onLoadFinished(@NonNull Loader arg0, Cursor cursor) {
- if ((cursor == null || cursor.getCount() <= 0) && TextUtils.isEmpty(queryFilter) && !archive) {
- list.setVisibility(View.INVISIBLE);
- emptyState.setVisibility(View.VISIBLE);
- emptySearch.setVisibility(View.INVISIBLE);
- emptyImage.setImageResource(EMPTY_IMAGES[(int) (Math.random() * EMPTY_IMAGES.length)]);
- fab.startPulse(3 * 1000);
- cameraFab.startPulse(3 * 1000);
- } else if ((cursor == null || cursor.getCount() <= 0) && !TextUtils.isEmpty(queryFilter)) {
- list.setVisibility(View.INVISIBLE);
- emptyState.setVisibility(View.GONE);
- emptySearch.setVisibility(View.VISIBLE);
- emptySearch.setText(getString(R.string.ConversationListFragment_no_results_found_for_s_, queryFilter));
- } else {
- list.setVisibility(View.VISIBLE);
- emptyState.setVisibility(View.GONE);
- emptySearch.setVisibility(View.INVISIBLE);
- fab.stopPulse();
- cameraFab.stopPulse();
- }
-
- getListAdapter().changeCursor(cursor);
- }
-
- @Override
- public void onLoaderReset(@NonNull Loader arg0) {
- getListAdapter().changeCursor(null);
- }
-
- @Override
- public void onItemClick(ConversationListItem item) {
- if (actionMode == null) {
- handleCreateConversation(item.getThreadId(), item.getRecipient(),
- item.getDistributionType(), item.getLastSeen());
- } else {
- ConversationListAdapter adapter = (ConversationListAdapter)list.getAdapter();
- adapter.toggleThreadInBatchSet(item.getThreadId());
-
- if (adapter.getBatchSelections().size() == 0) {
- actionMode.finish();
- } else {
- actionMode.setTitle(String.valueOf(getListAdapter().getBatchSelections().size()));
- }
-
- adapter.notifyDataSetChanged();
- }
- }
-
- @Override
- public void onItemLongClick(ConversationListItem item) {
- actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(ConversationListFragment.this);
-
- getListAdapter().initializeBatchMode(true);
- getListAdapter().toggleThreadInBatchSet(item.getThreadId());
- getListAdapter().notifyDataSetChanged();
- }
-
- @Override
- public void onSwitchToArchive() {
- ((Controller)getActivity()).onSwitchToArchive();
- }
-
- public interface Controller {
- void onCreateConversation(long threadId, Recipient recipient, int distributionType, long lastSeen);
- void onSwitchToArchive();
- void onListScrolledToTop();
- void onListScrolledAwayFromTop();
-}
-
- @Override
- public boolean onCreateActionMode(ActionMode mode, Menu menu) {
- MenuInflater inflater = getActivity().getMenuInflater();
-
- if (archive) inflater.inflate(R.menu.conversation_list_batch_unarchive, menu);
- else inflater.inflate(R.menu.conversation_list_batch_archive, menu);
-
- inflater.inflate(R.menu.conversation_list_batch, menu);
-
- mode.setTitle("1");
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
- }
-
- if (Build.VERSION.SDK_INT >= 23) {
- int current = getActivity().getWindow().getDecorView().getSystemUiVisibility();
- getActivity().getWindow().getDecorView().setSystemUiVisibility(current & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
- }
-
- return true;
- }
-
- @Override
- public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
- return false;
- }
-
- @Override
- public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
- switch (item.getItemId()) {
- case R.id.menu_select_all: handleSelectAllThreads(); return true;
- case R.id.menu_delete_selected: handleDeleteAllSelected(); return true;
- case R.id.menu_archive_selected: handleArchiveAllSelected(); return true;
- }
-
- return false;
- }
-
- @Override
- public void onDestroyActionMode(ActionMode mode) {
- getListAdapter().initializeBatchMode(false);
-
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
- TypedArray color = getActivity().getTheme().obtainStyledAttributes(new int[] {android.R.attr.statusBarColor});
- getActivity().getWindow().setStatusBarColor(color.getColor(0, Color.BLACK));
- color.recycle();
- }
-
- if (Build.VERSION.SDK_INT >= 23) {
- TypedArray lightStatusBarAttr = getActivity().getTheme().obtainStyledAttributes(new int[] {android.R.attr.windowLightStatusBar});
- int current = getActivity().getWindow().getDecorView().getSystemUiVisibility();
- int statusBarMode = lightStatusBarAttr.getBoolean(0, false) ? current | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
- : current & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
-
- getActivity().getWindow().getDecorView().setSystemUiVisibility(statusBarMode);
-
- lightStatusBarAttr.recycle();
- }
-
- actionMode = null;
- }
-
- @Subscribe(threadMode = ThreadMode.MAIN)
- public void onEvent(ReminderUpdateEvent event) {
- updateReminders(false);
- }
-
- private class ArchiveListenerCallback extends ItemTouchHelper.SimpleCallback {
-
- ArchiveListenerCallback() {
- super(0, ItemTouchHelper.RIGHT);
- }
-
- @Override
- public boolean onMove(@NonNull RecyclerView recyclerView,
- @NonNull RecyclerView.ViewHolder viewHolder,
- @NonNull RecyclerView.ViewHolder target)
- {
- return false;
- }
-
- @Override
- public int getSwipeDirs(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
- if (viewHolder.itemView instanceof ConversationListItemAction) {
- return 0;
- }
-
- if (actionMode != null) {
- return 0;
- }
-
- return super.getSwipeDirs(recyclerView, viewHolder);
- }
-
- @SuppressLint("StaticFieldLeak")
- @Override
- public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
- if (viewHolder.itemView instanceof ConversationListItemInboxZero) return;
- final long threadId = ((ConversationListItem)viewHolder.itemView).getThreadId();
- final int unreadCount = ((ConversationListItem)viewHolder.itemView).getUnreadCount();
-
- if (archive) {
- new SnackbarAsyncTask(getView(),
- getResources().getQuantityString(R.plurals.ConversationListFragment_moved_conversations_to_inbox, 1, 1),
- getString(R.string.ConversationListFragment_undo),
- getResources().getColor(R.color.amber_500),
- Snackbar.LENGTH_LONG, false)
- {
- @Override
- protected void executeAction(@Nullable Long parameter) {
- DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId);
- }
-
- @Override
- protected void reverseAction(@Nullable Long parameter) {
- DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId);
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId);
- } else {
- new SnackbarAsyncTask(getView(),
- getResources().getQuantityString(R.plurals.ConversationListFragment_conversations_archived, 1, 1),
- getString(R.string.ConversationListFragment_undo),
- getResources().getColor(R.color.amber_500),
- Snackbar.LENGTH_LONG, false)
- {
- @Override
- protected void executeAction(@Nullable Long parameter) {
- DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId);
-
- if (unreadCount > 0) {
- List messageIds = DatabaseFactory.getThreadDatabase(getActivity()).setRead(threadId, false);
- MessageNotifier.updateNotification(getActivity());
- MarkReadReceiver.process(getActivity(), messageIds);
- }
- }
-
- @Override
- protected void reverseAction(@Nullable Long parameter) {
- DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId);
-
- if (unreadCount > 0) {
- DatabaseFactory.getThreadDatabase(getActivity()).incrementUnread(threadId, unreadCount);
- MessageNotifier.updateNotification(getActivity());
- }
- }
- }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId);
- }
- }
-
- @Override
- public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
- @NonNull RecyclerView.ViewHolder viewHolder,
- float dX, float dY, int actionState,
- boolean isCurrentlyActive)
- {
- if (viewHolder.itemView instanceof ConversationListItemInboxZero) return;
- if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
- View itemView = viewHolder.itemView;
- Paint p = new Paint();
- float alpha = 1.0f - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
-
- if (dX > 0) {
- Bitmap icon;
-
- if (archive) icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_unarchive_white_36dp);
- else icon = BitmapFactory.decodeResource(getResources(), R.drawable.ic_archive_white_36dp);
-
- if (alpha > 0) p.setColor(getResources().getColor(R.color.green_500));
- else p.setColor(Color.WHITE);
-
- c.drawRect((float) itemView.getLeft(), (float) itemView.getTop(), dX,
- (float) itemView.getBottom(), p);
-
- c.drawBitmap(icon,
- (float) itemView.getLeft() + getResources().getDimension(R.dimen.conversation_list_fragment_archive_padding),
- (float) itemView.getTop() + ((float) itemView.getBottom() - (float) itemView.getTop() - icon.getHeight())/2,
- p);
- }
-
- viewHolder.itemView.setAlpha(alpha);
- viewHolder.itemView.setTranslationX(dX);
- } else {
- super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
- }
- }
- }
-
- private class ScrollListener extends RecyclerView.OnScrollListener {
- @Override
- public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
- if (recyclerView.canScrollVertically(-1)) {
- ((Controller) getActivity()).onListScrolledAwayFromTop();
- } else {
- ((Controller) getActivity()).onListScrolledToTop();
- }
- }
- }
-}
-
-
diff --git a/src/org/thoughtcrime/securesms/DatabaseMigrationActivity.java b/src/org/thoughtcrime/securesms/DatabaseMigrationActivity.java
index b9aef9aaa4e..bf1657b2eb7 100644
--- a/src/org/thoughtcrime/securesms/DatabaseMigrationActivity.java
+++ b/src/org/thoughtcrime/securesms/DatabaseMigrationActivity.java
@@ -149,7 +149,8 @@ private void handleImportComplete() {
if (getIntent().hasExtra("next_intent")) {
startActivity((Intent)getIntent().getParcelableExtra("next_intent"));
} else {
- startActivity(new Intent(this, ConversationListActivity.class));
+ // TODO [greyson] Navigation
+ startActivity(new Intent(this, MainActivity.class));
}
}
diff --git a/src/org/thoughtcrime/securesms/ExperienceUpgradeActivity.java b/src/org/thoughtcrime/securesms/ExperienceUpgradeActivity.java
index 4ebeed2bf7d..d0511f17f17 100644
--- a/src/org/thoughtcrime/securesms/ExperienceUpgradeActivity.java
+++ b/src/org/thoughtcrime/securesms/ExperienceUpgradeActivity.java
@@ -189,7 +189,8 @@ private void onContinue(Optional seenUpgrade) {
TextSecurePreferences.setLastExperienceVersionCode(this, latestVersion);
if (seenUpgrade.isPresent() && seenUpgrade.get().nextIntent != null) {
Intent intent = new Intent(this, seenUpgrade.get().nextIntent);
- Intent nextIntent = new Intent(this, ConversationListActivity.class);
+ // TODO [greyson] Navigation
+ Intent nextIntent = new Intent(this, MainActivity.class);
intent.putExtra("next_intent", nextIntent);
startActivity(intent);
} else {
diff --git a/src/org/thoughtcrime/securesms/MainActivity.java b/src/org/thoughtcrime/securesms/MainActivity.java
new file mode 100644
index 00000000000..e156f6133b4
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/MainActivity.java
@@ -0,0 +1,45 @@
+package org.thoughtcrime.securesms;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
+import org.thoughtcrime.securesms.util.DynamicTheme;
+
+public class MainActivity extends PassphraseRequiredActionBarActivity {
+
+ private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
+ private final MainNavigator navigator = new MainNavigator(this);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState, boolean ready) {
+ super.onCreate(savedInstanceState, ready);
+ setContentView(R.layout.main_activity);
+
+ navigator.onCreate(savedInstanceState);
+ }
+
+ @Override
+ protected void onPreCreate() {
+ super.onPreCreate();
+ dynamicTheme.onCreate(this);
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ dynamicTheme.onResume(this);
+ }
+
+ @Override
+ public void onBackPressed() {
+ if (!navigator.onBackPressed()) {
+ super.onBackPressed();
+ }
+ }
+
+ public @NonNull MainNavigator getNavigator() {
+ return navigator;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/MainFragment.java b/src/org/thoughtcrime/securesms/MainFragment.java
new file mode 100644
index 00000000000..deb6f5b4609
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/MainFragment.java
@@ -0,0 +1,22 @@
+package org.thoughtcrime.securesms;
+
+import android.content.Context;
+
+import androidx.annotation.NonNull;
+import androidx.fragment.app.Fragment;
+
+public class MainFragment extends Fragment {
+
+ @Override
+ public void onAttach(@NonNull Context context) {
+ super.onAttach(context);
+
+ if (!(requireActivity() instanceof MainActivity)) {
+ throw new IllegalStateException("Can only be used inside of MainActivity!");
+ }
+ }
+
+ protected @NonNull MainNavigator getNavigator() {
+ return MainNavigator.get(requireActivity());
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/MainNavigator.java b/src/org/thoughtcrime/securesms/MainNavigator.java
new file mode 100644
index 00000000000..38552b55926
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/MainNavigator.java
@@ -0,0 +1,104 @@
+package org.thoughtcrime.securesms;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+
+import org.thoughtcrime.securesms.conversation.ConversationActivity;
+import org.thoughtcrime.securesms.conversationlist.ConversationListArchiveFragment;
+import org.thoughtcrime.securesms.conversationlist.ConversationListFragment;
+import org.thoughtcrime.securesms.insights.InsightsLauncher;
+import org.thoughtcrime.securesms.recipients.RecipientId;
+
+public class MainNavigator {
+
+ private final MainActivity activity;
+
+ public MainNavigator(@NonNull MainActivity activity) {
+ this.activity = activity;
+ }
+
+ public static MainNavigator get(@NonNull Activity activity) {
+ if (!(activity instanceof MainActivity)) {
+ throw new IllegalArgumentException("Activity must be an instance of MainActivity!");
+ }
+
+ return ((MainActivity) activity).getNavigator();
+ }
+
+ public void onCreate(@Nullable Bundle savedInstanceState) {
+ if (savedInstanceState != null) {
+ return;
+ }
+
+ getFragmentManager().beginTransaction()
+ .add(R.id.fragment_container, ConversationListFragment.newInstance())
+ .commit();
+ }
+
+ /**
+ * @return True if the back pressed was handled in our own custom way, false if it should be given
+ * to the system to do the default behavior.
+ */
+ public boolean onBackPressed() {
+ Fragment fragment = getFragmentManager().findFragmentById(R.id.fragment_container);
+
+ if (fragment instanceof BackHandler) {
+ return ((BackHandler) fragment).onBackPressed();
+ }
+
+ return false;
+ }
+
+ public void goToConversation(@NonNull RecipientId recipientId, long threadId, int distributionType, long lastSeen, int startingPosition) {
+ Intent intent = ConversationActivity.buildIntent(activity, recipientId, threadId, distributionType, lastSeen, startingPosition);
+
+ activity.startActivity(intent);
+ activity.overridePendingTransition(R.anim.slide_from_end, R.anim.fade_scale_out);
+ }
+
+ public void goToAppSettings() {
+ Intent intent = new Intent(activity, ApplicationPreferencesActivity.class);
+ activity.startActivity(intent);
+ }
+
+
+ public void goToArchiveList() {
+ getFragmentManager().beginTransaction()
+ .setCustomAnimations(R.anim.slide_from_end, R.anim.slide_to_start, R.anim.slide_from_start, R.anim.slide_to_end)
+ .replace(R.id.fragment_container, ConversationListArchiveFragment.newInstance())
+ .addToBackStack(null)
+ .commit();
+ }
+
+ public void goToGroupCreation() {
+ Intent intent = new Intent(activity, GroupCreateActivity.class);
+ activity.startActivity(intent);
+ }
+
+ public void goToInvite() {
+ Intent intent = new Intent(activity, InviteActivity.class);
+ activity.startActivity(intent);
+ }
+
+ public void goToInsights() {
+ InsightsLauncher.showInsightsDashboard(activity.getSupportFragmentManager());
+ }
+
+ private @NonNull FragmentManager getFragmentManager() {
+ return activity.getSupportFragmentManager();
+ }
+
+ public interface BackHandler {
+ /**
+ * @return True if the back pressed was handled in our own custom way, false if it should be given
+ * to the system to do the default behavior.
+ */
+ boolean onBackPressed();
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java b/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java
index cd1895ffcd9..98cdd5c9770 100644
--- a/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java
+++ b/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java
@@ -187,7 +187,8 @@ private Intent getRoutedIntent(Class> destination, @Nullable Intent nextIntent
}
private Intent getConversationListIntent() {
- return new Intent(this, ConversationListActivity.class);
+ // TODO [greyson] Navigation
+ return new Intent(this, MainActivity.class);
}
private void initializeClearKeyReceiver() {
diff --git a/src/org/thoughtcrime/securesms/ShortcutLauncherActivity.java b/src/org/thoughtcrime/securesms/ShortcutLauncherActivity.java
index 19b5f10c41b..b798d177998 100644
--- a/src/org/thoughtcrime/securesms/ShortcutLauncherActivity.java
+++ b/src/org/thoughtcrime/securesms/ShortcutLauncherActivity.java
@@ -33,14 +33,16 @@ protected void onCreate(@Nullable Bundle savedInstanceState) {
if (rawId == null) {
Toast.makeText(this, R.string.ShortcutLauncherActivity_invalid_shortcut, Toast.LENGTH_SHORT).show();
- startActivity(new Intent(this, ConversationListActivity.class));
+ // TODO [greyson] Navigation
+ startActivity(new Intent(this, MainActivity.class));
finish();
return;
}
Recipient recipient = Recipient.live(RecipientId.from(rawId)).get();
+ // TODO [greyson] Navigation
TaskStackBuilder backStack = TaskStackBuilder.create(this)
- .addNextIntent(new Intent(this, ConversationListActivity.class));
+ .addNextIntent(new Intent(this, MainActivity.class));
CommunicationActions.startConversation(this, recipient, null, backStack);
finish();
diff --git a/src/org/thoughtcrime/securesms/components/reminder/SystemSmsImportReminder.java b/src/org/thoughtcrime/securesms/components/reminder/SystemSmsImportReminder.java
index d4a7824aadd..996be314adf 100644
--- a/src/org/thoughtcrime/securesms/components/reminder/SystemSmsImportReminder.java
+++ b/src/org/thoughtcrime/securesms/components/reminder/SystemSmsImportReminder.java
@@ -5,8 +5,8 @@
import android.view.View;
import android.view.View.OnClickListener;
-import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.DatabaseMigrationActivity;
+import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
@@ -21,7 +21,8 @@ public SystemSmsImportReminder(final Context context) {
intent.setAction(ApplicationMigrationService.MIGRATE_DATABASE);
context.startService(intent);
- Intent nextIntent = new Intent(context, ConversationListActivity.class);
+ // TODO [greyson] Navigation
+ Intent nextIntent = new Intent(context, MainActivity.class);
Intent activityIntent = new Intent(context, DatabaseMigrationActivity.class);
activityIntent.putExtra("next_intent", nextIntent);
context.startActivity(activityIntent);
diff --git a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java
index afd3a16b50b..b9450e635ec 100644
--- a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java
+++ b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java
@@ -34,9 +34,11 @@
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
+import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.thoughtcrime.securesms.util.Util;
import java.util.ArrayList;
import java.util.Collection;
@@ -183,20 +185,14 @@ private ContactData getContactData(Context context, String displayName, long id)
public List getNumbersForThreadSearchFilter(Context context, String constraint) {
LinkedList numberList = new LinkedList<>();
- Cursor cursor = null;
-
- try {
- cursor = context.getContentResolver().query(Uri.withAppendedPath(Phone.CONTENT_FILTER_URI,
- Uri.encode(constraint)),
- null, null, null, null);
+ try (Cursor cursor = DatabaseFactory.getRecipientDatabase(context).queryAllContacts(constraint)) {
while (cursor != null && cursor.moveToNext()) {
- numberList.add(cursor.getString(cursor.getColumnIndexOrThrow(Phone.NUMBER)));
- }
+ String phone = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.PHONE));
+ String email = cursor.getString(cursor.getColumnIndexOrThrow(RecipientDatabase.EMAIL));
- } finally {
- if (cursor != null)
- cursor.close();
+ numberList.add(Util.getFirstNonEmpty(phone, email));
+ }
}
GroupDatabase.Reader reader = null;
diff --git a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java
index 3f3b7e19e5e..dbd2da39af9 100644
--- a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java
+++ b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java
@@ -11,7 +11,6 @@
import android.widget.LinearLayout;
import android.widget.TextView;
-import org.thoughtcrime.securesms.ConversationListFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.FromTextView;
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index 456442b96b6..4b3c878e5a4 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -80,11 +80,10 @@
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.ApplicationContext;
-import org.thoughtcrime.securesms.ConversationListActivity;
-import org.thoughtcrime.securesms.ConversationListArchiveActivity;
import org.thoughtcrime.securesms.ExpirationDialog;
import org.thoughtcrime.securesms.GroupCreateActivity;
import org.thoughtcrime.securesms.GroupMembersDialog;
+import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.MediaOverviewActivity;
import org.thoughtcrime.securesms.MuteDialog;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
@@ -197,7 +196,7 @@
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
-import org.thoughtcrime.securesms.search.model.MessageResult;
+import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
@@ -276,7 +275,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public static final String MEDIA_EXTRA = "media_list";
public static final String STICKER_EXTRA = "sticker_extra";
public static final String DISTRIBUTION_TYPE_EXTRA = "distribution_type";
- public static final String TIMING_EXTRA = "timing";
public static final String LAST_SEEN_EXTRA = "last_seen";
public static final String STARTING_POSITION_EXTRA = "starting_position";
@@ -341,6 +339,23 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
+ public static @NonNull Intent buildIntent(@NonNull Context context,
+ @NonNull RecipientId recipientId,
+ long threadId,
+ int distributionType,
+ long lastSeen,
+ int startingPosition)
+ {
+ Intent intent = new Intent(context, ConversationActivity.class);
+ intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
+ intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
+ intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
+ intent.putExtra(ConversationActivity.LAST_SEEN_EXTRA, lastSeen);
+ intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, startingPosition);
+
+ return intent;
+ }
+
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
@@ -355,7 +370,8 @@ protected void onCreate(Bundle state, boolean ready) {
if (recipientId == null) {
Log.w(TAG, "[onCreate] Missing recipientId!");
- startActivity(new Intent(this, ConversationListActivity.class));
+ // TODO [greyson] Navigation
+ startActivity(new Intent(this, MainActivity.class));
finish();
return;
}
@@ -427,7 +443,8 @@ protected void onNewIntent(Intent intent) {
if (recipientId == null) {
Log.w(TAG, "[onNewIntent] Missing recipientId!");
- startActivity(new Intent(this, ConversationListActivity.class));
+ // TODO [greyson] Navigation
+ startActivity(new Intent(this, MainActivity.class));
finish();
return;
}
@@ -470,8 +487,6 @@ protected void onResume() {
MessageNotifier.setVisibleThread(threadId);
markThreadAsRead();
-
- Log.i(TAG, "onResume() Finished: " + (System.currentTimeMillis() - getIntent().getLongExtra(TIMING_EXTRA, 0)));
}
@Override
@@ -802,7 +817,7 @@ public boolean onOptionsItemSelected(MenuItem item) {
case R.id.menu_conversation_settings: handleConversationSettings(); return true;
case R.id.menu_expiring_messages_off:
case R.id.menu_expiring_messages: handleSelectMessageExpiration(); return true;
- case android.R.id.home: handleReturnToConversationList(); return true;
+ case android.R.id.home: onBackPressed(); return true;
}
return false;
@@ -832,13 +847,6 @@ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permis
//////// Event Handlers
- private void handleReturnToConversationList() {
- Intent intent = new Intent(this, (archived ? ConversationListArchiveActivity.class : ConversationListActivity.class));
- intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
- startActivity(intent);
- finish();
- }
-
private void handleSelectMessageExpiration() {
if (isPushGroupConversation() && !isActiveGroup()) {
return;
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationSearchViewModel.java b/src/org/thoughtcrime/securesms/conversation/ConversationSearchViewModel.java
index 56e8e451320..7f3dda31268 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationSearchViewModel.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationSearchViewModel.java
@@ -5,13 +5,15 @@
import androidx.lifecycle.LiveData;
import android.content.Context;
import androidx.annotation.NonNull;
+import androidx.lifecycle.MutableLiveData;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactRepository;
+import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.database.CursorList;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.search.SearchRepository;
-import org.thoughtcrime.securesms.search.model.MessageResult;
+import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.util.CloseableLiveData;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.Util;
@@ -22,9 +24,9 @@
public class ConversationSearchViewModel extends AndroidViewModel {
- private final SearchRepository searchRepository;
- private final CloseableLiveData result;
- private final Debouncer debouncer;
+ private final SearchRepository searchRepository;
+ private final MutableLiveData result;
+ private final Debouncer debouncer;
private boolean firstSearch;
private boolean searchOpen;
@@ -33,15 +35,9 @@ public class ConversationSearchViewModel extends AndroidViewModel {
public ConversationSearchViewModel(@NonNull Application application) {
super(application);
- Context context = application.getApplicationContext();
- result = new CloseableLiveData<>();
+ result = new MutableLiveData<>();
debouncer = new Debouncer(500);
- searchRepository = new SearchRepository(context,
- DatabaseFactory.getSearchDatabase(context),
- DatabaseFactory.getThreadDatabase(context),
- new ContactRepository(application),
- ContactAccessor.getInstance(),
- SignalExecutors.SERIAL);
+ searchRepository = new SearchRepository();
}
LiveData getSearchResults() {
@@ -73,7 +69,7 @@ void onMoveUp() {
CursorList messages = (CursorList) result.getValue().getResults();
int position = Math.min(result.getValue().getPosition() + 1, messages.size() - 1);
- result.setValue(new SearchResult(messages, position), false);
+ result.setValue(new SearchResult(messages, position));
}
void onMoveDown() {
@@ -82,7 +78,7 @@ void onMoveDown() {
CursorList messages = (CursorList) result.getValue().getResults();
int position = Math.max(result.getValue().getPosition() - 1, 0);
- result.setValue(new SearchResult(messages, position), false);
+ result.setValue(new SearchResult(messages, position));
}
@@ -94,13 +90,6 @@ void onSearchOpened() {
void onSearchClosed() {
searchOpen = false;
debouncer.clear();
- result.close();
- }
-
- @Override
- protected void onCleared() {
- super.onCleared();
- result.close();
}
private void updateQuery(@NonNull String query, long threadId) {
@@ -114,20 +103,18 @@ private void updateQuery(@NonNull String query, long threadId) {
Util.runOnMain(() -> {
if (searchOpen && query.equals(activeQuery)) {
result.setValue(new SearchResult(messages, 0));
- } else {
- messages.close();
}
});
});
});
}
- static class SearchResult implements Closeable {
+ static class SearchResult {
- private final CursorList results;
- private final int position;
+ private final List results;
+ private final int position;
- SearchResult(CursorList results, int position) {
+ SearchResult(@NonNull List results, int position) {
this.results = results;
this.position = position;
}
@@ -139,10 +126,5 @@ public List getResults() {
public int getPosition() {
return position;
}
-
- @Override
- public void close() {
- results.close();
- }
}
}
diff --git a/src/org/thoughtcrime/securesms/ConversationListAdapter.java b/src/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java
similarity index 97%
rename from src/org/thoughtcrime/securesms/ConversationListAdapter.java
rename to src/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java
index 63746d69b43..53c0491f7da 100644
--- a/src/org/thoughtcrime/securesms/ConversationListAdapter.java
+++ b/src/org/thoughtcrime/securesms/conversationlist/ConversationListAdapter.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.thoughtcrime.securesms;
+package org.thoughtcrime.securesms.conversationlist;
import android.content.Context;
import android.database.Cursor;
@@ -25,6 +25,8 @@
import android.view.View;
import android.view.ViewGroup;
+import org.thoughtcrime.securesms.BindableConversationListItem;
+import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;
diff --git a/src/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java b/src/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java
new file mode 100644
index 00000000000..b6673fe02e0
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright (C) 2015 Open Whisper Systems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.thoughtcrime.securesms.conversationlist;
+
+import android.annotation.SuppressLint;
+import android.database.Cursor;
+import android.os.AsyncTask;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.MenuRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.annotation.WorkerThread;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.view.ActionMode;
+import androidx.appcompat.widget.Toolbar;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.Loader;
+import androidx.recyclerview.widget.RecyclerView;
+
+import com.google.android.material.snackbar.Snackbar;
+
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
+import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.loaders.ConversationListLoader;
+import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
+
+
+public class ConversationListArchiveFragment extends ConversationListFragment
+ implements LoaderManager.LoaderCallbacks, ActionMode.Callback, ItemClickListener
+{
+ private RecyclerView list;
+ private View emptyState;
+ private PulsingFloatingActionButton fab;
+ private PulsingFloatingActionButton cameraFab;
+
+ public static ConversationListArchiveFragment newInstance() {
+ return new ConversationListArchiveFragment();
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setHasOptionsMenu(false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ list = view.findViewById(R.id.list);
+ fab = view.findViewById(R.id.fab);
+ cameraFab = view.findViewById(R.id.camera_fab);
+ emptyState = view.findViewById(R.id.empty_state);
+
+ ((AppCompatActivity) requireActivity()).getSupportActionBar().setDisplayHomeAsUpEnabled(true);
+ Toolbar toolbar = view.findViewById(R.id.toolbar_basic);
+ toolbar.setNavigationOnClickListener(v -> requireActivity().onBackPressed());
+ toolbar.setTitle(R.string.AndroidManifest_archived_conversations);
+
+ fab.hide();
+ cameraFab.hide();
+ }
+
+ @Override
+ public @NonNull Loader onCreateLoader(int arg0, Bundle arg1) {
+ return new ConversationListLoader(getActivity(), null, true);
+ }
+
+ @Override
+ public void onLoadFinished(@NonNull Loader arg0, Cursor cursor) {
+ super.onLoadFinished(arg0, cursor);
+
+ list.setVisibility(View.VISIBLE);
+ emptyState.setVisibility(View.GONE);
+ }
+
+ @Override
+ protected int getToolbarRes() {
+ return R.id.toolbar_basic;
+ }
+
+ @Override
+ protected @StringRes int getArchivedSnackbarTitleRes() {
+ return R.plurals.ConversationListFragment_moved_conversations_to_inbox;
+ }
+
+ @Override
+ protected @MenuRes int getActionModeMenuRes() {
+ return R.menu.conversation_list_batch_unarchive;
+ }
+
+ @Override
+ protected @DrawableRes int getArchiveIconRes() {
+ return R.drawable.ic_unarchive_white_36dp;
+ }
+
+ @Override
+ protected void archiveThread(long threadId) {
+ DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId);
+ }
+
+ @WorkerThread
+ protected void reverseArchiveThread(long threadId) {
+ DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId);
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ @Override
+ protected void onItemSwiped(long threadId, int unreadCount) {
+ new SnackbarAsyncTask(getView(),
+ getResources().getQuantityString(R.plurals.ConversationListFragment_moved_conversations_to_inbox, 1, 1),
+ getString(R.string.ConversationListFragment_undo),
+ getResources().getColor(R.color.amber_500),
+ Snackbar.LENGTH_LONG, false)
+ {
+ @Override
+ protected void executeAction(@Nullable Long parameter) {
+ DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId);
+ }
+
+ @Override
+ protected void reverseAction(@Nullable Long parameter) {
+ DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId);
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId);
+ }
+}
+
+
diff --git a/src/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/src/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
new file mode 100644
index 00000000000..b80e030f303
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
@@ -0,0 +1,880 @@
+/*
+ * Copyright (C) 2015 Open Whisper Systems
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+package org.thoughtcrime.securesms.conversationlist;
+
+import android.Manifest;
+import android.annotation.SuppressLint;
+import android.app.ProgressDialog;
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Build;
+import android.os.Bundle;
+
+import androidx.annotation.DrawableRes;
+import androidx.annotation.IdRes;
+import androidx.annotation.MenuRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.bumptech.glide.load.engine.DiskCacheStrategy;
+import com.google.android.material.snackbar.Snackbar;
+
+import androidx.annotation.PluralsRes;
+import androidx.annotation.WorkerThread;
+import androidx.appcompat.widget.Toolbar;
+import androidx.appcompat.widget.TooltipCompat;
+import androidx.lifecycle.ViewModelProviders;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.Loader;
+import androidx.appcompat.app.AlertDialog;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.view.ActionMode;
+import androidx.recyclerview.widget.LinearLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.recyclerview.widget.ItemTouchHelper;
+import android.text.TextUtils;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.Toast;
+
+import org.greenrobot.eventbus.EventBus;
+import org.greenrobot.eventbus.Subscribe;
+import org.greenrobot.eventbus.ThreadMode;
+import org.thoughtcrime.securesms.ApplicationContext;
+import org.thoughtcrime.securesms.MainFragment;
+import org.thoughtcrime.securesms.MainNavigator;
+import org.thoughtcrime.securesms.NewConversationActivity;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.conversationlist.ConversationListAdapter.ItemClickListener;
+import org.thoughtcrime.securesms.color.MaterialColor;
+import org.thoughtcrime.securesms.components.RatingManager;
+import org.thoughtcrime.securesms.components.SearchToolbar;
+import org.thoughtcrime.securesms.components.recyclerview.DeleteItemAnimator;
+import org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton;
+import org.thoughtcrime.securesms.components.reminder.DefaultSmsReminder;
+import org.thoughtcrime.securesms.components.reminder.DozeReminder;
+import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
+import org.thoughtcrime.securesms.components.reminder.OutdatedBuildReminder;
+import org.thoughtcrime.securesms.components.reminder.PushRegistrationReminder;
+import org.thoughtcrime.securesms.components.reminder.Reminder;
+import org.thoughtcrime.securesms.components.reminder.ReminderView;
+import org.thoughtcrime.securesms.components.reminder.ServiceOutageReminder;
+import org.thoughtcrime.securesms.components.reminder.ShareReminder;
+import org.thoughtcrime.securesms.components.reminder.SystemSmsImportReminder;
+import org.thoughtcrime.securesms.components.reminder.UnauthorizedReminder;
+import org.thoughtcrime.securesms.contacts.avatars.ContactColors;
+import org.thoughtcrime.securesms.contacts.avatars.GeneratedContactPhoto;
+import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
+import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
+import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.MessagingDatabase.MarkedMessageInfo;
+import org.thoughtcrime.securesms.database.ThreadDatabase;
+import org.thoughtcrime.securesms.database.loaders.ConversationListLoader;
+import org.thoughtcrime.securesms.database.model.ThreadRecord;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
+import org.thoughtcrime.securesms.insights.InsightsLauncher;
+import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
+import org.thoughtcrime.securesms.lock.RegistrationLockDialog;
+import org.thoughtcrime.securesms.logging.Log;
+import org.thoughtcrime.securesms.mediasend.MediaSendActivity;
+import org.thoughtcrime.securesms.mms.GlideApp;
+import org.thoughtcrime.securesms.notifications.MarkReadReceiver;
+import org.thoughtcrime.securesms.notifications.MessageNotifier;
+import org.thoughtcrime.securesms.permissions.Permissions;
+import org.thoughtcrime.securesms.recipients.Recipient;
+import org.thoughtcrime.securesms.service.KeyCachingService;
+import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
+import org.thoughtcrime.securesms.util.TextSecurePreferences;
+import org.thoughtcrime.securesms.util.Util;
+import org.thoughtcrime.securesms.util.ViewUtil;
+import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
+import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
+import org.thoughtcrime.securesms.util.task.SnackbarAsyncTask;
+import org.whispersystems.libsignal.util.guava.Optional;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+
+
+public class ConversationListFragment extends MainFragment implements LoaderManager.LoaderCallbacks,
+ ActionMode.Callback,
+ ItemClickListener,
+ ConversationListSearchAdapter.EventListener,
+ MainNavigator.BackHandler
+{
+ private static final String TAG = Log.tag(ConversationListFragment.class);
+
+ private static final int[] EMPTY_IMAGES = new int[] { R.drawable.empty_inbox_1,
+ R.drawable.empty_inbox_2,
+ R.drawable.empty_inbox_3,
+ R.drawable.empty_inbox_4,
+ R.drawable.empty_inbox_5 };
+
+ private ActionMode actionMode;
+ private RecyclerView list;
+ private ReminderView reminderView;
+ private View emptyState;
+ private ImageView emptyImage;
+ private TextView searchEmptyState;
+ private PulsingFloatingActionButton fab;
+ private PulsingFloatingActionButton cameraFab;
+ private SearchToolbar searchToolbar;
+ private ImageView searchAction;
+ private View toolbarShadow;
+ private ConversationListViewModel viewModel;
+ private RecyclerView.Adapter activeAdapter;
+ private ConversationListAdapter defaultAdapter;
+ private ConversationListSearchAdapter searchAdapter;
+ private StickyHeaderDecoration searchAdapterDecoration;
+
+ public static ConversationListFragment newInstance() {
+ return new ConversationListFragment();
+ }
+
+ @Override
+ public void onCreate(Bundle icicle) {
+ super.onCreate(icicle);
+ setHasOptionsMenu(true);
+ }
+
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
+ return inflater.inflate(R.layout.conversation_list_fragment, container, false);
+ }
+
+ @Override
+ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
+ reminderView = view.findViewById(R.id.reminder);
+ list = view.findViewById(R.id.list);
+ fab = view.findViewById(R.id.fab);
+ cameraFab = view.findViewById(R.id.camera_fab);
+ emptyState = view.findViewById(R.id.empty_state);
+ emptyImage = view.findViewById(R.id.empty);
+ searchEmptyState = view.findViewById(R.id.search_no_results);
+ searchToolbar = view.findViewById(R.id.search_toolbar);
+ searchAction = view.findViewById(R.id.search_action);
+ toolbarShadow = view.findViewById(R.id.conversation_list_toolbar_shadow);
+
+ Toolbar toolbar = view.findViewById(getToolbarRes());
+ toolbar.setVisibility(View.VISIBLE);
+ ((AppCompatActivity) requireActivity()).setSupportActionBar(toolbar);
+
+ fab.show();
+ cameraFab.show();
+
+ reminderView.setOnDismissListener(this::updateReminders);
+
+ list.setHasFixedSize(true);
+ list.setLayoutManager(new LinearLayoutManager(getActivity()));
+ list.setItemAnimator(new DeleteItemAnimator());
+ list.addOnScrollListener(new ScrollListener());
+
+ new ItemTouchHelper(new ArchiveListenerCallback()).attachToRecyclerView(list);
+
+ fab.setOnClickListener(v -> startActivity(new Intent(getActivity(), NewConversationActivity.class)));
+ cameraFab.setOnClickListener(v -> {
+ Permissions.with(requireActivity())
+ .request(Manifest.permission.CAMERA)
+ .ifNecessary()
+ .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_camera_solid_24)
+ .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video))
+ .onAllGranted(() -> startActivity(MediaSendActivity.buildCameraFirstIntent(requireActivity())))
+ .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show())
+ .execute();
+ });
+
+ initializeListAdapters();
+ initializeViewModel();
+ initializeTypingObserver();
+ initializeSearchListener();
+
+ RatingManager.showRatingDialogIfNecessary(requireContext());
+ RegistrationLockDialog.showReminderIfNecessary(requireContext());
+
+ TooltipCompat.setTooltipText(searchAction, getText(R.string.SearchToolbar_search_for_conversations_contacts_and_messages));
+ }
+
+ @Override
+ public void onResume() {
+ super.onResume();
+
+ updateReminders();
+ list.getAdapter().notifyDataSetChanged();
+ EventBus.getDefault().register(this);
+
+ if (TextSecurePreferences.isSmsEnabled(requireContext())) {
+ InsightsLauncher.showInsightsModal(requireContext(), requireFragmentManager());
+ }
+
+ SimpleTask.run(getLifecycle(), Recipient::self, this::initializeProfileIcon);
+
+ if (!searchToolbar.isVisible() && list.getAdapter() != defaultAdapter) {
+ activeAdapter = defaultAdapter;
+ list.removeItemDecoration(searchAdapterDecoration);
+ list.setAdapter(defaultAdapter);
+ }
+ }
+
+ @Override
+ public void onPause() {
+ super.onPause();
+
+ fab.stopPulse();
+ cameraFab.stopPulse();
+ EventBus.getDefault().unregister(this);
+ }
+
+
+ @Override
+ public void onPrepareOptionsMenu(Menu menu) {
+ MenuInflater inflater = requireActivity().getMenuInflater();
+ menu.clear();
+
+ inflater.inflate(R.menu.text_secure_normal, menu);
+
+ menu.findItem(R.id.menu_insights).setVisible(TextSecurePreferences.isSmsEnabled(requireContext()));
+ menu.findItem(R.id.menu_clear_passphrase).setVisible(!TextSecurePreferences.isPasswordDisabled(requireContext()));
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(@NonNull MenuItem item) {
+ super.onOptionsItemSelected(item);
+
+ switch (item.getItemId()) {
+ case R.id.menu_new_group: handleCreateGroup(); return true;
+ case R.id.menu_settings: handleDisplaySettings(); return true;
+ case R.id.menu_clear_passphrase: handleClearPassphrase(); return true;
+ case R.id.menu_mark_all_read: handleMarkAllRead(); return true;
+ case R.id.menu_invite: handleInvite(); return true;
+ case R.id.menu_insights: handleInsights(); return true;
+ case R.id.menu_help: handleHelp(); return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public boolean onBackPressed() {
+ if (searchToolbar.isVisible() || activeAdapter == searchAdapter) {
+ activeAdapter = defaultAdapter;
+ list.removeItemDecoration(searchAdapterDecoration);
+ list.setAdapter(defaultAdapter);
+ searchToolbar.collapse();
+ return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onConversationClicked(@NonNull ThreadRecord threadRecord) {
+ getNavigator().goToConversation(threadRecord.getRecipient().getId(),
+ threadRecord.getThreadId(),
+ threadRecord.getDistributionType(),
+ threadRecord.getLastSeen(),
+ -1);
+ }
+
+ @Override
+ public void onContactClicked(@NonNull Recipient contact) {
+ SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
+ return DatabaseFactory.getThreadDatabase(getContext()).getThreadIdIfExistsFor(contact);
+ }, threadId -> {
+ getNavigator().goToConversation(contact.getId(),
+ threadId,
+ ThreadDatabase.DistributionTypes.DEFAULT,
+ -1,
+ -1);
+ });
+ }
+
+ @Override
+ public void onMessageClicked(@NonNull MessageResult message) {
+ SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
+ int startingPosition = DatabaseFactory.getMmsSmsDatabase(getContext()).getMessagePositionInConversation(message.threadId, message.receivedTimestampMs);
+ return Math.max(0, startingPosition);
+ }, startingPosition -> {
+ getNavigator().goToConversation(message.conversationRecipient.getId(),
+ message.threadId,
+ ThreadDatabase.DistributionTypes.DEFAULT,
+ -1,
+ startingPosition);
+ });
+ }
+
+ private void initializeProfileIcon(@NonNull Recipient recipient) {
+ ImageView icon = requireView().findViewById(R.id.toolbar_icon);
+ String name = Optional.fromNullable(recipient.getDisplayName(requireContext())).or(Optional.fromNullable(TextSecurePreferences.getProfileName(requireContext()))).or("");
+ MaterialColor fallbackColor = recipient.getColor();
+
+ if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
+ fallbackColor = ContactColors.generateFor(name);
+ }
+
+ Drawable fallback = new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40).asDrawable(requireContext(), fallbackColor.toAvatarColor(requireContext()));
+
+ GlideApp.with(this)
+ .load(new ProfileContactPhoto(recipient.getId(), String.valueOf(TextSecurePreferences.getProfileAvatarId(requireContext()))))
+ .error(fallback)
+ .circleCrop()
+ .diskCacheStrategy(DiskCacheStrategy.ALL)
+ .into(icon);
+
+ icon.setOnClickListener(v -> getNavigator().goToAppSettings());
+ }
+
+ private void initializeSearchListener() {
+ searchAction.setOnClickListener(v -> {
+ searchToolbar.display(searchAction.getX() + (searchAction.getWidth() / 2.0f),
+ searchAction.getY() + (searchAction.getHeight() / 2.0f));
+ });
+
+ searchToolbar.setListener(new SearchToolbar.SearchListener() {
+ @Override
+ public void onSearchTextChange(String text) {
+ String trimmed = text.trim();
+
+ viewModel.updateQuery(trimmed);
+
+ if (trimmed.length() > 0) {
+ if (activeAdapter != searchAdapter) {
+ activeAdapter = searchAdapter;
+ list.setAdapter(searchAdapter);
+ list.removeItemDecoration(searchAdapterDecoration);
+ list.addItemDecoration(searchAdapterDecoration);
+ }
+ } else {
+ if (activeAdapter != defaultAdapter) {
+ activeAdapter = defaultAdapter;
+ list.removeItemDecoration(searchAdapterDecoration);
+ list.setAdapter(defaultAdapter);
+ }
+ }
+ }
+
+ @Override
+ public void onSearchClosed() {
+ list.removeItemDecoration(searchAdapterDecoration);
+ list.setAdapter(defaultAdapter);
+ }
+ });
+ }
+
+ private void initializeListAdapters() {
+ defaultAdapter = new ConversationListAdapter (requireContext(), GlideApp.with(this), Locale.getDefault(), null, this);
+ searchAdapter = new ConversationListSearchAdapter(GlideApp.with(this), this, Locale.getDefault () );
+ searchAdapterDecoration = new StickyHeaderDecoration(searchAdapter, false, false);
+ activeAdapter = defaultAdapter;
+
+ list.setAdapter(defaultAdapter);
+ LoaderManager.getInstance(this).restartLoader(0, null, this);
+ }
+
+ private void initializeTypingObserver() {
+ ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().getTypingThreads().observe(this, threadIds -> {
+ if (threadIds == null) {
+ threadIds = Collections.emptySet();
+ }
+
+ defaultAdapter.setTypingThreads(threadIds);
+ });
+ }
+
+ private void initializeViewModel() {
+ viewModel = ViewModelProviders.of(this, new ConversationListViewModel.Factory()).get(ConversationListViewModel.class);
+
+ viewModel.getSearchResult().observe(this, result -> {
+ result = result != null ? result : SearchResult.EMPTY;
+ searchAdapter.updateResults(result);
+
+ if (result.isEmpty() && activeAdapter == searchAdapter) {
+ searchEmptyState.setText(getString(R.string.SearchFragment_no_results, result.getQuery()));
+ searchEmptyState.setVisibility(View.VISIBLE);
+ } else {
+ searchEmptyState.setVisibility(View.GONE);
+ }
+ });
+ }
+
+ private void updateReminders() {
+ Context context = requireContext();
+
+ SimpleTask.run(getViewLifecycleOwner().getLifecycle(), () -> {
+ if (UnauthorizedReminder.isEligible(context)) {
+ return Optional.of(new UnauthorizedReminder(context));
+ } else if (ExpiredBuildReminder.isEligible()) {
+ return Optional.of(new ExpiredBuildReminder(context));
+ } else if (ServiceOutageReminder.isEligible(context)) {
+ ApplicationDependencies.getJobManager().add(new ServiceOutageDetectionJob());
+ return Optional.of(new ServiceOutageReminder(context));
+ } else if (OutdatedBuildReminder.isEligible()) {
+ return Optional.of(new OutdatedBuildReminder(context));
+ } else if (DefaultSmsReminder.isEligible(context)) {
+ return Optional.of(new DefaultSmsReminder(context));
+ } else if (Util.isDefaultSmsProvider(context) && SystemSmsImportReminder.isEligible(context)) {
+ return Optional.of((new SystemSmsImportReminder(context)));
+ } else if (PushRegistrationReminder.isEligible(context)) {
+ return Optional.of((new PushRegistrationReminder(context)));
+ } else if (ShareReminder.isEligible(context)) {
+ return Optional.of(new ShareReminder(context));
+ } else if (DozeReminder.isEligible(context)) {
+ return Optional.of(new DozeReminder(context));
+ } else {
+ return Optional.absent();
+ }
+ }, reminder -> {
+ if (reminder.isPresent() && getActivity() != null && !isRemoving()) {
+ reminderView.showReminder(reminder.get());
+ } else if (!reminder.isPresent()) {
+ reminderView.hide();
+ }
+ });
+ }
+
+ private void handleCreateGroup() {
+ getNavigator().goToGroupCreation();
+ }
+
+ private void handleDisplaySettings() {
+ getNavigator().goToAppSettings();
+ }
+
+ private void handleClearPassphrase() {
+ Intent intent = new Intent(requireActivity(), KeyCachingService.class);
+ intent.setAction(KeyCachingService.CLEAR_KEY_ACTION);
+ requireActivity().startService(intent);
+ }
+
+ private void handleMarkAllRead() {
+ Context context = requireContext();
+
+ SignalExecutors.BOUNDED.execute(() -> {
+ List messageIds = DatabaseFactory.getThreadDatabase(context).setAllThreadsRead();
+
+ MessageNotifier.updateNotification(context);
+ MarkReadReceiver.process(context, messageIds);
+ });
+ }
+
+ private void handleInvite() {
+ getNavigator().goToInvite();
+ }
+
+ private void handleInsights() {
+ getNavigator().goToInsights();
+ }
+
+ private void handleHelp() {
+ try {
+ startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse("https://support.signal.org")));
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(requireActivity(), R.string.ConversationListActivity_there_is_no_browser_installed_on_your_device, Toast.LENGTH_LONG).show();
+ }
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ private void handleArchiveAllSelected() {
+ Set selectedConversations = new HashSet<>(defaultAdapter.getBatchSelections());
+ int count = selectedConversations.size();
+ String snackBarTitle = getResources().getQuantityString(getArchivedSnackbarTitleRes(), count, count);
+
+ new SnackbarAsyncTask(getView(),
+ snackBarTitle,
+ getString(R.string.ConversationListFragment_undo),
+ getResources().getColor(R.color.amber_500),
+ Snackbar.LENGTH_LONG, true)
+ {
+
+ @Override
+ protected void onPostExecute(Void result) {
+ super.onPostExecute(result);
+
+ if (actionMode != null) {
+ actionMode.finish();
+ actionMode = null;
+ }
+ }
+
+ @Override
+ protected void executeAction(@Nullable Void parameter) {
+ for (long threadId : selectedConversations) {
+ archiveThread(threadId);
+ }
+ }
+
+ @Override
+ protected void reverseAction(@Nullable Void parameter) {
+ for (long threadId : selectedConversations) {
+ reverseArchiveThread(threadId);
+ }
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ private void handleDeleteAllSelected() {
+ int conversationsCount = defaultAdapter.getBatchSelections().size();
+ AlertDialog.Builder alert = new AlertDialog.Builder(getActivity());
+ alert.setIconAttribute(R.attr.dialog_alert_icon);
+ alert.setTitle(getActivity().getResources().getQuantityString(R.plurals.ConversationListFragment_delete_selected_conversations,
+ conversationsCount, conversationsCount));
+ alert.setMessage(getActivity().getResources().getQuantityString(R.plurals.ConversationListFragment_this_will_permanently_delete_all_n_selected_conversations,
+ conversationsCount, conversationsCount));
+ alert.setCancelable(true);
+
+ alert.setPositiveButton(R.string.delete, (dialog, which) -> {
+ final Set selectedConversations = defaultAdapter.getBatchSelections();
+
+ if (!selectedConversations.isEmpty()) {
+ new AsyncTask() {
+ private ProgressDialog dialog;
+
+ @Override
+ protected void onPreExecute() {
+ dialog = ProgressDialog.show(getActivity(),
+ getActivity().getString(R.string.ConversationListFragment_deleting),
+ getActivity().getString(R.string.ConversationListFragment_deleting_selected_conversations),
+ true, false);
+ }
+
+ @Override
+ protected Void doInBackground(Void... params) {
+ DatabaseFactory.getThreadDatabase(getActivity()).deleteConversations(selectedConversations);
+ MessageNotifier.updateNotification(getActivity());
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void result) {
+ dialog.dismiss();
+ if (actionMode != null) {
+ actionMode.finish();
+ actionMode = null;
+ }
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+ }
+ });
+
+ alert.setNegativeButton(android.R.string.cancel, null);
+ alert.show();
+ }
+
+ private void handleSelectAllThreads() {
+ defaultAdapter.selectAllThreads();
+ actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelections().size()));
+ }
+
+ private void handleCreateConversation(long threadId, Recipient recipient, int distributionType, long lastSeen) {
+ getNavigator().goToConversation(recipient.getId(), threadId, distributionType, lastSeen, -1);
+ }
+
+ @Override
+ public @NonNull Loader onCreateLoader(int arg0, Bundle arg1) {
+ return new ConversationListLoader(getActivity(), null, false);
+ }
+
+ @Override
+ public void onLoadFinished(@NonNull Loader arg0, Cursor cursor) {
+ if (cursor == null || cursor.getCount() <= 0) {
+ list.setVisibility(View.INVISIBLE);
+ emptyState.setVisibility(View.VISIBLE);
+ emptyImage.setImageResource(EMPTY_IMAGES[(int) (Math.random() * EMPTY_IMAGES.length)]);
+ fab.startPulse(3 * 1000);
+ cameraFab.startPulse(3 * 1000);
+ } else {
+ list.setVisibility(View.VISIBLE);
+ emptyState.setVisibility(View.GONE);
+ fab.stopPulse();
+ cameraFab.stopPulse();
+ }
+
+ defaultAdapter.changeCursor(cursor);
+ }
+
+ @Override
+ public void onLoaderReset(@NonNull Loader arg0) {
+ defaultAdapter.changeCursor(null);
+ }
+
+ @Override
+ public void onItemClick(ConversationListItem item) {
+ if (actionMode == null) {
+ handleCreateConversation(item.getThreadId(), item.getRecipient(),
+ item.getDistributionType(), item.getLastSeen());
+ } else {
+ ConversationListAdapter adapter = (ConversationListAdapter)list.getAdapter();
+ adapter.toggleThreadInBatchSet(item.getThreadId());
+
+ if (adapter.getBatchSelections().size() == 0) {
+ actionMode.finish();
+ } else {
+ actionMode.setTitle(String.valueOf(defaultAdapter.getBatchSelections().size()));
+ }
+
+ adapter.notifyDataSetChanged();
+ }
+ }
+
+ @Override
+ public void onItemLongClick(ConversationListItem item) {
+ actionMode = ((AppCompatActivity)getActivity()).startSupportActionMode(ConversationListFragment.this);
+
+ defaultAdapter.initializeBatchMode(true);
+ defaultAdapter.toggleThreadInBatchSet(item.getThreadId());
+ defaultAdapter.notifyDataSetChanged();
+ }
+
+ @Override
+ public void onSwitchToArchive() {
+ getNavigator().goToArchiveList();
+ }
+
+ @Override
+ public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+ MenuInflater inflater = getActivity().getMenuInflater();
+
+ inflater.inflate(getActionModeMenuRes(), menu);
+ inflater.inflate(R.menu.conversation_list_batch, menu);
+
+ mode.setTitle("1");
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ getActivity().getWindow().setStatusBarColor(getResources().getColor(R.color.action_mode_status_bar));
+ }
+
+ if (Build.VERSION.SDK_INT >= 23) {
+ int current = getActivity().getWindow().getDecorView().getSystemUiVisibility();
+ getActivity().getWindow().getDecorView().setSystemUiVisibility(current & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ }
+
+ return true;
+ }
+
+ @Override
+ public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+ return false;
+ }
+
+ @Override
+ public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_select_all: handleSelectAllThreads(); return true;
+ case R.id.menu_delete_selected: handleDeleteAllSelected(); return true;
+ case R.id.menu_archive_selected: handleArchiveAllSelected(); return true;
+ }
+
+ return false;
+ }
+
+ @Override
+ public void onDestroyActionMode(ActionMode mode) {
+ defaultAdapter.initializeBatchMode(false);
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ TypedArray color = getActivity().getTheme().obtainStyledAttributes(new int[] {android.R.attr.statusBarColor});
+ getActivity().getWindow().setStatusBarColor(color.getColor(0, Color.BLACK));
+ color.recycle();
+ }
+
+ if (Build.VERSION.SDK_INT >= 23) {
+ TypedArray lightStatusBarAttr = getActivity().getTheme().obtainStyledAttributes(new int[] {android.R.attr.windowLightStatusBar});
+ int current = getActivity().getWindow().getDecorView().getSystemUiVisibility();
+ int statusBarMode = lightStatusBarAttr.getBoolean(0, false) ? current | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
+ : current & ~View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR;
+
+ getActivity().getWindow().getDecorView().setSystemUiVisibility(statusBarMode);
+
+ lightStatusBarAttr.recycle();
+ }
+
+ actionMode = null;
+ }
+
+ @Subscribe(threadMode = ThreadMode.MAIN)
+ public void onEvent(ReminderUpdateEvent event) {
+ updateReminders();
+ }
+
+ protected @IdRes int getToolbarRes() {
+ return R.id.toolbar;
+ }
+
+ protected @PluralsRes int getArchivedSnackbarTitleRes() {
+ return R.plurals.ConversationListFragment_conversations_archived;
+ }
+
+ protected @MenuRes int getActionModeMenuRes() {
+ return R.menu.conversation_list_batch_archive;
+ }
+
+ protected @DrawableRes int getArchiveIconRes() {
+ return R.drawable.ic_archive_white_36dp;
+ }
+
+ @WorkerThread
+ protected void archiveThread(long threadId) {
+ DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId);
+ }
+
+ @WorkerThread
+ protected void reverseArchiveThread(long threadId) {
+ DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId);
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ protected void onItemSwiped(long threadId, int unreadCount) {
+ new SnackbarAsyncTask(getView(),
+ getResources().getQuantityString(R.plurals.ConversationListFragment_conversations_archived, 1, 1),
+ getString(R.string.ConversationListFragment_undo),
+ getResources().getColor(R.color.amber_500),
+ Snackbar.LENGTH_LONG, false)
+ {
+ @Override
+ protected void executeAction(@Nullable Long parameter) {
+ DatabaseFactory.getThreadDatabase(getActivity()).archiveConversation(threadId);
+
+ if (unreadCount > 0) {
+ List messageIds = DatabaseFactory.getThreadDatabase(getActivity()).setRead(threadId, false);
+ MessageNotifier.updateNotification(getActivity());
+ MarkReadReceiver.process(getActivity(), messageIds);
+ }
+ }
+
+ @Override
+ protected void reverseAction(@Nullable Long parameter) {
+ DatabaseFactory.getThreadDatabase(getActivity()).unarchiveConversation(threadId);
+
+ if (unreadCount > 0) {
+ DatabaseFactory.getThreadDatabase(getActivity()).incrementUnread(threadId, unreadCount);
+ MessageNotifier.updateNotification(getActivity());
+ }
+ }
+ }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, threadId);
+ }
+
+ private class ArchiveListenerCallback extends ItemTouchHelper.SimpleCallback {
+
+ ArchiveListenerCallback() {
+ super(0, ItemTouchHelper.RIGHT);
+ }
+
+ @Override
+ public boolean onMove(@NonNull RecyclerView recyclerView,
+ @NonNull RecyclerView.ViewHolder viewHolder,
+ @NonNull RecyclerView.ViewHolder target)
+ {
+ return false;
+ }
+
+ @Override
+ public int getSwipeDirs(@NonNull RecyclerView recyclerView, @NonNull RecyclerView.ViewHolder viewHolder) {
+ if (viewHolder.itemView instanceof ConversationListItemAction) {
+ return 0;
+ }
+
+ if (actionMode != null) {
+ return 0;
+ }
+
+ return super.getSwipeDirs(recyclerView, viewHolder);
+ }
+
+ @SuppressLint("StaticFieldLeak")
+ @Override
+ public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) {
+ if (viewHolder.itemView instanceof ConversationListItemInboxZero) return;
+ final long threadId = ((ConversationListItem)viewHolder.itemView).getThreadId();
+ final int unreadCount = ((ConversationListItem)viewHolder.itemView).getUnreadCount();
+
+ onItemSwiped(threadId, unreadCount);
+ }
+
+ @Override
+ public void onChildDraw(@NonNull Canvas c, @NonNull RecyclerView recyclerView,
+ @NonNull RecyclerView.ViewHolder viewHolder,
+ float dX, float dY, int actionState,
+ boolean isCurrentlyActive)
+ {
+ if (viewHolder.itemView instanceof ConversationListItemInboxZero) return;
+ if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
+ View itemView = viewHolder.itemView;
+ Paint p = new Paint();
+ float alpha = 1.0f - Math.abs(dX) / (float) viewHolder.itemView.getWidth();
+
+ if (dX > 0) {
+ Bitmap icon = BitmapFactory.decodeResource(getResources(), getArchiveIconRes());
+
+ if (alpha > 0) p.setColor(getResources().getColor(R.color.green_500));
+ else p.setColor(Color.WHITE);
+
+ c.drawRect((float) itemView.getLeft(), (float) itemView.getTop(), dX,
+ (float) itemView.getBottom(), p);
+
+ c.drawBitmap(icon,
+ (float) itemView.getLeft() + getResources().getDimension(R.dimen.conversation_list_fragment_archive_padding),
+ (float) itemView.getTop() + ((float) itemView.getBottom() - (float) itemView.getTop() - icon.getHeight())/2,
+ p);
+ }
+
+ viewHolder.itemView.setAlpha(alpha);
+ viewHolder.itemView.setTranslationX(dX);
+ } else {
+ super.onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive);
+ }
+ }
+ }
+
+ private class ScrollListener extends RecyclerView.OnScrollListener {
+ @Override
+ public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
+ if (recyclerView.canScrollVertically(-1)) {
+ if (toolbarShadow.getVisibility() != View.VISIBLE) {
+ ViewUtil.fadeIn(toolbarShadow, 250);
+ }
+ } else {
+ if (toolbarShadow.getVisibility() != View.GONE) {
+ ViewUtil.fadeOut(toolbarShadow, 250);
+ }
+ }
+ }
+ }
+}
+
+
diff --git a/src/org/thoughtcrime/securesms/ConversationListItem.java b/src/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java
similarity index 97%
rename from src/org/thoughtcrime/securesms/ConversationListItem.java
rename to src/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java
index a7b4bf806ea..87ccb53d5f3 100644
--- a/src/org/thoughtcrime/securesms/ConversationListItem.java
+++ b/src/org/thoughtcrime/securesms/conversationlist/ConversationListItem.java
@@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
-package org.thoughtcrime.securesms;
+package org.thoughtcrime.securesms.conversationlist;
import android.content.Context;
import android.content.res.ColorStateList;
@@ -32,6 +32,9 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
+import org.thoughtcrime.securesms.BindableConversationListItem;
+import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.Unbindable;
import org.thoughtcrime.securesms.components.AlertView;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
@@ -43,7 +46,7 @@
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
-import org.thoughtcrime.securesms.search.model.MessageResult;
+import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;
@@ -55,7 +58,7 @@
public class ConversationListItem extends RelativeLayout
implements RecipientForeverObserver,
- BindableConversationListItem, Unbindable
+ BindableConversationListItem, Unbindable
{
@SuppressWarnings("unused")
private final static String TAG = ConversationListItem.class.getSimpleName();
diff --git a/src/org/thoughtcrime/securesms/ConversationListItemAction.java b/src/org/thoughtcrime/securesms/conversationlist/ConversationListItemAction.java
similarity index 91%
rename from src/org/thoughtcrime/securesms/ConversationListItemAction.java
rename to src/org/thoughtcrime/securesms/conversationlist/ConversationListItemAction.java
index c30e429beca..0bf81c00529 100644
--- a/src/org/thoughtcrime/securesms/ConversationListItemAction.java
+++ b/src/org/thoughtcrime/securesms/conversationlist/ConversationListItemAction.java
@@ -1,4 +1,4 @@
-package org.thoughtcrime.securesms;
+package org.thoughtcrime.securesms.conversationlist;
import android.annotation.TargetApi;
import android.content.Context;
@@ -8,6 +8,8 @@
import android.widget.LinearLayout;
import android.widget.TextView;
+import org.thoughtcrime.securesms.BindableConversationListItem;
+import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.ViewUtil;
diff --git a/src/org/thoughtcrime/securesms/ConversationListItemInboxZero.java b/src/org/thoughtcrime/securesms/conversationlist/ConversationListItemInboxZero.java
similarity index 89%
rename from src/org/thoughtcrime/securesms/ConversationListItemInboxZero.java
rename to src/org/thoughtcrime/securesms/conversationlist/ConversationListItemInboxZero.java
index c24063b0e53..638b6905f40 100644
--- a/src/org/thoughtcrime/securesms/ConversationListItemInboxZero.java
+++ b/src/org/thoughtcrime/securesms/conversationlist/ConversationListItemInboxZero.java
@@ -1,4 +1,4 @@
-package org.thoughtcrime.securesms;
+package org.thoughtcrime.securesms.conversationlist;
import android.content.Context;
@@ -9,13 +9,14 @@
import android.util.AttributeSet;
import android.widget.LinearLayout;
+import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import java.util.Locale;
import java.util.Set;
-public class ConversationListItemInboxZero extends LinearLayout implements BindableConversationListItem{
+public class ConversationListItemInboxZero extends LinearLayout implements BindableConversationListItem {
public ConversationListItemInboxZero(Context context) {
super(context);
}
diff --git a/src/org/thoughtcrime/securesms/search/SearchListAdapter.java b/src/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.java
similarity index 91%
rename from src/org/thoughtcrime/securesms/search/SearchListAdapter.java
rename to src/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.java
index 475b52f7923..89a997c4403 100644
--- a/src/org/thoughtcrime/securesms/search/SearchListAdapter.java
+++ b/src/org/thoughtcrime/securesms/conversationlist/ConversationListSearchAdapter.java
@@ -1,4 +1,4 @@
-package org.thoughtcrime.securesms.search;
+package org.thoughtcrime.securesms.conversationlist;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -8,20 +8,19 @@
import android.view.ViewGroup;
import android.widget.TextView;
-import org.thoughtcrime.securesms.ConversationListItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.search.model.MessageResult;
-import org.thoughtcrime.securesms.search.model.SearchResult;
+import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
+import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import java.util.Collections;
import java.util.Locale;
-class SearchListAdapter extends RecyclerView.Adapter
- implements StickyHeaderDecoration.StickyHeaderAdapter
+class ConversationListSearchAdapter extends RecyclerView.Adapter
+ implements StickyHeaderDecoration.StickyHeaderAdapter
{
private static final int TYPE_CONVERSATIONS = 1;
private static final int TYPE_CONTACTS = 2;
@@ -34,9 +33,9 @@ class SearchListAdapter extends RecyclerView.Adapter searchResult;
+ private final SearchRepository searchRepository;
+ private final Debouncer debouncer;
+ private final ContentObserver observer;
+
+ private String lastQuery;
+
+ private ConversationListViewModel(@NonNull Application application, @NonNull SearchRepository searchRepository) {
+ this.application = application;
+ this.searchResult = new MutableLiveData<>();
+ this.searchRepository = searchRepository;
+ this.debouncer = new Debouncer(300);
+ this.observer = new ContentObserver(new Handler()) {
+ @Override
+ public void onChange(boolean selfChange) {
+ if (!TextUtils.isEmpty(getLastQuery())) {
+ searchRepository.query(getLastQuery(), searchResult::postValue);
+ }
+ }
+ };
+
+ application.getContentResolver().registerContentObserver(DatabaseContentProviders.ConversationList.CONTENT_URI, true, observer);
+ }
+
+ @NonNull LiveData getSearchResult() {
+ return searchResult;
+ }
+
+ void updateQuery(String query) {
+ lastQuery = query;
+ debouncer.publish(() -> searchRepository.query(query, result -> {
+ Util.runOnMain(() -> {
+ if (query.equals(lastQuery)) {
+ searchResult.setValue(result);
+ }
+ });
+ }));
+ }
+
+ private @NonNull String getLastQuery() {
+ return lastQuery == null ? "" : lastQuery;
+ }
+
+ @Override
+ protected void onCleared() {
+ debouncer.clear();
+ application.getContentResolver().unregisterContentObserver(observer);
+ }
+
+ public static class Factory extends ViewModelProvider.NewInstanceFactory {
+ @Override
+ public @NonNull T create(@NonNull Class modelClass) {
+ //noinspection ConstantConditions
+ return modelClass.cast(new ConversationListViewModel(ApplicationDependencies.getApplication(), new SearchRepository()));
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/search/model/MessageResult.java b/src/org/thoughtcrime/securesms/conversationlist/model/MessageResult.java
similarity index 93%
rename from src/org/thoughtcrime/securesms/search/model/MessageResult.java
rename to src/org/thoughtcrime/securesms/conversationlist/model/MessageResult.java
index b936109744c..903e23413e6 100644
--- a/src/org/thoughtcrime/securesms/search/model/MessageResult.java
+++ b/src/org/thoughtcrime/securesms/conversationlist/model/MessageResult.java
@@ -1,4 +1,4 @@
-package org.thoughtcrime.securesms.search.model;
+package org.thoughtcrime.securesms.conversationlist.model;
import androidx.annotation.NonNull;
diff --git a/src/org/thoughtcrime/securesms/conversationlist/model/SearchResult.java b/src/org/thoughtcrime/securesms/conversationlist/model/SearchResult.java
new file mode 100644
index 00000000000..fcc8c0f5642
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/conversationlist/model/SearchResult.java
@@ -0,0 +1,58 @@
+package org.thoughtcrime.securesms.conversationlist.model;
+
+import androidx.annotation.NonNull;
+
+import org.thoughtcrime.securesms.database.model.ThreadRecord;
+import org.thoughtcrime.securesms.recipients.Recipient;
+
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Represents an all-encompassing search result that can contain various result for different
+ * subcategories.
+ */
+public class SearchResult {
+
+ public static final SearchResult EMPTY = new SearchResult("", Collections.emptyList(), Collections.emptyList(), Collections.emptyList());
+
+ private final String query;
+ private final List contacts;
+ private final List conversations;
+ private final List messages;
+
+ public SearchResult(@NonNull String query,
+ @NonNull List contacts,
+ @NonNull List conversations,
+ @NonNull List messages)
+ {
+ this.query = query;
+ this.contacts = contacts;
+ this.conversations = conversations;
+ this.messages = messages;
+ }
+
+ public List getContacts() {
+ return contacts;
+ }
+
+ public List getConversations() {
+ return conversations;
+ }
+
+ public List getMessages() {
+ return messages;
+ }
+
+ public String getQuery() {
+ return query;
+ }
+
+ public int size() {
+ return contacts.size() + conversations.size() + messages.size();
+ }
+
+ public boolean isEmpty() {
+ return size() == 0;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/database/RecipientDatabase.java b/src/org/thoughtcrime/securesms/database/RecipientDatabase.java
index 06a12a023b3..4780c455e8f 100644
--- a/src/org/thoughtcrime/securesms/database/RecipientDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/RecipientDatabase.java
@@ -766,14 +766,31 @@ public void updateSystemContactColors(@NonNull ColorUpdater updater) {
"(" + PHONE + " NOT NULL OR " + EMAIL + " NOT NULL) AND " +
"(" +
PHONE + " LIKE ? OR " +
+ EMAIL + " LIKE ? OR " +
SYSTEM_DISPLAY_NAME + " LIKE ?" +
")";
- String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query };
+ String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query, query };
String orderBy = SYSTEM_DISPLAY_NAME + ", " + PHONE;
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
}
+ public @Nullable Cursor queryAllContacts(@NonNull String query) {
+ query = TextUtils.isEmpty(query) ? "*" : query;
+ query = "%" + query + "%";
+
+ String selection = BLOCKED + " = ? AND " +
+ "(" +
+ SYSTEM_DISPLAY_NAME + " LIKE ? OR " +
+ SIGNAL_PROFILE_NAME + " LIKE ? OR " +
+ PHONE + " LIKE ? OR " +
+ EMAIL + " LIKE ?" +
+ ")";
+ String[] args = new String[] { "0", query, query, query, query };
+
+ return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null);
+ }
+
public void applyBlockedUpdate(@NonNull List blocked, List groupIds) {
List blockedE164 = Stream.of(blocked)
.filter(b -> b.getNumber().isPresent())
diff --git a/src/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java b/src/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java
index 483bf60703e..02070fa460f 100644
--- a/src/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java
+++ b/src/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java
@@ -46,6 +46,7 @@ public static synchronized void init(@NonNull Application application, @NonNull
}
public static @NonNull Application getApplication() {
+ assertInitialization();
return application;
}
diff --git a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
index 54cd4b77530..eb364e7861e 100644
--- a/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
+++ b/src/org/thoughtcrime/securesms/jobs/PushDecryptJob.java
@@ -27,7 +27,7 @@
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SelfSendException;
import org.thoughtcrime.securesms.ApplicationContext;
-import org.thoughtcrime.securesms.ConversationListActivity;
+import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
@@ -95,7 +95,6 @@
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
-import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.util.guava.Optional;
@@ -210,6 +209,7 @@ private boolean needsMigration() {
}
private void postMigrationNotification() {
+ // TODO [greyson] Navigation
NotificationManagerCompat.from(context).notify(494949,
new NotificationCompat.Builder(context, NotificationChannels.getMessagesChannel(context))
.setSmallIcon(R.drawable.icon_notification)
@@ -217,7 +217,7 @@ private void postMigrationNotification() {
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setContentTitle(context.getString(R.string.PushDecryptJob_new_locked_message))
.setContentText(context.getString(R.string.PushDecryptJob_unlock_to_view_pending_messages))
- .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, ConversationListActivity.class), 0))
+ .setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0))
.setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE)
.build());
diff --git a/src/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java b/src/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java
index b2894b804a5..6d6199a2b3a 100644
--- a/src/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java
+++ b/src/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java
@@ -8,7 +8,7 @@
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
-import org.thoughtcrime.securesms.ConversationListActivity;
+import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.recipients.Recipient;
@@ -28,7 +28,8 @@ public MultipleRecipientNotificationBuilder(Context context, NotificationPrivacy
setColor(context.getResources().getColor(R.color.textsecure_primary));
setSmallIcon(R.drawable.icon_notification);
setContentTitle(context.getString(R.string.app_name));
- setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, ConversationListActivity.class), 0));
+ // TODO [greyson] Navigation
+ setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0));
setCategory(NotificationCompat.CATEGORY_MESSAGE);
setGroupSummary(true);
diff --git a/src/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java b/src/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java
index fb07b7e99cc..dcdc9a2b620 100644
--- a/src/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java
+++ b/src/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java
@@ -6,7 +6,7 @@
import android.content.Intent;
import androidx.core.app.NotificationCompat;
-import org.thoughtcrime.securesms.ConversationListActivity;
+import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
@@ -17,7 +17,8 @@ public class PendingMessageNotificationBuilder extends AbstractNotificationBuild
public PendingMessageNotificationBuilder(Context context, NotificationPrivacyPreference privacy) {
super(context, privacy);
- Intent intent = new Intent(context, ConversationListActivity.class);
+ // TODO [greyson] Navigation
+ Intent intent = new Intent(context, MainActivity.class);
setSmallIcon(R.drawable.icon_notification);
setColor(context.getResources().getColor(R.color.textsecure_primary));
diff --git a/src/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java b/src/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java
index 7bf64dcb772..634b1a00eac 100644
--- a/src/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java
+++ b/src/org/thoughtcrime/securesms/registration/fragments/RegistrationCompleteFragment.java
@@ -12,8 +12,8 @@
import androidx.fragment.app.FragmentActivity;
import androidx.navigation.ActivityNavigator;
-import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.CreateProfileActivity;
+import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
public final class RegistrationCompleteFragment extends BaseRegistrationFragment {
@@ -31,7 +31,8 @@ public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceStat
FragmentActivity activity = requireActivity();
if (!isReregister()) {
- activity.startActivity(getRoutedIntent(activity, CreateProfileActivity.class, new Intent(activity, ConversationListActivity.class)));
+ // TODO [greyson] Navigation
+ activity.startActivity(getRoutedIntent(activity, CreateProfileActivity.class, new Intent(activity, MainActivity.class)));
}
activity.finish();
diff --git a/src/org/thoughtcrime/securesms/search/SearchFragment.java b/src/org/thoughtcrime/securesms/search/SearchFragment.java
deleted file mode 100644
index 5fd57053c52..00000000000
--- a/src/org/thoughtcrime/securesms/search/SearchFragment.java
+++ /dev/null
@@ -1,181 +0,0 @@
-package org.thoughtcrime.securesms.search;
-
-import android.annotation.SuppressLint;
-import androidx.lifecycle.ViewModelProviders;
-import android.content.Intent;
-import android.os.AsyncTask;
-import android.os.Bundle;
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-import androidx.fragment.app.Fragment;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
-import android.text.TextUtils;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.TextView;
-
-import org.thoughtcrime.securesms.contacts.ContactRepository;
-import org.thoughtcrime.securesms.conversation.ConversationActivity;
-import org.thoughtcrime.securesms.ConversationListActivity;
-import org.thoughtcrime.securesms.R;
-import org.thoughtcrime.securesms.contacts.ContactAccessor;
-import org.thoughtcrime.securesms.database.DatabaseFactory;
-import org.thoughtcrime.securesms.database.ThreadDatabase;
-import org.thoughtcrime.securesms.database.model.ThreadRecord;
-import org.thoughtcrime.securesms.mms.GlideApp;
-import org.thoughtcrime.securesms.recipients.Recipient;
-import org.thoughtcrime.securesms.search.model.MessageResult;
-import org.thoughtcrime.securesms.search.model.SearchResult;
-import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
-import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
-
-import java.util.Locale;
-
-/**
- * A fragment that is displayed to do full-text search of messages, groups, and contacts.
- */
-public class SearchFragment extends Fragment implements SearchListAdapter.EventListener {
-
- public static final String TAG = "SearchFragment";
- public static final String EXTRA_LOCALE = "locale";
-
- private TextView noResultsView;
- private RecyclerView listView;
- private StickyHeaderDecoration listDecoration;
-
- private SearchViewModel viewModel;
- private SearchListAdapter listAdapter;
- private String pendingQuery;
- private Locale locale;
-
- public static SearchFragment newInstance(@NonNull Locale locale) {
- Bundle args = new Bundle();
- args.putSerializable(EXTRA_LOCALE, locale);
-
- SearchFragment fragment = new SearchFragment();
- fragment.setArguments(args);
-
- return fragment;
- }
-
- @Override
- public void onCreate(@Nullable Bundle savedInstanceState) {
- super.onCreate(savedInstanceState);
-
- this.locale = (Locale) getArguments().getSerializable(EXTRA_LOCALE);
-
- SearchRepository searchRepository = new SearchRepository(getContext(),
- DatabaseFactory.getSearchDatabase(getContext()),
- DatabaseFactory.getThreadDatabase(getContext()),
- new ContactRepository(requireContext()),
- ContactAccessor.getInstance(),
- SignalExecutors.SERIAL);
- viewModel = ViewModelProviders.of(this, new SearchViewModel.Factory(searchRepository)).get(SearchViewModel.class);
-
- if (pendingQuery != null) {
- viewModel.updateQuery(pendingQuery);
- pendingQuery = null;
- }
- }
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
- return inflater.inflate(R.layout.fragment_search, container, false);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- noResultsView = view.findViewById(R.id.search_no_results);
- listView = view.findViewById(R.id.search_list);
-
- listAdapter = new SearchListAdapter(GlideApp.with(this), this, locale);
- listDecoration = new StickyHeaderDecoration(listAdapter, false, false);
-
- listView.setAdapter(listAdapter);
- listView.addItemDecoration(listDecoration);
- listView.setLayoutManager(new LinearLayoutManager(getContext()));
- }
-
- @Override
- public void onStart() {
- super.onStart();
- viewModel.getSearchResult().observe(this, result -> {
- result = result != null ? result : SearchResult.EMPTY;
-
- listAdapter.updateResults(result);
-
- if (result.isEmpty()) {
- if (TextUtils.isEmpty(viewModel.getLastQuery().trim())) {
- noResultsView.setVisibility(View.GONE);
- } else {
- noResultsView.setVisibility(View.VISIBLE);
- noResultsView.setText(getString(R.string.SearchFragment_no_results, viewModel.getLastQuery()));
- }
- } else {
- noResultsView.setVisibility(View.VISIBLE);
- noResultsView.setText("");
- }
- });
- }
-
- @Override
- public void onConversationClicked(@NonNull ThreadRecord threadRecord) {
- ConversationListActivity conversationList = (ConversationListActivity) getActivity();
-
- if (conversationList != null) {
- conversationList.onCreateConversation(threadRecord.getThreadId(),
- threadRecord.getRecipient(),
- threadRecord.getDistributionType(),
- threadRecord.getLastSeen());
- }
- }
-
- @Override
- public void onContactClicked(@NonNull Recipient contact) {
- Intent intent = new Intent(getContext(), ConversationActivity.class);
- intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, contact.getId());
-
- long existingThread = DatabaseFactory.getThreadDatabase(getContext()).getThreadIdIfExistsFor(contact);
-
- intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, existingThread);
- intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, ThreadDatabase.DistributionTypes.DEFAULT);
- startActivity(intent);
- }
-
- @SuppressLint("StaticFieldLeak")
- @Override
- public void onMessageClicked(@NonNull MessageResult message) {
- new AsyncTask() {
- @Override
- protected Integer doInBackground(Void... voids) {
- int startingPosition = DatabaseFactory.getMmsSmsDatabase(getContext()).getMessagePositionInConversation(message.threadId, message.receivedTimestampMs);
- startingPosition = Math.max(0, startingPosition);
-
- return startingPosition;
- }
-
- @Override
- protected void onPostExecute(Integer startingPosition) {
- ConversationListActivity conversationList = (ConversationListActivity) getActivity();
- if (conversationList != null) {
- conversationList.openConversation(message.threadId,
- message.conversationRecipient,
- ThreadDatabase.DistributionTypes.DEFAULT,
- -1,
- startingPosition);
- }
- }
- }.execute();
- }
-
- public void updateSearchQuery(@NonNull String query) {
- if (viewModel != null) {
- viewModel.updateQuery(query);
- } else {
- pendingQuery = query;
- }
- }
-}
diff --git a/src/org/thoughtcrime/securesms/search/SearchRepository.java b/src/org/thoughtcrime/securesms/search/SearchRepository.java
index ee578a4e76b..8354f606599 100644
--- a/src/org/thoughtcrime/securesms/search/SearchRepository.java
+++ b/src/org/thoughtcrime/securesms/search/SearchRepository.java
@@ -1,11 +1,12 @@
package org.thoughtcrime.securesms.search;
-import android.Manifest;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.MergeCursor;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
import android.text.TextUtils;
import com.annimon.stream.Stream;
@@ -14,22 +15,29 @@
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactRepository;
import org.thoughtcrime.securesms.database.CursorList;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
-import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
-import org.thoughtcrime.securesms.search.model.MessageResult;
-import org.thoughtcrime.securesms.search.model.SearchResult;
+import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
+import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.util.Stopwatch;
+import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
+import java.util.ArrayList;
+import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
+import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Future;
/**
* Manages data retrieval for search.
@@ -60,21 +68,17 @@ public class SearchRepository {
private final ContactRepository contactRepository;
private final ThreadDatabase threadDatabase;
private final ContactAccessor contactAccessor;
- private final Executor executor;
-
- public SearchRepository(@NonNull Context context,
- @NonNull SearchDatabase searchDatabase,
- @NonNull ThreadDatabase threadDatabase,
- @NonNull ContactRepository contactRepository,
- @NonNull ContactAccessor contactAccessor,
- @NonNull Executor executor)
- {
- this.context = context.getApplicationContext();
- this.searchDatabase = searchDatabase;
- this.threadDatabase = threadDatabase;
- this.contactRepository = contactRepository;
- this.contactAccessor = contactAccessor;
- this.executor = executor;
+ private final Executor serialExecutor;
+ private final ExecutorService parallelExecutor;
+
+ public SearchRepository() {
+ this.context = ApplicationDependencies.getApplication().getApplicationContext();
+ this.searchDatabase = DatabaseFactory.getSearchDatabase(context);
+ this.threadDatabase = DatabaseFactory.getThreadDatabase(context);
+ this.contactRepository = new ContactRepository(context);
+ this.contactAccessor = ContactAccessor.getInstance();
+ this.serialExecutor = SignalExecutors.SERIAL;
+ this.parallelExecutor = SignalExecutors.BOUNDED;
}
public void query(@NonNull String query, @NonNull Callback callback) {
@@ -83,73 +87,99 @@ public void query(@NonNull String query, @NonNull Callback callbac
return;
}
- executor.execute(() -> {
- Stopwatch timer = new Stopwatch("FtsQuery");
+ serialExecutor.execute(() -> {
String cleanQuery = sanitizeQuery(query);
- timer.split("clean");
-
- CursorList contacts = queryContacts(cleanQuery);
- timer.split("contacts");
- CursorList conversations = queryConversations(cleanQuery);
- timer.split("conversations");
+ Future> contacts = parallelExecutor.submit(() -> queryContacts(cleanQuery));
+ Future> conversations = parallelExecutor.submit(() -> queryConversations(cleanQuery));
+ Future> messages = parallelExecutor.submit(() -> queryMessages(cleanQuery));
- CursorList messages = queryMessages(cleanQuery);
- timer.split("messages");
+ try {
+ long startTime = System.currentTimeMillis();
+ SearchResult result = new SearchResult(cleanQuery, contacts.get(), conversations.get(), messages.get());
- timer.stop(TAG);
+ Log.d(TAG, "Total time: " + (System.currentTimeMillis() - startTime) + " ms");
- callback.onResult(new SearchResult(cleanQuery, contacts, conversations, messages));
+ callback.onResult(result);
+ } catch (ExecutionException | InterruptedException e) {
+ Log.w(TAG, e);
+ callback.onResult(SearchResult.EMPTY);
+ }
});
}
- public void query(@NonNull String query, long threadId, @NonNull Callback> callback) {
+ public void query(@NonNull String query, long threadId, @NonNull Callback> callback) {
if (TextUtils.isEmpty(query)) {
callback.onResult(CursorList.emptyList());
return;
}
- executor.execute(() -> {
+ serialExecutor.execute(() -> {
long startTime = System.currentTimeMillis();
- CursorList messages = queryMessages(sanitizeQuery(query), threadId);
+ List messages = queryMessages(sanitizeQuery(query), threadId);
Log.d(TAG, "[ConversationQuery] " + (System.currentTimeMillis() - startTime) + " ms");
callback.onResult(messages);
});
}
- private CursorList queryContacts(String query) {
- if (!Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
- return CursorList.emptyList();
- }
+ private List queryContacts(String query) {
+ Cursor contacts = null;
+
+ try {
+ Cursor textSecureContacts = contactRepository.querySignalContacts(query);
+ Cursor systemContacts = contactRepository.queryNonSignalContacts(query);
- Cursor textSecureContacts = contactRepository.querySignalContacts(query);
- Cursor systemContacts = contactRepository.queryNonSignalContacts(query);
- MergeCursor contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts });
+ contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts });
- return new CursorList<>(contacts, new RecipientModelBuilder());
+ return readToList(contacts, new RecipientModelBuilder(), 250);
+ } finally {
+ if (contacts != null) {
+ contacts.close();
+ }
+ }
}
- private CursorList queryConversations(@NonNull String query) {
+ private @NonNull List queryConversations(@NonNull String query) {
List numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query);
List recipientIds = Stream.of(numbers).map(number -> Recipient.external(context, number)).map(Recipient::getId).toList();
- Cursor conversations = threadDatabase.getFilteredConversationList(recipientIds);
- return conversations != null ? new CursorList<>(conversations, new ThreadModelBuilder(threadDatabase))
- : CursorList.emptyList();
+ try (Cursor cursor = threadDatabase.getFilteredConversationList(recipientIds)) {
+ return readToList(cursor, new ThreadModelBuilder(threadDatabase));
+ }
}
- private CursorList queryMessages(@NonNull String query) {
- Cursor messages = searchDatabase.queryMessages(query);
- return messages != null ? new CursorList<>(messages, new MessageModelBuilder(context))
- : CursorList.emptyList();
+ private @NonNull List queryMessages(@NonNull String query) {
+ try (Cursor cursor = searchDatabase.queryMessages(query)) {
+ return readToList(cursor, new MessageModelBuilder(context));
+ }
}
- private CursorList queryMessages(@NonNull String query, long threadId) {
- Cursor messages = searchDatabase.queryMessages(query, threadId);
- return messages != null ? new CursorList<>(messages, new MessageModelBuilder(context))
- : CursorList.emptyList();
+ private @NonNull List queryMessages(@NonNull String query, long threadId) {
+ try (Cursor cursor = searchDatabase.queryMessages(query, threadId)) {
+ return readToList(cursor, new MessageModelBuilder(context));
+ }
+ }
+
+ private @NonNull List readToList(@Nullable Cursor cursor, @NonNull CursorList.ModelBuilder builder) {
+ return readToList(cursor, builder, -1);
+ }
+
+ private @NonNull List readToList(@Nullable Cursor cursor, @NonNull CursorList.ModelBuilder builder, int limit) {
+ if (cursor == null) {
+ return Collections.emptyList();
+ }
+
+ int i = 0;
+ List list = new ArrayList<>(cursor.getCount());
+
+ while (cursor.moveToNext() && (limit < 0 || i < limit)) {
+ list.add(builder.build(cursor));
+ i++;
+ }
+
+ return list;
}
/**
diff --git a/src/org/thoughtcrime/securesms/search/SearchViewModel.java b/src/org/thoughtcrime/securesms/search/SearchViewModel.java
deleted file mode 100644
index 7c307717f23..00000000000
--- a/src/org/thoughtcrime/securesms/search/SearchViewModel.java
+++ /dev/null
@@ -1,126 +0,0 @@
-package org.thoughtcrime.securesms.search;
-
-import androidx.lifecycle.LiveData;
-import androidx.lifecycle.MutableLiveData;
-import androidx.lifecycle.ViewModel;
-import androidx.lifecycle.ViewModelProvider;
-import android.database.ContentObserver;
-import android.os.Handler;
-import androidx.annotation.NonNull;
-import android.text.TextUtils;
-
-import androidx.fragment.app.Fragment;
-
-import org.thoughtcrime.securesms.search.model.SearchResult;
-import org.thoughtcrime.securesms.util.Debouncer;
-import org.thoughtcrime.securesms.util.Util;
-
-/**
- * A {@link ViewModel} for handling all the business logic and interactions that take place inside
- * of the {@link SearchFragment}.
- *
- * This class should be view- and Android-agnostic, and therefore should contain no references to
- * things like {@link android.content.Context}, {@link android.view.View},
- * {@link Fragment}, etc.
- */
-class SearchViewModel extends ViewModel {
-
- private final ObservingLiveData searchResult;
- private final SearchRepository searchRepository;
- private final Debouncer debouncer;
-
- private String lastQuery;
-
- private SearchViewModel(@NonNull SearchRepository searchRepository) {
- this.searchResult = new ObservingLiveData();
- this.searchRepository = searchRepository;
- this.debouncer = new Debouncer(500);
-
- searchResult.registerContentObserver(new ContentObserver(new Handler()) {
- @Override
- public void onChange(boolean selfChange) {
- if (!TextUtils.isEmpty(getLastQuery())) {
- searchRepository.query(getLastQuery(), searchResult::postValue);
- }
- }
- });
- }
-
- LiveData getSearchResult() {
- return searchResult;
- }
-
- void updateQuery(String query) {
- lastQuery = query;
- debouncer.publish(() -> searchRepository.query(query, result -> {
- Util.runOnMain(() -> {
- if (query.equals(lastQuery)) {
- searchResult.setValue(result);
- } else {
- result.close();
- }
- });
- }));
- }
-
- @NonNull
- String getLastQuery() {
- return lastQuery == null ? "" : lastQuery;
- }
-
- @Override
- protected void onCleared() {
- debouncer.clear();
- searchResult.close();
- }
-
- /**
- * Ensures that the previous {@link SearchResult} is always closed whenever we set a new one.
- */
- private static class ObservingLiveData extends MutableLiveData {
-
- private ContentObserver observer;
-
- @Override
- public void setValue(SearchResult value) {
- SearchResult previous = getValue();
-
- if (previous != null) {
- previous.unregisterContentObserver(observer);
- previous.close();
- }
-
- value.registerContentObserver(observer);
-
- super.setValue(value);
- }
-
- void close() {
- SearchResult value = getValue();
-
- if (value != null) {
- value.unregisterContentObserver(observer);
- value.close();
- }
- }
-
- void registerContentObserver(@NonNull ContentObserver observer) {
- this.observer = observer;
- }
- }
-
- public static class Factory extends ViewModelProvider.NewInstanceFactory {
-
- private final SearchRepository searchRepository;
-
- public Factory(@NonNull SearchRepository searchRepository) {
- this.searchRepository = searchRepository;
- }
-
- @NonNull
- @Override
- public T create(@NonNull Class modelClass) {
- return modelClass.cast(new SearchViewModel(searchRepository));
- }
- }
-}
diff --git a/src/org/thoughtcrime/securesms/search/model/SearchResult.java b/src/org/thoughtcrime/securesms/search/model/SearchResult.java
deleted file mode 100644
index 66d00a84296..00000000000
--- a/src/org/thoughtcrime/securesms/search/model/SearchResult.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package org.thoughtcrime.securesms.search.model;
-
-import android.database.ContentObserver;
-
-import androidx.annotation.NonNull;
-
-import org.thoughtcrime.securesms.database.CursorList;
-import org.thoughtcrime.securesms.database.model.ThreadRecord;
-import org.thoughtcrime.securesms.recipients.Recipient;
-
-import java.util.List;
-
-/**
- * Represents an all-encompassing search result that can contain various result for different
- * subcategories.
- */
-public class SearchResult {
-
- public static final SearchResult EMPTY = new SearchResult("", CursorList.emptyList(), CursorList.emptyList(), CursorList.emptyList());
-
- private final String query;
- private final CursorList contacts;
- private final CursorList conversations;
- private final CursorList messages;
-
- public SearchResult(@NonNull String query,
- @NonNull CursorList contacts,
- @NonNull CursorList conversations,
- @NonNull CursorList messages)
- {
- this.query = query;
- this.contacts = contacts;
- this.conversations = conversations;
- this.messages = messages;
- }
-
- public List getContacts() {
- return contacts;
- }
-
- public List getConversations() {
- return conversations;
- }
-
- public List getMessages() {
- return messages;
- }
-
- public String getQuery() {
- return query;
- }
-
- public int size() {
- return contacts.size() + conversations.size() + messages.size();
- }
-
- public boolean isEmpty() {
- return size() == 0;
- }
-
- public void registerContentObserver(@NonNull ContentObserver observer) {
- contacts.registerContentObserver(observer);
- conversations.registerContentObserver(observer);
- messages.registerContentObserver(observer);
- }
-
- public void unregisterContentObserver(@NonNull ContentObserver observer) {
- contacts.unregisterContentObserver(observer);
- conversations.unregisterContentObserver(observer);
- messages.unregisterContentObserver(observer);
- }
-
- public void close() {
- contacts.close();
- conversations.close();
- messages.close();
- }
-}
diff --git a/src/org/thoughtcrime/securesms/service/ApplicationMigrationService.java b/src/org/thoughtcrime/securesms/service/ApplicationMigrationService.java
index 70c27091ad5..710a1d2b803 100644
--- a/src/org/thoughtcrime/securesms/service/ApplicationMigrationService.java
+++ b/src/org/thoughtcrime/securesms/service/ApplicationMigrationService.java
@@ -16,7 +16,7 @@
import android.os.PowerManager.WakeLock;
import androidx.core.app.NotificationCompat;
-import org.thoughtcrime.securesms.ConversationListActivity;
+import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.SmsMigrator;
import org.thoughtcrime.securesms.database.SmsMigrator.ProgressDescription;
@@ -133,7 +133,8 @@ private NotificationCompat.Builder initializeBackgroundNotification() {
builder.setContentText(getString(R.string.ApplicationMigrationService_import_in_progress));
builder.setOngoing(true);
builder.setProgress(100, 0, false);
- builder.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ConversationListActivity.class), 0));
+ // TODO [greyson] Navigation
+ builder.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0));
stopForeground(true);
startForeground(4242, builder.build());
@@ -184,7 +185,8 @@ public void onReceive(Context context, Intent intent) {
builder.setSmallIcon(R.drawable.icon_notification);
builder.setContentTitle(context.getString(R.string.ApplicationMigrationService_import_complete));
builder.setContentText(context.getString(R.string.ApplicationMigrationService_system_database_import_is_complete));
- builder.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, ConversationListActivity.class), 0));
+ // TODO [greyson] Navigation
+ builder.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0));
builder.setWhen(System.currentTimeMillis());
builder.setDefaults(Notification.DEFAULT_VIBRATE);
builder.setAutoCancel(true);
diff --git a/src/org/thoughtcrime/securesms/service/GenericForegroundService.java b/src/org/thoughtcrime/securesms/service/GenericForegroundService.java
index bad0fe5dd9f..d98ee43d1c1 100644
--- a/src/org/thoughtcrime/securesms/service/GenericForegroundService.java
+++ b/src/org/thoughtcrime/securesms/service/GenericForegroundService.java
@@ -13,7 +13,7 @@
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
-import org.thoughtcrime.securesms.ConversationListActivity;
+import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
@@ -103,11 +103,12 @@ private synchronized void handleStop(@NonNull Intent intent) {
private void postObligatoryForegroundNotification(@NonNull Entry active) {
lastPosted = active;
+ // TODO [greyson] Navigation
startForeground(NOTIFICATION_ID, new NotificationCompat.Builder(this, active.channelId)
.setSmallIcon(active.iconRes)
.setContentTitle(active.title)
.setProgress(active.progressMax, active.progress, active.indeterminate)
- .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ConversationListActivity.class), 0))
+ .setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0))
.build());
}
diff --git a/src/org/thoughtcrime/securesms/service/KeyCachingService.java b/src/org/thoughtcrime/securesms/service/KeyCachingService.java
index 4bfa2f45a76..7bade8b3de5 100644
--- a/src/org/thoughtcrime/securesms/service/KeyCachingService.java
+++ b/src/org/thoughtcrime/securesms/service/KeyCachingService.java
@@ -32,9 +32,9 @@
import androidx.core.app.NotificationCompat;
import org.thoughtcrime.securesms.ApplicationContext;
+import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.logging.Log;
-import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.DummyActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
@@ -285,7 +285,8 @@ private PendingIntent buildLockIntent() {
}
private PendingIntent buildLaunchIntent() {
- Intent intent = new Intent(this, ConversationListActivity.class);
+ // TODO [greyson] Navigation
+ Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
return PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
}
diff --git a/src/org/thoughtcrime/securesms/util/CommunicationActions.java b/src/org/thoughtcrime/securesms/util/CommunicationActions.java
index 007f4735fd9..cd8237c3860 100644
--- a/src/org/thoughtcrime/securesms/util/CommunicationActions.java
+++ b/src/org/thoughtcrime/securesms/util/CommunicationActions.java
@@ -72,7 +72,6 @@ protected void onPostExecute(Long threadId) {
Intent intent = new Intent(context, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
- intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis());
if (!TextUtils.isEmpty(text)) {
intent.putExtra(ConversationActivity.TEXT_EXTRA, text);