From 1e7c93007d6434490f2d755eccad0952b1b28072 Mon Sep 17 00:00:00 2001 From: Greyson Parrelli Date: Thu, 14 Nov 2019 14:35:08 -0500 Subject: [PATCH] Convert the conversation list into a real fragment. It was a fragment before, but it's functionality was inappropriately split between the various layers. This also sets us up better to do tablet stuff in the future, if we choose to do that. --- AndroidManifest.xml | 32 +- res/layout/conversation_list_activity.xml | 90 -- res/layout/conversation_list_fragment.xml | 260 ++++-- res/layout/conversation_list_item_action.xml | 4 +- .../conversation_list_item_inbox_zero.xml | 4 +- res/layout/conversation_list_item_view.xml | 4 +- res/layout/fragment_search.xml | 20 - res/layout/main_activity.xml | 10 + .../ApplicationPreferencesActivity.java | 4 +- .../securesms/ConversationListActivity.java | 318 ------- .../ConversationListArchiveActivity.java | 81 -- .../securesms/ConversationListFragment.java | 660 ------------- .../securesms/DatabaseMigrationActivity.java | 3 +- .../securesms/ExperienceUpgradeActivity.java | 3 +- .../thoughtcrime/securesms/MainActivity.java | 45 + .../thoughtcrime/securesms/MainFragment.java | 22 + .../thoughtcrime/securesms/MainNavigator.java | 104 +++ .../PassphraseRequiredActionBarActivity.java | 3 +- .../securesms/ShortcutLauncherActivity.java | 6 +- .../reminder/SystemSmsImportReminder.java | 5 +- .../securesms/contacts/ContactAccessor.java | 18 +- .../contacts/ContactSelectionListItem.java | 1 - .../conversation/ConversationActivity.java | 40 +- .../ConversationSearchViewModel.java | 46 +- .../ConversationListAdapter.java | 4 +- .../ConversationListArchiveFragment.java | 153 +++ .../ConversationListFragment.java | 880 ++++++++++++++++++ .../ConversationListItem.java | 9 +- .../ConversationListItemAction.java | 4 +- .../ConversationListItemInboxZero.java | 5 +- .../ConversationListSearchAdapter.java} | 17 +- .../ConversationListViewModel.java | 80 ++ .../model/MessageResult.java | 2 +- .../conversationlist/model/SearchResult.java | 58 ++ .../securesms/database/RecipientDatabase.java | 19 +- .../dependencies/ApplicationDependencies.java | 1 + .../securesms/jobs/PushDecryptJob.java | 6 +- .../MultipleRecipientNotificationBuilder.java | 5 +- .../PendingMessageNotificationBuilder.java | 5 +- .../RegistrationCompleteFragment.java | 5 +- .../securesms/search/SearchFragment.java | 181 ---- .../securesms/search/SearchRepository.java | 138 +-- .../securesms/search/SearchViewModel.java | 126 --- .../securesms/search/model/SearchResult.java | 78 -- .../service/ApplicationMigrationService.java | 8 +- .../service/GenericForegroundService.java | 5 +- .../securesms/service/KeyCachingService.java | 5 +- .../securesms/util/CommunicationActions.java | 1 - 48 files changed, 1759 insertions(+), 1819 deletions(-) delete mode 100644 res/layout/conversation_list_activity.xml delete mode 100644 res/layout/fragment_search.xml create mode 100644 res/layout/main_activity.xml delete mode 100644 src/org/thoughtcrime/securesms/ConversationListActivity.java delete mode 100644 src/org/thoughtcrime/securesms/ConversationListArchiveActivity.java delete mode 100644 src/org/thoughtcrime/securesms/ConversationListFragment.java create mode 100644 src/org/thoughtcrime/securesms/MainActivity.java create mode 100644 src/org/thoughtcrime/securesms/MainFragment.java create mode 100644 src/org/thoughtcrime/securesms/MainNavigator.java rename src/org/thoughtcrime/securesms/{ => conversationlist}/ConversationListAdapter.java (97%) create mode 100644 src/org/thoughtcrime/securesms/conversationlist/ConversationListArchiveFragment.java create mode 100644 src/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java rename src/org/thoughtcrime/securesms/{ => conversationlist}/ConversationListItem.java (97%) rename src/org/thoughtcrime/securesms/{ => conversationlist}/ConversationListItemAction.java (91%) rename src/org/thoughtcrime/securesms/{ => conversationlist}/ConversationListItemInboxZero.java (89%) rename src/org/thoughtcrime/securesms/{search/SearchListAdapter.java => conversationlist/ConversationListSearchAdapter.java} (91%) create mode 100644 src/org/thoughtcrime/securesms/conversationlist/ConversationListViewModel.java rename src/org/thoughtcrime/securesms/{search => conversationlist}/model/MessageResult.java (93%) create mode 100644 src/org/thoughtcrime/securesms/conversationlist/model/SearchResult.java delete mode 100644 src/org/thoughtcrime/securesms/search/SearchFragment.java delete mode 100644 src/org/thoughtcrime/securesms/search/SearchViewModel.java delete mode 100644 src/org/thoughtcrime/securesms/search/model/SearchResult.java diff --git a/AndroidManifest.xml b/AndroidManifest.xml index ba3ff74d31..e61597dbd7 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 564696fa09..0000000000 --- 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 59251232fe..2a50709f90 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 77d2bc8375..e0338db939 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 149e45ad98..a9ff8b02d7 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 a1591d8a93..fdc983227a 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 ae8401c6e8..0000000000 --- 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 0000000000..43d7aa44d1 --- /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 483c6078e9..c67cdda4ce 100644 --- a/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java +++ b/src/org/thoughtcrime/securesms/ApplicationPreferencesActivity.java @@ -24,7 +24,6 @@ import android.os.Build; 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 class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA 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 db89d3a429..0000000000 --- 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 3aa7da6444..0000000000 --- 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 aea856c138..0000000000 --- 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 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 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 b9aef9aaa4..bf1657b2eb 100644 --- a/src/org/thoughtcrime/securesms/DatabaseMigrationActivity.java +++ b/src/org/thoughtcrime/securesms/DatabaseMigrationActivity.java @@ -149,7 +149,8 @@ public class DatabaseMigrationActivity extends PassphraseRequiredActionBarActivi 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 4ebeed2bf7..d0511f17f1 100644 --- a/src/org/thoughtcrime/securesms/ExperienceUpgradeActivity.java +++ b/src/org/thoughtcrime/securesms/ExperienceUpgradeActivity.java @@ -189,7 +189,8 @@ public class ExperienceUpgradeActivity extends BaseActionBarActivity implements 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 0000000000..e156f6133b --- /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 0000000000..deb6f5b460 --- /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 0000000000..38552b5592 --- /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 cd1895ffcd..98cdd5c977 100644 --- a/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java +++ b/src/org/thoughtcrime/securesms/PassphraseRequiredActionBarActivity.java @@ -187,7 +187,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA } 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 19b5f10c41..b798d17799 100644 --- a/src/org/thoughtcrime/securesms/ShortcutLauncherActivity.java +++ b/src/org/thoughtcrime/securesms/ShortcutLauncherActivity.java @@ -33,14 +33,16 @@ public class ShortcutLauncherActivity extends AppCompatActivity { 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 d4a7824aad..996be314ad 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.content.Intent; 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 class SystemSmsImportReminder extends Reminder { 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 afd3a16b50..b9450e635e 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactAccessor.java @@ -34,9 +34,11 @@ import com.annimon.stream.Stream; 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 @@ public class ContactAccessor { 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 3f3b7e19e5..dbd2da39af 100644 --- a/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java +++ b/src/org/thoughtcrime/securesms/contacts/ContactSelectionListItem.java @@ -11,7 +11,6 @@ import android.widget.CheckBox; 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 456442b96b..4b3c878e5a 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.EventBus; 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.RecipientFormattingException; 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 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity 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 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity 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 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity MessageNotifier.setVisibleThread(threadId); markThreadAsRead(); - - Log.i(TAG, "onResume() Finished: " + (System.currentTimeMillis() - getIntent().getLongExtra(TIMING_EXTRA, 0))); } @Override @@ -802,7 +817,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity 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 class ConversationActivity extends PassphraseRequiredActionBarActivity //////// 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 56e8e45132..7f3dda3126 100644 --- a/src/org/thoughtcrime/securesms/conversation/ConversationSearchViewModel.java +++ b/src/org/thoughtcrime/securesms/conversation/ConversationSearchViewModel.java @@ -5,13 +5,15 @@ import androidx.lifecycle.AndroidViewModel; 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 @@ import java.util.List; 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 @@ public class ConversationSearchViewModel extends AndroidViewModel { 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 @@ public class ConversationSearchViewModel extends AndroidViewModel { 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 @@ public class ConversationSearchViewModel extends AndroidViewModel { 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 @@ public class ConversationSearchViewModel extends AndroidViewModel { 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 class ConversationSearchViewModel extends AndroidViewModel { 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 63746d69b4..53c0491f7d 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.LayoutInflater; 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 0000000000..b6673fe02e --- /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 0000000000..b80e030f30 --- /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 a7b4bf806e..87ccb53d5f 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 android.widget.TextView; 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.mms.GlideRequests; 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 @@ import java.util.Set; 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 c30e429bec..0bf81c0052 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.util.AttributeSet; 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 c24063b0e5..638b6905f4 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 androidx.annotation.RequiresApi; 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 475b52f792..89a997c440 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.View; 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 b936109744..903e23413e 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 0000000000..fcc8c0f564 --- /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 06a12a023b..4780c455e8 100644 --- a/src/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/src/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -766,14 +766,31 @@ public class RecipientDatabase extends Database { "(" + 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 483bf60703..02070fa460 100644 --- a/src/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java +++ b/src/org/thoughtcrime/securesms/dependencies/ApplicationDependencies.java @@ -46,6 +46,7 @@ public class ApplicationDependencies { } 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 54cd4b7753..eb364e7861 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.ProtocolNoSessionException; 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.Hex; 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 @@ public class PushDecryptJob extends BaseJob { } 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 @@ public class PushDecryptJob extends BaseJob { .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 b2894b804a..6d6199a2b3 100644 --- a/src/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java +++ b/src/org/thoughtcrime/securesms/notifications/MultipleRecipientNotificationBuilder.java @@ -8,7 +8,7 @@ import androidx.annotation.NonNull; 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 class MultipleRecipientNotificationBuilder extends AbstractNotificationBu 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 fb07b7e99c..dcdc9a2b62 100644 --- a/src/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java +++ b/src/org/thoughtcrime/securesms/notifications/PendingMessageNotificationBuilder.java @@ -6,7 +6,7 @@ import android.content.Context; 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 7bf64dcb77..634b1a00ea 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.annotation.Nullable; 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 final class RegistrationCompleteFragment extends BaseRegistrationFragment 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 5fd57053c5..0000000000 --- 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 ee578a4e76..8354f60659 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 com.annimon.stream.Stream; 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; + private final Executor serialExecutor; + private final ExecutorService parallelExecutor; - 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; + 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 class SearchRepository { return; } - executor.execute(() -> { - Stopwatch timer = new Stopwatch("FtsQuery"); + serialExecutor.execute(() -> { String cleanQuery = sanitizeQuery(query); - timer.split("clean"); - CursorList contacts = queryContacts(cleanQuery); - timer.split("contacts"); + Future> contacts = parallelExecutor.submit(() -> queryContacts(cleanQuery)); + Future> conversations = parallelExecutor.submit(() -> queryConversations(cleanQuery)); + Future> messages = parallelExecutor.submit(() -> queryMessages(cleanQuery)); - CursorList conversations = queryConversations(cleanQuery); - timer.split("conversations"); + try { + long startTime = System.currentTimeMillis(); + SearchResult result = new SearchResult(cleanQuery, contacts.get(), conversations.get(), messages.get()); - CursorList messages = queryMessages(cleanQuery); - timer.split("messages"); + Log.d(TAG, "Total time: " + (System.currentTimeMillis() - startTime) + " ms"); - timer.stop(TAG); - - 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); + + contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts }); + + return readToList(contacts, new RecipientModelBuilder(), 250); + } finally { + if (contacts != null) { + contacts.close(); + } } - - Cursor textSecureContacts = contactRepository.querySignalContacts(query); - Cursor systemContacts = contactRepository.queryNonSignalContacts(query); - MergeCursor contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts }); - - return new CursorList<>(contacts, new RecipientModelBuilder()); } - 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 7c307717f2..0000000000 --- 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 66d00a8429..0000000000 --- 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 70c27091ad..710a1d2b80 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; 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 @@ public class ApplicationMigrationService extends Service 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 class ApplicationMigrationService extends Service 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 bad0fe5dd9..d98ee43d1c 100644 --- a/src/org/thoughtcrime/securesms/service/GenericForegroundService.java +++ b/src/org/thoughtcrime/securesms/service/GenericForegroundService.java @@ -13,7 +13,7 @@ import androidx.annotation.Nullable; 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 @@ public final class GenericForegroundService extends Service { 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 4bfa2f45a7..7bade8b3de 100644 --- a/src/org/thoughtcrime/securesms/service/KeyCachingService.java +++ b/src/org/thoughtcrime/securesms/service/KeyCachingService.java @@ -32,9 +32,9 @@ import androidx.annotation.Nullable; 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 @@ public class KeyCachingService extends Service { } 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 007f4735fd..cd8237c386 100644 --- a/src/org/thoughtcrime/securesms/util/CommunicationActions.java +++ b/src/org/thoughtcrime/securesms/util/CommunicationActions.java @@ -72,7 +72,6 @@ public class CommunicationActions { 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);