diff --git a/2_PROJECT_ANDROID.md b/2_PROJECT_ANDROID.md
new file mode 100644
index 0000000..01fc14d
--- /dev/null
+++ b/2_PROJECT_ANDROID.md
@@ -0,0 +1,242 @@
+
+---
+
+## 🛠️ Bước 1: Khởi tạo các file build cấu hình Gradle cho Android
+
+Để Android Studio có thể nhận diện và compile dự án, bạn cần tạo các file cấu hình bằng Kotlin DSL (`.gradle.kts`) tại thư mục `android-project`.
+
+### 1. Tạo file `android-project/settings.gradle.kts`
+
+File này dùng để khai báo tên dự án và quản lý kho chứa thư viện (Repositories).
+
+```kotlin
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.name = "RetroPhotobooth"
+include(":app")
+
+```
+
+### 2. Tạo file `android-project/build.gradle.kts` (Của thư mục gốc Android)
+
+```kotlin
+plugins {
+ id("com.android.application") version "8.2.2" apply false
+ id("org.jetbrains.kotlin.android") version "1.9.22" apply false
+}
+
+```
+
+### 3. Tạo file `android-project/app/build.gradle.kts` (Của module app)
+
+Đây là nơi chúng ta khai báo SDK và các thư viện cần dùng (CameraX, Jetpack Compose hoặc XML Views). Ở đây mình cấu hình sẵn các dependency cho **CameraX**.
+
+```kotlin
+plugins {
+ id("com.android.application")
+ id("org.jetbrains.kotlin.android")
+}
+
+android {
+ namespace = "com.photobooth.app"
+ compileSdk = 34
+
+ defaultConfig {
+ applicationId = "com.photobooth.app"
+ minSdk = 26 // Android 8.0 trở lên để hỗ trợ tốt các tính năng đồ họa
+ targetSdk = 34
+ versionCode = 1
+ versionName = "1.0"
+
+ testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
+ }
+
+ buildTypes {
+ release {
+ isMinifyEnabled = 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 // Bật ViewBinding để thao tác với giao diện dễ dàng
+ }
+}
+
+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")
+
+ // --- Thư viện CameraX Core ---
+ val cameraVersion = "1.3.1"
+ implementation("androidx.camera:camera-core:$cameraVersion")
+ implementation("androidx.camera:camera-camera2:$cameraVersion")
+ implementation("androidx.camera:camera-lifecycle:$cameraVersion")
+ implementation("androidx.camera:camera-view:$cameraVersion")
+ implementation("androidx.camera:camera-extensions:$cameraVersion")
+}
+
+```
+
+---
+
+## 📸 Bước 2: Thiết lập Giao diện Chụp ảnh (XML Layout)
+
+Tạo file giao diện hiển thị Camera preview và nút chụp ảnh tại đường dẫn:
+`android-project/app/src/main/res/layout/activity_main.xml` *(Bạn cần tạo thêm các thư mục `res/layout` nếu chưa có)*.
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## 💻 Bước 3: Viết Code Logic Khởi Tạo CameraX (Kotlin)
+
+Tạo file `MainActivity.kt` tại đường dẫn `android-project/app/src/main/java/com/photobooth/app/MainActivity.kt`. Đoạn code này thực hiện xin quyền Camera và kích hoạt kính ngắm.
+
+```kotlin
+package com.photobooth.app
+
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Bundle
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import com.photobooth.app.databinding.ActivityMainBinding
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class MainActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityMainBinding
+ private lateinit var cameraExecutor: ExecutorService
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ // Xin quyền Camera
+ if (allPermissionsGranted()) {
+ startCamera()
+ } else {
+ ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
+ }
+
+ cameraExecutor = Executors.newSingleThreadExecutor()
+ }
+
+ private fun startCamera() {
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
+
+ cameraProviderFuture.addListener({
+ val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
+
+ // Cấu hình Kính ngắm (Preview)
+ val preview = Preview.Builder().build().also {
+ it.setSurfaceProvider(binding.viewFinder.surfaceProvider)
+ }
+
+ // Chọn Camera Trước (Mặc định cho Photobooth)
+ val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
+
+ try {
+ cameraProvider.unbindAll()
+ cameraProvider.bindToLifecycle(this, cameraSelector, preview)
+ } catch (xc: Exception) {
+ Toast.makeText(this, "Không thể khởi động Camera", Toast.LENGTH_SHORT).show()
+ }
+ }, ContextCompat.getMainExecutor(this))
+ }
+
+ private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
+ ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ cameraExecutor.shutdown()
+ }
+
+ companion object {
+ private const val REQUEST_CODE_PERMISSIONS = 10
+ private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
+ }
+}
+
+```
+
+---
+
+## 🏃♂️ Việc cần làm ngay lúc này của bạn:
+
+1. **Mở Android Studio** trên Debian và thực hiện `Import Project`, trỏ tới thư mục `android-project`.
+2. Hệ thống sẽ tự động đồng bộ hóa (Gradle Sync).
+3. **Chạy thử nghiệm:** Bật Android Emulator (hoặc cắm điện thoại Android của bạn qua cổng USB bật gỡ lỗi ADB) để xem ứng dụng đã hiển thị được Camera lên màn hình chưa.
+
+Khi luồng hiển thị Camera này chạy thành công, chúng ta sẽ chuyển sang code tính năng **Đếm ngược tự động chụp liên tiếp 4 tấm** và lưu file ảnh thô.
\ No newline at end of file
diff --git a/android-project/app/build.gradle.kts b/android-project/app/build.gradle.kts
index cef6eca..361f2a4 100644
--- a/android-project/app/build.gradle.kts
+++ b/android-project/app/build.gradle.kts
@@ -5,19 +5,16 @@ plugins {
android {
namespace = "com.photobooth.app"
- compileSdk = 33
+ compileSdk = 34
defaultConfig {
applicationId = "com.photobooth.app"
- minSdk = 24
- targetSdk = 33
+ minSdk = 26 // Android 8.0 trở lên để hỗ trợ tốt các tính năng đồ họa
+ targetSdk = 34
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
- vectorDrawables {
- useSupportLibrary = true
- }
}
buildTypes {
@@ -30,48 +27,28 @@ android {
}
}
compileOptions {
- sourceCompatibility = JavaVersion.VERSION_1_8
- targetCompatibility = JavaVersion.VERSION_1_8
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
- jvmTarget = "1.8"
+ jvmTarget = "17"
}
buildFeatures {
- compose = true
- }
- composeOptions {
- kotlinCompilerExtensionVersion = "1.5.1"
- }
- packaging {
- resources {
- excludes += "/META-INF/{AL2.0,LGPL2.1}"
- }
+ viewBinding = true // Bật ViewBinding để thao tác với giao diện dễ dàng
}
}
dependencies {
- implementation("androidx.core:core-ktx:1.10.1")
- implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.1")
- implementation("androidx.activity:activity-compose:1.7.2")
- implementation(platform("androidx.compose:compose-bom:2023.06.01"))
- implementation("androidx.compose.ui:ui")
- implementation("androidx.compose.ui:ui-graphics")
- implementation("androidx.compose.ui:ui-tooling-preview")
- implementation("androidx.compose.material3:material3")
+ 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")
- // CameraX dependencies for Sprint 1 Camera preview
- val cameraVersion = "1.3.0-rc01"
+ // --- Thư viện CameraX Core ---
+ val cameraVersion = "1.3.1"
implementation("androidx.camera:camera-core:$cameraVersion")
implementation("androidx.camera:camera-camera2:$cameraVersion")
implementation("androidx.camera:camera-lifecycle:$cameraVersion")
implementation("androidx.camera:camera-view:$cameraVersion")
implementation("androidx.camera:camera-extensions:$cameraVersion")
-
- 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.06.01"))
- androidTestImplementation("androidx.compose.ui:ui-test-junit4")
- debugImplementation("androidx.compose.ui:ui-tooling")
- debugImplementation("androidx.compose.ui:ui-test-manifest")
}
diff --git a/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt b/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt
index ee640bd..d8496f4 100644
--- a/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt
+++ b/android-project/app/src/main/java/com/photobooth/app/MainActivity.kt
@@ -1,44 +1,72 @@
package com.photobooth.app
+import android.Manifest
+import android.content.pm.PackageManager
import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Surface
-import androidx.compose.material3.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.tooling.preview.Preview
+import android.widget.Toast
+import androidx.appcompat.app.AppCompatActivity
+import androidx.camera.core.CameraSelector
+import androidx.camera.core.Preview
+import androidx.camera.lifecycle.ProcessCameraProvider
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
+import com.photobooth.app.databinding.ActivityMainBinding
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+
+class MainActivity : AppCompatActivity() {
+ private lateinit var binding: ActivityMainBinding
+ private lateinit var cameraExecutor: ExecutorService
-class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState: Bundle?)
- setContent {
- MaterialTheme {
- Surface(
- modifier = Modifier.fillMaxSize(),
- color = MaterialTheme.colorScheme.background
- ) {
- Greeting("Retro Photobooth")
- }
- }
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ // Xin quyền Camera
+ if (allPermissionsGranted()) {
+ startCamera()
+ } else {
+ ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS)
}
- }
-}
-
-@Composable
-fun Greeting(name: String, modifier: Modifier = Modifier) {
- Text(
- text = "Welcome to $name!",
- modifier = modifier
- )
-}
-
-@Preview(showBackground = true)
-@Composable
-fun GreetingPreview() {
- MaterialTheme {
- Greeting("Retro Photobooth")
+
+ cameraExecutor = Executors.newSingleThreadExecutor()
+ }
+
+ private fun startCamera() {
+ val cameraProviderFuture = ProcessCameraProvider.getInstance(this)
+
+ cameraProviderFuture.addListener({
+ val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get()
+
+ // Cấu hình Kính ngắm (Preview)
+ val preview = Preview.Builder().build().also {
+ it.setSurfaceProvider(binding.viewFinder.surfaceProvider)
+ }
+
+ // Chọn Camera Trước (Mặc định cho Photobooth)
+ val cameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA
+
+ try {
+ cameraProvider.unbindAll()
+ cameraProvider.bindToLifecycle(this, cameraSelector, preview)
+ } catch (xc: Exception) {
+ Toast.makeText(this, "Không thể khởi động Camera", Toast.LENGTH_SHORT).show()
+ }
+ }, ContextCompat.getMainExecutor(this))
+ }
+
+ private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
+ ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
+ }
+
+ override fun onDestroy() {
+ super.onDestroy()
+ cameraExecutor.shutdown()
+ }
+
+ companion object {
+ private const val REQUEST_CODE_PERMISSIONS = 10
+ private val REQUIRED_PERMISSIONS = arrayOf(Manifest.permission.CAMERA)
}
}
diff --git a/android-project/app/src/main/res/layout/activity_main.xml b/android-project/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 0000000..fafff42
--- /dev/null
+++ b/android-project/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android-project/build.gradle.kts b/android-project/build.gradle.kts
index 95cbdb3..e1e81fa 100644
--- a/android-project/build.gradle.kts
+++ b/android-project/build.gradle.kts
@@ -1,5 +1,4 @@
-// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
- id("com.android.application") version "8.1.1" apply false
- id("org.jetbrains.kotlin.android") version "1.9.0" apply false
+ id("com.android.application") version "8.2.2" apply false
+ id("org.jetbrains.kotlin.android") version "1.9.22" apply false
}
diff --git a/android-project/settings.gradle.kts b/android-project/settings.gradle.kts
index 1807a71..9a48159 100644
--- a/android-project/settings.gradle.kts
+++ b/android-project/settings.gradle.kts
@@ -13,5 +13,5 @@ dependencyResolutionManagement {
}
}
-rootProject.name = "Retro Photobooth"
+rootProject.name = "RetroPhotobooth"
include(":app")