rearrange nav menu

This commit is contained in:
2025-07-24 22:33:31 +05:00
parent 41aefc447a
commit 3baf959062
21 changed files with 1031 additions and 75 deletions

View File

@@ -25,6 +25,11 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<activity
android:name=".AddAccountActivity"
android:exported="false"
android:label="Add Account"
android:theme="@style/Theme.GridFlow.NoActionBar" />
<activity <activity
android:name=".DebugLoginActivity" android:name=".DebugLoginActivity"
android:exported="false" android:exported="false"

View File

@@ -0,0 +1,219 @@
package sh.sar.gridflow
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import sh.sar.gridflow.databinding.ActivityAddAccountBinding
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 AddAccountActivity : AppCompatActivity() {
private lateinit var binding: ActivityAddAccountBinding
private lateinit var secureStorage: SecureStorage
private lateinit var apiService: FenakaApiService
companion object {
private const val TAG = "AddAccountActivity"
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Force system theme (follows device dark mode setting)
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
Log.d(TAG, "AddAccountActivity onCreate")
binding = ActivityAddAccountBinding.inflate(layoutInflater)
setContentView(binding.root)
// Set up the toolbar
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
supportActionBar?.title = "Add Account"
try {
secureStorage = SecureStorage(this)
apiService = FenakaApiService()
} catch (e: Exception) {
Log.e(TAG, "Failed to initialize SecureStorage, continuing without it", e)
apiService = FenakaApiService()
Toast.makeText(this, "Warning: Secure storage not available", Toast.LENGTH_SHORT).show()
}
setupClickListeners()
}
override fun onSupportNavigateUp(): Boolean {
onBackPressed()
return true
}
private fun setupClickListeners() {
binding.btnSignIn.setOnClickListener {
handleSignIn()
}
binding.btnRegister.setOnClickListener {
handleRegister()
}
binding.btnForgotPassword.setOnClickListener {
handleForgotPassword()
}
}
private fun showLoginError(message: String) {
val dialog = androidx.appcompat.app.AlertDialog.Builder(this)
.setTitle("Login Failed")
.setMessage(message)
.setPositiveButton("Retry") { _, _ ->
// 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
}
}

View File

@@ -12,6 +12,7 @@ import kotlinx.coroutines.launch
import sh.sar.gridflow.databinding.ActivityLoginBinding import sh.sar.gridflow.databinding.ActivityLoginBinding
import sh.sar.gridflow.network.ApiResult import sh.sar.gridflow.network.ApiResult
import sh.sar.gridflow.network.FenakaApiService import sh.sar.gridflow.network.FenakaApiService
import sh.sar.gridflow.utils.Account
import sh.sar.gridflow.utils.SecureStorage import sh.sar.gridflow.utils.SecureStorage
class LoginActivity : AppCompatActivity() { class LoginActivity : AppCompatActivity() {
@@ -124,19 +125,21 @@ class LoginActivity : AppCompatActivity() {
// Save credentials and user info (if SecureStorage is available) // Save credentials and user info (if SecureStorage is available)
if (this@LoginActivity::secureStorage.isInitialized) { if (this@LoginActivity::secureStorage.isInitialized) {
try { try {
secureStorage.saveCredentials(mobile, password) // Create new account object
secureStorage.saveUserInfo( val newAccount = Account(
result.data.name, id = mobile,
result.data.email, name = result.data.name,
result.data.id mobile = mobile,
email = result.data.email,
password = password,
cookie = extractSessionId(result.cookie ?: ""),
userId = result.data.id,
isActive = true
) )
// Extract and save cookie // Save the account and set as active
result.cookie?.let { cookie -> secureStorage.saveAccount(newAccount)
val sessionId = extractSessionId(cookie) secureStorage.setActiveAccount(mobile)
Log.d(TAG, "Extracted session ID: $sessionId")
secureStorage.saveCookie(sessionId)
}
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to save credentials securely", e) Log.e(TAG, "Failed to save credentials securely", e)
} }

View File

@@ -1,10 +1,14 @@
package sh.sar.gridflow package sh.sar.gridflow
import android.animation.ObjectAnimator
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.Menu import android.view.Menu
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import android.widget.ImageView
import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
@@ -14,8 +18,11 @@ import androidx.navigation.ui.AppBarConfiguration
import androidx.navigation.ui.navigateUp import androidx.navigation.ui.navigateUp
import androidx.navigation.ui.setupActionBarWithNavController import androidx.navigation.ui.setupActionBarWithNavController
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.navigation.NavigationView import com.google.android.material.navigation.NavigationView
import sh.sar.gridflow.databinding.ActivityMainBinding import sh.sar.gridflow.databinding.ActivityMainBinding
import sh.sar.gridflow.ui.accounts.AccountsAdapter
import sh.sar.gridflow.utils.SecureStorage import sh.sar.gridflow.utils.SecureStorage
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
@@ -23,6 +30,14 @@ class MainActivity : AppCompatActivity() {
private lateinit var appBarConfiguration: AppBarConfiguration private lateinit var appBarConfiguration: AppBarConfiguration
private lateinit var binding: ActivityMainBinding private lateinit var binding: ActivityMainBinding
private lateinit var secureStorage: SecureStorage 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?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@@ -41,6 +56,9 @@ class MainActivity : AppCompatActivity() {
val navView: NavigationView = binding.navView val navView: NavigationView = binding.navView
val navController = findNavController(R.id.nav_host_fragment_content_main) val navController = findNavController(R.id.nav_host_fragment_content_main)
// Initialize navigation header components
initializeNavigationHeader(navView)
// Update navigation header with user info // Update navigation header with user info
updateNavHeader(navView) updateNavHeader(navView)
@@ -48,9 +66,9 @@ class MainActivity : AppCompatActivity() {
appBarConfiguration = AppBarConfiguration( appBarConfiguration = AppBarConfiguration(
setOf( setOf(
R.id.nav_dashboard, R.id.nav_dashboard,
R.id.nav_bill_history,
R.id.nav_subscriptions, R.id.nav_subscriptions,
R.id.nav_band_rates, R.id.nav_band_rates,
R.id.nav_bill_history,
R.id.nav_pay_any_bill R.id.nav_pay_any_bill
), drawerLayout ), 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<View>(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<View>(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) { private fun openUrl(url: String) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent) startActivity(intent)
@@ -96,10 +241,22 @@ class MainActivity : AppCompatActivity() {
val mobileTextView = headerView.findViewById<TextView>(R.id.nav_header_mobile) val mobileTextView = headerView.findViewById<TextView>(R.id.nav_header_mobile)
val emailTextView = headerView.findViewById<TextView>(R.id.nav_header_email) val emailTextView = headerView.findViewById<TextView>(R.id.nav_header_email)
// Load user info from secure storage // Load user info from secure storage (active account)
nameTextView.text = secureStorage.getUserName() ?: "User" val activeAccount = secureStorage.getActiveAccount()
mobileTextView.text = secureStorage.getMobile() ?: "" nameTextView.text = activeAccount?.name ?: "User"
emailTextView.text = secureStorage.getUserEmail() ?: "" 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 { override fun onCreateOptionsMenu(menu: Menu): Boolean {
@@ -112,4 +269,10 @@ class MainActivity : AppCompatActivity() {
val navController = findNavController(R.id.nav_host_fragment_content_main) val navController = findNavController(R.id.nav_host_fragment_content_main)
return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp() return navController.navigateUp(appBarConfiguration) || super.onSupportNavigateUp()
} }
override fun onResume() {
super.onResume()
// Update navigation header in case accounts were added/removed
updateNavHeader(binding.navView)
}
} }

View File

@@ -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<Account>,
private var activeAccountId: String?,
private val onAccountClick: (Account) -> Unit
) : RecyclerView.Adapter<AccountsAdapter.AccountViewHolder>() {
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<Account>, newActiveAccountId: String?) {
accounts = newAccounts
activeAccountId = newActiveAccountId
notifyDataSetChanged()
}
}

View File

@@ -5,14 +5,32 @@ import android.content.SharedPreferences
import android.util.Log import android.util.Log
import androidx.security.crypto.EncryptedSharedPreferences import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKey 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) { class SecureStorage(context: Context) {
private val sharedPreferences: SharedPreferences private val sharedPreferences: SharedPreferences
private val gson = Gson()
companion object { companion object {
private const val TAG = "SecureStorage" private const val TAG = "SecureStorage"
private const val PREFS_NAME = "gridflow_secure_prefs" 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_MOBILE = "mobile"
private const val KEY_PASSWORD = "password" private const val KEY_PASSWORD = "password"
private const val KEY_COOKIE = "cookie" private const val KEY_COOKIE = "cookie"
@@ -36,49 +54,157 @@ class SecureStorage(context: Context) {
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
) )
Log.d(TAG, "SecureStorage initialized successfully") Log.d(TAG, "SecureStorage initialized successfully")
// Migrate legacy data if exists
migrateLegacyData()
} catch (e: Exception) { } catch (e: Exception) {
Log.e(TAG, "Failed to initialize SecureStorage", e) Log.e(TAG, "Failed to initialize SecureStorage", e)
throw e throw e
} }
} }
fun saveCredentials(mobile: String, password: String) { private fun migrateLegacyData() {
Log.d(TAG, "Saving credentials for mobile: $mobile") 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() sharedPreferences.edit()
.putString(KEY_MOBILE, mobile) .putString(KEY_ACCOUNTS, accountsJson)
.putString(KEY_PASSWORD, password)
.apply() .apply()
} }
fun getAllAccounts(): List<Account> {
val accountsJson = sharedPreferences.getString(KEY_ACCOUNTS, null)
return if (accountsJson != null) {
try {
val type = object : TypeToken<List<Account>>() {}.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) { fun saveCookie(cookie: String) {
Log.d(TAG, "Saving cookie: $cookie") val activeAccount = getActiveAccount()
sharedPreferences.edit() if (activeAccount != null) {
.putString(KEY_COOKIE, cookie) val updatedAccount = activeAccount.copy(cookie = cookie)
.apply() saveAccount(updatedAccount)
}
} }
fun saveUserInfo(name: String, email: String, userId: Int) { fun saveUserInfo(name: String, email: String, userId: Int) {
Log.d(TAG, "Saving user info: name=$name, email=$email, userId=$userId") val activeAccount = getActiveAccount()
sharedPreferences.edit() if (activeAccount != null) {
.putString(KEY_USER_NAME, name) val updatedAccount = activeAccount.copy(name = name, email = email, userId = userId)
.putString(KEY_USER_EMAIL, email) saveAccount(updatedAccount)
.putInt(KEY_USER_ID, userId) }
.apply()
} }
fun getMobile(): String? = sharedPreferences.getString(KEY_MOBILE, null) fun getMobile(): String? = getActiveAccount()?.mobile
fun getPassword(): String? = sharedPreferences.getString(KEY_PASSWORD, null) fun getPassword(): String? = getActiveAccount()?.password
fun getCookie(): String? = sharedPreferences.getString(KEY_COOKIE, null) fun getCookie(): String? = getActiveAccount()?.cookie
fun getUserName(): String? = sharedPreferences.getString(KEY_USER_NAME, null) fun getUserName(): String? = getActiveAccount()?.name
fun getUserEmail(): String? = sharedPreferences.getString(KEY_USER_EMAIL, null) fun getUserEmail(): String? = getActiveAccount()?.email
fun getUserId(): Int = sharedPreferences.getInt(KEY_USER_ID, -1) fun getUserId(): Int = getActiveAccount()?.userId ?: -1
fun isLoggedIn(): Boolean { fun isLoggedIn(): Boolean {
return getCookie() != null && getUserName() != null return getActiveAccount()?.cookie != null && getActiveAccount()?.name?.isNotEmpty() == true
} }
fun clearCredentials() { fun clearCredentials() {
Log.d(TAG, "Clearing all credentials") Log.d(TAG, "Clearing all credentials")
sharedPreferences.edit().clear().apply() sharedPreferences.edit().clear().apply()
} }
fun hasMultipleAccounts(): Boolean {
return getAllAccounts().size > 1
}
} }

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Safe system background color -->
<item android:color="@android:color/background_light"/>
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Safe system text color -->
<item android:color="@android:color/primary_text_light"/>
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Safe secondary text color -->
<item android:color="@android:color/secondary_text_light"/>
</selector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/background_light" />
</selector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/primary_text_light" />
</selector>

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Dark theme -->
<item android:color="#FFFFFF" android:state_selected="false"/>
<!-- Light theme fallback -->
<item android:color="#FFFFFF"/>
</selector>

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/white" />
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="?attr/colorPrimary" />
</shape>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorOnSurface">
<path
android:fillColor="@android:color/white"
android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorOnSurface">
<path
android:fillColor="@android:color/white"
android:pathData="M16.59,8.59L12,13.17 7.41,8.59 6,10l6,6 6,-6z"/>
</vector>

View File

@@ -0,0 +1,167 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:background="?android:attr/colorBackground">
<!-- Toolbar -->
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="?attr/colorPrimary"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"
app:popupTheme="@style/ThemeOverlay.AppCompat.Light" />
<!-- Content ScrollView -->
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:padding="32dp"
android:gravity="center">
<!-- App Logo Section -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:orientation="vertical"
android:gravity="center">
<ImageView
android:id="@+id/iv_app_logo"
android:layout_width="100dp"
android:layout_height="100dp"
android:layout_marginBottom="24dp"
android:src="@mipmap/ic_launcher"
android:contentDescription="GridFlow Logo" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add Account"
android:textSize="24sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:layout_marginBottom="8dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Sign in with another Fenaka account"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:alpha="0.7" />
</LinearLayout>
<!-- Login Form Section -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginTop="32dp">
<!-- Mobile Number Input -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:hint="Mobile Number"
app:boxStrokeColor="@color/design_default_color_primary"
app:hintTextColor="@color/design_default_color_primary"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_mobile_number"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="phone"
android:maxLength="7"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Password Input -->
<com.google.android.material.textfield.TextInputLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="24dp"
android:hint="Password"
app:boxStrokeColor="@color/design_default_color_primary"
app:hintTextColor="@color/design_default_color_primary"
app:passwordToggleEnabled="true"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/et_password"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
android:textSize="16sp" />
</com.google.android.material.textfield.TextInputLayout>
<!-- Action Buttons Row -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:layout_marginBottom="32dp"
android:weightSum="3">
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_sign_in"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginEnd="4dp"
android:text="Sign In"
android:textSize="12sp"
app:cornerRadius="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_register"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:layout_marginEnd="4dp"
android:text="Register"
android:textSize="12sp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
app:cornerRadius="8dp" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btn_forgot_password"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_marginStart="4dp"
android:text="Forgot?"
android:textSize="12sp"
style="@style/Widget.MaterialComponents.Button.TextButton"
app:cornerRadius="8dp" />
</LinearLayout>
</LinearLayout>
<!-- Spacer for bottom -->
<View
android:layout_width="match_parent"
android:layout_height="32dp" />
</LinearLayout>
</ScrollView>
</LinearLayout>

View File

@@ -4,7 +4,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:fillViewport="true" android:fillViewport="true"
android:background="?android:attr/colorBackground"> android:background="@android:color/white">
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
@@ -35,7 +35,7 @@
android:text="GridFlow" android:text="GridFlow"
android:textSize="28sp" android:textSize="28sp"
android:textStyle="bold" android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary" android:textColor="#000000"
android:layout_marginBottom="8dp" /> android:layout_marginBottom="8dp" />
<TextView <TextView
@@ -43,7 +43,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="Your Personal Fenaka Client" android:text="Your Personal Fenaka Client"
android:textSize="14sp" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:textColor="#666666"
android:alpha="0.7" /> android:alpha="0.7" />
</LinearLayout> </LinearLayout>
@@ -150,7 +150,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="1dp" android:layout_height="1dp"
android:layout_weight="1" android:layout_weight="1"
android:background="?android:attr/textColorSecondary" android:background="#CCCCCC"
android:alpha="0.3" /> android:alpha="0.3" />
<TextView <TextView
@@ -158,7 +158,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="OR" android:text="OR"
android:textSize="14sp" android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary" android:textColor="#666666"
android:alpha="0.7" android:alpha="0.7"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" /> android:layout_marginEnd="16dp" />
@@ -167,7 +167,7 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="1dp" android:layout_height="1dp"
android:layout_weight="1" android:layout_weight="1"
android:background="?android:attr/textColorSecondary" android:background="#CCCCCC"
android:alpha="0.3" /> android:alpha="0.3" />
</LinearLayout> </LinearLayout>

View File

@@ -0,0 +1,48 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingTop="12dp"
android:paddingRight="16dp"
android:paddingBottom="12dp">
<!-- Account info -->
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:orientation="vertical">
<TextView
android:id="@+id/account_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Shihaam Abdul Rahman"
android:textColor="@android:color/black"
android:textSize="14sp"
android:textStyle="bold" />
<TextView
android:id="@+id/account_mobile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="9198026"
android:textColor="#666666"
android:textSize="12sp" />
</LinearLayout>
<!-- Active indicator -->
<ImageView
android:id="@+id/active_indicator"
android:layout_width="20dp"
android:layout_height="20dp"
android:src="@drawable/ic_check_24"
android:tint="?attr/colorPrimary"
android:visibility="gone" />
</LinearLayout>

View File

@@ -2,42 +2,134 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="@dimen/nav_header_height" android:layout_height="wrap_content"
android:background="@drawable/side_nav_bar" android:background="@drawable/side_nav_bar"
android:gravity="bottom"
android:orientation="vertical" android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingBottom="@dimen/activity_vertical_margin"
android:theme="@style/ThemeOverlay.AppCompat.Dark"> android:theme="@style/ThemeOverlay.AppCompat.Dark">
<ImageView <!-- Main Account Section -->
android:id="@+id/imageView" <LinearLayout
android:layout_width="wrap_content" android:id="@+id/main_account_section"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:contentDescription="@string/nav_header_desc" android:layout_height="@dimen/nav_header_height"
android:paddingTop="@dimen/nav_header_vertical_spacing" android:gravity="bottom"
app:srcCompat="@mipmap/ic_launcher_round" /> android:orientation="vertical"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin"
android:paddingRight="@dimen/activity_vertical_margin"
android:paddingBottom="@dimen/activity_vertical_margin">
<TextView <!-- Top section with app logo and account switcher -->
android:id="@+id/nav_header_name" <LinearLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:gravity="center_vertical"
android:orientation="horizontal">
<View
android:layout_width="0dp"
android:layout_height="1dp"
android:layout_weight="1" />
<ImageView
android:id="@+id/account_switcher_arrow"
android:layout_width="24dp"
android:layout_height="24dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="Account switcher"
android:padding="4dp"
android:src="@drawable/ic_expand_more_24"
android:tint="@color/nav_header_text_color" />
</LinearLayout>
<!-- Account info section -->
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/nav_header_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Shihaam Abdul Rahman"
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
android:textStyle="bold" />
<TextView
android:id="@+id/nav_header_mobile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:text="9198026"
android:textSize="12sp"
android:alpha="0.8" />
<TextView
android:id="@+id/nav_header_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="shihaam_ab_r@outlook.com"
android:textSize="11sp"
android:alpha="0.7" />
</LinearLayout>
</LinearLayout>
<!-- Account List Section (initially hidden) -->
<LinearLayout
android:id="@+id/account_list_section"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing" android:orientation="vertical"
android:text="Shihaam Abdul Rahman" android:visibility="gone"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" /> android:background="@android:color/white">
<TextView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/nav_header_mobile" android:id="@+id/accounts_recycler_view"
android:layout_width="wrap_content" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:text="9198026" /> android:paddingTop="8dp"
android:paddingBottom="8dp" />
<TextView <!-- Add Account Button -->
android:id="@+id/nav_header_email" <LinearLayout
android:layout_width="wrap_content" android:id="@+id/add_account_button"
android:layout_height="wrap_content" android:layout_width="match_parent"
android:text="shihaam_ab_r@outlook.com" /> android:layout_height="48dp"
android:background="?attr/selectableItemBackground"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="16dp"
android:paddingRight="16dp">
<ImageView
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginRight="16dp"
android:src="@drawable/ic_add_24"
android:tint="?attr/colorPrimary" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Add account"
android:textColor="?attr/colorPrimary"
android:textSize="14sp" />
</LinearLayout>
<!-- Divider -->
<View
android:id="@+id/account_list_divider"
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="8dp"
android:background="#E0E0E0" />
</LinearLayout>
</LinearLayout> </LinearLayout>

View File

@@ -8,18 +8,21 @@
android:id="@+id/nav_dashboard" android:id="@+id/nav_dashboard"
android:icon="@drawable/ic_dashboard_24" android:icon="@drawable/ic_dashboard_24"
android:title="@string/menu_dashboard" /> android:title="@string/menu_dashboard" />
<item
android:id="@+id/nav_bill_history"
android:icon="@drawable/ic_bill_history_24"
android:title="@string/menu_bill_history" />
<item <item
android:id="@+id/nav_subscriptions" android:id="@+id/nav_subscriptions"
android:icon="@drawable/ic_subscriptions_24" android:icon="@drawable/ic_subscriptions_24"
android:title="@string/menu_subscriptions" /> android:title="@string/menu_subscriptions" />
</group>
<group android:id="@+id/group_utilities">
<item <item
android:id="@+id/nav_band_rates" android:id="@+id/nav_band_rates"
android:icon="@drawable/ic_band_rates_24" android:icon="@drawable/ic_band_rates_24"
android:title="@string/menu_band_rates" /> android:title="@string/menu_band_rates" />
<item
android:id="@+id/nav_bill_history"
android:icon="@drawable/ic_bill_history_24"
android:title="@string/menu_bill_history" />
<item <item
android:id="@+id/nav_pay_any_bill" android:id="@+id/nav_pay_any_bill"
android:icon="@drawable/ic_pay_any_bill_24" android:icon="@drawable/ic_pay_any_bill_24"