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
This commit was merged in pull request #48.
This commit is contained in:
Alex
2026-03-26 00:47:47 -06:00
committed by Harkamal
parent aec9f7b9e0
commit 75c39312fe
4 changed files with 125 additions and 12 deletions

View File

@@ -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);
}
}
}

View File

@@ -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<AuthDTO.LoginResponse> login(@Body AuthDTO.LoginRequest loginRequest);
@@ -26,4 +32,9 @@ public interface AuthApi {
@PUT("api/v1/auth/me")
Call<UserDTO> updateMe(@Body Map<String, String> updates);
//upload avatar endpoint
@Multipart
@POST("api/v1/auth/me/avatar")
Call<UserDTO> uploadAvatar(@Part MultipartBody.Part avatar);
}

View File

@@ -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<UserDTO>() {
@Override
public void onResponse(Call<UserDTO> call, Response<UserDTO> 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<UserDTO> 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());