do profile imageUpload feature

This commit is contained in:
Emin 2024-06-21 15:30:06 +05:00
parent 6f0c27d73a
commit ab44d68eac
4 changed files with 516 additions and 19 deletions

View file

@ -0,0 +1,70 @@
package app.tourism.ui.common
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
@Composable
fun ImagePicker(
modifier: Modifier = Modifier,
onSuccess: ((Uri?) -> Unit)? = null,
showPreview: Boolean = true,
previewContentScale: ContentScale = ContentScale.Fit,
content: @Composable () -> Unit,
) {
var hasImage by remember {
mutableStateOf(false)
}
var imageUri by rememberSaveable {
mutableStateOf<Uri?>(null)
}
val imagePicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.GetContent(),
onResult = { uri ->
hasImage = uri != null
imageUri = uri
if (uri != null) {
onSuccess?.invoke(imageUri)
}
}
)
Column(
modifier = Modifier.then(modifier),
horizontalAlignment = Alignment.CenterHorizontally
) {
if (showPreview && hasImage && imageUri != null) {
AsyncImage(
model = imageUri,
contentScale = previewContentScale,
contentDescription = null,
)
VerticalSpace(height = 10.dp)
}
Row(
Modifier.clickable {
hasImage = false
imageUri = null
imagePicker.launch("image/*")
},
) {
content()
}
}
}

View file

