diff --git a/res/drawable/ic_x_tinted.xml b/res/drawable/ic_x_tinted.xml
new file mode 100644
index 0000000000..a06051fb1c
--- /dev/null
+++ b/res/drawable/ic_x_tinted.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/res/drawable/insights_cta_button_background.xml b/res/drawable/insights_cta_button_background.xml
new file mode 100644
index 0000000000..5d8a5c7ee1
--- /dev/null
+++ b/res/drawable/insights_cta_button_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/insights_modal_background.xml b/res/drawable/insights_modal_background.xml
new file mode 100644
index 0000000000..3fa21373fe
--- /dev/null
+++ b/res/drawable/insights_modal_background.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/insights_modal_background_dark.xml b/res/drawable/insights_modal_background_dark.xml
new file mode 100644
index 0000000000..854e599bb2
--- /dev/null
+++ b/res/drawable/insights_modal_background_dark.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/res/drawable/reminder_progress_ring.xml b/res/drawable/reminder_progress_ring.xml
new file mode 100644
index 0000000000..d2b4d25356
--- /dev/null
+++ b/res/drawable/reminder_progress_ring.xml
@@ -0,0 +1,25 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
diff --git a/res/layout/insights_dashboard.xml b/res/layout/insights_dashboard.xml
new file mode 100644
index 0000000000..4d550f7191
--- /dev/null
+++ b/res/layout/insights_dashboard.xml
@@ -0,0 +1,200 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/insights_dashboard_adapter_item.xml b/res/layout/insights_dashboard_adapter_item.xml
new file mode 100644
index 0000000000..e6130c4137
--- /dev/null
+++ b/res/layout/insights_dashboard_adapter_item.xml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/insights_modal.xml b/res/layout/insights_modal.xml
new file mode 100644
index 0000000000..4fcc4c72f5
--- /dev/null
+++ b/res/layout/insights_modal.xml
@@ -0,0 +1,114 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/layout/reminder_action_button.xml b/res/layout/reminder_action_button.xml
new file mode 100644
index 0000000000..9b66c0842b
--- /dev/null
+++ b/res/layout/reminder_action_button.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/res/layout/reminder_header.xml b/res/layout/reminder_header.xml
index 8ec5032e50..cccf1d052d 100644
--- a/res/layout/reminder_header.xml
+++ b/res/layout/reminder_header.xml
@@ -1,6 +1,6 @@
-
-
+
+
+ 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%" />
-
+
-
+
-
+
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toTopOf="parent" />
-
+
+
+
diff --git a/res/layout/reminder_progress.xml b/res/layout/reminder_progress.xml
new file mode 100644
index 0000000000..ffad99d510
--- /dev/null
+++ b/res/layout/reminder_progress.xml
@@ -0,0 +1,9 @@
+
+
\ No newline at end of file
diff --git a/res/menu/text_secure_normal.xml b/res/menu/text_secure_normal.xml
index cb90a43076..baa40c98e2 100644
--- a/res/menu/text_secure_normal.xml
+++ b/res/menu/text_secure_normal.xml
@@ -16,6 +16,10 @@
+
+
diff --git a/res/raw/lottie_insights_100.json b/res/raw/lottie_insights_100.json
new file mode 100644
index 0000000000..495035cb42
--- /dev/null
+++ b/res/raw/lottie_insights_100.json
@@ -0,0 +1 @@
+{"v":"4.12.0","fr":29.9700012207031,"ip":0,"op":59.0000024031193,"w":800,"h":800,"nm":"confettis2","ddd":0,"assets":[{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"p20","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":4,"s":[100],"e":[0]},{"t":43.0000017514259}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":4,"s":[400,400,0],"e":[71,152,0],"to":[1.83333337306976,-220.66667175293,0],"ti":[85.1666641235352,-23.3333339691162,0]},{"t":43.0000017514259}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.901960790157,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":4.00000016292334,"op":364.000014826024,"st":4.00000016292334,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"p19","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":3,"s":[100],"e":[0]},{"t":42.0000017106951}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":3,"s":[400,400,0],"e":[579,746,0],"to":[101.833335876465,91.3333358764648,0],"ti":[27.1666660308838,-153.33332824707,0]},{"t":42.0000017106951}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":3.00000012219251,"op":363.000014785293,"st":3.00000012219251,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"p18","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":2,"s":[100],"e":[0]},{"t":41.0000016699642}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":2,"s":[400,400,0],"e":[503,238,0],"to":[-84.1666641235352,-74.6666641235352,0],"ti":[-88.8333358764648,-45.3333320617676,0]},{"t":41.0000016699642}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.074509806931,0.737254917622,0.172549024224,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2.00000008146167,"op":362.000014744562,"st":2.00000008146167,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"p17","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":1,"s":[100],"e":[0]},{"t":40.0000016292334}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":1,"s":[400,400,0],"e":[91,556,0],"to":[-82.1666641235352,-112.666664123535,0],"ti":[73.1666641235352,-199.33332824707,0]},{"t":40.0000016292334}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1.00000004073083,"op":361.000014703831,"st":1.00000004073083,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"p16","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":0,"s":[100],"e":[0]},{"t":39.0000015885026}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":0,"s":[400,400,0],"e":[511,308,0],"to":[77.8333358764648,91.3333358764648,0],"ti":[51.1666679382324,60.6666679382324,0]},{"t":39.0000015885026}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.270588248968,0.529411792755,0.952941179276,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360.000014663101,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"p15","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":1,"s":[100],"e":[0]},{"t":40.0000016292334}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":1,"s":[400,400,0],"e":[155,280,0],"to":[-30.1666660308838,-122.666664123535,0],"ti":[95.1666641235352,-53.3333320617676,0]},{"t":40.0000016292334}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.901960790157,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1.00000004073083,"op":361.000014703831,"st":1.00000004073083,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"p14","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":2,"s":[100],"e":[0]},{"t":41.0000016699642}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":2,"s":[400,400,0],"e":[681,388.872,0],"to":[143.83332824707,49.3333320617676,0],"ti":[-58.8333320617676,48.6666679382324,0]},{"t":41.0000016699642}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2.00000008146167,"op":362.000014744562,"st":2.00000008146167,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"p13","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":3,"s":[100],"e":[0]},{"t":42.0000017106951}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":3,"s":[400,400,0],"e":[257,284,0],"to":[-24.1666660308838,-72.6666641235352,0],"ti":[75.1666641235352,-5.33333349227905,0]},{"t":42.0000017106951}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.074509806931,0.737254917622,0.172549024224,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":3.00000012219251,"op":363.000014785293,"st":3.00000012219251,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"p12","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":4,"s":[100],"e":[0]},{"t":43.0000017514259}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":4,"s":[400,400,0],"e":[301,474,0],"to":[-14.1666669845581,31.3333339691162,0],"ti":[65.1666641235352,-13.3333330154419,0]},{"t":43.0000017514259}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":4.00000016292334,"op":364.000014826024,"st":4.00000016292334,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":"p11","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":0,"s":[100],"e":[0]},{"t":39.0000015885026}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":0,"s":[400,400,0],"e":[499,570,0],"to":[-8.16666698455811,63.3333320617676,0],"ti":[-62.8333320617676,-29.3333339691162,0]},{"t":39.0000015885026}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.270588248968,0.529411792755,0.952941179276,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360.000014663101,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"p10","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":1,"s":[100],"e":[0]},{"t":40.0000016292334}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":1,"s":[400,400,0],"e":[557,68,0],"to":[81.8333358764648,-104.666664123535,0],"ti":[-58.8333320617676,104.666664123535,0]},{"t":40.0000016292334}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.901960790157,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":1.00000004073083,"op":361.000014703831,"st":1.00000004073083,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"p9","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":2,"s":[100],"e":[0]},{"t":41.0000016699642}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":2,"s":[400,400,0],"e":[715,138,0],"to":[151.83332824707,-46.6666679382324,0],"ti":[-26.8333339691162,72.6666641235352,0]},{"t":41.0000016699642}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":2.00000008146167,"op":362.000014744562,"st":2.00000008146167,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"p8","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":3,"s":[100],"e":[0]},{"t":42.0000017106951}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":3,"s":[400,400,0],"e":[679,664,0],"to":[107.833335876465,89.3333358764648,0],"ti":[-92.8333358764648,-103.333335876465,0]},{"t":42.0000017106951}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.074509806931,0.737254917622,0.172549024224,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":3.00000012219251,"op":363.000014785293,"st":3.00000012219251,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"p7","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":4,"s":[100],"e":[0]},{"t":43.0000017514259}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":4,"s":[400,400,0],"e":[97,686,0],"to":[-36.1666679382324,53.3333320617676,0],"ti":[75.1666641235352,-39.3333320617676,0]},{"t":43.0000017514259}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":4.00000016292334,"op":364.000014826024,"st":4.00000016292334,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"p6","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":5,"s":[100],"e":[0]},{"t":44.0000017921567}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":5,"s":[400,400,0],"e":[65,340,0],"to":[-148.16667175293,-132.66667175293,0],"ti":[61.1666679382324,-65.3333358764648,0]},{"t":44.0000017921567}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.270588248968,0.529411792755,0.952941179276,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":5.00000020365417,"op":365.000014866755,"st":5.00000020365417,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"p5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":6,"s":[100],"e":[0]},{"t":45.0000018328876}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.004,"y":0.691},"o":{"x":0.014,"y":0},"n":"0p004_0p691_0p014_0","t":6,"s":[400,400,0],"e":[400,23.613,0],"to":[-55.3803939819336,-168.204071044922,0],"ti":[27.0301876068115,124.269813537598,0]},{"t":45.0000018328876}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.901960790157,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":6.00000024438501,"op":366.000014907486,"st":6.00000024438501,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"p4","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":7,"s":[100],"e":[0]},{"t":46.0000018736184}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":7,"s":[400,400,0],"e":[303,660,0],"to":[109.833335876465,69.3333358764648,0],"ti":[97.1666641235352,0.66666668653488,0]},{"t":46.0000018736184}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":7.00000028511585,"op":367.000014948216,"st":7.00000028511585,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"p3","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":8,"s":[100],"e":[0]},{"t":47.0000019143492}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":8,"s":[400,400,0],"e":[663,498,0],"to":[41.8333320617676,109.333335876465,0],"ti":[-71.8333358764648,39.6666679382324,0]},{"t":47.0000019143492}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.076272718608,0.735462605953,0.171031266451,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":8.00000032584668,"op":368.000014988947,"st":8.00000032584668,"bm":0},{"ddd":0,"ind":19,"ty":4,"nm":"p2","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":9,"s":[100],"e":[0]},{"t":48.0000019550801}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":9,"s":[400,400,0],"e":[187,88,0],"to":[-0.16666667163372,-158.66667175293,0],"ti":[89.1666641235352,6.66666650772095,0]},{"t":48.0000019550801}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.271778345108,0.528400123119,0.952267169952,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":9.00000036657752,"op":369.000015029678,"st":9.00000036657752,"bm":0},{"ddd":0,"ind":20,"ty":4,"nm":"p1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[1],"y":[1]},"o":{"x":[0.01],"y":[0]},"n":["1_1_0p01_0"],"t":0,"s":[100],"e":[0]},{"t":39.0000015885026}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.01,"y":0},"n":"0_1_0p01_0","t":0,"s":[400,400,0],"e":[621,234,0],"to":[-0.16666667163372,-158.66667175293,0],"ti":[-92.8333358764648,-103.333335876465,0]},{"t":39.0000015885026}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.855,0],[0,-7.855],[-7.855,0],[0,7.855]],"o":[[-7.855,0],[0,7.855],[7.855,0],[0,-7.855]],"v":[[0,-14.223],[-14.223,0],[0,14.223],[14.223,0]],"c":true},"ix":2},"nm":"Tracé 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.903676450253,0,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fond 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[80,80],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transformer "}],"nm":"Ellipse 1","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":360.000014663101,"st":0,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"confettis1","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":42,"ix":10},"p":{"a":0,"k":[396,400,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[91.5,91.5,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":9.00000036657752,"op":369.000015029678,"st":9.00000036657752,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"confettis1","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":267,"ix":10},"p":{"a":0,"k":[416,420,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[64.5,64.5,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":7.00000028511585,"op":367.000014948216,"st":7.00000028511585,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"confettis1","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":358,"ix":10},"p":{"a":0,"k":[436,400,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[64.5,64.5,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":5.00000020365417,"op":365.000014866755,"st":5.00000020365417,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"confettis1","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":177,"ix":10},"p":{"a":0,"k":[416,380,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[64.5,64.5,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":3.00000012219251,"op":363.000014785293,"st":3.00000012219251,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"confettis1","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":132,"ix":10},"p":{"a":0,"k":[416,400,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[91.5,91.5,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":1.00000004073083,"op":361.000014703831,"st":1.00000004073083,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"confettis1","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-90,"ix":10},"p":{"a":0,"k":[396,400,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[91.5,91.5,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":8.00000032584668,"op":368.000014988947,"st":8.00000032584668,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"confettis1","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":135,"ix":10},"p":{"a":0,"k":[416,420,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[64.5,64.5,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":6.00000024438501,"op":366.000014907486,"st":6.00000024438501,"bm":0},{"ddd":0,"ind":8,"ty":0,"nm":"confettis1","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":226,"ix":10},"p":{"a":0,"k":[436,400,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[64.5,64.5,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":4.00000016292334,"op":364.000014826024,"st":4.00000016292334,"bm":0},{"ddd":0,"ind":9,"ty":0,"nm":"confettis1","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":45,"ix":10},"p":{"a":0,"k":[416,380,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[64.5,64.5,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":2.00000008146167,"op":362.000014744562,"st":2.00000008146167,"bm":0},{"ddd":0,"ind":10,"ty":0,"nm":"confettis1","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[416,400,0],"ix":2},"a":{"a":0,"k":[400,400,0],"ix":1},"s":{"a":0,"k":[91.5,91.5,100],"ix":6}},"ao":0,"w":800,"h":800,"ip":0,"op":360.000014663101,"st":0,"bm":0}]}
\ No newline at end of file
diff --git a/res/values-land/dimens.xml b/res/values-land/dimens.xml
index 7c0fab9157..e8956da24e 100644
--- a/res/values-land/dimens.xml
+++ b/res/values-land/dimens.xml
@@ -1,4 +1,16 @@
5
+
+ 12dp
+ 10dp
+ 17dp
+ 131dp
+ 94dp
+ 2dp
+ 6dp
+ 31dp
+ 20sp
+ 16sp
+
\ No newline at end of file
diff --git a/res/values/attrs.xml b/res/values/attrs.xml
index 486081f42c..9996d1f590 100644
--- a/res/values/attrs.xml
+++ b/res/values/attrs.xml
@@ -35,6 +35,13 @@
+
+
+
+
+
+
+
diff --git a/res/values/dimens.xml b/res/values/dimens.xml
index 31666fc1fd..cf5d5a03ec 100644
--- a/res/values/dimens.xml
+++ b/res/values/dimens.xml
@@ -91,6 +91,17 @@
13sp
26sp
+ 41dp
+ 12dp
+ 23dp
+ 187dp
+ 140dp
+ 6dp
+ 15dp
+ 41dp
+ 28sp
+ 20sp
+
diff --git a/res/values/ids.xml b/res/values/ids.xml
index cb9392f697..ebcc8de541 100644
--- a/res/values/ids.xml
+++ b/res/values/ids.xml
@@ -3,4 +3,7 @@
+
+
+
diff --git a/res/values/strings.xml b/res/values/strings.xml
index eff41c3e55..d18f040487 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -1517,6 +1517,7 @@
The more friends use Signal, the better it gets.
Signal is experiencing technical difficulties. We are working hard to restore service as quickly as possible.
The latest Signal features won\'t work on this version of Android. Please upgrade this device to receive future Signal updates.
+ %1$d%%
Save
@@ -1538,6 +1539,34 @@
Deleting old messages…
Old messages successfully deleted
+
+ %
+ Insights
+ Insights
+ %1$d%% of your outgoing messages in the past 7 days were end-to-end encrypted with Signal Protocol.
+ Boost your Signal
+ Your Signal is Strong
+ No Signal (yet)
+ You\'re just getting started. Insights will be displayed after you send a few messages.
+ Start a conversation
+ Signal\'s advanced privacy-preserving technology automatically protected all of your recent outgoing messages.
+ Start communicating securely and enable new features that go beyond the limitations of unencrypted SMS messages by inviting more contacts to join Signal.
+ This stat was locally generated on your device and can only be seen by you. It is never transmitted anywhere.
+ Encrypted messages
+ Cancel
+ Send
+ Not using Signal yet
+ Introducing Insights
+ Find out how many of your outgoing messages were sent securely, then quickly invite new contacts to boost your Signal percentage.
+ View Insights
+
+ Invite to Signal
+ You could increase the number of encrypted messages you send by %1$d%%
+ Boost your Signal
+ Invite %1$s
+ View Insights
+ Invite
+
Transport icon
Loading…
diff --git a/res/values/styles.xml b/res/values/styles.xml
index fac5e03dc4..6ef51acb82 100644
--- a/res/values/styles.xml
+++ b/res/values/styles.xml
@@ -232,7 +232,7 @@
- textAutoCorrect|textCapSentences|textMultiLine
- @string/conversation_activity__compose_description
-
+
-
+
+
@@ -362,4 +373,14 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/res/values/text_styles.xml b/res/values/text_styles.xml
index bf7f3f668c..d8d5b77eee 100644
--- a/res/values/text_styles.xml
+++ b/res/values/text_styles.xml
@@ -45,4 +45,46 @@
- @color/core_white
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/res/values/themes.xml b/res/values/themes.xml
index 1d953c0b6e..0bd1d8ed1f 100644
--- a/res/values/themes.xml
+++ b/res/values/themes.xml
@@ -149,6 +149,13 @@
- @color/core_grey_75
- @color/core_grey_15
+ - @drawable/insights_modal_background
+ - @color/core_grey_10
+ - @color/core_grey_90
+ - @color/core_grey_60
+ - @color/core_grey_02
+ - @color/core_grey_15
+
- @style/Signal.SearchView
- @style/Signal.SearchView.Dark
@@ -365,6 +372,13 @@
- @color/core_grey_15
- ?icon_tint
+ - @drawable/insights_modal_background_dark
+ - @color/core_grey_60
+ - @color/core_grey_25
+ - @color/core_grey_25
+ - @color/core_grey_60
+ - @color/core_grey_80
+
- @style/Signal.SearchView
- @style/Signal.SearchView.Dark
@@ -575,6 +589,9 @@
- @drawable/permission_rationale_dialog_corners
+
+
diff --git a/src/org/thoughtcrime/securesms/ConversationListActivity.java b/src/org/thoughtcrime/securesms/ConversationListActivity.java
index cc75f23a83..d515068267 100644
--- a/src/org/thoughtcrime/securesms/ConversationListActivity.java
+++ b/src/org/thoughtcrime/securesms/ConversationListActivity.java
@@ -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")));
diff --git a/src/org/thoughtcrime/securesms/ConversationListFragment.java b/src/org/thoughtcrime/securesms/ConversationListFragment.java
index 4b68dcb913..aea856c138 100644
--- a/src/org/thoughtcrime/securesms/ConversationListFragment.java
+++ b/src/org/thoughtcrime/securesms/ConversationListFragment.java
@@ -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
diff --git a/src/org/thoughtcrime/securesms/InviteActivity.java b/src/org/thoughtcrime/securesms/InviteActivity.java
index 0933c2b32c..66f181e45d 100644
--- a/src/org/thoughtcrime/securesms/InviteActivity.java
+++ b/src/org/thoughtcrime/securesms/InviteActivity.java
@@ -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());
}
}
diff --git a/src/org/thoughtcrime/securesms/components/ArcProgressBar.java b/src/org/thoughtcrime/securesms/components/ArcProgressBar.java
new file mode 100644
index 0000000000..e9062fb69b
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/ArcProgressBar.java
@@ -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);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/components/reminder/FirstInviteReminder.java b/src/org/thoughtcrime/securesms/components/reminder/FirstInviteReminder.java
new file mode 100644
index 0000000000..b8709c9e83
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/reminder/FirstInviteReminder.java
@@ -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));
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/components/reminder/InviteReminder.java b/src/org/thoughtcrime/securesms/components/reminder/InviteReminder.java
deleted file mode 100644
index e19739f11b..0000000000
--- a/src/org/thoughtcrime/securesms/components/reminder/InviteReminder.java
+++ /dev/null
@@ -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);
- }));
- }
-}
diff --git a/src/org/thoughtcrime/securesms/components/reminder/Reminder.java b/src/org/thoughtcrime/securesms/components/reminder/Reminder.java
index be0c867173..109d9f4497 100644
--- a/src/org/thoughtcrime/securesms/components/reminder/Reminder.java
+++ b/src/org/thoughtcrime/securesms/components/reminder/Reminder.java
@@ -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 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 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;
+ }
+ }
}
diff --git a/src/org/thoughtcrime/securesms/components/reminder/ReminderActionsAdapter.java b/src/org/thoughtcrime/securesms/components/reminder/ReminderActionsAdapter.java
new file mode 100644
index 0000000000..64afd9f809
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/reminder/ReminderActionsAdapter.java
@@ -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 {
+
+ private final List actions;
+ private final ReminderView.OnActionClickListener actionClickListener;
+
+ ReminderActionsAdapter(List 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);
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/components/reminder/ReminderView.java b/src/org/thoughtcrime/securesms/components/reminder/ReminderView.java
index 18e29b3745..2ea65728d8 100644
--- a/src/org/thoughtcrime/securesms/components/reminder/ReminderView.java
+++ b/src/org/thoughtcrime/securesms/components/reminder/ReminderView.java
@@ -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 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);
+ }
}
diff --git a/src/org/thoughtcrime/securesms/components/reminder/SecondInviteReminder.java b/src/org/thoughtcrime/securesms/components/reminder/SecondInviteReminder.java
new file mode 100644
index 0000000000..0aede6213f
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/components/reminder/SecondInviteReminder.java
@@ -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;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index 45b51fbe6e..e4dd33179e 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -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 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 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);
diff --git a/src/org/thoughtcrime/securesms/conversation/ConversationPopupActivity.java b/src/org/thoughtcrime/securesms/conversation/ConversationPopupActivity.java
index 1c4455de47..a56526216e 100644
--- a/src/org/thoughtcrime/securesms/conversation/ConversationPopupActivity.java
+++ b/src/org/thoughtcrime/securesms/conversation/ConversationPopupActivity.java
@@ -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);
}
diff --git a/src/org/thoughtcrime/securesms/database/MessagingDatabase.java b/src/org/thoughtcrime/securesms/database/MessagingDatabase.java
index d48e5ca92e..f84e51b6d0 100644
--- a/src/org/thoughtcrime/securesms/database/MessagingDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MessagingDatabase.java
@@ -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 recipients) {
+ return getMessageCountForRecipientsAndType(recipients, getOutgoingInsecureMessageClause());
+ }
+
+ final int getSecureMessageCountForRecipients(List recipients) {
+ return getMessageCountForRecipientsAndType(recipients, getOutgoingSecureMessageClause());
+ }
+
+ private int getMessageCountForRecipientsAndType(List 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,
diff --git a/src/org/thoughtcrime/securesms/database/MmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
index b83e8d7757..dabe8440a3 100644
--- a/src/org/thoughtcrime/securesms/database/MmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsDatabase.java
@@ -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;
diff --git a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
index dc4582421b..0cb9dff8e8 100644
--- a/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
@@ -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 recipients) {
+ int count = DatabaseFactory.getSmsDatabase(context).getInsecureMessageCountForRecipients(recipients);
+ count += DatabaseFactory.getMmsDatabase(context).getInsecureMessageCountForRecipients(recipients);
+
+ return count;
+ }
+
+ public int getSecureMessageCountForRecipients(List 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);
diff --git a/src/org/thoughtcrime/securesms/database/RecipientDatabase.java b/src/org/thoughtcrime/securesms/database/RecipientDatabase.java
index e2919ea789..282f454873 100644
--- a/src/org/thoughtcrime/securesms/database/RecipientDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/RecipientDatabase.java
@@ -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 getRegistered() {
+ public @NonNull List getUninvitedRecipientsForInsights() {
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+ List 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 getNotRegisteredForInsights() {
+ return getRecipientsForInsights(REGISTERED + " = ?", new String[]{String.valueOf(RegisteredState.NOT_REGISTERED.id)});
+ }
+
+ public @NonNull List 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 getRecipientsForInsights(@NonNull String query, @NonNull String[] args) {
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+ List 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 getRegistered() {
SQLiteDatabase db = databaseHelper.getReadableDatabase();
List 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 getDefaultSubscriptionId() {
diff --git a/src/org/thoughtcrime/securesms/database/SmsDatabase.java b/src/org/thoughtcrime/securesms/database/SmsDatabase.java
index 4c7a6a47dc..322dd91435 100644
--- a/src/org/thoughtcrime/securesms/database/SmsDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/SmsDatabase.java
@@ -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);
diff --git a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java
index d0f40ef363..60b7f1bcb3 100644
--- a/src/org/thoughtcrime/securesms/database/ThreadDatabase.java
+++ b/src/org/thoughtcrime/securesms/database/ThreadDatabase.java
@@ -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, " +
diff --git a/src/org/thoughtcrime/securesms/insights/InsightsAnimatorSetFactory.java b/src/org/thoughtcrime/securesms/insights/InsightsAnimatorSetFactory.java
new file mode 100644
index 0000000000..a46499ef47
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/insights/InsightsAnimatorSetFactory.java
@@ -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);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/insights/InsightsDashboardDialogFragment.java b/src/org/thoughtcrime/securesms/insights/InsightsDashboardDialogFragment.java
new file mode 100644
index 0000000000..323794bc96
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/insights/InsightsDashboardDialogFragment.java
@@ -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 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) {
+
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/insights/InsightsDashboardState.java b/src/org/thoughtcrime/securesms/insights/InsightsDashboardState.java
new file mode 100644
index 0000000000..f97933342c
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/insights/InsightsDashboardState.java
@@ -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 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 getInsecureRecipients() {
+ return insecureRecipients;
+ }
+
+ @Nullable InsightsUserAvatar getUserAvatar() {
+ return userAvatar;
+ }
+
+ @Nullable InsightsData getData() {
+ return insightsData;
+ }
+
+ static final class Builder {
+ private List insecureRecipients = Collections.emptyList();
+ private InsightsUserAvatar userAvatar;
+ private InsightsData insightsData;
+
+ private Builder() {
+ }
+
+ @NonNull Builder withInsecureRecipients(@NonNull List 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);
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/insights/InsightsDashboardViewModel.java b/src/org/thoughtcrime/securesms/insights/InsightsDashboardViewModel.java
new file mode 100644
index 0000000000..af604a9cde
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/insights/InsightsDashboardViewModel.java
@@ -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 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 builderConsumer) {
+ InsightsDashboardState.Builder builder = internalState.getValue().buildUpon();
+ builderConsumer.accept(builder);
+ return builder.build();
+ }
+
+ @NonNull LiveData getState() {
+ return internalState;
+ }
+
+ public void sendSmsInvite(@NonNull Recipient recipient) {
+ repository.sendSmsInvite(recipient, this::updateInsecureRecipients);
+ }
+
+ interface Repository {
+ void getInsightsData(@NonNull Consumer insightsDataConsumer);
+ void getInsecureRecipients(@NonNull Consumer> insecureRecipientsConsumer);
+ void getUserAvatar(@NonNull Consumer 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 create(@NonNull Class modelClass) {
+ return (T) new InsightsDashboardViewModel(repository);
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/insights/InsightsData.java b/src/org/thoughtcrime/securesms/insights/InsightsData.java
new file mode 100644
index 0000000000..5fb4b5da32
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/insights/InsightsData.java
@@ -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;
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/insights/InsightsInsecureRecipientsAdapter.java b/src/org/thoughtcrime/securesms/insights/InsightsInsecureRecipientsAdapter.java
new file mode 100644
index 0000000000..d36e0be26a
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/insights/InsightsInsecureRecipientsAdapter.java
@@ -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 {
+
+ private List data = Collections.emptyList();
+
+ private final Consumer onInviteClickedConsumer;
+
+ InsightsInsecureRecipientsAdapter(Consumer onInviteClickedConsumer) {
+ this.onInviteClickedConsumer = onInviteClickedConsumer;
+ }
+
+ public void updateData(List recipients) {
+ List 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 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 oldData;
+ private final List newData;
+
+ private DiffCallback(@NonNull List oldData,
+ @NonNull List 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));
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/insights/InsightsLauncher.java b/src/org/thoughtcrime/securesms/insights/InsightsLauncher.java
new file mode 100644
index 0000000000..e5de5fee4d
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/insights/InsightsLauncher.java
@@ -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);
+ }
+
+}
diff --git a/src/org/thoughtcrime/securesms/insights/InsightsModalDialogFragment.java b/src/org/thoughtcrime/securesms/insights/InsightsModalDialogFragment.java
new file mode 100644
index 0000000000..0fbc2d9bda
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/insights/InsightsModalDialogFragment.java
@@ -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();
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/insights/InsightsModalState.java b/src/org/thoughtcrime/securesms/insights/InsightsModalState.java
new file mode 100644
index 0000000000..8ce6542628
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/insights/InsightsModalState.java
@@ -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);
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/insights/InsightsModalViewModel.java b/src/org/thoughtcrime/securesms/insights/InsightsModalViewModel.java
new file mode 100644
index 0000000000..ad80c40847
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/insights/InsightsModalViewModel.java
@@ -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 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 builderConsumer) {
+ InsightsModalState.Builder builder = internalState.getValue().buildUpon();
+ builderConsumer.accept(builder);
+ return builder.build();
+ }
+
+ @NonNull LiveData getState() {
+ return internalState;
+ }
+
+ interface Repository {
+ void getInsightsData(Consumer insecurePercentConsumer);
+ void getUserAvatar(@NonNull Consumer userAvatarConsumer);
+ }
+
+ final static class Factory implements ViewModelProvider.Factory {
+
+ private final Repository repository;
+
+ Factory(@NonNull Repository repository) {
+ this.repository = repository;
+ }
+
+ @NonNull
+ @Override
+ public T create(@NonNull Class modelClass) {
+ return (T) new InsightsModalViewModel(repository);
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/insights/InsightsOptOut.java b/src/org/thoughtcrime/securesms/insights/InsightsOptOut.java
new file mode 100644
index 0000000000..80a9e398ac
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/insights/InsightsOptOut.java
@@ -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);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/insights/InsightsRepository.java b/src/org/thoughtcrime/securesms/insights/InsightsRepository.java
new file mode 100644
index 0000000000..281767a216
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/insights/InsightsRepository.java
@@ -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 insightsDataConsumer) {
+ SimpleTask.run(() -> {
+ RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
+ List unregisteredRecipients = recipientDatabase.getNotRegisteredForInsights();
+ List 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> insecureRecipientsConsumer) {
+ SimpleTask.run(() -> {
+ RecipientDatabase recipientDatabase = DatabaseFactory.getRecipientDatabase(context);
+ List unregisteredRecipients = recipientDatabase.getUninvitedRecipientsForInsights();
+
+ return Stream.of(unregisteredRecipients)
+ .map(Recipient::resolved)
+ .toList();
+ },
+ insecureRecipientsConsumer::accept);
+ }
+
+ @Override
+ public void getUserAvatar(@NonNull Consumer 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());
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/insights/InsightsUserAvatar.java b/src/org/thoughtcrime/securesms/insights/InsightsUserAvatar.java
new file mode 100644
index 0000000000..d9e7caa891
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/insights/InsightsUserAvatar.java
@@ -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);
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/invites/InviteReminderModel.java b/src/org/thoughtcrime/securesms/invites/InviteReminderModel.java
new file mode 100644
index 0000000000..a92de5efe6
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/invites/InviteReminderModel.java
@@ -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 = 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 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);
+ }
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/invites/InviteReminderRepository.java b/src/org/thoughtcrime/securesms/invites/InviteReminderRepository.java
new file mode 100644
index 0000000000..c57bc374c3
--- /dev/null
+++ b/src/org/thoughtcrime/securesms/invites/InviteReminderRepository.java
@@ -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 registeredRecipients = recipientDatabase.getRegisteredForInsights();
+ List 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)));
+ }
+}
diff --git a/src/org/thoughtcrime/securesms/recipients/Recipient.java b/src/org/thoughtcrime/securesms/recipients/Recipient.java
index b4ceaac77e..2bdfe8d564 100644
--- a/src/org/thoughtcrime/securesms/recipients/Recipient.java
+++ b/src/org/thoughtcrime/securesms/recipients/Recipient.java
@@ -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 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() {
diff --git a/src/org/thoughtcrime/securesms/recipients/RecipientDetails.java b/src/org/thoughtcrime/securesms/recipients/RecipientDetails.java
index afd9ac7a9c..767ca1c458 100644
--- a/src/org/thoughtcrime/securesms/recipients/RecipientDetails.java
+++ b/src/org/thoughtcrime/securesms/recipients/RecipientDetails.java
@@ -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 participants;
final String profileName;
- final boolean seenInviteReminder;
final Optional 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;