Convert the conversation list into a real fragment.

It was a fragment before, but it's functionality was inappropriately
split between the various layers.

This also sets us up better to do tablet stuff in the future, if we
choose to do that.
This commit is contained in:
Greyson Parrelli 2019-11-14 14:35:08 -05:00
parent 608815a69b
commit 1e7c93007d
48 changed files with 1759 additions and 1819 deletions

View File

@ -122,11 +122,11 @@
<activity android:name=".InviteActivity"
android:theme="@style/TextSecure.HighlightTheme"
android:windowSoftInputMode="stateHidden"
android:parentActivityName=".ConversationListActivity"
android:parentActivityName=".MainActivity"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
android:value="org.thoughtcrime.securesms.MainActivity" />
</activity>
<activity android:name=".PromptMmsActivity"
@ -189,16 +189,8 @@
</intent-filter>
</activity>
<activity android:name=".ConversationListActivity"
android:label="@string/app_name"
android:launchMode="singleTask"
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:exported="true" />
<activity-alias android:name=".RoutingActivity"
android:targetActivity=".ConversationListActivity"
android:targetActivity=".MainActivity"
android:exported="true">
<intent-filter>
@ -214,24 +206,14 @@
</activity-alias>
<activity android:name=".ConversationListArchiveActivity"
android:label="@string/AndroidManifest_archived_conversations"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:parentActivityName=".ConversationListActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
</activity>
<activity android:name=".conversation.ConversationActivity"
android:windowSoftInputMode="stateUnchanged"
android:launchMode="singleTask"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"
android:parentActivityName=".ConversationListActivity">
android:parentActivityName=".MainActivity">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.ConversationListActivity" />
android:value="org.thoughtcrime.securesms.MainActivity" />
</activity>
<activity android:name=".longmessage.LongMessageActivity" />
@ -468,6 +450,10 @@
android:theme="@style/TextSecure.LightNoActionBar"
android:windowSoftInputMode="adjustResize"/>
<activity android:name=".MainActivity"
android:theme="@style/TextSecure.LightNoActionBar"
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize" />
<service android:enabled="true" android:name="org.thoughtcrime.securesms.service.WebRtcCallService"/>
<service android:enabled="true" android:name=".service.ApplicationMigrationService"/>
<service android:enabled="true" android:exported="false" android:name=".service.KeyCachingService"/>

View File

@ -1,90 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@id/container"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_alignParentTop="true"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:minHeight="?attr/actionBarSize"
android:background="?attr/conversation_list_toolbar_background"
android:theme="?attr/actionBarStyle">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/toolbar_icon"
android:contentDescription="@string/conversation_list_settings_shortcut"
android:layout_width="58dp"
android:layout_height="48dp"
android:layout_alignParentStart="true"
android:paddingTop="10dp"
android:paddingBottom="10dp"
android:paddingStart="12dp"
android:paddingEnd="18dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
tools:src="@drawable/ic_contact_picture" />
<TextView android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="@string/app_name"
android:textColor="?attr/title_text_color_primary"
android:textSize="20sp"
android:textStyle="bold"
android:layout_marginStart="6dp"
app:layout_constraintStart_toEndOf="@id/toolbar_icon"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/search_action"
android:textAlignment="viewStart" />
<ImageView android:id="@+id/search_action"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?actionBarItemBackground"
app:srcCompat="@drawable/ic_search_24"
android:tint="?icon_tint"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:contentDescription="@string/conversation_list_search_description"
android:padding="12dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
<FrameLayout android:id="@+id/fragment_container"
android:layout_below="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<org.thoughtcrime.securesms.components.SearchToolbar
android:id="@+id/search_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="?attr/conversation_list_toolbar_background"
android:elevation="4dp"
android:visibility="invisible" />
<View
android:id="@+id/conversation_list_toolbar_shadow"
android:layout_width="match_parent"
android:layout_height="4dp"
android:layout_below="@id/toolbar"
android:background="@drawable/toolbar_shadow"
android:visibility="gone"
tools:visibility="visible" />
</RelativeLayout>

View File

