Initial commit: Android & Garmin Remote Camera App with Live Preview
This commit is contained in:
82
android-app/app/build.gradle.kts
Normal file
82
android-app/app/build.gradle.kts
Normal file
@@ -0,0 +1,82 @@
|
||||
plugins {
|
||||
id("com.android.application")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.example.fotocompanion"
|
||||
compileSdk = 34
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "com.example.fotocompanion"
|
||||
minSdk = 26 // Android 8.0, reasonable for modern apps
|
||||
targetSdk = 34
|
||||
versionCode = 1
|
||||
versionName = "1.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
vectorDrawables {
|
||||
useSupportLibrary = true
|
||||
}
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = false
|
||||
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
|
||||
}
|
||||
}
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
}
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = "1.5.4"
|
||||
}
|
||||
packaging {
|
||||
resources {
|
||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
implementation("androidx.core:core-ktx:1.12.0")
|
||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
|
||||
implementation("androidx.activity:activity-compose:1.8.1")
|
||||
implementation(platform("androidx.compose:compose-bom:2023.08.00"))
|
||||
implementation("androidx.compose.ui:ui")
|
||||
implementation("androidx.compose.ui:ui-graphics")
|
||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||
implementation("androidx.compose.material3:material3")
|
||||
|
||||
// CameraX
|
||||
val cameraxVersion = "1.3.0"
|
||||
implementation("androidx.camera:camera-core:${cameraxVersion}")
|
||||
implementation("androidx.camera:camera-camera2:${cameraxVersion}")
|
||||
implementation("androidx.camera:camera-lifecycle:${cameraxVersion}")
|
||||
implementation("androidx.camera:camera-view:${cameraxVersion}")
|
||||
implementation("androidx.camera:camera-extensions:${cameraxVersion}")
|
||||
|
||||
// Permissions
|
||||
implementation("com.google.accompanist:accompanist-permissions:0.32.0")
|
||||
|
||||
// Local Libs (Connect IQ SDK)
|
||||
// User needs to place connectiq.jar in app/libs/
|
||||
implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar"))))
|
||||
|
||||
testImplementation("junit:junit:4.13.2")
|
||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||
androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00"))
|
||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||
}
|
||||
33
android-app/app/src/main/AndroidManifest.xml
Normal file
33
android-app/app/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
<uses-feature android:name="android.hardware.camera.any" />
|
||||
<uses-permission android:name="android.permission.CAMERA" />
|
||||
|
||||
<!-- Connect IQ Permissions (some might be needed depending on SDK version) -->
|
||||
<uses-permission android:name="com.garmin.android.connectiq.permission.CONNECTIQ_ALLOW_SDK_ACCESS" />
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||
android:fullBackupContent="@xml/backup_rules"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="Foto Companion"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.FotoCompanion"
|
||||
tools:targetApi="31">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.FotoCompanion">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.example.fotocompanion
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import com.garmin.android.connectiq.ConnectIQ
|
||||
import com.garmin.android.connectiq.IQApp
|
||||
import com.garmin.android.connectiq.IQDevice
|
||||
import com.garmin.android.connectiq.exception.InvalidStateException
|
||||
import com.garmin.android.connectiq.exception.ServiceUnavailableException
|
||||
|
||||
class ConnectIQHelper(context: Context, private val onCommandReceived: (String) -> Unit) {
|
||||
|
||||
private var connectIQ: ConnectIQ? = null
|
||||
private var connectedDevice: IQDevice? = null
|
||||
// Replace with the UUID from your Garmin App Manifest
|
||||
private val MY_APP_ID = "561284d7-4633-4d43-9876-068305612345"
|
||||
|
||||
init {
|
||||
// Initialize ConnectIQ
|
||||
// Note: This requires the ConnectIQ SDK library to be in your libs folder
|
||||
try {
|
||||
connectIQ = ConnectIQ.getInstance(context, ConnectIQ.IQConnectType.WIRELESS)
|
||||
connectIQ?.initialize(context, true, object : ConnectIQ.ConnectIQListener {
|
||||
override fun onSdkReady() {
|
||||
Log.d("ConnectIQ", "SDK Ready")
|
||||
findDevices()
|
||||
}
|
||||
|
||||
override fun onInitializeError(errStatus: ConnectIQ.IQSdkErrorStatus) {
|
||||
Log.e("ConnectIQ", "Initialization error: $errStatus")
|
||||
}
|
||||
|
||||
override fun onSdkShutDown() {
|
||||
Log.d("ConnectIQ", "SDK Shutdown")
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
Log.e("ConnectIQ", "Failed to initialize ConnectIQ", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun findDevices() {
|
||||
try {
|
||||
val devices = connectIQ?.knownDevices ?: return
|
||||
if (devices.isNotEmpty()) {
|
||||
// Just pick the first one for now
|
||||
val device = devices[0]
|
||||
connectToDevice(device)
|
||||
}
|
||||
} catch (e: InvalidStateException) {
|
||||
Log.e("ConnectIQ", "Invalid State during findDevices", e)
|
||||
} catch (e: ServiceUnavailableException) {
|
||||
Log.e("ConnectIQ", "Service Unavailable during findDevices", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectToDevice(device: IQDevice) {
|
||||
try {
|
||||
connectIQ?.registerForDeviceEvents(device, object : ConnectIQ.IQDeviceEventListener {
|
||||
override fun onDeviceStatusChanged(dev: IQDevice, status: IQDevice.IQDeviceStatus) {
|
||||
if (status == IQDevice.IQDeviceStatus.CONNECTED) {
|
||||
Log.d("ConnectIQ", "Device Connected: ${dev.friendlyName}")
|
||||
connectedDevice = dev
|
||||
listenForAppEvents(dev)
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
Log.e("ConnectIQ", "Error connecting to device", e)
|
||||
}
|
||||
}
|
||||
|
||||
private fun listenForAppEvents(device: IQDevice) {
|
||||
val app = IQApp(MY_APP_ID)
|
||||
try {
|
||||
connectIQ?.registerForAppEvents(device, app) { _, _, message, _ ->
|
||||
// Handle messages from watch
|
||||
if (message is List<*>) {
|
||||
// Often messages come as lists
|
||||
val cmd = message.firstOrNull()?.toString()
|
||||
if (cmd != null) onCommandReceived(cmd)
|
||||
} else {
|
||||
onCommandReceived(message.toString())
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ConnectIQ", "Error registering for app events", e)
|
||||
}
|
||||
}
|
||||
|
||||
fun sendImageToWatch(imageBytes: ByteArray) {
|
||||
val device = connectedDevice ?: return
|
||||
val app = IQApp(MY_APP_ID)
|
||||
|
||||
try {
|
||||
// Check if we need to chunk? SDK handles some fragmentation, but keeping it small is safe.
|
||||
// sendMessage sends an object. A ByteArray is an object.
|
||||
// Note: If image is too large, this will throw an exception or fail silently.
|
||||
// For live preview, we need to be efficient.
|
||||
|
||||
// Converting ByteArray to a list of Integers might be required by Monkey C JSON parsing?
|
||||
// No, Monkey C handles Byte Arrays.
|
||||
|
||||
// However, sendMessage is for "Object Messages" (JSON-like).
|
||||
// Image data is binary.
|
||||
// Connect IQ 3.1+ supports primitive types including Byte Arrays.
|
||||
|
||||
// Send as a list to match expected structure on watch if needed,
|
||||
// or just the raw array if the watch expects it.
|
||||
// Let's send a map: { "img": [bytes...] } or just bytes
|
||||
|
||||
connectIQ?.sendMessage(device, app, imageBytes) { device, app, status ->
|
||||
if (status != ConnectIQ.IQMessageStatus.SUCCESS) {
|
||||
Log.w("ConnectIQ", "Message failed: $status")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e("ConnectIQ", "Error sending image", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,283 @@
|
||||
package com.example.fotocompanion
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.ImageFormat
|
||||
import android.graphics.Matrix
|
||||
import android.graphics.Rect
|
||||
import android.graphics.YuvImage
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.camera.core.CameraSelector
|
||||
import androidx.camera.core.ImageAnalysis
|
||||
import androidx.camera.core.ImageCapture
|
||||
import androidx.camera.core.ImageCaptureException
|
||||
import androidx.camera.core.ImageProxy
|
||||
import androidx.camera.core.Preview
|
||||
import androidx.camera.lifecycle.ProcessCameraProvider
|
||||
import androidx.camera.view.PreviewView
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.viewinterop.AndroidView
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
private var imageCapture: ImageCapture? = null
|
||||
// Placeholder for ConnectIQ Helper
|
||||
private var connectIQHelper: ConnectIQHelper? = null
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Initialize ConnectIQ Helper
|
||||
connectIQHelper = ConnectIQHelper(this) { command ->
|
||||
handleCommand(command)
|
||||
}
|
||||
|
||||
setContent {
|
||||
MaterialTheme {
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
FotoCompanionApp(
|
||||
onImageAnalyzed = { byteArray ->
|
||||
// Send image to watch via ConnectIQ
|
||||
connectIQHelper?.sendImageToWatch(byteArray)
|
||||
},
|
||||
onCaptureInit = { capture ->
|
||||
imageCapture = capture
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleCommand(command: String) {
|
||||
if (command == "TRIGGER") {
|
||||
takePhoto()
|
||||
}
|
||||
}
|
||||
|
||||
private fun takePhoto() {
|
||||
val imageCapture = imageCapture ?: return
|
||||
|
||||
// Create time stamped name and MediaStore entry.
|
||||
val name = java.text.SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", java.util.Locale.US)
|
||||
.format(System.currentTimeMillis())
|
||||
|
||||
val contentValues = android.content.ContentValues().apply {
|
||||
put(android.provider.MediaStore.MediaColumns.DISPLAY_NAME, name)
|
||||
put(android.provider.MediaStore.MediaColumns.MIME_TYPE, "image/jpeg")
|
||||
if(android.os.Build.VERSION.SDK_INT > android.os.Build.VERSION_CODES.P) {
|
||||
put(android.provider.MediaStore.Images.Media.RELATIVE_PATH, "Pictures/FotoCompanion")
|
||||
}
|
||||
}
|
||||
|
||||
val outputOptions = ImageCapture.OutputFileOptions
|
||||
.Builder(contentResolver, android.provider.MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
|
||||
.build()
|
||||
|
||||
imageCapture.takePicture(
|
||||
outputOptions,
|
||||
ContextCompat.getMainExecutor(this),
|
||||
object : ImageCapture.OnImageSavedCallback {
|
||||
override fun onError(exc: ImageCaptureException) {
|
||||
Log.e("FotoCompanion", "Photo capture failed: ${exc.message}", exc)
|
||||
}
|
||||
|
||||
override fun onImageSaved(output: ImageCapture.OutputFileResults) {
|
||||
val msg = "Photo capture succeeded: ${output.savedUri}"
|
||||
Log.d("FotoCompanion", msg)
|
||||
// Optional: Send success message back to watch
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun FotoCompanionApp(
|
||||
onImageAnalyzed: (ByteArray) -> Unit,
|
||||
onCaptureInit: (ImageCapture) -> Unit
|
||||
) {
|
||||
val cameraPermissionState = rememberPermissionState(android.Manifest.permission.CAMERA)
|
||||
|
||||
if (cameraPermissionState.status.isGranted) {
|
||||
CameraPreviewScreen(onImageAnalyzed, onCaptureInit)
|
||||
} else {
|
||||
LaunchedEffect(Unit) {
|
||||
cameraPermissionState.launchPermissionRequest()
|
||||
}
|
||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Text("Waiting for Camera Permission...")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CameraPreviewScreen(
|
||||
onImageAnalyzed: (ByteArray) -> Unit,
|
||||
onCaptureInit: (ImageCapture) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val lifecycleOwner = LocalLifecycleOwner.current
|
||||
|
||||
// Last sent timestamp to control FPS
|
||||
var lastSentTime by remember { mutableStateOf(0L) }
|
||||
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
val previewView = PreviewView(ctx)
|
||||
val executor = ContextCompat.getMainExecutor(ctx)
|
||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(ctx)
|
||||
|
||||
cameraProviderFuture.addListener({
|
||||
val cameraProvider = cameraProviderFuture.get()
|
||||
|
||||
// Preview
|
||||
val preview = Preview.Builder().build().also {
|
||||
it.setSurfaceProvider(previewView.surfaceProvider)
|
||||
}
|
||||
|
||||
// Image Capture
|
||||
val imageCapture = ImageCapture.Builder().build()
|
||||
onCaptureInit(imageCapture)
|
||||
|
||||
// Image Analysis (for Watch Preview)
|
||||
val imageAnalyzer = ImageAnalysis.Builder()
|
||||
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||
.build()
|
||||
.also {
|
||||
it.setAnalyzer(Executors.newSingleThreadExecutor()) { imageProxy ->
|
||||
val currentTime = System.currentTimeMillis()
|
||||
// Limit FPS (e.g., 2 FPS = 500ms)
|
||||
if (currentTime - lastSentTime > 500) {
|
||||
// Target size for watch
|
||||
val width = 120
|
||||
val height = 120
|
||||
val bytes = imageProxy.toGarminRawBytes(width, height)
|
||||
if (bytes != null) {
|
||||
onImageAnalyzed(bytes)
|
||||
lastSentTime = currentTime
|
||||
}
|
||||
}
|
||||
imageProxy.close()
|
||||
}
|
||||
}
|
||||
|
||||
// Select back camera
|
||||
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
|
||||
|
||||
try {
|
||||
cameraProvider.unbindAll()
|
||||
cameraProvider.bindToLifecycle(
|
||||
lifecycleOwner,
|
||||
cameraSelector,
|
||||
preview,
|
||||
imageCapture,
|
||||
imageAnalyzer
|
||||
)
|
||||
} catch(exc: Exception) {
|
||||
Log.e("FotoCompanion", "Use case binding failed", exc)
|
||||
}
|
||||
|
||||
}, executor)
|
||||
previewView
|
||||
},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
|
||||
Box(modifier = Modifier.fillMaxSize().padding(16.dp), contentAlignment = Alignment.BottomCenter) {
|
||||
Text("Foto Companion Running", color = Color.White)
|
||||
}
|
||||
}
|
||||
|
||||
// Extension to convert ImageProxy to Garmin Raw Bytes (8-bit palette index)
|
||||
fun ImageProxy.toGarminRawBytes(reqWidth: Int, reqHeight: Int): ByteArray? {
|
||||
if (format != ImageFormat.YUV_420_888) return null
|
||||
|
||||
val yBuffer = planes[0].buffer
|
||||
val uBuffer = planes[1].buffer
|
||||
val vBuffer = planes[2].buffer
|
||||
|
||||
val ySize = yBuffer.remaining()
|
||||
val uSize = uBuffer.remaining()
|
||||
val vSize = vBuffer.remaining()
|
||||
|
||||
val nv21 = ByteArray(ySize + uSize + vSize)
|
||||
|
||||
yBuffer.get(nv21, 0, ySize)
|
||||
vBuffer.get(nv21, ySize, vSize)
|
||||
uBuffer.get(nv21, ySize + vSize, uSize)
|
||||
|
||||
val yuvImage = YuvImage(nv21, ImageFormat.NV21, this.width, this.height, null)
|
||||
val out = ByteArrayOutputStream()
|
||||
|
||||
// Compress to JPEG first to handle YUV->RGB conversion easily (inefficient but standard without libs)
|
||||
yuvImage.compressToJpeg(Rect(0, 0, this.width, this.height), 50, out)
|
||||
val originalBytes = out.toByteArray()
|
||||
|
||||
// Decode to Bitmap
|
||||
val bitmap = BitmapFactory.decodeByteArray(originalBytes, 0, originalBytes.size)
|
||||
// Scale
|
||||
val scaledBitmap = Bitmap.createScaledBitmap(bitmap, reqWidth, reqHeight, false)
|
||||
|
||||
// Quantize to Garmin 64 colors (simulated simple mapping)
|
||||
// Garmin 64 color palette usually: 00, 55, AA, FF combinations.
|
||||
// We will map RGB to 6 bits (2 bits R, 2 bits G, 2 bits B) -> 64 colors
|
||||
// Format: 00RRGGBB
|
||||
|
||||
val pixels = IntArray(reqWidth * reqHeight)
|
||||
scaledBitmap.getPixels(pixels, 0, reqWidth, 0, 0, reqWidth, reqHeight)
|
||||
|
||||
val rawBytes = ByteArray(reqWidth * reqHeight)
|
||||
|
||||
for (i in pixels.indices) {
|
||||
val color = pixels[i]
|
||||
val r = (color shr 16) and 0xFF
|
||||
val g = (color shr 8) and 0xFF
|
||||
val b = color and 0xFF
|
||||
|
||||
// Map 0-255 to 0-3 (2 bits)
|
||||
val r2 = (r / 85) // 0..3
|
||||
val g2 = (g / 85)
|
||||
val b2 = (b / 85)
|
||||
|
||||
// Garmin 64 color index: 00RRGGBB (binary) where RR, GG, BB are 00, 01, 10, 11
|
||||
// Actually Garmin palette index is specific, but mapping to standard VGA-like 6 bits is easiest
|
||||
// if we construct the palette on the watch side similarly.
|
||||
|
||||
val index = (r2 shl 4) or (g2 shl 2) or b2
|
||||
rawBytes[i] = index.toByte()
|
||||
}
|
||||
|
||||
return rawBytes
|
||||
}
|
||||
BIN
android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
BIN
android-app/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 675 B |
7
android-app/app/src/main/res/values/themes.xml
Normal file
7
android-app/app/src/main/res/values/themes.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.FotoCompanion" parent="android:Theme.Material.Light.NoActionBar">
|
||||
<!-- Customize your light theme here. -->
|
||||
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
|
||||
</style>
|
||||
</resources>
|
||||
5
android-app/app/src/main/res/xml/backup_rules.xml
Normal file
5
android-app/app/src/main/res/xml/backup_rules.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<full-backup-content>
|
||||
<include domain="user" path="." />
|
||||
<include domain="root" path="." />
|
||||
</full-backup-content>
|
||||
11
android-app/app/src/main/res/xml/data_extraction_rules.xml
Normal file
11
android-app/app/src/main/res/xml/data_extraction_rules.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<data-extraction-rules>
|
||||
<cloud-backup>
|
||||
<include domain="user" path="." />
|
||||
<include domain="root" path="." />
|
||||
</cloud-backup>
|
||||
<device-transfer>
|
||||
<include domain="user" path="." />
|
||||
<include domain="root" path="." />
|
||||
</device-transfer>
|
||||
</data-extraction-rules>
|
||||
5
android-app/build.gradle.kts
Normal file
5
android-app/build.gradle.kts
Normal file
@@ -0,0 +1,5 @@
|
||||
// Top-level build file where you can add configuration options common to all sub-projects/modules.
|
||||
plugins {
|
||||
id("com.android.application") version "8.2.0" apply false
|
||||
id("org.jetbrains.kotlin.android") version "1.9.20" apply false
|
||||
}
|
||||
17
android-app/settings.gradle.kts
Normal file
17
android-app/settings.gradle.kts
Normal file
@@ -0,0 +1,17 @@
|
||||
pluginManagement {
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
gradlePluginPortal()
|
||||
}
|
||||
}
|
||||
dependencyResolutionManagement {
|
||||
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
|
||||
repositories {
|
||||
google()
|
||||
mavenCentral()
|
||||
maven("https://jitpack.io") // Often useful
|
||||
}
|
||||
}
|
||||
rootProject.name = "FotoCompanion"
|
||||
include(":app")
|
||||
Reference in New Issue
Block a user