From 8ba860f6c32a9dba0c796bc031db19052655ddc8 Mon Sep 17 00:00:00 2001 From: "FC (Fay) Stegerman" Date: Tue, 29 Aug 2023 19:57:49 +0000 Subject: [PATCH] embed history/privacy policy/license in app (#1346) --- PRIVACY.md | 18 + app/build.gradle | 15 + .../protect/card_locker/AboutActivity.java | 61 +- .../protect/card_locker/AboutContent.java | 45 ++ .../main/java/protect/card_locker/Utils.java | 52 +- app/src/main/res/raw/.gitignore | 2 + app/src/main/res/raw/license.html | 696 ++++++++++++++++++ app/src/main/res/values/strings.xml | 1 + 8 files changed, 875 insertions(+), 15 deletions(-) create mode 100644 PRIVACY.md create mode 100644 app/src/main/res/raw/.gitignore create mode 100644 app/src/main/res/raw/license.html diff --git a/PRIVACY.md b/PRIVACY.md new file mode 100644 index 000000000..29aa0909d --- /dev/null +++ b/PRIVACY.md @@ -0,0 +1,18 @@ +**Last updated** +August 30 2023 + +# Privacy Policy +Catima does not collect or transmit any personal information. + +To ensure correct app functionality, we require access to the following: + +- Camera: We need access to your camera to be able to scan barcodes. The app can still be used when camera access is denied, but you will have to manually type the barcode information. +- Storage (Android 5 and 6 only): We need access to your device storage to create or import backups. The app can still be used when storage access is denied, but you will not be able to create or import backups. + +Catima offers a feature to share cards with other users. All the relevant data is in the generated shareable URLs and never transmitted to our servers. When viewed through catima.app, the data in the URL is rendered using client-side Javascript to further ensure no data is ever transmitted to us. + +# Changes +This Privacy Policy may be updated from time to time for any reason. We will notify you of any changes to our Privacy Policy by posting the new Privacy Policy to https://catima.app/privacy-policy/. A snapshot of the Privacy Policy is available within the Catima app, though it may be outdated. When the Privacy Policy on the website and in the app differ, the website should be considered leading. You are advised to consult the Privacy Policy regularly for any changes, as continued use is deemed approval of all changes. + +# Contact us +If you have any questions regarding privacy while using the Application, or have questions about our practices, please contact us via email at catima.g9ex3@hackerchick.me. diff --git a/app/build.gradle b/app/build.gradle index ca4e1df9c..1758fc5dd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -132,3 +132,18 @@ tasks.withType(SpotBugsTask) { html.enabled = true } } + +tasks.register('copyRawResFiles', Copy) { + from layout.projectDirectory.file("../CHANGELOG.md"), + layout.projectDirectory.file("../PRIVACY.md") + into layout.projectDirectory.dir("src/main/res/raw") + rename { String fileName -> fileName.toLowerCase() } +} + +project.afterEvaluate { + tasks.each { task -> + if (task != copyRawResFiles) { + task.dependsOn(copyRawResFiles) + } + } +} diff --git a/app/src/main/java/protect/card_locker/AboutActivity.java b/app/src/main/java/protect/card_locker/AboutActivity.java index 5fecdc320..6a7c70031 100644 --- a/app/src/main/java/protect/card_locker/AboutActivity.java +++ b/app/src/main/java/protect/card_locker/AboutActivity.java @@ -1,10 +1,14 @@ package protect.card_locker; import android.os.Bundle; +import android.text.Spanned; import android.view.MenuItem; import android.view.View; +import android.widget.ScrollView; import android.widget.TextView; +import androidx.annotation.StringRes; + import com.google.android.material.dialog.MaterialAlertDialogBuilder; @@ -68,20 +72,14 @@ public class AboutActivity extends CatimaAppCompatActivity { } private void bindClickListeners() { - View.OnClickListener openExternalBrowser = view -> { - Object tag = view.getTag(); - if (tag instanceof String && ((String) tag).startsWith("https://")) { - (new OpenWebLinkHandler()).openBrowser(this, (String) tag); - } - }; - binding.versionHistory.setOnClickListener(openExternalBrowser); - binding.translate.setOnClickListener(openExternalBrowser); - binding.license.setOnClickListener(openExternalBrowser); - binding.repo.setOnClickListener(openExternalBrowser); - binding.privacy.setOnClickListener(openExternalBrowser); - binding.reportError.setOnClickListener(openExternalBrowser); - binding.rate.setOnClickListener(openExternalBrowser); - binding.donate.setOnClickListener(openExternalBrowser); + binding.versionHistory.setOnClickListener(this::showHistory); + binding.translate.setOnClickListener(this::openExternalBrowser); + binding.license.setOnClickListener(this::showLicense); + binding.repo.setOnClickListener(this::openExternalBrowser); + binding.privacy.setOnClickListener(this::showPrivacy); + binding.reportError.setOnClickListener(this::openExternalBrowser); + binding.rate.setOnClickListener(this::openExternalBrowser); + binding.donate.setOnClickListener(this::openExternalBrowser); binding.credits.setOnClickListener(view -> showCredits()); } @@ -106,4 +104,39 @@ public class AboutActivity extends CatimaAppCompatActivity { .setPositiveButton(R.string.ok, null) .show(); } + + private void showHistory(View view) { + showHTML(R.string.version_history, content.getHistoryInfo(), view); + } + + private void showLicense(View view) { + showHTML(R.string.license, content.getLicenseInfo(), view); + } + + private void showPrivacy(View view) { + showHTML(R.string.privacy_policy, content.getPrivacyInfo(), view); + } + + private void showHTML(@StringRes int title, final Spanned text, View view) { + int dialogContentPadding = getResources().getDimensionPixelSize(R.dimen.alert_dialog_content_padding); + TextView textView = new TextView(this); + textView.setText(text); + Utils.makeTextViewLinksClickable(textView, text); + ScrollView scrollView = new ScrollView(this); + scrollView.addView(textView); + scrollView.setPadding(dialogContentPadding, dialogContentPadding / 2, dialogContentPadding, 0); + new MaterialAlertDialogBuilder(this) + .setTitle(title) + .setView(scrollView) + .setPositiveButton(R.string.ok, null) + .setNeutralButton(R.string.view_online, (dialog, which) -> openExternalBrowser(view)) + .show(); + } + + private void openExternalBrowser(View view) { + Object tag = view.getTag(); + if (tag instanceof String && ((String) tag).startsWith("https://")) { + (new OpenWebLinkHandler()).openBrowser(this, (String) tag); + } + } } diff --git a/app/src/main/java/protect/card_locker/AboutContent.java b/app/src/main/java/protect/card_locker/AboutContent.java index 95270bf14..005ec7d3b 100644 --- a/app/src/main/java/protect/card_locker/AboutContent.java +++ b/app/src/main/java/protect/card_locker/AboutContent.java @@ -3,6 +3,7 @@ package protect.card_locker; import android.content.Context; import android.content.pm.PackageInfo; import android.content.pm.PackageManager; +import android.text.Spanned; import android.util.Log; import androidx.core.text.HtmlCompat; @@ -64,6 +65,38 @@ public class AboutContent { return contributors.replace("\n", "
"); } + public String getHistory() { + String versionHistory; + try { + versionHistory = Utils.readTextFile(context, R.raw.changelog) + .replace("# Changelog\n\n", ""); + } catch (IOException ignored) { + return ""; + } + return Utils.linkify(Utils.basicMDToHTML(versionHistory)) + .replace("\n", "
"); + } + + public String getLicense() { + try { + return Utils.readTextFile(context, R.raw.license); + } catch (IOException ignored) { + return ""; + } + } + + public String getPrivacy() { + String privacyPolicy; + try { + privacyPolicy = Utils.readTextFile(context, R.raw.privacy) + .replace("# Privacy Policy\n", ""); + } catch (IOException ignored) { + return ""; + } + return Utils.linkify(Utils.basicMDToHTML(privacyPolicy)) + .replace("\n", "
"); + } + public String getThirdPartyLibraries() { final List usedLibraries = new ArrayList<>(); usedLibraries.add(new ThirdPartyInfo("Color Picker", "https://github.com/jaredrummler/ColorPicker", "Apache 2.0")); @@ -111,6 +144,18 @@ public class AboutContent { return contributorInfo.toString(); } + public Spanned getHistoryInfo() { + return HtmlCompat.fromHtml(getHistory(), HtmlCompat.FROM_HTML_MODE_COMPACT); + } + + public Spanned getLicenseInfo() { + return HtmlCompat.fromHtml(getLicense(), HtmlCompat.FROM_HTML_MODE_LEGACY); + } + + public Spanned getPrivacyInfo() { + return HtmlCompat.fromHtml(getPrivacy(), HtmlCompat.FROM_HTML_MODE_COMPACT); + } + public String getVersionHistory() { return String.format(context.getString(R.string.debug_version_fmt), getAppVersion()); } diff --git a/app/src/main/java/protect/card_locker/Utils.java b/app/src/main/java/protect/card_locker/Utils.java index 7d1afaf31..304b6baef 100644 --- a/app/src/main/java/protect/card_locker/Utils.java +++ b/app/src/main/java/protect/card_locker/Utils.java @@ -1,5 +1,6 @@ package protect.card_locker; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.Intent; @@ -14,8 +15,12 @@ import android.graphics.Matrix; import android.net.Uri; import android.os.Build; import android.provider.MediaStore; +import android.text.Layout; +import android.text.Spanned; +import android.text.style.ClickableSpan; import android.util.Log; import android.util.TypedValue; +import android.view.MotionEvent; import android.view.View; import android.widget.ImageView; import android.widget.TextView; @@ -688,7 +693,7 @@ public class Utils { while (true) { String nextLine = reader.readLine(); - if (nextLine == null || nextLine.isEmpty()) { + if (nextLine == null) { reader.close(); break; } @@ -700,6 +705,28 @@ public class Utils { return result.toString(); } + // Very crude Markdown to HTML conversion. + // Only supports what's currently being used in CHANGELOG.md and PRIVACY.md. + // May break easily. + public static String basicMDToHTML(final String input) { + return input + .replaceAll("(?m)^#\\s+(.*)", "

$1

") + .replaceAll("(?m)^##\\s+(.*)", "

$1

") + .replaceAll("\\[([^]]+)\\]\\((https?://[\\w@#%&+=:?/.-]+)\\)", "$1") + .replaceAll("\\*\\*([^*]+)\\*\\*", "$1") + .replaceAll("(?m)^-\\s+(.*)", "") + .replace("\n