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