@ -1,96 +1,200 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<TextView android:id="@+id/empty_search"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="100dp"
android:gravity="center"
android:textSize="18sp"
android:padding="16dp"
android:visibility="invisible"
tools:text="No results found for 'foo'"/>
android:layout_height="match_parent"
android:background="?android:windowBackground">
<LinearLayout android:id="@+id/empty_state"
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:orientation="vertical"
android:layout_height="?attr/actionBarSize"
android:background="?attr/conversation_list_toolbar_background"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarStyle"
android:visibility="gone"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView android:id="@+id/empty"
<org.thoughtcrime.securesms.components.AvatarImageView
android:id="@+id/toolbar_icon"
android:layout_width="58dp"
android:layout_height="48dp"
android:layout_alignParentStart="true"
android:contentDescription="@string/conversation_list_settings_shortcut"
android:paddingStart="12dp"
android:paddingTop="10dp"
android:paddingEnd="18dp"
android:paddingBottom="10dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_contact_picture" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="6dp"
android:text="@string/app_name"
android:textAlignment="viewStart"
android:textColor="?attr/title_text_color_primary"
android:textSize="20sp"
android:textStyle="bold"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/search_action"
app:layout_constraintStart_toEndOf="@id/toolbar_icon"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/search_action"
android:layout_width="48dp"
android:layout_height="48dp"
android:background="?actionBarItemBackground"
android:contentDescription="@string/conversation_list_search_description"
android:padding="12dp"
android:tint="?icon_tint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_search_24" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.appcompat.widget.Toolbar>
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar_basic"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/conversation_list_toolbar_background"
android:minHeight="?attr/actionBarSize"
android:theme="?attr/actionBarStyle"
android:visibility="gone"
app:titleTextAppearance="@style/TextSecure.TitleTextStyle"
app:layout_constraintTop_toTopOf="parent" />
<org.thoughtcrime.securesms.components.SearchToolbar
android:id="@+id/search_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/conversation_list_toolbar_background"
android:elevation="4dp"
android:visibility="invisible"
app:layout_constraintTop_toTopOf="parent" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/toolbar_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="toolbar,toolbar_basic" />
<View
android:id="@+id/conversation_list_toolbar_shadow"
android:layout_width="match_parent"
android:layout_height="4dp"
android:background="@drawable/toolbar_shadow"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar_barrier"
tools:visibility="visible" />
<TextView
android:id="@+id/search_no_results"
android:layout_width="0dp"
android:layout_height="0dp"
android:gravity="center"
android:background="?attr/search_background"
android:visibility="gone"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar_barrier"
app:layout_constraintBottom_toBottomOf="parent"
tools:text="@string/SearchFragment_no_results" />
<LinearLayout
android:id="@+id/empty_state"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="32dp"
android:orientation="vertical"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/toolbar_barrier"
tools:visibility="visible">
<ImageView
android:id="@+id/empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
tools:src="@drawable/conversation_list_empty_state" />
<TextView android:layout_width="match_parent"
<TextView
style="@style/Signal.Text.Body"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginEnd="32dp"
android:layout_marginTop="20dp"
android:layout_marginEnd="32dp"
android:layout_marginBottom="20dp"
style="@style/Signal.Text.Body"
android:text="@string/conversation_list_fragment__give_your_inbox_something_to_write_home_about_get_started_by_messaging_a_friend"
android:gravity="center" />
</LinearLayout>
android:gravity="center"
android:text="@string/conversation_list_fragment__give_your_inbox_something_to_write_home_about_get_started_by_messaging_a_friend" />
<LinearLayout android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:visibility="gone">
</LinearLayout>
<org.thoughtcrime.securesms.components.reminder.ReminderView
android:id="@+id/reminder"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
android:layout_height="wrap_content"
app:layout_constraintTop_toBottomOf="@id/toolbar_barrier"
tools:visibility="gone"/>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:paddingBottom="150dp"
android:scrollbars="vertical"
android:layout_height="0dp"
android:clipToPadding="false"
android:nextFocusDown="@+id/fab"
android:nextFocusForward="@+id/fab"
android:clipToPadding="false"
tools:listitem="@layout/conversation_list_item_view" />
</LinearLayout>
android:paddingBottom="150dp"
android:scrollbars="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintTop_toBottomOf="@id/reminder"
tools:listitem="@layout/conversation_list_item_view"
tools:visibility="gone"/>
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
android:id="@+id/camera_fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_marginEnd="16dp"
android:layout_marginBottom="88dp"
android:layout_marginBottom="20dp"
android:contentDescription="@string/conversation_list_fragment__open_camera_description"
app:srcCompat="@drawable/ic_camera_solid_24"
android:tint="?conversation_list_camera_icon_tint"
android:focusable="true"
app:backgroundTint="?conversation_list_camera_button_background"/>
android:tint="?conversation_list_camera_icon_tint"
app:backgroundTint="?conversation_list_camera_button_background"
app:layout_constraintBottom_toTopOf="@id/fab"
app:layout_constraintEnd_toEndOf="@id/fab"
app:srcCompat="@drawable/ic_camera_solid_24" />
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
android:id="@+id/fab"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:tint="?conversation_list_compose_icon_tint"
app:srcCompat="@drawable/ic_compose_solid_24"
android:contentDescription="@string/conversation_list_fragment__fab_content_description"
android:focusable="true"
android:contentDescription="@string/conversation_list_fragment__fab_content_description"/>
android:tint="?conversation_list_compose_icon_tint"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_compose_solid_24" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.ConversationListItemAction
<org.thoughtcrime.securesms.conversationlist.ConversationListItemAction
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
@ -14,4 +14,4 @@
android:textStyle="bold"
tools:text="Archived conversations (2)"/>
</org.thoughtcrime.securesms.ConversationListItemAction>
</org.thoughtcrime.securesms.conversationlist.ConversationListItemAction>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.ConversationListItemInboxZero
<org.thoughtcrime.securesms.conversationlist.ConversationListItemInboxZero
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
@ -28,4 +28,4 @@
android:padding="16dp"
android:text="@string/conversation_list_item_inbox_zero__zip_zilch_zero_nada_nyou_re_all_caught_up"/>
</org.thoughtcrime.securesms.ConversationListItemInboxZero>
</org.thoughtcrime.securesms.conversationlist.ConversationListItemInboxZero>

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<org.thoughtcrime.securesms.ConversationListItem
<org.thoughtcrime.securesms.conversationlist.ConversationListItem
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
@ -170,4 +170,4 @@
</RelativeLayout>
</org.thoughtcrime.securesms.ConversationListItem>
</org.thoughtcrime.securesms.conversationlist.ConversationListItem>

View File

@ -1,20 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/search_no_results"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:background="?attr/search_background"
android:visibility="gone" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/search_list"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/fragment_container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
</FrameLayout>

View File

@ -24,7 +24,6 @@ import android.os.Build;
import android.os.Bundle;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentManager;
import androidx.fragment.app.FragmentTransaction;
@ -110,7 +109,8 @@ public class ApplicationPreferencesActivity extends PassphraseRequiredActionBarA
if (fragmentManager.getBackStackEntryCount() > 0) {
fragmentManager.popBackStack();
} else {
Intent intent = new Intent(this, ConversationListActivity.class);
// TODO [greyson] Navigation
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Void, Void, Void>() {
@Override
protected Void doInBackground(Void... params) {
Context context = ConversationListActivity.this;
List<MarkedMessageInfo> 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();
}
}
}

