KtDevLog
  • Home
  • Jetpack Compose
  • Kotlin Fundamentals
  • Android Studio
No Result
View All Result
KtDevLog
  • Home
  • Jetpack Compose
  • Kotlin Fundamentals
  • Android Studio
No Result
View All Result
KtDevLog
No Result
View All Result
upload image to firebase storage android

How to Upload an Image to Firebase Storage in Android

Md Sharif Mia by Md Sharif Mia
May 17, 2026
in Firebase
0
0
Share on FacebookShare on PinterestShare on X

Profile pictures. Post images. Product photos. Recipe thumbnails. If your Android app involves users and content, it almost certainly involves image uploads.

Firebase Storage is how most Android developers handle this — a Google-managed cloud storage bucket with tight Firebase Authentication integration, built-in upload progress tracking, automatic retry on poor connections, and a simple Kotlin SDK. You select an image, call putFile(), and Firebase handles compression, chunked upload, CDN distribution, and serving at scale.

This guide covers the complete upload image to firebase storage android workflow in 2026: using the Android Photo Picker (the modern, privacy-safe replacement for old gallery intents), uploading with progress tracking, configuring Storage Security Rules to protect your bucket, saving the download URL to Firestore for later retrieval, and loading uploaded images back into your UI with Coil.

Related Posts

Firebase Crashlytics Setup in Android Studio: Track App Crashes

Firebase Crashlytics Setup in Android Studio: Track App Crashes

May 19, 2026
Firebase Realtime Database vs Firestore: Which is Better?

Firebase Realtime Database vs Firestore: Which is Better?

May 18, 2026
firebase push notifications android tutorial

Firebase Push Notifications Android Tutorial (FCM Setup 2026)

May 16, 2026
firestore CRUD operations in Android

Firestore CRUD Operations in Android: Complete Kotlin Guide

May 13, 2026

Table of Contents

  • The Android Photo Picker — Why It Matters in 2026
  • Step 1 — Firebase and Storage Setup
    • Enable Firebase Storage
    • Add Dependencies
  • Step 2 — Android Photo Picker Integration
  • Step 3 — Storage Repository
  • Step 4 — ViewModel With Upload Flow
  • Step 5 — Upload Screen UI
  • Step 6 — Firebase Storage Security Rules
  • Step 7 — Loading Uploaded Images With Coil
  • Complete File Path Strategy
  • Common Mistakes to Avoid
  • Frequently Asked Questions
    • Photo Picker and Permissions
      • Does the Android Photo Picker require storage permissions?
      • What versions of Android does the Photo Picker support?
    • Firebase Storage
      • How do I track upload progress in Firebase Storage?
      • What is a Firebase Storage download URL and how is it different from the storage path?
      • How do I delete an old profile image when a user uploads a new one?
  • Conclusion

The Android Photo Picker — Why It Matters in 2026

Before writing any Firebase code, it’s worth understanding why this guide uses the Android Photo Picker instead of ActivityResultContracts.GetContent() or the old ACTION_PICK intent that most older tutorials use.

The Photo Picker was introduced in Android 13 and backported all the way to Android 4.4 via Jetpack. It’s the Google-recommended way to let users select images in 2026 for three reasons:

Privacy — The Photo Picker gives users access to a sandboxed view of their gallery. Your app never receives broad storage permissions. The user picks specific photos and only those URIs are accessible to you — the rest of their gallery stays private.

No permission needed — READ_EXTERNAL_STORAGE or READ_MEDIA_IMAGES permissions are not required when using the Photo Picker. This is a significant UX improvement — no permission dialog before the user even sees the picker.

Consistent UI — The Photo Picker provides a Google-designed, consistent experience across all Android versions rather than device-specific gallery apps.

According to the official Android developer documentation, the Photo Picker is the recommended approach for all new Android apps in 2026 when you need users to select images or videos.

Step 1 — Firebase and Storage Setup

Enable Firebase Storage

In Firebase Console:

Build → Storage → Get Started

Choose Start in production mode — you’ll write proper Security Rules in Step 5. Select the nearest storage region (e.g. asia-south1 for Bangladesh).

Add Dependencies

