"Notice: as you see digital absence in others, where does your own presence go?"
HeadsUp is a local-first Android application designed to map collective digital attention patterns and assist users in cultivating physical presence. Instead of traditional screen-time trackers that treat digital distraction as an individual failing, HeadsUp reframes it as a collective, atmospheric condition—like fog or pollution—that we generate together.
Observers use a hardware-key, screenless logger to register incidents of phone absorption around them. This data is geohashed, stored locally in a Room database, and synced in real-time to a serverless, user-owned Google Sheets backend to construct an ambient geospatial ledger of human attention.
Modern mobile interfaces are built to capture and commodify human attention, creating spaces of digital absence—where people are physically present but mentally detached. HeadsUp provides two key mechanisms to break this loop:
To break the cycle of checking one's own phone, the user does not interact with a UI. Instead:
- While holding the phone down by their side or in their pocket, the user presses the Volume Up or Volume Down button upon noticing screen absorption.
- The app intercepts this hardware event in the background, fetches a coarse location, encodes it, commits a record to the local SQLite database, and plays a subtle haptic feedback tick.
- The action serves as a micro-meditation: by noticing screen distraction, the user immediately pulls themselves back into physical awareness.
Logged events are aggregated into Geohash-5 zones (approximately 4.9km × 4.9km squares) and mapped.
- Active distraction zones glow in warm brass geometric grids.
- Inactive zones settle into a deep graphite hue.
- The map visualizes attention not as a coordinate of surveillance, but as an atmospheric weather condition.
HeadsUp is built using a local-first, decentralized, and serverless architecture:
graph TD
A[Hardware Volume Click / Screen Tap] --> B[Room Local SQLite DB]
B --> C[HeadsUpRepository]
C -->|Unsynced Taps| D[SheetsNetworkClient - Retrofit]
D -->|HTTPS POST JSON| E[Google Apps Script Web App]
E -->|Write Row| F[(Google Sheets Database)]
F -->|Read Stream| E
E -->|HTTPS GET JSON| C
C -->|Live Event Flow| G[HeadsUpViewModel]
G -->|Compose State| H[Jetpack Compose UI & Leaflet Map WebView]
The local database stores two primary entities to support offline capabilities and caching:
@Entity(tableName = "taps")
data class TapRecord(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val latitude: Double,
val longitude: Double,
val geohash5: String,
val geohash7: String,
val createdAt: Long = System.currentTimeMillis(),
val isSynced: Boolean = false
)
@Entity(tableName = "user_session")
data class UserSession(
@PrimaryKey val id: Int = 1,
val uid: String = UUID.randomUUID().toString(),
val nickname: String = "Observer_" + uid.take(6),
val createdAt: Long = System.currentTimeMillis(),
val totalTaps: Int = 0,
val currentStreak: Int = 0,
val longestStreak: Int = 0,
val lastTapAt: Long = 0L,
val lastTapGeohash: String = "",
val publicOnMap: Boolean = true,
val onboardingCompleted: Boolean = false,
val googleSheetsUrl: String
)To avoid server maintenance costs, the app uses a serverless, user-owned Google Sheets backend deployed via Google Apps Script:
doPost(e): Appends incoming JSON Tap records into the Google Sheet. Generates UUIDs if missing.doGet(e): Reads the spreadsheet rows, aggregates taps by Geohash-5, sorts them, and returns a JSON payload containing the last 50 events and all spatial aggregates.- Refer to BACKEND_SETUP.md for full script source code and deployment instructions.
- Android Studio (Koala or later)
- Java Development Kit (JDK) 17 or higher
- Android SDK 36 (targetSdk 36, minSdk 24)
Configure your local environment parameters by copying the example environment template:
cp .env.example .envIn your .env file, specify your Google Sheets script URL under backend_url if you wish to initialize the app with a default endpoint.
Specify your local Android SDK location inside local.properties:
sdk.dir=C\:\\Users\\YourUsername\\AppData\\Local\\Android\\SdkUse the Gradle wrapper to clean build, compile, and run:
- Clean Build Cache:
./gradlew clean
- Compile Debug APK:
Output binary:
./gradlew assembleDebug
releases/HeadsUp-debug.apk - Compile Production Release APK:
Output binary:
./gradlew assembleRelease
releases/HeadsUp-release.apk - Run Unit Tests:
./gradlew test
app/build.gradle.kts is pre-configured to build without crashing on missing environment keys by falling back to the debug keystore when production environment variables are not found:
signingConfigs {
create("release") {
val keystorePath = System.getenv("KEYSTORE_PATH")
if (keystorePath != null && keystorePath.isNotEmpty()) {
storeFile = file(keystorePath)
storePassword = System.getenv("STORE_PASSWORD")
keyAlias = "upload"
keyPassword = System.getenv("KEY_PASSWORD")
} else {
storeFile = file("${rootDir}/debug.keystore")
storePassword = "android"
keyAlias = "androiddebugkey"
keyPassword = "android"
}
}
}The map view is rendered in a custom dark-themed Leaflet JS template inside an Android WebView. To protect the Kotlin-to-WebView interface bridge from code obfuscation during minification, the following rule is configured in app/proguard-rules.pro:
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
- Coarse Geo-Fencing: Exact GPS coordinates are never leaked to the shared map. The app decodes coordinates into a Geohash-5 string (representing a grid boundary of ~4.9km × 4.9km) before syncing, protecting the observer's exact location.
- Zero Tracker Telemetry: The app does not compile with third-party tracking, analytics, crash-telemetry, or advertising SDKs.
- Data Sovereignty: Observations are written directly to your own Google Sheet. No corporate databases are utilized. You own, edit, and retain 100% of your data logs.
Contributions that align with the philosophy of reclaiming attention and cultivating human presence are welcome. Please refer to:
- CONTRIBUTING.md for coding and design standards.
- SUPPORT.md for support guidelines and reporting bugs.