# MyCloudVault Android Integration Guide

Complete guide for building an Android client app for MyCloudVault backup system.

## Table of Contents

1. [Prerequisites](#prerequisites)
2. [Project Setup](#project-setup)
3. [Architecture Overview](#architecture-overview)
4. [Implementation Steps](#implementation-steps)
5. [Code Examples](#code-examples)
6. [Testing](#testing)
7. [Best Practices](#best-practices)

---

## Prerequisites

### Development Environment

- **Android Studio**: Arctic Fox or later
- **Minimum SDK**: API 26 (Android 8.0)
- **Target SDK**: API 34 (Android 14)
- **Language**: Kotlin 1.9+
- **Build System**: Gradle 8.0+

### Required Permissions

Add to `AndroidManifest.xml`:

```xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Network access -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    
    <!-- Storage access -->
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
        android:maxSdkVersion="28" />
    <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
        tools:ignore="ScopedStorage" />
    
    <!-- Backup permissions -->
    <uses-permission android:name="android.permission.BACKUP" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    
    <!-- Wake lock for background operations -->
    <uses-permission android:name="android.permission.WAKE_LOCK" />
</manifest>
```

### Dependencies

Add to `build.gradle.kts` (app module):

```kotlin
dependencies {
    // Core Android
    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
    implementation("androidx.work:work-runtime-ktx:2.9.0")
    
    // UI
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    
    // Networking
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
    
    // Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
    
    // Encryption
    implementation("androidx.security:security-crypto:1.1.0-alpha06")
    
    // Room Database (for local metadata)
    implementation("androidx.room:room-runtime:2.6.1")
    implementation("androidx.room:room-ktx:2.6.1")
    kapt("androidx.room:room-compiler:2.6.1")
    
    // Preferences
    implementation("androidx.datastore:datastore-preferences:1.0.0")
}
```

---

## Project Setup

### 1. Create Project Structure

```
app/src/main/java/com/yourcompany/mycloudvault/
├── api/
│   ├── MyCloudVaultApi.kt
│   ├── ApiClient.kt
│   └── models/
│       ├── Device.kt
│       ├── Backup.kt
│       └── ApiResponse.kt
├── data/
│   ├── repository/
│   │   ├── BackupRepository.kt
│   │   └── DeviceRepository.kt
│   ├── local/
│   │   ├── AppDatabase.kt
│   │   └── dao/
│   └── prefs/
│       └── PreferencesManager.kt
├── domain/
│   ├── usecase/
│   │   ├── StartBackupUseCase.kt
│   │   ├── UploadBackupUseCase.kt
│   │   └── VerifyBackupUseCase.kt
│   └── model/
├── ui/
│   ├── main/
│   ├── pairing/
│   ├── backup/
│   └── settings/
├── service/
│   ├── BackupService.kt
│   └── BackupWorker.kt
├── util/
│   ├── EncryptionUtil.kt
│   ├── FileUtil.kt
│   └── NetworkUtil.kt
└── MyCloudVaultApplication.kt
```

---

## Architecture Overview

### MVVM Architecture

```
View (Activity/Fragment)
    ↓
ViewModel
    ↓
UseCase
    ↓
Repository
    ↓
[API Client] ← → [Local Database]
```

### Key Components

1. **API Client**: Retrofit interface for server communication
2. **Repository**: Data layer abstraction
3. **UseCase**: Business logic for backup operations
4. **ViewModel**: UI state management
5. **Service**: Background backup operations
6. **Worker**: Scheduled backup jobs

---

## Implementation Steps

### Step 1: API Client Setup

**MyCloudVaultApi.kt**

```kotlin
package com.yourcompany.mycloudvault.api

import com.yourcompany.mycloudvault.api.models.*
import okhttp3.MultipartBody
import okhttp3.RequestBody
import retrofit2.Response
import retrofit2.http.*

interface MyCloudVaultApi {
    
    @GET("health")
    suspend fun healthCheck(): Response<ApiResponse<HealthStatus>>
    
    @POST("devices/pair")
    suspend fun pairDevice(
        @Body request: PairDeviceRequest
    ): Response<ApiResponse<PairDeviceResponse>>
    
    @GET("devices/{deviceId}")
    suspend fun getDevice(
        @Path("deviceId") deviceId: String
    ): Response<ApiResponse<Device>>
    
    @POST("backup/start")
    suspend fun startBackup(
        @Body request: StartBackupRequest
    ): Response<ApiResponse<Backup>>
    
    @Multipart
    @POST("backup/upload-chunk")
    suspend fun uploadChunk(
        @Part("deviceId") deviceId: RequestBody,
        @Part("backupId") backupId: RequestBody,
        @Part("chunkIndex") chunkIndex: RequestBody,
        @Part("totalChunks") totalChunks: RequestBody,
        @Part chunk: MultipartBody.Part
    ): Response<ApiResponse<UploadProgress>>
    
    @Multipart
    @POST("backup/upload-encrypted")
    suspend fun uploadEncrypted(
        @Part("deviceId") deviceId: RequestBody,
        @Part("backupId") backupId: RequestBody,
        @Part("encryptionMetadata") metadata: RequestBody,
        @Part encryptedData: MultipartBody.Part
    ): Response<ApiResponse<Backup>>
    
    @POST("backup/verify")
    suspend fun verifyBackup(
        @Body request: VerifyBackupRequest
    ): Response<ApiResponse<VerificationResult>>
    
    @GET("backup/list")
    suspend fun listBackups(
        @Query("deviceId") deviceId: String,
        @Query("limit") limit: Int = 50,
        @Query("offset") offset: Int = 0,
        @Query("status") status: String? = null
    ): Response<ApiResponse<BackupList>>
    
    @Streaming
    @GET("backup/download/{backupId}")
    suspend fun downloadBackup(
        @Path("backupId") backupId: String,
        @Query("deviceId") deviceId: String
    ): Response<okhttp3.ResponseBody>
    
    @DELETE("backup/delete/{backupId}")
    suspend fun deleteBackup(
        @Path("backupId") backupId: String,
        @Query("deviceId") deviceId: String
    ): Response<ApiResponse<Unit>>
    
    @POST("encryption/generate-key")
    suspend fun generateEncryptionKey(
        @Body request: GenerateKeyRequest
    ): Response<ApiResponse<EncryptionKey>>
}
```

**ApiClient.kt**

```kotlin
package com.yourcompany.mycloudvault.api

import android.content.Context
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

class ApiClient(private val context: Context) {
    
    private val prefs = context.getSharedPreferences("mycloudvault", Context.MODE_PRIVATE)
    
    private val authInterceptor = Interceptor { chain ->
        val deviceId = prefs.getString("device_id", null)
        val apiKey = prefs.getString("api_key", null)
        
        val request = chain.request().newBuilder().apply {
            deviceId?.let { addHeader("X-Device-ID", it) }
            apiKey?.let { addHeader("X-API-Key", it) }
        }.build()
        
        chain.proceed(request)
    }
    
    private val loggingInterceptor = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    }
    
    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(authInterceptor)
        .addInterceptor(loggingInterceptor)
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(60, TimeUnit.SECONDS)
        .writeTimeout(60, TimeUnit.SECONDS)
        .build()
    
    private val retrofit = Retrofit.Builder()
        .baseUrl(getBaseUrl())
        .client(okHttpClient)
        .addConverterFactory(GsonConverterFactory.create())
        .build()
    
    val api: MyCloudVaultApi = retrofit.create(MyCloudVaultApi::class.java)
    
    private fun getBaseUrl(): String {
        return prefs.getString("server_url", "http://192.168.1.100:8080/api/") ?: ""
    }
    
    fun updateServerUrl(url: String) {
        prefs.edit().putString("server_url", url).apply()
    }
    
    fun saveCredentials(deviceId: String, apiKey: String) {
        prefs.edit()
            .putString("device_id", deviceId)
            .putString("api_key", apiKey)
            .apply()
    }
    
    fun clearCredentials() {
        prefs.edit()
            .remove("device_id")
            .remove("api_key")
            .apply()
    }
}
```

### Step 2: Data Models

**Device.kt**

```kotlin
package com.yourcompany.mycloudvault.api.models

import com.google.gson.annotations.SerializedName

data class Device(
    val id: Int,
    @SerializedName("deviceId") val deviceId: String,
    @SerializedName("deviceName") val deviceName: String,
    @SerializedName("deviceModel") val deviceModel: String?,
    @SerializedName("osVersion") val osVersion: String?,
    val status: String,
    @SerializedName("lastSeen") val lastSeen: String
)

data class PairDeviceRequest(
    val deviceId: String,
    val deviceName: String,
    val deviceModel: String,
    val osVersion: String,
    val pairingCode: String
)

data class PairDeviceResponse(
    val device: Device,
    val apiKey: String
)
```

**Backup.kt**

```kotlin
package com.yourcompany.mycloudvault.api.models

import com.google.gson.annotations.SerializedName

data class Backup(
    val id: Int,
    @SerializedName("backupId") val backupId: String,
    @SerializedName("deviceId") val deviceId: String,
    @SerializedName("backupType") val backupType: String,
    val status: String,
    val size: Long,
    @SerializedName("uploadedSize") val uploadedSize: Long,
    @SerializedName("fileCount") val fileCount: Int,
    @SerializedName("encryptionEnabled") val encryptionEnabled: Boolean,
    @SerializedName("verificationStatus") val verificationStatus: String?,
    @SerializedName("startedAt") val startedAt: String,
    @SerializedName("completedAt") val completedAt: String?
)

data class StartBackupRequest(
    val deviceId: String,
    val backupType: String,
    val totalSize: Long,
    val fileCount: Int,
    val metadata: Map<String, Any>? = null
)

data class UploadProgress(
    val chunkIndex: Int,
    val uploadedSize: Long,
    val progress: Float
)

data class BackupList(
    val backups: List<Backup>,
    val total: Int,
    val limit: Int,
    val offset: Int
)
```

### Step 3: Encryption Utility

**EncryptionUtil.kt**

```kotlin
package com.yourcompany.mycloudvault.util

import android.util.Base64
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.KeyGenerator
import javax.crypto.SecretKey
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec

object EncryptionUtil {
    
    private const val ALGORITHM = "AES/GCM/NoPadding"
    private const val KEY_SIZE = 256
    private const val IV_SIZE = 12
    private const val TAG_SIZE = 128
    
    fun generateKey(): SecretKey {
        val keyGenerator = KeyGenerator.getInstance("AES")
        keyGenerator.init(KEY_SIZE)
        return keyGenerator.generateKey()
    }
    
    fun generateIV(): ByteArray {
        val iv = ByteArray(IV_SIZE)
        SecureRandom().nextBytes(iv)
        return iv
    }
    
    fun encrypt(data: ByteArray, key: SecretKey, iv: ByteArray): EncryptionResult {
        val cipher = Cipher.getInstance(ALGORITHM)
        val spec = GCMParameterSpec(TAG_SIZE, iv)
        cipher.init(Cipher.ENCRYPT_MODE, key, spec)
        
        val encryptedData = cipher.doFinal(data)
        val authTag = encryptedData.takeLast(TAG_SIZE / 8).toByteArray()
        val ciphertext = encryptedData.dropLast(TAG_SIZE / 8).toByteArray()
        
        return EncryptionResult(
            ciphertext = ciphertext,
            iv = iv,
            authTag = authTag
        )
    }
    
    fun decrypt(encryptedData: ByteArray, key: SecretKey, iv: ByteArray, authTag: ByteArray): ByteArray {
        val cipher = Cipher.getInstance(ALGORITHM)
        val spec = GCMParameterSpec(TAG_SIZE, iv)
        cipher.init(Cipher.DECRYPT_MODE, key, spec)
        
        val dataWithTag = encryptedData + authTag
        return cipher.doFinal(dataWithTag)
    }
    
    fun keyToBase64(key: SecretKey): String {
        return Base64.encodeToString(key.encoded, Base64.NO_WRAP)
    }
    
    fun base64ToKey(base64: String): SecretKey {
        val keyBytes = Base64.decode(base64, Base64.NO_WRAP)
        return SecretKeySpec(keyBytes, "AES")
    }
    
    data class EncryptionResult(
        val ciphertext: ByteArray,
        val iv: ByteArray,
        val authTag: ByteArray
    )
}
```

### Step 4: Backup Service

**BackupService.kt**

```kotlin
package com.yourcompany.mycloudvault.service

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import com.yourcompany.mycloudvault.R
import com.yourcompany.mycloudvault.api.ApiClient
import com.yourcompany.mycloudvault.util.EncryptionUtil
import kotlinx.coroutines.*
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File

class BackupService : Service() {
    
    private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    private lateinit var apiClient: ApiClient
    private val notificationId = 1001
    
    override fun onCreate() {
        super.onCreate()
        apiClient = ApiClient(this)
        createNotificationChannel()
    }
    
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        val backupType = intent?.getStringExtra("backup_type") ?: "full"
        val encryptionEnabled = intent?.getBooleanExtra("encryption_enabled", true) ?: true
        
        startForeground(notificationId, createNotification("Starting backup...", 0))
        
        serviceScope.launch {
            try {
                performBackup(backupType, encryptionEnabled)
            } catch (e: Exception) {
                e.printStackTrace()
                showError("Backup failed: ${e.message}")
            } finally {
                stopSelf()
            }
        }
        
        return START_NOT_STICKY
    }
    
    private suspend fun performBackup(backupType: String, encryptionEnabled: Boolean) {
        // 1. Start backup session
        val deviceId = getDeviceId()
        val files = collectFilesToBackup()
        val totalSize = files.sumOf { it.length() }
        
        val startResponse = apiClient.api.startBackup(
            StartBackupRequest(
                deviceId = deviceId,
                backupType = backupType,
                totalSize = totalSize,
                fileCount = files.size
            )
        )
        
        if (!startResponse.isSuccessful) {
            throw Exception("Failed to start backup")
        }
        
        val backup = startResponse.body()?.data ?: throw Exception("No backup data")
        val backupId = backup.backupId
        
        // 2. Upload files
        if (encryptionEnabled) {
            uploadEncrypted(deviceId, backupId, files)
        } else {
            uploadChunked(deviceId, backupId, files)
        }
        
        // 3. Verify backup
        val verifyResponse = apiClient.api.verifyBackup(
            VerifyBackupRequest(deviceId, backupId)
        )
        
        if (verifyResponse.isSuccessful) {
            showSuccess("Backup completed successfully")
        } else {
            showError("Backup verification failed")
        }
    }
    
    private suspend fun uploadChunked(deviceId: String, backupId: String, files: List<File>) {
        val chunkSize = 5 * 1024 * 1024 // 5MB chunks
        var uploadedSize = 0L
        val totalSize = files.sumOf { it.length() }
        
        files.forEachIndexed { fileIndex, file ->
            val fileBytes = file.readBytes()
            val chunks = fileBytes.toList().chunked(chunkSize)
            
            chunks.forEachIndexed { chunkIndex, chunk ->
                val chunkFile = File.createTempFile("chunk_", ".tmp", cacheDir)
                chunkFile.writeBytes(chunk.toByteArray())
                
                val chunkPart = MultipartBody.Part.createFormData(
                    "chunk",
                    chunkFile.name,
                    chunkFile.asRequestBody("application/octet-stream".toMediaTypeOrNull())
                )
                
                val response = apiClient.api.uploadChunk(
                    deviceId = deviceId.toRequestBody(),
                    backupId = backupId.toRequestBody(),
                    chunkIndex = chunkIndex.toString().toRequestBody(),
                    totalChunks = chunks.size.toString().toRequestBody(),
                    chunk = chunkPart
                )
                
                chunkFile.delete()
                
                if (response.isSuccessful) {
                    uploadedSize += chunk.size
                    val progress = (uploadedSize.toFloat() / totalSize * 100).toInt()
                    updateNotification("Uploading backup...", progress)
                }
            }
        }
    }
    
    private suspend fun uploadEncrypted(deviceId: String, backupId: String, files: List<File>) {
        // Generate encryption key
        val key = EncryptionUtil.generateKey()
        val iv = EncryptionUtil.generateIV()
        
        // Combine all files into single archive
        val archiveFile = createArchive(files)
        val archiveBytes = archiveFile.readBytes()
        
        // Encrypt
        val encrypted = EncryptionUtil.encrypt(archiveBytes, key, iv)
        
        // Save encrypted file
        val encryptedFile = File.createTempFile("backup_", ".enc", cacheDir)
        encryptedFile.writeBytes(encrypted.ciphertext)
        
        // Upload
        val filePart = MultipartBody.Part.createFormData(
            "encryptedData",
            encryptedFile.name,
            encryptedFile.asRequestBody("application/octet-stream".toMediaTypeOrNull())
        )
        
        val metadata = mapOf(
            "algorithm" to "aes-256-gcm",
            "iv" to android.util.Base64.encodeToString(encrypted.iv, android.util.Base64.NO_WRAP),
            "authTag" to android.util.Base64.encodeToString(encrypted.authTag, android.util.Base64.NO_WRAP),
            "salt" to ""
        )
        
        val response = apiClient.api.uploadEncrypted(
            deviceId = deviceId.toRequestBody(),
            backupId = backupId.toRequestBody(),
            metadata = com.google.gson.Gson().toJson(metadata).toRequestBody(),
            encryptedData = filePart
        )
        
        encryptedFile.delete()
        archiveFile.delete()
        
        if (!response.isSuccessful) {
            throw Exception("Upload failed")
        }
    }
    
    private fun collectFilesToBackup(): List<File> {
        // Implement file collection logic
        // This would scan storage for photos, videos, documents, etc.
        return emptyList()
    }
    
    private fun createArchive(files: List<File>): File {
        // Implement archive creation (ZIP or TAR)
        return File.createTempFile("archive_", ".zip", cacheDir)
    }
    
    private fun getDeviceId(): String {
        return getSharedPreferences("mycloudvault", MODE_PRIVATE)
            .getString("device_id", "") ?: ""
    }
    
    private fun createNotificationChannel() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val channel = NotificationChannel(
                "backup_channel",
                "Backup Service",
                NotificationManager.IMPORTANCE_LOW
            )
            val manager = getSystemService(NotificationManager::class.java)
            manager.createNotificationChannel(channel)
        }
    }
    
    private fun createNotification(message: String, progress: Int) =
        NotificationCompat.Builder(this, "backup_channel")
            .setContentTitle("MyCloudVault Backup")
            .setContentText(message)
            .setSmallIcon(R.drawable.ic_backup)
            .setProgress(100, progress, progress == 0)
            .setOngoing(true)
            .build()
    
    private fun updateNotification(message: String, progress: Int) {
        val manager = getSystemService(NotificationManager::class.java)
        manager.notify(notificationId, createNotification(message, progress))
    }
    
    private fun showSuccess(message: String) {
        val notification = NotificationCompat.Builder(this, "backup_channel")
            .setContentTitle("Backup Complete")
            .setContentText(message)
            .setSmallIcon(R.drawable.ic_check)
            .build()
        
        val manager = getSystemService(NotificationManager::class.java)
        manager.notify(notificationId + 1, notification)
    }
    
    private fun showError(message: String) {
        val notification = NotificationCompat.Builder(this, "backup_channel")
            .setContentTitle("Backup Failed")
            .setContentText(message)
            .setSmallIcon(R.drawable.ic_error)
            .build()
        
        val manager = getSystemService(NotificationManager::class.java)
        manager.notify(notificationId + 2, notification)
    }
    
    override fun onBind(intent: Intent?): IBinder? = null
    
    override fun onDestroy() {
        super.onDestroy()
        serviceScope.cancel()
    }
}
```

### Step 5: Pairing Activity

**PairingActivity.kt**

```kotlin
package com.yourcompany.mycloudvault.ui.pairing

import android.os.Bundle
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.yourcompany.mycloudvault.api.ApiClient
import com.yourcompany.mycloudvault.api.models.PairDeviceRequest
import com.yourcompany.mycloudvault.databinding.ActivityPairingBinding
import kotlinx.coroutines.launch

class PairingActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityPairingBinding
    private lateinit var apiClient: ApiClient
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityPairingBinding.inflate(layoutInflater)
        setContentView(binding.root)
        
        apiClient = ApiClient(this)
        
        binding.btnPair.setOnClickListener {
            pairDevice()
        }
    }
    
    private fun pairDevice() {
        val serverUrl = binding.etServerUrl.text.toString()
        val pairingCode = binding.etPairingCode.text.toString()
        
        if (serverUrl.isEmpty() || pairingCode.isEmpty()) {
            Toast.makeText(this, "Please fill all fields", Toast.LENGTH_SHORT).show()
            return
        }
        
        apiClient.updateServerUrl(serverUrl)
        
        val deviceId = "android-${android.os.Build.ID}"
        val deviceName = "${android.os.Build.MANUFACTURER} ${android.os.Build.MODEL}"
        val deviceModel = android.os.Build.MODEL
        val osVersion = android.os.Build.VERSION.RELEASE
        
        lifecycleScope.launch {
            try {
                binding.btnPair.isEnabled = false
                binding.btnPair.text = "Pairing..."
                
                val response = apiClient.api.pairDevice(
                    PairDeviceRequest(
                        deviceId = deviceId,
                        deviceName = deviceName,
                        deviceModel = deviceModel,
                        osVersion = osVersion,
                        pairingCode = pairingCode
                    )
                )
                
                if (response.isSuccessful) {
                    val data = response.body()?.data
                    if (data != null) {
                        apiClient.saveCredentials(deviceId, data.apiKey)
                        Toast.makeText(this@PairingActivity, "Pairing successful!", Toast.LENGTH_SHORT).show()
                        finish()
                    }
                } else {
                    Toast.makeText(this@PairingActivity, "Pairing failed", Toast.LENGTH_SHORT).show()
                }
            } catch (e: Exception) {
                Toast.makeText(this@PairingActivity, "Error: ${e.message}", Toast.LENGTH_SHORT).show()
            } finally {
                binding.btnPair.isEnabled = true
                binding.btnPair.text = "Pair Device"
            }
        }
    }
}
```

---

## Testing

### Unit Tests

```kotlin
class EncryptionUtilTest {
    
    @Test
    fun testEncryptDecrypt() {
        val data = "Hello, MyCloudVault!".toByteArray()
        val key = EncryptionUtil.generateKey()
        val iv = EncryptionUtil.generateIV()
        
        val encrypted = EncryptionUtil.encrypt(data, key, iv)
        val decrypted = EncryptionUtil.decrypt(encrypted.ciphertext, key, iv, encrypted.authTag)
        
        assertEquals(String(data), String(decrypted))
    }
}
```

### Integration Tests

```kotlin
class ApiClientTest {
    
    @Test
    fun testHealthCheck() = runBlocking {
        val apiClient = ApiClient(context)
        val response = apiClient.api.healthCheck()
        assertTrue(response.isSuccessful)
    }
}
```

---

## Best Practices

### Security

1. **Store API keys in EncryptedSharedPreferences**
2. **Use Android Keystore for encryption keys**
3. **Implement certificate pinning**
4. **Clear sensitive data from memory**
5. **Use ProGuard/R8 for code obfuscation**

### Performance

1. **Use WorkManager for scheduled backups**
2. **Implement exponential backoff for retries**
3. **Compress files before encryption**
4. **Use coroutines for async operations**
5. **Batch file operations**

### User Experience

1. **Show progress notifications**
2. **Handle network errors gracefully**
3. **Provide offline mode**
4. **Allow backup cancellation**
5. **Display storage usage**

---

## In-App Purchases & Monetization

### Overview

CloudVault uses a **freemium model** where:
- **Server software**: Free and open-source (self-hosted)
- **Mobile apps**: Paid with in-app purchases for premium features

### Google Play Billing Library Setup

Add to `build.gradle.kts`:

```kotlin
dependencies {
    // Google Play Billing
    implementation("com.android.billingclient:billing-ktx:6.1.0")
}
```

### Product IDs

Define in `BillingConstants.kt`:

```kotlin
object BillingConstants {
    // Subscription products
    const val PRODUCT_PREMIUM_MONTHLY = "cloudvault_premium_monthly"
    const val PRODUCT_PREMIUM_YEARLY = "cloudvault_premium_yearly"
    
    // One-time purchase
    const val PRODUCT_LIFETIME = "cloudvault_lifetime"
    
    // Feature flags
    const val MAX_FREE_DEVICES = 1
    const val MAX_FREE_BACKUPS = 10
    const val MAX_FREE_STORAGE_GB = 5
}
```

### Subscription Tiers

#### Free Tier
- 1 device
- 10 backups maximum
- 5 GB storage limit
- Manual backups only
- Standard support

#### Premium Monthly ($4.99/month)
- Unlimited devices
- Unlimited backups
- Unlimited storage
- Automated scheduling
- Priority support
- Advanced compression

#### Premium Yearly ($49.99/year) - Save 17%
- All Premium Monthly features
- Annual billing
- Best value

#### Lifetime ($99.99 one-time)
- All Premium features forever
- One-time payment
- No recurring fees

### Billing Manager Implementation

```kotlin
class BillingManager(private val context: Context) {
    private var billingClient: BillingClient? = null
    private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
        if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
            for (purchase in purchases) {
                handlePurchase(purchase)
            }
        }
    }
    
    fun initialize() {
        billingClient = BillingClient.newBuilder(context)
            .setListener(purchasesUpdatedListener)
            .enablePendingPurchases()
            .build()
        
        billingClient?.startConnection(object : BillingClientStateListener {
            override fun onBillingSetupFinished(billingResult: BillingResult) {
                if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                    queryPurchases()
                }
            }
            
            override fun onBillingServiceDisconnected() {
                // Retry connection
            }
        })
    }
    
    private fun queryPurchases() {
        billingClient?.queryPurchasesAsync(
            QueryPurchasesParams.newBuilder()
                .setProductType(BillingClient.ProductType.SUBS)
                .build()
        ) { billingResult, purchases ->
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                purchases.forEach { handlePurchase(it) }
            }
        }
    }
    
    private fun handlePurchase(purchase: Purchase) {
        if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
            if (!purchase.isAcknowledged) {
                acknowledgePurchase(purchase)
            }
            // Update user's premium status
            updatePremiumStatus(true)
        }
    }
    
    private fun acknowledgePurchase(purchase: Purchase) {
        val params = AcknowledgePurchaseParams.newBuilder()
            .setPurchaseToken(purchase.purchaseToken)
            .build()
        
        billingClient?.acknowledgePurchase(params) { billingResult ->
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                // Purchase acknowledged
            }
        }
    }
    
    fun launchPurchaseFlow(activity: Activity, productId: String) {
        val productDetailsParams = QueryProductDetailsParams.Product.newBuilder()
            .setProductId(productId)
            .setProductType(BillingClient.ProductType.SUBS)
            .build()
        
        val params = QueryProductDetailsParams.newBuilder()
            .setProductList(listOf(productDetailsParams))
            .build()
        
        billingClient?.queryProductDetailsAsync(params) { billingResult, productDetailsList ->
            if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
                productDetailsList.firstOrNull()?.let { productDetails ->
                    val flowParams = BillingFlowParams.newBuilder()
                        .setProductDetailsParamsList(
                            listOf(
                                BillingFlowParams.ProductDetailsParams.newBuilder()
                                    .setProductDetails(productDetails)
                                    .build()
                            )
                        )
                        .build()
                    
                    billingClient?.launchBillingFlow(activity, flowParams)
                }
            }
        }
    }
}
```

### Feature Gating

```kotlin
class FeatureManager(private val context: Context) {
    private val prefs = context.getSharedPreferences("cloudvault_prefs", Context.MODE_PRIVATE)
    
    fun isPremium(): Boolean {
        return prefs.getBoolean("is_premium", false)
    }
    
    fun canAddDevice(): Boolean {
        val deviceCount = getDeviceCount()
        return isPremium() || deviceCount < BillingConstants.MAX_FREE_DEVICES
    }
    
    fun canCreateBackup(): Boolean {
        val backupCount = getBackupCount()
        return isPremium() || backupCount < BillingConstants.MAX_FREE_BACKUPS
    }
    
    fun canScheduleBackups(): Boolean {
        return isPremium()
    }
    
    fun hasStorageAvailable(sizeBytes: Long): Boolean {
        if (isPremium()) return true
        
        val usedStorageGB = getTotalStorageUsed() / (1024 * 1024 * 1024)
        val newStorageGB = sizeBytes / (1024 * 1024 * 1024)
        return (usedStorageGB + newStorageGB) <= BillingConstants.MAX_FREE_STORAGE_GB
    }
}
```

### Purchase Verification

For security, verify purchases server-side:

```kotlin
class PurchaseVerifier(private val apiService: ApiService) {
    suspend fun verifyPurchase(purchase: Purchase): Boolean {
        return try {
            val response = apiService.verifyPurchase(
                VerifyPurchaseRequest(
                    productId = purchase.products.first(),
                    purchaseToken = purchase.purchaseToken,
                    orderId = purchase.orderId
                )
            )
            response.isSuccessful && response.body()?.valid == true
        } catch (e: Exception) {
            false
        }
    }
}
```

---

## QR Code Device Pairing

### Overview

CloudVault uses QR code scanning for instant device pairing, reducing setup time from 5 minutes to 10 seconds.

### Dependencies

Add to `build.gradle.kts`:

```kotlin
dependencies {
    // ML Kit Barcode Scanning
    implementation("com.google.mlkit:barcode-scanning:17.2.0")
    
    // CameraX
    implementation("androidx.camera:camera-camera2:1.3.1")
    implementation("androidx.camera:camera-lifecycle:1.3.1")
    implementation("androidx.camera:camera-view:1.3.1")
}
```

### QR Code Scanner Implementation

```kotlin
class QRCodeScannerActivity : AppCompatActivity() {
    private lateinit var cameraProvider: ProcessCameraProvider
    private lateinit var preview: Preview
    private lateinit var imageAnalysis: ImageAnalysis
    private val barcodeScanner = BarcodeScanning.getClient()
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_qr_scanner)
        
        if (checkCameraPermission()) {
            startCamera()
        } else {
            requestCameraPermission()
        }
    }
    
    private fun startCamera() {
        val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
        
        cameraProviderFuture.addListener({
            cameraProvider = cameraProviderFuture.get()
            
            preview = Preview.Builder().build().also {
                it.setSurfaceProvider(previewView.surfaceProvider)
            }
            
            imageAnalysis = ImageAnalysis.Builder()
                .setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
                .build()
                .also {
                    it.setAnalyzer(cameraExecutor, QRCodeAnalyzer { qrCode ->
                        handleQRCode(qrCode)
                    })
                }
            
            try {
                cameraProvider.unbindAll()
                cameraProvider.bindToLifecycle(
                    this,
                    CameraSelector.DEFAULT_BACK_CAMERA,
                    preview,
                    imageAnalysis
                )
            } catch (e: Exception) {
                Log.e(TAG, "Camera binding failed", e)
            }
        }, ContextCompat.getMainExecutor(this))
    }
    
    private fun handleQRCode(qrCode: String) {
        // Parse QR code data
        try {
            val data = JSONObject(qrCode)
            val serverUrl = data.getString("serverUrl")
            val pairingToken = data.getString("token")
            val expiresAt = data.getLong("expiresAt")
            
            // Check if token is expired
            if (System.currentTimeMillis() > expiresAt) {
                showError("QR code expired. Please generate a new one.")
                return
            }
            
            // Pair device
            lifecycleScope.launch {
                pairDevice(serverUrl, pairingToken)
            }
        } catch (e: Exception) {
            showError("Invalid QR code")
        }
    }
    
    private suspend fun pairDevice(serverUrl: String, token: String) {
        try {
            val deviceId = UUID.randomUUID().toString()
            val deviceName = "${Build.MANUFACTURER} ${Build.MODEL}"
            
            val response = apiService.pairDevice(
                PairDeviceRequest(
                    token = token,
                    deviceId = deviceId,
                    deviceName = deviceName,
                    platform = "android",
                    osVersion = Build.VERSION.RELEASE
                )
            )
            
            if (response.isSuccessful) {
                // Save credentials
                saveDeviceCredentials(serverUrl, deviceId)
                
                // Navigate to main screen
                startActivity(Intent(this, MainActivity::class.java))
                finish()
            } else {
                showError("Pairing failed: ${response.message()}")
            }
        } catch (e: Exception) {
            showError("Network error: ${e.message}")
        }
    }
}

class QRCodeAnalyzer(private val onQRCodeDetected: (String) -> Unit) : ImageAnalysis.Analyzer {
    private val barcodeScanner = BarcodeScanning.getClient()
    
    @androidx.camera.core.ExperimentalGetImage
    override fun analyze(imageProxy: ImageProxy) {
        val mediaImage = imageProxy.image
        if (mediaImage != null) {
            val image = InputImage.fromMediaImage(mediaImage, imageProxy.imageInfo.rotationDegrees)
            
            barcodeScanner.process(image)
                .addOnSuccessListener { barcodes ->
                    for (barcode in barcodes) {
                        barcode.rawValue?.let { onQRCodeDetected(it) }
                    }
                }
                .addOnCompleteListener {
                    imageProxy.close()
                }
        }
    }
}
```

---

## Compression Support

### Overview

CloudVault implements automatic compression to reduce storage usage by up to 70%, saving significant costs.

### Implementation

```kotlin
class CompressionManager {
    fun compressFile(inputFile: File, outputFile: File, level: Int = 6): Boolean {
        return try {
            FileInputStream(inputFile).use { input ->
                FileOutputStream(outputFile).use { output ->
                    GZIPOutputStream(output).use { gzip ->
                        input.copyTo(gzip)
                    }
                }
            }
            true
        } catch (e: Exception) {
            Log.e(TAG, "Compression failed", e)
            false
        }
    }
    
    fun decompressFile(inputFile: File, outputFile: File): Boolean {
        return try {
            FileInputStream(inputFile).use { input ->
                GZIPInputStream(input).use { gzip ->
                    FileOutputStream(outputFile).use { output ->
                        gzip.copyTo(output)
                    }
                }
            }
            true
        } catch (e: Exception) {
            Log.e(TAG, "Decompression failed", e)
            false
        }
    }
    
    fun getCompressionRatio(originalSize: Long, compressedSize: Long): Float {
        return if (originalSize > 0) {
            (1 - (compressedSize.toFloat() / originalSize)) * 100
        } else {
            0f
        }
    }
}
```

### Integration with Upload

```kotlin
suspend fun uploadBackupWithCompression(file: File, deviceId: String) {
    val compressedFile = File(context.cacheDir, "${file.name}.gz")
    
    try {
        // Compress file
        if (compressionManager.compressFile(file, compressedFile)) {
            val originalSize = file.length()
            val compressedSize = compressedFile.length()
            val ratio = compressionManager.getCompressionRatio(originalSize, compressedSize)
            
            Log.d(TAG, "Compressed: ${originalSize}B -> ${compressedSize}B (${ratio}% savings)")
            
            // Upload compressed file
            uploadFile(compressedFile, deviceId, compressed = true)
        } else {
            // Upload original if compression fails
            uploadFile(file, deviceId, compressed = false)
        }
    } finally {
        compressedFile.delete()
    }
}
```

---

## Backup Scheduling

### Overview

CloudVault supports automated backup scheduling with conditions (WiFi-only, minimum battery, etc.).

### WorkManager Implementation

```kotlin
class BackupWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {
    
    override suspend fun doWork(): Result {
        val scheduleId = inputData.getString("scheduleId") ?: return Result.failure()
        
        return try {
            // Fetch schedule details
            val schedule = getSchedule(scheduleId)
            
            // Check conditions
            if (!checkConditions(schedule)) {
                return Result.retry()
            }
            
            // Perform backup
            val backupResult = performBackup(schedule)
            
            if (backupResult) {
                Result.success()
            } else {
                Result.retry()
            }
        } catch (e: Exception) {
            Log.e(TAG, "Backup failed", e)
            Result.failure()
        }
    }
    
    private fun checkConditions(schedule: BackupSchedule): Boolean {
        // Check WiFi
        if (schedule.wifiOnly && !isWiFiConnected()) {
            return false
        }
        
        // Check battery
        if (schedule.minBattery > 0 && getBatteryLevel() < schedule.minBattery) {
            return false
        }
        
        // Check charging
        if (schedule.requireCharging && !isCharging()) {
            return false
        }
        
        return true
    }
}
```

### Schedule Creation

```kotlin
class ScheduleManager(private val context: Context) {
    private val workManager = WorkManager.getInstance(context)
    
    fun createDailySchedule(
        scheduleId: String,
        hour: Int,
        minute: Int,
        wifiOnly: Boolean = true,
        minBattery: Int = 20
    ) {
        val constraints = Constraints.Builder()
            .setRequiredNetworkType(
                if (wifiOnly) NetworkType.UNMETERED else NetworkType.CONNECTED
            )
            .setRequiresBatteryNotLow(minBattery > 20)
            .build()
        
        val inputData = workDataOf(
            "scheduleId" to scheduleId
        )
        
        val dailyWork = PeriodicWorkRequestBuilder<BackupWorker>(
            1, TimeUnit.DAYS
        )
            .setConstraints(constraints)
            .setInputData(inputData)
            .setInitialDelay(calculateInitialDelay(hour, minute), TimeUnit.MILLISECONDS)
            .build()
        
        workManager.enqueueUniquePeriodicWork(
            "backup_$scheduleId",
            ExistingPeriodicWorkPolicy.REPLACE,
            dailyWork
        )
    }
    
    private fun calculateInitialDelay(hour: Int, minute: Int): Long {
        val now = Calendar.getInstance()
        val target = Calendar.getInstance().apply {
            set(Calendar.HOUR_OF_DAY, hour)
            set(Calendar.MINUTE, minute)
            set(Calendar.SECOND, 0)
            
            if (before(now)) {
                add(Calendar.DAY_OF_MONTH, 1)
            }
        }
        
        return target.timeInMillis - now.timeInMillis
    }
}
```

---

## Upload Progress Tracking

### Overview

Real-time upload progress tracking with per-chunk progress bars for multi-threaded uploads.

### Progress API Integration

```kotlin
class UploadProgressTracker(private val apiService: ApiService) {
    private val _progress = MutableStateFlow<UploadProgress?>(null)
    val progress: StateFlow<UploadProgress?> = _progress.asStateFlow()
    
    suspend fun startTracking(backupId: String) {
        while (true) {
            try {
                val response = apiService.getUploadProgress(backupId)
                if (response.isSuccessful) {
                    val progressData = response.body()?.progress
                    _progress.value = progressData
                    
                    if (progressData?.status == "completed") {
                        break
                    }
                }
            } catch (e: Exception) {
                Log.e(TAG, "Failed to fetch progress", e)
            }
            
            delay(2000) // Poll every 2 seconds
        }
    }
}
```

### UI Implementation

```kotlin
@Composable
fun UploadProgressCard(progress: UploadProgress) {
    Card(
        modifier = Modifier
            .fillMaxWidth()
            .padding(16.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(
                text = progress.fileName,
                style = MaterialTheme.typography.titleMedium
            )
            
            Spacer(modifier = Modifier.height(8.dp))
            
            LinearProgressIndicator(
                progress = progress.uploadedChunks.toFloat() / progress.totalChunks,
                modifier = Modifier.fillMaxWidth()
            )
            
            Spacer(modifier = Modifier.height(8.dp))
            
            Row(
                modifier = Modifier.fillMaxWidth(),
                horizontalArrangement = Arrangement.SpaceBetween
            ) {
                Text(
                    text = "${progress.uploadedChunks} / ${progress.totalChunks} chunks",
                    style = MaterialTheme.typography.bodySmall
                )
                Text(
                    text = "${(progress.uploadedChunks.toFloat() / progress.totalChunks * 100).toInt()}%",
                    style = MaterialTheme.typography.bodySmall
                )
            }
        }
    }
}
```

---

## Next Steps

1. Implement file selection UI
2. Add restore functionality
3. Create settings screen
4. Implement scheduled backups
5. Add backup history view
6. Implement incremental backups
7. Add backup verification UI
8. Create backup logs

---

## Resources

- [Android Backup API](https://developer.android.com/guide/topics/data/backup)
- [Retrofit Documentation](https://square.github.io/retrofit/)
- [Android Encryption](https://developer.android.com/topic/security/data)
- [WorkManager Guide](https://developer.android.com/topic/libraries/architecture/workmanager)
