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")