This commit is contained in:
Binondi
2024-12-15 14:32:04 +05:30
parent 78ff4fe28a
commit 7b93bf8789
17 changed files with 374 additions and 176 deletions

View File

@@ -1,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
xmlns:tools="http://schemas.android.com/tools"
package="devs.org.calculator"
>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
android:maxSdkVersion="32" />
@@ -17,6 +19,7 @@
<application
android:name=".CalculatorApp"
android:allowBackup="true"
android:requestLegacyExternalStorage="true"
android:dataExtractionRules="@xml/data_extraction_rules"
android:fullBackupContent="@xml/backup_rules"
android:icon="@mipmap/ic_launcher"

View File

@@ -7,11 +7,9 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope
import devs.org.calculator.callbacks.FileProcessCallback
import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.FileProcessCallback
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class AudioGalleryActivity : BaseGalleryActivity(), FileProcessCallback {

View File

@@ -7,12 +7,9 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope
import devs.org.calculator.activities.AudioGalleryActivity
import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.FileProcessCallback
import kotlinx.coroutines.Dispatchers
import devs.org.calculator.callbacks.FileProcessCallback
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class DocumentsActivity : BaseGalleryActivity(), FileProcessCallback {

View File

@@ -15,13 +15,10 @@ import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.lifecycle.lifecycleScope
import devs.org.calculator.utils.FileManager
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import android.Manifest
import devs.org.calculator.activities.AudioGalleryActivity
import devs.org.calculator.utils.FileProcessCallback
import devs.org.calculator.callbacks.FileProcessCallback
class ImageGalleryActivity : BaseGalleryActivity(), FileProcessCallback {
override val fileType = FileManager.FileType.IMAGE

View File

@@ -3,25 +3,38 @@ package devs.org.calculator.activities
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.button.MaterialButton
import devs.org.calculator.R
import devs.org.calculator.callbacks.DialogActionsCallback
import devs.org.calculator.databinding.ActivityMainBinding
import devs.org.calculator.utils.DialogUtil
import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.PrefsUtil
import net.objecthunter.exp4j.ExpressionBuilder
class MainActivity : AppCompatActivity() {
class MainActivity : AppCompatActivity(), DialogActionsCallback {
private lateinit var binding: ActivityMainBinding
private var currentExpression = "0"
private var lastWasOperator = false
private var hasDecimal = false
private lateinit var launcher: ActivityResultLauncher<Intent>
private lateinit var baseDocumentTreeUri: Uri
private val dialogUtil = DialogUtil(this)
private val fileManager = FileManager(this, this)
@RequiresApi(Build.VERSION_CODES.R)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
@@ -32,8 +45,18 @@ class MainActivity : AppCompatActivity() {
handleActivityResult(result)
}
// Ask for base directory picker on startup
// Ask permission
if(!Environment.isExternalStorageManager()) {
dialogUtil.showMaterialDialog(
"Storage Permission",
"To ensure the app works properly and allows you to easily hide or unhide your private files, please grant storage access permission.\n" +
"\n" +
"For devices running Android 11 or higher, you'll need to grant the 'All Files Access' permission.",
"Grant",
"Cancel",
this
)
}
// Number buttons
setupNumberButton(binding.btn0, "0")
setupNumberButton(binding.btn1, "1")
@@ -60,13 +83,8 @@ class MainActivity : AppCompatActivity() {
binding.cut.setOnClickListener { cutNumbers() }
}
private fun launchBaseDirectoryPicker() {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
launcher.launch(intent)
}
private fun handleActivityResult(result: androidx.activity.result.ActivityResult) {
if (result.resultCode == Activity.RESULT_OK) {
if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri ->
baseDocumentTreeUri = uri
val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
@@ -74,7 +92,7 @@ class MainActivity : AppCompatActivity() {
// Take persistable Uri Permission for future use
contentResolver.takePersistableUriPermission(uri, takeFlags)
val preferences = getSharedPreferences("com.example.fileutility", Context.MODE_PRIVATE)
val preferences = getSharedPreferences("com.example.fileutility", MODE_PRIVATE)
preferences.edit().putString("filestorageuri", uri.toString()).apply()
}
} else {
@@ -126,7 +144,7 @@ class MainActivity : AppCompatActivity() {
currentExpression = (value / 100).toString()
updateDisplay()
} catch (e: Exception) {
binding.display.text = "Error"
binding.display.text = getString(R.string.invalid_message)
}
}
@@ -163,7 +181,7 @@ class MainActivity : AppCompatActivity() {
hasDecimal = currentExpression.contains(".")
updateDisplay()
} catch (e: Exception) {
binding.display.text = "Invalid Value"
binding.display.text = getString(R.string.invalid_message)
}
}
@@ -180,6 +198,29 @@ class MainActivity : AppCompatActivity() {
updateDisplay()
}
override fun onPositiveButtonClicked() {
fileManager.askPermission(this)
}
override fun onNegativeButtonClicked() {
}
override fun onNaturalButtonClicked() {
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == 6767) {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "Permission granted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show()
}
}
}
}

View File

@@ -69,12 +69,11 @@ class PreviewActivity : AppCompatActivity() {
val fileUri = Uri.fromFile(files[currentPosition])
val fileName = FileManager.FileName(this).getFileNameFromUri(fileUri).toString()
binding.title.text = fileName
}
private fun clickListeners() {
binding.delete.setOnClickListener {
val fileUri = FileManager.FileManager().getContentUri(this, files[binding.viewPager.currentItem])
val fileUri = FileManager.FileManager().getContentUri(this, files[binding.viewPager.currentItem], filetype)
if (fileUri != null) {
MaterialAlertDialogBuilder(this)
.setTitle("Delete File")
@@ -94,7 +93,7 @@ class PreviewActivity : AppCompatActivity() {
}
binding.unHide.setOnClickListener {
val fileUri = FileManager.FileManager().getContentUri(this, files[binding.viewPager.currentItem])
val fileUri = FileManager.FileManager().getContentUri(this, files[binding.viewPager.currentItem], filetype)
if (fileUri != null) {
MaterialAlertDialogBuilder(this)
.setTitle("Unhide File")

View File

@@ -2,9 +2,15 @@ package devs.org.calculator.activities
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import devs.org.calculator.databinding.ActivitySetupPasswordBinding
import devs.org.calculator.utils.PrefsUtil
import devs.org.calculator.R
class SetupPasswordActivity : AppCompatActivity() {
private lateinit var binding: ActivitySetupPasswordBinding
@@ -19,19 +25,76 @@ class SetupPasswordActivity : AppCompatActivity() {
binding.btnSavePassword.setOnClickListener {
val password = binding.etPassword.text.toString()
val confirmPassword = binding.etConfirmPassword.text.toString()
val securityQuestion = binding.etSecurityQuestion.text.toString()
val securityAnswer = binding.etSecurityAnswer.text.toString()
if (password == confirmPassword && password.isNotEmpty()) {
if (password.isEmpty()){
binding.etPassword.error = "Enter password"
return@setOnClickListener
}
if (confirmPassword.isEmpty()){
binding.etConfirmPassword.error = "Confirm password"
return@setOnClickListener
}
if (securityQuestion.isEmpty()){
binding.etSecurityQuestion.error = "Enter security question"
return@setOnClickListener
}
if (securityAnswer.isEmpty()){
binding.etSecurityAnswer.error = "Enter security answer"
return@setOnClickListener
}
if (password != confirmPassword) {
binding.etPassword.error = "Passwords don't match"
return@setOnClickListener
}
prefsUtil.savePassword(password)
prefsUtil.saveSecurityQA(securityQuestion, securityAnswer)
Toast.makeText(this, "Password set successfully", Toast.LENGTH_SHORT).show()
startActivity(Intent(this, MainActivity::class.java))
finish()
} else {
binding.etPassword.error = "Passwords don't match"
}
}
binding.btnResetPassword.setOnClickListener {
// Implement password reset logic
// Could use security questions or email verification
if (prefsUtil.getSecurityQuestion() != null) showSecurityQuestionDialog(prefsUtil.getSecurityQuestion().toString())
else Toast.makeText(this, "Security question not set yet.", Toast.LENGTH_SHORT).show()
}
}
private fun showSecurityQuestionDialog(securityQuestion: String) {
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_security_question, null)
val questionTextView: TextView = dialogView.findViewById(R.id.security_question)
questionTextView.text = securityQuestion
MaterialAlertDialogBuilder(this)
.setTitle("Answer the Security Question!")
.setView(dialogView)
.setPositiveButton("Verify") { dialog, _ ->
val inputEditText: TextInputEditText = dialogView.findViewById(R.id.text_input_edit_text)
val userAnswer = inputEditText.text.toString().trim()
if (userAnswer.isEmpty()) {
Toast.makeText(this, "Answer cannot be empty!", Toast.LENGTH_SHORT).show()
} else {
if (prefsUtil.validateSecurityAnswer(userAnswer)){
prefsUtil.resetPassword()
Toast.makeText(this, "Password successfully reset.", Toast.LENGTH_SHORT).show()
dialog.dismiss()
}else {
Toast.makeText(this, "Invalid answer!", Toast.LENGTH_SHORT).show()
}
}
}
.setNegativeButton("Cancel") { dialog, _ ->
dialog.dismiss()
}
.show()
}
}

View File

@@ -7,12 +7,9 @@ import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.lifecycle.lifecycleScope
import devs.org.calculator.activities.ImageGalleryActivity
import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.FileProcessCallback
import kotlinx.coroutines.Dispatchers
import devs.org.calculator.callbacks.FileProcessCallback
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
class VideoGalleryActivity : BaseGalleryActivity(), FileProcessCallback {

View File

@@ -7,6 +7,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.Toast
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.DiffUtil
@@ -33,6 +34,7 @@ class FileAdapter(private val fileType: FileManager.FileType, var context: Conte
private val imageView: ImageView = view.findViewById(R.id.imageView)
fun bind(file: File) {
when (fileType) {
FileManager.FileType.IMAGE -> {
Glide.with(imageView)
@@ -58,6 +60,7 @@ class FileAdapter(private val fileType: FileManager.FileType, var context: Conte
}
itemView.setOnClickListener {
var fileTypes = when(fileType){
FileManager.FileType.IMAGE -> {
@@ -80,38 +83,39 @@ class FileAdapter(private val fileType: FileManager.FileType, var context: Conte
}
itemView.setOnLongClickListener {
val fileUri = FileManager.FileManager().getContentUri(context, file, fileType)
if (fileUri == null) {
Toast.makeText(context, "Unable to access file: $file", Toast.LENGTH_SHORT).show()
return@setOnLongClickListener true
}
val fileUri = FileManager.FileManager().getContentUri(context, file)
val filesName = FileManager.FileName(context).getFileNameFromUri(fileUri!!).toString()
val fileName = FileManager.FileName(context).getFileNameFromUri(fileUri)?.toString() ?: "Unknown File"
MaterialAlertDialogBuilder(context)
.setTitle("Details")
.setMessage("File Name: $filesName\n\nYou can delete or Unhide this file.")
.setMessage("File Name: $fileName\n\nYou can delete or unhide this file.")
.setPositiveButton("Delete") { dialog, _ ->
// Handle positive button click
lifecycleOwner.lifecycleScope.launch {
FileManager(context, context as LifecycleOwner).deletePhotoFromExternalStorage(fileUri)
FileManager(context, lifecycleOwner).deletePhotoFromExternalStorage(fileUri)
}
val currentList = currentList.toMutableList()
currentList.remove(file)
submitList(currentList)
dialog.dismiss()
}
.setNegativeButton("Unhide") { dialog, _ ->
// Handle negative button click
FileManager(context, context as LifecycleOwner).copyFileToNormalDir(fileUri)
FileManager(context, lifecycleOwner).copyFileToNormalDir(fileUri)
val currentList = currentList.toMutableList()
currentList.remove(file)
submitList(currentList)
dialog.dismiss()
dialog.dismiss()
}
.show()
return@setOnLongClickListener true
}
}
}
@@ -137,4 +141,6 @@ class FileAdapter(private val fileType: FileManager.FileType, var context: Conte
return oldItem == newItem
}
}
}

View File

@@ -0,0 +1,7 @@
package devs.org.calculator.callbacks
interface DialogActionsCallback {
fun onPositiveButtonClicked()
fun onNegativeButtonClicked()
fun onNaturalButtonClicked()
}

View File

@@ -1,4 +1,4 @@
package devs.org.calculator.utils
package devs.org.calculator.callbacks
import java.io.File

View File

@@ -10,29 +10,36 @@ import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import devs.org.calculator.callbacks.DialogActionsCallback
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class DialogUtil(private val context: Context, private var lifecycleOwner: LifecycleOwner) {
class DialogUtil(private val context: Context) {
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
fun showMaterialDialog(
fun showMaterialDialogWithNaturalButton(
title: String,
message: String,
positiveButton: String,
negativeButton: String,
neutralButton: String,
callback: DialogActionsCallback
) {
MaterialAlertDialogBuilder(context)
.setTitle(title)
.setMessage(message)
.setPositiveButton(positiveButton) { dialog, _ ->
// Handle positive button click
callback.onPositiveButtonClicked()
dialog.dismiss()
}
.setNegativeButton(negativeButton) { dialog, _ ->
// Handle negative button click
callback.onNegativeButtonClicked()
dialog.dismiss()
}
.setNeutralButton(neutralButton) { dialog, _ ->
callback.onNaturalButtonClicked()
dialog.dismiss()
}
.show()
@@ -42,80 +49,20 @@ class DialogUtil(private val context: Context, private var lifecycleOwner: Lifec
message: String,
positiveButton: String,
negativeButton: String,
uri: Uri
callback: DialogActionsCallback
) {
MaterialAlertDialogBuilder(context)
.setTitle(title)
.setMessage(message)
.setPositiveButton(positiveButton) { dialog, _ ->
// Handle positive button click
if (positiveButton == "Delete") {
lifecycleOwner.lifecycleScope.launch {
deletePhotoFromExternalStorage(uri)
}
}else{
// copy file to a visible directory
}
callback.onPositiveButtonClicked()
}
.setNegativeButton(negativeButton) { dialog, _ ->
// Handle negative button click
callback.onNegativeButtonClicked()
dialog.dismiss()
}
.show()
}
suspend fun deletePhotoFromExternalStorage(photoUri: Uri) {
withContext(Dispatchers.IO) {
try {
// First try to delete using DocumentFile
val documentFile = DocumentFile.fromSingleUri(context, photoUri)
if (documentFile?.exists() == true && documentFile.canWrite()) {
val deleted = documentFile.delete()
withContext(Dispatchers.Main) {
if (deleted) {
// Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(context, "Failed to delete file", Toast.LENGTH_SHORT).show()
}
}
return@withContext
}
// If DocumentFile approach fails, try content resolver
try {
context.contentResolver.delete(photoUri, null, null)
withContext(Dispatchers.Main) {
Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show()
}
} catch (e: SecurityException) {
// Handle security exception for Android 10 and above
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val intentSender = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.R -> {
MediaStore.createDeleteRequest(context.contentResolver, listOf(photoUri)).intentSender
}
else -> {
val recoverableSecurityException = e as? RecoverableSecurityException
recoverableSecurityException?.userAction?.actionIntent?.intentSender
}
}
intentSender?.let { sender ->
intentSenderLauncher.launch(
IntentSenderRequest.Builder(sender).build()
)
}
}
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
Toast.makeText(
context,
"Error deleting file: ${e.message}",
Toast.LENGTH_LONG
).show()
}
}
}
}
}

View File

@@ -1,31 +1,33 @@
package devs.org.calculator.utils
import android.app.Activity
import android.app.RecoverableSecurityException
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.ContactsContract
import android.provider.ContactsContract.Directory
import android.provider.DocumentsContract
import android.provider.MediaStore
import android.provider.Settings
import android.webkit.MimeTypeMap
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.core.app.ActivityCompat
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.lifecycleScope
import devs.org.calculator.activities.VideoGalleryActivity
import devs.org.calculator.adapters.FileAdapter
import devs.org.calculator.callbacks.FileProcessCallback
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.File
import android.Manifest
class FileManager(private val context: Context, private val lifecycleOwner: LifecycleOwner) {
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
val intent = Intent()
companion object {
const val HIDDEN_DIR = ".CalculatorHide"
@@ -222,7 +224,9 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
}
class FileManager(){
fun getContentUri(context: Context, file: File): Uri? {
fun getContentUri(context: Context, file: File, fileType: FileType): Uri? {
when(fileType){
FileType.IMAGE -> {
val projection = arrayOf(MediaStore.MediaColumns._ID)
val selection = "${MediaStore.MediaColumns.DATA} = ?"
val selectionArgs = arrayOf(file.absolutePath)
@@ -236,7 +240,83 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
}
return null
}
FileType.VIDEO -> {
val projection = arrayOf(MediaStore.Video.Media._ID)
val selection = "${MediaStore.Video.Media.DATA} = ?"
val selectionArgs = arrayOf(file.absolutePath)
val queryUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI
context.contentResolver.query(queryUri, projection, selection, selectionArgs, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID))
return Uri.withAppendedPath(queryUri, id.toString())
}
}
return null
}
FileType.AUDIO -> {
val projection = arrayOf(MediaStore.Audio.Media._ID)
val selection = "${MediaStore.Audio.Media.DATA} = ?"
val selectionArgs = arrayOf(file.absolutePath)
val queryUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
context.contentResolver.query(queryUri, projection, selection, selectionArgs, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID))
return Uri.withAppendedPath(queryUri, id.toString())
}
}
return null
}
FileType.DOCUMENT -> {
val projection = arrayOf(MediaStore.Files.FileColumns._ID)
val selection = "${MediaStore.Files.FileColumns.DATA} = ?"
val selectionArgs = arrayOf(file.absolutePath)
val queryUri = MediaStore.Files.getContentUri("external")
context.contentResolver.query(queryUri, projection, selection, selectionArgs, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID))
return Uri.withAppendedPath(queryUri, id.toString())
}
}
return null
}
}
}
}
fun askPermission(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
if (!Environment.isExternalStorageManager()) {
val intent = Intent(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
try {
activity.startActivity(intent)
} catch (e: ActivityNotFoundException) {
Toast.makeText(activity, "Unable to open settings. Please grant permission manually.", Toast.LENGTH_SHORT).show()
}
} else {
Toast.makeText(activity, "Permission already granted", Toast.LENGTH_SHORT).show()
}
} else {
// For Android 10 and below
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE),
6767
)
}
}
suspend fun processMultipleFiles(

View File

@@ -18,6 +18,14 @@ class PrefsUtil(context: Context) {
.apply()
}
fun resetPassword(){
prefs.edit()
.remove("password")
.remove("security_question")
.remove("security_answer")
.apply()
}
fun validatePassword(input: String): Boolean {
val stored = prefs.getString("password", "") ?: ""
return stored == hashPassword(input)

View File

@@ -12,260 +12,283 @@
<TextView
android:id="@+id/display"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="160dp"
android:layout_margin="16dp"
android:layout_weight="2"
android:layout_weight="3"
android:layout_marginTop="0dp"
android:background="#00000000"
android:elevation="4dp"
android:gravity="end|bottom"
android:padding="10dp"
android:text="0"
android:textSize="48sp"
android:autoSizeTextType="uniform"
android:autoSizeMinTextSize="20sp"
android:autoSizeMaxTextSize="48sp"
android:autoSizeStepGranularity="2sp"
tools:ignore="Suspicious0dp" />
<!-- Calculator Buttons -->
<GridLayout
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:columnCount="4"
android:layout_weight="5"
android:layout_weight="4"
android:rowCount="5">
<!-- Row 1 -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btnClear"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="4dp"
android:textSize="30sp"
android:text="C"
app:layout_constraintDimensionRatio="1:1"
app:cornerRadius="15dp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPercent"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:textSize="30sp"
android:layout_margin="4dp"
android:text="%"
app:cornerRadius="15dp"
app:layout_constraintDimensionRatio="1:1"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDivide"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:textSize="30sp"
android:layout_margin="4dp"
android:text="÷"
app:cornerRadius="15dp"
app:layout_constraintDimensionRatio="1:1"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/cut"
android:layout_height="0dp"
android:layout_width="0dp"
android:layout_height="70dp"
android:layout_width="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:textSize="30sp"
android:layout_margin="4dp"
app:icon="@drawable/backspace"
app:layout_constraintDimensionRatio="1:1"
app:iconSize="30dp"
app:cornerRadius="15dp"/>
<!-- Row 2 -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn7"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:textSize="30sp"
android:layout_margin="4dp"
android:text="7"
app:layout_constraintDimensionRatio="1:1"
app:cornerRadius="15dp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn8"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:textSize="30sp"
android:layout_margin="4dp"
android:text="8"
app:layout_constraintDimensionRatio="1:1"
app:cornerRadius="15dp"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn9"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="4dp"
android:textSize="30sp"
android:text="9"
app:cornerRadius="15dp"
app:layout_constraintDimensionRatio="1:1"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnMultiply"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:layout_margin="4dp"
android:textSize="30sp"
android:text="×"
app:cornerRadius="15dp"
app:layout_constraintDimensionRatio="1:1"
style="@style/Widget.MaterialComponents.Button"/>
<!-- Row 3 -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn4"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:textSize="30sp"
android:layout_margin="4dp"
app:cornerRadius="15dp"
android:text="4"
app:layout_constraintDimensionRatio="1:1"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn5"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:textSize="30sp"
android:layout_margin="4dp"
android:text="5"
app:cornerRadius="15dp"
app:layout_constraintDimensionRatio="1:1"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn6"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:textSize="30sp"
android:layout_margin="4dp"
android:text="6"
app:cornerRadius="15dp"
app:layout_constraintDimensionRatio="1:1"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnMinus"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:textSize="30sp"
android:layout_margin="4dp"
android:text="-"
app:cornerRadius="15dp"
app:layout_constraintDimensionRatio="1:1"
style="@style/Widget.MaterialComponents.Button"/>
<!-- Row 4 -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn1"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:textSize="30sp"
android:layout_margin="4dp"
android:text="1"
app:cornerRadius="15dp"
app:layout_constraintDimensionRatio="1:1"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn2"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:textSize="30sp"
android:layout_columnWeight="1"
android:layout_margin="4dp"
android:text="2"
app:cornerRadius="15dp"
app:layout_constraintDimensionRatio="1:1"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btn3"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:textSize="30sp"
android:layout_columnWeight="1"
android:layout_margin="4dp"
android:text="3"
app:cornerRadius="15dp"
app:layout_constraintDimensionRatio="1:1"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnPlus"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:textSize="30sp"
android:layout_margin="4dp"
android:text="+"
app:cornerRadius="15dp"
app:layout_constraintDimensionRatio="1:1"
style="@style/Widget.MaterialComponents.Button"/>
<!-- Row 5 -->
<com.google.android.material.button.MaterialButton
android:id="@+id/btn0"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnSpan="2"
android:textSize="30sp"
android:layout_columnWeight="2"
android:layout_margin="4dp"
app:cornerRadius="15dp"
app:layout_constraintDimensionRatio="1:1"
android:text="0" />
<com.google.android.material.button.MaterialButton
android:id="@+id/btnDot"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:layout_columnWeight="1"
android:textSize="30sp"
android:layout_margin="4dp"
android:text="."
app:cornerRadius="15dp"
app:layout_constraintDimensionRatio="1:1"
style="@style/Widget.MaterialComponents.Button.OutlinedButton"/>
<com.google.android.material.button.MaterialButton
android:id="@+id/btnEquals"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_rowWeight="1"
android:textSize="30sp"
android:layout_columnWeight="1"
android:layout_margin="4dp"
android:text="="
app:cornerRadius="15dp"
app:layout_constraintDimensionRatio="1:1"
style="@style/Widget.MaterialComponents.Button"/>
</GridLayout>

View File

@@ -0,0 +1,31 @@
<?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="wrap_content"
android:orientation="vertical"
android:padding="16dp">
<!-- Security Question -->
<TextView
android:id="@+id/security_question"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Security Question"
android:textSize="16sp"
android:layout_marginLeft="10dp"
android:layout_marginBottom="8dp" />
<!-- User Answer Input -->
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/text_input_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Your Answer">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/text_input_edit_text"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
</com.google.android.material.textfield.TextInputLayout>
</LinearLayout>

View File

@@ -1,3 +1,4 @@
<resources>
<string name="app_name">Calculator</string>
<string name="invalid_message">Invalid Value Entered</string>
</resources>