From 3baf959062db7056da3bcefcba36b406d43195d1 Mon Sep 17 00:00:00 2001 From: Shihaam Abdul Rahman Date: Thu, 24 Jul 2025 22:33:31 +0500 Subject: [PATCH] rearrange nav menu --- app/src/main/AndroidManifest.xml | 5 + .../sh/sar/gridflow/AddAccountActivity.kt | 219 ++++++++++++++++++ .../java/sh/sar/gridflow/LoginActivity.kt | 25 +- .../main/java/sh/sar/gridflow/MainActivity.kt | 173 +++++++++++++- .../gridflow/ui/accounts/AccountsAdapter.kt | 71 ++++++ .../sh/sar/gridflow/utils/SecureStorage.kt | 168 ++++++++++++-- .../res/color/account_dropdown_background.xml | 5 + .../main/res/color/account_dropdown_text.xml | 5 + .../color/account_dropdown_text_secondary.xml | 5 + .../res/color/account_list_background.xml | 4 + .../res/color/account_list_text_color.xml | 4 + .../main/res/color/nav_header_arrow_tint.xml | 7 + .../main/res/color/nav_header_text_color.xml | 4 + .../drawable/account_circle_background.xml | 5 + app/src/main/res/drawable/ic_check_24.xml | 10 + .../main/res/drawable/ic_expand_more_24.xml | 10 + .../main/res/layout/activity_add_account.xml | 167 +++++++++++++ app/src/main/res/layout/activity_login.xml | 12 +- app/src/main/res/layout/item_account.xml | 48 ++++ app/src/main/res/layout/nav_header_main.xml | 148 +++++++++--- .../main/res/menu/activity_main_drawer.xml | 11 +- 21 files changed, 1031 insertions(+), 75 deletions(-) create mode 100644 app/src/main/java/sh/sar/gridflow/AddAccountActivity.kt create mode 100644 app/src/main/java/sh/sar/gridflow/ui/accounts/AccountsAdapter.kt create mode 100644 app/src/main/res/color/account_dropdown_background.xml create mode 100644 app/src/main/res/color/account_dropdown_text.xml create mode 100644 app/src/main/res/color/account_dropdown_text_secondary.xml create mode 100644 app/src/main/res/color/account_list_background.xml create mode 100644 app/src/main/res/color/account_list_text_color.xml create mode 100644 app/src/main/res/color/nav_header_arrow_tint.xml create mode 100644 app/src/main/res/color/nav_header_text_color.xml create mode 100644 app/src/main/res/drawable/account_circle_background.xml create mode 100644 app/src/main/res/drawable/ic_check_24.xml create mode 100644 app/src/main/res/drawable/ic_expand_more_24.xml create mode 100644 app/src/main/res/layout/activity_add_account.xml create mode 100644 app/src/main/res/layout/item_account.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8ce50c4..fd2922f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -25,6 +25,11 @@ + + // Retry the login with the same credentials + handleSignIn() + } + .setNegativeButton("Cancel") { dialog, _ -> + dialog.dismiss() + } + .setCancelable(true) + .create() + + dialog.show() + } + + private fun handleSignIn() { + val mobileNumber = binding.etMobileNumber.text.toString().trim() + val password = binding.etPassword.text.toString().trim() + + Log.d(TAG, "handleSignIn called with mobile: $mobileNumber") + + if (validateInput(mobileNumber, password)) { + Log.d(TAG, "Input validation passed, calling performLogin") + performLogin(mobileNumber, password) + } else { + Log.d(TAG, "Input validation failed") + } + } + + private fun performLogin(mobile: String, password: String) { + Log.d(TAG, "performLogin called with mobile: $mobile") + + // Check if account already exists + val existingAccounts = secureStorage.getAllAccounts() + if (existingAccounts.any { it.mobile == mobile }) { + Toast.makeText(this, "Account already exists", Toast.LENGTH_SHORT).show() + return + } + + setLoading(true) + + lifecycleScope.launch { + Log.d(TAG, "Starting coroutine for API call") + try { + when (val result = apiService.login(mobile, password)) { + is ApiResult.Success -> { + Log.d(TAG, "Login successful: ${result.data}") + + // Create new account object + val newAccount = Account( + id = mobile, + name = result.data.name, + mobile = mobile, + email = result.data.email, + password = password, + cookie = extractSessionId(result.cookie ?: ""), + userId = result.data.id, + isActive = false // Will be set as active when switched to + ) + + // Save the new account + secureStorage.saveAccount(newAccount) + + setLoading(false) + + // Show success message and finish + Toast.makeText(this@AddAccountActivity, "Account added successfully", Toast.LENGTH_SHORT).show() + + // Return to main activity + val intent = Intent(this@AddAccountActivity, MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP + startActivity(intent) + finish() + } + is ApiResult.Error -> { + Log.d(TAG, "Login failed: ${result.message} (code: ${result.code})") + setLoading(false) + showLoginError(result.message) + } + } + } catch (e: Exception) { + Log.e(TAG, "Exception in performLogin coroutine", e) + setLoading(false) + showLoginError("Login failed: ${e.message}") + } + } + } + + private fun extractSessionId(setCookieHeader: String): String { + // Extract the session ID from Set-Cookie header + return setCookieHeader.substringBefore(";").substringAfter("connect.sid=") + } + + private fun setLoading(isLoading: Boolean) { + binding.btnSignIn.isEnabled = !isLoading + binding.btnSignIn.text = if (isLoading) "Signing in..." else "Sign In" + + // Disable other buttons during loading + binding.btnRegister.isEnabled = !isLoading + binding.btnForgotPassword.isEnabled = !isLoading + + // Show/hide progress indicator + binding.etMobileNumber.isEnabled = !isLoading + binding.etPassword.isEnabled = !isLoading + } + + private fun handleRegister() { + Log.d(TAG, "Register button clicked") + val intent = Intent(this, RegisterActivity::class.java) + startActivity(intent) + } + + private fun handleForgotPassword() { + Log.d(TAG, "Forgot password button clicked") + val intent = Intent(this, ForgotPasswordActivity::class.java) + startActivity(intent) + } + + private fun validateInput(mobileNumber: String, password: String): Boolean { + if (mobileNumber.isEmpty()) { + binding.etMobileNumber.error = "Mobile number is required" + return false + } + + if (password.isEmpty()) { + binding.etPassword.error = "Password is required" + return false + } + + // Client-side password validation - must be at least 8 characters + if (password.length < 8) { + binding.etPassword.error = "Password must be at least 8 characters" + return false + } + + // Basic Maldives mobile number validation (7xxxxxx format) + if (!mobileNumber.matches(Regex("^[79]\\d{6}$"))) { + binding.etMobileNumber.error = "Enter a valid Maldives mobile number" + return false + } + + return true + } +} diff --git a/app/src/main/java/sh/sar/gridflow/LoginActivity.kt b/app/src/main/java/sh/sar/gridflow/LoginActivity.kt index 8e74b09..dd59ff1 100644 --- a/app/src/main/java/sh/sar/gridflow/LoginActivity.kt +++ b/app/src/main/java/sh/sar/gridflow/LoginActivity.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.launch import sh.sar.gridflow.databinding.ActivityLoginBinding import sh.sar.gridflow.network.ApiResult import sh.sar.gridflow.network.FenakaApiService +import sh.sar.gridflow.utils.Account import sh.sar.gridflow.utils.SecureStorage class LoginActivity : AppCompatActivity() { @@ -124,19 +125,21 @@ class LoginActivity : AppCompatActivity() { // Save credentials and user info (if SecureStorage is available) if (this@LoginActivity::secureStorage.isInitialized) { try { - secureStorage.saveCredentials(mobile, password) - secureStorage.saveUserInfo( - result.data.name, - result.data.email, - result.data.id + // Create new account object + val newAccount = Account( + id = mobile, + name = result.data.name, + mobile = mobile, + email = result.data.email, + password = password, + cookie = extractSessionId(result.cookie ?: ""), + userId = result.data.id, + isActive = true ) - // Extract and save cookie - result.cookie?.let { cookie -> - val sessionId = extractSessionId(cookie) - Log.d(TAG, "Extracted session ID: $sessionId") - secureStorage.saveCookie(sessionId) - } + // Save the account and set as active + secureStorage.saveAccount(newAccount) + secureStorage.setActiveAccount(mobile) } catch (e: Exception) { Log.e(TAG, "Failed to save credentials securely", e) } diff --git a/app/src/main/java/sh/sar/gridflow/MainActivity.kt b/app/src/main/java/sh/sar/gridflow/MainActivity.kt index 64feec9..f7de399 100644 --- a/app/src/main/java/sh/sar/gridflow/MainActivity.kt +++ b/app/src/main/java/sh/sar/gridflow/MainActivity.kt @@ -1,10 +1,14 @@ package sh.sar.gridflow +import android.animation.ObjectAnimator import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.Menu import android.view.MenuItem +import android.view.View +import android.widget.ImageView +import android.widget.LinearLayout import android.widget.TextView import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate @@ -14,8 +18,11 @@ import androidx.navigation.ui.AppBarConfiguration import androidx.navigation.ui.navigateUp import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupWithNavController +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import com.google.android.material.navigation.NavigationView import sh.sar.gridflow.databinding.ActivityMainBinding +import sh.sar.gridflow.ui.accounts.AccountsAdapter import sh.sar.gridflow.utils.SecureStorage class MainActivity : AppCompatActivity() { @@ -23,6 +30,14 @@ class MainActivity : AppCompatActivity() { private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var binding: ActivityMainBinding private lateinit var secureStorage: SecureStorage + private lateinit var accountsAdapter: AccountsAdapter + + private lateinit var accountSwitcherArrow: ImageView + private lateinit var accountListSection: LinearLayout + private lateinit var addAccountButton: LinearLayout + private lateinit var accountsRecyclerView: RecyclerView + + private var isAccountListVisible = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -41,6 +56,9 @@ class MainActivity : AppCompatActivity() { val navView: NavigationView = binding.navView val navController = findNavController(R.id.nav_host_fragment_content_main) + // Initialize navigation header components + initializeNavigationHeader(navView) + // Update navigation header with user info updateNavHeader(navView) @@ -48,9 +66,9 @@ class MainActivity : AppCompatActivity() { appBarConfiguration = AppBarConfiguration( setOf( R.id.nav_dashboard, + R.id.nav_bill_history, R.id.nav_subscriptions, R.id.nav_band_rates, - R.id.nav_bill_history, R.id.nav_pay_any_bill ), drawerLayout ) @@ -85,6 +103,133 @@ class MainActivity : AppCompatActivity() { } } + private fun initializeNavigationHeader(navView: NavigationView) { + val headerView = navView.getHeaderView(0) + + try { + accountSwitcherArrow = headerView.findViewById(R.id.account_switcher_arrow) + accountListSection = headerView.findViewById(R.id.account_list_section) + addAccountButton = headerView.findViewById(R.id.add_account_button) + accountsRecyclerView = headerView.findViewById(R.id.accounts_recycler_view) + + android.util.Log.d("MainActivity", "Navigation header views initialized successfully") + + // Set theme-appropriate background for account list + setAccountListTheme() + + // Set up RecyclerView for accounts + accountsRecyclerView.layoutManager = LinearLayoutManager(this) + accountsAdapter = AccountsAdapter( + accounts = emptyList(), + activeAccountId = null, + onAccountClick = { account -> + switchToAccount(account.id) + toggleAccountList(false) + } + ) + accountsRecyclerView.adapter = accountsAdapter + + // Set up click listeners + accountSwitcherArrow.setOnClickListener { + android.util.Log.d("MainActivity", "Account switcher arrow clicked") + toggleAccountList() + } + + addAccountButton.setOnClickListener { + android.util.Log.d("MainActivity", "Add account button clicked") + val intent = Intent(this, AddAccountActivity::class.java) + startActivity(intent) + } + + android.util.Log.d("MainActivity", "Click listeners set up successfully") + + } catch (e: Exception) { + android.util.Log.e("MainActivity", "Error initializing navigation header", e) + } + } + + private fun setAccountListTheme() { + // Check if we're in dark mode + val nightModeFlags = resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK + val headerView = binding.navView.getHeaderView(0) + + when (nightModeFlags) { + android.content.res.Configuration.UI_MODE_NIGHT_YES -> { + // Dark theme - use darker background + accountListSection.setBackgroundColor(android.graphics.Color.parseColor("#2C2C2C")) + + // Update divider color for dark theme + val divider = headerView.findViewById(R.id.account_list_divider) + divider?.setBackgroundColor(android.graphics.Color.parseColor("#555555")) + } + android.content.res.Configuration.UI_MODE_NIGHT_NO -> { + // Light theme - use white background + accountListSection.setBackgroundColor(android.graphics.Color.WHITE) + + // Update divider color for light theme + val divider = headerView.findViewById(R.id.account_list_divider) + divider?.setBackgroundColor(android.graphics.Color.parseColor("#E0E0E0")) + } + else -> { + // Default to white + accountListSection.setBackgroundColor(android.graphics.Color.WHITE) + } + } + } + + private fun toggleAccountList(forceState: Boolean? = null) { + val targetVisibility = forceState ?: !isAccountListVisible + + if (targetVisibility) { + // Show account list + updateAccountsList() + accountListSection.visibility = View.VISIBLE + isAccountListVisible = true + + // Rotate arrow down + ObjectAnimator.ofFloat(accountSwitcherArrow, "rotation", 0f, 180f).apply { + duration = 200 + start() + } + } else { + // Hide account list + accountListSection.visibility = View.GONE + isAccountListVisible = false + + // Rotate arrow up + ObjectAnimator.ofFloat(accountSwitcherArrow, "rotation", 180f, 0f).apply { + duration = 200 + start() + } + } + } + + private fun updateAccountsList() { + val allAccounts = secureStorage.getAllAccounts() + val activeAccount = secureStorage.getActiveAccount() + + // Show only other accounts (not the currently active one) + val otherAccounts = allAccounts.filter { it.id != activeAccount?.id } + + accountsAdapter.updateAccounts(otherAccounts, activeAccount?.id) + + // Show/hide add account button based on whether we have multiple accounts + addAccountButton.visibility = View.VISIBLE + } + + private fun switchToAccount(accountId: String) { + android.util.Log.d("MainActivity", "Switching to account: $accountId") + secureStorage.setActiveAccount(accountId) + updateNavHeader(binding.navView) + + // Navigate to dashboard to show selected account data + val navController = findNavController(R.id.nav_host_fragment_content_main) + navController.navigate(R.id.nav_dashboard) + + // Close the drawer + binding.drawerLayout.closeDrawers() + } + private fun openUrl(url: String) { val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) startActivity(intent) @@ -96,10 +241,22 @@ class MainActivity : AppCompatActivity() { val mobileTextView = headerView.findViewById(R.id.nav_header_mobile) val emailTextView = headerView.findViewById(R.id.nav_header_email) - // Load user info from secure storage - nameTextView.text = secureStorage.getUserName() ?: "User" - mobileTextView.text = secureStorage.getMobile() ?: "" - emailTextView.text = secureStorage.getUserEmail() ?: "" + // Load user info from secure storage (active account) + val activeAccount = secureStorage.getActiveAccount() + nameTextView.text = activeAccount?.name ?: "User" + mobileTextView.text = activeAccount?.mobile ?: "" + emailTextView.text = activeAccount?.email ?: "" + + // Always show account switcher arrow for now (for testing) + // Will hide it only when we're sure there's only one account + val allAccounts = secureStorage.getAllAccounts() + val hasMultipleAccounts = allAccounts.size > 1 + + // For debugging: always show the arrow initially + accountSwitcherArrow.visibility = View.VISIBLE + + // Log for debugging + android.util.Log.d("MainActivity", "Total accounts: ${allAccounts.size}, Active: ${activeAccount?.mobile}") } override fun onCreateOptionsMenu(menu: Menu): Boolean { @@ -112,4 +269,10 @@ class MainActivity : AppCompatActivity() { val navController = findNavController(R.id.nav_host_fragment_content_main) return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() } + + override fun onResume() { + super.onResume() + // Update navigation header in case accounts were added/removed + updateNavHeader(binding.navView) + } } \ No newline at end of file diff --git a/app/src/main/java/sh/sar/gridflow/ui/accounts/AccountsAdapter.kt b/app/src/main/java/sh/sar/gridflow/ui/accounts/AccountsAdapter.kt new file mode 100644 index 0000000..ca8c7e7 --- /dev/null +++ b/app/src/main/java/sh/sar/gridflow/ui/accounts/AccountsAdapter.kt @@ -0,0 +1,71 @@ +package sh.sar.gridflow.ui.accounts + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView +import sh.sar.gridflow.R +import sh.sar.gridflow.utils.Account + +class AccountsAdapter( + private var accounts: List, + private var activeAccountId: String?, + private val onAccountClick: (Account) -> Unit +) : RecyclerView.Adapter() { + + class AccountViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { + val accountName: TextView = itemView.findViewById(R.id.account_name) + val accountMobile: TextView = itemView.findViewById(R.id.account_mobile) + val activeIndicator: ImageView = itemView.findViewById(R.id.active_indicator) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): AccountViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_account, parent, false) + return AccountViewHolder(view) + } + + override fun onBindViewHolder(holder: AccountViewHolder, position: Int) { + val account = accounts[position] + + holder.accountName.text = account.name + holder.accountMobile.text = account.mobile + + // Set theme-appropriate text colors + val context = holder.itemView.context + val nightModeFlags = context.resources.configuration.uiMode and android.content.res.Configuration.UI_MODE_NIGHT_MASK + when (nightModeFlags) { + android.content.res.Configuration.UI_MODE_NIGHT_YES -> { + // Dark theme - use light text + holder.accountName.setTextColor(android.graphics.Color.WHITE) + holder.accountMobile.setTextColor(android.graphics.Color.parseColor("#CCCCCC")) + } + else -> { + // Light theme - use dark text + holder.accountName.setTextColor(android.graphics.Color.BLACK) + holder.accountMobile.setTextColor(android.graphics.Color.parseColor("#666666")) + } + } + + // Show active indicator for current account + holder.activeIndicator.visibility = if (account.id == activeAccountId) { + View.VISIBLE + } else { + View.GONE + } + + holder.itemView.setOnClickListener { + onAccountClick(account) + } + } + + override fun getItemCount(): Int = accounts.size + + fun updateAccounts(newAccounts: List, newActiveAccountId: String?) { + accounts = newAccounts + activeAccountId = newActiveAccountId + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/sh/sar/gridflow/utils/SecureStorage.kt b/app/src/main/java/sh/sar/gridflow/utils/SecureStorage.kt index c680bf4..6211ab1 100644 --- a/app/src/main/java/sh/sar/gridflow/utils/SecureStorage.kt +++ b/app/src/main/java/sh/sar/gridflow/utils/SecureStorage.kt @@ -5,14 +5,32 @@ import android.content.SharedPreferences import android.util.Log import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.MasterKey +import com.google.gson.Gson +import com.google.gson.reflect.TypeToken + +data class Account( + val id: String, // mobile number as unique ID + val name: String, + val mobile: String, + val email: String, + val password: String, + val cookie: String?, + val userId: Int, + val isActive: Boolean = false +) class SecureStorage(context: Context) { private val sharedPreferences: SharedPreferences + private val gson = Gson() companion object { private const val TAG = "SecureStorage" private const val PREFS_NAME = "gridflow_secure_prefs" + private const val KEY_ACCOUNTS = "accounts" + private const val KEY_ACTIVE_ACCOUNT_ID = "active_account_id" + + // Legacy keys for backward compatibility private const val KEY_MOBILE = "mobile" private const val KEY_PASSWORD = "password" private const val KEY_COOKIE = "cookie" @@ -36,49 +54,157 @@ class SecureStorage(context: Context) { EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ) Log.d(TAG, "SecureStorage initialized successfully") + + // Migrate legacy data if exists + migrateLegacyData() } catch (e: Exception) { Log.e(TAG, "Failed to initialize SecureStorage", e) throw e } } - fun saveCredentials(mobile: String, password: String) { - Log.d(TAG, "Saving credentials for mobile: $mobile") + private fun migrateLegacyData() { + val legacyMobile = sharedPreferences.getString(KEY_MOBILE, null) + if (legacyMobile != null && getAllAccounts().isEmpty()) { + Log.d(TAG, "Migrating legacy account data") + val legacyAccount = Account( + id = legacyMobile, + name = sharedPreferences.getString(KEY_USER_NAME, "") ?: "", + mobile = legacyMobile, + email = sharedPreferences.getString(KEY_USER_EMAIL, "") ?: "", + password = sharedPreferences.getString(KEY_PASSWORD, "") ?: "", + cookie = sharedPreferences.getString(KEY_COOKIE, null), + userId = sharedPreferences.getInt(KEY_USER_ID, -1), + isActive = true + ) + saveAccount(legacyAccount) + setActiveAccount(legacyMobile) + + // Clear legacy keys + sharedPreferences.edit() + .remove(KEY_MOBILE) + .remove(KEY_PASSWORD) + .remove(KEY_COOKIE) + .remove(KEY_USER_NAME) + .remove(KEY_USER_EMAIL) + .remove(KEY_USER_ID) + .apply() + } + } + + fun saveAccount(account: Account) { + Log.d(TAG, "Saving account: ${account.mobile}") + val accounts = getAllAccounts().toMutableList() + + // Remove existing account with same ID if exists + accounts.removeAll { it.id == account.id } + + // Add the new/updated account + accounts.add(account) + + // Save accounts list + val accountsJson = gson.toJson(accounts) sharedPreferences.edit() - .putString(KEY_MOBILE, mobile) - .putString(KEY_PASSWORD, password) + .putString(KEY_ACCOUNTS, accountsJson) .apply() } + fun getAllAccounts(): List { + val accountsJson = sharedPreferences.getString(KEY_ACCOUNTS, null) + return if (accountsJson != null) { + try { + val type = object : TypeToken>() {}.type + gson.fromJson(accountsJson, type) + } catch (e: Exception) { + Log.e(TAG, "Failed to parse accounts JSON", e) + emptyList() + } + } else { + emptyList() + } + } + + fun getActiveAccount(): Account? { + val activeAccountId = sharedPreferences.getString(KEY_ACTIVE_ACCOUNT_ID, null) + return if (activeAccountId != null) { + getAllAccounts().find { it.id == activeAccountId } + } else { + // Fallback to first account if no active account set + getAllAccounts().firstOrNull() + } + } + + fun setActiveAccount(accountId: String) { + Log.d(TAG, "Setting active account: $accountId") + sharedPreferences.edit() + .putString(KEY_ACTIVE_ACCOUNT_ID, accountId) + .apply() + } + + fun removeAccount(accountId: String) { + Log.d(TAG, "Removing account: $accountId") + val accounts = getAllAccounts().toMutableList() + accounts.removeAll { it.id == accountId } + + val accountsJson = gson.toJson(accounts) + sharedPreferences.edit() + .putString(KEY_ACCOUNTS, accountsJson) + .apply() + + // If this was the active account, set another one as active + val currentActiveId = sharedPreferences.getString(KEY_ACTIVE_ACCOUNT_ID, null) + if (currentActiveId == accountId) { + val remainingAccounts = getAllAccounts() + if (remainingAccounts.isNotEmpty()) { + setActiveAccount(remainingAccounts.first().id) + } else { + sharedPreferences.edit().remove(KEY_ACTIVE_ACCOUNT_ID).apply() + } + } + } + + // Legacy compatibility methods - these now work with the active account + fun saveCredentials(mobile: String, password: String) { + val activeAccount = getActiveAccount() + if (activeAccount != null) { + val updatedAccount = activeAccount.copy(mobile = mobile, password = password) + saveAccount(updatedAccount) + } + } + fun saveCookie(cookie: String) { - Log.d(TAG, "Saving cookie: $cookie") - sharedPreferences.edit() - .putString(KEY_COOKIE, cookie) - .apply() + val activeAccount = getActiveAccount() + if (activeAccount != null) { + val updatedAccount = activeAccount.copy(cookie = cookie) + saveAccount(updatedAccount) + } } fun saveUserInfo(name: String, email: String, userId: Int) { - Log.d(TAG, "Saving user info: name=$name, email=$email, userId=$userId") - sharedPreferences.edit() - .putString(KEY_USER_NAME, name) - .putString(KEY_USER_EMAIL, email) - .putInt(KEY_USER_ID, userId) - .apply() + val activeAccount = getActiveAccount() + if (activeAccount != null) { + val updatedAccount = activeAccount.copy(name = name, email = email, userId = userId) + saveAccount(updatedAccount) + } } - fun getMobile(): String? = sharedPreferences.getString(KEY_MOBILE, null) - fun getPassword(): String? = sharedPreferences.getString(KEY_PASSWORD, null) - fun getCookie(): String? = sharedPreferences.getString(KEY_COOKIE, null) - fun getUserName(): String? = sharedPreferences.getString(KEY_USER_NAME, null) - fun getUserEmail(): String? = sharedPreferences.getString(KEY_USER_EMAIL, null) - fun getUserId(): Int = sharedPreferences.getInt(KEY_USER_ID, -1) + fun getMobile(): String? = getActiveAccount()?.mobile + fun getPassword(): String? = getActiveAccount()?.password + fun getCookie(): String? = getActiveAccount()?.cookie + fun getUserName(): String? = getActiveAccount()?.name + fun getUserEmail(): String? = getActiveAccount()?.email + fun getUserId(): Int = getActiveAccount()?.userId ?: -1 fun isLoggedIn(): Boolean { - return getCookie() != null && getUserName() != null + return getActiveAccount()?.cookie != null && getActiveAccount()?.name?.isNotEmpty() == true } fun clearCredentials() { Log.d(TAG, "Clearing all credentials") sharedPreferences.edit().clear().apply() } + + fun hasMultipleAccounts(): Boolean { + return getAllAccounts().size > 1 + } } diff --git a/app/src/main/res/color/account_dropdown_background.xml b/app/src/main/res/color/account_dropdown_background.xml new file mode 100644 index 0000000..7d75e13 --- /dev/null +++ b/app/src/main/res/color/account_dropdown_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/color/account_dropdown_text.xml b/app/src/main/res/color/account_dropdown_text.xml new file mode 100644 index 0000000..df0cd70 --- /dev/null +++ b/app/src/main/res/color/account_dropdown_text.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/color/account_dropdown_text_secondary.xml b/app/src/main/res/color/account_dropdown_text_secondary.xml new file mode 100644 index 0000000..e098382 --- /dev/null +++ b/app/src/main/res/color/account_dropdown_text_secondary.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/color/account_list_background.xml b/app/src/main/res/color/account_list_background.xml new file mode 100644 index 0000000..3227908 --- /dev/null +++ b/app/src/main/res/color/account_list_background.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/color/account_list_text_color.xml b/app/src/main/res/color/account_list_text_color.xml new file mode 100644 index 0000000..32da1f8 --- /dev/null +++ b/app/src/main/res/color/account_list_text_color.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/color/nav_header_arrow_tint.xml b/app/src/main/res/color/nav_header_arrow_tint.xml new file mode 100644 index 0000000..a9eea66 --- /dev/null +++ b/app/src/main/res/color/nav_header_arrow_tint.xml @@ -0,0 +1,7 @@ + + + + + + + diff --git a/app/src/main/res/color/nav_header_text_color.xml b/app/src/main/res/color/nav_header_text_color.xml new file mode 100644 index 0000000..282713b --- /dev/null +++ b/app/src/main/res/color/nav_header_text_color.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/account_circle_background.xml b/app/src/main/res/drawable/account_circle_background.xml new file mode 100644 index 0000000..b489954 --- /dev/null +++ b/app/src/main/res/drawable/account_circle_background.xml @@ -0,0 +1,5 @@ + + + + diff --git a/app/src/main/res/drawable/ic_check_24.xml b/app/src/main/res/drawable/ic_check_24.xml new file mode 100644 index 0000000..0346c59 --- /dev/null +++ b/app/src/main/res/drawable/ic_check_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_expand_more_24.xml b/app/src/main/res/drawable/ic_expand_more_24.xml new file mode 100644 index 0000000..e27707e --- /dev/null +++ b/app/src/main/res/drawable/ic_expand_more_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/layout/activity_add_account.xml b/app/src/main/res/layout/activity_add_account.xml new file mode 100644 index 0000000..6e9cc48 --- /dev/null +++ b/app/src/main/res/layout/activity_add_account.xml @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml index 1eb85e3..6995e26 100644 --- a/app/src/main/res/layout/activity_login.xml +++ b/app/src/main/res/layout/activity_login.xml @@ -4,7 +4,7 @@ android:layout_width="match_parent" android:layout_height="match_parent" android:fillViewport="true" - android:background="?android:attr/colorBackground"> + android:background="@android:color/white"> @@ -150,7 +150,7 @@ android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" - android:background="?android:attr/textColorSecondary" + android:background="#CCCCCC" android:alpha="0.3" /> @@ -167,7 +167,7 @@ android:layout_width="0dp" android:layout_height="1dp" android:layout_weight="1" - android:background="?android:attr/textColorSecondary" + android:background="#CCCCCC" android:alpha="0.3" /> diff --git a/app/src/main/res/layout/item_account.xml b/app/src/main/res/layout/item_account.xml new file mode 100644 index 0000000..16aecef --- /dev/null +++ b/app/src/main/res/layout/item_account.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/nav_header_main.xml b/app/src/main/res/layout/nav_header_main.xml index 43cdb2d..b1ddaa7 100644 --- a/app/src/main/res/layout/nav_header_main.xml +++ b/app/src/main/res/layout/nav_header_main.xml @@ -2,42 +2,134 @@ - + + - + + + + + + + + + + + + + + + + + + + + + + + + android:orientation="vertical" + android:visibility="gone" + android:background="@android:color/white"> - + - + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml index 9e77f69..6627d1b 100644 --- a/app/src/main/res/menu/activity_main_drawer.xml +++ b/app/src/main/res/menu/activity_main_drawer.xml @@ -8,18 +8,21 @@ android:id="@+id/nav_dashboard" android:icon="@drawable/ic_dashboard_24" android:title="@string/menu_dashboard" /> + + + + -