From 39f912f711dcbcf799528ad398229de3c64ab7c4 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 26 Mar 2026 00:47:47 -0600 Subject: [PATCH] Added profile photo loading and uploading - profile photos now load from backend - profile photos can be uploaded to the backend - RetrofitClient now automatically determines if the device is an emulator or hardware so we dont have to comment and uncomment everytime we test with a different device --- android/app/build.gradle.kts | 3 + .../petstoremobile/api/RetrofitClient.java | 23 +++- .../petstoremobile/api/auth/AuthApi.java | 11 ++ .../fragments/ProfileFragment.java | 100 ++++++++++++++++-- 4 files changed, 125 insertions(+), 12 deletions(-) diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index d1cc3c30..fa8c88a7 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -56,6 +56,9 @@ dependencies { implementation("io.reactivex.rxjava2:rxjava:2.2.21") implementation("io.reactivex.rxjava2:rxandroid:2.1.1") + implementation("com.github.bumptech.glide:glide:4.16.0") + annotationProcessor("com.github.bumptech.glide:compiler:4.16.0") + testImplementation(libs.junit) androidTestImplementation(libs.ext.junit) androidTestImplementation(libs.espresso.core) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java index 8a7f48bc..1500315f 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java @@ -1,6 +1,7 @@ package com.example.petstoremobile.api; import android.content.Context; +import android.os.Build; import com.example.petstoremobile.api.auth.AuthApi; import com.example.petstoremobile.api.auth.AuthInterceptor; @@ -12,9 +13,23 @@ import retrofit2.converter.gson.GsonConverterFactory; //Retrofit client Used for API calls public class RetrofitClient { - //base URL - public static final String BASE_URL = "http://10.0.2.2:8080"; //for emulator testing -// public static final String BASE_URL = "http://10.0.0.200:8080/"; //for hardware testing + public static final String BASE_URL = getBaseUrl(); + + // Helper function to determine BASE_URL based on whether we are testing on an emulator or a real device + private static String getBaseUrl() { + if (Build.FINGERPRINT.contains("generic") + || Build.FINGERPRINT.contains("unknown") + || Build.MODEL.contains("google_sdk") + || Build.MODEL.contains("Emulator") + || Build.MODEL.contains("Android SDK built for x86") + || Build.MANUFACTURER.contains("Genymotion") + || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) + || "google_sdk".equals(Build.PRODUCT)) { + return "http://10.0.2.2:8080/"; //emulator testing + } else { + return "http://10.0.0.200:8080/"; //Hardware testing + } + } private static Retrofit retrofit = null; @@ -67,4 +82,4 @@ public class RetrofitClient { return getClient(context).create(MessageApi.class); } -} \ No newline at end of file +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java index a60fc536..75605083 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java @@ -5,15 +5,21 @@ import com.example.petstoremobile.dtos.UserDTO; import java.util.Map; +import okhttp3.MultipartBody; import retrofit2.Call; import retrofit2.http.Body; import retrofit2.http.GET; +import retrofit2.http.Multipart; import retrofit2.http.POST; import retrofit2.http.PUT; +import retrofit2.http.Part; //Api for logging in and getting current user public interface AuthApi { + // endpoint for downloading the current user's avatar file + String AVATAR_FILE_PATH = "api/v1/auth/me/avatar/file"; + //login endpoint @POST("api/v1/auth/login") Call login(@Body AuthDTO.LoginRequest loginRequest); @@ -26,4 +32,9 @@ public interface AuthApi { @PUT("api/v1/auth/me") Call updateMe(@Body Map updates); + //upload avatar endpoint + @Multipart + @POST("api/v1/auth/me/avatar") + Call uploadAvatar(@Part MultipartBody.Part avatar); + } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java index e754b7fa..c7253c70 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -26,6 +26,10 @@ import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; +import com.bumptech.glide.load.model.GlideUrl; +import com.bumptech.glide.load.model.LazyHeaders; import com.example.petstoremobile.R; import com.example.petstoremobile.activities.MainActivity; import com.example.petstoremobile.api.RetrofitClient; @@ -38,9 +42,14 @@ import com.example.petstoremobile.utils.InputValidator; import com.google.gson.Gson; import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; import java.util.HashMap; import java.util.Map; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; @@ -73,8 +82,7 @@ public class ProfileFragment extends Fragment { && result.getData() != null) { //get the selected image and set the image to the profile Uri selectedImage = result.getData().getData(); - imgProfile.setImageURI(selectedImage); - //TODO: SAVE CHANGED PHOTO TO DATABASE + uploadAvatar(selectedImage); } } ); @@ -86,10 +94,7 @@ public class ProfileFragment extends Fragment { success -> { //if a photo is taken set the image profile to it otherwise do nothing if (success) { - //Clear the old image and set the new one - imgProfile.setImageURI(null); - imgProfile.setImageURI(photoUri); - //TODO: SAVE CHANGED PHOTO TO DATABASE + uploadAvatar(photoUri); } } ); @@ -167,7 +172,6 @@ public class ProfileFragment extends Fragment { } }) .show(); - //TODO: UPDATE PHOTO IN DATABASE }); //Edit email button @@ -272,7 +276,31 @@ public class ProfileFragment extends Fragment { tvProfileEmail.setText(currentUser.getEmail()); tvProfilePhone.setText(currentUser.getPhone()); tvProfileRole.setText(currentUser.getRole()); - //TODO: LOAD PHOTO FROM DATABASE + + // get the avatar endpoint to load profile image and the token for authorization + String avatarUrl = RetrofitClient.BASE_URL + AuthApi.AVATAR_FILE_PATH; + String token = TokenManager.getInstance(requireContext()).getToken(); + + if (token != null) { + // Create GlideUrl with token to fetch the image + GlideUrl glideUrl = new GlideUrl(avatarUrl, new LazyHeaders.Builder() + .addHeader("Authorization", "Bearer " + token) + .build()); + + // Load image using Glide + Glide.with(ProfileFragment.this) + .load(glideUrl) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .placeholder(R.drawable.placeholder) + .error(R.drawable.placeholder) + .into(imgProfile); + } else { + // load placeholder image if token is null + Glide.with(ProfileFragment.this) + .load(R.drawable.placeholder) + .into(imgProfile); + } } else { Log.e("onResponse: ", response.message()); @@ -288,6 +316,62 @@ public class ProfileFragment extends Fragment { }); } + //Helper function to call the backend to upload a profile image + private void uploadAvatar(Uri uri) { + try { + File file = getFileFromUri(uri); + if (file == null) return; + + // Create RequestBody for file upload + RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); + MultipartBody.Part body = MultipartBody.Part.createFormData("avatar", file.getName(), requestFile); + + //Call the backend to upload the avatar + AuthApi authApi = RetrofitClient.getAuthApi(requireContext()); + authApi.uploadAvatar(body).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + currentUser = response.body(); + Toast.makeText(requireContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show(); + // Reload image after successful upload + loadProfileData(); + } else { + Toast.makeText(requireContext(), "Failed to upload avatar", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("UPLOAD_AVATAR", "Failure: " + t.getMessage()); + Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } catch (Exception e) { + Log.e("UPLOAD_AVATAR", "Error: " + e.getMessage()); + } + } + + // Helper function to create a temporary File object from a Uri for uploading the avatar + private File getFileFromUri(Uri uri) { + try { + InputStream inputStream = requireContext().getContentResolver().openInputStream(uri); + File tempFile = new File(requireContext().getCacheDir(), "upload_avatar.jpg"); + FileOutputStream outputStream = new FileOutputStream(tempFile); + byte[] buffer = new byte[1024]; + int length; + while ((length = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, length); + } + outputStream.close(); + inputStream.close(); + return tempFile; + } catch (Exception e) { + Log.e("FILE_UTILS", "Error creating temp file", e); + return null; + } + } + //Helper function to update a profile field in the backend private void updateProfileField(String fieldName, String value) { AuthApi authApi = RetrofitClient.getAuthApi(requireContext());