From b7681499aef4cd3769b8205501832dbce4dba630 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Thu, 2 Apr 2026 19:21:55 -0600 Subject: [PATCH] Added images to products for android - also added the option to delete the images to profile and pets --- .../adapters/ProductAdapter.java | 20 ++ .../example/petstoremobile/api/PetApi.java | 4 + .../petstoremobile/api/ProductApi.java | 9 + .../petstoremobile/api/auth/AuthApi.java | 5 + .../fragments/ProfileFragment.java | 55 ++++- .../listfragments/ProductFragment.java | 26 +- .../ProductDetailFragment.java | 229 +++++++++++++++++- .../PetProfileFragment.java | 54 ++++- .../src/main/res/drawable/placeholder2.png | Bin 0 -> 8806 bytes .../res/layout/fragment_product_detail.xml | 26 ++ .../app/src/main/res/layout/item_product.xml | 110 +++++---- 11 files changed, 467 insertions(+), 71 deletions(-) create mode 100644 android/app/src/main/res/drawable/placeholder2.png diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java index 300b7e57..a44ec993 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java @@ -1,10 +1,16 @@ package com.example.petstoremobile.adapters; import android.view.*; +import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.example.petstoremobile.R; +import com.example.petstoremobile.api.ProductApi; +import com.example.petstoremobile.api.RetrofitClient; import com.example.petstoremobile.dtos.ProductDTO; import java.util.List; @@ -24,6 +30,7 @@ public class ProductAdapter extends RecyclerView.Adapter listener.onProductClick(position)); } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java index e2eb3090..ff7b79a7 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java @@ -48,4 +48,8 @@ public interface PetApi { @POST("api/v1/pets/{id}/image") Call uploadPetImage(@Path("id") Long id, @Part MultipartBody.Part image); + // Delete pet image + @DELETE("api/v1/pets/{id}/image") + Call deletePetImage(@Path("id") Long id); + } diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java index 422a39e5..dc02fd6c 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ProductApi.java @@ -2,10 +2,12 @@ package com.example.petstoremobile.api; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.ProductDTO; +import okhttp3.MultipartBody; import retrofit2.Call; import retrofit2.http.*; public interface ProductApi { + String PRODUCT_IMAGE_PATH = "api/v1/products/%d/image"; @GET("api/v1/products") Call> getAllProducts( @@ -24,4 +26,11 @@ public interface ProductApi { @DELETE("api/v1/products/{id}") Call deleteProduct(@Path("id") Long id); + + @Multipart + @POST("api/v1/products/{id}/image") + Call uploadProductImage(@Path("id") Long id, @Part MultipartBody.Part image); + + @DELETE("api/v1/products/{id}/image") + Call deleteProductImage(@Path("id") Long id); } \ 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 75605083..88c5312c 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 @@ -8,6 +8,7 @@ import java.util.Map; import okhttp3.MultipartBody; import retrofit2.Call; import retrofit2.http.Body; +import retrofit2.http.DELETE; import retrofit2.http.GET; import retrofit2.http.Multipart; import retrofit2.http.POST; @@ -37,4 +38,8 @@ public interface AuthApi { @POST("api/v1/auth/me/avatar") Call uploadAvatar(@Part MultipartBody.Part avatar); + //delete avatar endpoint + @DELETE("api/v1/auth/me/avatar") + Call deleteAvatar(); + } 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 c7253c70..b29c038d 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 @@ -44,7 +44,9 @@ import com.google.gson.Gson; import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import okhttp3.MediaType; @@ -61,6 +63,7 @@ public class ProfileFragment extends Fragment { private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole; private Uri photoUri; private UserDTO currentUser; + private boolean hasImage = false; //Initialize the launchers for camera and gallery private ActivityResultLauncher galleryLauncher; @@ -149,12 +152,20 @@ public class ProfileFragment extends Fragment { //Set up listeners for the buttons //Change photo button btnChangePhoto.setOnClickListener(v -> { + List options = new ArrayList<>(); + options.add("Take Photo"); + options.add("Choose from Gallery"); + if (hasImage) { + options.add("Remove Photo"); + } + //Show alert dialog to user to select from gallery or camera new AlertDialog.Builder(requireContext()) .setTitle("Change Profile Photo") //set the options for the alert dialog - .setItems(new String[]{"Take Photo", "Choose from Gallery"}, (dialog, which) -> { - if (which == 0) { + .setItems(options.toArray(new String[0]), (dialog, which) -> { + String selected = options.get(which); + if (selected.equals("Take Photo")) { // Choose Camera //Checks if the user has granted the camera permission already if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { @@ -164,11 +175,13 @@ public class ProfileFragment extends Fragment { //otherwise request the permission permissionLauncher.launch(Manifest.permission.CAMERA); } - } else { + } else if (selected.equals("Choose from Gallery")) { // Choose Gallery Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); galleryLauncher.launch(intent); + } else if (selected.equals("Remove Photo")) { + deleteAvatar(); } }) .show(); @@ -294,9 +307,23 @@ public class ProfileFragment extends Fragment { .skipMemoryCache(true) .placeholder(R.drawable.placeholder) .error(R.drawable.placeholder) + .listener(new com.bumptech.glide.request.RequestListener() { + @Override + public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target target, boolean isFirstResource) { + hasImage = false; + return false; + } + + @Override + public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target target, com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) { + hasImage = true; + return false; + } + }) .into(imgProfile); } else { // load placeholder image if token is null + hasImage = false; Glide.with(ProfileFragment.this) .load(R.drawable.placeholder) .into(imgProfile); @@ -352,6 +379,28 @@ public class ProfileFragment extends Fragment { } } + private void deleteAvatar() { + AuthApi authApi = RetrofitClient.getAuthApi(requireContext()); + authApi.deleteAvatar().enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(requireContext(), "Avatar removed successfully", Toast.LENGTH_SHORT).show(); + hasImage = false; + imgProfile.setImageResource(R.drawable.placeholder); + } else { + Toast.makeText(requireContext(), "Failed to remove avatar", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("DELETE_AVATAR", "Failure: " + t.getMessage()); + Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + // Helper function to create a temporary File object from a Uri for uploading the avatar private File getFileFromUri(Uri uri) { try { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java index 4e72b6cd..e8b29611 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -36,6 +36,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc setupRecyclerView(view); setupSearch(view); setupSwipeRefresh(view); + loadProducts(); FloatingActionButton fab = view.findViewById(R.id.fabAddProduct); @@ -63,7 +64,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc public void beforeTextChanged(CharSequence s, int a, int b, int c) {} public void afterTextChanged(Editable s) {} public void onTextChanged(CharSequence s, int a, int b, int c) { - filter(s.toString()); + filter(); } }); } @@ -73,17 +74,18 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc swipeRefresh.setOnRefreshListener(this::loadProducts); } - private void filter(String query) { + private void filter() { + String query = etSearch.getText().toString().toLowerCase(); + filteredList.clear(); - if (query.isEmpty()) { - filteredList.addAll(productList); - } else { - String lower = query.toLowerCase(); - for (ProductDTO p : productList) { - if ((p.getProdName() != null && p.getProdName().toLowerCase().contains(lower)) - || (p.getCategoryName() != null && p.getCategoryName().toLowerCase().contains(lower))) { - filteredList.add(p); - } + for (ProductDTO p : productList) { + boolean matchesSearch = query.isEmpty() || + (p.getProdName() != null && p.getProdName().toLowerCase().contains(query)) || + (p.getCategoryName() != null && p.getCategoryName().toLowerCase().contains(query)) || + (p.getProdDesc() != null && p.getProdDesc().toLowerCase().contains(query)); + + if (matchesSearch) { + filteredList.add(p); } } adapter.notifyDataSetChanged(); @@ -99,7 +101,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc if (r.isSuccessful() && r.body() != null) { productList.clear(); productList.addAll(r.body().getContent()); - filter(etSearch != null ? etSearch.getText().toString() : ""); + filter(); } else { Toast.makeText(getContext(), "Failed to load products", Toast.LENGTH_SHORT).show(); diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java index 8427ab65..40bdc91b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -1,19 +1,37 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; import android.os.Bundle; +import android.provider.MediaStore; import android.util.Log; import android.view.*; import android.widget.*; +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; import androidx.fragment.app.Fragment; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.BlackTextArrayAdapter; import com.example.petstoremobile.api.*; import com.example.petstoremobile.dtos.*; import com.example.petstoremobile.fragments.ListFragment; +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; import java.math.BigDecimal; import java.util.*; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; import retrofit2.*; public class ProductDetailFragment extends Fragment { @@ -22,12 +40,59 @@ public class ProductDetailFragment extends Fragment { private EditText etProductName, etProductDesc, etProductPrice; private Spinner spinnerCategory; private Button btnSave, btnDelete, btnBack; + private ImageView ivProductImage; private long prodId = -1; private boolean isEditing = false; private long preselectedCategoryId = -1; + private boolean hasImage = false; private List categoryList = new ArrayList<>(); + private Uri photoUri; + + private ActivityResultLauncher galleryLauncher; + private ActivityResultLauncher cameraLauncher; + private ActivityResultLauncher permissionLauncher; + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + galleryLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + Uri selectedImage = result.getData().getData(); + if (isEditing) { + uploadProductImage(selectedImage); + } else { + ivProductImage.setImageURI(selectedImage); + photoUri = selectedImage; + hasImage = true; + } + } + } + ); + cameraLauncher = registerForActivityResult( + new ActivityResultContracts.TakePicture(), + success -> { + if (success) { + if (isEditing) { + uploadProductImage(photoUri); + } else { + ivProductImage.setImageURI(photoUri); + hasImage = true; + } + } + } + ); + permissionLauncher = registerForActivityResult( + new ActivityResultContracts.RequestPermission(), + granted -> { + if (granted) launchCamera(); + else Toast.makeText(getContext(), "Camera permission denied", Toast.LENGTH_SHORT).show(); + } + ); + } @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -40,6 +105,7 @@ public class ProductDetailFragment extends Fragment { btnBack.setOnClickListener(v -> navigateBack()); btnSave.setOnClickListener(v -> saveProduct()); btnDelete.setOnClickListener(v -> confirmDelete()); + ivProductImage.setOnClickListener(v -> showImagePickerDialog()); return view; } @@ -53,6 +119,71 @@ public class ProductDetailFragment extends Fragment { btnSave = v.findViewById(R.id.btnSaveProduct); btnDelete = v.findViewById(R.id.btnDeleteProduct); btnBack = v.findViewById(R.id.btnProductBack); + ivProductImage = v.findViewById(R.id.ivProductImage); + } + + // Helper function to show the image picker dialog + private void showImagePickerDialog() { + List options = new ArrayList<>(); + options.add("Take Photo"); + options.add("Choose from Gallery"); + if (hasImage) { + options.add("Remove Photo"); + } + + new AlertDialog.Builder(requireContext()) + .setTitle("Select Product Image") + .setItems(options.toArray(new String[0]), (dialog, which) -> { + String selectedOption = options.get(which); + if (selectedOption.equals("Take Photo")) { + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) + == PackageManager.PERMISSION_GRANTED) { + launchCamera(); + } else { + permissionLauncher.launch(Manifest.permission.CAMERA); + } + } else if (selectedOption.equals("Choose from Gallery")) { + Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + galleryLauncher.launch(intent); + } else if (selectedOption.equals("Remove Photo")) { + removePhoto(); + } + }) + .show(); + } + + // Helper function to remove the photo + private void removePhoto() { + if (isEditing) { + RetrofitClient.getProductApi(requireContext()).deleteProductImage(prodId) + .enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Photo removed", Toast.LENGTH_SHORT).show(); + ivProductImage.setImageResource(R.drawable.placeholder2); + hasImage = false; + } else { + Toast.makeText(getContext(), "Failed to remove photo", Toast.LENGTH_SHORT).show(); + } + } + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } else { + photoUri = null; + hasImage = false; + ivProductImage.setImageResource(R.drawable.placeholder2); + } + } + + // Helper function to launch the camera + private void launchCamera() { + File photoFile = new File(requireContext().getCacheDir(), "product_photo.jpg"); + photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile); + cameraLauncher.launch(photoUri); } private void loadCategories() { @@ -92,6 +223,7 @@ public class ProductDetailFragment extends Fragment { isEditing = true; prodId = a.getLong("prodId"); preselectedCategoryId = a.getLong("categoryId", -1); + hasImage = true; tvMode.setText("Edit Product"); tvProductId.setText("ID: " + prodId); @@ -100,10 +232,74 @@ public class ProductDetailFragment extends Fragment { etProductDesc.setText(a.getString("prodDesc")); etProductPrice.setText(a.getString("prodPrice")); btnDelete.setVisibility(View.VISIBLE); + loadProductImage(); } else { tvMode.setText("Add Product"); btnDelete.setVisibility(View.GONE); tvProductId.setVisibility(View.GONE); + hasImage = false; + } + } + + //load the product image from the backend + private void loadProductImage() { + String imageUrl = RetrofitClient.BASE_URL + String.format(Locale.US, ProductApi.PRODUCT_IMAGE_PATH, prodId); + Glide.with(this) + .load(imageUrl) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .placeholder(R.drawable.placeholder2) + .error(R.drawable.placeholder2) + .into(ivProductImage); + } + + // Function to upload the product image by calling the backend + private void uploadProductImage(Uri uri) { + try { + File file = getFileFromUri(uri); + if (file == null) return; + + RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); + MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); + + RetrofitClient.getProductApi(requireContext()).uploadProductImage(prodId, body) + .enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Image uploaded", Toast.LENGTH_SHORT).show(); + hasImage = true; + loadProductImage(); + } else { + Toast.makeText(getContext(), "Upload failed", Toast.LENGTH_SHORT).show(); + } + } + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } catch (Exception e) { + Log.e("ProductDetail", "Error uploading image", e); + } + } + + // Helper function to get the File from the Uri + private File getFileFromUri(Uri uri) { + try { + InputStream inputStream = requireContext().getContentResolver().openInputStream(uri); + File tempFile = new File(requireContext().getCacheDir(), "upload_product_image.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) { + return null; } } @@ -132,14 +328,30 @@ public class ProductDetailFragment extends Fragment { ProductDTO dto = new ProductDTO(name, category.getCategoryId(), desc, price); - Log.d("PRODUCT_SAVE", "name=" + name + " categoryId=" + category.getCategoryId() - + " price=" + price); - ProductApi api = RetrofitClient.getProductApi(requireContext()); if (isEditing) { api.updateProduct(prodId, dto).enqueue(simpleCallback("Updated")); } else { - api.createProduct(dto).enqueue(simpleCallback("Saved")); + api.createProduct(dto).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + long newId = response.body().getProdId(); + if (photoUri != null) { + prodId = newId; + uploadProductImage(photoUri); + } + Toast.makeText(getContext(), "Saved", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + Toast.makeText(getContext(), "Error saving", Toast.LENGTH_SHORT).show(); + } + } + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); } } @@ -150,17 +362,10 @@ public class ProductDetailFragment extends Fragment { Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); navigateBack(); } else { - try { - String err = r.errorBody().string(); - Log.e("PRODUCT_SAVE", "Error: " + err); - Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show(); - } catch (Exception e) { - Log.e("PRODUCT_SAVE", "Failed to read error"); - } + Toast.makeText(getContext(), "Error " + r.code(), Toast.LENGTH_SHORT).show(); } } public void onFailure(Call c, Throwable t) { - Log.e("PRODUCT_SAVE", "Failure: " + t.getMessage()); Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); } }; diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java index 454d263b..c4c47d31 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -36,6 +36,8 @@ import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDet import java.io.File; import java.io.FileOutputStream; import java.io.InputStream; +import java.util.ArrayList; +import java.util.List; import java.util.Locale; import okhttp3.MediaType; @@ -52,6 +54,7 @@ public class PetProfileFragment extends Fragment { private ImageView imgPet; private Uri photoUri; private int petId; + private boolean hasImage = false; // launchers for camera and gallery private ActivityResultLauncher galleryLauncher; @@ -162,10 +165,18 @@ public class PetProfileFragment extends Fragment { //Make change photo button ask user to select a new photo btnChangePhoto.setOnClickListener(v -> { + List options = new ArrayList<>(); + options.add("Take Photo"); + options.add("Choose from Gallery"); + if (hasImage) { + options.add("Remove Photo"); + } + new AlertDialog.Builder(requireContext()) .setTitle("Change Pet Photo") - .setItems(new String[]{"Take Photo", "Choose from Gallery"}, (dialog, which) -> { - if (which == 0) { + .setItems(options.toArray(new String[0]), (dialog, which) -> { + String selected = options.get(which); + if (selected.equals("Take Photo")) { // Choose Camera //Checks if the user has granted the camera permission already if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { @@ -175,9 +186,11 @@ public class PetProfileFragment extends Fragment { //otherwise request the permission permissionLauncher.launch(Manifest.permission.CAMERA); } - } else { + } else if (selected.equals("Choose from Gallery")) { Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); galleryLauncher.launch(intent); + } else if (selected.equals("Remove Photo")) { + deletePetImage(); } }) .show(); @@ -196,6 +209,19 @@ public class PetProfileFragment extends Fragment { .skipMemoryCache(true) .placeholder(R.drawable.placeholder) .error(R.drawable.placeholder) + .listener(new com.bumptech.glide.request.RequestListener() { + @Override + public boolean onLoadFailed(@androidx.annotation.Nullable com.bumptech.glide.load.engine.GlideException e, Object model, com.bumptech.glide.request.target.Target target, boolean isFirstResource) { + hasImage = false; + return false; + } + + @Override + public boolean onResourceReady(android.graphics.drawable.Drawable resource, Object model, com.bumptech.glide.request.target.Target target, com.bumptech.glide.load.DataSource dataSource, boolean isFirstResource) { + hasImage = true; + return false; + } + }) .into(imgPet); } @@ -234,6 +260,28 @@ public class PetProfileFragment extends Fragment { } } + private void deletePetImage() { + PetApi petApi = RetrofitClient.getPetApi(requireContext()); + petApi.deletePetImage((long) petId).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(requireContext(), "Pet photo removed", Toast.LENGTH_SHORT).show(); + hasImage = false; + imgPet.setImageResource(R.drawable.placeholder); + } else { + Toast.makeText(requireContext(), "Failed to remove pet photo", Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("DELETE_PET_IMAGE", "Failure: " + t.getMessage()); + Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + // Helper function to create a temporary File object from a Uri for uploading private File getFileFromUri(Uri uri) { try { diff --git a/android/app/src/main/res/drawable/placeholder2.png b/android/app/src/main/res/drawable/placeholder2.png new file mode 100644 index 0000000000000000000000000000000000000000..dec35ec57f578998726aba4e5bb1541929e29832 GIT binary patch literal 8806 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 literal 0 HcmV?d00001 diff --git a/android/app/src/main/res/layout/fragment_product_detail.xml b/android/app/src/main/res/layout/fragment_product_detail.xml index b7d14a9d..7ab22faa 100644 --- a/android/app/src/main/res/layout/fragment_product_detail.xml +++ b/android/app/src/main/res/layout/fragment_product_detail.xml @@ -64,6 +64,32 @@ android:layout_gravity="end" android:layout_marginBottom="8dp"/> + + + + + + + + + - - - - - - + android:gravity="center_vertical"> - + + + android:orientation="vertical"> + + + + + + + + + + + +