1818package net.kollnig.missioncontrol
1919
2020import android.animation.ValueAnimator
21+ import android.content.Intent
22+ import android.graphics.Bitmap
23+ import android.graphics.Canvas
24+ import android.graphics.Color
25+ import android.graphics.Typeface
2126import android.graphics.drawable.GradientDrawable
2227import android.os.Bundle
28+ import android.util.Log
2329import android.util.Pair
2430import android.util.TypedValue
2531import android.view.Gravity
32+ import android.view.LayoutInflater
2633import android.view.MenuItem
2734import android.view.View
2835import android.view.animation.DecelerateInterpolator
2936import android.widget.LinearLayout
3037import android.widget.ProgressBar
3138import android.widget.TextView
39+ import android.widget.Toast
3240import androidx.appcompat.app.AppCompatActivity
3341import androidx.core.content.ContextCompat
42+ import androidx.core.content.FileProvider
3443import androidx.lifecycle.lifecycleScope
44+ import com.google.android.material.floatingactionbutton.FloatingActionButton
45+ import eu.faircode.netguard.Util
3546import kotlinx.coroutines.Dispatchers
3647import kotlinx.coroutines.launch
3748import kotlinx.coroutines.withContext
3849import net.kollnig.missioncontrol.data.InsightsData
3950import net.kollnig.missioncontrol.data.InsightsDataProvider
51+ import java.io.File
52+ import java.io.FileOutputStream
4053import java.text.NumberFormat
4154import java.util.Locale
4255
@@ -59,9 +72,12 @@ class InsightsActivity : AppCompatActivity() {
5972 private lateinit var llPervasiveTrackers: LinearLayout
6073 private lateinit var llTopDomains: LinearLayout
6174 private lateinit var llNoData: LinearLayout
75+ private lateinit var llBlockedAllowed: LinearLayout
6276 private lateinit var progressBar: ProgressBar
77+ private lateinit var fabShare: FloatingActionButton
6378
6479 private lateinit var dataProvider: InsightsDataProvider
80+ private var currentData: InsightsData ? = null
6581
6682 override fun onCreate (savedInstanceState : Bundle ? ) {
6783 super .onCreate(savedInstanceState)
@@ -87,7 +103,13 @@ class InsightsActivity : AppCompatActivity() {
87103 llPervasiveTrackers = findViewById(R .id.llPervasiveTrackers)
88104 llTopDomains = findViewById(R .id.llTopDomains)
89105 llNoData = findViewById(R .id.llNoData)
106+ llBlockedAllowed = findViewById(R .id.llBlockedAllowed)
90107 progressBar = findViewById(R .id.progressBar)
108+ fabShare = findViewById(R .id.fabShare)
109+
110+ fabShare.setOnClickListener {
111+ shareInsights()
112+ }
91113
92114 dataProvider = InsightsDataProvider (this )
93115
@@ -119,20 +141,34 @@ class InsightsActivity : AppCompatActivity() {
119141 private fun displayData (data : InsightsData ) {
120142 if (! data.hasData()) {
121143 llNoData.visibility = View .VISIBLE
144+ fabShare.visibility = View .GONE
122145 return
123146 }
124147
125148 llNoData.visibility = View .GONE
149+ currentData = data
150+ fabShare.visibility = View .VISIBLE
126151
127152 // Animate main number
128153 animateNumber(tvTotalAttempts, 0 , data.totalTrackingAttempts)
129154
130- // Set shocking fact
131- tvShockingFact.text = getString(
132- R .string.insights_shocking_fact,
133- data.uniqueTrackerCompanies,
134- data.totalTrackingAttempts
135- )
155+ // Hide blocked/allowed stats for Play Store version
156+ val isPlayStore = Util .isPlayStoreInstall()
157+ llBlockedAllowed.visibility = if (isPlayStore) View .GONE else View .VISIBLE
158+
159+ // Set shocking fact - different text for Play Store
160+ if (isPlayStore) {
161+ tvShockingFact.text = getString(
162+ R .string.insights_shocking_fact_playstore,
163+ data.uniqueTrackerCompanies
164+ )
165+ } else {
166+ tvShockingFact.text = getString(
167+ R .string.insights_shocking_fact,
168+ data.uniqueTrackerCompanies,
169+ data.totalTrackingAttempts
170+ )
171+ }
136172
137173 // Blocked/Allowed stats
138174 animateNumber(tvBlockedCount, 0 , data.blockedTrackingAttempts)
@@ -247,5 +283,146 @@ class InsightsActivity : AppCompatActivity() {
247283 container.addView(row)
248284 }
249285 }
286+
287+ /* *
288+ * Share privacy insights as an image with text.
289+ */
290+ private fun shareInsights () {
291+ val data = currentData ? : return
292+
293+ lifecycleScope.launch {
294+ try {
295+ val imageFile = withContext(Dispatchers .IO ) {
296+ generateShareImage(data)
297+ }
298+
299+ if (imageFile != null ) {
300+ val uri = FileProvider .getUriForFile(
301+ this @InsightsActivity,
302+ " ${packageName} .provider" ,
303+ imageFile
304+ )
305+
306+ val isPlayStore = Util .isPlayStoreInstall()
307+ val shareMsgRes = if (isPlayStore) R .string.insights_share_message_playstore else R .string.insights_share_message
308+
309+ val shareText = getString(
310+ shareMsgRes,
311+ if (isPlayStore) data.totalTrackingAttempts else data.blockedTrackingAttempts,
312+ data.uniqueTrackerCompanies
313+ )
314+
315+ val intent = Intent (Intent .ACTION_SEND ).apply {
316+ type = " image/png"
317+ putExtra(Intent .EXTRA_STREAM , uri)
318+ putExtra(Intent .EXTRA_TEXT , shareText)
319+ addFlags(Intent .FLAG_GRANT_READ_URI_PERMISSION )
320+ }
321+
322+ startActivity(Intent .createChooser(intent, getString(R .string.insights_share)))
323+ }
324+ } catch (e: Exception ) {
325+ Log .e(TAG , " Failed to share insights" , e)
326+ Toast .makeText(this @InsightsActivity, R .string.export_failed, Toast .LENGTH_SHORT ).show()
327+ }
328+ }
329+ }
330+
331+ private fun generateShareImage (data : InsightsData ): File ? {
332+ return try {
333+ // Inflate and populate view
334+ val inflater = LayoutInflater .from(this )
335+ val shareView = inflater.inflate(R .layout.layout_insights_share, null )
336+
337+ val tvTotalBlocked = shareView.findViewById<TextView >(R .id.tvShareTotalBlocked)
338+ val llBlockedStat = shareView.findViewById<LinearLayout >(R .id.llShareBlockedStat)
339+ val tvBlockedCount = shareView.findViewById<TextView >(R .id.tvShareBlockedCount)
340+ val tvCompanies = shareView.findViewById<TextView >(R .id.tvShareCompanies)
341+ val llTopCompanies = shareView.findViewById<LinearLayout >(R .id.llShareTopCompanies)
342+
343+ val nf = NumberFormat .getNumberInstance(Locale .getDefault())
344+
345+ // Hero stat: Total Hosts
346+ tvTotalBlocked.text = nf.format(data.totalTrackingAttempts)
347+
348+ // Blocked stat: hide on Play Store
349+ if (Util .isPlayStoreInstall()) {
350+ llBlockedStat.visibility = View .GONE
351+ } else {
352+ llBlockedStat.visibility = View .VISIBLE
353+ tvBlockedCount.text = nf.format(data.blockedTrackingAttempts)
354+ }
355+
356+ // Companies count
357+ tvCompanies.text = data.uniqueTrackerCompanies.toString()
358+
359+ // Top 3 Companies (dynamically added)
360+ val top3 = data.topTrackerCompanies.take(3 )
361+ val density = resources.displayMetrics.density
362+
363+ for (company in top3) {
364+ val row = LinearLayout (this ).apply {
365+ layoutParams = LinearLayout .LayoutParams (
366+ LinearLayout .LayoutParams .MATCH_PARENT ,
367+ LinearLayout .LayoutParams .WRAP_CONTENT
368+ ).apply {
369+ topMargin = (4 * density).toInt()
370+ }
371+ orientation = LinearLayout .HORIZONTAL
372+ }
373+
374+ val nameView = TextView (this ).apply {
375+ layoutParams = LinearLayout .LayoutParams (0 , LinearLayout .LayoutParams .WRAP_CONTENT , 1f )
376+ text = company.first
377+ setTextColor(Color .WHITE )
378+ textSize = 12f
379+ }
380+
381+ val countView = TextView (this ).apply {
382+ layoutParams = LinearLayout .LayoutParams (LinearLayout .LayoutParams .WRAP_CONTENT , LinearLayout .LayoutParams .WRAP_CONTENT )
383+ text = getString(R .string.insights_in_apps, company.second)
384+ setTextColor(Color .WHITE )
385+ textSize = 12f
386+ setTypeface(null , Typeface .BOLD )
387+ }
388+
389+ row.addView(nameView)
390+ row.addView(countView)
391+ llTopCompanies.addView(row)
392+ }
393+
394+ // Measure and layout
395+ val width = TypedValue .applyDimension(TypedValue .COMPLEX_UNIT_DIP , 400f , resources.displayMetrics).toInt()
396+ val widthSpec = View .MeasureSpec .makeMeasureSpec(width, View .MeasureSpec .EXACTLY )
397+ val heightSpec = View .MeasureSpec .makeMeasureSpec(0 , View .MeasureSpec .UNSPECIFIED )
398+
399+ shareView.measure(widthSpec, heightSpec)
400+ shareView.layout(0 , 0 , shareView.measuredWidth, shareView.measuredHeight)
401+
402+ // Draw to bitmap
403+ val bitmap = Bitmap .createBitmap(shareView.measuredWidth, shareView.measuredHeight, Bitmap .Config .ARGB_8888 )
404+ val canvas = Canvas (bitmap)
405+ shareView.draw(canvas)
406+
407+ // Save
408+ val shareDir = File (cacheDir, " share" )
409+ if (! shareDir.exists()) shareDir.mkdirs()
410+
411+ val imageFile = File (shareDir, " trackercontrol_insights.png" )
412+ FileOutputStream (imageFile).use { out ->
413+ bitmap.compress(Bitmap .CompressFormat .PNG , 100 , out )
414+ }
415+ bitmap.recycle()
416+
417+ imageFile
418+ } catch (e: Exception ) {
419+ Log .e(TAG , " Failed to generate share image" , e)
420+ null
421+ }
422+ }
423+
424+ companion object {
425+ private const val TAG = " InsightsActivity"
426+ }
250427}
251428
0 commit comments