Kotlin
// app/build.gradle.kts
dependencies {
    // Firebase BoM — manages all Firebase library versions
    implementation(platform("com.google.firebase:firebase-bom:34.12.0"))
    implementation("com.google.firebase:firebase-storage")
    implementation("com.google.firebase:firebase-firestore")  // For saving download URL
    implementation("com.google.firebase:firebase-auth")       // For Security Rules

    // Coil — for loading Firebase Storage images in Compose
    implementation("io.coil-kt:coil-compose:2.7.0")

    // Coroutines for .await() on Firebase Tasks
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-play-services:1.8.1")
}
Kotlin

Step 2 — Android Photo Picker Integration

The Photo Picker in Compose uses rememberLauncherForActivityResult with PickVisualMedia:

Kotlin
// ImagePickerScreen.kt
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts

@Composable
fun ImagePickerButton(onImageSelected: (Uri) -> Unit) {
    // Photo Picker launcher — no storage permissions needed
    val photoPickerLauncher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.PickVisualMedia()
    ) { uri ->
        uri?.let { onImageSelected(it) }
    }

    Button(
        onClick = {
            photoPickerLauncher.launch(
                PickVisualMediaRequest(
                    ActivityResultContracts.PickVisualMedia.ImageOnly  // Images only, no videos
                )
            )
        },
        modifier = Modifier.fillMaxWidth(),
        shape    = RoundedCornerShape(12.dp),
        colors   = ButtonDefaults.buttonColors(containerColor = Color(0xFF7C3AED))
    ) {
        Icon(Icons.Default.Image, contentDescription = null)
        Spacer(modifier = Modifier.width(8.dp))
        Text("Choose Photo")
    }
}
Kotlin

PickVisualMedia.ImageOnly restricts the picker to images — no video files. If you need both, use PickVisualMedia.ImageAndVideo. For multiple image selection, use PickMultipleVisualMedia(maxItems = 5).

Step 3 — Storage Repository

All Firebase Storage operations belong in a repository — keeping upload logic completely out of your ViewModel and composables:

Kotlin
// StorageRepository.kt
import android.net.Uri
import com.google.firebase.storage.FirebaseStorage
import com.google.firebase.storage.StorageMetadata
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.tasks.await
import java.util.UUID

class StorageRepository {

    private val storage = FirebaseStorage.getInstance()

    // ── Upload With Progress ────────────────────────────────────
    fun uploadImage(
        uri: Uri,
        userId: String,
        folder: String = "profile_images"
    ): Flow<UploadState> = callbackFlow {

        // Build a unique file path: folder/userId/uuid.jpg
        val fileName = "${UUID.randomUUID()}.jpg"
        val storageRef = storage.reference
            .child(folder)
            .child(userId)
            .child(fileName)

        // Set metadata — content type tells Firebase this is an image
        val metadata = StorageMetadata.Builder()
            .setContentType("image/jpeg")
            .build()

        // Start the upload task
        val uploadTask = storageRef.putFile(uri, metadata)

        // Emit progress updates
        uploadTask.addOnProgressListener { snapshot ->
            val progress = snapshot.bytesTransferred.toFloat() /
                           snapshot.totalByteCount.toFloat()
            trySend(UploadState.Progress(progress))
        }

        // Emit success with download URL
        uploadTask.addOnSuccessListener {
            kotlinx.coroutines.launch {
                try {
                    val downloadUrl = storageRef.downloadUrl.await().toString()
                    trySend(UploadState.Success(downloadUrl))
                    close()
                } catch (e: Exception) {
                    trySend(UploadState.Error("Failed to get download URL: ${e.message}"))
                    close(e)
                }
            }
        }

        // Emit error
        uploadTask.addOnFailureListener { e ->
            trySend(UploadState.Error(e.message ?: "Upload failed"))
            close(e)
        }

        // Cancel the upload if the Flow is cancelled
        awaitClose { uploadTask.cancel() }
    }

