mirror of
https://github.com/oxen-io/session-android.git
synced 2025-06-09 15:38:33 +00:00
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:
parent
608815a69b
commit
1e7c93007d
@ -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"/>
|
||||
|
@ -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>
|
@ -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"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
<androidx.constraintlayout.widget.ConstraintLayout 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"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="?android:windowBackground">
|
||||
|
||||
<TextView android:id="@+id/empty_search"
|
||||
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'"/>
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
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:layout_constraintTop_toTopOf="parent"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout android:id="@+id/empty_state"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
<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: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" />
|
||||
|
||||
<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="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" />
|
||||
|
||||
<TextView android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginTop="20dp"
|
||||
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>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<LinearLayout android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical"
|
||||
tools:visibility="gone">
|
||||
</androidx.appcompat.widget.Toolbar>
|
||||
|
||||
<org.thoughtcrime.securesms.components.reminder.ReminderView
|
||||
android:id="@+id/reminder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
<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" />
|
||||
|
||||
<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:nextFocusDown="@+id/fab"
|
||||
android:nextFocusForward="@+id/fab"
|
||||
android:clipToPadding="false"
|
||||
tools:listitem="@layout/conversation_list_item_view" />
|
||||
<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" />
|
||||
|
||||
</LinearLayout>
|
||||
<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" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.registration.PulsingFloatingActionButton
|
||||
android:id="@+id/camera_fab"
|
||||
<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="bottom|end"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="88dp"
|
||||
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:layout_gravity="center_horizontal"
|
||||
tools:src="@drawable/conversation_list_empty_state" />
|
||||
|
||||
<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:focusable="true"
|
||||
android:contentDescription="@string/conversation_list_fragment__fab_content_description"/>
|
||||
<TextView
|
||||
style="@style/Signal.Text.Body"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="32dp"
|
||||
android:layout_marginTop="20dp"
|
||||
android:layout_marginEnd="32dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/conversation_list_fragment__give_your_inbox_something_to_write_home_about_get_started_by_messaging_a_friend" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
<org.thoughtcrime.securesms.components.reminder.ReminderView
|
||||
android:id="@+id/reminder"
|
||||
android:layout_width="match_parent"
|
||||
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="0dp"
|
||||
android:clipToPadding="false"
|
||||
android:nextFocusDown="@+id/fab"
|
||||
android:nextFocusForward="@+id/fab"
|
||||
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_marginBottom="20dp"
|
||||
android:contentDescription="@string/conversation_list_fragment__open_camera_description"
|
||||
android:focusable="true"
|
||||
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_margin="16dp"
|
||||
android:contentDescription="@string/conversation_list_fragment__fab_content_description"
|
||||
android:focusable="true"
|
||||
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.constraintlayout.widget.ConstraintLayout>
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
@ -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>
|
10
res/layout/main_activity.xml
Normal file
10
res/layout/main_activity.xml
Normal 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>
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
|
45
src/org/thoughtcrime/securesms/MainActivity.java
Normal file
45
src/org/thoughtcrime/securesms/MainActivity.java
Normal 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;
|
||||
}
|
||||
}
|
22
src/org/thoughtcrime/securesms/MainFragment.java
Normal file
22
src/org/thoughtcrime/securesms/MainFragment.java
Normal 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());
|
||||
}
|
||||
}
|
104
src/org/thoughtcrime/securesms/MainNavigator.java
Normal file
104
src/org/thoughtcrime/securesms/MainNavigator.java
Normal 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();
|
||||
}
|
||||
}
|
@ -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() {
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -5,13 +5,15 @@ import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||
import org.thoughtcrime.securesms.contacts.ContactRepository;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.SearchResult;
|
||||
import org.thoughtcrime.securesms.database.CursorList;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.search.SearchRepository;
|
||||
import org.thoughtcrime.securesms.search.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.conversationlist.model.MessageResult;
|
||||
import org.thoughtcrime.securesms.util.CloseableLiveData;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
@ -22,9 +24,9 @@ import java.util.List;
|
||||
|
||||
public class ConversationSearchViewModel extends AndroidViewModel {
|
||||
|
||||
private final SearchRepository searchRepository;
|
||||
private final CloseableLiveData<SearchResult> result;
|
||||
private final Debouncer debouncer;
|
||||
private final SearchRepository searchRepository;
|
||||
private final MutableLiveData<SearchResult> result;
|
||||
private final Debouncer debouncer;
|
||||
|
||||
private boolean firstSearch;
|
||||
private boolean searchOpen;
|
||||
@ -33,15 +35,9 @@ public class ConversationSearchViewModel extends AndroidViewModel {
|
||||
|
||||
public ConversationSearchViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
Context context = application.getApplicationContext();
|
||||
result = new CloseableLiveData<>();
|
||||
result = new MutableLiveData<>();
|
||||
debouncer = new Debouncer(500);
|
||||
searchRepository = new SearchRepository(context,
|
||||
DatabaseFactory.getSearchDatabase(context),
|
||||
DatabaseFactory.getThreadDatabase(context),
|
||||
new ContactRepository(application),
|
||||
ContactAccessor.getInstance(),
|
||||
SignalExecutors.SERIAL);
|
||||
searchRepository = new SearchRepository();
|
||||
}
|
||||
|
||||
LiveData<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 int position;
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
@ -55,7 +58,7 @@ import java.util.Set;
|
||||
|
||||
public class ConversationListItem extends RelativeLayout
|
||||
implements RecipientForeverObserver,
|
||||
BindableConversationListItem, Unbindable
|
||||
BindableConversationListItem, Unbindable
|
||||
{
|
||||
@SuppressWarnings("unused")
|
||||
private final static String TAG = ConversationListItem.class.getSimpleName();
|
@ -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;
|
@ -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);
|
||||
}
|
@ -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,9 +33,9 @@ class SearchListAdapter extends RecyclerView.Adapter<SearchListAdapter.Search
|
||||
@NonNull
|
||||
private SearchResult searchResult = SearchResult.EMPTY;
|
||||
|
||||
SearchListAdapter(@NonNull GlideRequests glideRequests,
|
||||
@NonNull EventListener eventListener,
|
||||
@NonNull Locale locale)
|
||||
ConversationListSearchAdapter(@NonNull GlideRequests glideRequests,
|
||||
@NonNull EventListener eventListener,
|
||||
@NonNull Locale locale)
|
||||
{
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
@ -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()));
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package org.thoughtcrime.securesms.search.model;
|
||||
package org.thoughtcrime.securesms.conversationlist.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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())
|
||||
|
@ -46,6 +46,7 @@ public class ApplicationDependencies {
|
||||
}
|
||||
|
||||
public static @NonNull Application getApplication() {
|
||||
assertInitialization();
|
||||
return application;
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts });
|
||||
|
||||
return readToList(contacts, new RecipientModelBuilder(), 250);
|
||||
} finally {
|
||||
if (contacts != null) {
|
||||
contacts.close();
|
||||
}
|
||||
}
|
||||
|
||||
Cursor textSecureContacts = contactRepository.querySignalContacts(query);
|
||||
Cursor systemContacts = contactRepository.queryNonSignalContacts(query);
|
||||
MergeCursor contacts = new MergeCursor(new Cursor[]{ textSecureContacts, systemContacts });
|
||||
|
||||
return new CursorList<>(contacts, new RecipientModelBuilder());
|
||||
}
|
||||
|
||||
private CursorList<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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
Loading…
x
Reference in New Issue
Block a user