Azure deployment setup #297

Closed
RecentRunner wants to merge 429 commits from azure-deploy into main
7 changed files with 278 additions and 56 deletions
Showing only changes of commit 5d8d37dee4 - Show all commits

View File

@@ -88,7 +88,13 @@ public class HomeActivity extends AppCompatActivity {
// 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());
ChatFragment chatFragment = new ChatFragment();
if (intent.hasExtra("conversation_id")) {
Bundle args = new Bundle();
args.putLong("conversation_id", intent.getLongExtra("conversation_id", -1));
chatFragment.setArguments(args);
}
loadFragment(chatFragment);
bottomNav.setSelectedItemId(R.id.nav_chat);
} else {
loadFragment(new ListFragment());

View File

@@ -25,6 +25,7 @@ import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.dtos.SendMessageRequest;
import com.example.petstoremobile.models.Chat;
import com.example.petstoremobile.models.Message;
import com.example.petstoremobile.services.ChatNotificationService;
import com.example.petstoremobile.websocket.StompChatManager;
import java.util.*;
import java.util.stream.Collectors;
@@ -40,6 +41,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
private RecyclerView rvChatList, rvMessages;
private EditText etMessage;
private Button btnSend;
private TextView tvChatTitle;
// Adapters
private ChatAdapter chatAdapter;
@@ -75,6 +77,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
rvMessages = view.findViewById(R.id.rvMessages);
etMessage = view.findViewById(R.id.etMessage);
btnSend = view.findViewById(R.id.btnSend);
tvChatTitle = view.findViewById(R.id.tvChatTitle);
ImageButton hamburger = view.findViewById(R.id.btnHamburger);
hamburger.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START));
@@ -121,6 +124,10 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
Log.e(TAG, "No token found");
}
if (getArguments() != null && getArguments().containsKey("conversation_id")) {
activeConversationId = getArguments().getLong("conversation_id");
}
loadCustomers();
}
@@ -165,7 +172,21 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
.collect(Collectors.toList());
chatList.addAll(loaded);
chatAdapter.notifyDataSetChanged();
if (activeConversationId == null) {
if (activeConversationId != null) {
setConversationActive(true);
// Update title to customer name of active conversation
for (Chat chat : chatList) {
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
tvChatTitle.setText(chat.getCustomerName());
break;
}
}
if (stompChatManager != null) {
stompChatManager.subscribeToConversation(activeConversationId);
}
loadMessageHistory(activeConversationId);
} else {
messageList.clear();
messageAdapter.notifyDataSetChanged();
setConversationActive(false);
@@ -186,6 +207,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
public void onChatClick(Chat chat) {
activeConversationId = Long.parseLong(chat.getChatId());
setConversationActive(true);
tvChatTitle.setText(chat.getCustomerName());
drawerLayout.closeDrawer(GravityCompat.START);
if (stompChatManager != null) {
@@ -305,6 +327,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
setConversationActive(true);
tvChatTitle.setText(name);
}
}
@@ -386,6 +409,8 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
etMessage.setEnabled(active);
if (!active) {
activeConversationId = null;
ChatNotificationService.activeConversationIdInUi = null;
if (tvChatTitle != null) tvChatTitle.setText("Customer Chat");
if (stompChatManager != null) {
stompChatManager.clearConversationSubscription();
}
@@ -395,6 +420,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
etMessage.setHint("Select a chat to start messaging");
} else {
etMessage.setHint("Type a message...");
ChatNotificationService.activeConversationIdInUi = activeConversationId;
}
}
@@ -402,6 +428,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
@Override
public void onDestroyView() {
super.onDestroyView();
ChatNotificationService.activeConversationIdInUi = null;
if (stompChatManager != null) stompChatManager.disconnect();
}
}

View File