View File

@ -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() {
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Cursor>, 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<Context, Void, Optional<? extends Reminder>>() {
@Override
protected Optional<? extends Reminder> doInBackground(Context... params) {
final Context context = params[0];
if (UnauthorizedReminder.isEligible(context)) {
return Optional.of(new UnauthorizedReminder(context));
} else if (ExpiredBuildReminder.isEligible()) {
return Optional.of(new ExpiredBuildReminder(context));
} else if (ServiceOutageReminder.isEligible(context)) {
ApplicationDependencies.getJobManager().add(new ServiceOutageDetectionJob());
return Optional.of(new ServiceOutageReminder(context));
} else if (OutdatedBuildReminder.isEligible()) {
return Optional.of(new OutdatedBuildReminder(context));
} else if (DefaultSmsReminder.isEligible(context)) {
return Optional.of(new DefaultSmsReminder(context));
} else if (Util.isDefaultSmsProvider(context) && SystemSmsImportReminder.isEligible(context)) {
return Optional.of((new SystemSmsImportReminder(context)));
} else if (PushRegistrationReminder.isEligible(context)) {
return Optional.of((new PushRegistrationReminder(context)));
} else if (ShareReminder.isEligible(context)) {
return Optional.of(new ShareReminder(context));
} else if (DozeReminder.isEligible(context)) {
return Optional.of(new DozeReminder(context));
} else {
return Optional.absent();
}
}
@Override
protected void onPostExecute(Optional<? extends Reminder> reminder) {
if (reminder.isPresent() && getActivity() != null && !isRemoving()) {
reminderView.showReminder(reminder.get());
} else if (!reminder.isPresent()) {
reminderView.hide();
}
}
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, getActivity());
}
private void initializeListAdapter() {
list.setAdapter(new ConversationListAdapter(getActivity(), GlideApp.with(this), locale, null, this));
getLoaderManager().restartLoader(0, null, this);
}
private void initializeTypingObserver() {
ApplicationContext.getInstance(requireContext()).getTypingStatusRepository().getTypingThreads().observe(this, threadIds -> {
if (threadIds == null) {
threadIds = Collections.emptySet();
}
getListAdapter().setTypingThreads(threadIds);
});
}
@SuppressLint("StaticFieldLeak")
private void handleArchiveAllSelected() {
final Set<Long> 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<Void>(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<Long> selectedConversations = (getListAdapter())
.getBatchSelections();
if (!selectedConversations.isEmpty()) {
new AsyncTask<Void, Void, Void>() {
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<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return new ConversationListLoader(getActivity(), queryFilter, archive);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> 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<Cursor> 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<Long>(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<Long>(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<MarkedMessageInfo> 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();
}
}
}
}

View File

@ -149,7 +149,8 @@ public class DatabaseMigrationActivity extends PassphraseRequiredActionBarActivi
if (getIntent().hasExtra("next_intent")) {
startActivity((Intent)getIntent().getParcelableExtra("next_intent"));
} else {
startActivity(new Intent(this, ConversationListActivity.class));
// TODO [greyson] Navigation
startActivity(new Intent(this, MainActivity.class));
}
}

View File

@ -189,7 +189,8 @@ public class ExperienceUpgradeActivity extends BaseActionBarActivity implements
TextSecurePreferences.setLastExperienceVersionCode(this, latestVersion);
if (seenUpgrade.isPresent() && seenUpgrade.get().nextIntent != null) {
Intent intent = new Intent(this, seenUpgrade.get().nextIntent);
Intent nextIntent = new Intent(this, ConversationListActivity.class);
// TODO [greyson] Navigation
Intent nextIntent = new Intent(this, MainActivity.class);
intent.putExtra("next_intent", nextIntent);
startActivity(intent);
} else {

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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();
}
}

View File

