Skip to content
Draft
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
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
*.iml
.gradle
/local.properties
/.idea
.DS_Store
/build
/captures
.externalNativeBuild
.cxx
local.properties
app/build/
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,40 @@
# carputerandroid
androd capupter dashboard
# CarPuter Android

Android car computer dashboard application.

## Features

- **Speed Display** – Large, colour-coded GPS speedometer (green / yellow / red)
- **Unit Toggle** – Tap "km/h" or "mph" label to switch units
- **Max Speed Tracker** – Tracks trip maximum speed; long-press to reset
- **Real-Time Clock** – Shows current time and date
- **GPS Status** – Shows GPS accuracy in metres
- **Always-On Screen** – Keeps the display on while driving
- **Immersive Fullscreen** – Hides navigation and status bars
- **Landscape Orientation** – Optimised for dashboard mounting

## Requirements

- Android 8.0 (API 26) or higher
- GPS / Location permission

## Building

1. Open in Android Studio, or build from the command line:

```bash
./gradlew assembleDebug
```

2. Install on device:

```bash
adb install app/build/outputs/apk/debug/app-debug.apk
```

## Permissions

| Permission | Purpose |
|---|---|
| `ACCESS_FINE_LOCATION` | GPS speed reading |
| `ACCESS_COARSE_LOCATION` | Fallback location |
48 changes: 48 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}

android {
namespace 'com.kingfrezz.carputerandroid'
compileSdk 34

defaultConfig {
applicationId "com.kingfrezz.carputerandroid"
minSdk 26
targetSdk 34
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = '17'
}
buildFeatures {
viewBinding true
}
}

dependencies {
implementation 'androidx.core:core-ktx:1.12.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.11.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.7.0'

testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
3 changes: 3 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
28 changes: 28 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

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

<activity
android:name=".MainActivity"
android:exported="true"
android:screenOrientation="landscape"
android:windowSoftInputMode="adjustNothing">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

</application>

</manifest>
225 changes: 225 additions & 0 deletions app/src/main/java/com/kingfrezz/carputerandroid/MainActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package com.kingfrezz.carputerandroid

import android.Manifest
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.View
import android.view.WindowManager
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.kingfrezz.carputerandroid.databinding.ActivityMainBinding
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale

class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding
private lateinit var locationManager: LocationManager
private lateinit var locationListener: LocationListener

private val clockHandler = Handler(Looper.getMainLooper())
private val clockRunnable = object : Runnable {
override fun run() {
updateClock()
clockHandler.postDelayed(this, 1000)
}
}

companion object {
private const val LOCATION_PERMISSION_REQUEST = 1001
private const val MS_TO_KMH = 3.6f
private const val MS_TO_MPH = 2.23694f
}

private var useKmh = true
private var maxSpeed = 0f

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

// Keep screen on while driving
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)

// Full screen immersive mode
@Suppress("DEPRECATION")
window.decorView.systemUiVisibility = (
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
or View.SYSTEM_UI_FLAG_FULLSCREEN
or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
)

binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)

locationManager = getSystemService(LOCATION_SERVICE) as LocationManager

setupLocationListener()
setupClickListeners()
requestLocationPermissions()

binding.tvSpeed.text = "0"
binding.tvUnit.text = if (useKmh) "km/h" else "mph"
binding.tvMaxSpeed.text = getString(R.string.max_speed_format, 0, if (useKmh) "km/h" else "mph")
}

