From 3d4c5bd92cf4656d443308e9ce39eeba7bdc5ca3 Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 28 Feb 2026 11:00:15 +0000 Subject: [PATCH] Fix all Play Store blocking issues in Android app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Client ID — no longer hardcoded in build.gradle - build.gradle reads reddit.client_id from local.properties (gitignored) - Falls back to placeholder so the build never breaks without the file - Developers set it once in local.properties; no build file edits needed Release signing - Added signingConfigs.release that reads storeFile / storePassword / keyAlias / keyPassword from keystore.properties (gitignored) - Added buildTypes.release with signingConfig signingConfigs.release - Added buildTypes.debug with .debug applicationId suffix - Updated .gitignore: keystore.properties, *.jks, *.keystore ProGuard / R8 - Release build sets minifyEnabled=true and shrinkResources=true - Created proguard-rules.pro with rules for Retrofit, OkHttp, Gson, Kotlin, EncryptedSharedPreferences, and the app's own model/api packages Adaptive app icon (minSdk 26 — no PNGs required) - res/drawable/ic_launcher_background.xml — Reddit-orange (#FF4500) fill - res/drawable/ic_launcher_foreground.xml — white trash-can icon scaled to the 72×72dp adaptive safe zone inside the 108×108dp canvas - res/mipmap-anydpi-v26/ic_launcher.xml and ic_launcher_round.xml - AndroidManifest now references @mipmap/ic_launcher and roundIcon Network security config - res/xml/network_security_config.xml — cleartextTrafficPermitted=false, trust system CAs only (no user-installed CAs) - AndroidManifest references @xml/network_security_config Privacy policy - android/PRIVACY_POLICY.md — covers scopes, local storage, data-not-collected, and Reddit API third-party disclosure - SETUP.md updated with Play Store listing checklist (host policy URL) Bonus fixes (important tier, trivially co-located) - Removed unused vote scope from OAUTH_SCOPES - Downgraded security-crypto from alpha 1.1.0-alpha06 → stable 1.0.0 - RedditApiClient: logging interceptor now NONE in release, BASIC in debug - RedditApiClient: replaced tokenStorage.refreshToken!! with safe local val to eliminate potential NullPointerException on race condition - AndroidManifest: allowBackup changed from true to false (tokens are sensitive; backup of EncryptedSharedPreferences is unnecessary) - SETUP.md: full release build walkthrough added (keytool, keystore.properties, bundleRelease); project structure table updated; vote scope removed https://claude.ai/code/session_014HLhrFtCVRFnEyfRexiy3d --- android/.gitignore | 3 + android/PRIVACY_POLICY.md | 52 ++++++++++++ android/SETUP.md | 81 ++++++++++++++++--- android/app/build.gradle | 53 +++++++++++- android/app/proguard-rules.pro | 43 ++++++++++ android/app/src/main/AndroidManifest.xml | 5 +- .../api/RedditApiClient.kt | 8 +- .../res/drawable/ic_launcher_background.xml | 11 +++ .../res/drawable/ic_launcher_foreground.xml | 26 ++++++ .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 ++ .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 ++ .../main/res/xml/network_security_config.xml | 19 +++++ 12 files changed, 294 insertions(+), 17 deletions(-) create mode 100644 android/PRIVACY_POLICY.md create mode 100644 android/app/proguard-rules.pro create mode 100644 android/app/src/main/res/drawable/ic_launcher_background.xml create mode 100644 android/app/src/main/res/drawable/ic_launcher_foreground.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml create mode 100644 android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml create mode 100644 android/app/src/main/res/xml/network_security_config.xml diff --git a/android/.gitignore b/android/.gitignore index 7eec6b1..18d261f 100644 --- a/android/.gitignore +++ b/android/.gitignore @@ -2,6 +2,9 @@ .gradle/ .idea/ /local.properties +/keystore.properties +*.jks +*.keystore /.DS_Store /build/ /captures/ diff --git a/android/PRIVACY_POLICY.md b/android/PRIVACY_POLICY.md new file mode 100644 index 0000000..0853194 --- /dev/null +++ b/android/PRIVACY_POLICY.md @@ -0,0 +1,52 @@ +# Privacy Policy — Reddit Comment Cleaner (Android) + +**Last updated: 2026-02-28** + +## Overview + +Reddit Comment Cleaner is a tool that lets you bulk-delete your own Reddit comments and posts. This policy explains what data the app accesses, how it is used, and how it is stored. + +## Data accessed + +The app requests the following Reddit OAuth scopes when you log in: + +| Scope | Purpose | +|-------|---------| +| `identity` | Read your Reddit username to display in the dashboard | +| `history` | List your own comments and posts so you can review them | +| `edit` | Overwrite each item with `"."` before deletion, preventing content-scraping tools from capturing the original text | + +The app does **not** request `vote`, `submit`, `read`, `subscribe`, or any moderation scopes. + +## Data stored on your device + +| Data | Storage | Cleared when | +|------|---------|--------------| +| OAuth access token | EncryptedSharedPreferences (AES-256-GCM) | You tap "Log out" | +| OAuth refresh token | EncryptedSharedPreferences (AES-256-GCM) | You tap "Log out" | +| Reddit username | EncryptedSharedPreferences | You tap "Log out" | +| Token expiry timestamp | EncryptedSharedPreferences | You tap "Log out" | + +No data is written to external storage or shared with any third party. + +## Data transmitted + +All network requests go directly to the Reddit API (`oauth.reddit.com`, `www.reddit.com`). No data is sent to any server operated by this app's developer. + +## Data not collected + +- The app does not collect analytics, crash reports, or usage telemetry. +- No advertising SDKs are included. +- No data is shared with advertisers or data brokers. + +## Third-party services + +The app communicates only with Reddit's official API. Reddit's own privacy policy applies to that communication: + +## Changes to this policy + +If the app's data practices change, this document will be updated and the "Last updated" date above will be revised. + +## Contact + +If you have questions about this policy, open an issue at the project's GitHub repository. diff --git a/android/SETUP.md b/android/SETUP.md index 54e4e6b..12a65e9 100644 --- a/android/SETUP.md +++ b/android/SETUP.md @@ -18,17 +18,21 @@ --- -## 2. Put your client ID in the build config +## 2. Add your client ID to `local.properties` -Open `app/build.gradle` and replace `YOUR_CLIENT_ID`: +`local.properties` lives in the `android/` directory and is already listed in `.gitignore` — **never commit it**. -```groovy -buildConfigField "String", "REDDIT_CLIENT_ID", '"abc123xyz"' // ← your client ID here +```properties +# android/local.properties +sdk.dir=/path/to/your/android/sdk +reddit.client_id=abc123xyz ``` +The build script reads `reddit.client_id` automatically. Do **not** edit `app/build.gradle` to add the client ID directly. + --- -## 3. Build & run +## 3. Build & run (debug) ```bash cd android @@ -41,6 +45,61 @@ Or open the `android/` directory in Android Studio and click **Run**. --- +## 4. Build a release APK / AAB for Play Store + +### 4a. Generate a release keystore (one-time setup) + +```bash +keytool -genkeypair -v \ + -keystore my-release-key.jks \ + -keyalg RSA -keysize 2048 \ + -validity 10000 \ + -alias my-key-alias +``` + +Move the resulting `.jks` file somewhere safe and **outside** the repo. Never commit it. + +### 4b. Create `keystore.properties` + +Create `android/keystore.properties` (already in `.gitignore`): + +```properties +storeFile=/absolute/path/to/my-release-key.jks +storePassword=your_store_password +keyAlias=my-key-alias +keyPassword=your_key_password +``` + +### 4c. Build the signed AAB + +```bash +cd android +./gradlew bundleRelease +# Output: app/build/outputs/bundle/release/app-release.aab +``` + +Or build a signed APK: + +```bash +./gradlew assembleRelease +# Output: app/build/outputs/apk/release/app-release.apk +``` + +--- + +## 5. Play Store listing requirements + +Before submitting to the Play Store you will also need: + +| Requirement | Where | +|---|---| +| **Privacy policy URL** | Host `PRIVACY_POLICY.md` (e.g. GitHub Pages) and paste the URL in the Play Console listing | +| **Store listing icon** | 512×512 PNG (export from the vector at `res/drawable/ic_launcher_foreground.xml`) | +| **Feature graphic** | 1024×500 PNG (create separately) | +| **Screenshots** | Minimum 2 per supported form factor | + +--- + ## How the app works | Step | What happens | @@ -62,8 +121,7 @@ Or open the `android/` directory in Android Studio and click **Run**. |-------|---------| | `identity` | Read username | | `history` | List comments and posts | -| `edit` | Overwrite item body with `"."` | -| `vote` | (reserved for future score filtering) | +| `edit` | Overwrite item body with `"."` before deletion | --- @@ -71,7 +129,8 @@ Or open the `android/` directory in Android Studio and click **Run**. ``` android/ -├── app/build.gradle ← dependencies & buildConfigFields +├── app/build.gradle ← dependencies, signing config, buildConfigFields +├── app/proguard-rules.pro ← R8/ProGuard rules for release builds ├── app/src/main/ │ ├── AndroidManifest.xml │ ├── java/com/redditcommentcleaner/ @@ -92,7 +151,11 @@ android/ │ │ ├── TokenStorage.kt ← EncryptedSharedPreferences wrapper │ │ └── PkceHelper.kt ← PKCE code_verifier/challenge generation │ └── res/ +│ ├── drawable/ ← ic_launcher_background/foreground (vector) │ ├── layout/ ← activity and item layouts -│ └── values/ ← colors, strings, themes +│ ├── mipmap-anydpi-v26/ ← adaptive icon (ic_launcher, ic_launcher_round) +│ ├── values/ ← colors, strings, themes +│ └── xml/ ← network_security_config.xml +├── PRIVACY_POLICY.md ← host this URL in Play Console listing └── SETUP.md ← this file ``` diff --git a/android/app/build.gradle b/android/app/build.gradle index ae365b4..0c37371 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -3,6 +3,21 @@ plugins { id 'kotlin-android' } +// ── Local secrets (never committed) ────────────────────────────────────────── +// local.properties must contain: reddit.client_id= +def localProps = new Properties() +def localPropsFile = rootProject.file('local.properties') +if (localPropsFile.exists()) { + localProps.load(new FileInputStream(localPropsFile)) +} + +// keystore.properties must contain: storeFile, storePassword, keyAlias, keyPassword +def keystoreProps = new Properties() +def keystorePropsFile = rootProject.file('keystore.properties') +if (keystorePropsFile.exists()) { + keystoreProps.load(new FileInputStream(keystorePropsFile)) +} + android { namespace 'com.redditcommentcleaner' compileSdk 34 @@ -14,10 +29,39 @@ android { versionCode 1 versionName "1.0" - // Replace YOUR_CLIENT_ID with the client_id from your Reddit "installed app" - buildConfigField "String", "REDDIT_CLIENT_ID", '"YOUR_CLIENT_ID"' + // Set reddit.client_id in local.properties (never commit that file) + buildConfigField "String", "REDDIT_CLIENT_ID", + "\"${localProps.getProperty('reddit.client_id', 'YOUR_CLIENT_ID')}\"" buildConfigField "String", "REDIRECT_URI", '"redditcommentcleaner://auth"' - buildConfigField "String", "OAUTH_SCOPES", '"identity history edit vote"' + // vote scope removed — app only needs identity, history, and edit + buildConfigField "String", "OAUTH_SCOPES", '"identity history edit"' + } + + signingConfigs { + release { + // Populate keystore.properties with the four keys below; + // do NOT commit that file (it is listed in .gitignore). + if (keystorePropsFile.exists()) { + storeFile file(keystoreProps['storeFile']) + storePassword keystoreProps['storePassword'] + keyAlias keystoreProps['keyAlias'] + keyPassword keystoreProps['keyPassword'] + } + } + } + + buildTypes { + release { + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), + 'proguard-rules.pro' + signingConfig signingConfigs.release + } + debug { + applicationIdSuffix '.debug' + versionNameSuffix '-debug' + } } buildFeatures { @@ -43,7 +87,8 @@ dependencies { implementation 'androidx.fragment:fragment-ktx:1.6.2' implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0' implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.7.0' - implementation 'androidx.security:security-crypto:1.1.0-alpha06' + // Use stable release — 1.1.0-alpha06 is an alpha build + implementation 'androidx.security:security-crypto:1.0.0' implementation 'androidx.browser:browser:1.7.0' implementation 'androidx.recyclerview:recyclerview:1.3.2' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0' diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..361188f --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,43 @@ +# ── Retrofit ────────────────────────────────────────────────────────────────── +-dontwarn retrofit2.** +-keep class retrofit2.** { *; } +-keepattributes Signature +-keepattributes Exceptions +-keepattributes *Annotation* + +# ── OkHttp / Okio ───────────────────────────────────────────────────────────── +-dontwarn okhttp3.** +-dontwarn okio.** +-keep class okhttp3.** { *; } +-keep interface okhttp3.** { *; } + +# ── Gson ────────────────────────────────────────────────────────────────────── +-dontwarn sun.misc.** +-keep class com.google.gson.** { *; } +-keep class * implements com.google.gson.TypeAdapterFactory +-keep class * implements com.google.gson.JsonSerializer +-keep class * implements com.google.gson.JsonDeserializer +# Preserve fields annotated with @SerializedName so Gson can map JSON keys +-keepclassmembers,allowobfuscation class * { + @com.google.gson.annotations.SerializedName ; +} + +# ── App models & API interfaces ─────────────────────────────────────────────── +# Keep all data classes used in Retrofit/Gson serialisation +-keep class com.redditcommentcleaner.model.** { *; } +-keep class com.redditcommentcleaner.api.** { *; } + +# ── Kotlin ──────────────────────────────────────────────────────────────────── +-keep class kotlin.** { *; } +-keep class kotlin.Metadata { *; } +-dontwarn kotlin.** +-keepclassmembers class **$WhenMappings { + ; +} +-keepclassmembers class kotlin.coroutines.** { *; } + +# ── AndroidX Security / EncryptedSharedPreferences ─────────────────────────── +-keep class androidx.security.crypto.** { *; } + +# ── BuildConfig ─────────────────────────────────────────────────────────────── +-keep class com.redditcommentcleaner.BuildConfig { *; } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 625099f..3200abf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -4,8 +4,11 @@ diff --git a/android/app/src/main/java/com/redditcommentcleaner/api/RedditApiClient.kt b/android/app/src/main/java/com/redditcommentcleaner/api/RedditApiClient.kt index 95b4cd9..e13ce63 100644 --- a/android/app/src/main/java/com/redditcommentcleaner/api/RedditApiClient.kt +++ b/android/app/src/main/java/com/redditcommentcleaner/api/RedditApiClient.kt @@ -22,11 +22,12 @@ object RedditApiClient { val client = OkHttpClient.Builder() .addInterceptor { chain -> // Refresh token if near expiry - if (tokenStorage.isTokenExpired() && !tokenStorage.refreshToken.isNullOrBlank()) { + val currentRefreshToken = tokenStorage.refreshToken + if (tokenStorage.isTokenExpired() && !currentRefreshToken.isNullOrBlank()) { runBlocking { runCatching { val resp = authService().refreshToken( - refreshToken = tokenStorage.refreshToken!! + refreshToken = currentRefreshToken ) tokenStorage.accessToken = resp.accessToken tokenStorage.tokenExpiryMs = System.currentTimeMillis() + resp.expiresIn * 1000L @@ -76,6 +77,7 @@ object RedditApiClient { "android:com.redditcommentcleaner:v1.0 (by /u/$username)" private fun loggingInterceptor() = HttpLoggingInterceptor().apply { - level = HttpLoggingInterceptor.Level.BASIC + level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BASIC + else HttpLoggingInterceptor.Level.NONE } } diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..a112ca8 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 0000000..4ff13b8 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..6b78462 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..6b78462 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml new file mode 100644 index 0000000..511f197 --- /dev/null +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -0,0 +1,19 @@ + + + + + + + + +