@ -187,7 +187,8 @@ public abstract class PassphraseRequiredActionBarActivity extends BaseActionBarA
}
private Intent getConversationListIntent() {
return new Intent(this, ConversationListActivity.class);
// TODO [greyson] Navigation
return new Intent(this, MainActivity.class);
}
private void initializeClearKeyReceiver() {

View File

@ -33,14 +33,16 @@ public class ShortcutLauncherActivity extends AppCompatActivity {
if (rawId == null) {
Toast.makeText(this, R.string.ShortcutLauncherActivity_invalid_shortcut, Toast.LENGTH_SHORT).show();
startActivity(new Intent(this, ConversationListActivity.class));
// TODO [greyson] Navigation
startActivity(new Intent(this, MainActivity.class));
finish();
return;
}
Recipient recipient = Recipient.live(RecipientId.from(rawId)).get();
// TODO [greyson] Navigation
TaskStackBuilder backStack = TaskStackBuilder.create(this)
.addNextIntent(new Intent(this, ConversationListActivity.class));
.addNextIntent(new Intent(this, MainActivity.class));
CommunicationActions.startConversation(this, recipient, null, backStack);
finish();

View File

@ -5,8 +5,8 @@ import android.content.Intent;
import android.view.View;
import android.view.View.OnClickListener;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.DatabaseMigrationActivity;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.service.ApplicationMigrationService;
@ -21,7 +21,8 @@ public class SystemSmsImportReminder extends Reminder {
intent.setAction(ApplicationMigrationService.MIGRATE_DATABASE);
context.startService(intent);
Intent nextIntent = new Intent(context, ConversationListActivity.class);
// TODO [greyson] Navigation
Intent nextIntent = new Intent(context, MainActivity.class);
Intent activityIntent = new Intent(context, DatabaseMigrationActivity.class);
activityIntent.putExtra("next_intent", nextIntent);
context.startActivity(activityIntent);

View File

@ -34,9 +34,11 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import java.util.ArrayList;
import java.util.Collection;
@ -183,20 +185,14 @@ public class ContactAccessor {
public List<String> getNumbersForThreadSearchFilter(Context context, String constraint) {
LinkedList<String> 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;

View File

@ -11,7 +11,6 @@ import android.widget.CheckBox;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.ConversationListFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.FromTextView;

View File

@ -80,11 +80,10 @@ import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.ConversationListArchiveActivity;
import org.thoughtcrime.securesms.ExpirationDialog;
import org.thoughtcrime.securesms.GroupCreateActivity;
import org.thoughtcrime.securesms.GroupMembersDialog;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.MediaOverviewActivity;
import org.thoughtcrime.securesms.MuteDialog;
import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity;
@ -197,7 +196,7 @@ import org.thoughtcrime.securesms.recipients.RecipientFormattingException;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity;
import org.thoughtcrime.securesms.search.model.MessageResult;
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.service.KeyCachingService;
import org.thoughtcrime.securesms.sms.MessageSender;
import org.thoughtcrime.securesms.sms.OutgoingEncryptedMessage;
@ -276,7 +275,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
public static final String MEDIA_EXTRA = "media_list";
public static final String STICKER_EXTRA = "sticker_extra";
public static final String DISTRIBUTION_TYPE_EXTRA = "distribution_type";
public static final String TIMING_EXTRA = "timing";
public static final String LAST_SEEN_EXTRA = "last_seen";
public static final String STARTING_POSITION_EXTRA = "starting_position";
@ -341,6 +339,23 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme();
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
public static @NonNull Intent buildIntent(@NonNull Context context,
@NonNull RecipientId recipientId,
long threadId,
int distributionType,
long lastSeen,
int startingPosition)
{
Intent intent = new Intent(context, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipientId);
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.DISTRIBUTION_TYPE_EXTRA, distributionType);
intent.putExtra(ConversationActivity.LAST_SEEN_EXTRA, lastSeen);
intent.putExtra(ConversationActivity.STARTING_POSITION_EXTRA, startingPosition);
return intent;
}
@Override
protected void onPreCreate() {
dynamicTheme.onCreate(this);
@ -355,7 +370,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (recipientId == null) {
Log.w(TAG, "[onCreate] Missing recipientId!");
startActivity(new Intent(this, ConversationListActivity.class));
// TODO [greyson] Navigation
startActivity(new Intent(this, MainActivity.class));
finish();
return;
}
@ -427,7 +443,8 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
if (recipientId == null) {
Log.w(TAG, "[onNewIntent] Missing recipientId!");
startActivity(new Intent(this, ConversationListActivity.class));
// TODO [greyson] Navigation
startActivity(new Intent(this, MainActivity.class));
finish();
return;
}
@ -470,8 +487,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
MessageNotifier.setVisibleThread(threadId);
markThreadAsRead();
Log.i(TAG, "onResume() Finished: " + (System.currentTimeMillis() - getIntent().getLongExtra(TIMING_EXTRA, 0)));
}
@Override
@ -802,7 +817,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
case R.id.menu_conversation_settings: handleConversationSettings(); return true;
case R.id.menu_expiring_messages_off:
case R.id.menu_expiring_messages: handleSelectMessageExpiration(); return true;
case android.R.id.home: handleReturnToConversationList(); return true;
case android.R.id.home: onBackPressed(); return true;
}
return false;
@ -832,13 +847,6 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
//////// Event Handlers
private void handleReturnToConversationList() {
Intent intent = new Intent(this, (archived ? ConversationListArchiveActivity.class : ConversationListActivity.class));
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
finish();
}
private void handleSelectMessageExpiration() {
if (isPushGroupConversation() && !isActiveGroup()) {
return;

View File

@ -5,13 +5,15 @@ import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactRepository;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.database.CursorList;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.search.SearchRepository;
import org.thoughtcrime.securesms.search.model.MessageResult;
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.util.CloseableLiveData;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.Util;
@ -23,7 +25,7 @@ import java.util.List;
public class ConversationSearchViewModel extends AndroidViewModel {
private final SearchRepository searchRepository;
private final CloseableLiveData<SearchResult> result;
private final MutableLiveData<SearchResult> result;
private final Debouncer debouncer;
private boolean firstSearch;
@ -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<SearchResult> getSearchResults() {
@ -73,7 +69,7 @@ public class ConversationSearchViewModel extends AndroidViewModel {
CursorList<MessageResult> messages = (CursorList<MessageResult>) result.getValue().getResults();
int position = Math.min(result.getValue().getPosition() + 1, messages.size() - 1);
result.setValue(new SearchResult(messages, position), false);
result.setValue(new SearchResult(messages, position));
}
void onMoveDown() {
@ -82,7 +78,7 @@ public class ConversationSearchViewModel extends AndroidViewModel {
CursorList<MessageResult> messages = (CursorList<MessageResult>) result.getValue().getResults();
int position = Math.max(result.getValue().getPosition() - 1, 0);
result.setValue(new SearchResult(messages, position), false);
result.setValue(new SearchResult(messages, position));
}
@ -94,13 +90,6 @@ public class ConversationSearchViewModel extends AndroidViewModel {
void onSearchClosed() {
searchOpen = false;
debouncer.clear();
result.close();
}
@Override
protected void onCleared() {
super.onCleared();
result.close();
}
private void updateQuery(@NonNull String query, long threadId) {
@ -114,20 +103,18 @@ public class ConversationSearchViewModel extends AndroidViewModel {
Util.runOnMain(() -> {
if (searchOpen && query.equals(activeQuery)) {
result.setValue(new SearchResult(messages, 0));
} else {
messages.close();
}
});
});
});
}
static class SearchResult implements Closeable {
static class SearchResult {
private final CursorList<MessageResult> results;
private final List<MessageResult> results;
private final int position;
SearchResult(CursorList<MessageResult> results, int position) {
SearchResult(@NonNull List<MessageResult> results, int position) {
this.results = results;
this.position = position;
}
@ -139,10 +126,5 @@ public class ConversationSearchViewModel extends AndroidViewModel {
public int getPosition() {
return position;
}
@Override
public void close() {
results.close();
}
}
}

View File

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
package org.thoughtcrime.securesms.conversationlist;
import android.content.Context;
import android.database.Cursor;
@ -25,6 +25,8 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.ThreadDatabase;

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Cursor>, 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<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return new ConversationListLoader(getActivity(), null, true);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> 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<Long>(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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<Cursor>,
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.<Reminder>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<MarkedMessageInfo> 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<Long> selectedConversations = new HashSet<>(defaultAdapter.getBatchSelections());
int count = selectedConversations.size();
String snackBarTitle = getResources().getQuantityString(getArchivedSnackbarTitleRes(), count, count);
new SnackbarAsyncTask<Void>(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<Long> selectedConversations = defaultAdapter.getBatchSelections();
if (!selectedConversations.isEmpty()) {
new AsyncTask<Void, Void, Void>() {
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<Cursor> onCreateLoader(int arg0, Bundle arg1) {
return new ConversationListLoader(getActivity(), null, false);
}
@Override
public void onLoadFinished(@NonNull Loader<Cursor> 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<Cursor> 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<Long>(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<MarkedMessageInfo> 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);
}
}
}
}
}

View File

@ -14,7 +14,7 @@
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.thoughtcrime.securesms;
package org.thoughtcrime.securesms.conversationlist;
import android.content.Context;
import android.content.res.ColorStateList;
@ -32,6 +32,9 @@ import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.Unbindable;
import org.thoughtcrime.securesms.components.AlertView;
import org.thoughtcrime.securesms.components.AvatarImageView;
import org.thoughtcrime.securesms.components.DeliveryStatusView;
@ -43,7 +46,7 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientForeverObserver;
import org.thoughtcrime.securesms.search.model.MessageResult;
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.SearchUtil;
import org.thoughtcrime.securesms.util.ThemeUtil;

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms;
package org.thoughtcrime.securesms.conversationlist;
import android.annotation.TargetApi;
import android.content.Context;
@ -8,6 +8,8 @@ import android.util.AttributeSet;
import android.widget.LinearLayout;
import android.widget.TextView;
import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.util.ViewUtil;

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms;
package org.thoughtcrime.securesms.conversationlist;
import android.content.Context;
@ -9,13 +9,14 @@ import androidx.annotation.RequiresApi;
import android.util.AttributeSet;
import android.widget.LinearLayout;
import org.thoughtcrime.securesms.BindableConversationListItem;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import java.util.Locale;
import java.util.Set;
public class ConversationListItemInboxZero extends LinearLayout implements BindableConversationListItem{
public class ConversationListItemInboxZero extends LinearLayout implements BindableConversationListItem {
public ConversationListItemInboxZero(Context context) {
super(context);
}

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.search;
package org.thoughtcrime.securesms.conversationlist;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -8,20 +8,19 @@ import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.thoughtcrime.securesms.ConversationListItem;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.search.model.MessageResult;
import org.thoughtcrime.securesms.search.model.SearchResult;
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
import java.util.Collections;
import java.util.Locale;
class SearchListAdapter extends RecyclerView.Adapter<SearchListAdapter.SearchResultViewHolder>
implements StickyHeaderDecoration.StickyHeaderAdapter<SearchListAdapter.HeaderViewHolder>
class ConversationListSearchAdapter extends RecyclerView.Adapter<ConversationListSearchAdapter.SearchResultViewHolder>
implements StickyHeaderDecoration.StickyHeaderAdapter<ConversationListSearchAdapter.HeaderViewHolder>
{
private static final int TYPE_CONVERSATIONS = 1;
private static final int TYPE_CONTACTS = 2;
@ -34,7 +33,7 @@ class SearchListAdapter extends RecyclerView.Adapter<SearchListAdapter.Search
@NonNull
private SearchResult searchResult = SearchResult.EMPTY;
SearchListAdapter(@NonNull GlideRequests glideRequests,
ConversationListSearchAdapter(@NonNull GlideRequests glideRequests,
@NonNull EventListener eventListener,
@NonNull Locale locale)
{

View File

@ -0,0 +1,80 @@
package org.thoughtcrime.securesms.conversationlist;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;
import android.app.Application;
import android.database.ContentObserver;
import android.os.Handler;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.search.SearchRepository;
import org.thoughtcrime.securesms.util.Debouncer;
import org.thoughtcrime.securesms.util.Util;
class ConversationListViewModel extends ViewModel {
private final Application application;
private final MutableLiveData<SearchResult> 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<SearchResult> 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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ConversationListViewModel(ApplicationDependencies.getApplication(), new SearchRepository()));
}
}
}

View File

@ -1,4 +1,4 @@
package org.thoughtcrime.securesms.search.model;
package org.thoughtcrime.securesms.conversationlist.model;
import androidx.annotation.NonNull;

View File

@ -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<Recipient> contacts;
private final List<ThreadRecord> conversations;
private final List<MessageResult> messages;
public SearchResult(@NonNull String query,
@NonNull List<Recipient> contacts,
@NonNull List<ThreadRecord> conversations,
@NonNull List<MessageResult> messages)
{
this.query = query;
this.contacts = contacts;
this.conversations = conversations;
this.messages = messages;
}
public List<Recipient> getContacts() {
return contacts;
}
public List<ThreadRecord> getConversations() {
return conversations;
}
public List<MessageResult> getMessages() {
return messages;
}
public String getQuery() {
return query;
}
public int size() {
return contacts.size() + conversations.size() + messages.size();
}
public boolean isEmpty() {
return size() == 0;
}
}

View File

@ -766,14 +766,31 @@ public class RecipientDatabase extends Database {
"(" + PHONE + " NOT NULL OR " + EMAIL + " NOT NULL) AND " +
"(" +
PHONE + " LIKE ? OR " +
EMAIL + " LIKE ? OR " +
SYSTEM_DISPLAY_NAME + " LIKE ?" +
")";
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query };
String[] args = new String[] { "0", String.valueOf(RegisteredState.REGISTERED.getId()), query, query, query };
String orderBy = SYSTEM_DISPLAY_NAME + ", " + PHONE;
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, orderBy);
}
public @Nullable Cursor queryAllContacts(@NonNull String query) {
query = TextUtils.isEmpty(query) ? "*" : query;
query = "%" + query + "%";
String selection = BLOCKED + " = ? AND " +
"(" +
SYSTEM_DISPLAY_NAME + " LIKE ? OR " +
SIGNAL_PROFILE_NAME + " LIKE ? OR " +
PHONE + " LIKE ? OR " +
EMAIL + " LIKE ?" +
")";
String[] args = new String[] { "0", query, query, query, query };
return databaseHelper.getReadableDatabase().query(TABLE_NAME, SEARCH_PROJECTION, selection, args, null, null, null);
}
public void applyBlockedUpdate(@NonNull List<SignalServiceAddress> blocked, List<byte[]> groupIds) {
List<String> blockedE164 = Stream.of(blocked)
.filter(b -> b.getNumber().isPresent())

View File

@ -46,6 +46,7 @@ public class ApplicationDependencies {
}
public static @NonNull Application getApplication() {
assertInitialization();
return application;
}

View File

@ -27,7 +27,7 @@ import org.signal.libsignal.metadata.ProtocolNoSessionException;
import org.signal.libsignal.metadata.ProtocolUntrustedIdentityException;
import org.signal.libsignal.metadata.SelfSendException;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.Attachment;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
@ -95,7 +95,6 @@ import org.thoughtcrime.securesms.util.Hex;
import org.thoughtcrime.securesms.util.IdentityUtil;
import org.thoughtcrime.securesms.util.MediaUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.whispersystems.libsignal.state.SessionStore;
import org.whispersystems.libsignal.state.SignalProtocolStore;
import org.whispersystems.libsignal.util.guava.Optional;
@ -210,6 +209,7 @@ public class PushDecryptJob extends BaseJob {
}
private void postMigrationNotification() {
// TODO [greyson] Navigation
NotificationManagerCompat.from(context).notify(494949,
new NotificationCompat.Builder(context, NotificationChannels.getMessagesChannel(context))
.setSmallIcon(R.drawable.icon_notification)
@ -217,7 +217,7 @@ public class PushDecryptJob extends BaseJob {
.setCategory(NotificationCompat.CATEGORY_MESSAGE)
.setContentTitle(context.getString(R.string.PushDecryptJob_new_locked_message))
.setContentText(context.getString(R.string.PushDecryptJob_unlock_to_view_pending_messages))
.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, ConversationListActivity.class), 0))
.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0))
.setDefaults(NotificationCompat.DEFAULT_SOUND | NotificationCompat.DEFAULT_VIBRATE)
.build());

View File

@ -8,7 +8,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
import org.thoughtcrime.securesms.recipients.Recipient;
@ -28,7 +28,8 @@ public class MultipleRecipientNotificationBuilder extends AbstractNotificationBu
setColor(context.getResources().getColor(R.color.textsecure_primary));
setSmallIcon(R.drawable.icon_notification);
setContentTitle(context.getString(R.string.app_name));
setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, ConversationListActivity.class), 0));
// TODO [greyson] Navigation
setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0));
setCategory(NotificationCompat.CATEGORY_MESSAGE);
setGroupSummary(true);