override fun onResume() {
super.onResume()
clockHandler.post(clockRunnable)
if (ContextCompat.checkSelfPermission(
this, Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
startLocationUpdates()
}
}

override fun onPause() {
super.onPause()
clockHandler.removeCallbacks(clockRunnable)
stopLocationUpdates()
}

private fun setupClickListeners() {
// Toggle speed unit on tap; preserve max speed by converting to new unit
binding.tvUnit.setOnClickListener {
if (useKmh) {
// km/h → mph
maxSpeed = maxSpeed / MS_TO_KMH * MS_TO_MPH
useKmh = false
} else {
// mph → km/h
maxSpeed = maxSpeed / MS_TO_MPH * MS_TO_KMH
useKmh = true
}
val unitLabel = if (useKmh) "km/h" else "mph"
binding.tvUnit.text = unitLabel
binding.tvMaxSpeed.text = getString(R.string.max_speed_format, maxSpeed.toInt(), unitLabel)
}

// Reset max speed on long press
binding.tvMaxSpeed.setOnLongClickListener {
maxSpeed = 0f
binding.tvMaxSpeed.text = getString(R.string.max_speed_format, 0, if (useKmh) "km/h" else "mph")
true
}
}

private fun setupLocationListener() {
locationListener = LocationListener { location ->
updateSpeed(location)
updateGpsAccuracy(location)
}
}

private fun updateSpeed(location: Location) {
val speedMs = if (location.hasSpeed()) location.speed else 0f
val speed = if (useKmh) speedMs * MS_TO_KMH else speedMs * MS_TO_MPH
val speedInt = speed.toInt()

if (speed > maxSpeed) {
maxSpeed = speed
binding.tvMaxSpeed.text = getString(
R.string.max_speed_format,
maxSpeed.toInt(),
if (useKmh) "km/h" else "mph"
)
}

binding.tvSpeed.text = speedInt.toString()

// Color code speed using unit-appropriate thresholds:
// green < 60 km/h (37 mph)
// yellow < 100 km/h (62 mph)
// red >= 100 km/h (62 mph)
val lowThreshold = if (useKmh) 60 else 37
val highThreshold = if (useKmh) 100 else 62
val color = when {
speedInt < lowThreshold -> ContextCompat.getColor(this, R.color.speed_low)
speedInt < highThreshold -> ContextCompat.getColor(this, R.color.speed_mid)
else -> ContextCompat.getColor(this, R.color.speed_high)
}
binding.tvSpeed.setTextColor(color)
}

private fun updateGpsAccuracy(location: Location) {
val accuracyText = if (location.hasAccuracy()) {
getString(R.string.gps_accuracy_format, location.accuracy.toInt())
} else {
getString(R.string.gps_active)
}
binding.tvGpsStatus.text = accuracyText
}

private fun updateClock() {
val timeFormat = SimpleDateFormat("HH:mm", Locale.getDefault())
val dateFormat = SimpleDateFormat("EEE, MMM d", Locale.getDefault())
val now = Date()
binding.tvTime.text = timeFormat.format(now)
binding.tvDate.text = dateFormat.format(now)
}

private fun requestLocationPermissions() {
val fineLocation = Manifest.permission.ACCESS_FINE_LOCATION
val coarseLocation = Manifest.permission.ACCESS_COARSE_LOCATION

if (ContextCompat.checkSelfPermission(this, fineLocation) == PackageManager.PERMISSION_GRANTED) {
startLocationUpdates()
} else {
ActivityCompat.requestPermissions(
this,
arrayOf(fineLocation, coarseLocation),
LOCATION_PERMISSION_REQUEST
)
}
}

override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == LOCATION_PERMISSION_REQUEST) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
startLocationUpdates()
} else {
binding.tvGpsStatus.text = getString(R.string.gps_denied)
}
}
}

private fun startLocationUpdates() {
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.ACCESS_FINE_LOCATION
) != PackageManager.PERMISSION_GRANTED
) return

val hasGps = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
if (hasGps) {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
500L,
0f,
locationListener
)
binding.tvGpsStatus.text = getString(R.string.gps_active)
} else {
binding.tvGpsStatus.text = getString(R.string.gps_unavailable)
}
}

private fun stopLocationUpdates() {
locationManager.removeUpdates(locationListener)
}
}
23 changes: 23 additions & 0 deletions app/src/main/res/drawable-v24/ic_launcher_foreground.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- Speedometer icon -->
<path
android:fillColor="#4CAF50"
android:pathData="M54,20 A34,34 0 0,1 88,54 L78,54 A24,24 0 0,0 54,30 Z" />
<path
android:fillColor="#FFFFFF"
android:pathData="M54,20 A34,34 0 1,0 88,54 L78,54 A24,24 0 1,1 54,30 Z" />
<path
android:fillColor="#4CAF50"
android:pathData="M54,54 L72,36"
android:strokeWidth="3"
android:strokeColor="#F44336"
android:strokeLineCap="round" />
<path
android:fillColor="#FFFFFF"
android:pathData="M54,54 m-4,0 a4,4 0 1,0 8,0 a4,4 0 1,0 -8,0" />
</vector>
9 changes: 9 additions & 0 deletions app/src/main/res/drawable/chip_background.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/chip_bg" />
<corners android:radius="20dp" />
<stroke
android:width="1dp"
android:color="#2A2A2A" />
</shape>
Loading