Merge pull request #30 from RecentRunner/WorkingOnProfileAndPushNotification
Working on profile and push notification
This commit is contained in:
1
android/.idea/gradle.xml
generated
1
android/.idea/gradle.xml
generated
@@ -1,5 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
<component name="GradleSettings">
|
<component name="GradleSettings">
|
||||||
<option name="linkedExternalProjectsSettings">
|
<option name="linkedExternalProjectsSettings">
|
||||||
<GradleProjectSettings>
|
<GradleProjectSettings>
|
||||||
|
|||||||
1
android/.idea/misc.xml
generated
1
android/.idea/misc.xml
generated
@@ -1,4 +1,3 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_21" default="true" project-jdk-name="jbr-21" project-jdk-type="JavaSDK">
|
||||||
|
|||||||
6
android/.idea/vcs.xml
generated
Normal file
6
android/.idea/vcs.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.camera"
|
android:name="android.hardware.camera"
|
||||||
@@ -24,6 +25,11 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.PetStoreMobile">
|
android:theme="@style/Theme.PetStoreMobile">
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".services.ChatNotificationService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.HomeActivity"
|
android:name=".activities.HomeActivity"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
|||||||
@@ -7,7 +7,5 @@ public class PetStoreApplication extends Application {
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
// Clear login data on app so when the application closes, the user is logged out and have to re-login
|
|
||||||
TokenManager.getInstance(this).clearLoginData();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,40 @@
|
|||||||
package com.example.petstoremobile.activities;
|
package com.example.petstoremobile.activities;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.activity.EdgeToEdge;
|
import androidx.activity.EdgeToEdge;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.graphics.Insets;
|
import androidx.core.graphics.Insets;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.core.view.WindowCompat;
|
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import androidx.core.view.WindowInsetsControllerCompat;
|
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.fragments.ChatFragment;
|
import com.example.petstoremobile.fragments.ChatFragment;
|
||||||
import com.example.petstoremobile.fragments.ListFragment;
|
import com.example.petstoremobile.fragments.ListFragment;
|
||||||
import com.example.petstoremobile.fragments.ProfileFragment;
|
import com.example.petstoremobile.fragments.ProfileFragment;
|
||||||
|
import com.example.petstoremobile.services.ChatNotificationService;
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||||
|
|
||||||
public class HomeActivity extends AppCompatActivity {
|
public class HomeActivity extends AppCompatActivity {
|
||||||
|
private BottomNavigationView bottomNav;
|
||||||
|
|
||||||
|
// Launcher to ask for notification permission
|
||||||
|
private final ActivityResultLauncher<String> requestPermissionLauncher =
|
||||||
|
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
|
||||||
|
if (!isGranted) {
|
||||||
|
Log.w("HomeActivity", "Notification permission denied");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -34,17 +49,15 @@ public class HomeActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//get the bottom navbar from the layout
|
//get the bottom navbar from the layout
|
||||||
BottomNavigationView bottomNav = findViewById(R.id.bottom_navigation);
|
bottomNav = findViewById(R.id.bottom_navigation);
|
||||||
|
|
||||||
// Load ListFragment by default only if this is a fresh start
|
//load the list fragment by default if it's a fresh start
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
loadFragment(new ListFragment());
|
handleIntent(getIntent());
|
||||||
bottomNav.setSelectedItemId(R.id.nav_list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//when an item in the bar is selected, load the corresponding fragment
|
//when an item in the bottom bar is selected, load the corresponding fragment
|
||||||
bottomNav.setOnItemSelectedListener(item -> {
|
bottomNav.setOnItemSelectedListener(item -> {
|
||||||
|
|
||||||
if (item.getItemId() == R.id.nav_list) {
|
if (item.getItemId() == R.id.nav_list) {
|
||||||
loadFragment(new ListFragment());
|
loadFragment(new ListFragment());
|
||||||
return true;
|
return true;
|
||||||
@@ -57,9 +70,49 @@ public class HomeActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Start the notification service and request for notification permission
|
||||||
|
startNotificationService();
|
||||||
|
requestNotificationPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
//helper function to load a fragment
|
// Handle new intents when the activity is already running,
|
||||||
|
// like clicking a notification while the app is in use
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
handleIntent(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to process intents for navigation.
|
||||||
|
// like clicking a notification or just launching the app from a fresh start
|
||||||
|
private void handleIntent(Intent intent) {
|
||||||
|
if (intent != null && "chat".equals(intent.getStringExtra("navigate_to"))) {
|
||||||
|
loadFragment(new ChatFragment());
|
||||||
|
bottomNav.setSelectedItemId(R.id.nav_chat);
|
||||||
|
} else {
|
||||||
|
loadFragment(new ListFragment());
|
||||||
|
bottomNav.setSelectedItemId(R.id.nav_list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to start the notification service in the background
|
||||||
|
// to receive notifications when a new conversation is created
|
||||||
|
private void startNotificationService() {
|
||||||
|
Intent serviceIntent = new Intent(this, ChatNotificationService.class);
|
||||||
|
startService(serviceIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Helper function to request for notification permission
|
||||||
|
private void requestNotificationPermission() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Helper function to load a fragment
|
||||||
private void loadFragment(Fragment fragment) {
|
private void loadFragment(Fragment fragment) {
|
||||||
getSupportFragmentManager()
|
getSupportFragmentManager()
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import com.example.petstoremobile.api.auth.AuthApi;
|
|||||||
import com.example.petstoremobile.api.auth.TokenManager;
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
import com.example.petstoremobile.api.RetrofitClient;
|
import com.example.petstoremobile.api.RetrofitClient;
|
||||||
import com.example.petstoremobile.dtos.AuthDTO;
|
import com.example.petstoremobile.dtos.AuthDTO;
|
||||||
|
import com.example.petstoremobile.dtos.UserDTO;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
@@ -90,11 +91,11 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
);
|
);
|
||||||
|
|
||||||
//fetch user id from api then login to home activity
|
//fetch user id from api then login to home activity
|
||||||
RetrofitClient.getAuthApi(MainActivity.this).getCurrentUser()
|
RetrofitClient.getAuthApi(MainActivity.this).getMe()
|
||||||
.enqueue(new Callback<AuthDTO.UserResponse>() {
|
.enqueue(new Callback<UserDTO>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<AuthDTO.UserResponse> call,
|
public void onResponse(Call<UserDTO> call,
|
||||||
Response<AuthDTO.UserResponse> response) {
|
Response<UserDTO> response) {
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
TokenManager.getInstance(MainActivity.this)
|
TokenManager.getInstance(MainActivity.this)
|
||||||
.saveUserId(response.body().getId());
|
.saveUserId(response.body().getId());
|
||||||
@@ -106,7 +107,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<AuthDTO.UserResponse> call,
|
public void onFailure(Call<UserDTO> call,
|
||||||
Throwable t) {
|
Throwable t) {
|
||||||
Log.e("MainActivity", "Failed to fetch userId", t);
|
Log.e("MainActivity", "Failed to fetch userId", t);
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,29 @@
|
|||||||
package com.example.petstoremobile.api.auth;
|
package com.example.petstoremobile.api.auth;
|
||||||
|
|
||||||
import com.example.petstoremobile.dtos.AuthDTO;
|
import com.example.petstoremobile.dtos.AuthDTO;
|
||||||
|
import com.example.petstoremobile.dtos.UserDTO;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.http.Body;
|
import retrofit2.http.Body;
|
||||||
import retrofit2.http.GET;
|
import retrofit2.http.GET;
|
||||||
import retrofit2.http.POST;
|
import retrofit2.http.POST;
|
||||||
|
import retrofit2.http.PUT;
|
||||||
|
|
||||||
//Api for logging in and getting current user
|
//Api for logging in and getting current user
|
||||||
public interface AuthApi {
|
public interface AuthApi {
|
||||||
|
|
||||||
|
//login endpoint
|
||||||
@POST("api/v1/auth/login")
|
@POST("api/v1/auth/login")
|
||||||
Call<AuthDTO.LoginResponse> login(@Body AuthDTO.LoginRequest loginRequest);
|
Call<AuthDTO.LoginResponse> login(@Body AuthDTO.LoginRequest loginRequest);
|
||||||
|
|
||||||
|
//get current user endpoint
|
||||||
@GET("api/v1/auth/me")
|
@GET("api/v1/auth/me")
|
||||||
Call<AuthDTO.UserResponse> getCurrentUser();
|
Call<UserDTO> getMe();
|
||||||
|
|
||||||
|
//update current user endpoint
|
||||||
|
@PUT("api/v1/auth/me")
|
||||||
|
Call<UserDTO> updateMe(@Body Map<String, String> updates);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.example.petstoremobile.dtos;
|
||||||
|
|
||||||
|
//Used to get messages of any errors from the backend
|
||||||
|
|
||||||
|
public class ErrorResponse {
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.example.petstoremobile.dtos;
|
||||||
|
|
||||||
|
public class UserDTO {
|
||||||
|
private Long id;
|
||||||
|
private String username;
|
||||||
|
private String email;
|
||||||
|
private String fullName;
|
||||||
|
private String phone;
|
||||||
|
private String avatarUrl;
|
||||||
|
private String role;
|
||||||
|
private Long storeId;
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFullName() {
|
||||||
|
return fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhone() {
|
||||||
|
return phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAvatarUrl() {
|
||||||
|
return avatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getStoreId() {
|
||||||
|
return storeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStoreName() {
|
||||||
|
return storeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
|
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -23,20 +24,34 @@ import android.widget.Button;
|
|||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.activities.MainActivity;
|
import com.example.petstoremobile.activities.MainActivity;
|
||||||
|
import com.example.petstoremobile.api.RetrofitClient;
|
||||||
|
import com.example.petstoremobile.api.auth.AuthApi;
|
||||||
import com.example.petstoremobile.api.auth.TokenManager;
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
|
import com.example.petstoremobile.dtos.ErrorResponse;
|
||||||
|
import com.example.petstoremobile.dtos.UserDTO;
|
||||||
|
import com.example.petstoremobile.services.ChatNotificationService;
|
||||||
|
import com.example.petstoremobile.utils.InputValidator;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import retrofit2.Call;
|
||||||
|
import retrofit2.Callback;
|
||||||
|
import retrofit2.Response;
|
||||||
|
|
||||||
public class ProfileFragment extends Fragment {
|
public class ProfileFragment extends Fragment {
|
||||||
|
|
||||||
//initialize the view/controls
|
//initialize the view/controls
|
||||||
private ImageView imgProfile;
|
private ImageView imgProfile;
|
||||||
private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole;
|
private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole;
|
||||||
private Button btnChangePhoto, btnEditEmail, btnEditPhone, btnLogout;
|
|
||||||
private Uri photoUri;
|
private Uri photoUri;
|
||||||
|
private UserDTO currentUser;
|
||||||
|
|
||||||
//Initialize the launchers for camera and gallery
|
//Initialize the launchers for camera and gallery
|
||||||
private ActivityResultLauncher<Intent> galleryLauncher;
|
private ActivityResultLauncher<Intent> galleryLauncher;
|
||||||
@@ -107,7 +122,6 @@ public class ProfileFragment extends Fragment {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: MAKE PROFILE VIEW DISPLAY PROFILE DATA FROM DATABASE
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
@@ -119,10 +133,13 @@ public class ProfileFragment extends Fragment {
|
|||||||
tvProfileEmail = view.findViewById(R.id.tvProfileEmail);
|
tvProfileEmail = view.findViewById(R.id.tvProfileEmail);
|
||||||
tvProfilePhone = view.findViewById(R.id.tvProfilePhone);
|
tvProfilePhone = view.findViewById(R.id.tvProfilePhone);
|
||||||
tvProfileRole = view.findViewById(R.id.tvProfileRole);
|
tvProfileRole = view.findViewById(R.id.tvProfileRole);
|
||||||
btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
|
Button btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
|
||||||
btnEditEmail = view.findViewById(R.id.btnEditEmail);
|
Button btnEditEmail = view.findViewById(R.id.btnEditEmail);
|
||||||
btnEditPhone = view.findViewById(R.id.btnEditPhone);
|
Button btnEditPhone = view.findViewById(R.id.btnEditPhone);
|
||||||
btnLogout = view.findViewById(R.id.btnLogout);
|
Button btnLogout = view.findViewById(R.id.btnLogout);
|
||||||
|
|
||||||
|
//Load Profile Data from backend
|
||||||
|
loadProfileData();
|
||||||
|
|
||||||
//Set up listeners for the buttons
|
//Set up listeners for the buttons
|
||||||
//Change photo button
|
//Change photo button
|
||||||
@@ -170,19 +187,10 @@ public class ProfileFragment extends Fragment {
|
|||||||
.setTitle("Edit Email")
|
.setTitle("Edit Email")
|
||||||
.setView(input)
|
.setView(input)
|
||||||
.setPositiveButton("Save", (dialog, which) -> {
|
.setPositiveButton("Save", (dialog, which) -> {
|
||||||
String newEmail = input.getText().toString();
|
if (InputValidator.isValidEmail(input)) {
|
||||||
//if the new value is a valid email then set the email to the new value
|
updateProfileField("email", input.getText().toString());
|
||||||
if (android.util.Patterns.EMAIL_ADDRESS.matcher(newEmail).matches()) {
|
} else {
|
||||||
tvProfileEmail.setText(newEmail);
|
Toast.makeText(requireContext(), "Email is invalid", Toast.LENGTH_SHORT).show();
|
||||||
//TODO: UPDATE THE EMAIL IN DATABASE
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
//tell the user to email is invalid
|
|
||||||
new AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle("Error")
|
|
||||||
.setMessage("Email is invalid")
|
|
||||||
.setPositiveButton("OK", null)
|
|
||||||
.show();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setNegativeButton("Cancel", null)
|
.setNegativeButton("Cancel", null)
|
||||||
@@ -210,19 +218,10 @@ public class ProfileFragment extends Fragment {
|
|||||||
.setTitle("Edit Phone Number")
|
.setTitle("Edit Phone Number")
|
||||||
.setView(input)
|
.setView(input)
|
||||||
.setPositiveButton("Save", (dialog, which) -> {
|
.setPositiveButton("Save", (dialog, which) -> {
|
||||||
String newPhone = input.getText().toString();
|
if (InputValidator.isValidPhone(input)) {
|
||||||
//if the new value is format: (XXX) XXX-XXXX then set the phone to the new value
|
updateProfileField("phone", input.getText().toString());
|
||||||
if (newPhone.matches("\\(\\d{3}\\) \\d{3}-\\d{4}")) { //TODO MAKE VALIDATION CLASS INSTEAD FOR THIS
|
} else {
|
||||||
tvProfilePhone.setText(newPhone);
|
Toast.makeText(requireContext(), "Phone number is invalid", Toast.LENGTH_SHORT).show();
|
||||||
//TODO: UPDATE PHONE IN DATABASE
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
//tell the user to email cannot be empty
|
|
||||||
new AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle("Error")
|
|
||||||
.setMessage("Phone number is invalid. Format: (XXX) XXX-XXXX")
|
|
||||||
.setPositiveButton("OK", null)
|
|
||||||
.show();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setNegativeButton("Cancel", null)
|
.setNegativeButton("Cancel", null)
|
||||||
@@ -231,6 +230,10 @@ public class ProfileFragment extends Fragment {
|
|||||||
|
|
||||||
//Logout button
|
//Logout button
|
||||||
btnLogout.setOnClickListener(v -> {
|
btnLogout.setOnClickListener(v -> {
|
||||||
|
// Stop notification service before logging out so notifications stop
|
||||||
|
Intent serviceIntent = new Intent(requireContext(), ChatNotificationService.class);
|
||||||
|
requireContext().stopService(serviceIntent);
|
||||||
|
|
||||||
TokenManager.getInstance(requireContext()).clearLoginData(); // clear the token for next login
|
TokenManager.getInstance(requireContext()).clearLoginData(); // clear the token for next login
|
||||||
//get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen
|
//get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen
|
||||||
Intent intent = new Intent(getActivity(), MainActivity.class);
|
Intent intent = new Intent(getActivity(), MainActivity.class);
|
||||||
@@ -252,4 +255,72 @@ public class ProfileFragment extends Fragment {
|
|||||||
//launch the camera to capture the photo and save the photo to photoUri
|
//launch the camera to capture the photo and save the photo to photoUri
|
||||||
cameraLauncher.launch(photoUri);
|
cameraLauncher.launch(photoUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Helper function to call the backend to get profile data and load it to the view
|
||||||
|
private void loadProfileData() {
|
||||||
|
AuthApi authApi = RetrofitClient.getAuthApi(requireContext());
|
||||||
|
|
||||||
|
authApi.getMe().enqueue(new Callback<UserDTO>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<UserDTO> call, Response<UserDTO> response) {
|
||||||
|
//if the response is successful and the body is not null then set the user to the view
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
currentUser = response.body();
|
||||||
|
|
||||||
|
//set the user data to the view
|
||||||
|
tvProfileName.setText(currentUser.getFullName());
|
||||||
|
tvProfileEmail.setText(currentUser.getEmail());
|
||||||
|
tvProfilePhone.setText(currentUser.getPhone());
|
||||||
|
tvProfileRole.setText(currentUser.getRole());
|
||||||
|
//TODO: LOAD PHOTO FROM DATABASE
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.e("onResponse: ", response.message());
|
||||||
|
Toast.makeText(getContext(), "Failed to load profile: ", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<UserDTO> call, Throwable t) {
|
||||||
|
Log.e("PROFILE", "onFailure: " + t.getMessage());
|
||||||
|
Toast.makeText(getContext(), "Network error: could not load profile", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Helper function to update a profile field in the backend
|
||||||
|
private void updateProfileField(String fieldName, String value) {
|
||||||
|
AuthApi authApi = RetrofitClient.getAuthApi(requireContext());
|
||||||
|
Map<String, String> updates = new HashMap<>();
|
||||||
|
updates.put(fieldName, value);
|
||||||
|
|
||||||
|
authApi.updateMe(updates).enqueue(new Callback<UserDTO>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<UserDTO> call, Response<UserDTO> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
currentUser = response.body();
|
||||||
|
// Update the view with the new data from backend
|
||||||
|
tvProfileEmail.setText(currentUser.getEmail());
|
||||||
|
tvProfilePhone.setText(currentUser.getPhone());
|
||||||
|
Toast.makeText(requireContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
String errorJson = response.errorBody().string();
|
||||||
|
ErrorResponse errorResponse = new Gson().fromJson(errorJson, ErrorResponse.class);
|
||||||
|
String errorMessage = errorResponse.getMessage();
|
||||||
|
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_LONG).show();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("UPDATE_PROFILE", "Error parsing error body", e);
|
||||||
|
Toast.makeText(requireContext(), "Failed to update profile", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<UserDTO> call, Throwable t) {
|
||||||
|
Log.e("UPDATE_PROFILE", "Failure: " + t.getMessage());
|
||||||
|
Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
package com.example.petstoremobile.services;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.util.Log;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
|
import com.example.petstoremobile.dtos.ConversationDTO;
|
||||||
|
import com.example.petstoremobile.utils.NotificationHelper;
|
||||||
|
import com.example.petstoremobile.websocket.StompChatManager;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
// Service to receive notifications when a new conversation is created
|
||||||
|
public class ChatNotificationService extends Service {
|
||||||
|
private static final String TAG = "ChatNotificationService";
|
||||||
|
private StompChatManager stompChatManager;
|
||||||
|
private final Set<Long> knownConversationIds = new HashSet<>();
|
||||||
|
|
||||||
|
//When the service starts, connect to the websocket
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
Log.d(TAG, "Service started");
|
||||||
|
connectWebSocket();
|
||||||
|
return START_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to connect to the websocket
|
||||||
|
private void connectWebSocket() {
|
||||||
|
//get the token and role from the shared preferences
|
||||||
|
TokenManager tm = TokenManager.getInstance(this);
|
||||||
|
String token = tm.getToken();
|
||||||
|
String role = tm.getRole();
|
||||||
|
|
||||||
|
|
||||||
|
if (token != null && stompChatManager == null) {
|
||||||
|
stompChatManager = new StompChatManager(token, role);
|
||||||
|
|
||||||
|
//When a conversation gets created, show a notification
|
||||||
|
stompChatManager.setConversationListener(conversation -> {
|
||||||
|
//check if the conversation exists
|
||||||
|
if (conversation != null && conversation.getId() != null) {
|
||||||
|
//check if the conversation is new
|
||||||
|
if (!knownConversationIds.contains(conversation.getId())) {
|
||||||
|
//add the conversation to the set of known conversations
|
||||||
|
knownConversationIds.add(conversation.getId());
|
||||||
|
NotificationHelper.showNotification(
|
||||||
|
getApplicationContext(),
|
||||||
|
"Customer Support",
|
||||||
|
"A customer request for assistance"
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
stompChatManager.setConnectionListener(new StompChatManager.ConnectionListener() {
|
||||||
|
// when the websocket is connected, set isFirstLoad to false after a delay
|
||||||
|
@Override
|
||||||
|
public void onSocketOpened() {
|
||||||
|
Log.d(TAG, "WebSocket connected in service");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSocketClosed() { Log.d(TAG, "WebSocket closed in service"); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSocketError() { Log.e(TAG, "WebSocket error in service"); }
|
||||||
|
});
|
||||||
|
stompChatManager.connect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//When the service is destroyed, disconnect from the websocket
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
if (stompChatManager != null) {
|
||||||
|
stompChatManager.disconnect();
|
||||||
|
}
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,10 +61,11 @@ public class InputValidator {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if the phone number is valid (digits, spaces, dashes, brackets allowed)
|
// Checks if the phone number is valid
|
||||||
public static boolean isValidPhone(EditText field) {
|
public static boolean isValidPhone(EditText field) {
|
||||||
String phone = field.getText().toString().trim();
|
String phone = field.getText().toString().trim();
|
||||||
if (phone.isEmpty() || !phone.matches("[0-9\\-\\s\\(\\)\\+]+")) {
|
// Android built in phone validation pattern
|
||||||
|
if (phone.isEmpty() || !android.util.Patterns.PHONE.matcher(phone).matches()) {
|
||||||
field.setError("Enter a valid phone number");
|
field.setError("Enter a valid phone number");
|
||||||
field.requestFocus();
|
field.requestFocus();
|
||||||
return false;
|
return false;
|
||||||
@@ -94,4 +95,3 @@ public class InputValidator {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package com.example.petstoremobile.utils;
|
||||||
|
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
import com.example.petstoremobile.R;
|
||||||
|
import com.example.petstoremobile.activities.HomeActivity;
|
||||||
|
|
||||||
|
// Helper class to show notifications when called
|
||||||
|
public class NotificationHelper {
|
||||||
|
private static final String CHANNEL_ID = "chat_notifications";
|
||||||
|
private static final String CHANNEL_NAME = "Chat Notifications";
|
||||||
|
private static final String CHANNEL_DESC = "Notifications for new conversations";
|
||||||
|
private static final int NOTIFICATION_ID = 1;
|
||||||
|
|
||||||
|
// a function to show a notification
|
||||||
|
public static void showNotification(Context context, String title, String message) {
|
||||||
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
|
//check if the device is running on Oreo or higher so we can set up a notification channel
|
||||||
|
// for these devices
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
// Create a notification channel
|
||||||
|
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||||
|
channel.setDescription(CHANNEL_DESC);
|
||||||
|
notificationManager.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
//make the notification navigate the chat if it is clicked
|
||||||
|
Intent intent = new Intent(context, HomeActivity.class);
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||||
|
intent.putExtra("navigate_to", "chat");
|
||||||
|
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||||
|
|
||||||
|
//build the notification for display
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(message)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(pendingIntent);
|
||||||
|
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -83,7 +83,7 @@
|
|||||||
android:id="@+id/tvProfileEmail"
|
android:id="@+id/tvProfileEmail"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="example@email.com"
|
android:text="No email loaded"
|
||||||
android:textColor="@color/text_dark"
|
android:textColor="@color/text_dark"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
android:id="@+id/tvProfilePhone"
|
android:id="@+id/tvProfilePhone"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="(123) 123-1234"
|
android:text="No phone loaded"
|
||||||
android:textColor="@color/text_dark"
|
android:textColor="@color/text_dark"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
android:id="@+id/tvProfileRole"
|
android:id="@+id/tvProfileRole"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Manager"
|
android:text="No role loaded"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textColor="@color/accent_coral"/>
|
android:textColor="@color/accent_coral"/>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user