Skip to content

Commit 7d6527f

Browse files
committed
improve insights: add to mainactivity and allow export to friends
1 parent 6ed548a commit 7d6527f

11 files changed

Lines changed: 847 additions & 25 deletions

File tree

app/src/main/java/eu/faircode/netguard/ActivityMain.java

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070
import androidx.core.graphics.drawable.DrawableCompat;
7171
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
7272
import androidx.preference.PreferenceManager;
73+
import androidx.recyclerview.widget.ConcatAdapter;
7374
import androidx.recyclerview.widget.LinearLayoutManager;
7475
import androidx.recyclerview.widget.RecyclerView;
7576
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
@@ -78,7 +79,10 @@
7879

7980
import net.kollnig.missioncontrol.ActivityOnboarding;
8081
import net.kollnig.missioncontrol.Common;
82+
import net.kollnig.missioncontrol.InsightsHeaderAdapter;
8183
import net.kollnig.missioncontrol.R;
84+
import net.kollnig.missioncontrol.data.InsightsData;
85+
import net.kollnig.missioncontrol.data.InsightsDataProvider;
8286
import net.kollnig.missioncontrol.data.Tracker;
8387
import net.kollnig.missioncontrol.data.TrackerBlocklist;
8488
import net.kollnig.missioncontrol.data.TrackerList;
@@ -91,6 +95,8 @@
9195
import java.util.Collections;
9296
import java.util.Date;
9397
import java.util.List;
98+
import java.util.concurrent.ExecutorService;
99+
import java.util.concurrent.Executors;
94100

95101
public class ActivityMain extends AppCompatActivity implements SharedPreferences.OnSharedPreferenceChangeListener {
96102
private static final String TAG = "TrackerControl.Main";
@@ -109,6 +115,7 @@ public class ActivityMain extends AppCompatActivity implements SharedPreferences
109115
private SwitchCompat swEnabled;
110116
private ImageView ivMetered;
111117
private SwipeRefreshLayout swipeRefresh;
118+
private InsightsHeaderAdapter headerAdapter;
112119
private AdapterRule adapter = null;
113120
private MenuItem menuSearch = null;
114121
private AlertDialog dialogVpn = null;
@@ -260,11 +267,13 @@ public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
260267
if (!TextUtils.isEmpty(alwaysOn))
261268
if (getPackageName().equals(alwaysOn)) {
262269
if (prefs.getBoolean("filter", true)) {
263-
int lockdown = Settings.Secure.getInt(getContentResolver(), "always_on_vpn_lockdown", 0);
270+
int lockdown = Settings.Secure.getInt(getContentResolver(),
271+
"always_on_vpn_lockdown", 0);
264272
Log.i(TAG, "Lockdown=" + lockdown);
265273
if (lockdown != 0) {
266274
swEnabled.setChecked(false);
267-
Toast.makeText(ActivityMain.this, R.string.msg_always_on_lockdown, Toast.LENGTH_LONG).show();
275+
Toast.makeText(ActivityMain.this, R.string.msg_always_on_lockdown,
276+
Toast.LENGTH_LONG).show();
268277
return;
269278
}
270279
}
@@ -383,7 +392,13 @@ public void onClick(View v) {
383392
llm.setAutoMeasureEnabled(true);
384393
rvApplication.setLayoutManager(llm);
385394
adapter = new AdapterRule(this, findViewById(R.id.vwPopupAnchor));
386-
rvApplication.setAdapter(adapter);
395+
rvApplication.setLayoutManager(llm);
396+
headerAdapter = new InsightsHeaderAdapter(this);
397+
adapter = new AdapterRule(this, findViewById(R.id.vwPopupAnchor));
398+
ConcatAdapter concatAdapter = new ConcatAdapter(headerAdapter, adapter);
399+
rvApplication.setAdapter(concatAdapter);
400+
401+
loadInsightsData();
387402

388403
// Swipe to refresh
389404
swipeRefresh = findViewById(R.id.swipeRefresh);
@@ -393,6 +408,7 @@ public void onRefresh() {
393408
Rule.clearCache(ActivityMain.this);
394409
ServiceSinkhole.reload("pull", ActivityMain.this, false);
395410
updateApplicationList(null);
411+
loadInsightsData();
396412
}
397413
});
398414

@@ -493,6 +509,8 @@ protected void onResume() {
493509
return;
494510
}
495511

512+
loadInsightsData();
513+
496514
DatabaseHelper.getInstance(this).addAccessChangedListener(accessChangedListener);
497515
if (adapter != null)
498516
adapter.notifyDataSetChanged();
@@ -805,8 +823,7 @@ public void onReceive(Context context, Intent intent) {
805823

806824
if (adapter != null)
807825
if (intent.hasExtra(EXTRA_CONNECTED) && intent.hasExtra(EXTRA_METERED)) {
808-
ivIcon.setImageResource(Util.isNetworkActive(ActivityMain.this)
809-
? R.drawable.ic_rocket_white
826+
ivIcon.setImageResource(Util.isNetworkActive(ActivityMain.this) ? R.drawable.ic_rocket_white
810827
: R.drawable.ic_rocket_white_60);
811828
if (intent.getBooleanExtra(EXTRA_CONNECTED, false)) {
812829
if (intent.getBooleanExtra(EXTRA_METERED, false))
@@ -1358,4 +1375,17 @@ private static Intent getIntentSupport(Context context) {
13581375
return new Intent(Intent.ACTION_VIEW,
13591376
Uri.parse("https://github.com/TrackerControl/tracker-control-android#support-trackercontrol"));
13601377
}
1378+
1379+
private void loadInsightsData() {
1380+
ExecutorService executor = Executors.newSingleThreadExecutor();
1381+
executor.execute(() -> {
1382+
InsightsDataProvider provider = new InsightsDataProvider(this);
1383+
InsightsData data = provider.computeInsights();
1384+
runOnUiThread(() -> {
1385+
if (headerAdapter != null) {
1386+
headerAdapter.setData(data);
1387+
}
1388+
});
1389+
});
1390+
}
13611391
}

app/src/main/java/net/kollnig/missioncontrol/InsightsActivity.kt

Lines changed: 183 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,25 +18,38 @@
1818
package net.kollnig.missioncontrol
1919

2020
import 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
2126
import android.graphics.drawable.GradientDrawable
2227
import android.os.Bundle
28+
import android.util.Log
2329
import android.util.Pair
2430
import android.util.TypedValue
2531
import android.view.Gravity
32+
import android.view.LayoutInflater
2633
import android.view.MenuItem
2734
import android.view.View
2835
import android.view.animation.DecelerateInterpolator
2936
import android.widget.LinearLayout
3037
import android.widget.ProgressBar
3138
import android.widget.TextView
39+
import android.widget.Toast
3240
import androidx.appcompat.app.AppCompatActivity
3341
import androidx.core.content.ContextCompat
42+
import androidx.core.content.FileProvider
3443
import androidx.lifecycle.lifecycleScope
44+
import com.google.android.material.floatingactionbutton.FloatingActionButton
45+
import eu.faircode.netguard.Util
3546
import kotlinx.coroutines.Dispatchers
3647
import kotlinx.coroutines.launch
3748
import kotlinx.coroutines.withContext
3849
import net.kollnig.missioncontrol.data.InsightsData
3950
import net.kollnig.missioncontrol.data.InsightsDataProvider
51+
import java.io.File
52+
import java.io.FileOutputStream
4053
import java.text.NumberFormat
4154
import 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

Comments
 (0)