View File

@ -6,7 +6,7 @@ import android.content.Context;
import android.content.Intent;
import androidx.core.app.NotificationCompat;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.RecipientDatabase;
import org.thoughtcrime.securesms.preferences.widgets.NotificationPrivacyPreference;
@ -17,7 +17,8 @@ public class PendingMessageNotificationBuilder extends AbstractNotificationBuild
public PendingMessageNotificationBuilder(Context context, NotificationPrivacyPreference privacy) {
super(context, privacy);
Intent intent = new Intent(context, ConversationListActivity.class);
// TODO [greyson] Navigation
Intent intent = new Intent(context, MainActivity.class);
setSmallIcon(R.drawable.icon_notification);
setColor(context.getResources().getColor(R.color.textsecure_primary));

View File

@ -12,8 +12,8 @@ import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.navigation.ActivityNavigator;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.CreateProfileActivity;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
public final class RegistrationCompleteFragment extends BaseRegistrationFragment {
@ -31,7 +31,8 @@ public final class RegistrationCompleteFragment extends BaseRegistrationFragment
FragmentActivity activity = requireActivity();
if (!isReregister()) {
activity.startActivity(getRoutedIntent(activity, CreateProfileActivity.class, new Intent(activity, ConversationListActivity.class)));
// TODO [greyson] Navigation
activity.startActivity(getRoutedIntent(activity, CreateProfileActivity.class, new Intent(activity, MainActivity.class)));
}
activity.finish();

View File

@ -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<Void, Void, Integer>() {
@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;
}
}
}

