Implement in-app insights.
This commit is contained in:
parent
e2e9cd40b3
commit
a7dd78cce6
56 changed files with 2541 additions and 127 deletions
9
res/drawable/ic_x_tinted.xml
Normal file
9
res/drawable/ic_x_tinted.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?icon_tint"
|
||||
android:pathData="M20.5,4.5l-1,-1l-7.5,7.4l-7.5,-7.4l-1,1l7.4,7.5l-7.4,7.5l1,1l7.5,-7.4l7.5,7.4l1,-1l-7.4,-7.5z"/>
|
||||
</vector>
|
5
res/drawable/insights_cta_button_background.xml
Normal file
5
res/drawable/insights_cta_button_background.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/signal_primary" />
|
||||
<corners android:radius="2dp" />
|
||||
</shape>
|
5
res/drawable/insights_modal_background.xml
Normal file
5
res/drawable/insights_modal_background.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/white" />
|
||||
<corners android:radius="4dp" />
|
||||
</shape>
|
5
res/drawable/insights_modal_background_dark.xml
Normal file
5
res/drawable/insights_modal_background_dark.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/core_grey_75" />
|
||||
<corners android:radius="4dp" />
|
||||
</shape>
|
25
res/drawable/reminder_progress_ring.xml
Normal file
25
res/drawable/reminder_progress_ring.xml
Normal file
|
@ -0,0 +1,25 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item>
|
||||
<shape
|
||||
android:innerRadius="20dp"
|
||||
android:shape="ring"
|
||||
android:thickness="6dp"
|
||||
android:useLevel="false">
|
||||
|
||||
<solid android:color="@color/transparent_black_40" />
|
||||
|
||||
</shape>
|
||||
</item>
|
||||
<item>
|
||||
<shape
|
||||
android:innerRadius="20dp"
|
||||
android:shape="ring"
|
||||
android:thickness="6dp"
|
||||
android:useLevel="true">
|
||||
|
||||
<solid android:color="@color/white" />
|
||||
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
200
res/layout/insights_dashboard.xml
Normal file
200
res/layout/insights_dashboard.xml
Normal file
|
@ -0,0 +1,200 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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">
|
||||
|
||||
<androidx.core.widget.NestedScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:fillViewport="true"
|
||||
app:layout_constraintBottom_toTopOf="@id/insights_dashboard_this_stat_was_generated_locally"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="?actionBarSize">
|
||||
|
||||
<com.airbnb.lottie.LottieAnimationView
|
||||
android:id="@+id/insights_dashboard_lottie_animation"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/insights_dashboard_progress"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/insights_dashboard_progress"
|
||||
app:lottie_rawRes="@raw/lottie_insights_100" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.ArcProgressBar
|
||||
android:id="@+id/insights_dashboard_progress"
|
||||
style="@style/Widget.Signal.ArcProgressBar"
|
||||
android:layout_width="187dp"
|
||||
android:layout_height="187dp"
|
||||
android:layout_marginTop="8dp"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/insights_dashboard_avatar"
|
||||
android:layout_width="140dp"
|
||||
android:layout_height="140dp"
|
||||
app:layout_constraintBottom_toBottomOf="@id/insights_dashboard_progress"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/insights_dashboard_progress" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/insights_dashboard_percent_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/insights_dashboard_avatar">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/insights_dashboard_percent_secure"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Headline.Insights"
|
||||
tools:text="100" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/insights_dashboard_percent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:layout_marginBottom="1dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/Insights__percent"
|
||||
android:textAppearance="@style/TextAppearance.Signal.SubHead.Insights" />
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/insights_dashboard_encrypted_messages"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:text="@string/InsightsDashboardFragment__encrypted_messages"
|
||||
android:textAppearance="@style/TextAppearance.Signal.SubHead.Insights"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/insights_dashboard_percent_container" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/insights_dashboard_tagline"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body.Insights"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/insights_dashboard_encrypted_messages"
|
||||
tools:text="100% of your outgoing messages in the past 7 days were end-to-end encrypted with Signal Protocol." />
|
||||
|
||||
<Button
|
||||
android:id="@+id/insights_dashboard_start_a_conversation"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="22dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:text="@string/InsightsDashboardFragment__start_a_conversation"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Caption.Insights"
|
||||
android:textColor="@color/signal_primary"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/insights_dashboard_tagline" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/insights_dashboard_make_signal_secure"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/InsightsDashboardFragment__boost_your_signal"
|
||||
android:textAppearance="@style/TextAppearance.Signal.SubHead.Insights"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/insights_dashboard_tagline" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/insights_dashboard_invite_your_contacts"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:text="@string/InsightsDashboardFragment__invite_your_contacts"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body.Insights"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/insights_dashboard_make_signal_secure" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/insights_dashboard_recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="20dp"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constrainedHeight="true"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/insights_dashboard_invite_your_contacts"
|
||||
tools:itemCount="10"
|
||||
tools:listitem="@layout/insights_dashboard_adapter_item" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/insights_dashboard_toolbar"
|
||||
style="?actionBarStyle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?android:windowBackground"
|
||||
android:theme="?actionBarStyle"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navigationIcon="@drawable/ic_x_tinted"
|
||||
app:title="@string/InsightsDashboardFragment__title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/insights_dashboard_this_stat_was_generated_locally"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?insight_dashboard_bottom_bar_background"
|
||||
android:gravity="center"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="24dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:paddingBottom="24dp"
|
||||
android:text="@string/InsightsDashboardFragment__this_stat_was_generated_locally"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2.Insights"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
56
res/layout/insights_dashboard_adapter_item.xml
Normal file
56
res/layout/insights_dashboard_adapter_item.xml
Normal file
|
@ -0,0 +1,56 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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="68dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/recipient_avatar"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:layout_marginStart="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recipient_display_name"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="9dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title.Insights"
|
||||
app:layout_constraintBottom_toTopOf="@id/recipient_insecure_contribution"
|
||||
app:layout_constraintStart_toEndOf="@id/recipient_avatar"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_chainStyle="packed"
|
||||
tools:text="Cayce Pollard" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/recipient_insecure_contribution"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="9dp"
|
||||
android:layout_marginTop="1dp"
|
||||
android:text="@string/InsightsDashboardFragment__not_using_signal_yet"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body.Insights"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/recipient_avatar"
|
||||
app:layout_constraintTop_toBottomOf="@id/recipient_display_name"
|
||||
app:layout_constraintVertical_chainStyle="packed" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/recipient_invite"
|
||||
style="@style/Button.Borderless"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="48dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:text="@string/conversation_insecure__invite"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Caption.Insights"
|
||||
android:textColor="@color/signal_primary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
114
res/layout/insights_modal.xml
Normal file
114
res/layout/insights_modal.xml
Normal file
|
@ -0,0 +1,114 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<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="wrap_content"
|
||||
android:background="?insight_modal_background"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/insights_modal_close"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:tint="?icon_tint"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_x" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/insights_modal_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="@dimen/insights_modal_title_margin_top"
|
||||
android:text="@string/InsightsModalFragment__title"
|
||||
android:textAppearance="@style/TextAppearance.Signal.SubHead.Insights"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/insights_modal_description"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="@dimen/insights_modal_description_margin_top"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:gravity="center"
|
||||
android:lineSpacingMultiplier="1.2"
|
||||
android:text="@string/InsightsModalFragment__description"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Title.Insights"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/insights_modal_title" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.ArcProgressBar
|
||||
android:id="@+id/insights_modal_progress"
|
||||
style="@style/Widget.Signal.ArcProgressBar"
|
||||
android:layout_width="@dimen/insights_modal_progress_size"
|
||||
android:layout_height="@dimen/insights_modal_progress_size"
|
||||
android:layout_marginTop="@dimen/insights_modal_progress_margin_top"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/insights_modal_description" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.AvatarImageView
|
||||
android:id="@+id/insights_modal_avatar"
|
||||
android:layout_width="@dimen/insights_modal_avatar_size"
|
||||
android:layout_height="@dimen/insights_modal_avatar_size"
|
||||
app:layout_constraintBottom_toBottomOf="@id/insights_modal_progress"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/insights_modal_progress" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/insights_modal_percent_container"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constrainedWidth="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHorizontal_chainStyle="packed"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/insights_modal_avatar">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/insights_modal_percent_secure"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Headline.Insights.Modal.Percent"
|
||||
tools:text="100" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/insights_modal_percent"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom"
|
||||
android:gravity="center"
|
||||
android:text="@string/Insights__percent"
|
||||
android:textAppearance="@style/TextAppearance.Signal.SubHead.Insights.Modal.Percent" />
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/insights_modal_view_insights"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="@dimen/insights_modal_view_insights_margin_top"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
android:background="@drawable/insights_cta_button_background"
|
||||
android:text="@string/InsightsModalFragment__view_insights"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Caption.Insights"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/insights_modal_progress" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
12
res/layout/reminder_action_button.xml
Normal file
12
res/layout/reminder_action_button.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Button xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/Widget.AppCompat.Button.Borderless"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingEnd="16dp"
|
||||
android:layout_marginStart="3dp"
|
||||
tools:text="Action" />
|
|
@ -1,6 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<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:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -12,33 +12,75 @@
|
|||
android:visibility="gone"
|
||||
tools:visibility="visible">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/reminder"
|
||||
android:layout_width="0dp"
|
||||
<ProgressBar
|
||||
android:id="@+id/reminder_progress"
|
||||
style="@style/Widget.ProgressBar.Horizontal"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:progressDrawable="@drawable/reminder_progress_ring"
|
||||
android:rotation="90"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:progress="10" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reminder_progress_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_margin="16dp"
|
||||
android:orientation="vertical">
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/reminder_progress"
|
||||
app:layout_constraintEnd_toEndOf="@id/reminder_progress"
|
||||
app:layout_constraintStart_toStartOf="@id/reminder_progress"
|
||||
app:layout_constraintTop_toTopOf="@id/reminder_progress"
|
||||
tools:text="100%" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reminder_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="18sp"
|
||||
tools:text="Invite to Signal"/>
|
||||
<TextView
|
||||
android:id="@+id/reminder_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="14dp"
|
||||
android:layout_marginBottom="6dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintBottom_toTopOf="@id/reminder_text"
|
||||
app:layout_constraintStart_toEndOf="@id/reminder_progress"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_goneMarginStart="16dp"
|
||||
tools:text="Invite to Signal" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/reminder_text"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:fontFamily="sans-serif-light"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
tools:text="Take your conversation with Jules Bonnot to the next level."/>
|
||||
<TextView
|
||||
android:id="@+id/reminder_text"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="16sp"
|
||||
app:layout_constraintBottom_toTopOf="@id/reminder_actions"
|
||||
app:layout_constraintEnd_toStartOf="@id/reminder_space"
|
||||
app:layout_constraintStart_toEndOf="@id/reminder_progress"
|
||||
app:layout_constraintTop_toBottomOf="@id/reminder_title"
|
||||
app:layout_goneMarginBottom="12dp"
|
||||
app:layout_goneMarginEnd="16dp"
|
||||
app:layout_goneMarginStart="16dp"
|
||||
app:layout_goneMarginTop="15dp"
|
||||
tools:text="Take your conversation with Jules Bonnot to the next level." />
|
||||
|
||||
</LinearLayout>
|
||||
<Space
|
||||
android:id="@+id/reminder_space"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/cancel"
|
||||
|
@ -46,10 +88,27 @@
|
|||
android:layout_height="48dp"
|
||||
android:layout_marginTop="2dp"
|
||||
android:background="?selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/InviteActivity_cancel"
|
||||
android:focusable="true"
|
||||
android:nextFocusLeft="@+id/container"
|
||||
android:nextFocusRight="@+id/container"
|
||||
android:src="@drawable/ic_close_white_24dp"
|
||||
android:contentDescription="@string/InviteActivity_cancel"/>
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</LinearLayout>
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/reminder_actions"
|
||||
android:layout_width="0dip"
|
||||
android:layout_height="46dp"
|
||||
android:orientation="horizontal"
|
||||
android:overScrollMode="never"
|
||||
android:visibility="gone"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:reverseLayout="true"
|
||||
tools:itemCount="2"
|
||||
tools:listitem="@layout/reminder_action_button" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
9
res/layout/reminder_progress.xml
Normal file
9
res/layout/reminder_progress.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ProgressBar xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
style="@style/Widget.ProgressBar.Horizontal"
|
||||
android:progressDrawable="@drawable/reminder_progress_ring"
|
||||
tools:progress="10"
|
||||
android:rotation="90"
|
||||
android:layout_width="52dp"
|
||||
android:layout_height="52dp" />
|
|
@ -16,6 +16,10 @@
|
|||
<item android:title="@string/text_secure_normal__menu_settings"
|
||||
android:id="@+id/menu_settings" />
|
||||
|
||||
<item android:title="@string/Insights__title"
|
||||
android:id="@+id/menu_insights"
|
||||
android:visible="false" />
|
||||
|
||||
<item android:title="@string/text_secure_normal__help"
|
||||
android:id="@+id/menu_help"/>
|
||||
|
||||
|
|
1
res/raw/lottie_insights_100.json
Normal file
1
res/raw/lottie_insights_100.json
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,4 +1,16 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<integer name="media_overview_cols">5</integer>
|
||||
|
||||
<dimen name="insights_modal_title_margin_top">12dp</dimen>
|
||||
<dimen name="insights_modal_description_margin_top">10dp</dimen>
|
||||
<dimen name="insights_modal_progress_margin_top">17dp</dimen>
|
||||
<dimen name="insights_modal_progress_size">131dp</dimen>
|
||||
<dimen name="insights_modal_avatar_size">94dp</dimen>
|
||||
<dimen name="insights_modal_percent_margin_top">2dp</dimen>
|
||||
<dimen name="insights_modal_percent_sign_margin_top">6dp</dimen>
|
||||
<dimen name="insights_modal_view_insights_margin_top">31dp</dimen>
|
||||
<dimen name="insights_modal_percent_text_size">20sp</dimen>
|
||||
<dimen name="insights_modal_percent_sign_text_size">16sp</dimen>
|
||||
|
||||
</resources>
|
|
@ -35,6 +35,13 @@
|
|||
<attr name="fab_color" format="reference|color" />
|
||||
<attr name="lower_right_divet" format="reference" />
|
||||
|
||||
<attr name="insight_modal_background" format="reference" />
|
||||
<attr name="insight_modal_button_background" format="color" />
|
||||
<attr name="insight_title" format="color" />
|
||||
<attr name="insight_body_2" format="color" />
|
||||
<attr name="insight_dashboard_bottom_bar_background" format="color" />
|
||||
<attr name="insight_progress_background" format="color" />
|
||||
|
||||
<attr name="centered_app_title_color" format="reference|color" />
|
||||
<attr name="ic_arrow_forward" format="reference" />
|
||||
<attr name="ic_visibility" format="reference" />
|
||||
|
|
|
@ -91,6 +91,17 @@
|
|||
<dimen name="unread_count_bubble_radius">13sp</dimen>
|
||||
<dimen name="unread_count_bubble_diameter">26sp</dimen>
|
||||
|
||||
<dimen name="insights_modal_title_margin_top">41dp</dimen>
|
||||
<dimen name="insights_modal_description_margin_top">12dp</dimen>
|
||||
<dimen name="insights_modal_progress_margin_top">23dp</dimen>
|
||||
<dimen name="insights_modal_progress_size">187dp</dimen>
|
||||
<dimen name="insights_modal_avatar_size">140dp</dimen>
|
||||
<dimen name="insights_modal_percent_margin_top">6dp</dimen>
|
||||
<dimen name="insights_modal_percent_sign_margin_top">15dp</dimen>
|
||||
<dimen name="insights_modal_view_insights_margin_top">41dp</dimen>
|
||||
<dimen name="insights_modal_percent_text_size">28sp</dimen>
|
||||
<dimen name="insights_modal_percent_sign_text_size">20sp</dimen>
|
||||
|
||||
<!-- RedPhone -->
|
||||
|
||||
<!-- Height of the main row of in-call buttons. -->
|
||||
|
|
|
@ -3,4 +3,7 @@
|
|||
<item type="id" name="holder_tag"/>
|
||||
<item type="id" name="contact_info_tag"/>
|
||||
<item type="id" name="motion_view_edittext"/>
|
||||
|
||||
<item name="reminder_action_view_insights" type="id" />
|
||||
<item name="reminder_action_invite" type="id" />
|
||||
</resources>
|
||||
|
|
|
@ -1517,6 +1517,7 @@
|
|||
<string name="reminder_header_share_text">The more friends use Signal, the better it gets.</string>
|
||||
<string name="reminder_header_service_outage_text">Signal is experiencing technical difficulties. We are working hard to restore service as quickly as possible.</string>
|
||||
<string name="reminder_header_the_latest_signal_features_wont_work">The latest Signal features won\'t work on this version of Android. Please upgrade this device to receive future Signal updates.</string>
|
||||
<string name="reminder_header_progress">%1$d%%</string>
|
||||
|
||||
<!-- media_preview -->
|
||||
<string name="media_preview__save_title">Save</string>
|
||||
|
@ -1538,6 +1539,34 @@
|
|||
<string name="trimmer__deleting_old_messages">Deleting old messages…</string>
|
||||
<string name="trimmer__old_messages_successfully_deleted">Old messages successfully deleted</string>
|
||||
|
||||
<!-- Insights -->
|
||||
<string name="Insights__percent">%</string>
|
||||
<string name="Insights__title">Insights</string>
|
||||
<string name="InsightsDashboardFragment__title">Insights</string>
|
||||
<string name="InsightsDashboardFragment__tagline">%1$d%% of your outgoing messages in the past 7 days were end-to-end encrypted with Signal Protocol.</string>
|
||||
<string name="InsightsDashboardFragment__boost_your_signal">Boost your Signal</string>
|
||||
<string name="InsightsDashboardFragment__100_title">Your Signal is Strong</string>
|
||||
<string name="InsightsDashboardFragment__no_signal_yet">No Signal (yet)</string>
|
||||
<string name="InsightsDashboardFragment__youre_just_getting_started">You\'re just getting started. Insights will be displayed after you send a few messages.</string>
|
||||
<string name="InsightsDashboardFragment__start_a_conversation">Start a conversation</string>
|
||||
<string name="InsightsDashboardFragment__100_description">Signal\'s advanced privacy-preserving technology automatically protected all of your recent outgoing messages.</string>
|
||||
<string name="InsightsDashboardFragment__invite_your_contacts">Start communicating securely and enable new features that go beyond the limitations of unencrypted SMS messages by inviting more contacts to join Signal.</string>
|
||||
<string name="InsightsDashboardFragment__this_stat_was_generated_locally">This stat was locally generated on your device and can only be seen by you. It is never transmitted anywhere.</string>
|
||||
<string name="InsightsDashboardFragment__encrypted_messages">Encrypted messages</string>
|
||||
<string name="InsightsDashboardFragment__cancel">Cancel</string>
|
||||
<string name="InsightsDashboardFragment__send">Send</string>
|
||||
<string name="InsightsDashboardFragment__not_using_signal_yet">Not using Signal yet</string>
|
||||
<string name="InsightsModalFragment__title">Introducing Insights</string>
|
||||
<string name="InsightsModalFragment__description">Find out how many of your outgoing messages were sent securely, then quickly invite new contacts to boost your Signal percentage.</string>
|
||||
<string name="InsightsModalFragment__view_insights">View Insights</string>
|
||||
|
||||
<string name="FirstInviteReminder__title">Invite to Signal</string>
|
||||
<string name="FirstInviteReminder__description">You could increase the number of encrypted messages you send by %1$d%%</string>
|
||||
<string name="SecondInviteReminder__title">Boost your Signal</string>
|
||||
<string name="SecondInviteReminder__description">Invite %1$s</string>
|
||||
<string name="InsightsReminder__view_insights">View Insights</string>
|
||||
<string name="InsightsReminder__invite">Invite</string>
|
||||
|
||||
<!-- transport_selection_list_item -->
|
||||
<string name="transport_selection_list_item__transport_icon">Transport icon</string>
|
||||
<string name="ConversationListFragment_loading">Loading…</string>
|
||||
|
|
|
@ -232,7 +232,7 @@
|
|||
<item name="android:inputType">textAutoCorrect|textCapSentences|textMultiLine</item>
|
||||
<item name="android:contentDescription">@string/conversation_activity__compose_description</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="AttachmentTypeLabel">
|
||||
<item name="android:textColor">#ff999999</item>
|
||||
<item name="android:textSize">14sp</item>
|
||||
|
@ -280,7 +280,6 @@
|
|||
<item name="android:focusable">false</item>
|
||||
</style>
|
||||
|
||||
|
||||
<style name="PreferenceThemeOverlay.Fix" parent="PreferenceThemeOverlay.v14.Material">
|
||||
<item name="android:divider">@null</item>
|
||||
<item name="android:dividerHeight">0dp</item>
|
||||
|
@ -334,6 +333,18 @@
|
|||
<item name="titleTextStyle">@style/TextSecure.TitleTextStyle.Conversation</item>
|
||||
</style>
|
||||
|
||||
<style name="Widget.Signal.ArcProgressBar" parent="">
|
||||
<item name="android:layout_width">187dp</item>
|
||||
<item name="android:layout_height">187dp</item>
|
||||
<item name="arcBackgroundColor">?insight_progress_background</item>
|
||||
<item name="arcForegroundColor">@color/signal_primary</item>
|
||||
<item name="arcProgress">0.0</item>
|
||||
<item name="arcStartAngle">120</item>
|
||||
<item name="arcSweepAngle">300</item>
|
||||
<item name="arcWidth">8dp</item>
|
||||
<item name="arcRoundedEnds">true</item>
|
||||
</style>
|
||||
|
||||
<declare-styleable name="CameraButtonView">
|
||||
<attr name="imageCaptureSize" format="dimension" />
|
||||
<attr name="recordSize" format="dimension" />
|
||||
|
@ -362,4 +373,14 @@
|
|||
|
||||
<attr format="boolean" name="pinchToZoomEnabled"/>
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="ArcProgressBar">
|
||||
<attr name="arcWidth" format="dimension" />
|
||||
<attr name="arcBackgroundColor" format="color" />
|
||||
<attr name="arcForegroundColor" format="color" />
|
||||
<attr name="arcRoundedEnds" format="boolean" />
|
||||
<attr name="arcStartAngle" format="float" />
|
||||
<attr name="arcSweepAngle" format="float" />
|
||||
<attr name="arcProgress" format="float" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
|
|
|
@ -45,4 +45,46 @@
|
|||
<item name="android:textColor">@color/core_white</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.Headline.Insights" parent="">
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:textSize">28sp</item>
|
||||
<item name="android:textColor">?title_text_color_primary</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.SubHead.Insights" parent="">
|
||||
<item name="android:textStyle">bold</item>
|
||||
<item name="android:textSize">20sp</item>
|
||||
<item name="android:textColor">?title_text_color_primary</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.Headline.Insights.Modal.Percent" parent="TextAppearance.Signal.Headline.Insights">
|
||||
<item name="android:textSize">@dimen/insights_modal_percent_text_size</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.SubHead.Insights.Modal.Percent" parent="TextAppearance.Signal.SubHead.Insights">
|
||||
<item name="android:textSize">@dimen/insights_modal_percent_sign_text_size</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.Title.Insights" parent="">
|
||||
<item name="android:textSize">16sp</item>
|
||||
<item name="android:textColor">?insight_title</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.Body.Insights" parent="">
|
||||
<item name="android:textColor">?title_text_color_secondary</item>
|
||||
<item name="android:textSize">14sp</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.Body2.Insights" parent="">
|
||||
<item name="android:textColor">?insight_body_2</item>
|
||||
<item name="android:textSize">13sp</item>
|
||||
</style>
|
||||
|
||||
<style name="TextAppearance.Signal.Caption.Insights" parent="">
|
||||
<item name="android:textColor">@color/core_white</item>
|
||||
<item name="android:textSize">16sp</item>
|
||||
<item name="android:textAllCaps">true</item>
|
||||
<item name="android:textStyle">bold</item>
|
||||
</style>
|
||||
|
||||
</resources>
|
|
@ -149,6 +149,13 @@
|
|||
<item name="icon_tint">@color/core_grey_75</item>
|
||||
<item name="icon_tint_dark">@color/core_grey_15</item>
|
||||
|
||||
<item name="insight_modal_background">@drawable/insights_modal_background</item>
|
||||
<item name="insight_modal_button_background">@color/core_grey_10</item>
|
||||
<item name="insight_title">@color/core_grey_90</item>
|
||||
<item name="insight_body_2">@color/core_grey_60</item>
|
||||
<item name="insight_dashboard_bottom_bar_background">@color/core_grey_02</item>
|
||||
<item name="insight_progress_background">@color/core_grey_15</item>
|
||||
|
||||
<item name="search_view_style">@style/Signal.SearchView</item>
|
||||
<item name="search_view_style_dark">@style/Signal.SearchView.Dark</item>
|
||||
|
||||
|
@ -365,6 +372,13 @@
|
|||
<item name="icon_tint">@color/core_grey_15</item>
|
||||
<item name="icon_tint_dark">?icon_tint</item>
|
||||
|
||||
<item name="insight_modal_background">@drawable/insights_modal_background_dark</item>
|
||||
<item name="insight_modal_button_background">@color/core_grey_60</item>
|
||||
<item name="insight_title">@color/core_grey_25</item>
|
||||
<item name="insight_body_2">@color/core_grey_25</item>
|
||||
<item name="insight_progress_background">@color/core_grey_60</item>
|
||||
<item name="insight_dashboard_bottom_bar_background">@color/core_grey_80</item>
|
||||
|
||||
<item name="search_view_style">@style/Signal.SearchView</item>
|
||||
<item name="search_view_style_dark">@style/Signal.SearchView.Dark</item>
|
||||
|
||||
|
@ -575,6 +589,9 @@
|
|||
<item name="android:windowBackground">@drawable/permission_rationale_dialog_corners</item>
|
||||
</style>
|
||||
|
||||
<style name="Theme.Signal.Insights.Modal" parent="@style/Theme.AppCompat.Dialog.MinWidth">
|
||||
</style>
|
||||
|
||||
<style name="TextSecure.MediaSendProgressDialog" parent="@android:style/Theme.Dialog">
|
||||
<item name="android:background">@color/core_grey_95</item>
|
||||
</style>
|
||||
|
|
|
@ -25,10 +25,6 @@ import android.graphics.drawable.Drawable;
|
|||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.appcompat.widget.TooltipCompat;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
|
@ -38,6 +34,10 @@ 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;
|
||||
|
@ -49,6 +49,7 @@ 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;
|
||||
|
@ -131,6 +132,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
|||
|
||||
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);
|
||||
|
@ -212,6 +214,7 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
|||
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;
|
||||
}
|
||||
|
||||
|
@ -300,6 +303,10 @@ public class ConversationListActivity extends PassphraseRequiredActionBarActivit
|
|||
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")));
|
||||
|
|
|
@ -77,6 +77,7 @@ 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;
|
||||
|
@ -84,6 +85,7 @@ 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;
|
||||
|
@ -188,6 +190,10 @@ public class ConversationListFragment extends Fragment
|
|||
updateReminders(true);
|
||||
list.getAdapter().notifyDataSetChanged();
|
||||
EventBus.getDefault().register(this);
|
||||
|
||||
if (TextSecurePreferences.isSmsEnabled(requireContext())) {
|
||||
InsightsLauncher.showInsightsModal(requireContext(), requireFragmentManager());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -5,15 +5,12 @@ import android.annotation.SuppressLint;
|
|||
import android.annotation.TargetApi;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build.VERSION;
|
||||
import android.os.Build.VERSION_CODES;
|
||||
import android.os.Bundle;
|
||||
import androidx.annotation.AnimRes;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import android.view.View;
|
||||
|
@ -237,7 +234,7 @@ public class InviteActivity extends PassphraseRequiredActionBarActivity implemen
|
|||
MessageSender.send(context, new OutgoingTextMessage(recipient, message, subscriptionId), -1L, true, null);
|
||||
|
||||
if (recipient.getContactUri() != null) {
|
||||
DatabaseFactory.getRecipientDatabase(context).setSeenInviteReminder(recipient.getId(), true);
|
||||
DatabaseFactory.getRecipientDatabase(context).setHasSentInvite(recipient.getId());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
125
src/org/thoughtcrime/securesms/components/ArcProgressBar.java
Normal file
125
src/org/thoughtcrime/securesms/components/ArcProgressBar.java
Normal file
|
@ -0,0 +1,125 @@
|
|||
package org.thoughtcrime.securesms.components;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Paint;
|
||||
import android.graphics.RectF;
|
||||
import android.os.Bundle;
|
||||
import android.os.Parcelable;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
public class ArcProgressBar extends View {
|
||||
|
||||
private static final int DEFAULT_WIDTH = 10;
|
||||
private static final float DEFAULT_PROGRESS = 0f;
|
||||
private static final int DEFAULT_BACKGROUND_COLOR = 0xFF000000;
|
||||
private static final int DEFAULT_FOREGROUND_COLOR = 0xFFFFFFFF;
|
||||
private static final float DEFAULT_START_ANGLE = 0f;
|
||||
private static final float DEFAULT_SWEEP_ANGLE = 360f;
|
||||
private static final boolean DEFAULT_ROUNDED_ENDS = true;
|
||||
|
||||
private static final String SUPER = "arcprogressbar.super";
|
||||
private static final String PROGRESS = "arcprogressbar.progress";
|
||||
|
||||
private float progress;
|
||||
private final float width;
|
||||
private final RectF arcRect = new RectF();
|
||||
|
||||
private final Paint arcBackgroundPaint;
|
||||
private final Paint arcForegroundPaint;
|
||||
private final float arcStartAngle;
|
||||
private final float arcSweepAngle;
|
||||
|
||||
public ArcProgressBar(@NonNull Context context) {
|
||||
this(context, null);
|
||||
}
|
||||
|
||||
public ArcProgressBar(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
this(context, attrs, 0);
|
||||
}
|
||||
|
||||
public ArcProgressBar(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
|
||||
TypedArray attributes = context.getTheme().obtainStyledAttributes(attrs, R.styleable.ArcProgressBar, defStyleAttr, 0);
|
||||
|
||||
width = attributes.getDimensionPixelSize(R.styleable.ArcProgressBar_arcWidth, DEFAULT_WIDTH);
|
||||
progress = attributes.getFloat(R.styleable.ArcProgressBar_arcProgress, DEFAULT_PROGRESS);
|
||||
arcBackgroundPaint = createPaint(width, attributes.getColor(R.styleable.ArcProgressBar_arcBackgroundColor, DEFAULT_BACKGROUND_COLOR));
|
||||
arcForegroundPaint = createPaint(width, attributes.getColor(R.styleable.ArcProgressBar_arcForegroundColor, DEFAULT_FOREGROUND_COLOR));
|
||||
arcStartAngle = attributes.getFloat(R.styleable.ArcProgressBar_arcStartAngle, DEFAULT_START_ANGLE);
|
||||
arcSweepAngle = attributes.getFloat(R.styleable.ArcProgressBar_arcSweepAngle, DEFAULT_SWEEP_ANGLE);
|
||||
|
||||
if (attributes.getBoolean(R.styleable.ArcProgressBar_arcRoundedEnds, DEFAULT_ROUNDED_ENDS)) {
|
||||
arcForegroundPaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
|
||||
if (arcSweepAngle <= 360f) {
|
||||
arcBackgroundPaint.setStrokeCap(Paint.Cap.ROUND);
|
||||
}
|
||||
}
|
||||
|
||||
attributes.recycle();
|
||||
}
|
||||
|
||||
private static Paint createPaint(float width, @ColorInt int color) {
|
||||
Paint paint = new Paint();
|
||||
|
||||
paint.setStrokeWidth(width);
|
||||
paint.setStyle(Paint.Style.STROKE);
|
||||
paint.setAntiAlias(true);
|
||||
paint.setColor(color);
|
||||
|
||||
return paint;
|
||||
}
|
||||
|
||||
public void setProgress(float progress) {
|
||||
if (this.progress != progress) {
|
||||
this.progress = progress;
|
||||
invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable Parcelable onSaveInstanceState() {
|
||||
Parcelable superState = super.onSaveInstanceState();
|
||||
|
||||
Bundle bundle = new Bundle();
|
||||
bundle.putParcelable(SUPER, superState);
|
||||
bundle.putFloat(PROGRESS, progress);
|
||||
|
||||
return bundle;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(Parcelable state) {
|
||||
if (state.getClass() != Bundle.class) throw new IllegalStateException("Expected");
|
||||
|
||||
Bundle restoreState = (Bundle) state;
|
||||
|
||||
Parcelable superState = restoreState.getParcelable(SUPER);
|
||||
super.onRestoreInstanceState(superState);
|
||||
|
||||
progress = restoreState.getLong(PROGRESS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDraw(Canvas canvas) {
|
||||
float halfWidth = width / 2f;
|
||||
arcRect.set(0 + halfWidth,
|
||||
0 + halfWidth,
|
||||
getWidth() - halfWidth,
|
||||
getHeight() - halfWidth);
|
||||
|
||||
canvas.drawArc(arcRect, arcStartAngle, arcSweepAngle, false, arcBackgroundPaint);
|
||||
canvas.drawArc(arcRect, arcStartAngle, arcSweepAngle * Util.clamp(progress, 0f, 1f), false, arcForegroundPaint);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
public final class FirstInviteReminder extends Reminder {
|
||||
|
||||
public FirstInviteReminder(final @NonNull Context context,
|
||||
final @NonNull Recipient recipient,
|
||||
final int percentIncrease) {
|
||||
super(context.getString(R.string.FirstInviteReminder__title),
|
||||
context.getString(R.string.FirstInviteReminder__description, percentIncrease));
|
||||
|
||||
addAction(new Action(context.getString(R.string.InsightsReminder__invite), R.id.reminder_action_invite));
|
||||
addAction(new Action(context.getString(R.string.InsightsReminder__view_insights), R.id.reminder_action_view_insights));
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.AsyncTask;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
public class InviteReminder extends Reminder {
|
||||
|
||||
@SuppressLint("StaticFieldLeak")
|
||||
public InviteReminder(final @NonNull Context context,
|
||||
final @NonNull Recipient recipient)
|
||||
{
|
||||
super(context.getString(R.string.reminder_header_invite_title),
|
||||
context.getString(R.string.reminder_header_invite_text, recipient.toShortString(context)));
|
||||
|
||||
setDismissListener(v -> SignalExecutors.BOUNDED.execute(() -> {
|
||||
DatabaseFactory.getRecipientDatabase(context).setSeenInviteReminder(recipient.getId(), true);
|
||||
}));
|
||||
}
|
||||
}
|
|
@ -1,9 +1,15 @@
|
|||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import android.view.View;
|
||||
import android.view.View.OnClickListener;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public abstract class Reminder {
|
||||
private CharSequence title;
|
||||
private CharSequence text;
|
||||
|
@ -11,6 +17,8 @@ public abstract class Reminder {
|
|||
private OnClickListener okListener;
|
||||
private OnClickListener dismissListener;
|
||||
|
||||
private final List<Action> actions = new LinkedList<>();
|
||||
|
||||
public Reminder(@Nullable CharSequence title,
|
||||
@NonNull CharSequence text)
|
||||
{
|
||||
|
@ -50,8 +58,37 @@ public abstract class Reminder {
|
|||
return Importance.NORMAL;
|
||||
}
|
||||
|
||||
public void addAction(@NonNull Action action) {
|
||||
actions.add(action);
|
||||
}
|
||||
|
||||
public List<Action> getActions() {
|
||||
return actions;
|
||||
}
|
||||
|
||||
public int getProgress() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
public enum Importance {
|
||||
NORMAL, ERROR
|
||||
}
|
||||
|
||||
public final class Action {
|
||||
private final CharSequence title;
|
||||
private final int actionId;
|
||||
|
||||
public Action(CharSequence title, @IdRes int actionId) {
|
||||
this.title = title;
|
||||
this.actionId = actionId;
|
||||
}
|
||||
|
||||
CharSequence getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
int getActionId() {
|
||||
return actionId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
final class ReminderActionsAdapter extends RecyclerView.Adapter<ReminderActionsAdapter.ActionViewHolder> {
|
||||
|
||||
private final List<Reminder.Action> actions;
|
||||
private final ReminderView.OnActionClickListener actionClickListener;
|
||||
|
||||
ReminderActionsAdapter(List<Reminder.Action> actions, ReminderView.OnActionClickListener actionClickListener) {
|
||||
this.actions = Collections.unmodifiableList(actions);
|
||||
this.actionClickListener = actionClickListener;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ActionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ActionViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.reminder_action_button, parent, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ActionViewHolder holder, int position) {
|
||||
final Reminder.Action action = actions.get(position);
|
||||
|
||||
((Button) holder.itemView).setText(action.getTitle());
|
||||
holder.itemView.setOnClickListener(v -> {
|
||||
if (holder.getAdapterPosition() == RecyclerView.NO_POSITION) return;
|
||||
|
||||
actionClickListener.onActionClick(action.getActionId());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return actions.size();
|
||||
}
|
||||
|
||||
final class ActionViewHolder extends RecyclerView.ViewHolder {
|
||||
ActionViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,22 +8,35 @@ import android.util.AttributeSet;
|
|||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Space;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* View to display actionable reminders to the user
|
||||
*/
|
||||
public class ReminderView extends LinearLayout {
|
||||
private ViewGroup container;
|
||||
private ImageButton closeButton;
|
||||
private TextView title;
|
||||
private TextView text;
|
||||
private OnDismissListener dismissListener;
|
||||
public final class ReminderView extends FrameLayout {
|
||||
private ProgressBar progressBar;
|
||||
private TextView progressText;
|
||||
private ViewGroup container;
|
||||
private ImageButton closeButton;
|
||||
private TextView title;
|
||||
private TextView text;
|
||||
private OnDismissListener dismissListener;
|
||||
private Space space;
|
||||
private RecyclerView actionsRecycler;
|
||||
private OnActionClickListener actionClickListener;
|
||||
|
||||
public ReminderView(Context context) {
|
||||
super(context);
|
||||
|
@ -43,19 +56,25 @@ public class ReminderView extends LinearLayout {
|
|||
|
||||
private void initialize() {
|
||||
LayoutInflater.from(getContext()).inflate(R.layout.reminder_header, this, true);
|
||||
container = ViewUtil.findById(this, R.id.container);
|
||||
closeButton = ViewUtil.findById(this, R.id.cancel);
|
||||
title = ViewUtil.findById(this, R.id.reminder_title);
|
||||
text = ViewUtil.findById(this, R.id.reminder_text);
|
||||
progressBar = ViewUtil.findById(this, R.id.reminder_progress);
|
||||
progressText = ViewUtil.findById(this, R.id.reminder_progress_text);
|
||||
container = ViewUtil.findById(this, R.id.container);
|
||||
closeButton = ViewUtil.findById(this, R.id.cancel);
|
||||
title = ViewUtil.findById(this, R.id.reminder_title);
|
||||
text = ViewUtil.findById(this, R.id.reminder_text);
|
||||
space = ViewUtil.findById(this, R.id.reminder_space);
|
||||
actionsRecycler = ViewUtil.findById(this, R.id.reminder_actions);
|
||||
}
|
||||
|
||||
public void showReminder(final Reminder reminder) {
|
||||
if (!TextUtils.isEmpty(reminder.getTitle())) {
|
||||
title.setText(reminder.getTitle());
|
||||
title.setVisibility(VISIBLE);
|
||||
space.setVisibility(GONE);
|
||||
} else {
|
||||
title.setText("");
|
||||
title.setVisibility(GONE);
|
||||
space.setVisibility(VISIBLE);
|
||||
}
|
||||
text.setText(reminder.getText());
|
||||
container.setBackgroundResource(reminder.getImportance() == Reminder.Importance.ERROR ? R.drawable.reminder_background_error
|
||||
|
@ -73,13 +92,40 @@ public class ReminderView extends LinearLayout {
|
|||
}
|
||||
});
|
||||
|
||||
int progress = reminder.getProgress();
|
||||
if (progress != -1) {
|
||||
progressBar.setProgress(progress);
|
||||
progressBar.setVisibility(VISIBLE);
|
||||
progressText.setText(getContext().getString(R.string.reminder_header_progress, progress));
|
||||
progressText.setVisibility(VISIBLE);
|
||||
} else {
|
||||
progressBar.setVisibility(GONE);
|
||||
progressText.setVisibility(GONE);
|
||||
}
|
||||
|
||||
List<Reminder.Action> actions = reminder.getActions();
|
||||
if (actions.isEmpty()) {
|
||||
actionsRecycler.setVisibility(GONE);
|
||||
} else {
|
||||
actionsRecycler.setVisibility(VISIBLE);
|
||||
actionsRecycler.setAdapter(new ReminderActionsAdapter(actions, this::handleActionClicked));
|
||||
}
|
||||
|
||||
container.setVisibility(View.VISIBLE);
|
||||
}
|
||||
|
||||
private void handleActionClicked(@IdRes int actionId) {
|
||||
if (actionClickListener != null) actionClickListener.onActionClick(actionId);
|
||||
}
|
||||
|
||||
public void setOnDismissListener(OnDismissListener dismissListener) {
|
||||
this.dismissListener = dismissListener;
|
||||
}
|
||||
|
||||
public void setOnActionClickListener(@Nullable OnActionClickListener actionClickListener) {
|
||||
this.actionClickListener = actionClickListener;
|
||||
}
|
||||
|
||||
public void requestDismiss() {
|
||||
closeButton.performClick();
|
||||
}
|
||||
|
@ -91,4 +137,8 @@ public class ReminderView extends LinearLayout {
|
|||
public interface OnDismissListener {
|
||||
void onDismiss();
|
||||
}
|
||||
|
||||
public interface OnActionClickListener {
|
||||
void onActionClick(@IdRes int actionId);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
package org.thoughtcrime.securesms.components.reminder;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
public final class SecondInviteReminder extends Reminder {
|
||||
|
||||
private final int progress;
|
||||
|
||||
public SecondInviteReminder(final @NonNull Context context,
|
||||
final @NonNull Recipient recipient,
|
||||
final int percent)
|
||||
{
|
||||
super(context.getString(R.string.SecondInviteReminder__title),
|
||||
context.getString(R.string.SecondInviteReminder__description, recipient.getDisplayName(context)));
|
||||
|
||||
this.progress = percent;
|
||||
|
||||
addAction(new Action(context.getString(R.string.InsightsReminder__invite), R.id.reminder_action_invite));
|
||||
addAction(new Action(context.getString(R.string.InsightsReminder__view_insights), R.id.reminder_action_view_insights));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getProgress() {
|
||||
return progress;
|
||||
}
|
||||
}
|
|
@ -32,13 +32,11 @@ import android.graphics.BitmapFactory;
|
|||
import android.graphics.Color;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.hardware.Camera;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.os.Vibrator;
|
||||
import android.provider.Browser;
|
||||
import android.provider.ContactsContract;
|
||||
|
@ -62,16 +60,15 @@ import android.widget.ImageButton;
|
|||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.annotation.IdRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.content.pm.ShortcutInfoCompat;
|
||||
import androidx.core.content.pm.ShortcutManagerCompat;
|
||||
import androidx.core.graphics.drawable.DrawableCompat;
|
||||
import androidx.core.graphics.drawable.IconCompat;
|
||||
import androidx.core.view.MenuItemCompat;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
@ -100,7 +97,6 @@ import org.thoughtcrime.securesms.attachments.Attachment;
|
|||
import org.thoughtcrime.securesms.attachments.TombstoneAttachment;
|
||||
import org.thoughtcrime.securesms.audio.AudioRecorder;
|
||||
import org.thoughtcrime.securesms.audio.AudioSlidePlayer;
|
||||
import org.thoughtcrime.securesms.blurhash.BlurHash;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.components.AnimatingToggle;
|
||||
import org.thoughtcrime.securesms.components.AttachmentTypeSelector;
|
||||
|
@ -120,12 +116,13 @@ import org.thoughtcrime.securesms.components.identity.UnverifiedBannerView;
|
|||
import org.thoughtcrime.securesms.components.identity.UnverifiedSendDialog;
|
||||
import org.thoughtcrime.securesms.components.location.SignalPlace;
|
||||
import org.thoughtcrime.securesms.components.reminder.ExpiredBuildReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.InviteReminder;
|
||||
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.UnauthorizedReminder;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor;
|
||||
import org.thoughtcrime.securesms.contacts.ContactAccessor.ContactData;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactShareEditActivity;
|
||||
import org.thoughtcrime.securesms.contactshare.ContactUtil;
|
||||
|
@ -152,6 +149,9 @@ import org.thoughtcrime.securesms.database.model.StickerRecord;
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.ReminderUpdateEvent;
|
||||
import org.thoughtcrime.securesms.giph.ui.GiphyActivity;
|
||||
import org.thoughtcrime.securesms.insights.InsightsLauncher;
|
||||
import org.thoughtcrime.securesms.invites.InviteReminderModel;
|
||||
import org.thoughtcrime.securesms.invites.InviteReminderRepository;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceBlockedUpdateJob;
|
||||
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
|
||||
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
|
||||
|
@ -210,7 +210,6 @@ import org.thoughtcrime.securesms.util.CharacterCalculator.CharacterState;
|
|||
import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.Dialogs;
|
||||
import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
|
||||
import org.thoughtcrime.securesms.contacts.sync.DirectoryHelper;
|
||||
import org.thoughtcrime.securesms.util.DynamicLanguage;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
|
@ -220,7 +219,6 @@ import org.thoughtcrime.securesms.util.MediaUtil;
|
|||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences.MediaKeyboardMode;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
|
@ -322,6 +320,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
private LinkPreviewViewModel linkPreviewViewModel;
|
||||
private ConversationSearchViewModel searchViewModel;
|
||||
private ConversationStickerViewModel stickerViewModel;
|
||||
private InviteReminderModel inviteReminderModel;
|
||||
|
||||
private LiveRecipient recipient;
|
||||
private long threadId;
|
||||
|
@ -399,6 +398,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
});
|
||||
}
|
||||
});
|
||||
initializeInsightObserver();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -816,7 +816,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(ReminderUpdateEvent event) {
|
||||
updateReminders(recipient.get().hasSeenInviteReminder());
|
||||
updateReminders();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -1423,12 +1423,17 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
|
||||
private void onSecurityUpdated() {
|
||||
Log.i(TAG, "onSecurityUpdated()");
|
||||
updateReminders(recipient.get().hasSeenInviteReminder());
|
||||
updateReminders();
|
||||
updateDefaultSubscriptionId(recipient.get().getDefaultSubscriptionId());
|
||||
}
|
||||
|
||||
protected void updateReminders(boolean seenInvite) {
|
||||
Log.i(TAG, "updateReminders(" + seenInvite + ")");
|
||||
private void initializeInsightObserver() {
|
||||
inviteReminderModel = new InviteReminderModel(this, new InviteReminderRepository(this));
|
||||
inviteReminderModel.loadReminder(recipient, this::updateReminders);
|
||||
}
|
||||
|
||||
protected void updateReminders() {
|
||||
Optional<Reminder> inviteReminder = inviteReminderModel.getReminder();
|
||||
|
||||
if (UnauthorizedReminder.isEligible(this)) {
|
||||
reminderView.get().showReminder(new UnauthorizedReminder(this));
|
||||
|
@ -1439,21 +1444,31 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
reminderView.get().showReminder(new ServiceOutageReminder(this));
|
||||
} else if (TextSecurePreferences.isPushRegistered(this) &&
|
||||
TextSecurePreferences.isShowInviteReminders(this) &&
|
||||
!isSecureText &&
|
||||
!seenInvite &&
|
||||
!recipient.get().isGroup())
|
||||
{
|
||||
InviteReminder reminder = new InviteReminder(this, recipient.get());
|
||||
reminder.setOkListener(v -> {
|
||||
handleInviteLink();
|
||||
reminderView.get().requestDismiss();
|
||||
});
|
||||
reminderView.get().showReminder(reminder);
|
||||
!isSecureText &&
|
||||
inviteReminder.isPresent() &&
|
||||
!recipient.get().isGroup()) {
|
||||
reminderView.get().setOnActionClickListener(this::handleReminderAction);
|
||||
reminderView.get().setOnDismissListener(() -> inviteReminderModel.dismissReminder());
|
||||
reminderView.get().showReminder(inviteReminder.get());
|
||||
} else if (reminderView.resolved()) {
|
||||
reminderView.get().hide();
|
||||
}
|
||||
}
|
||||
|
||||
private void handleReminderAction(@IdRes int reminderActionId) {
|
||||
switch (reminderActionId) {
|
||||
case R.id.reminder_action_invite:
|
||||
handleInviteLink();
|
||||
reminderView.get().requestDismiss();
|
||||
break;
|
||||
case R.id.reminder_action_view_insights:
|
||||
InsightsLauncher.showInsightsDashboard(getSupportFragmentManager());
|
||||
break;
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown ID: " + reminderActionId);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateDefaultSubscriptionId(Optional<Integer> defaultSubscriptionId) {
|
||||
Log.i(TAG, "updateDefaultSubscriptionId(" + defaultSubscriptionId.orNull() + ")");
|
||||
sendButton.setDefaultSubscriptionId(defaultSubscriptionId);
|
||||
|
@ -1742,7 +1757,7 @@ public class ConversationActivity extends PassphraseRequiredActionBarActivity
|
|||
setBlockedUserState(recipient, isSecureText, isDefaultSms);
|
||||
setActionBarColor(recipient.getColor());
|
||||
setGroupShareProfileReminder(recipient);
|
||||
updateReminders(recipient.hasSeenInviteReminder());
|
||||
updateReminders();
|
||||
updateDefaultSubscriptionId(recipient.getDefaultSubscriptionId());
|
||||
initializeSecurity(isSecureText, isDefaultSms);
|
||||
|
||||
|
|
|
@ -116,7 +116,7 @@ public class ConversationPopupActivity extends ConversationActivity {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected void updateReminders(boolean seenInvite) {
|
||||
protected void updateReminders() {
|
||||
if (reminderView.resolved()) {
|
||||
reminderView.get().setVisibility(View.GONE);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,8 @@ import android.text.TextUtils;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
|
||||
import org.thoughtcrime.securesms.database.documents.Document;
|
||||
|
@ -16,12 +18,14 @@ import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
|||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public abstract class MessagingDatabase extends Database implements MmsSmsColumns {
|
||||
|
||||
|
@ -32,6 +36,8 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
|||
}
|
||||
|
||||
protected abstract String getTableName();
|
||||
protected abstract String getTypeField();
|
||||
protected abstract String getDateSentColumnName();
|
||||
|
||||
public abstract void markExpireStarted(long messageId);
|
||||
public abstract void markExpireStarted(long messageId, long startTime);
|
||||
|
@ -39,6 +45,61 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
|||
public abstract void markAsSent(long messageId, boolean secure);
|
||||
public abstract void markUnidentified(long messageId, boolean unidentified);
|
||||
|
||||
final int getInsecureMessagesSentForThread(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] projection = new String[]{"COUNT(*)"};
|
||||
String query = THREAD_ID + " = ? AND " + getOutgoingInsecureMessageClause() + " AND " + getDateSentColumnName() + " > ?";
|
||||
String[] args = new String[]{String.valueOf(threadId), String.valueOf(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7))};
|
||||
|
||||
try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final int getInsecureMessageCountForRecipients(List<RecipientId> recipients) {
|
||||
return getMessageCountForRecipientsAndType(recipients, getOutgoingInsecureMessageClause());
|
||||
}
|
||||
|
||||
final int getSecureMessageCountForRecipients(List<RecipientId> recipients) {
|
||||
return getMessageCountForRecipientsAndType(recipients, getOutgoingSecureMessageClause());
|
||||
}
|
||||
|
||||
private int getMessageCountForRecipientsAndType(List<RecipientId> recipients, String typeClause) {
|
||||
if (recipients.size() == 0) return 0;
|
||||
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String placeholders = Util.join(Stream.of(recipients).map(r -> "?").toList(), ",");
|
||||
String[] projection = new String[] {"COUNT(*)"};
|
||||
String query = RECIPIENT_ID + " IN ( " + placeholders + " ) AND " + typeClause + " AND " + getDateSentColumnName() + " > ?";
|
||||
String[] args = new String[recipients.size() + 1];
|
||||
|
||||
for (int i = 0; i < recipients.size(); i++) {
|
||||
args[i] = recipients.get(i).serialize();
|
||||
}
|
||||
|
||||
args[args.length - 1] = String.valueOf(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7));
|
||||
|
||||
try (Cursor cursor = db.query(getTableName(), projection, query, args, null, null, null, null)) {
|
||||
if (cursor != null && cursor.moveToFirst()) {
|
||||
return cursor.getInt(0);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String getOutgoingInsecureMessageClause() {
|
||||
return "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND NOT (" + getTypeField() + " & " + Types.SECURE_MESSAGE_BIT + ")";
|
||||
}
|
||||
|
||||
private String getOutgoingSecureMessageClause() {
|
||||
return "(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND (" + getTypeField() + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")";
|
||||
}
|
||||
|
||||
public void addMismatchedIdentity(long messageId, @NonNull RecipientId recipientId, IdentityKey identityKey) {
|
||||
try {
|
||||
addToDocument(messageId, MISMATCHED_IDENTITIES,
|
||||
|
|
|
@ -35,7 +35,6 @@ import net.sqlcipher.database.SQLiteDatabase;
|
|||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.json.JSONObject;
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.AttachmentId;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
|
@ -81,6 +80,7 @@ import java.util.LinkedList;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static org.thoughtcrime.securesms.contactshare.Contact.Avatar;
|
||||
|
||||
|
@ -182,6 +182,9 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
|
||||
private static final String RAW_ID_WHERE = TABLE_NAME + "._id = ?";
|
||||
|
||||
private static final String OUTGOING_INSECURE_MESSAGES_CLAUSE = "(" + MESSAGE_BOX + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND NOT (" + MESSAGE_BOX + " & " + Types.SECURE_MESSAGE_BIT + ")";
|
||||
private static final String OUTGOING_SECURE_MESSAGES_CLAUSE = "(" + MESSAGE_BOX + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND (" + MESSAGE_BOX + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")";
|
||||
|
||||
private final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache("MmsDelivery");
|
||||
private final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache("MmsRead");
|
||||
|
||||
|
@ -194,6 +197,16 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
return TABLE_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDateSentColumnName() {
|
||||
return DATE_SENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTypeField() {
|
||||
return MESSAGE_BOX;
|
||||
}
|
||||
|
||||
public int getMessageCountForThread(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = null;
|
||||
|
|
|
@ -31,6 +31,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public class MmsSmsDatabase extends Database {
|
||||
|
@ -155,6 +156,27 @@ public class MmsSmsDatabase extends Database {
|
|||
return count;
|
||||
}
|
||||
|
||||
public int getInsecureSentCount(long threadId) {
|
||||
int count = DatabaseFactory.getSmsDatabase(context).getInsecureMessagesSentForThread(threadId);
|
||||
count += DatabaseFactory.getMmsDatabase(context).getInsecureMessagesSentForThread(threadId);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public int getInsecureMessageCountForRecipients(List<RecipientId> recipients) {
|
||||
int count = DatabaseFactory.getSmsDatabase(context).getInsecureMessageCountForRecipients(recipients);
|
||||
count += DatabaseFactory.getMmsDatabase(context).getInsecureMessageCountForRecipients(recipients);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public int getSecureMessageCountForRecipients(List<RecipientId> recipients) {
|
||||
int count = DatabaseFactory.getSmsDatabase(context).getSecureMessageCountForRecipients(recipients);
|
||||
count += DatabaseFactory.getMmsDatabase(context).getSecureMessageCountForRecipients(recipients);
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
public void incrementDeliveryReceiptCount(SyncMessageId syncMessageId, long timestamp) {
|
||||
DatabaseFactory.getSmsDatabase(context).incrementReceiptCount(syncMessageId, true, false);
|
||||
DatabaseFactory.getMmsDatabase(context).incrementReceiptCount(syncMessageId, timestamp, true, false);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package org.thoughtcrime.securesms.database;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
@ -140,6 +141,28 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public enum InsightsBannerTier {
|
||||
NO_TIER(0), TIER_ONE(1), TIER_TWO(2);
|
||||
|
||||
private final int id;
|
||||
|
||||
InsightsBannerTier(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public boolean seen(InsightsBannerTier tier) {
|
||||
return tier.getId() <= id;
|
||||
}
|
||||
|
||||
public static InsightsBannerTier fromId(int id) {
|
||||
return values()[id];
|
||||
}
|
||||
}
|
||||
|
||||
public static final String CREATE_TABLE =
|
||||
"CREATE TABLE " + TABLE_NAME + " (" + ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
|
||||
UUID + " TEXT UNIQUE DEFAULT NULL, " +
|
||||
|
@ -154,7 +177,7 @@ public class RecipientDatabase extends Database {
|
|||
NOTIFICATION_CHANNEL + " TEXT DEFAULT NULL, " +
|
||||
MUTE_UNTIL + " INTEGER DEFAULT 0, " +
|
||||
COLOR + " TEXT DEFAULT NULL, " +
|
||||
SEEN_INVITE_REMINDER + " INTEGER DEFAULT 0, " +
|
||||
SEEN_INVITE_REMINDER + " INTEGER DEFAULT " + InsightsBannerTier.NO_TIER.getId() + ", " +
|
||||
DEFAULT_SUBSCRIPTION_ID + " INTEGER DEFAULT -1, " +
|
||||
MESSAGE_EXPIRATION_TIME + " INTEGER DEFAULT 0, " +
|
||||
REGISTERED + " INTEGER DEFAULT " + RegisteredState.UNKNOWN.getId() + ", " +
|
||||
|
@ -171,6 +194,17 @@ public class RecipientDatabase extends Database {
|
|||
FORCE_SMS_SELECTION + " INTEGER DEFAULT 0, " +
|
||||
UUID_SUPPORTED + " INTEGER DEFAULT 0);";
|
||||
|
||||
private static final String INSIGHTS_INVITEE_LIST = "SELECT " + TABLE_NAME + "." + ID +
|
||||
" FROM " + TABLE_NAME +
|
||||
" INNER JOIN " + ThreadDatabase.TABLE_NAME +
|
||||
" ON " + TABLE_NAME + "." + ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.RECIPIENT_ID +
|
||||
" WHERE " +
|
||||
TABLE_NAME + "." + GROUP_ID + " IS NULL AND " +
|
||||
TABLE_NAME + "." + REGISTERED + " = " + RegisteredState.NOT_REGISTERED.id + " AND " +
|
||||
TABLE_NAME + "." + SEEN_INVITE_REMINDER + " < " + InsightsBannerTier.TIER_TWO.id + " AND " +
|
||||
ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.HAS_SENT +
|
||||
" ORDER BY " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.DATE + " DESC";
|
||||
|
||||
public RecipientDatabase(Context context, SQLCipherOpenHelper databaseHelper) {
|
||||
super(context, databaseHelper);
|
||||
}
|
||||
|
@ -264,7 +298,7 @@ public class RecipientDatabase extends Database {
|
|||
int callVibrateState = cursor.getInt(cursor.getColumnIndexOrThrow(CALL_VIBRATE));
|
||||
long muteUntil = cursor.getLong(cursor.getColumnIndexOrThrow(MUTE_UNTIL));
|
||||
String serializedColor = cursor.getString(cursor.getColumnIndexOrThrow(COLOR));
|
||||
boolean seenInviteReminder = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER)) == 1;
|
||||
int insightsBannerTier = cursor.getInt(cursor.getColumnIndexOrThrow(SEEN_INVITE_REMINDER));
|
||||
int defaultSubscriptionId = cursor.getInt(cursor.getColumnIndexOrThrow(DEFAULT_SUBSCRIPTION_ID));
|
||||
int expireMessages = cursor.getInt(cursor.getColumnIndexOrThrow(MESSAGE_EXPIRATION_TIME));
|
||||
int registeredState = cursor.getInt(cursor.getColumnIndexOrThrow(REGISTERED));
|
||||
|
@ -304,14 +338,13 @@ public class RecipientDatabase extends Database {
|
|||
VibrateState.fromId(messageVibrateState),
|
||||
VibrateState.fromId(callVibrateState),
|
||||
Util.uri(messageRingtone), Util.uri(callRingtone),
|
||||
color, seenInviteReminder,
|
||||
defaultSubscriptionId, expireMessages,
|
||||
color, defaultSubscriptionId, expireMessages,
|
||||
RegisteredState.fromId(registeredState),
|
||||
profileKey, systemDisplayName, systemContactPhoto,
|
||||
systemPhoneLabel, systemContactUri,
|
||||
signalProfileName, signalProfileAvatar, profileSharing,
|
||||
notificationChannel, UnidentifiedAccessMode.fromMode(unidentifiedAccessMode),
|
||||
forceSmsSelection, uuidSupported);
|
||||
forceSmsSelection, uuidSupported, InsightsBannerTier.fromId(insightsBannerTier));
|
||||
}
|
||||
|
||||
public BulkOperationsHandle resetAllSystemContactInfo() {
|
||||
|
@ -392,10 +425,26 @@ public class RecipientDatabase extends Database {
|
|||
Recipient.live(id).refresh();
|
||||
}
|
||||
|
||||
public void setSeenInviteReminder(@NonNull RecipientId id, @SuppressWarnings("SameParameterValue") boolean seen) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(SEEN_INVITE_REMINDER, seen ? 1 : 0);
|
||||
update(id, values);
|
||||
public void setSeenFirstInviteReminder(@NonNull RecipientId id) {
|
||||
setInsightsBannerTier(id, InsightsBannerTier.TIER_ONE);
|
||||
}
|
||||
|
||||
public void setSeenSecondInviteReminder(@NonNull RecipientId id) {
|
||||
setInsightsBannerTier(id, InsightsBannerTier.TIER_TWO);
|
||||
}
|
||||
|
||||
public void setHasSentInvite(@NonNull RecipientId id) {
|
||||
setSeenSecondInviteReminder(id);
|
||||
}
|
||||
|
||||
private void setInsightsBannerTier(@NonNull RecipientId id, @NonNull InsightsBannerTier insightsBannerTier) {
|
||||
SQLiteDatabase database = databaseHelper.getWritableDatabase();
|
||||
ContentValues values = new ContentValues(1);
|
||||
String query = ID + " = ? AND " + SEEN_INVITE_REMINDER + " < ?";
|
||||
String[] args = new String[]{ id.serialize(), String.valueOf(insightsBannerTier) };
|
||||
|
||||
values.put(SEEN_INVITE_REMINDER, insightsBannerTier.id);
|
||||
database.update(TABLE_NAME, values, query, args);
|
||||
Recipient.live(id).refresh();
|
||||
}
|
||||
|
||||
|
@ -563,7 +612,45 @@ public class RecipientDatabase extends Database {
|
|||
}
|
||||
}
|
||||
|
||||
public List<RecipientId> getRegistered() {
|
||||
public @NonNull List<RecipientId> getUninvitedRecipientsForInsights() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
List<RecipientId> results = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = db.rawQuery(INSIGHTS_INVITEE_LIST, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID))));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientId> getNotRegisteredForInsights() {
|
||||
return getRecipientsForInsights(REGISTERED + " = ?", new String[]{String.valueOf(RegisteredState.NOT_REGISTERED.id)});
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientId> getRegisteredForInsights() {
|
||||
final String selfId = Recipient.self().getId().serialize();
|
||||
final String query = REGISTERED + " = ? AND " + ID + " != ?";
|
||||
final String[] args = new String[]{String.valueOf(RegisteredState.REGISTERED.id), selfId};
|
||||
|
||||
return getRecipientsForInsights(query, args);
|
||||
}
|
||||
|
||||
private @NonNull List<RecipientId> getRecipientsForInsights(@NonNull String query, @NonNull String[] args) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
List<RecipientId> results = new LinkedList<>();
|
||||
|
||||
try (Cursor cursor = db.query(TABLE_NAME, ID_PROJECTION, query + " AND " + GROUP_ID + " IS NULL", args, null, null, null)) {
|
||||
while (cursor != null && cursor.moveToNext()) {
|
||||
results.add(RecipientId.from(cursor.getLong(cursor.getColumnIndexOrThrow(ID))));
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
public @NonNull List<RecipientId> getRegistered() {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
List<RecipientId> results = new LinkedList<>();
|
||||
|
||||
|
@ -776,7 +863,6 @@ public class RecipientDatabase extends Database {
|
|||
private final Uri messageRingtone;
|
||||
private final Uri callRingtone;
|
||||
private final MaterialColor color;
|
||||
private final boolean seenInviteReminder;
|
||||
private final int defaultSubscriptionId;
|
||||
private final int expireMessages;
|
||||
private final RegisteredState registered;
|
||||
|
@ -792,6 +878,7 @@ public class RecipientDatabase extends Database {
|
|||
private final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
private final boolean forceSmsSelection;
|
||||
private final boolean uuidSupported;
|
||||
private final InsightsBannerTier insightsBannerTier;
|
||||
|
||||
RecipientSettings(@NonNull RecipientId id,
|
||||
@Nullable UUID uuid,
|
||||
|
@ -804,7 +891,6 @@ public class RecipientDatabase extends Database {
|
|||
@Nullable Uri messageRingtone,
|
||||
@Nullable Uri callRingtone,
|
||||
@Nullable MaterialColor color,
|
||||
boolean seenInviteReminder,
|
||||
int defaultSubscriptionId,
|
||||
int expireMessages,
|
||||
@NonNull RegisteredState registered,
|
||||
|
@ -819,7 +905,8 @@ public class RecipientDatabase extends Database {
|
|||
@Nullable String notificationChannel,
|
||||
@NonNull UnidentifiedAccessMode unidentifiedAccessMode,
|
||||
boolean forceSmsSelection,
|
||||
boolean uuidSupported)
|
||||
boolean uuidSupported,
|
||||
@NonNull InsightsBannerTier insightsBannerTier)
|
||||
{
|
||||
this.id = id;
|
||||
this.uuid = uuid;
|
||||
|
@ -833,7 +920,6 @@ public class RecipientDatabase extends Database {
|
|||
this.messageRingtone = messageRingtone;
|
||||
this.callRingtone = callRingtone;
|
||||
this.color = color;
|
||||
this.seenInviteReminder = seenInviteReminder;
|
||||
this.defaultSubscriptionId = defaultSubscriptionId;
|
||||
this.expireMessages = expireMessages;
|
||||
this.registered = registered;
|
||||
|
@ -849,6 +935,7 @@ public class RecipientDatabase extends Database {
|
|||
this.unidentifiedAccessMode = unidentifiedAccessMode;
|
||||
this.forceSmsSelection = forceSmsSelection;
|
||||
this.uuidSupported = uuidSupported;
|
||||
this.insightsBannerTier = insightsBannerTier;
|
||||
}
|
||||
|
||||
public RecipientId getId() {
|
||||
|
@ -899,8 +986,8 @@ public class RecipientDatabase extends Database {
|
|||
return callRingtone;
|
||||
}
|
||||
|
||||
public boolean hasSeenInviteReminder() {
|
||||
return seenInviteReminder;
|
||||
public @NonNull InsightsBannerTier getInsightsBannerTier() {
|
||||
return insightsBannerTier;
|
||||
}
|
||||
|
||||
public Optional<Integer> getDefaultSubscriptionId() {
|
||||
|
|
|
@ -29,7 +29,6 @@ import com.annimon.stream.Stream;
|
|||
import net.sqlcipher.database.SQLiteDatabase;
|
||||
import net.sqlcipher.database.SQLiteStatement;
|
||||
|
||||
import org.thoughtcrime.securesms.ApplicationContext;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
|
@ -45,6 +44,7 @@ import org.thoughtcrime.securesms.sms.IncomingTextMessage;
|
|||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -53,6 +53,7 @@ import java.util.LinkedList;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Database for storage of SMS messages.
|
||||
|
@ -102,6 +103,9 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED
|
||||
};
|
||||
|
||||
private final String OUTGOING_INSECURE_MESSAGE_CLAUSE = "(" + TYPE + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND NOT (" + TYPE + " & " + Types.SECURE_MESSAGE_BIT + ")";
|
||||
private final String OUTGOING_SECURE_MESSAGE_CLAUSE = "(" + TYPE + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_SENT_TYPE + " AND (" + TYPE + " & " + (Types.SECURE_MESSAGE_BIT | Types.PUSH_MESSAGE_BIT) + ")";
|
||||
|
||||
private static final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache("SmsDelivery");
|
||||
private static final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache("SmsRead");
|
||||
|
||||
|
@ -113,6 +117,16 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
return TABLE_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDateSentColumnName() {
|
||||
return DATE_SENT;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getTypeField() {
|
||||
return TYPE;
|
||||
}
|
||||
|
||||
private void updateTypeBitmask(long id, long maskOff, long maskOn) {
|
||||
Log.i("MessageDatabase", "Updating ID: " + id + " to base type: " + maskOn);
|
||||
|
||||
|
|
|
@ -79,7 +79,7 @@ public class ThreadDatabase extends Database {
|
|||
public static final String READ_RECEIPT_COUNT = "read_receipt_count";
|
||||
public static final String EXPIRES_IN = "expires_in";
|
||||
public static final String LAST_SEEN = "last_seen";
|
||||
private static final String HAS_SENT = "has_sent";
|
||||
public static final String HAS_SENT = "has_sent";
|
||||
|
||||
public static final String CREATE_TABLE = "CREATE TABLE " + TABLE_NAME + " (" +
|
||||
ID + " INTEGER PRIMARY KEY, " + DATE + " INTEGER DEFAULT 0, " +
|
||||
|
|
|
@ -0,0 +1,94 @@
|
|||
package org.thoughtcrime.securesms.insights;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.animation.ValueAnimator;
|
||||
import android.view.animation.DecelerateInterpolator;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
final class InsightsAnimatorSetFactory {
|
||||
private static final int PROGRESS_ANIMATION_DURATION = 800;
|
||||
private static final int DETAILS_ANIMATION_DURATION = 200;
|
||||
private static final int PERCENT_SECURE_ANIMATION_DURATION = 400;
|
||||
private static final int LOTTIE_ANIMATION_DURATION = 1500;
|
||||
private static final int ANIMATION_START_DELAY = PROGRESS_ANIMATION_DURATION - DETAILS_ANIMATION_DURATION;
|
||||
private static final float PERCENT_SECURE_MAX_SCALE = 1.3f;
|
||||
|
||||
private InsightsAnimatorSetFactory() {
|
||||
}
|
||||
|
||||
static AnimatorSet create(int insecurePercent,
|
||||
@Nullable final UpdateListener progressUpdateListener,
|
||||
@Nullable final UpdateListener detailsUpdateListener,
|
||||
@Nullable final UpdateListener percentSecureListener,
|
||||
@Nullable final UpdateListener lottieListener)
|
||||
{
|
||||
final int securePercent = 100 - insecurePercent;
|
||||
final AnimatorSet animatorSet = new AnimatorSet();
|
||||
final ValueAnimator[] animators = Stream.of(createProgressAnimator(securePercent, progressUpdateListener),
|
||||
createDetailsAnimator(detailsUpdateListener),
|
||||
createPercentSecureAnimator(percentSecureListener),
|
||||
createLottieAnimator(lottieListener))
|
||||
.filter(a -> a != null)
|
||||
.toArray(ValueAnimator[]::new);
|
||||
|
||||
animatorSet.setInterpolator(new DecelerateInterpolator());
|
||||
animatorSet.playTogether(animators);
|
||||
|
||||
return animatorSet;
|
||||
}
|
||||
|
||||
private static @Nullable Animator createProgressAnimator(int securePercent, @Nullable UpdateListener updateListener) {
|
||||
if (updateListener == null) return null;
|
||||
|
||||
final ValueAnimator progressAnimator = ValueAnimator.ofFloat(0, securePercent / 100f);
|
||||
|
||||
progressAnimator.setDuration(PROGRESS_ANIMATION_DURATION);
|
||||
progressAnimator.addUpdateListener(animation -> updateListener.onUpdate((float) animation.getAnimatedValue()));
|
||||
|
||||
return progressAnimator;
|
||||
}
|
||||
|
||||
private static @Nullable Animator createDetailsAnimator(@Nullable UpdateListener updateListener) {
|
||||
if (updateListener == null) return null;
|
||||
|
||||
final ValueAnimator detailsAnimator = ValueAnimator.ofFloat(0, 1f);
|
||||
|
||||
detailsAnimator.setDuration(DETAILS_ANIMATION_DURATION);
|
||||
detailsAnimator.setStartDelay(ANIMATION_START_DELAY);
|
||||
detailsAnimator.addUpdateListener(animation -> updateListener.onUpdate((float) animation.getAnimatedValue()));
|
||||
|
||||
return detailsAnimator;
|
||||
}
|
||||
|
||||
private static @Nullable Animator createPercentSecureAnimator(@Nullable UpdateListener updateListener) {
|
||||
if (updateListener == null) return null;
|
||||
|
||||
final ValueAnimator percentSecureAnimator = ValueAnimator.ofFloat(1f, PERCENT_SECURE_MAX_SCALE, 1f);
|
||||
|
||||
percentSecureAnimator.setStartDelay(ANIMATION_START_DELAY);
|
||||
percentSecureAnimator.setDuration(PERCENT_SECURE_ANIMATION_DURATION);
|
||||
percentSecureAnimator.addUpdateListener(animation -> updateListener.onUpdate((float) animation.getAnimatedValue()));
|
||||
|
||||
return percentSecureAnimator;
|
||||
}
|
||||
|
||||
private static @Nullable Animator createLottieAnimator(@Nullable UpdateListener updateListener) {
|
||||
if (updateListener == null) return null;
|
||||
|
||||
final ValueAnimator lottieAnimator = ValueAnimator.ofFloat(0, 1f);
|
||||
|
||||
lottieAnimator.setStartDelay(ANIMATION_START_DELAY);
|
||||
lottieAnimator.setDuration(LOTTIE_ANIMATION_DURATION);
|
||||
lottieAnimator.addUpdateListener(animation -> updateListener.onUpdate((float) animation.getAnimatedValue()));
|
||||
|
||||
return lottieAnimator;
|
||||
}
|
||||
|
||||
interface UpdateListener {
|
||||
void onUpdate(float value);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,269 @@
|
|||
package org.thoughtcrime.securesms.insights;
|
||||
|
||||
import android.animation.Animator;
|
||||
import android.animation.AnimatorSet;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.airbnb.lottie.LottieAnimationView;
|
||||
|
||||
import org.thoughtcrime.securesms.NewConversationActivity;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.ArcProgressBar;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class InsightsDashboardDialogFragment extends DialogFragment {
|
||||
|
||||
private TextView securePercentage;
|
||||
private ArcProgressBar progress;
|
||||
private View progressContainer;
|
||||
private TextView tagline;
|
||||
private TextView encryptedMessages;
|
||||
private TextView title;
|
||||
private TextView description;
|
||||
private RecyclerView insecureRecipients;
|
||||
private TextView locallyGenerated;
|
||||
private AvatarImageView avatarImageView;
|
||||
private InsightsInsecureRecipientsAdapter adapter;
|
||||
private LottieAnimationView lottieAnimationView;
|
||||
private AnimatorSet animatorSet;
|
||||
private Button startAConversation;
|
||||
private Toolbar toolbar;
|
||||
private InsightsDashboardViewModel viewModel;
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
requireFragmentManager().beginTransaction()
|
||||
.detach(this)
|
||||
.attach(this)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
if (ThemeUtil.isDarkTheme(requireActivity())) {
|
||||
setStyle(STYLE_NO_FRAME, R.style.TextSecure_DarkTheme);
|
||||
} else {
|
||||
setStyle(STYLE_NO_FRAME, R.style.TextSecure_LightTheme);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.insights_dashboard, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
securePercentage = view.findViewById(R.id.insights_dashboard_percent_secure);
|
||||
progress = view.findViewById(R.id.insights_dashboard_progress);
|
||||
progressContainer = view.findViewById(R.id.insights_dashboard_percent_container);
|
||||
encryptedMessages = view.findViewById(R.id.insights_dashboard_encrypted_messages);
|
||||
tagline = view.findViewById(R.id.insights_dashboard_tagline);
|
||||
title = view.findViewById(R.id.insights_dashboard_make_signal_secure);
|
||||
description = view.findViewById(R.id.insights_dashboard_invite_your_contacts);
|
||||
insecureRecipients = view.findViewById(R.id.insights_dashboard_recycler);
|
||||
locallyGenerated = view.findViewById(R.id.insights_dashboard_this_stat_was_generated_locally);
|
||||
avatarImageView = view.findViewById(R.id.insights_dashboard_avatar);
|
||||
startAConversation = view.findViewById(R.id.insights_dashboard_start_a_conversation);
|
||||
lottieAnimationView = view.findViewById(R.id.insights_dashboard_lottie_animation);
|
||||
toolbar = view.findViewById(R.id.insights_dashboard_toolbar);
|
||||
|
||||
setupStartAConversation();
|
||||
setDashboardDetailsAlpha(0f);
|
||||
setNotEnoughDataAlpha(0f);
|
||||
setupToolbar();
|
||||
setupRecycler();
|
||||
initializeViewModel();
|
||||
}
|
||||
|
||||
private void setupStartAConversation() {
|
||||
startAConversation.setOnClickListener(v -> startActivity(new Intent(requireActivity(), NewConversationActivity.class)));
|
||||
}
|
||||
|
||||
private void setDashboardDetailsAlpha(float alpha) {
|
||||
tagline.setAlpha(alpha);
|
||||
title.setAlpha(alpha);
|
||||
description.setAlpha(alpha);
|
||||
insecureRecipients.setAlpha(alpha);
|
||||
locallyGenerated.setAlpha(alpha);
|
||||
encryptedMessages.setAlpha(alpha);
|
||||
}
|
||||
|
||||
private void setupToolbar() {
|
||||
toolbar.setNavigationOnClickListener(v -> dismiss());
|
||||
}
|
||||
|
||||
private void setupRecycler() {
|
||||
adapter = new InsightsInsecureRecipientsAdapter(this::handleInviteRecipient);
|
||||
insecureRecipients.setAdapter(adapter);
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
final InsightsDashboardViewModel.Repository repository = new InsightsRepository(requireContext());
|
||||
final InsightsDashboardViewModel.Factory factory = new InsightsDashboardViewModel.Factory(repository);
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(InsightsDashboardViewModel.class);
|
||||
|
||||
viewModel.getState().observe(this, state -> {
|
||||
updateInsecurePercent(state.getData());
|
||||
updateInsecureRecipients(state.getInsecureRecipients());
|
||||
updateUserAvatar(state.getUserAvatar());
|
||||
});
|
||||
}
|
||||
|
||||
private void updateInsecurePercent(@Nullable InsightsData insightsData) {
|
||||
if (insightsData == null) return;
|
||||
|
||||
if (insightsData.hasEnoughData()) {
|
||||
setTitleAndDescriptionText(insightsData.getPercentInsecure());
|
||||
animateProgress(insightsData.getPercentInsecure());
|
||||
} else {
|
||||
setNotEnoughDataText();
|
||||
animateNotEnoughData();
|
||||
}
|
||||
}
|
||||
|
||||
private void animateProgress(int insecurePercent) {
|
||||
startAConversation.setVisibility(View.GONE);
|
||||
if (animatorSet == null) {
|
||||
animatorSet = InsightsAnimatorSetFactory.create(insecurePercent,
|
||||
this::setProgressPercentage,
|
||||
this::setDashboardDetailsAlpha,
|
||||
this::setPercentSecureScale,
|
||||
insecurePercent == 0 ? this::setLottieProgress : null);
|
||||
|
||||
if (insecurePercent == 0) {
|
||||
animatorSet.addListener(new ToolbarBackgroundColorAnimationListener());
|
||||
}
|
||||
|
||||
animatorSet.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void setProgressPercentage(float percent) {
|
||||
securePercentage.setText(String.valueOf(Math.round(percent * 100)));
|
||||
progress.setProgress(percent);
|
||||
}
|
||||
|
||||
private void setPercentSecureScale(float scale) {
|
||||
progressContainer.setScaleX(scale);
|
||||
progressContainer.setScaleY(scale);
|
||||
}
|
||||
|
||||
private void setLottieProgress(float progress) {
|
||||
lottieAnimationView.setProgress(progress);
|
||||
}
|
||||
|
||||
private void setTitleAndDescriptionText(int insecurePercent) {
|
||||
startAConversation.setVisibility(View.GONE);
|
||||
progressContainer.setVisibility(View.VISIBLE);
|
||||
insecureRecipients.setVisibility(View.VISIBLE);
|
||||
encryptedMessages.setText(R.string.InsightsDashboardFragment__encrypted_messages);
|
||||
tagline.setText(getString(R.string.InsightsDashboardFragment__tagline, 100 - insecurePercent));
|
||||
|
||||
if (insecurePercent == 0) {
|
||||
lottieAnimationView.setVisibility(View.VISIBLE);
|
||||
title.setText(R.string.InsightsDashboardFragment__100_title);
|
||||
description.setText(R.string.InsightsDashboardFragment__100_description);
|
||||
} else {
|
||||
lottieAnimationView.setVisibility(View.GONE);
|
||||
title.setText(R.string.InsightsDashboardFragment__boost_your_signal);
|
||||
description.setText(R.string.InsightsDashboardFragment__invite_your_contacts);
|
||||
}
|
||||
}
|
||||
|
||||
private void setNotEnoughDataText() {
|
||||
startAConversation.setVisibility(View.VISIBLE);
|
||||
progressContainer.setVisibility(View.INVISIBLE);
|
||||
insecureRecipients.setVisibility(View.GONE);
|
||||
encryptedMessages.setText(R.string.InsightsDashboardFragment__no_signal_yet);
|
||||
tagline.setText(R.string.InsightsDashboardFragment__youre_just_getting_started);
|
||||
}
|
||||
|
||||
private void animateNotEnoughData() {
|
||||
if (animatorSet == null) {
|
||||
animatorSet = InsightsAnimatorSetFactory.create(0, null, this::setNotEnoughDataAlpha, null, null);
|
||||
animatorSet.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void setNotEnoughDataAlpha(float alpha) {
|
||||
encryptedMessages.setAlpha(alpha);
|
||||
tagline.setAlpha(alpha);
|
||||
startAConversation.setAlpha(alpha);
|
||||
}
|
||||
|
||||
private void updateInsecureRecipients(@NonNull List<Recipient> recipients) {
|
||||
adapter.updateData(recipients);
|
||||
}
|
||||
|
||||
private void updateUserAvatar(@Nullable InsightsUserAvatar userAvatar) {
|
||||
if (userAvatar == null) avatarImageView.setImageDrawable(null);
|
||||
else userAvatar.load(avatarImageView);
|
||||
}
|
||||
|
||||
private void handleInviteRecipient(final @NonNull Recipient recipient) {
|
||||
new AlertDialog.Builder(requireContext())
|
||||
.setTitle(getResources().getQuantityString(R.plurals.InviteActivity_send_sms_invites, 1, 1))
|
||||
.setMessage(getString(R.string.InviteActivity_lets_switch_to_signal, getString(R.string.install_url)))
|
||||
.setPositiveButton(R.string.InsightsDashboardFragment__send, (dialog, which) -> viewModel.sendSmsInvite(recipient))
|
||||
.setNegativeButton(R.string.InsightsDashboardFragment__cancel, (dialog, which) -> dialog.dismiss())
|
||||
.show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (animatorSet != null) {
|
||||
animatorSet.cancel();
|
||||
animatorSet = null;
|
||||
}
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
private final class ToolbarBackgroundColorAnimationListener implements Animator.AnimatorListener {
|
||||
|
||||
@Override
|
||||
public void onAnimationStart(Animator animation) {
|
||||
toolbar.setBackgroundResource(R.color.transparent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
toolbar.setBackgroundColor(ThemeUtil.getThemedColor(requireContext(), android.R.attr.windowBackground));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationCancel(Animator animation) {
|
||||
toolbar.setBackgroundColor(ThemeUtil.getThemedColor(requireContext(), android.R.attr.windowBackground));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(Animator animation) {
|
||||
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package org.thoughtcrime.securesms.insights;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
final class InsightsDashboardState {
|
||||
|
||||
private final List<Recipient> insecureRecipients;
|
||||
private final InsightsData insightsData;
|
||||
private final InsightsUserAvatar userAvatar;
|
||||
|
||||
private InsightsDashboardState(@NonNull Builder builder) {
|
||||
this.insecureRecipients = builder.insecureRecipients;
|
||||
this.insightsData = builder.insightsData;
|
||||
this.userAvatar = builder.userAvatar;
|
||||
}
|
||||
|
||||
static @NonNull InsightsDashboardState.Builder builder() {
|
||||
return new InsightsDashboardState.Builder();
|
||||
}
|
||||
|
||||
@NonNull InsightsDashboardState.Builder buildUpon() {
|
||||
return builder().withData(insightsData).withUserAvatar(userAvatar).withInsecureRecipients(insecureRecipients);
|
||||
}
|
||||
|
||||
@NonNull List<Recipient> getInsecureRecipients() {
|
||||
return insecureRecipients;
|
||||
}
|
||||
|
||||
@Nullable InsightsUserAvatar getUserAvatar() {
|
||||
return userAvatar;
|
||||
}
|
||||
|
||||
@Nullable InsightsData getData() {
|
||||
return insightsData;
|
||||
}
|
||||
|
||||
static final class Builder {
|
||||
private List<Recipient> insecureRecipients = Collections.emptyList();
|
||||
private InsightsUserAvatar userAvatar;
|
||||
private InsightsData insightsData;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
@NonNull Builder withInsecureRecipients(@NonNull List<Recipient> insecureRecipients) {
|
||||
this.insecureRecipients = insecureRecipients;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull Builder withData(@NonNull InsightsData insightsData) {
|
||||
this.insightsData = insightsData;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull Builder withUserAvatar(@NonNull InsightsUserAvatar userAvatar) {
|
||||
this.userAvatar = userAvatar;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull InsightsDashboardState build() {
|
||||
return new InsightsDashboardState(this);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,68 @@
|
|||
package org.thoughtcrime.securesms.insights;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
final class InsightsDashboardViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<InsightsDashboardState> internalState = new MutableLiveData<>(InsightsDashboardState.builder().build());
|
||||
private final Repository repository;
|
||||
|
||||
private InsightsDashboardViewModel(@NonNull Repository repository) {
|
||||
this.repository = repository;
|
||||
|
||||
repository.getInsightsData(data -> internalState.setValue(getNewState(b -> b.withData(data))));
|
||||
repository.getUserAvatar(avatar -> internalState.setValue(getNewState(b -> b.withUserAvatar(avatar))));
|
||||
updateInsecureRecipients();
|
||||
}
|
||||
|
||||
private void updateInsecureRecipients() {
|
||||
repository.getInsecureRecipients(recipients -> internalState.setValue(getNewState(b -> b.withInsecureRecipients(recipients))));
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private InsightsDashboardState getNewState(Consumer<InsightsDashboardState.Builder> builderConsumer) {
|
||||
InsightsDashboardState.Builder builder = internalState.getValue().buildUpon();
|
||||
builderConsumer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@NonNull LiveData<InsightsDashboardState> getState() {
|
||||
return internalState;
|
||||
}
|
||||
|
||||
public void sendSmsInvite(@NonNull Recipient recipient) {
|
||||
repository.sendSmsInvite(recipient, this::updateInsecureRecipients);
|
||||
}
|
||||
|
||||
interface Repository {
|
||||
void getInsightsData(@NonNull Consumer<InsightsData> insightsDataConsumer);
|
||||
void getInsecureRecipients(@NonNull Consumer<List<Recipient>> insecureRecipientsConsumer);
|
||||
void getUserAvatar(@NonNull Consumer<InsightsUserAvatar> userAvatarConsumer);
|
||||
void sendSmsInvite(@NonNull Recipient recipient, Runnable onSmsMessageSent);
|
||||
}
|
||||
|
||||
final static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final Repository repository;
|
||||
|
||||
Factory(@NonNull Repository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return (T) new InsightsDashboardViewModel(repository);
|
||||
}
|
||||
}
|
||||
}
|
19
src/org/thoughtcrime/securesms/insights/InsightsData.java
Normal file
19
src/org/thoughtcrime/securesms/insights/InsightsData.java
Normal file
|
@ -0,0 +1,19 @@
|
|||
package org.thoughtcrime.securesms.insights;
|
||||
|
||||
final class InsightsData {
|
||||
private final boolean hasEnoughData;
|
||||
private final int percentInsecure;
|
||||
|
||||
InsightsData(boolean hasEnoughData, int percentInsecure) {
|
||||
this.hasEnoughData = hasEnoughData;
|
||||
this.percentInsecure = percentInsecure;
|
||||
}
|
||||
|
||||
public boolean hasEnoughData() {
|
||||
return hasEnoughData;
|
||||
}
|
||||
|
||||
public int getPercentInsecure() {
|
||||
return percentInsecure;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,118 @@
|
|||
package org.thoughtcrime.securesms.insights;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
final class InsightsInsecureRecipientsAdapter extends RecyclerView.Adapter<InsightsInsecureRecipientsAdapter.ViewHolder> {
|
||||
|
||||
private List<Recipient> data = Collections.emptyList();
|
||||
|
||||
private final Consumer<Recipient> onInviteClickedConsumer;
|
||||
|
||||
InsightsInsecureRecipientsAdapter(Consumer<Recipient> onInviteClickedConsumer) {
|
||||
this.onInviteClickedConsumer = onInviteClickedConsumer;
|
||||
}
|
||||
|
||||
public void updateData(List<Recipient> recipients) {
|
||||
List<Recipient> oldData = data;
|
||||
data = recipients;
|
||||
|
||||
DiffUtil.calculateDiff(new DiffCallback(oldData, data)).dispatchUpdatesTo(this);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.insights_dashboard_adapter_item, parent, false), this::handleInviteClicked);
|
||||
}
|
||||
|
||||
private void handleInviteClicked(@NonNull Integer position) {
|
||||
onInviteClickedConsumer.accept(data.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(data.get(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return data.size();
|
||||
}
|
||||
|
||||
static final class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private AvatarImageView avatarImageView;
|
||||
private TextView displayName;
|
||||
|
||||
private ViewHolder(@NonNull View itemView, Consumer<Integer> onInviteClicked) {
|
||||
super(itemView);
|
||||
|
||||
avatarImageView = itemView.findViewById(R.id.recipient_avatar);
|
||||
displayName = itemView.findViewById(R.id.recipient_display_name);
|
||||
|
||||
Button invite = itemView.findViewById(R.id.recipient_invite);
|
||||
invite.setOnClickListener(v -> {
|
||||
int adapterPosition = getAdapterPosition();
|
||||
|
||||
if (adapterPosition == RecyclerView.NO_POSITION) return;
|
||||
|
||||
onInviteClicked.accept(adapterPosition);
|
||||
});
|
||||
}
|
||||
|
||||
private void bind(@NonNull Recipient recipient) {
|
||||
displayName.setText(recipient.getDisplayName(itemView.getContext()));
|
||||
avatarImageView.setAvatar(GlideApp.with(itemView), recipient, false);
|
||||
}
|
||||
}
|
||||
|
||||
private static class DiffCallback extends DiffUtil.Callback {
|
||||
|
||||
private final List<Recipient> oldData;
|
||||
private final List<Recipient> newData;
|
||||
|
||||
private DiffCallback(@NonNull List<Recipient> oldData,
|
||||
@NonNull List<Recipient> newData)
|
||||
{
|
||||
this.oldData = oldData;
|
||||
this.newData = newData;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getOldListSize() {
|
||||
return oldData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getNewListSize() {
|
||||
return newData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
return oldData.get(oldItemPosition).getId() == newData.get(newItemPosition).getId();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
|
||||
return oldData.get(oldItemPosition).equals(newData.get(newItemPosition));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package org.thoughtcrime.securesms.insights;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
|
||||
public final class InsightsLauncher {
|
||||
|
||||
private static final String MODAL_TAG = "modal.fragment";
|
||||
|
||||
public static void showInsightsModal(@NonNull Context context, @NonNull FragmentManager fragmentManager) {
|
||||
if (InsightsOptOut.userHasOptedOut(context)) return;
|
||||
|
||||
final Fragment fragment = fragmentManager.findFragmentByTag(MODAL_TAG);
|
||||
|
||||
if (fragment == null) new InsightsModalDialogFragment().show(fragmentManager, MODAL_TAG);
|
||||
}
|
||||
|
||||
public static void showInsightsDashboard(@NonNull FragmentManager fragmentManager) {
|
||||
new InsightsDashboardDialogFragment().show(fragmentManager, null);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
package org.thoughtcrime.securesms.insights;
|
||||
|
||||
import android.animation.AnimatorSet;
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.content.res.Configuration;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.ArcProgressBar;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
|
||||
public final class InsightsModalDialogFragment extends DialogFragment {
|
||||
|
||||
private ArcProgressBar progress;
|
||||
private TextView securePercentage;
|
||||
private AvatarImageView avatarImageView;
|
||||
private AnimatorSet animatorSet;
|
||||
private View progressContainer;
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
requireFragmentManager().beginTransaction()
|
||||
.detach(this)
|
||||
.attach(this)
|
||||
.commit();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setStyle(STYLE_NO_FRAME, R.style.Theme_Signal_Insights_Modal);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) {
|
||||
Dialog dialog = super.onCreateDialog(savedInstanceState);
|
||||
dialog.getWindow().setBackgroundDrawableResource(android.R.color.transparent);
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
|
||||
return inflater.inflate(R.layout.insights_modal, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
View close = view.findViewById(R.id.insights_modal_close);
|
||||
Button viewInsights = view.findViewById(R.id.insights_modal_view_insights);
|
||||
|
||||
progress = view.findViewById(R.id.insights_modal_progress);
|
||||
securePercentage = view.findViewById(R.id.insights_modal_percent_secure);
|
||||
avatarImageView = view.findViewById(R.id.insights_modal_avatar);
|
||||
progressContainer = view.findViewById(R.id.insights_modal_percent_container);
|
||||
|
||||
close.setOnClickListener(v -> dismiss());
|
||||
viewInsights.setOnClickListener(v -> openInsightsAndDismiss());
|
||||
|
||||
initializeViewModel();
|
||||
}
|
||||
|
||||
private void initializeViewModel() {
|
||||
final InsightsModalViewModel.Repository repository = new InsightsRepository(requireContext());
|
||||
final InsightsModalViewModel.Factory factory = new InsightsModalViewModel.Factory(repository);
|
||||
final InsightsModalViewModel viewModel = ViewModelProviders.of(this, factory).get(InsightsModalViewModel.class);
|
||||
|
||||
viewModel.getState().observe(this, state -> {
|
||||
updateInsecurePercent(state.getData());
|
||||
updateUserAvatar(state.getUserAvatar());
|
||||
});
|
||||
}
|
||||
|
||||
private void updateInsecurePercent(@Nullable InsightsData insightsData) {
|
||||
if (insightsData == null) return;
|
||||
|
||||
if (animatorSet == null) {
|
||||
animatorSet = InsightsAnimatorSetFactory.create(insightsData.getPercentInsecure(), this::setProgressPercentage, null, this::setPercentSecureScale, null);
|
||||
animatorSet.start();
|
||||
}
|
||||
}
|
||||
|
||||
private void setProgressPercentage(float percent) {
|
||||
securePercentage.setText(String.valueOf(Math.round(percent * 100)));
|
||||
progress.setProgress(percent);
|
||||
}
|
||||
|
||||
private void setPercentSecureScale(float scale) {
|
||||
progressContainer.setScaleX(scale);
|
||||
progressContainer.setScaleY(scale);
|
||||
}
|
||||
|
||||
private void updateUserAvatar(@Nullable InsightsUserAvatar userAvatar) {
|
||||
if (userAvatar == null) avatarImageView.setImageDrawable(null);
|
||||
else userAvatar.load(avatarImageView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDismiss(@NonNull DialogInterface dialog) {
|
||||
InsightsOptOut.userRequestedOptOut(requireContext());
|
||||
}
|
||||
|
||||
private void openInsightsAndDismiss() {
|
||||
InsightsLauncher.showInsightsDashboard(requireFragmentManager());
|
||||
dismiss();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (animatorSet != null) {
|
||||
animatorSet.cancel();
|
||||
animatorSet = null;
|
||||
}
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package org.thoughtcrime.securesms.insights;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
final class InsightsModalState {
|
||||
|
||||
private final InsightsData insightsData;
|
||||
private final InsightsUserAvatar userAvatar;
|
||||
|
||||
private InsightsModalState(@NonNull Builder builder) {
|
||||
this.insightsData = builder.insightsData;
|
||||
this.userAvatar = builder.userAvatar;
|
||||
}
|
||||
|
||||
static @NonNull InsightsModalState.Builder builder() {
|
||||
return new InsightsModalState.Builder();
|
||||
}
|
||||
|
||||
@NonNull InsightsModalState.Builder buildUpon() {
|
||||
return builder().withUserAvatar(userAvatar).withData(insightsData);
|
||||
}
|
||||
|
||||
@Nullable InsightsUserAvatar getUserAvatar() {
|
||||
return userAvatar;
|
||||
}
|
||||
|
||||
@Nullable InsightsData getData() {
|
||||
return insightsData;
|
||||
}
|
||||
|
||||
static final class Builder {
|
||||
private InsightsData insightsData;
|
||||
private InsightsUserAvatar userAvatar;
|
||||
|
||||
private Builder() {
|
||||
}
|
||||
|
||||
@NonNull Builder withData(@NonNull InsightsData insightsData) {
|
||||
this.insightsData = insightsData;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull Builder withUserAvatar(@NonNull InsightsUserAvatar userAvatar) {
|
||||
this.userAvatar = userAvatar;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull InsightsModalState build() {
|
||||
return new InsightsModalState(this);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
package org.thoughtcrime.securesms.insights;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
final class InsightsModalViewModel extends ViewModel {
|
||||
|
||||
private final MutableLiveData<InsightsModalState> internalState = new MutableLiveData<>(InsightsModalState.builder().build());
|
||||
|
||||
private InsightsModalViewModel(@NonNull Repository repository) {
|
||||
repository.getInsightsData(data -> internalState.setValue(getNewState(b -> b.withData(data))));
|
||||
repository.getUserAvatar(avatar -> internalState.setValue(getNewState(b -> b.withUserAvatar(avatar))));
|
||||
}
|
||||
|
||||
@MainThread
|
||||
private InsightsModalState getNewState(Consumer<InsightsModalState.Builder> builderConsumer) {
|
||||
InsightsModalState.Builder builder = internalState.getValue().buildUpon();
|
||||
builderConsumer.accept(builder);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
@NonNull LiveData<InsightsModalState> getState() {
|
||||
return internalState;
|
||||
}
|
||||
|
||||
interface Repository {
|
||||
void getInsightsData(Consumer<InsightsData> insecurePercentConsumer);
|
||||
void getUserAvatar(@NonNull Consumer<InsightsUserAvatar> userAvatarConsumer);
|
||||
}
|
||||
|
||||
final static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final Repository repository;
|
||||
|
||||
Factory(@NonNull Repository repository) {
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
return (T) new InsightsModalViewModel(repository);
|
||||
}
|
||||
}
|
||||
}
|
19
src/org/thoughtcrime/securesms/insights/InsightsOptOut.java
Normal file
19
src/org/thoughtcrime/securesms/insights/InsightsOptOut.java
Normal file
|
@ -0,0 +1,19 @@
|
|||
package org.thoughtcrime.securesms.insights;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
class InsightsOptOut {
|
||||
private static final String INSIGHTS_OPT_OUT_PREFERENCE = "insights.opt.out";
|
||||
|
||||
static boolean userHasOptedOut(@NonNull Context context) {
|
||||
return TextSecurePreferences.getBooleanPreference(context, INSIGHTS_OPT_OUT_PREFERENCE, false);
|
||||
}
|
||||
|
||||
static void userRequestedOptOut(@NonNull Context context) {
|
||||
TextSecurePreferences.setBooleanPreference(context, INSIGHTS_OPT_OUT_PREFERENCE, true);
|
||||
}
|
||||
}
|
102
src/org/thoughtcrime/securesms/insights/InsightsRepository.java
Normal file
102
src/org/thoughtcrime/securesms/insights/InsightsRepository.java
Normal file
|
@ -0,0 +1,102 @@
|
|||
package org.thoughtcrime.securesms.insights;
|
||||
|
||||
import android.content.Context;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.Consumer;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
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.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.sms.OutgoingTextMessage;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class InsightsRepository implements InsightsDashboardViewModel.Repository, InsightsModalViewModel.Repository {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public InsightsRepository(Context context) {
|
||||
this.context = context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getInsightsData(@NonNull Consumer<InsightsData> insightsDataConsumer) {
|
||||
SimpleTask.run(() -> {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
List<RecipientId> unregisteredRecipients = recipientDatabase.getNotRegisteredForInsights();
|
||||
List<RecipientId> registeredRecipients = recipientDatabase.getRegisteredForInsights();
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
int insecure = mmsSmsDatabase.getInsecureMessageCountForRecipients(unregisteredRecipients);
|
||||
int secure = mmsSmsDatabase.getSecureMessageCountForRecipients(registeredRecipients);
|
||||
|
||||
if (insecure + secure == 0) {
|
||||
return new InsightsData(false, 0);
|
||||
} else {
|
||||
return new InsightsData(true, Util.clamp((int) Math.ceil((insecure * 100f) / (insecure + secure)), 0, 100));
|
||||
}
|
||||
}, insightsDataConsumer::accept);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getInsecureRecipients(@NonNull Consumer<List<Recipient>> insecureRecipientsConsumer) {
|
||||
SimpleTask.run(() -> {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
List<RecipientId> unregisteredRecipients = recipientDatabase.getUninvitedRecipientsForInsights();
|
||||
|
||||
return Stream.of(unregisteredRecipients)
|
||||
.map(Recipient::resolved)
|
||||
.toList();
|
||||
},
|
||||
insecureRecipientsConsumer::accept);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void getUserAvatar(@NonNull Consumer<InsightsUserAvatar> avatarConsumer) {
|
||||
SimpleTask.run(() -> {
|
||||
Recipient self = Recipient.self().resolve();
|
||||
String name = Optional.fromNullable(self.getName(context)).or(Optional.fromNullable(TextSecurePreferences.getProfileName(context))).or("");
|
||||
MaterialColor fallbackColor = self.getColor();
|
||||
|
||||
if (fallbackColor == ContactColors.UNKNOWN_COLOR && !TextUtils.isEmpty(name)) {
|
||||
fallbackColor = ContactColors.generateFor(name);
|
||||
}
|
||||
|
||||
return new InsightsUserAvatar(new ProfileContactPhoto(self.getId(), String.valueOf(TextSecurePreferences.getProfileAvatarId(context))),
|
||||
fallbackColor,
|
||||
new GeneratedContactPhoto(name, R.drawable.ic_profile_outline_40));
|
||||
}, avatarConsumer::accept);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendSmsInvite(@NonNull Recipient recipient, Runnable onSmsMessageSent) {
|
||||
SimpleTask.run(() -> {
|
||||
Recipient resolved = recipient.resolve();
|
||||
int subscriptionId = resolved.getDefaultSubscriptionId().or(-1);
|
||||
String message = context.getString(R.string.InviteActivity_lets_switch_to_signal, context.getString(R.string.install_url));
|
||||
|
||||
MessageSender.send(context, new OutgoingTextMessage(resolved, message, subscriptionId), -1L, true, null);
|
||||
|
||||
RecipientDatabase database = DatabaseFactory.getRecipientDatabase(context);
|
||||
database.setHasSentInvite(recipient.getId());
|
||||
|
||||
return null;
|
||||
}, v -> onSmsMessageSent.run());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package org.thoughtcrime.securesms.insights;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.FallbackContactPhoto;
|
||||
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
|
||||
import org.thoughtcrime.securesms.mms.GlideApp;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
|
||||
class InsightsUserAvatar {
|
||||
private final ProfileContactPhoto profileContactPhoto;
|
||||
private final MaterialColor fallbackColor;
|
||||
private final FallbackContactPhoto fallbackContactPhoto;
|
||||
|
||||
InsightsUserAvatar(@NonNull ProfileContactPhoto profileContactPhoto, @NonNull MaterialColor fallbackColor, @NonNull FallbackContactPhoto fallbackContactPhoto) {
|
||||
this.profileContactPhoto = profileContactPhoto;
|
||||
this.fallbackColor = fallbackColor;
|
||||
this.fallbackContactPhoto = fallbackContactPhoto;
|
||||
}
|
||||
|
||||
private Drawable fallbackDrawable(@NonNull Context context) {
|
||||
return fallbackContactPhoto.asDrawable(context, fallbackColor.toAvatarColor(context));
|
||||
}
|
||||
|
||||
void load(ImageView into) {
|
||||
GlideApp.with(into)
|
||||
.load(profileContactPhoto)
|
||||
.error(fallbackDrawable(into.getContext()))
|
||||
.circleCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.into(into);
|
||||
}
|
||||
}
|
144
src/org/thoughtcrime/securesms/invites/InviteReminderModel.java
Normal file
144
src/org/thoughtcrime/securesms/invites/InviteReminderModel.java
Normal file
|
@ -0,0 +1,144 @@
|
|||
package org.thoughtcrime.securesms.invites;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.thoughtcrime.securesms.components.reminder.SecondInviteReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.FirstInviteReminder;
|
||||
import org.thoughtcrime.securesms.components.reminder.Reminder;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SimpleTask;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
|
||||
public final class InviteReminderModel {
|
||||
|
||||
private static final int FIRST_INVITE_REMINDER_MESSAGE_THRESHOLD = 10;
|
||||
private static final int SECOND_INVITE_REMINDER_MESSAGE_THRESHOLD = 500;
|
||||
|
||||
private final Context context;
|
||||
private final Repository repository;
|
||||
private final AtomicReference<ReminderInfo> reminderInfo = new AtomicReference<>();
|
||||
|
||||
public InviteReminderModel(@NonNull Context context, @NonNull Repository repository) {
|
||||
this.context = context;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void loadReminder(LiveRecipient liveRecipient, Runnable reminderCheckComplete) {
|
||||
SimpleTask.run(() -> createReminderInfo(liveRecipient.resolve()), result -> {
|
||||
reminderInfo.set(result);
|
||||
reminderCheckComplete.run();
|
||||
});
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private @NonNull ReminderInfo createReminderInfo(Recipient recipient) {
|
||||
Recipient resolved = recipient.resolve();
|
||||
|
||||
if (resolved.isRegistered() || resolved.isGroup() || resolved.hasSeenSecondInviteReminder()) {
|
||||
return new NoReminderInfo();
|
||||
}
|
||||
|
||||
ThreadDatabase threadDatabase = DatabaseFactory.getThreadDatabase(context);
|
||||
long threadId = threadDatabase.getThreadIdFor(recipient);
|
||||
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
int conversationCount = mmsSmsDatabase.getInsecureSentCount(threadId);
|
||||
|
||||
if (conversationCount >= SECOND_INVITE_REMINDER_MESSAGE_THRESHOLD && !resolved.hasSeenSecondInviteReminder()) {
|
||||
return new SecondInviteReminderInfo(context, resolved, repository, repository.getPercentOfInsecureMessages(conversationCount));
|
||||
} else if (conversationCount >= FIRST_INVITE_REMINDER_MESSAGE_THRESHOLD && !resolved.hasSeenFirstInviteReminder()) {
|
||||
return new FirstInviteReminderInfo(context, resolved, repository, repository.getPercentOfInsecureMessages(conversationCount));
|
||||
} else {
|
||||
return new NoReminderInfo();
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull Optional<Reminder> getReminder() {
|
||||
ReminderInfo info = reminderInfo.get();
|
||||
if (info == null) return Optional.absent();
|
||||
else return Optional.fromNullable(info.reminder);
|
||||
}
|
||||
|
||||
public void dismissReminder() {
|
||||
final ReminderInfo info = reminderInfo.getAndSet(null);
|
||||
|
||||
SimpleTask.run(() -> {
|
||||
info.dismiss();
|
||||
return null;
|
||||
}, (v) -> {});
|
||||
}
|
||||
|
||||
interface Repository {
|
||||
void setHasSeenFirstInviteReminder(Recipient recipient);
|
||||
void setHasSeenSecondInviteReminder(Recipient recipient);
|
||||
int getPercentOfInsecureMessages(int insecureCount);
|
||||
}
|
||||
|
||||
private static abstract class ReminderInfo {
|
||||
|
||||
private final Reminder reminder;
|
||||
|
||||
ReminderInfo(Reminder reminder) {
|
||||
this.reminder = reminder;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
void dismiss() {
|
||||
}
|
||||
}
|
||||
|
||||
private static class NoReminderInfo extends ReminderInfo {
|
||||
private NoReminderInfo() {
|
||||
super(null);
|
||||
}
|
||||
}
|
||||
|
||||
private class FirstInviteReminderInfo extends ReminderInfo {
|
||||
|
||||
private final Repository repository;
|
||||
private final Recipient recipient;
|
||||
|
||||
private FirstInviteReminderInfo(@NonNull Context context, @NonNull Recipient recipient, @NonNull Repository repository, int percentInsecure) {
|
||||
super(new FirstInviteReminder(context, recipient, percentInsecure));
|
||||
|
||||
this.recipient = recipient;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@Override
|
||||
@WorkerThread
|
||||
void dismiss() {
|
||||
repository.setHasSeenFirstInviteReminder(recipient);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SecondInviteReminderInfo extends ReminderInfo {
|
||||
|
||||
private final Repository repository;
|
||||
private final Recipient recipient;
|
||||
|
||||
private SecondInviteReminderInfo(@NonNull Context context, @NonNull Recipient recipient, @NonNull Repository repository, int percentInsecure) {
|
||||
super(new SecondInviteReminder(context, recipient, percentInsecure));
|
||||
|
||||
this.repository = repository;
|
||||
this.recipient = recipient;
|
||||
}
|
||||
|
||||
@Override
|
||||
@WorkerThread
|
||||
void dismiss() {
|
||||
repository.setHasSeenSecondInviteReminder(recipient);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
package org.thoughtcrime.securesms.invites;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public final class InviteReminderRepository implements InviteReminderModel.Repository {
|
||||
|
||||
private final Context context;
|
||||
|
||||
public InviteReminderRepository(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHasSeenFirstInviteReminder(Recipient recipient) {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
recipientDatabase.setSeenFirstInviteReminder(recipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setHasSeenSecondInviteReminder(Recipient recipient) {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
recipientDatabase.setSeenSecondInviteReminder(recipient.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPercentOfInsecureMessages(int insecureCount) {
|
||||
RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
|
||||
List<RecipientId> registeredRecipients = recipientDatabase.getRegisteredForInsights();
|
||||
List<RecipientId> unregisteredRecipients = recipientDatabase.getNotRegisteredForInsights();
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
int insecure = mmsSmsDatabase.getInsecureMessageCountForRecipients(unregisteredRecipients);
|
||||
int secure = mmsSmsDatabase.getSecureMessageCountForRecipients(registeredRecipients);
|
||||
|
||||
if (insecure + secure == 0) return 0;
|
||||
return Math.round(100f * (insecureCount / (float) (insecure + secure)));
|
||||
}
|
||||
}
|
|
@ -47,6 +47,8 @@ import java.util.List;
|
|||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
|
||||
import static org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier;
|
||||
|
||||
public class Recipient {
|
||||
|
||||
public static final Recipient UNKNOWN = new Recipient(RecipientId.UNKNOWN, new RecipientDetails());
|
||||
|
@ -69,7 +71,6 @@ public class Recipient {
|
|||
private final Uri messageRingtone;
|
||||
private final Uri callRingtone;
|
||||
private final MaterialColor color;
|
||||
private final boolean seenInviteReminder;
|
||||
private final Optional<Integer> defaultSubscriptionId;
|
||||
private final int expireMessages;
|
||||
private final RegisteredState registered;
|
||||
|
@ -85,6 +86,7 @@ public class Recipient {
|
|||
private final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
private final boolean forceSmsSelection;
|
||||
private final boolean uuidSupported;
|
||||
private final InsightsBannerTier insightsBannerTier;
|
||||
|
||||
|
||||
/**
|
||||
|
@ -246,7 +248,7 @@ public class Recipient {
|
|||
this.messageRingtone = null;
|
||||
this.callRingtone = null;
|
||||
this.color = null;
|
||||
this.seenInviteReminder = true;
|
||||
this.insightsBannerTier = InsightsBannerTier.TIER_TWO;
|
||||
this.defaultSubscriptionId = Optional.absent();
|
||||
this.expireMessages = 0;
|
||||
this.registered = RegisteredState.UNKNOWN;
|
||||
|
@ -281,7 +283,7 @@ public class Recipient {
|
|||
this.messageRingtone = details.messageRingtone;
|
||||
this.callRingtone = details.callRingtone;
|
||||
this.color = details.color;
|
||||
this.seenInviteReminder = details.seenInviteReminder;
|
||||
this.insightsBannerTier = details.insightsBannerTier;
|
||||
this.defaultSubscriptionId = details.defaultSubscriptionId;
|
||||
this.expireMessages = details.expireMessages;
|
||||
this.registered = details.registered;
|
||||
|
@ -571,8 +573,12 @@ public class Recipient {
|
|||
return expireMessages;
|
||||
}
|
||||
|
||||
public boolean hasSeenInviteReminder() {
|
||||
return seenInviteReminder;
|
||||
public boolean hasSeenFirstInviteReminder() {
|
||||
return insightsBannerTier.seen(InsightsBannerTier.TIER_ONE);
|
||||
}
|
||||
|
||||
public boolean hasSeenSecondInviteReminder() {
|
||||
return insightsBannerTier.seen(InsightsBannerTier.TIER_TWO);
|
||||
}
|
||||
|
||||
public @NonNull RegisteredState getRegistered() {
|
||||
|
|
|
@ -7,6 +7,7 @@ import androidx.annotation.NonNull;
|
|||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.color.MaterialColor;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.InsightsBannerTier;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RecipientSettings;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.RegisteredState;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||
|
@ -40,7 +41,6 @@ public class RecipientDetails {
|
|||
final int expireMessages;
|
||||
final List<Recipient> participants;
|
||||
final String profileName;
|
||||
final boolean seenInviteReminder;
|
||||
final Optional<Integer> defaultSubscriptionId;
|
||||
final RegisteredState registered;
|
||||
final byte[] profileKey;
|
||||
|
@ -52,6 +52,7 @@ public class RecipientDetails {
|
|||
final UnidentifiedAccessMode unidentifiedAccessMode;
|
||||
final boolean forceSmsSelection;
|
||||
final boolean uuidSuported;
|
||||
final InsightsBannerTier insightsBannerTier;
|
||||
|
||||
RecipientDetails(@NonNull Context context,
|
||||
@Nullable String name,
|
||||
|
@ -79,7 +80,6 @@ public class RecipientDetails {
|
|||
this.expireMessages = settings.getExpireMessages();
|
||||
this.participants = participants == null ? new LinkedList<>() : participants;
|
||||
this.profileName = isLocalNumber ? TextSecurePreferences.getProfileName(context) : settings.getProfileName();
|
||||
this.seenInviteReminder = settings.hasSeenInviteReminder();
|
||||
this.defaultSubscriptionId = settings.getDefaultSubscriptionId();
|
||||
this.registered = settings.getRegistered();
|
||||
this.profileKey = settings.getProfileKey();
|
||||
|
@ -91,6 +91,7 @@ public class RecipientDetails {
|
|||
this.unidentifiedAccessMode = settings.getUnidentifiedAccessMode();
|
||||
this.forceSmsSelection = settings.isForceSmsSelection();
|
||||
this.uuidSuported = settings.isUuidSupported();
|
||||
this.insightsBannerTier = settings.getInsightsBannerTier();
|
||||
|
||||
if (name == null) this.name = settings.getSystemDisplayName();
|
||||
else this.name = name;
|
||||
|
@ -115,7 +116,7 @@ public class RecipientDetails {
|
|||
this.expireMessages = 0;
|
||||
this.participants = new LinkedList<>();
|
||||
this.profileName = null;
|
||||
this.seenInviteReminder = true;
|
||||
this.insightsBannerTier = InsightsBannerTier.TIER_TWO;
|
||||
this.defaultSubscriptionId = Optional.absent();
|
||||
this.registered = RegisteredState.UNKNOWN;
|
||||
this.profileKey = null;
|
||||
|
|
Loading…
Add table
Reference in a new issue