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" />
</intent-filter>
</activity>
<activity
android:name=".AddAccountActivity"
android:exported="false"
android:label="Add Account"
android:theme="@style/Theme.GridFlow.NoActionBar" />
<activity
android:name=".DebugLoginActivity"
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.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)
}

View File

@@ -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<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) {
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
startActivity(intent)
@@ -96,10 +241,22 @@ class MainActivity : AppCompatActivity() {
val mobileTextView = headerView.findViewById<TextView>(R.id.nav_header_mobile)
val emailTextView = headerView.findViewById<TextView>(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)
}
}

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 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<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) {
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
}
}

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_height="match_parent"
android:fillViewport="true"
android:background="?android:attr/colorBackground">
android:background="@android:color/white">
<LinearLayout
android:layout_width="match_parent"
@@ -35,7 +35,7 @@
android:text="GridFlow"
android:textSize="28sp"
android:textStyle="bold"
android:textColor="?android:attr/textColorPrimary"
android:textColor="#000000"
android:layout_marginBottom="8dp" />
<TextView
@@ -43,7 +43,7 @@
android:layout_height="wrap_content"
android:text="Your Personal Fenaka Client"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:textColor="#666666"
android:alpha="0.7" />
</LinearLayout>
@@ -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" />
<TextView
@@ -158,7 +158,7 @@
android:layout_height="wrap_content"
android:text="OR"
android:textSize="14sp"
android:textColor="?android:attr/textColorSecondary"
android:textColor="#666666"
android:alpha="0.7"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" />
@@ -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" />
</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"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="@dimen/nav_header_height"
android:layout_height="wrap_content"
android:background="@drawable/side_nav_bar"
android:gravity="bottom"
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">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/nav_header_desc"
android:paddingTop="@dimen/nav_header_vertical_spacing"
app:srcCompat="@mipmap/ic_launcher_round" />
<!-- Main Account Section -->
<LinearLayout
android:id="@+id/main_account_section"
android:layout_width="match_parent"
android:layout_height="@dimen/nav_header_height"
android:gravity="bottom"
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
android:id="@+id/nav_header_name"
<!-- Top section with app logo and account switcher -->
<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_height="wrap_content"
android:paddingTop="@dimen/nav_header_vertical_spacing"
android:text="Shihaam Abdul Rahman"
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
android:orientation="vertical"
android:visibility="gone"
android:background="@android:color/white">
<TextView
android:id="@+id/nav_header_mobile"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="9198026" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/accounts_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
<TextView
android:id="@+id/nav_header_email"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="shihaam_ab_r@outlook.com" />
<!-- Add Account Button -->
<LinearLayout
android:id="@+id/add_account_button"
android:layout_width="match_parent"
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>

View File

@@ -8,18 +8,21 @@
android:id="@+id/nav_dashboard"
android:icon="@drawable/ic_dashboard_24"
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
android:id="@+id/nav_subscriptions"
android:icon="@drawable/ic_subscriptions_24"
android:title="@string/menu_subscriptions" />
</group>
<group android:id="@+id/group_utilities">
<item
android:id="@+id/nav_band_rates"
android:icon="@drawable/ic_band_rates_24"
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
android:id="@+id/nav_pay_any_bill"
android:icon="@drawable/ic_pay_any_bill_24"