Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
.gradle/
.idea/
/local.properties
/keystore.properties
*.jks
*.keystore
/.DS_Store
/build/
/captures/
Expand Down
52 changes: 52 additions & 0 deletions android/PRIVACY_POLICY.md
Original file line number Diff line number Diff line change
@@ -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: <https://www.reddit.com/policies/privacy-policy>

## 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.
81 changes: 72 additions & 9 deletions android/SETUP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 |
Expand All @@ -62,16 +121,16 @@ 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 |

---

## Project structure

```
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/
Expand All @@ -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
```
53 changes: 49 additions & 4 deletions android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,21 @@ plugins {
id 'kotlin-android'
}

// ── Local secrets (never committed) ──────────────────────────────────────────
// local.properties must contain: reddit.client_id=<your_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
Expand All @@ -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 {
Expand All @@ -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'
Expand Down
43 changes: 43 additions & 0 deletions android/app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -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 <fields>;
}

# ── 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 {
<fields>;
}
-keepclassmembers class kotlin.coroutines.** { *; }

# ── AndroidX Security / EncryptedSharedPreferences ───────────────────────────
-keep class androidx.security.crypto.** { *; }

# ── BuildConfig ───────────────────────────────────────────────────────────────
-keep class com.redditcommentcleaner.BuildConfig { *; }
5 changes: 4 additions & 1 deletion android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@
<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:networkSecurityConfig="@xml/network_security_config"
android:supportsRtl="true"
android:theme="@style/Theme.RedditCommentCleaner">

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
11 changes: 11 additions & 0 deletions android/app/src/main/res/drawable/ic_launcher_background.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Solid Reddit-orange background for the adaptive icon -->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#FF4500"
android:pathData="M0,0h108v108H0z" />
</vector>
26 changes: 26 additions & 0 deletions android/app/src/main/res/drawable/ic_launcher_foreground.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Adaptive icon foreground — white trash-can (delete) icon centred in the
108×108dp canvas. The Material Design "delete" paths are scaled ×3 and
offset by 18dp so the artwork sits entirely within the 72×72dp safe zone.

Original 24×24 Material delete icon:
body: M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12z
lid: M19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">

<!-- Trash-can body -->
<path
android:fillColor="#FFFFFF"
android:pathData="M36,75c0,3.3 2.7,6 6,6h24c3.3,0 6,-2.7 6,-6V39H36v36z" />

<!-- Trash-can lid and handle -->
<path
android:fillColor="#FFFFFF"
android:pathData="M75,30h-10.5l-3,-3h-15l-3,3H33v6h42V30z" />
</vector>
5 changes: 5 additions & 0 deletions android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>
Loading
Loading