Loading app/src/main/AndroidManifest.xml +9 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2025 E FOUNDATION Copyright (C) 2022 ECORP, 2022 MURENA SAS This program is free software: you can redistribute it and/or modify Loading Loading @@ -57,6 +58,14 @@ android:enabled="true" android:exported="true" /> <provider android:name=".externalinterfaces.contentproviders.ScreenshotsProvider" android:authorities="foundation.e.advancedprivacy.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/fileprovider_paths" /> </provider> <receiver android:name="foundation.e.advancedprivacy.common.BootCompletedReceiver" Loading app/src/main/java/foundation/e/advancedprivacy/externalinterfaces/contentproviders/ScreenshotsProvider.kt 0 → 100644 +57 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 E FOUNDATION * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.advancedprivacy.externalinterfaces.contentproviders import android.content.Context import android.content.Intent import android.graphics.Bitmap import androidx.core.content.FileProvider import androidx.core.content.FileProvider.getUriForFile import java.io.File // Subclass (empty) of FileProvider, as advised in documentation // https://developer.android.com/reference/androidx/core/content/FileProvider : // "It is possible to use FileProvider directly instead of extending it. However, this is // not reliable and will causes crashes on some devices." class ScreenshotsProvider : FileProvider() { companion object { const val AUTHORITY = "foundation.e.advancedprivacy.fileprovider" private const val PATH_WEEKLYREPORTS = "weeklyreports" fun buildSendIntent(context: Context, bmp: Bitmap, message: String): Intent { val baseDir = File(context.cacheDir, PATH_WEEKLYREPORTS) baseDir.mkdirs() val screenshotOutputFile = File(baseDir, "lastreport.png") screenshotOutputFile.delete() val screenshotOutputStream = screenshotOutputFile.outputStream() bmp.compress(Bitmap.CompressFormat.PNG, 100, screenshotOutputStream) screenshotOutputStream.close() val contentUri = getUriForFile(context, AUTHORITY, screenshotOutputFile) return Intent().apply { action = Intent.ACTION_SEND flags = Intent.FLAG_GRANT_READ_URI_PERMISSION setDataAndType(contentUri, "image/png") putExtra(Intent.EXTRA_TEXT, message) putExtra(Intent.EXTRA_STREAM, contentUri) } } } } app/src/main/java/foundation/e/advancedprivacy/features/debug/DebugWeeklyReportFragment.kt +17 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package foundation.e.advancedprivacy.features.debug import android.content.Intent import android.os.Bundle import android.util.Log import android.view.LayoutInflater Loading @@ -37,6 +38,7 @@ import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableRepo import foundation.e.advancedprivacy.domain.entities.weeklyreport.WeeklyReport import foundation.e.advancedprivacy.domain.entities.weeklyreport.WeeklyReportScore import foundation.e.advancedprivacy.domain.usecases.WeeklyReportUseCase import foundation.e.advancedprivacy.externalinterfaces.contentproviders.ScreenshotsProvider import foundation.e.advancedprivacy.features.weeklyreport.WeeklyReportViewFactory import foundation.e.advancedprivacy.trackers.data.TrackersRepository import java.time.Instant Loading Loading @@ -91,6 +93,21 @@ class DebugWeeklyReportFragment : Fragment(R.layout.debug_weekly_report_fragment } }" } holder.binding.share.setOnClickListener { val bmp = report.first?.let { reportsFactory.createShareBmp(requireContext(), it) } if (bmp == null) return@setOnClickListener val sendIntent = ScreenshotsProvider.buildSendIntent( requireContext(), bmp, getString(R.string.weeklyreport_share_message) + " " + getString(R.string.weeklyreport_share_privacy_guide) ) startActivity(Intent.createChooser(sendIntent, null)) } } } Loading app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt +25 −10 Original line number Diff line number Diff line Loading @@ -43,6 +43,8 @@ import foundation.e.advancedprivacy.common.extensions.findViewHolderForAdapterPo import foundation.e.advancedprivacy.common.extensions.safeNavigate import foundation.e.advancedprivacy.common.extensions.updatePagerHeightForChild import foundation.e.advancedprivacy.databinding.FragmentTrackersBinding import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport import foundation.e.advancedprivacy.externalinterfaces.contentproviders.ScreenshotsProvider import foundation.e.advancedprivacy.features.weeklyreport.WeeklyReportViewFactory import kotlin.getValue import kotlinx.coroutines.flow.SharedFlow Loading Loading @@ -82,6 +84,19 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { } }) binding.weeklyreportShareBtn.setOnClickListener { viewModel.state.value?.let { report -> val bmp = weeklyReportViewFactory.createShareBmp(requireContext(), report) val sendIntent = ScreenshotsProvider.buildSendIntent( requireContext(), bmp, getString(R.string.weeklyreport_share_message) + " " + getString(R.string.weeklyreport_share_privacy_guide) ) startActivity(Intent.createChooser(sendIntent, null)) } } listenViewModel() } Loading Loading @@ -112,16 +127,7 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.state.collect { report -> if (report != null) { weeklyReportViewFactory.createView(report, layoutInflater, binding.weeklyreportContainer)?.let { binding.weeklyreportContainer.addView(it) binding.weeklyreportContainer.isVisible = true } } else { binding.weeklyreportContainer.isVisible = false } } viewModel.state.collect(::render) } } Loading Loading @@ -192,6 +198,15 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { } } private fun render(report: DisplayableReport?) { binding.weeklyreport.isVisible = report != null if (report != null) { binding.weeklyreportContainer.addView(weeklyReportViewFactory.createView(report, layoutInflater, binding.weeklyreportContainer)) binding.weeklyreportShareTitle.text = weeklyReportViewFactory.getShareTitle(report) } } private fun displayToast(message: String) { Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() } Loading app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/WeeklyReportViewFactory.kt +50 −2 Original line number Diff line number Diff line Loading @@ -17,23 +17,71 @@ package foundation.e.advancedprivacy.features.weeklyreport import android.content.Context import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.drawable.Drawable import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View import android.view.View.MeasureSpec import android.view.ViewGroup import foundation.e.advancedprivacy.databinding.WeeklyReportItemTextBinding import foundation.e.advancedprivacy.databinding.WeeklyReportShareTemplateBinding import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport class WeeklyReportViewFactory() { fun createView(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup): View? { fun getShareTitle(report: DisplayableReport): CharSequence { return report.report.statType.name } fun createView(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup): View { val binding = WeeklyReportItemTextBinding.inflate(inflater, viewGroup, false) binding.title.text = getTitle(report) binding.title.text = report.report.labelId.name binding.description.text = getDescription(report) return binding.root } fun createShareBmp(context: Context, report: DisplayableReport): Bitmap { val screenshotWidthPx = 1080 // expected Meta width, 360dp with density x3.0 val screenshotDensity = DisplayMetrics.DENSITY_XXHIGH // x3.0 val screenshotFontScale = 1f // base value, 1sp == 1dp val screenshotUiMode = Configuration.UI_MODE_NIGHT_NO // Light mode. val configuration = context.resources.configuration configuration.uiMode = screenshotUiMode configuration.densityDpi = screenshotDensity configuration.fontScale = screenshotFontScale val screenshotContext = context.createConfigurationContext(configuration) val layoutInflater = LayoutInflater.from(screenshotContext) val shareTemplateBinding = WeeklyReportShareTemplateBinding.inflate(layoutInflater) shareTemplateBinding.container.addView( createView(report, layoutInflater, shareTemplateBinding.container) ) val view = shareTemplateBinding.root view.measure( MeasureSpec.makeMeasureSpec(screenshotWidthPx, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) ) view.layout(0, 0, view.measuredWidth, view.measuredHeight) val bmp = Bitmap.createBitmap( view.measuredWidth, view.measuredHeight, Bitmap.Config.ARGB_8888 ) val c = Canvas(bmp) view.draw(c) return bmp } private fun getTitle(report: DisplayableReport): CharSequence { return report.report.labelId.name } Loading Loading
app/src/main/AndroidManifest.xml +9 −0 Original line number Diff line number Diff line <?xml version="1.0" encoding="utf-8"?> <!-- Copyright (C) 2025 E FOUNDATION Copyright (C) 2022 ECORP, 2022 MURENA SAS This program is free software: you can redistribute it and/or modify Loading Loading @@ -57,6 +58,14 @@ android:enabled="true" android:exported="true" /> <provider android:name=".externalinterfaces.contentproviders.ScreenshotsProvider" android:authorities="foundation.e.advancedprivacy.fileprovider" android:exported="false" android:grantUriPermissions="true"> <meta-data android:name="android.support.FILE_PROVIDER_PATHS" android:resource="@xml/fileprovider_paths" /> </provider> <receiver android:name="foundation.e.advancedprivacy.common.BootCompletedReceiver" Loading
app/src/main/java/foundation/e/advancedprivacy/externalinterfaces/contentproviders/ScreenshotsProvider.kt 0 → 100644 +57 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 E FOUNDATION * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ package foundation.e.advancedprivacy.externalinterfaces.contentproviders import android.content.Context import android.content.Intent import android.graphics.Bitmap import androidx.core.content.FileProvider import androidx.core.content.FileProvider.getUriForFile import java.io.File // Subclass (empty) of FileProvider, as advised in documentation // https://developer.android.com/reference/androidx/core/content/FileProvider : // "It is possible to use FileProvider directly instead of extending it. However, this is // not reliable and will causes crashes on some devices." class ScreenshotsProvider : FileProvider() { companion object { const val AUTHORITY = "foundation.e.advancedprivacy.fileprovider" private const val PATH_WEEKLYREPORTS = "weeklyreports" fun buildSendIntent(context: Context, bmp: Bitmap, message: String): Intent { val baseDir = File(context.cacheDir, PATH_WEEKLYREPORTS) baseDir.mkdirs() val screenshotOutputFile = File(baseDir, "lastreport.png") screenshotOutputFile.delete() val screenshotOutputStream = screenshotOutputFile.outputStream() bmp.compress(Bitmap.CompressFormat.PNG, 100, screenshotOutputStream) screenshotOutputStream.close() val contentUri = getUriForFile(context, AUTHORITY, screenshotOutputFile) return Intent().apply { action = Intent.ACTION_SEND flags = Intent.FLAG_GRANT_READ_URI_PERMISSION setDataAndType(contentUri, "image/png") putExtra(Intent.EXTRA_TEXT, message) putExtra(Intent.EXTRA_STREAM, contentUri) } } } }
app/src/main/java/foundation/e/advancedprivacy/features/debug/DebugWeeklyReportFragment.kt +17 −0 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package foundation.e.advancedprivacy.features.debug import android.content.Intent import android.os.Bundle import android.util.Log import android.view.LayoutInflater Loading @@ -37,6 +38,7 @@ import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableRepo import foundation.e.advancedprivacy.domain.entities.weeklyreport.WeeklyReport import foundation.e.advancedprivacy.domain.entities.weeklyreport.WeeklyReportScore import foundation.e.advancedprivacy.domain.usecases.WeeklyReportUseCase import foundation.e.advancedprivacy.externalinterfaces.contentproviders.ScreenshotsProvider import foundation.e.advancedprivacy.features.weeklyreport.WeeklyReportViewFactory import foundation.e.advancedprivacy.trackers.data.TrackersRepository import java.time.Instant Loading Loading @@ -91,6 +93,21 @@ class DebugWeeklyReportFragment : Fragment(R.layout.debug_weekly_report_fragment } }" } holder.binding.share.setOnClickListener { val bmp = report.first?.let { reportsFactory.createShareBmp(requireContext(), it) } if (bmp == null) return@setOnClickListener val sendIntent = ScreenshotsProvider.buildSendIntent( requireContext(), bmp, getString(R.string.weeklyreport_share_message) + " " + getString(R.string.weeklyreport_share_privacy_guide) ) startActivity(Intent.createChooser(sendIntent, null)) } } } Loading
app/src/main/java/foundation/e/advancedprivacy/features/trackers/TrackersFragment.kt +25 −10 Original line number Diff line number Diff line Loading @@ -43,6 +43,8 @@ import foundation.e.advancedprivacy.common.extensions.findViewHolderForAdapterPo import foundation.e.advancedprivacy.common.extensions.safeNavigate import foundation.e.advancedprivacy.common.extensions.updatePagerHeightForChild import foundation.e.advancedprivacy.databinding.FragmentTrackersBinding import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport import foundation.e.advancedprivacy.externalinterfaces.contentproviders.ScreenshotsProvider import foundation.e.advancedprivacy.features.weeklyreport.WeeklyReportViewFactory import kotlin.getValue import kotlinx.coroutines.flow.SharedFlow Loading Loading @@ -82,6 +84,19 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { } }) binding.weeklyreportShareBtn.setOnClickListener { viewModel.state.value?.let { report -> val bmp = weeklyReportViewFactory.createShareBmp(requireContext(), report) val sendIntent = ScreenshotsProvider.buildSendIntent( requireContext(), bmp, getString(R.string.weeklyreport_share_message) + " " + getString(R.string.weeklyreport_share_privacy_guide) ) startActivity(Intent.createChooser(sendIntent, null)) } } listenViewModel() } Loading Loading @@ -112,16 +127,7 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.state.collect { report -> if (report != null) { weeklyReportViewFactory.createView(report, layoutInflater, binding.weeklyreportContainer)?.let { binding.weeklyreportContainer.addView(it) binding.weeklyreportContainer.isVisible = true } } else { binding.weeklyreportContainer.isVisible = false } } viewModel.state.collect(::render) } } Loading Loading @@ -192,6 +198,15 @@ class TrackersFragment : NavToolbarFragment(R.layout.fragment_trackers) { } } private fun render(report: DisplayableReport?) { binding.weeklyreport.isVisible = report != null if (report != null) { binding.weeklyreportContainer.addView(weeklyReportViewFactory.createView(report, layoutInflater, binding.weeklyreportContainer)) binding.weeklyreportShareTitle.text = weeklyReportViewFactory.getShareTitle(report) } } private fun displayToast(message: String) { Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() } Loading
app/src/main/java/foundation/e/advancedprivacy/features/weeklyreport/WeeklyReportViewFactory.kt +50 −2 Original line number Diff line number Diff line Loading @@ -17,23 +17,71 @@ package foundation.e.advancedprivacy.features.weeklyreport import android.content.Context import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.drawable.Drawable import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View import android.view.View.MeasureSpec import android.view.ViewGroup import foundation.e.advancedprivacy.databinding.WeeklyReportItemTextBinding import foundation.e.advancedprivacy.databinding.WeeklyReportShareTemplateBinding import foundation.e.advancedprivacy.domain.entities.weeklyreport.DisplayableReport class WeeklyReportViewFactory() { fun createView(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup): View? { fun getShareTitle(report: DisplayableReport): CharSequence { return report.report.statType.name } fun createView(report: DisplayableReport, inflater: LayoutInflater, viewGroup: ViewGroup): View { val binding = WeeklyReportItemTextBinding.inflate(inflater, viewGroup, false) binding.title.text = getTitle(report) binding.title.text = report.report.labelId.name binding.description.text = getDescription(report) return binding.root } fun createShareBmp(context: Context, report: DisplayableReport): Bitmap { val screenshotWidthPx = 1080 // expected Meta width, 360dp with density x3.0 val screenshotDensity = DisplayMetrics.DENSITY_XXHIGH // x3.0 val screenshotFontScale = 1f // base value, 1sp == 1dp val screenshotUiMode = Configuration.UI_MODE_NIGHT_NO // Light mode. val configuration = context.resources.configuration configuration.uiMode = screenshotUiMode configuration.densityDpi = screenshotDensity configuration.fontScale = screenshotFontScale val screenshotContext = context.createConfigurationContext(configuration) val layoutInflater = LayoutInflater.from(screenshotContext) val shareTemplateBinding = WeeklyReportShareTemplateBinding.inflate(layoutInflater) shareTemplateBinding.container.addView( createView(report, layoutInflater, shareTemplateBinding.container) ) val view = shareTemplateBinding.root view.measure( MeasureSpec.makeMeasureSpec(screenshotWidthPx, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED) ) view.layout(0, 0, view.measuredWidth, view.measuredHeight) val bmp = Bitmap.createBitmap( view.measuredWidth, view.measuredHeight, Bitmap.Config.ARGB_8888 ) val c = Canvas(bmp) view.draw(c) return bmp } private fun getTitle(report: DisplayableReport): CharSequence { return report.report.labelId.name } Loading