    // ── Delete an Image ─────────────────────────────────────────
    suspend fun deleteImage(downloadUrl: String): Result<Unit> {
        return try {
            val ref = storage.getReferenceFromUrl(downloadUrl)
            ref.delete().await()
            Result.success(Unit)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

// Upload state sealed class
sealed class UploadState {
    data class Progress(val fraction: Float) : UploadState()
    data class Success(val downloadUrl: String) : UploadState()
    data class Error(val message: String) : UploadState()
}
Kotlin

File path structure: folder/userId/uuid.jpg — this is the pattern that keeps Firebase Storage organised and securable. The userId segment means your Security Rules can enforce that users only access their own files. The UUID ensures each upload gets a unique filename — no collisions, no overwriting.

awaitClose { uploadTask.cancel() } — when the Compose screen leaves the composition and stops collecting the Flow, this cancels the upload automatically. No hanging upload tasks, no wasted bandwidth on abandoned screens.

Step 4 — ViewModel With Upload Flow

Kotlin
// UploadViewModel.kt
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch

data class ImageUploadUiState(
    val selectedImageUri: Uri?      = null,
    val uploadProgress: Float       = 0f,
    val isUploading: Boolean        = false,
    val downloadUrl: String?        = null,
    val errorMessage: String?       = null
)

class UploadViewModel(
    private val storageRepository: StorageRepository = StorageRepository()
) : ViewModel() {

    private val _uiState = MutableStateFlow(ImageUploadUiState())
    val uiState: StateFlow<ImageUploadUiState> = _uiState

    fun onImageSelected(uri: Uri) {
        _uiState.update { it.copy(selectedImageUri = uri, downloadUrl = null, errorMessage = null) }
    }

    fun uploadImage(userId: String) {
        val uri = _uiState.value.selectedImageUri ?: return

        viewModelScope.launch {
            _uiState.update { it.copy(isUploading = true, uploadProgress = 0f, errorMessage = null) }

            storageRepository.uploadImage(uri = uri, userId = userId)
                .collect { state ->
                    when (state) {
                        is UploadState.Progress -> {
                            _uiState.update { it.copy(uploadProgress = state.fraction) }
                        }
                        is UploadState.Success  -> {
                            _uiState.update {
                                it.copy(
                                    isUploading    = false,
                                    downloadUrl    = state.downloadUrl,
                                    uploadProgress = 1f
                                )
                            }
                            // Save download URL to Firestore
                            saveUrlToFirestore(userId, state.downloadUrl)
                        }
                        is UploadState.Error    -> {
                            _uiState.update {
                                it.copy(isUploading = false, errorMessage = state.message)
                            }
                        }
                    }
                }
        }
    }

    private fun saveUrlToFirestore(userId: String, downloadUrl: String) {
        viewModelScope.launch {
            com.google.firebase.firestore.FirebaseFirestore.getInstance()
                .collection("users")
                .document(userId)
                .update("profileImageUrl", downloadUrl)
                .addOnSuccessListener {
                    android.util.Log.d("Upload", "Profile URL saved to Firestore")
                }
                .addOnFailureListener { e ->
                    android.util.Log.e("Upload", "Failed to save URL: ${e.message}")
                }
        }
    }

    fun clearError() { _uiState.update { it.copy(errorMessage = null) } }
}
Kotlin

Step 5 — Upload Screen UI

Kotlin
@Composable
fun ImageUploadScreen(
    viewModel: UploadViewModel = viewModel(),
    userId: String
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    val context = LocalContext.current

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp),
        verticalArrangement   = Arrangement.spacedBy(16.dp),
        horizontalAlignment   = Alignment.CenterHorizontally
    ) {
        Text(
            text = "Upload Profile Photo",
            fontSize = 24.sp,
            fontWeight = FontWeight.Bold
        )

        // Image preview
        Box(
            modifier = Modifier
                .size(200.dp)
                .clip(CircleShape)
                .background(Color(0xFFF1F5F9)),
            contentAlignment = Alignment.Center
        ) {
            if (uiState.selectedImageUri != null) {
                AsyncImage(
                    model           = uiState.selectedImageUri,
                    contentDescription = "Selected image",
                    contentScale    = ContentScale.Crop,
                    modifier        = Modifier.fillMaxSize()
                )
            } else if (uiState.downloadUrl != null) {
                AsyncImage(
                    model           = uiState.downloadUrl,
                    contentDescription = "Uploaded profile photo",
                    contentScale    = ContentScale.Crop,
                    modifier        = Modifier.fillMaxSize()
                )
            } else {
                Icon(
                    imageVector = Icons.Default.Person,
                    contentDescription = null,
                    modifier = Modifier.size(80.dp),
                    tint     = Color(0xFF94A3B8)
                )
            }
        }

        // Upload progress bar
        if (uiState.isUploading) {
            Column(modifier = Modifier.fillMaxWidth()) {
                LinearProgressIndicator(
                    progress = { uiState.uploadProgress },
                    modifier = Modifier
                        .fillMaxWidth()
                        .height(8.dp)
                        .clip(RoundedCornerShape(4.dp)),
                    color      = Color(0xFF7C3AED),
                    trackColor = Color(0xFFE2E8F0)
                )
                Spacer(modifier = Modifier.height(4.dp))
                Text(
                    text     = "${(uiState.uploadProgress * 100).toInt()}% uploaded",
                    fontSize = 12.sp,
                    color    = Color(0xFF64748B),
                    modifier = Modifier.fillMaxWidth(),
                    textAlign = TextAlign.End
                )
            }
        }

        // Success message
        if (uiState.downloadUrl != null && !uiState.isUploading) {
            Row(
                verticalAlignment = Alignment.CenterVertically,
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                Icon(
                    Icons.Default.CheckCircle,
                    contentDescription = null,
                    tint = Color(0xFF16A34A)
                )
                Text(
                    text     = "Photo uploaded successfully!",
                    color    = Color(0xFF16A34A),
                    fontWeight = FontWeight.Medium
                )
            }
        }

        // Error message
        if (uiState.errorMessage != null) {
            Text(
                text  = uiState.errorMessage!!,
                color = Color(0xFFDC2626),
                textAlign = TextAlign.Center
            )
        }

        // Photo Picker button
        ImagePickerButton(
            onImageSelected = { uri -> viewModel.onImageSelected(uri) }
        )

        // Upload button
        Button(
            onClick   = { viewModel.uploadImage(userId) },
            modifier  = Modifier.fillMaxWidth(),
            shape     = RoundedCornerShape(12.dp),
            colors    = ButtonDefaults.buttonColors(containerColor = Color(0xFF16A34A)),
            enabled   = uiState.selectedImageUri != null && !uiState.isUploading
        ) {
            if (uiState.isUploading) {
                CircularProgressIndicator(
                    color    = Color.White,
                    modifier = Modifier.size(20.dp),
                    strokeWidth = 2.dp
                )
                Spacer(modifier = Modifier.width(8.dp))
                Text("Uploading...")
            } else {
                Icon(Icons.Default.CloudUpload, contentDescription = null)
                Spacer(modifier = Modifier.width(8.dp))
                Text("Upload Photo")
            }
        }
    }
}
Kotlin

Step 6 — Firebase Storage Security Rules

This is the step that determines whether your app is production-safe or a security liability. Never deploy with allow read, write: if true — it makes your entire Storage bucket public.

JavaScript
// Firebase Console → Storage → Rules
rules_version = '2';
service firebase.storage {
  match /b/{bucket}/o {

    // Profile images — users can only access their own folder
    match /profile_images/{userId}/{fileName} {
      // Read: only the owner can read their own images
      allow read: if request.auth != null
                  && request.auth.uid == userId;

      // Write: only the owner can upload to their folder
      // AND the file must be an image under 5MB
      allow write: if request.auth != null
                   && request.auth.uid == userId
                   && request.resource.size <= 5 * 1024 * 1024        // 5MB limit
                   && request.resource.contentType.matches('image/.*'); // Images only
    }

    // Public images — anyone can read, only authenticated users can write
    match /public_images/{userId}/{fileName} {
      allow read: if true;
      allow write: if request.auth != null
                   && request.auth.uid == userId
                   && request.resource.size <= 10 * 1024 * 1024
                   && request.resource.contentType.matches('image/.*');
    }
  }
}
JavaScript

request.resource.size <= 5 * 1024 * 1024 — enforces a 5MB file size limit at the server level. Without this, users could upload gigabyte files that exhaust your Firebase Storage quota. Always set size limits in Security Rules, not just client-side validation.

request.resource.contentType.matches('image/.*') — verifies the MIME type is an image before accepting the upload. This prevents users from uploading executable files, scripts, or other potentially dangerous content by simply renaming them with an image extension.

request.auth.uid == userId — the path variable {userId} must match the authenticated user’s UID. This is the structural rule that makes each user’s folder private — no user can read or write to another user’s folder regardless of what URI they try to access.

Step 7 — Loading Uploaded Images With Coil

Once uploaded, the download URL can be used anywhere. In Jetpack Compose, Coil loads it automatically with caching:

Kotlin
// Display a Firebase Storage image anywhere in your app
AsyncImage(
    model = ImageRequest.Builder(LocalContext.current)
        .data(userProfileUrl)                          // Firebase Storage download URL
        .crossfade(true)                               // Smooth fade-in when loaded
        .placeholder(R.drawable.ic_profile_placeholder) // Show while loading
        .error(R.drawable.ic_profile_placeholder)      // Show on load failure
        .build(),
    contentDescription = "Profile photo",
    contentScale       = ContentScale.Crop,
    modifier           = Modifier
        .size(56.dp)
        .clip(CircleShape)
)
Kotlin

Coil automatically caches Firebase Storage images on disk — the second load is instantaneous. Combined with the Firebase CDN, images load fast from anywhere in the world.

Complete File Path Strategy

How you structure paths in Firebase Storage affects both Security Rules and organisation. Here’s a production-tested strategy:

Firebase Storage bucket:
├── profile_images/
│   ├── {userId}/
│   │   └── abc123.jpg          ← User's profile photo
├── post_images/
│   ├── {userId}/
│   │   ├── post_001.jpg        ← User's post images
│   │   └── post_002.jpg
├── public_images/
│   └── banners/
│       └── welcome_banner.jpg  ← App-wide public images

Never use predictable file names like profile.jpg — if a user uploads a new photo, it would overwrite their old one without invalidating Coil’s cache. Use UUIDs (${UUID.randomUUID()}.jpg) to ensure each upload is a fresh file, and delete the old one explicitly when replacing.

Common Mistakes to Avoid

Using deprecated ACTION_PICK intent or GetContent() contract. These require READ_MEDIA_IMAGES or READ_EXTERNAL_STORAGE permissions — which Android shows as a scary permissions dialog. The Photo Picker API needs no permissions at all for image selection.

Not setting contentType in StorageMetadata. Without the content type, Firebase Storage doesn’t know the file format. Some browsers and CDN clients mishandle files without a declared MIME type. Always set "image/jpeg" or "image/png" in metadata.

Saving the storage path instead of the download URL. The storage path (e.g. profile_images/user123/abc.jpg) requires a Storage reference to access. The download URL is a public HTTPS URL that works directly in AsyncImage, Glide, or any HTTP client. Save the download URL.

Not enforcing size limits in Security Rules. Client-side validation can be bypassed. Security Rules run on Firebase’s servers and cannot be circumvented by a malicious client. Always include request.resource.size and contentType checks.

Frequently Asked Questions

Photo Picker and Permissions

Does the Android Photo Picker require storage permissions?

No — this is one of the Photo Picker’s biggest advantages over older gallery selection approaches. The Photo Picker (ActivityResultContracts.PickVisualMedia) operates in a sandboxed mode that doesn’t expose your full gallery to the app. Users select specific images and only those URIs are accessible. No READ_MEDIA_IMAGES or READ_EXTERNAL_STORAGE permissions are needed or requested.

What versions of Android does the Photo Picker support?

The Photo Picker is natively available on Android 13 (API 33) and higher. For older Android versions, Google backported it via the Jetpack androidx.activity:activity-compose library down to Android 4.4 (API 19). When you use ActivityResultContracts.PickVisualMedia on older Android versions, Jetpack automatically falls back to a system picker that matches the Photo Picker’s behaviour as closely as the OS supports.

Firebase Storage

How do I track upload progress in Firebase Storage?

Use addOnProgressListener on the StorageReference.putFile() upload task. The listener receives a TaskSnapshot with bytesTransferred and totalByteCount fields. Divide them to get a fraction from 0.0 to 1.0. In the repository pattern shown in this guide, wrap the upload in callbackFlow and trySend(UploadState.Progress(fraction)) to emit progress to your ViewModel as a Kotlin Flow.

What is a Firebase Storage download URL and how is it different from the storage path?

The storage path is the internal location of a file within your Firebase Storage bucket — for example profile_images/user123/abc.jpg. To access a file using a path, you need a StorageReference from the Firebase SDK. A download URL is a full HTTPS URL like https://firebasestorage.googleapis.com/... that can be used directly in AsyncImage, Glide, or any standard HTTP client without the Firebase SDK. Always save the download URL to Firestore — it’s what allows you to display images in your app without another Firebase Storage SDK call.

How do I delete an old profile image when a user uploads a new one?

Store the current download URL in Firestore. When the user uploads a new image, retrieve the old URL, call FirebaseStorage.getInstance().getReferenceFromUrl(oldUrl).delete().await() to delete the old file, then save the new URL. This prevents unused files from accumulating in your Storage bucket. Also update your Security Rules size limits — unchecked file accumulation inflates your Firebase Storage costs.

Conclusion

Uploading an image to Firebase Storage in Android involves more moving parts than most tutorials show — the Photo Picker for privacy-safe selection, StorageMetadata for content type, a callbackFlow for progress tracking, Security Rules for access control, and Firestore for persisting the download URL.

The patterns here — the storage path structure with userId segments, Security Rules enforcing both authentication and content type, and deleting old files on replacement — are production standards, not tutorial conventions.

Combine this with the Firebase Authentication guide for user identity and the Firestore CRUD operations guide for storing user data alongside the upload URL — and you have a complete, production-ready user profile system.

Every profile photo, every product image, every user-generated post — it all starts with a URI and a storage reference. Now you know exactly what comes next.

Tags: upload image to firebase storage android
SharePinTweet
Md Sharif Mia

Md Sharif Mia

Md Sharif Mia is a Kotlin and Android developer with hands-on experience building real-world Android applications using Kotlin, Jetpack Compose, and Firebase. He created KtDevLog to help aspiring Android developers learn through practical, step-by-step tutorials — from writing their first line of Kotlin to shipping complete apps.Through KtDevLog, Sharif shares what actually works in Android development: clean code patterns, common beginner mistakes to avoid, and project-based lessons that go beyond theory. His writing style is direct and beginner-friendly, making complex Android concepts easy to understand for developers at any stage.When he is not writing tutorials, Sharif is experimenting with new Android features, exploring Kotlin best practices, and building apps that solve everyday problems.

Related Posts

Firebase Crashlytics Setup in Android Studio: Track App Crashes
Firebase

Firebase Crashlytics Setup in Android Studio: Track App Crashes

May 19, 2026

Your app passed every test. You deployed it to the Play Store. And then...

Firebase Realtime Database vs Firestore: Which is Better?
Firebase

Firebase Realtime Database vs Firestore: Which is Better?

May 18, 2026

You're starting a new Android app. You want a cloud database. You open the...

firebase push notifications android tutorial
Firebase

Firebase Push Notifications Android Tutorial (FCM Setup 2026)

May 16, 2026

Your app does something useful. A new message arrives. A friend completes a task....

firestore CRUD operations in Android
Firebase

Firestore CRUD Operations in Android: Complete Kotlin Guide

May 13, 2026

Your users are logged in. Firebase Authentication is handling who they are. Now comes...

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

  • About Us
  • Contact Us
  • Privacy Policy
  • Terms & Conditions

© Copyright 2026 KtDevLog. All Rights Reserved.

Welcome Back!

Login to your account below

Forgotten Password?

Retrieve your password

Please enter your username or email address to reset your password.

Log In
No Result
View All Result
  • Home
  • Jetpack Compose
  • Kotlin Fundamentals
  • Android Studio

© Copyright 2026 KtDevLog. All Rights Reserved.