View File

@ -1,11 +1,12 @@
package org.thoughtcrime.securesms.search;
import android.Manifest;
import android.content.Context;
import android.database.Cursor;
import android.database.DatabaseUtils;
import android.database.MergeCursor;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.TextUtils;
import com.annimon.stream.Stream;
@ -14,22 +15,29 @@ import com.annimon.stream.Stream;
import org.thoughtcrime.securesms.contacts.ContactAccessor;
import org.thoughtcrime.securesms.contacts.ContactRepository;
import org.thoughtcrime.securesms.database.CursorList;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.MmsSmsColumns;
import org.thoughtcrime.securesms.database.SearchDatabase;
import org.thoughtcrime.securesms.database.ThreadDatabase;
import org.thoughtcrime.securesms.database.model.ThreadRecord;
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.search.model.MessageResult;
import org.thoughtcrime.securesms.search.model.SearchResult;
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
import org.thoughtcrime.securesms.util.Stopwatch;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
/**
* Manages data retrieval for search.
@ -60,21 +68,17 @@ public class SearchRepository {
private final ContactRepository contactRepository;
private final ThreadDatabase threadDatabase;
private final ContactAccessor contactAccessor;
private final Executor executor;
private final Executor serialExecutor;
private final ExecutorService parallelExecutor;
public SearchRepository(@NonNull Context context,
@NonNull SearchDatabase searchDatabase,
@NonNull ThreadDatabase threadDatabase,
@NonNull ContactRepository contactRepository,
@NonNull ContactAccessor contactAccessor,
@NonNull Executor executor)
{
this.context = context.getApplicationContext();
this.searchDatabase = searchDatabase;
this.threadDatabase = threadDatabase;
this.contactRepository = contactRepository;
this.contactAccessor = contactAccessor;
this.executor = executor;
public SearchRepository() {
this.context = ApplicationDependencies.getApplication().getApplicationContext();
this.searchDatabase = DatabaseFactory.getSearchDatabase(context);
this.threadDatabase = DatabaseFactory.getThreadDatabase(context);
this.contactRepository = new ContactRepository(context);
this.contactAccessor = ContactAccessor.getInstance();
this.serialExecutor = SignalExecutors.SERIAL;
this.parallelExecutor = SignalExecutors.BOUNDED;
}
public void query(@NonNull String query, @NonNull Callback<SearchResult> callback) {
@ -83,73 +87,99 @@ public class SearchRepository {
return;
}
executor.execute(() -> {
Stopwatch timer = new Stopwatch("FtsQuery");
serialExecutor.execute(() -> {
String cleanQuery = sanitizeQuery(query);
timer.split("clean");
CursorList<Recipient> contacts = queryContacts(cleanQuery);
timer.split("contacts");
Future<List<Recipient>> contacts = parallelExecutor.submit(() -> queryContacts(cleanQuery));
Future<List<ThreadRecord>> conversations = parallelExecutor.submit(() -> queryConversations(cleanQuery));
Future<List<MessageResult>> messages = parallelExecutor.submit(() -> queryMessages(cleanQuery));
CursorList<ThreadRecord> conversations = queryConversations(cleanQuery);
timer.split("conversations");
try {
long startTime = System.currentTimeMillis();
SearchResult result = new SearchResult(cleanQuery, contacts.get(), conversations.get(), messages.get());
CursorList<MessageResult> messages = queryMessages(cleanQuery);
timer.split("messages");
Log.d(TAG, "Total time: " + (System.currentTimeMillis() - startTime) + " ms");
timer.stop(TAG);
callback.onResult(new SearchResult(cleanQuery, contacts, conversations, messages));
callback.onResult(result);
} catch (ExecutionException | InterruptedException e) {
Log.w(TAG, e);
callback.onResult(SearchResult.EMPTY);
}
});
}
public void query(@NonNull String query, long threadId, @NonNull Callback<CursorList<MessageResult>> callback) {
public void query(@NonNull String query, long threadId, @NonNull Callback<List<MessageResult>> callback) {
if (TextUtils.isEmpty(query)) {
callback.onResult(CursorList.emptyList());
return;
}
executor.execute(() -> {
serialExecutor.execute(() -> {
long startTime = System.currentTimeMillis();
CursorList<MessageResult> messages = queryMessages(sanitizeQuery(query), threadId);
List<MessageResult> messages = queryMessages(sanitizeQuery(query), threadId);
Log.d(TAG, "[ConversationQuery] " + (System.currentTimeMillis() - startTime) + " ms");
callback.onResult(messages);
});
}
private CursorList<Recipient> queryContacts(String query) {
if (!Permissions.hasAny(context, Manifest.permission.READ_CONTACTS, Manifest.permission.WRITE_CONTACTS)) {
return CursorList.emptyList();
}
private List<Recipient> queryContacts(String query) {
Cursor contacts = null;
try {
Cursor textSecureContacts = contactRepository.querySignalContacts(query);
Cursor systemContacts = contactRepository.queryNonSignalContacts(query);
MergeCursor contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts });
return new CursorList<>(contacts, new RecipientModelBuilder());
contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts });
return readToList(contacts, new RecipientModelBuilder(), 250);
} finally {
if (contacts != null) {
contacts.close();
}
}
}
private CursorList<ThreadRecord> queryConversations(@NonNull String query) {
private @NonNull List<ThreadRecord> queryConversations(@NonNull String query) {
List<String> numbers = contactAccessor.getNumbersForThreadSearchFilter(context, query);
List<RecipientId> 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<MessageResult> queryMessages(@NonNull String query) {
Cursor messages = searchDatabase.queryMessages(query);
return messages != null ? new CursorList<>(messages, new MessageModelBuilder(context))
: CursorList.emptyList();
private @NonNull List<MessageResult> queryMessages(@NonNull String query) {
try (Cursor cursor = searchDatabase.queryMessages(query)) {
return readToList(cursor, new MessageModelBuilder(context));
}
}
private CursorList<MessageResult> 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<MessageResult> queryMessages(@NonNull String query, long threadId) {
try (Cursor cursor = searchDatabase.queryMessages(query, threadId)) {
return readToList(cursor, new MessageModelBuilder(context));
}
}
private @NonNull <T> List<T> readToList(@Nullable Cursor cursor, @NonNull CursorList.ModelBuilder<T> builder) {
return readToList(cursor, builder, -1);
}
private @NonNull <T> List<T> readToList(@Nullable Cursor cursor, @NonNull CursorList.ModelBuilder<T> builder, int limit) {
if (cursor == null) {
return Collections.emptyList();
}
int i = 0;
List<T> list = new ArrayList<>(cursor.getCount());
while (cursor.moveToNext() && (limit < 0 || i < limit)) {
list.add(builder.build(cursor));
i++;
}
return list;
}
/**

View File

@ -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<SearchResult> 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<SearchResult> {
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 extends ViewModel> T create(@NonNull Class<T> modelClass) {
return modelClass.cast(new SearchViewModel(searchRepository));
}
}
}

View File

@ -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<Recipient> contacts;
private final CursorList<ThreadRecord> conversations;
private final CursorList<MessageResult> messages;
public SearchResult(@NonNull String query,
@NonNull CursorList<Recipient> contacts,
@NonNull CursorList<ThreadRecord> conversations,
@NonNull CursorList<MessageResult> messages)
{
this.query = query;
this.contacts = contacts;
this.conversations = conversations;
this.messages = messages;
}
public List<Recipient> getContacts() {
return contacts;
}
public List<ThreadRecord> getConversations() {
return conversations;
}
public List<MessageResult> 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();
}
}

View File

@ -16,7 +16,7 @@ import android.os.PowerManager;
import android.os.PowerManager.WakeLock;
import androidx.core.app.NotificationCompat;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.database.SmsMigrator;
import org.thoughtcrime.securesms.database.SmsMigrator.ProgressDescription;
@ -133,7 +133,8 @@ public class ApplicationMigrationService extends Service
builder.setContentText(getString(R.string.ApplicationMigrationService_import_in_progress));
builder.setOngoing(true);
builder.setProgress(100, 0, false);
builder.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ConversationListActivity.class), 0));
// TODO [greyson] Navigation
builder.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0));
stopForeground(true);
startForeground(4242, builder.build());
@ -184,7 +185,8 @@ public class ApplicationMigrationService extends Service
builder.setSmallIcon(R.drawable.icon_notification);
builder.setContentTitle(context.getString(R.string.ApplicationMigrationService_import_complete));
builder.setContentText(context.getString(R.string.ApplicationMigrationService_system_database_import_is_complete));
builder.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, ConversationListActivity.class), 0));
// TODO [greyson] Navigation
builder.setContentIntent(PendingIntent.getActivity(context, 0, new Intent(context, MainActivity.class), 0));
builder.setWhen(System.currentTimeMillis());
builder.setDefaults(Notification.DEFAULT_VIBRATE);
builder.setAutoCancel(true);

View File

@ -13,7 +13,7 @@ import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import androidx.core.content.ContextCompat;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.notifications.NotificationChannels;
@ -103,11 +103,12 @@ public final class GenericForegroundService extends Service {
private void postObligatoryForegroundNotification(@NonNull Entry active) {
lastPosted = active;
// TODO [greyson] Navigation
startForeground(NOTIFICATION_ID, new NotificationCompat.Builder(this, active.channelId)
.setSmallIcon(active.iconRes)
.setContentTitle(active.title)
.setProgress(active.progressMax, active.progress, active.indeterminate)
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, ConversationListActivity.class), 0))
.setContentIntent(PendingIntent.getActivity(this, 0, new Intent(this, MainActivity.class), 0))
.build());
}

View File

@ -32,9 +32,9 @@ import androidx.annotation.Nullable;
import androidx.core.app.NotificationCompat;
import org.thoughtcrime.securesms.ApplicationContext;
import org.thoughtcrime.securesms.MainActivity;
import org.thoughtcrime.securesms.logging.Log;
import org.thoughtcrime.securesms.ConversationListActivity;
import org.thoughtcrime.securesms.DummyActivity;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.crypto.InvalidPassphraseException;
@ -285,7 +285,8 @@ public class KeyCachingService extends Service {
}
private PendingIntent buildLaunchIntent() {
Intent intent = new Intent(this, ConversationListActivity.class);
// TODO [greyson] Navigation
Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
return PendingIntent.getActivity(getApplicationContext(), 0, intent, 0);
}

View File

@ -72,7 +72,6 @@ public class CommunicationActions {
Intent intent = new Intent(context, ConversationActivity.class);
intent.putExtra(ConversationActivity.RECIPIENT_EXTRA, recipient.getId());
intent.putExtra(ConversationActivity.THREAD_ID_EXTRA, threadId);
intent.putExtra(ConversationActivity.TIMING_EXTRA, System.currentTimeMillis());
if (!TextUtils.isEmpty(text)) {
intent.putExtra(ConversationActivity.TEXT_EXTRA, text);