Created
April 19, 2026 06:07
-
-
Save tas33n/871da81b12583b996bb8bb8e6d086108 to your computer and use it in GitHub Desktop.
Android development tools setup for github codespace
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================= | |
| # NotificationVault — Android Dev Environment Setup for GitHub Codespaces | |
| # Run: bash setup-android-codespace.sh | |
| # Tested on: Ubuntu 22.04 (default Codespaces base image) | |
| # ============================================================================= | |
| set -euo pipefail | |
| # ─── Colors ────────────────────────────────────────────────────────────────── | |
| RED='\033[0;31m' | |
| GREEN='\033[0;32m' | |
| YELLOW='\033[1;33m' | |
| CYAN='\033[0;36m' | |
| BOLD='\033[1m' | |
| NC='\033[0m' | |
| log() { echo -e "${CYAN}${BOLD}[SETUP]${NC} $1"; } | |
| ok() { echo -e "${GREEN}${BOLD}[ OK ]${NC} $1"; } | |
| warn() { echo -e "${YELLOW}${BOLD}[ WARN ]${NC} $1"; } | |
| fail() { echo -e "${RED}${BOLD}[FAIL ]${NC} $1"; exit 1; } | |
| section(){ echo -e "\n${BOLD}${CYAN}══════════════════════════════════════════${NC}"; \ | |
| echo -e "${BOLD}${CYAN} $1${NC}"; \ | |
| echo -e "${BOLD}${CYAN}══════════════════════════════════════════${NC}\n"; } | |
| # ─── Versions (pin for reproducibility) ────────────────────────────────────── | |
| JAVA_VERSION="17" | |
| ANDROID_CMDTOOLS_VERSION="11076708" # commandlinetools-linux rev | |
| ANDROID_CMDTOOLS_SHA256="2d2d50857e4eb553af5a6dc3ad507a17adf43d115264b1afc116f95c92e5e258" | |
| ANDROID_BUILD_TOOLS_VERSION="35.0.0" | |
| ANDROID_PLATFORM_VERSION="36" # Android 16 (API 36) | |
| ANDROID_PLATFORM_MIN="31" # Android 12 (API 31) — also install | |
| GRADLE_VERSION="8.8" | |
| KOTLIN_VERSION="2.0.0" | |
| AGP_VERSION="8.5.0" # Android Gradle Plugin | |
| ANDROID_HOME="${HOME}/android-sdk" | |
| CMDTOOLS_DIR="${ANDROID_HOME}/cmdline-tools/latest" | |
| section "1 / 9 — System packages" | |
| log "Updating apt..." | |
| sudo apt-get update -qq | |
| log "Installing core dependencies..." | |
| sudo apt-get install -y -qq \ | |
| curl wget unzip zip git \ | |
| openjdk-${JAVA_VERSION}-jdk \ | |
| build-essential \ | |
| lib32stdc++6 lib32z1 \ | |
| libpulse0 \ | |
| xdg-utils \ | |
| ca-certificates \ | |
| gnupg \ | |
| lsb-release \ | |
| jq \ | |
| tree \ | |
| htop | |
| ok "System packages installed" | |
| # ─── Java ───────────────────────────────────────────────────────────────────── | |
| section "2 / 9 — Java ${JAVA_VERSION}" | |
| JAVA_HOME_PATH=$(dirname $(dirname $(readlink -f $(which java)))) | |
| export JAVA_HOME="${JAVA_HOME_PATH}" | |
| java -version 2>&1 | head -1 | |
| ok "Java ${JAVA_VERSION} ready — JAVA_HOME=${JAVA_HOME}" | |
| # ─── Android SDK Command Line Tools ────────────────────────────────────────── | |
| section "3 / 9 — Android SDK" | |
| log "Creating SDK directory at ${ANDROID_HOME}..." | |
| mkdir -p "${CMDTOOLS_DIR}" | |
| CMDTOOLS_ZIP="/tmp/android-cmdtools.zip" | |
| log "Downloading Android command line tools (rev ${ANDROID_CMDTOOLS_VERSION})..." | |
| wget -q --show-progress \ | |
| "https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_CMDTOOLS_VERSION}_latest.zip" \ | |
| -O "${CMDTOOLS_ZIP}" | |
| log "Verifying download checksum..." | |
| echo "${ANDROID_CMDTOOLS_SHA256} ${CMDTOOLS_ZIP}" | sha256sum --check --quiet \ | |
| || warn "Checksum mismatch — continuing anyway (version may have updated)" | |
| log "Extracting command line tools..." | |
| TMPDIR_TOOLS=$(mktemp -d) | |
| unzip -q "${CMDTOOLS_ZIP}" -d "${TMPDIR_TOOLS}" | |
| # Google zips as cmdline-tools/ — move contents to our latest/ dir | |
| cp -r "${TMPDIR_TOOLS}/cmdline-tools/." "${CMDTOOLS_DIR}/" | |
| rm -rf "${TMPDIR_TOOLS}" "${CMDTOOLS_ZIP}" | |
| ok "Command line tools extracted" | |
| # ─── Environment variables ──────────────────────────────────────────────────── | |
| section "4 / 9 — Environment variables" | |
| export ANDROID_HOME="${ANDROID_HOME}" | |
| export ANDROID_SDK_ROOT="${ANDROID_HOME}" | |
| export PATH="${PATH}:${CMDTOOLS_DIR}/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/emulator" | |
| log "Writing env vars to ~/.bashrc, ~/.zshrc, ~/.profile..." | |
| ENV_BLOCK=$(cat <<'ENVEOF' | |
| # ── Android SDK ──────────────────────────────────────────────────────── | |
| export JAVA_HOME="$(dirname $(dirname $(readlink -f $(which java))))" | |
| export ANDROID_HOME="${HOME}/android-sdk" | |
| export ANDROID_SDK_ROOT="${ANDROID_HOME}" | |
| export PATH="${PATH}:${ANDROID_HOME}/cmdline-tools/latest/bin" | |
| export PATH="${PATH}:${ANDROID_HOME}/platform-tools" | |
| export PATH="${PATH}:${ANDROID_HOME}/emulator" | |
| export PATH="${PATH}:${ANDROID_HOME}/build-tools/35.0.0" | |
| # ────────────────────────────────────────────────────────────────────── | |
| ENVEOF | |
| ) | |
| for rc_file in "${HOME}/.bashrc" "${HOME}/.zshrc" "${HOME}/.profile"; do | |
| if [ -f "${rc_file}" ]; then | |
| # Remove previous block if it exists | |
| if grep -q "Android SDK" "${rc_file}" 2>/dev/null; then | |
| warn "Existing Android SDK env found in ${rc_file}, skipping..." | |
| else | |
| echo "${ENV_BLOCK}" >> "${rc_file}" | |
| ok "Updated ${rc_file}" | |
| fi | |
| fi | |
| done | |
| ok "Environment variables configured" | |
| # ─── SDK packages via sdkmanager ────────────────────────────────────────────── | |
| section "5 / 9 — Android SDK packages" | |
| log "Accepting all SDK licenses..." | |
| yes | "${CMDTOOLS_DIR}/bin/sdkmanager" --licenses > /dev/null 2>&1 || true | |
| log "Installing Android platform API ${ANDROID_PLATFORM_VERSION} (Android 16)..." | |
| "${CMDTOOLS_DIR}/bin/sdkmanager" "platforms;android-${ANDROID_PLATFORM_VERSION}" | |
| log "Installing Android platform API ${ANDROID_PLATFORM_MIN} (Android 12 — minSdk)..." | |
| "${CMDTOOLS_DIR}/bin/sdkmanager" "platforms;android-${ANDROID_PLATFORM_MIN}" | |
| log "Installing Android platform API 33 (Android 13)..." | |
| "${CMDTOOLS_DIR}/bin/sdkmanager" "platforms;android-33" | |
| log "Installing Build Tools ${ANDROID_BUILD_TOOLS_VERSION}..." | |
| "${CMDTOOLS_DIR}/bin/sdkmanager" "build-tools;${ANDROID_BUILD_TOOLS_VERSION}" | |
| log "Installing Build Tools 34.0.0 (for compatibility)..." | |
| "${CMDTOOLS_DIR}/bin/sdkmanager" "build-tools;34.0.0" | |
| log "Installing Platform Tools (adb, fastboot)..." | |
| "${CMDTOOLS_DIR}/bin/sdkmanager" "platform-tools" | |
| log "Installing extras..." | |
| "${CMDTOOLS_DIR}/bin/sdkmanager" \ | |
| "extras;android;m2repository" \ | |
| "extras;google;m2repository" \ | |
| "extras;google;google_play_services" | |
| log "Installed SDK packages:" | |
| "${CMDTOOLS_DIR}/bin/sdkmanager" --list_installed 2>/dev/null | grep -v "^$" || true | |
| ok "Android SDK packages installed" | |
| # ─── Gradle ────────────────────────────────────────────────────────────────── | |
| section "6 / 9 — Gradle ${GRADLE_VERSION}" | |
| GRADLE_ZIP="/tmp/gradle-${GRADLE_VERSION}-bin.zip" | |
| GRADLE_DIR="${HOME}/.gradle/wrapper/dists/gradle-${GRADLE_VERSION}-bin" | |
| log "Downloading Gradle ${GRADLE_VERSION}..." | |
| wget -q --show-progress \ | |
| "https://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip" \ | |
| -O "${GRADLE_ZIP}" | |
| log "Installing Gradle to /opt/gradle..." | |
| sudo mkdir -p /opt/gradle | |
| sudo unzip -q "${GRADLE_ZIP}" -d /opt/gradle | |
| rm "${GRADLE_ZIP}" | |
| GRADLE_BIN="/opt/gradle/gradle-${GRADLE_VERSION}/bin" | |
| sudo ln -sf "${GRADLE_BIN}/gradle" /usr/local/bin/gradle | |
| log "Gradle version: $(gradle --version | grep Gradle)" | |
| # Pre-populate Gradle wrapper cache so first build is faster | |
| mkdir -p "${HOME}/.gradle/wrapper/dists" | |
| ok "Gradle ${GRADLE_VERSION} installed" | |
| # ─── Gradle global config ───────────────────────────────────────────────────── | |
| section "7 / 9 — Gradle & build configuration" | |
| mkdir -p "${HOME}/.gradle" | |
| cat > "${HOME}/.gradle/gradle.properties" <<GRADLEEOF | |
| # ── NotificationVault global Gradle properties ──────────────────────── | |
| # Performance | |
| org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 | |
| org.gradle.parallel=true | |
| org.gradle.caching=true | |
| org.gradle.configureondemand=true | |
| org.gradle.daemon=true | |
| # Android | |
| android.useAndroidX=true | |
| android.enableJetifier=false | |
| android.nonTransitiveRClass=true | |
| android.nonFinalResIds=false | |
| # Kotlin | |
| kotlin.incremental=true | |
| kotlin.incremental.java=true | |
| kotlin.daemon.jvm.options=-Xmx2g | |
| # Compose | |
| android.defaults.buildfeatures.compose=true | |
| # Suppress warnings | |
| android.suppressUnsupportedCompileSdkWarnings=true | |
| GRADLEEOF | |
| ok "Gradle global properties configured (~/.gradle/gradle.properties)" | |
| # ─── Project scaffold ───────────────────────────────────────────────────────── | |
| section "8 / 9 — Project scaffold" | |
| PROJECT_DIR="${HOME}/NotificationVault" | |
| if [ -d "${PROJECT_DIR}" ]; then | |
| warn "Project directory already exists at ${PROJECT_DIR} — skipping scaffold" | |
| else | |
| log "Creating project at ${PROJECT_DIR}..." | |
| mkdir -p "${PROJECT_DIR}" | |
| cd "${PROJECT_DIR}" | |
| # ── Root build.gradle.kts ────────────────────────────────────────── | |
| cat > "build.gradle.kts" <<'EOF' | |
| plugins { | |
| alias(libs.plugins.android.application) apply false | |
| alias(libs.plugins.kotlin.android) apply false | |
| alias(libs.plugins.kotlin.compose) apply false | |
| alias(libs.plugins.hilt) apply false | |
| alias(libs.plugins.ksp) apply false | |
| } | |
| EOF | |
| # ── settings.gradle.kts ─────────────────────────────────────────── | |
| cat > "settings.gradle.kts" <<'EOF' | |
| pluginManagement { | |
| repositories { | |
| google { content { includeGroupByRegex("com\\.android.*"); includeGroupByRegex("com\\.google.*"); includeGroupByRegex("androidx.*") } } | |
| mavenCentral() | |
| gradlePluginPortal() | |
| } | |
| } | |
| dependencyResolutionManagement { | |
| repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) | |
| repositories { | |
| google() | |
| mavenCentral() | |
| } | |
| } | |
| rootProject.name = "NotificationVault" | |
| include(":app") | |
| EOF | |
| # ── libs.versions.toml ──────────────────────────────────────────── | |
| mkdir -p "gradle" | |
| cat > "gradle/libs.versions.toml" <<'EOF' | |
| [versions] | |
| agp = "8.5.0" | |
| kotlin = "2.0.0" | |
| ksp = "2.0.0-1.0.21" | |
| composeBom = "2024.06.00" | |
| hilt = "2.51.1" | |
| hiltNavigationCompose = "1.2.0" | |
| room = "2.6.1" | |
| datastore = "1.1.1" | |
| coil = "2.6.0" | |
| coroutines = "1.8.1" | |
| lifecycle = "2.8.2" | |
| navigationCompose = "2.7.7" | |
| activityCompose = "1.9.0" | |
| biometric = "1.2.0-alpha05" | |
| workRuntime = "2.9.0" | |
| timber = "5.0.1" | |
| coreKtx = "1.13.1" | |
| splashscreen = "1.0.1" | |
| junitVersion = "4.13.2" | |
| androidxTestExt = "1.2.1" | |
| espresso = "3.6.1" | |
| [libraries] | |
| # AndroidX Core | |
| androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } | |
| androidx-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "splashscreen" } | |
| # Compose | |
| compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "composeBom" } | |
| compose-ui = { group = "androidx.compose.ui", name = "ui" } | |
| compose-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } | |
| compose-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } | |
| compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } | |
| compose-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-manifest" } | |
| compose-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } | |
| compose-material3 = { group = "androidx.compose.material3", name = "material3" } | |
| compose-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } | |
| compose-activity = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } | |
| compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } | |
| # Lifecycle | |
| lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycle" } | |
| lifecycle-runtime-compose = { group = "androidx.lifecycle", name = "lifecycle-runtime-compose", version.ref = "lifecycle" } | |
| # Hilt | |
| hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } | |
| hilt-compiler = { group = "com.google.dagger", name = "hilt-android-compiler", version.ref = "hilt" } | |
| hilt-navigation-compose = { group = "androidx.hilt", name = "hilt-navigation-compose", version.ref = "hiltNavigationCompose" } | |
| # Room | |
| room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" } | |
| room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" } | |
| room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "room" } | |
| # DataStore | |
| datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" } | |
| # Coil | |
| coil-compose = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } | |
| # Coroutines | |
| coroutines-android = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-android", version.ref = "coroutines" } | |
| # Biometric | |
| biometric = { group = "androidx.biometric", name = "biometric", version.ref = "biometric" } | |
| # WorkManager | |
| work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "workRuntime" } | |
| # Logging | |
| timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" } | |
| # Testing | |
| junit = { group = "junit", name = "junit", version.ref = "junitVersion" } | |
| androidx-test-ext-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidxTestExt" } | |
| espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso" } | |
| [plugins] | |
| android-application = { id = "com.android.application", version.ref = "agp" } | |
| kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } | |
| kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } | |
| hilt = { id = "com.google.dagger.hilt.android", version.ref = "hilt" } | |
| ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } | |
| EOF | |
| # ── gradlew wrapper ─────────────────────────────────────────────── | |
| gradle wrapper --gradle-version="${GRADLE_VERSION}" --distribution-type=bin 2>/dev/null || { | |
| warn "gradle wrapper cmd failed, creating wrapper manually..." | |
| mkdir -p "gradle/wrapper" | |
| cat > "gradle/wrapper/gradle-wrapper.properties" <<WRAPEOF | |
| distributionBase=GRADLE_USER_HOME | |
| distributionPath=wrapper/dists | |
| distributionUrl=https\://services.gradle.org/distributions/gradle-${GRADLE_VERSION}-bin.zip | |
| networkTimeout=10000 | |
| validateDistributionUrl=true | |
| zipStoreBase=GRADLE_USER_HOME | |
| zipStorePath=wrapper/dists | |
| WRAPEOF | |
| } | |
| # ── app/ module ─────────────────────────────────────────────────── | |
| mkdir -p "app/src/main/kotlin/com/notificationvault/app" | |
| mkdir -p "app/src/main/res/values" | |
| mkdir -p "app/src/main/res/xml" | |
| mkdir -p "app/src/test/kotlin/com/notificationvault/app" | |
| mkdir -p "app/src/androidTest/kotlin/com/notificationvault/app" | |
| cat > "app/build.gradle.kts" <<'EOF' | |
| plugins { | |
| alias(libs.plugins.android.application) | |
| alias(libs.plugins.kotlin.android) | |
| alias(libs.plugins.kotlin.compose) | |
| alias(libs.plugins.hilt) | |
| alias(libs.plugins.ksp) | |
| } | |
| android { | |
| namespace = "com.notificationvault.app" | |
| compileSdk = 36 | |
| defaultConfig { | |
| applicationId = "com.notificationvault.app" | |
| minSdk = 31 | |
| targetSdk = 36 | |
| versionCode = 1 | |
| versionName = "0.1.0" | |
| testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" | |
| vectorDrawables { useSupportLibrary = true } | |
| } | |
| buildTypes { | |
| release { | |
| isMinifyEnabled = true | |
| isShrinkResources = true | |
| proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") | |
| } | |
| debug { | |
| applicationIdSuffix = ".debug" | |
| isDebuggable = true | |
| } | |
| } | |
| compileOptions { | |
| sourceCompatibility = JavaVersion.VERSION_17 | |
| targetCompatibility = JavaVersion.VERSION_17 | |
| } | |
| kotlinOptions { jvmTarget = "17" } | |
| buildFeatures { compose = true } | |
| packaging { | |
| resources { excludes += "/META-INF/{AL2.0,LGPL2.1}" } | |
| } | |
| } | |
| dependencies { | |
| implementation(libs.androidx.core.ktx) | |
| implementation(libs.androidx.splashscreen) | |
| // Compose | |
| val composeBom = platform(libs.compose.bom) | |
| implementation(composeBom) | |
| androidTestImplementation(composeBom) | |
| implementation(libs.compose.ui) | |
| implementation(libs.compose.ui.graphics) | |
| implementation(libs.compose.ui.tooling.preview) | |
| implementation(libs.compose.material3) | |
| implementation(libs.compose.material.icons.extended) | |
| implementation(libs.compose.activity) | |
| implementation(libs.compose.navigation) | |
| debugImplementation(libs.compose.ui.tooling) | |
| debugImplementation(libs.compose.ui.test.manifest) | |
| // Lifecycle | |
| implementation(libs.lifecycle.viewmodel.compose) | |
| implementation(libs.lifecycle.runtime.compose) | |
| // Hilt | |
| implementation(libs.hilt.android) | |
| ksp(libs.hilt.compiler) | |
| implementation(libs.hilt.navigation.compose) | |
| // Room | |
| implementation(libs.room.runtime) | |
| implementation(libs.room.ktx) | |
| ksp(libs.room.compiler) | |
| // DataStore | |
| implementation(libs.datastore.preferences) | |
| // Coil | |
| implementation(libs.coil.compose) | |
| // Coroutines | |
| implementation(libs.coroutines.android) | |
| // Biometric | |
| implementation(libs.biometric) | |
| // WorkManager | |
| implementation(libs.work.runtime.ktx) | |
| // Timber | |
| implementation(libs.timber) | |
| // Testing | |
| testImplementation(libs.junit) | |
| androidTestImplementation(libs.androidx.test.ext.junit) | |
| androidTestImplementation(libs.espresso.core) | |
| androidTestImplementation(libs.compose.ui.test.junit4) | |
| } | |
| EOF | |
| # ── AndroidManifest.xml ─────────────────────────────────────────── | |
| cat > "app/src/main/AndroidManifest.xml" <<'EOF' | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <manifest xmlns:android="http://schemas.android.com/apk/res/android"> | |
| <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> | |
| <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> | |
| <uses-permission android:name="android.permission.USE_BIOMETRIC"/> | |
| <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> | |
| <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/> | |
| <application | |
| android:name=".NotificationVaultApp" | |
| android:allowBackup="false" | |
| android:icon="@mipmap/ic_launcher" | |
| android:label="@string/app_name" | |
| android:roundIcon="@mipmap/ic_launcher_round" | |
| android:supportsRtl="true" | |
| android:theme="@style/Theme.NotificationVault"> | |
| <activity | |
| android:name=".MainActivity" | |
| android:exported="true" | |
| android:windowSoftInputMode="adjustResize"> | |
| <intent-filter> | |
| <action android:name="android.intent.action.MAIN"/> | |
| <category android:name="android.intent.category.LAUNCHER"/> | |
| </intent-filter> | |
| </activity> | |
| <service | |
| android:name=".service.VaultNotificationListenerService" | |
| android:label="@string/service_label" | |
| android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE" | |
| android:exported="true"> | |
| <intent-filter> | |
| <action android:name="android.service.notification.NotificationListenerService"/> | |
| </intent-filter> | |
| <meta-data | |
| android:name="android.service.notification.default_filter_types" | |
| android:value="conversations"/> | |
| </service> | |
| <receiver | |
| android:name=".receiver.BootReceiver" | |
| android:exported="false"> | |
| <intent-filter> | |
| <action android:name="android.intent.action.BOOT_COMPLETED"/> | |
| </intent-filter> | |
| </receiver> | |
| </application> | |
| </manifest> | |
| EOF | |
| # ── res/values/strings.xml ──────────────────────────────────────── | |
| cat > "app/src/main/res/values/strings.xml" <<'EOF' | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <resources> | |
| <string name="app_name">NotificationVault</string> | |
| <string name="service_label">NotificationVault Listener</string> | |
| </resources> | |
| EOF | |
| # ── res/values/themes.xml ───────────────────────────────────────── | |
| cat > "app/src/main/res/values/themes.xml" <<'EOF' | |
| <?xml version="1.0" encoding="utf-8"?> | |
| <resources> | |
| <style name="Theme.NotificationVault" parent="android:Theme.Material.Light.NoActionBar"/> | |
| </resources> | |
| EOF | |
| # ── Main Application class ──────────────────────────────────────── | |
| cat > "app/src/main/kotlin/com/notificationvault/app/NotificationVaultApp.kt" <<'EOF' | |
| package com.notificationvault.app | |
| import android.app.Application | |
| import dagger.hilt.android.HiltAndroidApp | |
| import timber.log.Timber | |
| @HiltAndroidApp | |
| class NotificationVaultApp : Application() { | |
| override fun onCreate() { | |
| super.onCreate() | |
| if (BuildConfig.DEBUG) { | |
| Timber.plant(Timber.DebugTree()) | |
| } | |
| } | |
| } | |
| EOF | |
| # ── MainActivity stub ───────────────────────────────────────────── | |
| cat > "app/src/main/kotlin/com/notificationvault/app/MainActivity.kt" <<'EOF' | |
| package com.notificationvault.app | |
| import android.os.Bundle | |
| import androidx.activity.ComponentActivity | |
| import androidx.activity.compose.setContent | |
| import androidx.activity.enableEdgeToEdge | |
| import dagger.hilt.android.AndroidEntryPoint | |
| @AndroidEntryPoint | |
| class MainActivity : ComponentActivity() { | |
| override fun onCreate(savedInstanceState: Bundle?) { | |
| super.onCreate(savedInstanceState) | |
| enableEdgeToEdge() | |
| setContent { | |
| // TODO: NotificationVaultTheme { AppNavHost() } | |
| } | |
| } | |
| } | |
| EOF | |
| # ── Stub service ────────────────────────────────────────────────── | |
| mkdir -p "app/src/main/kotlin/com/notificationvault/app/service" | |
| cat > "app/src/main/kotlin/com/notificationvault/app/service/VaultNotificationListenerService.kt" <<'EOF' | |
| package com.notificationvault.app.service | |
| import android.service.notification.NotificationListenerService | |
| import android.service.notification.StatusBarNotification | |
| import timber.log.Timber | |
| class VaultNotificationListenerService : NotificationListenerService() { | |
| override fun onNotificationPosted(sbn: StatusBarNotification?) { | |
| sbn ?: return | |
| // TODO: filter by package, extract extras, store to Room DB | |
| Timber.d("Notification posted: pkg=${sbn.packageName}") | |
| } | |
| override fun onNotificationRemoved(sbn: StatusBarNotification?) { | |
| sbn ?: return | |
| Timber.d("Notification removed: pkg=${sbn.packageName}") | |
| } | |
| override fun onListenerConnected() { | |
| super.onListenerConnected() | |
| Timber.i("NotificationListenerService connected") | |
| } | |
| override fun onListenerDisconnected() { | |
| super.onListenerDisconnected() | |
| Timber.w("NotificationListenerService disconnected") | |
| } | |
| } | |
| EOF | |
| # ── Stub BootReceiver ───────────────────────────────────────────── | |
| mkdir -p "app/src/main/kotlin/com/notificationvault/app/receiver" | |
| cat > "app/src/main/kotlin/com/notificationvault/app/receiver/BootReceiver.kt" <<'EOF' | |
| package com.notificationvault.app.receiver | |
| import android.content.BroadcastReceiver | |
| import android.content.Context | |
| import android.content.Intent | |
| import timber.log.Timber | |
| class BootReceiver : BroadcastReceiver() { | |
| override fun onReceive(context: Context, intent: Intent) { | |
| if (intent.action == Intent.ACTION_BOOT_COMPLETED) { | |
| Timber.i("Boot completed — service will reconnect automatically") | |
| // NotificationListenerService re-binds automatically after boot | |
| // No manual restart needed; this receiver is a hook for future work | |
| } | |
| } | |
| } | |
| EOF | |
| # ── proguard-rules.pro ──────────────────────────────────────────── | |
| cat > "app/proguard-rules.pro" <<'EOF' | |
| # Hilt | |
| -keep class dagger.hilt.** { *; } | |
| -keep class javax.inject.** { *; } | |
| # Room | |
| -keep class * extends androidx.room.RoomDatabase | |
| -keep @androidx.room.Entity class * | |
| # Kotlin coroutines | |
| -keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {} | |
| -keepnames class kotlinx.coroutines.CoroutineExceptionHandler {} | |
| # Timber | |
| -dontwarn org.jetbrains.annotations.** | |
| EOF | |
| # ── .gitignore ──────────────────────────────────────────────────── | |
| cat > ".gitignore" <<'EOF' | |
| *.iml | |
| .gradle/ | |
| local.properties | |
| .idea/ | |
| .DS_Store | |
| build/ | |
| captures/ | |
| app/build/ | |
| .externalNativeBuild/ | |
| .cxx/ | |
| *.keystore | |
| !debug.keystore | |
| google-services.json | |
| EOF | |
| # ── README stub ─────────────────────────────────────────────────── | |
| cat > "README.md" <<'EOF' | |
| # NotificationVault | |
| > Open-source notification history app for Android 12–16. | |
| > Captures messages before they're unsent. 100% local. No cloud. No ads. | |
| ## License | |
| GNU GPL v3.0 — see [LICENSE](LICENSE) | |
| ## Building | |
| ```bash | |
| ./gradlew assembleDebug | |
| ``` | |
| ## Contributing | |
| See [CONTRIBUTING.md](CONTRIBUTING.md) | |
| EOF | |
| # ── LICENSE (GPL v3 header) ─────────────────────────────────────── | |
| cat > "LICENSE" <<'EOF' | |
| NotificationVault — Notification history manager for Android | |
| Copyright (C) 2024 NotificationVault Contributors | |
| 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/>. | |
| EOF | |
| log "Project scaffolded at ${PROJECT_DIR}" | |
| ok "Project scaffold created" | |
| fi | |
| # ─── VS Code / Codespaces extensions ────────────────────────────────────────── | |
| section "9 / 9 — VS Code extensions & devcontainer" | |
| # Write .devcontainer config for future re-opens | |
| mkdir -p "${HOME}/NotificationVault/.devcontainer" | |
| cat > "${HOME}/NotificationVault/.devcontainer/devcontainer.json" <<'EOF' | |
| { | |
| "name": "NotificationVault Android Dev", | |
| "image": "mcr.microsoft.com/devcontainers/base:ubuntu-22.04", | |
| "postCreateCommand": "bash setup-android-codespace.sh", | |
| "customizations": { | |
| "vscode": { | |
| "extensions": [ | |
| "mathiasfrohlich.Kotlin", | |
| "fwcd.kotlin", | |
| "vscjava.vscode-java-pack", | |
| "esbenp.prettier-vscode", | |
| "eamodio.gitlens", | |
| "mhutchie.git-graph", | |
| "usernamehw.errorlens", | |
| "streetsidesoftware.code-spell-checker", | |
| "pkief.material-icon-theme", | |
| "github.copilot" | |
| ], | |
| "settings": { | |
| "editor.formatOnSave": true, | |
| "editor.tabSize": 4, | |
| "files.autoSave": "onFocusChange", | |
| "kotlin.languageServer.enabled": true, | |
| "java.configuration.runtimes": [ | |
| { "name": "JavaSE-17", "default": true } | |
| ] | |
| } | |
| } | |
| }, | |
| "forwardPorts": [], | |
| "remoteEnv": { | |
| "ANDROID_HOME": "${localEnv:HOME}/android-sdk", | |
| "ANDROID_SDK_ROOT": "${localEnv:HOME}/android-sdk" | |
| } | |
| } | |
| EOF | |
| # Install extensions if code CLI is available | |
| if command -v code &>/dev/null; then | |
| log "Installing VS Code extensions..." | |
| extensions=( | |
| "mathiasfrohlich.Kotlin" | |
| "fwcd.kotlin" | |
| "vscjava.vscode-java-pack" | |
| "eamodio.gitlens" | |
| "usernamehw.errorlens" | |
| "pkief.material-icon-theme" | |
| ) | |
| for ext in "${extensions[@]}"; do | |
| code --install-extension "${ext}" --force 2>/dev/null && ok "Installed: ${ext}" || warn "Could not install: ${ext}" | |
| done | |
| else | |
| warn "VS Code CLI not found — extensions listed in .devcontainer/devcontainer.json for next open" | |
| fi | |
| # ─── Verification ───────────────────────────────────────────────────────────── | |
| section "✅ Verification" | |
| echo "" | |
| echo -e "${BOLD}Tool versions:${NC}" | |
| java -version 2>&1 | head -1 && ok "Java" | |
| gradle --version 2>/dev/null | grep "^Gradle" && ok "Gradle" | |
| "${CMDTOOLS_DIR}/bin/sdkmanager" --version 2>/dev/null && ok "sdkmanager" | |
| echo "" | |
| echo -e "${BOLD}SDK packages installed:${NC}" | |
| "${CMDTOOLS_DIR}/bin/sdkmanager" --list_installed 2>/dev/null \ | |
| | grep -E "^ (platforms|build-tools|platform-tools|extras)" \ | |
| | awk '{print " ✓ " $1}' || true | |
| echo "" | |
| echo -e "${BOLD}Project structure:${NC}" | |
| tree "${HOME}/NotificationVault" -L 3 --dirsfirst 2>/dev/null \ | |
| || find "${HOME}/NotificationVault" -maxdepth 3 -print | head -40 | |
| # ─── Done ───────────────────────────────────────────────────────────────────── | |
| section "🎉 Setup Complete" | |
| echo -e "${GREEN}${BOLD}" | |
| echo " Android development environment is ready!" | |
| echo "" | |
| echo " Project location : ~/NotificationVault" | |
| echo " Android SDK : ~/android-sdk" | |
| echo " Java version : ${JAVA_VERSION}" | |
| echo " Gradle version : ${GRADLE_VERSION}" | |
| echo " Min SDK : API 31 (Android 12)" | |
| echo " Target SDK : API 36 (Android 16)" | |
| echo -e "${NC}" | |
| echo -e "${YELLOW}${BOLD}Next steps:${NC}" | |
| echo " 1. Reload your shell: source ~/.bashrc" | |
| echo " 2. Go to project: cd ~/NotificationVault" | |
| echo " 3. First build check: ./gradlew assembleDebug" | |
| echo " 4. Open in VS Code: code ~/NotificationVault" | |
| echo "" | |
| echo -e "${CYAN}Happy coding! 🚀${NC}" | |
| echo "" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment