diff --git a/app/src/main/java/com/example/partymaker/PartyApplication.java b/app/src/main/java/com/example/partymaker/PartyApplication.java index e164ba1f..c4fdc026 100644 --- a/app/src/main/java/com/example/partymaker/PartyApplication.java +++ b/app/src/main/java/com/example/partymaker/PartyApplication.java @@ -1,7 +1,12 @@ package com.example.partymaker; +import android.app.Activity; import android.app.Application; +import android.os.Bundle; import android.util.Log; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import com.example.partymaker.BuildConfig; import com.example.partymaker.data.api.ConnectivityManager; import com.example.partymaker.data.api.FirebaseServerClient; import com.example.partymaker.data.api.NetworkManager; @@ -16,10 +21,12 @@ /** Application class for PartyMaker. Initializes repositories and other app-wide components. */ public class PartyApplication extends Application { private static final String TAG = "PartyApplication"; + private static PartyApplication instance; @Override public void onCreate() { super.onCreate(); + instance = this; // Apply saved theme preference ServerSettingsActivity.applyThemeFromPreferences(this); @@ -62,6 +69,14 @@ public void onCreate() { // Initialize repositories initializeRepositories(); + // Initialize memory management + MemoryManager.getInstance(); + + // Setup memory monitoring in debug builds + if (BuildConfig.DEBUG) { + setupMemoryMonitoring(); + } + // Log memory info Log.d(TAG, "Initial memory usage: " + MemoryManager.getDetailedMemoryInfo()); @@ -107,13 +122,55 @@ public void onTerminate() { Log.d(TAG, "Application terminated"); } + /** + * Gets the singleton instance of the application. + */ + public static PartyApplication getInstance() { + return instance; + } + + /** + * Sets up memory monitoring for debug builds. + */ + private void setupMemoryMonitoring() { + // LeakCanary is automatically initialized + + // Additional memory monitoring + registerActivityLifecycleCallbacks(new ActivityLifecycleCallbacks() { + @Override + public void onActivityCreated(@NonNull Activity activity, @Nullable Bundle savedInstanceState) { + Log.d("MemoryMonitor", "Activity created: " + activity.getClass().getSimpleName()); + MemoryManager.getInstance().logMemoryStats(); + } + + @Override + public void onActivityDestroyed(@NonNull Activity activity) { + Log.d("MemoryMonitor", "Activity destroyed: " + activity.getClass().getSimpleName()); + MemoryManager.getInstance().logMemoryStats(); + } + + // Other lifecycle methods... + @Override public void onActivityStarted(@NonNull Activity activity) {} + @Override public void onActivityResumed(@NonNull Activity activity) {} + @Override public void onActivityPaused(@NonNull Activity activity) {} + @Override public void onActivityStopped(@NonNull Activity activity) {} + @Override public void onActivitySaveInstanceState(@NonNull Activity activity, @NonNull Bundle outState) {} + }); + } + @Override public void onLowMemory() { super.onLowMemory(); - // Perform memory cleanup - MemoryManager.performMemoryCleanup(this); + // Perform memory cleanup using enhanced memory manager + MemoryManager.getInstance().emergencyCleanup(); Log.d(TAG, "Low memory cleanup performed"); } + + @Override + public void onTrimMemory(int level) { + super.onTrimMemory(level); + MemoryManager.getInstance().emergencyCleanup(); + } } diff --git a/app/src/main/java/com/example/partymaker/data/model/ChatMessage.java b/app/src/main/java/com/example/partymaker/data/model/ChatMessage.java index 17539752..2688f085 100644 --- a/app/src/main/java/com/example/partymaker/data/model/ChatMessage.java +++ b/app/src/main/java/com/example/partymaker/data/model/ChatMessage.java @@ -4,6 +4,7 @@ import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.Ignore; +import androidx.room.Index; import androidx.room.PrimaryKey; import java.util.HashMap; import java.util.Locale; @@ -13,7 +14,14 @@ * Represents a chat message in the PartyMaker application. This class is annotated for Room * database storage. */ -@Entity(tableName = "chat_messages") +@Entity(tableName = "chat_messages", + indices = { + @Index(value = "groupKey", name = "idx_message_group"), + @Index(value = {"groupKey", "timestamp"}, name = "idx_message_group_time"), + @Index(value = "senderKey", name = "idx_message_sender"), + @Index(value = "timestamp", name = "idx_message_time"), + @Index(value = {"groupKey", "encrypted"}, name = "idx_message_group_encrypted") + }) public class ChatMessage { /** The unique key for the message. */ @PrimaryKey diff --git a/app/src/main/java/com/example/partymaker/data/model/Group.java b/app/src/main/java/com/example/partymaker/data/model/Group.java index 791bb658..cb164e3a 100644 --- a/app/src/main/java/com/example/partymaker/data/model/Group.java +++ b/app/src/main/java/com/example/partymaker/data/model/Group.java @@ -4,6 +4,7 @@ import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.Ignore; +import androidx.room.Index; import androidx.room.PrimaryKey; import com.google.gson.annotations.SerializedName; import java.util.HashMap; @@ -12,7 +13,15 @@ * Represents a group (party) in the PartyMaker application. This class is annotated for Room * database storage. */ -@Entity(tableName = "groups") +@Entity(tableName = "groups", + indices = { + @Index(value = "created_at", name = "idx_group_created"), + @Index(value = "admin_key", name = "idx_group_admin"), + @Index(value = {"group_type", "created_at"}, name = "idx_group_type_created"), + @Index(value = "admin_key", name = "idx_group_user"), + @Index(value = {"admin_key", "group_type"}, name = "idx_group_user_type"), + @Index(value = "group_name", name = "idx_group_name") // For search + }) public class Group { /** Constants for group types */ diff --git a/app/src/main/java/com/example/partymaker/data/model/User.java b/app/src/main/java/com/example/partymaker/data/model/User.java index 10e1e6a2..86f1bb62 100644 --- a/app/src/main/java/com/example/partymaker/data/model/User.java +++ b/app/src/main/java/com/example/partymaker/data/model/User.java @@ -4,6 +4,7 @@ import androidx.room.ColumnInfo; import androidx.room.Entity; import androidx.room.Ignore; +import androidx.room.Index; import androidx.room.PrimaryKey; import com.google.firebase.database.PropertyName; import java.util.HashMap; @@ -14,7 +15,13 @@ * Represents a user in the PartyMaker application. This class is annotated for Room database * storage. */ -@Entity(tableName = "users") +@Entity(tableName = "users", + indices = { + @Index(value = "email", name = "idx_user_email", unique = true), + @Index(value = "username", name = "idx_user_name"), + @Index(value = "created_at", name = "idx_user_created"), + @Index(value = {"username", "email"}, name = "idx_user_search") + }) public class User { /** The user's unique key. */ @PrimaryKey diff --git a/app/src/main/java/com/example/partymaker/ui/adapters/ChatRecyclerAdapter.java b/app/src/main/java/com/example/partymaker/ui/adapters/ChatRecyclerAdapter.java index 10499b6c..91b54f35 100644 --- a/app/src/main/java/com/example/partymaker/ui/adapters/ChatRecyclerAdapter.java +++ b/app/src/main/java/com/example/partymaker/ui/adapters/ChatRecyclerAdapter.java @@ -6,6 +6,7 @@ import android.view.ViewGroup; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; import androidx.recyclerview.widget.RecyclerView; import com.example.partymaker.R; import com.example.partymaker.data.model.ChatMessage; @@ -15,16 +16,16 @@ import java.util.Date; import java.util.List; import java.util.Locale; +import java.util.Objects; public class ChatRecyclerAdapter extends RecyclerView.Adapter { private final Context context; - private List messages; + private final List currentMessages = new ArrayList<>(); private String currentUserKey; public ChatRecyclerAdapter(Context context) { this.context = context; - this.messages = new ArrayList<>(); try { currentUserKey = AuthenticationManager.getCurrentUserKey(context); } catch (Exception e) { @@ -32,19 +33,33 @@ public ChatRecyclerAdapter(Context context) { } } + public void updateMessages(List newMessages) { + List messages = newMessages != null ? newMessages : new ArrayList<>(); + DiffUtil.DiffResult result = DiffUtil.calculateDiff(new MessageDiffCallback(currentMessages, messages)); + currentMessages.clear(); + currentMessages.addAll(messages); + result.dispatchUpdatesTo(this); + } + + /** + * @deprecated Use updateMessages() instead for better performance + */ + @Deprecated public void setMessages(List messages) { - this.messages = messages != null ? messages : new ArrayList<>(); - notifyDataSetChanged(); + updateMessages(messages); } public void addMessage(ChatMessage message) { - messages.add(message); - notifyItemInserted(messages.size() - 1); + currentMessages.add(message); + notifyItemInserted(currentMessages.size() - 1); } public void clear() { - messages.clear(); - notifyDataSetChanged(); + int size = currentMessages.size(); + if (size > 0) { + currentMessages.clear(); + notifyItemRangeRemoved(0, size); + } } @NonNull @@ -56,7 +71,7 @@ public MessageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewT @Override public void onBindViewHolder(@NonNull MessageViewHolder holder, int position) { - ChatMessage message = messages.get(position); + ChatMessage message = currentMessages.get(position); // Get message text String messageText = message.getMessage(); @@ -107,17 +122,77 @@ public void onBindViewHolder(@NonNull MessageViewHolder holder, int position) { @Override public int getItemCount() { - return messages.size(); + return currentMessages.size(); + } + + @Override + public void onViewRecycled(@NonNull MessageViewHolder holder) { + super.onViewRecycled(holder); + // Clear text views to prevent showing old data during recycling + holder.clear(); + } + + /** + * DiffUtil callback for efficiently comparing chat messages + */ + private static class MessageDiffCallback extends DiffUtil.Callback { + private final List oldList; + private final List newList; + + MessageDiffCallback(List oldList, List newList) { + this.oldList = oldList; + this.newList = newList; + } + + @Override + public int getOldListSize() { + return oldList.size(); + } + + @Override + public int getNewListSize() { + return newList.size(); + } + + @Override + public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) { + ChatMessage oldMessage = oldList.get(oldItemPosition); + ChatMessage newMessage = newList.get(newItemPosition); + + // Compare message IDs first + String oldId = oldMessage.getMessageKey(); + String newId = newMessage.getMessageKey(); + if (oldId != null && newId != null) { + return oldId.equals(newId); + } + + // Fallback: compare timestamp and sender + return oldMessage.getTimestamp() == newMessage.getTimestamp() && + Objects.equals(oldMessage.getSenderKey(), newMessage.getSenderKey()); + } + + @Override + public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) { + ChatMessage oldMessage = oldList.get(oldItemPosition); + ChatMessage newMessage = newList.get(newItemPosition); + + return Objects.equals(oldMessage.getMessage(), newMessage.getMessage()) && + Objects.equals(oldMessage.getMessageText(), newMessage.getMessageText()) && + Objects.equals(oldMessage.getSenderName(), newMessage.getSenderName()) && + Objects.equals(oldMessage.getMessageUser(), newMessage.getMessageUser()) && + Objects.equals(oldMessage.getMessageTime(), newMessage.getMessageTime()) && + oldMessage.getTimestamp() == newMessage.getTimestamp(); + } } static class MessageViewHolder extends RecyclerView.ViewHolder { - View sentMessageLayout; - View receivedMessageLayout; - TextView tvSentMessage; - TextView tvSentTime; - TextView tvReceivedMessage; - TextView tvSenderName; - TextView tvReceivedTime; + final View sentMessageLayout; + final View receivedMessageLayout; + final TextView tvSentMessage; + final TextView tvSentTime; + final TextView tvReceivedMessage; + final TextView tvSenderName; + final TextView tvReceivedTime; MessageViewHolder(@NonNull View itemView) { super(itemView); @@ -129,5 +204,18 @@ static class MessageViewHolder extends RecyclerView.ViewHolder { tvSenderName = itemView.findViewById(R.id.tvSenderName); tvReceivedTime = itemView.findViewById(R.id.tvReceivedTime); } + + /** + * Clears all text views to prevent showing old data during recycling + */ + void clear() { + tvSentMessage.setText(""); + tvSentTime.setText(""); + tvReceivedMessage.setText(""); + tvSenderName.setText(""); + tvReceivedTime.setText(""); + sentMessageLayout.setVisibility(View.GONE); + receivedMessageLayout.setVisibility(View.GONE); + } } } \ No newline at end of file diff --git a/app/src/main/java/com/example/partymaker/ui/features/auth/IntroActivity.java b/app/src/main/java/com/example/partymaker/ui/features/auth/IntroActivity.java index 510d8389..2907de0b 100644 --- a/app/src/main/java/com/example/partymaker/ui/features/auth/IntroActivity.java +++ b/app/src/main/java/com/example/partymaker/ui/features/auth/IntroActivity.java @@ -129,7 +129,7 @@ public void btnSkipClick(View v) { } public void btnNextClick(View v) { - int current = getItem(PAGE_OFFSET); + int current = getItem(); if (current < SLIDE_LAYOUTS.length) { viewPager.setCurrentItem(current); } else { @@ -180,11 +180,10 @@ private void setActiveDot(TextView[] dots, int currentPage) { /** * Returns the next item index for the ViewPager. * - * @param i the offset * @return the next item index */ - private int getItem(int i) { - return viewPager.getCurrentItem() + i; + private int getItem() { + return viewPager.getCurrentItem() + IntroActivity.PAGE_OFFSET; } /** Launches the LoginActivity and finishes the intro. */ diff --git a/app/src/main/java/com/example/partymaker/ui/features/groups/creation/CreateGroupActivity.java b/app/src/main/java/com/example/partymaker/ui/features/groups/creation/CreateGroupActivity.java index 06faef81..4692a464 100644 --- a/app/src/main/java/com/example/partymaker/ui/features/groups/creation/CreateGroupActivity.java +++ b/app/src/main/java/com/example/partymaker/ui/features/groups/creation/CreateGroupActivity.java @@ -34,6 +34,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.ActionBar; import androidx.appcompat.app.AppCompatActivity; +import com.example.partymaker.ui.base.BaseActivity; import androidx.core.content.ContextCompat; import com.airbnb.lottie.LottieAnimationView; import com.example.partymaker.R; @@ -78,7 +79,7 @@ import java.util.Map; import java.util.Objects; -public class CreateGroupActivity extends AppCompatActivity implements OnMapReadyCallback { +public class CreateGroupActivity extends BaseActivity implements OnMapReadyCallback { // Constants private static final int IMAGE_PICKER_REQUEST_CODE = 100; private static final int INSTRUCTION_DELAY_MS = 3000; @@ -1013,4 +1014,13 @@ public static class GroupType { public static final int PUBLIC = 0; public static final int PRIVATE = 1; } + + @Override + protected void clearActivityReferences() { + // Clear any activity-specific references + if (map != null) { + map.clear(); + map = null; + } + } } diff --git a/app/src/main/java/com/example/partymaker/ui/features/groups/main/PartyMainActivity.java b/app/src/main/java/com/example/partymaker/ui/features/groups/main/PartyMainActivity.java index a680ec7e..94135a8b 100644 --- a/app/src/main/java/com/example/partymaker/ui/features/groups/main/PartyMainActivity.java +++ b/app/src/main/java/com/example/partymaker/ui/features/groups/main/PartyMainActivity.java @@ -23,6 +23,7 @@ import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; import androidx.cardview.widget.CardView; +import com.example.partymaker.ui.base.BaseActivity; import com.example.partymaker.R; import com.example.partymaker.data.api.FirebaseServerClient; import com.example.partymaker.data.model.ChatMessage; @@ -45,7 +46,7 @@ import java.util.HashMap; import java.util.Map; -public class PartyMainActivity extends AppCompatActivity { +public class PartyMainActivity extends BaseActivity { private static final String TAG = "PartyMainActivity"; // Animation durations @@ -1796,4 +1797,9 @@ public void onBackPressed() { startActivity(intent); finish(); } + + @Override + protected void clearActivityReferences() { + // Clear any activity-specific references + } } diff --git a/app/src/main/java/com/example/partymaker/utils/auth/AuthenticationManager.java b/app/src/main/java/com/example/partymaker/utils/auth/AuthenticationManager.java index a817fe78..58d620a4 100644 --- a/app/src/main/java/com/example/partymaker/utils/auth/AuthenticationManager.java +++ b/app/src/main/java/com/example/partymaker/utils/auth/AuthenticationManager.java @@ -154,9 +154,8 @@ public static void logout(Context context) { * Clears all authentication data * * @param context Application context - * @return true if successful, false otherwise */ - public static boolean clearAuthData(Context context) { + public static void clearAuthData(Context context) { boolean firebaseSignOutSuccess = false; boolean sharedPrefsSuccess = false; @@ -188,7 +187,6 @@ public static boolean clearAuthData(Context context) { Log.e(TAG, "Error clearing SharedPreferences auth data", e); } - return firebaseSignOutSuccess || sharedPrefsSuccess; } /** diff --git a/app/src/main/java/com/example/partymaker/utils/media/FileManager.java b/app/src/main/java/com/example/partymaker/utils/media/FileManager.java index f30bd98e..8b211be1 100644 --- a/app/src/main/java/com/example/partymaker/utils/media/FileManager.java +++ b/app/src/main/java/com/example/partymaker/utils/media/FileManager.java @@ -76,7 +76,7 @@ public static Uri getUriForFile(Context context, File file) { * @param callback Callback for the operation */ public static void saveBitmapToFile(Bitmap bitmap, File file, FileOperationCallback callback) { - ThreadUtils.runInBackground( + ThreadUtils.executeImageTask( () -> { try { FileOutputStream outputStream = new FileOutputStream(file); @@ -103,7 +103,7 @@ public static void saveBitmapToFile(Bitmap bitmap, File file, FileOperationCallb */ public static void copyFile( Context context, Uri sourceUri, File destFile, FileOperationCallback callback) { - ThreadUtils.runInBackground( + ThreadUtils.executeImageTask( () -> { try { InputStream inputStream = context.getContentResolver().openInputStream(sourceUri); diff --git a/app/src/main/java/com/example/partymaker/utils/ui/feedback/UserFeedbackManager.java b/app/src/main/java/com/example/partymaker/utils/ui/feedback/UserFeedbackManager.java index 9326e785..0279cf32 100644 --- a/app/src/main/java/com/example/partymaker/utils/ui/feedback/UserFeedbackManager.java +++ b/app/src/main/java/com/example/partymaker/utils/ui/feedback/UserFeedbackManager.java @@ -1,5 +1,6 @@ package com.example.partymaker.utils.ui.feedback; +import android.app.Activity; import android.content.Context; import android.view.View; import android.widget.Toast; @@ -79,6 +80,14 @@ public static void showDestructiveConfirmationDialog( /** Shows an information dialog with single OK button */ public static void showInfoDialog( @NonNull Context context, @NonNull String title, @NonNull String message) { + // Check if the context is still valid before showing dialog + if (context instanceof Activity) { + Activity activity = (Activity) context; + if (activity.isFinishing() || activity.isDestroyed()) { + return; // Don't show dialog if activity is finishing or destroyed + } + } + new MaterialAlertDialogBuilder(context) .setTitle(title) .setMessage(message) @@ -93,6 +102,14 @@ public static void showErrorDialog( @NonNull String title, @NonNull String message, @NonNull Runnable onRetry) { + // Check if the context is still valid before showing dialog + if (context instanceof Activity) { + Activity activity = (Activity) context; + if (activity.isFinishing() || activity.isDestroyed()) { + return; // Don't show dialog if activity is finishing or destroyed + } + } + new MaterialAlertDialogBuilder(context) .setTitle(title) .setMessage(message) diff --git a/app/src/main/java/com/example/partymaker/viewmodel/auth/AuthViewModel.java b/app/src/main/java/com/example/partymaker/viewmodel/auth/AuthViewModel.java index 4f97037a..96402c9c 100644 --- a/app/src/main/java/com/example/partymaker/viewmodel/auth/AuthViewModel.java +++ b/app/src/main/java/com/example/partymaker/viewmodel/auth/AuthViewModel.java @@ -195,9 +195,9 @@ public void loginWithEmail(@NonNull String email, @NonNull String password) { () -> { setLoading(false); if (task.isSuccessful()) { - handleSuccessfulLogin("Email login successful"); + handleSuccessfulLogin(); } else { - handleAuthError("Email login failed", task.getException()); + handleAuthError(task.getException()); } })); }); @@ -467,16 +467,14 @@ public boolean isValidUsername(String username) { /** * Handles successful login operations. - * - * @param message Success message to display */ - private void handleSuccessfulLogin(String message) { + private void handleSuccessfulLogin() { FirebaseUser user = auth.getCurrentUser(); if (user != null) { currentUser.setValue(user); isAuthenticated.setValue(true); - setSuccess(message); - Log.d(TAG, "Login successful: " + message); + setSuccess("Email login successful"); + Log.d(TAG, "Login successful: " + "Email login successful"); } else { setError("Authentication error: User not found"); Log.e(TAG, "Login success but user is null"); @@ -486,19 +484,18 @@ private void handleSuccessfulLogin(String message) { /** * Handles authentication errors with proper error messaging. * - * @param baseMessage Base error message * @param exception The exception that occurred */ - private void handleAuthError(String baseMessage, Exception exception) { - String errorMessage = baseMessage; + private void handleAuthError(Exception exception) { + String errorMessage = "Email login failed"; if (exception != null) { String exceptionMessage = exception.getMessage(); if (exceptionMessage != null && !exceptionMessage.isEmpty()) { errorMessage = exceptionMessage; } - Log.e(TAG, baseMessage, exception); + Log.e(TAG, "Email login failed", exception); } else { - Log.e(TAG, baseMessage); + Log.e(TAG, "Email login failed"); } setError(errorMessage); }