1+ // ui/components/ProfilePicturePicker.kt
2+ package com.example.dosezy.ui.components
3+
4+ import android.Manifest
5+ import android.content.Context
6+ import android.content.pm.PackageManager
7+ import android.net.Uri
8+ import android.os.Build
9+ import androidx.activity.compose.rememberLauncherForActivityResult
10+ import androidx.activity.result.contract.ActivityResultContracts
11+ import androidx.compose.foundation.BorderStroke
12+ import androidx.compose.foundation.clickable
13+ import androidx.compose.foundation.layout.Box
14+ import androidx.compose.foundation.layout.Column
15+ import androidx.compose.foundation.layout.Spacer
16+ import androidx.compose.foundation.layout.height
17+ import androidx.compose.foundation.layout.padding
18+ import androidx.compose.foundation.layout.size
19+ import androidx.compose.foundation.shape.CircleShape
20+ import androidx.compose.material.icons.Icons
21+ import androidx.compose.material.icons.filled.Add
22+ import androidx.compose.material.icons.filled.CameraAlt
23+ import androidx.compose.material.icons.filled.PhotoLibrary
24+ import androidx.compose.material3.AlertDialog
25+ import androidx.compose.material3.Icon
26+ import androidx.compose.material3.MaterialTheme
27+ import androidx.compose.material3.Surface
28+ import androidx.compose.material3.Text
29+ import androidx.compose.material3.TextButton
30+ import androidx.compose.runtime.Composable
31+ import androidx.compose.runtime.getValue
32+ import androidx.compose.runtime.mutableStateOf
33+ import androidx.compose.runtime.remember
34+ import androidx.compose.runtime.setValue
35+ import androidx.compose.ui.Alignment
36+ import androidx.compose.ui.Modifier
37+ import androidx.compose.ui.draw.clip
38+ import androidx.compose.ui.graphics.Color
39+ import androidx.compose.ui.layout.ContentScale
40+ import androidx.compose.ui.platform.LocalContext
41+ import androidx.compose.ui.res.painterResource
42+ import androidx.compose.ui.unit.dp
43+ import androidx.core.content.ContextCompat
44+ import coil.compose.AsyncImage
45+ import coil.request.ImageRequest
46+ import com.example.dosezy.R
47+ import com.example.dosezy.utils.rememberImagePicker
48+ import java.io.File
49+
50+ @Composable
51+ fun ProfilePicturePicker (
52+ profilePicPath : String? ,
53+ onProfilePictureSelected : (String? ) -> Unit ,
54+ modifier : Modifier = Modifier
55+ ) {
56+ val context = LocalContext .current
57+ var showImageSourceDialog by remember { mutableStateOf(false ) }
58+ var showPermissionDialog by remember { mutableStateOf(false ) }
59+ var permissionMessage by remember { mutableStateOf(" " ) }
60+ var pendingAction by remember { mutableStateOf< (() -> Unit )? > (null ) }
61+
62+ val imagePicker = rememberImagePicker(context) { uri ->
63+ if (uri != null ) {
64+ val savedPath = saveImageToInternalStorage(context, uri)
65+ onProfilePictureSelected(savedPath)
66+ }
67+ }
68+
69+ // Camera Permission Launcher
70+ val cameraPermissionLauncher = rememberLauncherForActivityResult(
71+ contract = ActivityResultContracts .RequestPermission (),
72+ onResult = { isGranted ->
73+ if (isGranted) {
74+ pendingAction?.invoke()
75+ } else {
76+ permissionMessage = " Camera permission is required to take photos. Please enable it in app settings."
77+ showPermissionDialog = true
78+ }
79+ pendingAction = null
80+ }
81+ )
82+
83+ // Storage Permission Launcher (for Android 12 and below, for Android 13+ use READ_MEDIA_IMAGES)
84+ val storagePermissionLauncher = rememberLauncherForActivityResult(
85+ contract = ActivityResultContracts .RequestPermission (),
86+ onResult = { isGranted ->
87+ if (isGranted) {
88+ pendingAction?.invoke()
89+ } else {
90+ permissionMessage = " Storage permission is required to access photos. Please enable it in app settings."
91+ showPermissionDialog = true
92+ }
93+ pendingAction = null
94+ }
95+ )
96+
97+ // Check and request permissions
98+ @Composable
99+ fun checkCameraPermission (action : @Composable () -> Unit ) {
100+ val hasPermission = ContextCompat .checkSelfPermission(
101+ context,
102+ Manifest .permission.CAMERA
103+ ) == PackageManager .PERMISSION_GRANTED
104+
105+ if (hasPermission) {
106+ action()
107+ } else {
108+ pendingAction = action as (() -> Unit )?
109+ cameraPermissionLauncher.launch(Manifest .permission.CAMERA )
110+ }
111+ }
112+
113+ @Composable
114+ fun checkStoragePermission (action : @Composable () -> Unit ) {
115+ // For Android 13+, use READ_MEDIA_IMAGES instead of READ_EXTERNAL_STORAGE
116+ val permission = if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .TIRAMISU ) {
117+ Manifest .permission.READ_MEDIA_IMAGES
118+ } else {
119+ Manifest .permission.READ_EXTERNAL_STORAGE
120+ }
121+
122+ val hasPermission = ContextCompat .checkSelfPermission(
123+ context,
124+ permission
125+ ) == PackageManager .PERMISSION_GRANTED
126+
127+ if (hasPermission) {
128+ action()
129+ } else {
130+ pendingAction = action as (() -> Unit )?
131+ storagePermissionLauncher.launch(permission)
132+ }
133+ }
134+
135+ Box (
136+ modifier = modifier
137+ .size(128 .dp)
138+ .clickable { showImageSourceDialog = true },
139+ contentAlignment = Alignment .Center
140+ ) {
141+ // Profile Image or Placeholder
142+ if (! profilePicPath.isNullOrEmpty()) {
143+ AsyncImage (
144+ model = ImageRequest .Builder (context)
145+ .data(File (profilePicPath))
146+ .crossfade(true )
147+ .build(),
148+ contentDescription = " Profile Picture" ,
149+ modifier = Modifier
150+ .size(128 .dp)
151+ .clip(CircleShape ),
152+ placeholder = painterResource(id = R .drawable.default_profile),
153+ error = painterResource(id = R .drawable.default_profile),
154+ contentScale = ContentScale .Crop
155+ )
156+ } else {
157+ Surface (
158+ shape = CircleShape ,
159+ color = MaterialTheme .colorScheme.surfaceVariant,
160+ border = BorderStroke (2 .dp, MaterialTheme .colorScheme.outline),
161+ modifier = Modifier .size(128 .dp)
162+ ) {
163+ Box (contentAlignment = Alignment .Center ) {
164+ Icon (
165+ imageVector = Icons .Default .Add ,
166+ contentDescription = " Add Profile Picture" ,
167+ tint = MaterialTheme .colorScheme.onSurfaceVariant,
168+ modifier = Modifier .size(32 .dp)
169+ )
170+ }
171+ }
172+ }
173+
174+ // Camera Icon Overlay
175+ Surface (
176+ shape = CircleShape ,
177+ color = MaterialTheme .colorScheme.primary,
178+ modifier = Modifier
179+ .align(Alignment .BottomEnd )
180+ .size(40 .dp)
181+ .clickable { showImageSourceDialog = true }
182+ ) {
183+ Box (contentAlignment = Alignment .Center ) {
184+ Icon (
185+ imageVector = Icons .Default .CameraAlt ,
186+ contentDescription = " Change Profile Picture" ,
187+ tint = Color .White ,
188+ modifier = Modifier .size(20 .dp)
189+ )
190+ }
191+ }
192+ }
193+
194+ // Image Source Selection Dialog
195+ if (showImageSourceDialog) {
196+ ImageSourceDialog (
197+ onDismiss = { showImageSourceDialog = false },
198+ onCameraSelected = {
199+ showImageSourceDialog = false
200+ checkCameraPermission {
201+ val tempUri = imagePicker.prepareCameraIntent()
202+ imagePicker.getCameraLauncher().launch(tempUri)
203+ }
204+ },
205+ onGallerySelected = {
206+ showImageSourceDialog = false
207+ checkStoragePermission {
208+ imagePicker.getGalleryLauncher().launch(" image/*" )
209+ }
210+ }
211+ )
212+ }
213+
214+ // Permission Dialog
215+ if (showPermissionDialog) {
216+ PermissionAlertDialog (
217+ message = permissionMessage,
218+ onDismiss = { showPermissionDialog = false }
219+ )
220+ }
221+ }
222+
223+ @Composable
224+ fun ImageSourceDialog (
225+ onDismiss : () -> Unit ,
226+ onCameraSelected : @Composable () -> Unit ,
227+ onGallerySelected : @Composable () -> Unit
228+ ) {
229+ AlertDialog (
230+ onDismissRequest = onDismiss,
231+ title = { Text (" Select Image Source" ) },
232+ text = {
233+ Column {
234+ Text (" Choose how you want to add your profile picture" )
235+ Spacer (modifier = Modifier .height(8 .dp))
236+ Text (
237+ " Camera: Take a new photo" ,
238+ style = MaterialTheme .typography.bodySmall,
239+ color = MaterialTheme .colorScheme.onSurfaceVariant
240+ )
241+ Text (
242+ " Gallery: Choose from your photos" ,
243+ style = MaterialTheme .typography.bodySmall,
244+ color = MaterialTheme .colorScheme.onSurfaceVariant
245+ )
246+ }
247+ },
248+ confirmButton = {
249+ TextButton (onClick = onGallerySelected as () -> Unit ) {
250+ Icon (
251+ imageVector = Icons .Default .PhotoLibrary ,
252+ contentDescription = null ,
253+ modifier = Modifier .size(18 .dp)
254+ )
255+ Text (" Gallery" , modifier = Modifier .padding(start = 8 .dp))
256+ }
257+ },
258+ dismissButton = {
259+ TextButton (onClick = onCameraSelected as () -> Unit ) {
260+ Icon (
261+ imageVector = Icons .Default .CameraAlt ,
262+ contentDescription = null ,
263+ modifier = Modifier .size(18 .dp)
264+ )
265+ Text (" Camera" , modifier = Modifier .padding(start = 8 .dp))
266+ }
267+ }
268+ )
269+ }
270+
271+ @Composable
272+ fun PermissionAlertDialog (
273+ message : String ,
274+ onDismiss : () -> Unit
275+ ) {
276+ AlertDialog (
277+ onDismissRequest = onDismiss,
278+ title = { Text (" Permission Required" ) },
279+ text = { Text (message) },
280+ confirmButton = {
281+ TextButton (onClick = onDismiss) {
282+ Text (" OK" )
283+ }
284+ }
285+ )
286+ }
287+
288+ // Utility function to save image to internal storage
289+ private fun saveImageToInternalStorage (context : Context , uri : Uri ): String {
290+ val inputStream = context.contentResolver.openInputStream(uri)
291+ val timeStamp = System .currentTimeMillis()
292+ val file = File (context.filesDir, " profile_$timeStamp .jpg" )
293+
294+ inputStream?.use { input ->
295+ file.outputStream().use { output ->
296+ input.copyTo(output)
297+ }
298+ }
299+
300+ return file.absolutePath
301+ }
0 commit comments