@@ -13,8 +13,11 @@ import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ImageButton;
import android.widget.Spinner;
import android.widget.Toast;
import com.example.petstoremobile.R;
@@ -43,6 +46,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
private PetApi api;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText etSearch;
private Spinner spinnerStatus;
//load pet view
@Override
@@ -57,6 +61,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
setupRecyclerView(view);
setupSearch(view);
setupStatusFilter(view);
setupSwipeRefresh(view);
loadPetData();
@@ -82,24 +87,48 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
etSearch.addTextChangedListener(new TextWatcher() {
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
filterPets(s.toString());
filterPets();
}
@Override public void afterTextChanged(Editable s) {}
});
}
private void filterPets(String query) {
//Setup the status filter spinner
private void setupStatusFilter(View view) {
spinnerStatus = view.findViewById(R.id.spinnerStatus);
String[] statuses = {"All Statuses", "Available", "Adopted"};
ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses);
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
spinnerStatus.setAdapter(adapter);
spinnerStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
filterPets();
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
}
// Helper function to filter pets based on search and status filter
private void filterPets() {
String query = etSearch.getText().toString().toLowerCase();
String selectedStatus = spinnerStatus.getSelectedItem().toString();
filteredList.clear();
if (query.isEmpty()) {
filteredList.addAll(petList);
} else {
String lower = query.toLowerCase();
for (PetDTO p : petList) {
if (p.getPetName().toLowerCase().contains(lower)
|| p.getPetSpecies().toLowerCase().contains(lower)
|| p.getPetBreed().toLowerCase().contains(lower)) {
filteredList.add(p);
}
for (PetDTO p : petList) {
boolean matchesSearch = query.isEmpty() ||
p.getPetName().toLowerCase().contains(query) ||
p.getPetSpecies().toLowerCase().contains(query) ||
p.getPetBreed().toLowerCase().contains(query);
boolean matchesStatus = selectedStatus.equals("All Statuses") ||
p.getPetStatus().equalsIgnoreCase(selectedStatus);
if (matchesSearch && matchesStatus) {
filteredList.add(p);
}
}
adapter.notifyDataSetChanged();
@@ -173,7 +202,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
if (response.isSuccessful() && response.body() != null) {
petList.clear();
petList.addAll(response.body().getContent());
filterPets(etSearch.getText().toString());
filterPets();
} else {
Log.e("onResponse: ", response.message());

View File

@@ -4,19 +4,38 @@ import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.example.petstoremobile.api.ChatApi;
import com.example.petstoremobile.api.CustomerApi;
import com.example.petstoremobile.api.RetrofitClient;
import com.example.petstoremobile.api.auth.TokenManager;
import com.example.petstoremobile.dtos.ConversationDTO;
import com.example.petstoremobile.dtos.CustomerDTO;
import com.example.petstoremobile.dtos.MessageDTO;
import com.example.petstoremobile.dtos.PageResponse;
import com.example.petstoremobile.utils.NotificationHelper;
import com.example.petstoremobile.websocket.StompChatManager;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
// Service to receive notifications when a new conversation is created
public class ChatNotificationService extends Service {
private static final String TAG = "ChatNotificationService";
public static Long activeConversationIdInUi = null;
private StompChatManager stompChatManager;
private final Set<Long> knownConversationIds = new HashSet<>();
private final Map<Long, Long> conversationToCustomerId = new HashMap<>();
private final Map<Long, String> customerIdToName = new HashMap<>();
private Long currentUserId;
//When the service starts, connect to the websocket
@Override
@@ -32,45 +51,164 @@ public class ChatNotificationService extends Service {
TokenManager tm = TokenManager.getInstance(this);
String token = tm.getToken();
String role = tm.getRole();
currentUserId = tm.getUserId();
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"
);
//load customers to have names associated with customer ids
CustomerApi customerApi = RetrofitClient.getCustomerApi(this);
customerApi.getAllCustomers(0, 1000).enqueue(new Callback<PageResponse<CustomerDTO>>() {
@Override
public void onResponse(@NonNull Call<PageResponse<CustomerDTO>> call, @NonNull Response<PageResponse<CustomerDTO>> response) {
if (response.isSuccessful() && response.body() != null) {
for (CustomerDTO customer : response.body().getContent()) {
customerIdToName.put(customer.getCustomerId(), customer.getFullName());
}
}
}
});
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");
loadConversationsAndStartStomp(token, role);
}
@Override
public void onSocketClosed() { Log.d(TAG, "WebSocket closed in service"); }
@Override
public void onSocketError() { Log.e(TAG, "WebSocket error in service"); }
public void onFailure(@NonNull Call<PageResponse<CustomerDTO>> call, @NonNull Throwable t) {
Log.e(TAG, "Failed to load customers", t);
loadConversationsAndStartStomp(token, role);
}
});
stompChatManager.connect();
}
}
private void loadConversationsAndStartStomp(String token, String role) {
// Fetch existing conversations
ChatApi chatApi = RetrofitClient.getChatApi(this);
chatApi.getAllConversations().enqueue(new Callback<List<ConversationDTO>>() {
@Override
public void onResponse(@NonNull Call<List<ConversationDTO>> call, @NonNull Response<List<ConversationDTO>> response) {
if (response.isSuccessful() && response.body() != null) {
for (ConversationDTO conversation : response.body()) {
if (conversation.getId() != null) {
knownConversationIds.add(conversation.getId());
conversationToCustomerId.put(conversation.getId(), conversation.getCustomerId());
// subscribe to existing conversations to get message notifications
if (stompChatManager != null) {
stompChatManager.subscribeToConversation(conversation.getId());
}
}
}
Log.d(TAG, "Loaded " + knownConversationIds.size() + " existing conversations");
}
startStomp(token, role);
}
@Override
public void onFailure(@NonNull Call<List<ConversationDTO>> call, @NonNull Throwable t) {
Log.e(TAG, "Failed to load existing conversations", t);
//tries to connect if loading fails
startStomp(token, role);
}
});
}
private void startStomp(String token, String role) {
if (stompChatManager != null) return;
stompChatManager = new StompChatManager(token, role);
// Listen for messages in existing conversations
stompChatManager.setMessageListener(message -> {
if (message != null && !message.getSenderId().equals(currentUserId)) {
// Check if this conversation is already active in the view
//if it is then don't make a notification for this chat
if (activeConversationIdInUi != null && activeConversationIdInUi.equals(message.getConversationId())) {
Log.d(TAG, "Disable notification for active conversation: " + message.getConversationId());
return;
}
String title = "New Message";
Long customerId = conversationToCustomerId.get(message.getConversationId());
if (customerId != null && customerIdToName.containsKey(customerId)) {
//append the customer name to the title of the notification
title = "New message from " + customerIdToName.get(customerId);
}
NotificationHelper.showNotification(
getApplicationContext(),
title,
message.getContent(),
message.getConversationId()
);
}
});
//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());
conversationToCustomerId.put(conversation.getId(), conversation.getCustomerId());
// Subscribe to the new conversation's messages
stompChatManager.subscribeToConversation(conversation.getId());
String title = "New Support Request";
if (customerIdToName.containsKey(conversation.getCustomerId())) {
//append the customer name to the title of the notification
title = "New Support Request from " + customerIdToName.get(conversation.getCustomerId());
} else {
// Try to fetch customer name for the new request
fetchCustomerName(conversation.getCustomerId());
}
//Display a notification
NotificationHelper.showNotification(
getApplicationContext(),
title,
"A customer is requesting assistance",
conversation.getId()
);
}
}
});
// Subscribe to existing conversations if they were already loaded
for (Long id : knownConversationIds) {
stompChatManager.subscribeToConversation(id);
}
stompChatManager.setConnectionListener(new StompChatManager.ConnectionListener() {
@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();
}
// Helper function to fetch customer name for a conversation
private void fetchCustomerName(Long customerId) {
CustomerApi customerApi = RetrofitClient.getCustomerApi(this);
customerApi.getCustomerById(customerId).enqueue(new Callback<CustomerDTO>() {
@Override
public void onResponse(@NonNull Call<CustomerDTO> call, @NonNull Response<CustomerDTO> response) {
if (response.isSuccessful() && response.body() != null) {
customerIdToName.put(customerId, response.body().getFullName());
}
}
@Override
public void onFailure(@NonNull Call<CustomerDTO> call, @NonNull Throwable t) {
Log.e(TAG, "Failed to fetch customer name", t);
}
});
}
//When the service is destroyed, disconnect from the websocket
@Override
public void onDestroy() {

View File

@@ -18,7 +18,7 @@ public class NotificationHelper {
private static final int NOTIFICATION_ID = 1;
// a function to show a notification
public static void showNotification(Context context, String title, String message) {
public static void showNotification(Context context, String title, String message, Long conversationId) {
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
@@ -34,6 +34,9 @@ public class NotificationHelper {
Intent intent = new Intent(context, HomeActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
intent.putExtra("navigate_to", "chat");
if (conversationId != null) {
intent.putExtra("conversation_id", conversationId);
}
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);

View File

@@ -28,12 +28,15 @@
android:contentDescription="Open menu"/>
<TextView
android:id="@+id/tvChatTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Customer Chat"
android:textColor="@color/white"
android:textSize="20sp"
android:textStyle="bold"/>
android:textStyle="bold"
android:paddingStart="8dp"
android:paddingEnd="8dp"/>
</LinearLayout>

View File

@@ -38,18 +38,34 @@
</LinearLayout>
<EditText
android:id="@+id/etSearchPet"
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:hint="Search by name, species or breed..."
android:inputType="text"
android:drawableStart="@android:drawable/ic_menu_search"
android:drawablePadding="8dp"
android:background="@android:color/white"
android:padding="12dp"
android:textColor="@color/text_dark"/>
android:orientation="horizontal"
android:padding="8dp"
android:gravity="center_vertical">
<EditText
android:id="@+id/etSearchPet"
android:layout_width="0dp"
android:layout_height="48dp"
android:layout_weight="1"
android:hint="Search..."
android:inputType="text"
android:drawableStart="@android:drawable/ic_menu_search"
android:drawablePadding="8dp"
android:background="@android:color/white"
android:padding="12dp"
android:textColor="@color/text_dark"/>
<Spinner
android:id="@+id/spinnerStatus"
android:layout_width="140dp"
android:layout_height="48dp"
android:layout_marginStart="8dp"
android:background="@android:color/white"
android:padding="10dp"/>
</LinearLayout>
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
android:id="@+id/swipeRefreshPet"