@ -1,7 +1,6 @@
package app.tourism.ui.screens.main.profile.personal_data
import android.view.LayoutInflater
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
@ -20,15 +19,16 @@ import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
@ -40,7 +40,7 @@ import app.tourism.Constants
import app.tourism.domain.models.resource.Resource
import app.tourism.ui.ObserveAsEvents
import app.tourism.ui.common.HorizontalSpace
import app.tourism.ui.common.LoadImg
import app.tourism.ui.common.ImagePicker
import app.tourism.ui.common.SpaceForNavBar
import app.tourism.ui.common.VerticalSpace
import app.tourism.ui.common.buttons.PrimaryButton
@ -50,14 +50,21 @@ import app.tourism.ui.screens.main.profile.profile.ProfileViewModel
import app.tourism.ui.screens.main.profile.profile.UiEvent
import app.tourism.ui.theme.TextStyles
import app.tourism.ui.utils.showToast
import app.tourism.utils.FileUtils
import coil.compose.AsyncImage
import com.hbb20.CountryCodePicker
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import java.io.File
@Composable
fun PersonalDataScreen(onBackClick: () -> Boolean, profileVM: ProfileViewModel) {
val context = LocalContext.current
val focusManager = LocalFocusManager.current
val coroutineScope = rememberCoroutineScope()
val personalData = profileVM.profileDataResource.collectAsState().value
val pfpFile = profileVM.pfpFile.collectAsState().value
val fullName = profileVM.fullName.collectAsState().value
val email = profileVM.email.collectAsState().value
val countryCodeName = profileVM.countryCodeName.collectAsState().value
@ -87,29 +94,39 @@ fun PersonalDataScreen(onBackClick: () -> Boolean, profileVM: ProfileViewModel)
Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
LoadImg(
AsyncImage(
modifier = Modifier
.size(100.dp)
.clip(CircleShape),
url = data.pfpUrl
model = pfpFile,
contentScale = ContentScale.Crop,
contentDescription = null,
)
HorizontalSpace(width = 20.dp)
Row(
modifier = Modifier
.clickable {
ImagePicker(
showPreview = false,
onSuccess = { uri ->
coroutineScope.launch(Dispatchers.IO) {
profileVM.setPfpFile(
File(FileUtils(context).getPath(uri))
)
}
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
}
) {
val uploadPhotoText = stringResource(id = R.string.upload_photo)
Icon(
painter = painterResource(id = R.drawable.image_down),
contentDescription = uploadPhotoText,
)
HorizontalSpace(width = 8.dp)
Text(text = uploadPhotoText, style = TextStyles.h4)
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
) {
val uploadPhotoText = stringResource(id = R.string.upload_photo)
Icon(
painter = painterResource(id = R.drawable.image_down),
contentDescription = uploadPhotoText,
)
HorizontalSpace(width = 8.dp)
Text(text = uploadPhotoText, style = TextStyles.h4)
}
}
}
VerticalSpace(height = 24.dp)

View file

@ -1,5 +1,6 @@
package app.tourism.ui.screens.main.profile.profile
import android.net.Uri
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import app.tourism.data.prefs.UserPreferences
@ -15,6 +16,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.launch
import java.io.File
import javax.inject.Inject
@HiltViewModel
@ -27,13 +29,20 @@ class ProfileViewModel @Inject constructor(
val uiEventsChannelFlow = uiChannel.receiveAsFlow()
// region fields to update
private val _pfpFile = MutableStateFlow<File?>(null)
val pfpFile = _pfpFile.asStateFlow()
fun setPfpFile(pfpFile: File) {
_pfpFile.value = pfpFile
}
private val _fullName = MutableStateFlow("")
val fullName = _fullName.asStateFlow()
fun setFullName(value: String) {
_fullName.value = value
}
private val _email = MutableStateFlow("")
val email = _email.asStateFlow()

View file

@ -0,0 +1,401 @@
package app.tourism.utils;
import android.annotation.SuppressLint;
import android.content.ContentUris;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Environment;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import android.provider.OpenableColumns;
import android.text.TextUtils;
import android.util.Log;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
/*
Trying to get path from uri is pretty hard in Android, in the end I just decided
to use this comprehensive method from stackoverflow, tried to be concise, but
really tired of this shit, it just took too much time. Works like charm, btw.
*/
public class FileUtils {
private static Uri contentUri = null;
static Context context;
public FileUtils(Context context) {
this.context=context;
}
@SuppressLint("NewApi")
public String getPath( final Uri uri) {
// check here to KITKAT or new version
final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
String selection = null;
String[] selectionArgs = null;
// DocumentProvider
if (isKitKat ) {
// ExternalStorageProvider
if (isExternalStorageDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
String fullPath = getPathFromExtSD(split);
if (fullPath != "") {
return fullPath;
} else {
return null;
}
}
// DownloadsProvider
if (isDownloadsDocument(uri)) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
final String id;
Cursor cursor = null;
try {
cursor = context.getContentResolver().query(uri, new String[]{MediaStore.MediaColumns.DISPLAY_NAME}, null, null, null);
if (cursor != null && cursor.moveToFirst()) {
String fileName = cursor.getString(0);
String path = Environment.getExternalStorageDirectory().toString() + "/Download/" + fileName;
if (!TextUtils.isEmpty(path)) {
return path;
}
}
}
finally {
if (cursor != null)
cursor.close();
}
id = DocumentsContract.getDocumentId(uri);
if (!TextUtils.isEmpty(id)) {
if (id.startsWith("raw:")) {
return id.replaceFirst("raw:", "");
}
String[] contentUriPrefixesToTry = new String[]{
"content://downloads/public_downloads",
"content://downloads/my_downloads"
};
for (String contentUriPrefix : contentUriPrefixesToTry) {
try {
final Uri contentUri = ContentUris.withAppendedId(Uri.parse(contentUriPrefix), Long.valueOf(id));
return getDataColumn(context, contentUri, null, null);
} catch (NumberFormatException e) {
//In Android 8 and Android P the id is not a number
return uri.getPath().replaceFirst("^/document/raw:", "").replaceFirst("^raw:", "");
}
}
}
}
else {
final String id = DocumentsContract.getDocumentId(uri);
if (id.startsWith("raw:")) {
return id.replaceFirst("raw:", "");
}
try {
contentUri = ContentUris.withAppendedId(
Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));
}
catch (NumberFormatException e) {
e.printStackTrace();
}
if (contentUri != null) {
return getDataColumn(context, contentUri, null, null);
}
}
}
// MediaProvider
if (isMediaDocument(uri)) {
final String docId = DocumentsContract.getDocumentId(uri);
final String[] split = docId.split(":");
final String type = split[0];
Uri contentUri = null;
if ("image".equals(type)) {
contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
} else if ("video".equals(type)) {
contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
} else if ("audio".equals(type)) {
contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
}
selection = "_id=?";
selectionArgs = new String[]{split[1]};
return getDataColumn(context, contentUri, selection,
selectionArgs);
}
if (isGoogleDriveUri(uri)) {
return getDriveFilePath(uri);
}
if(isWhatsAppFile(uri)){
return getFilePathForWhatsApp(uri);
}
if ("content".equalsIgnoreCase(uri.getScheme())) {
if (isGooglePhotosUri(uri)) {
return uri.getLastPathSegment();
}
if (isGoogleDriveUri(uri)) {
return getDriveFilePath(uri);
}
if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
{
// return getFilePathFromURI(context,uri);
return copyFileToInternalStorage(uri,"userfiles");
// return getRealPathFromURI(context,uri);
}
else
{
return getDataColumn(context, uri, null, null);
}
}
if ("file".equalsIgnoreCase(uri.getScheme())) {
return uri.getPath();
}
}
else {
if(isWhatsAppFile(uri)){
return getFilePathForWhatsApp(uri);
}
if ("content".equalsIgnoreCase(uri.getScheme())) {
String[] projection = {
MediaStore.Images.Media.DATA
};
Cursor cursor = null;
try {
cursor = context.getContentResolver()
.query(uri, projection, selection, selectionArgs, null);
int column_index = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
if (cursor.moveToFirst()) {
return cursor.getString(column_index);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
return null;
}
private static boolean fileExists(String filePath) {
File file = new File(filePath);
return file.exists();
}
private static String getPathFromExtSD(String[] pathData) {
final String type = pathData[0];
final String relativePath = "/" + pathData[1];
String fullPath = "";
// on my Sony devices (4.4.4 & 5.1.1), `type` is a dynamic string
// something like "71F8-2C0A", some kind of unique id per storage
// don't know any API that can get the root path of that storage based on its id.
//
// so no "primary" type, but let the check here for other devices
if ("primary".equalsIgnoreCase(type)) {
fullPath = Environment.getExternalStorageDirectory() + relativePath;
if (fileExists(fullPath)) {
return fullPath;
}
}
// Environment.isExternalStorageRemovable() is `true` for external and internal storage
// so we cannot relay on it.
//
// instead, for each possible path, check if file exists
// we'll start with secondary storage as this could be our (physically) removable sd card
fullPath = System.getenv("SECONDARY_STORAGE") + relativePath;
if (fileExists(fullPath)) {
return fullPath;
}
fullPath = System.getenv("EXTERNAL_STORAGE") + relativePath;
if (fileExists(fullPath)) {
return fullPath;
}
return fullPath;
}
private static String getDriveFilePath(Uri uri) {
Uri returnUri = uri;
Cursor returnCursor = context.getContentResolver().query(returnUri, null, null, null, null);
/*
* Get the column indexes of the data in the Cursor,
* * move to the first row in the Cursor, get the data,
* * and display it.
* */
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
returnCursor.moveToFirst();
String name = (returnCursor.getString(nameIndex));
String size = (Long.toString(returnCursor.getLong(sizeIndex)));
File file = new File(context.getCacheDir(), name);
try {
InputStream inputStream = context.getContentResolver().openInputStream(uri);
FileOutputStream outputStream = new FileOutputStream(file);
int read = 0;
int maxBufferSize = 1 * 1024 * 1024;
int bytesAvailable = inputStream.available();
//int bufferSize = 1024;
int bufferSize = Math.min(bytesAvailable, maxBufferSize);
final byte[] buffers = new byte[bufferSize];
while ((read = inputStream.read(buffers)) != -1) {
outputStream.write(buffers, 0, read);
}
Log.e("File Size", "Size " + file.length());
inputStream.close();
outputStream.close();
Log.e("File Path", "Path " + file.getPath());
Log.e("File Size", "Size " + file.length());
} catch (Exception e) {
Log.e("Exception", e.getMessage());
}
return file.getPath();
}
/***
* Used for Android Q+
* @param uri
* @param newDirName if you want to create a directory, you can set this variable
* @return
*/
private static String copyFileToInternalStorage(Uri uri, String newDirName) {
Uri returnUri = uri;
Cursor returnCursor = context.getContentResolver().query(returnUri, new String[]{
OpenableColumns.DISPLAY_NAME,OpenableColumns.SIZE
}, null, null, null);
/*
* Get the column indexes of the data in the Cursor,
* * move to the first row in the Cursor, get the data,
* * and display it.
* */
int nameIndex = returnCursor.getColumnIndex(OpenableColumns.DISPLAY_NAME);
int sizeIndex = returnCursor.getColumnIndex(OpenableColumns.SIZE);
returnCursor.moveToFirst();
String name = (returnCursor.getString(nameIndex));
String size = (Long.toString(returnCursor.getLong(sizeIndex)));
File output;
if(!newDirName.equals("")) {
File dir = new File(context.getFilesDir() + "/" + newDirName);
if (!dir.exists()) {
dir.mkdir();
}
output = new File(context.getFilesDir() + "/" + newDirName + "/" + name);
}
else{
output = new File(context.getFilesDir() + "/" + name);
}
try {
InputStream inputStream = context.getContentResolver().openInputStream(uri);
FileOutputStream outputStream = new FileOutputStream(output);
int read = 0;
int bufferSize = 1024;
final byte[] buffers = new byte[bufferSize];
while ((read = inputStream.read(buffers)) != -1) {
outputStream.write(buffers, 0, read);
}
inputStream.close();
outputStream.close();
}
catch (Exception e) {
Log.e("Exception", e.getMessage());
}
return output.getPath();
}
private static String getFilePathForWhatsApp(Uri uri){
return copyFileToInternalStorage(uri,"whatsapp");
}
private static String getDataColumn(Context context, Uri uri, String selection, String[] selectionArgs) {
Cursor cursor = null;
final String column = "_data";
final String[] projection = {column};
try {
cursor = context.getContentResolver().query(uri, projection,
selection, selectionArgs, null);
if (cursor != null && cursor.moveToFirst()) {
final int index = cursor.getColumnIndexOrThrow(column);
return cursor.getString(index);
}
}
finally {
if (cursor != null)
cursor.close();
}
return null;
}
private static boolean isExternalStorageDocument(Uri uri) {
return "com.android.externalstorage.documents".equals(uri.getAuthority());
}
private static boolean isDownloadsDocument(Uri uri) {
return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}
private static boolean isMediaDocument(Uri uri) {
return "com.android.providers.media.documents".equals(uri.getAuthority());
}
private static boolean isGooglePhotosUri(Uri uri) {
return "com.google.android.apps.photos.content".equals(uri.getAuthority());
}
public static boolean isWhatsAppFile(Uri uri){
return "com.whatsapp.provider.media".equals(uri.getAuthority());
}
private static boolean isGoogleDriveUri(Uri uri) {
return "com.google.android.apps.docs.storage".equals(uri.getAuthority()) || "com.google.android.apps.docs.storage.legacy".equals(uri.getAuthority());
}
}