diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d47700b..dad983e 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -11,8 +11,8 @@ android { applicationId = "devs.org.calculator" minSdk = 26 targetSdk = 34 - versionCode = 1 - versionName = "1.0" + versionCode = 2 + versionName = "1.1" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } @@ -45,15 +45,16 @@ dependencies { implementation(libs.material) implementation(libs.androidx.activity) implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.gridlayout) testImplementation(libs.junit) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso.core) //custom dependencies implementation(libs.exp4j) - implementation("com.github.bumptech.glide:glide:4.16.0") - implementation("androidx.documentfile:documentfile:1.0.1") - implementation("com.github.chrisbanes:PhotoView:2.3.0") - implementation("androidx.viewpager:viewpager:1.0.0") - implementation("com.jsibbold:zoomage:1.3.1") + implementation(libs.glide) + implementation(libs.androidx.documentfile) + implementation(libs.photoview) + implementation(libs.androidx.viewpager) + implementation(libs.zoomage) } \ No newline at end of file diff --git a/app/src/main/java/devs/org/calculator/activities/AudioGalleryActivity.kt b/app/src/main/java/devs/org/calculator/activities/AudioGalleryActivity.kt index 74d4ff8..b00239f 100644 --- a/app/src/main/java/devs/org/calculator/activities/AudioGalleryActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/AudioGalleryActivity.kt @@ -78,7 +78,7 @@ class AudioGalleryActivity : BaseGalleryActivity(), FileProcessCallback { } override fun openPreview() { - // Implement audio preview + // Not implemented audio preview } diff --git a/app/src/main/java/devs/org/calculator/activities/BaseGalleryActivity.kt b/app/src/main/java/devs/org/calculator/activities/BaseGalleryActivity.kt index 671df0b..93e76ca 100644 --- a/app/src/main/java/devs/org/calculator/activities/BaseGalleryActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/BaseGalleryActivity.kt @@ -13,6 +13,7 @@ import androidx.activity.result.IntentSenderRequest import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.recyclerview.widget.GridLayoutManager +import devs.org.calculator.R import devs.org.calculator.adapters.FileAdapter import devs.org.calculator.databinding.ActivityGalleryBinding import devs.org.calculator.utils.FileManager @@ -20,9 +21,9 @@ import java.io.File abstract class BaseGalleryActivity : AppCompatActivity() { protected lateinit var binding: ActivityGalleryBinding - protected lateinit var fileManager: FileManager - protected lateinit var adapter: FileAdapter - protected lateinit var files: List + private lateinit var fileManager: FileManager + private lateinit var adapter: FileAdapter + private lateinit var files: List private lateinit var intentSenderLauncher: ActivityResultLauncher private val storagePermissionLauncher = registerForActivityResult( @@ -48,16 +49,16 @@ abstract class BaseGalleryActivity : AppCompatActivity() { binding.fabAdd.text = when(fileType){ FileManager.FileType.IMAGE -> { - "Add Image" + getString(R.string.add_image) } FileManager.FileType.AUDIO -> { - "Add Audio" + getString(R.string.add_audio) } FileManager.FileType.VIDEO -> { - "Add Video" + getString(R.string.add_video) } FileManager.FileType.DOCUMENT -> { - "Add Files" + getString(R.string.add_files) } } binding.recyclerView.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY -> @@ -128,6 +129,7 @@ abstract class BaseGalleryActivity : AppCompatActivity() { // permission denied } + @Deprecated("This method has been deprecated in favor of using the Activity Result API\n which brings increased type safety via an {@link ActivityResultContract} and the prebuilt\n contracts for common intents available in\n {@link androidx.activity.result.contract.ActivityResultContracts}, provides hooks for\n testing, and allow receiving results in separate, testable classes independent from your\n activity. Use\n {@link #registerForActivityResult(ActivityResultContract, ActivityResultCallback)}\n with the appropriate {@link ActivityResultContract} and handling the result in the\n {@link ActivityResultCallback#onActivityResult(Object) callback}.") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (requestCode == 2296 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { diff --git a/app/src/main/java/devs/org/calculator/activities/DocumentsActivity.kt b/app/src/main/java/devs/org/calculator/activities/DocumentsActivity.kt index 24eac74..d48aa3d 100644 --- a/app/src/main/java/devs/org/calculator/activities/DocumentsActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/DocumentsActivity.kt @@ -7,6 +7,7 @@ import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.lifecycle.lifecycleScope +import devs.org.calculator.R import devs.org.calculator.utils.FileManager import devs.org.calculator.callbacks.FileProcessCallback import kotlinx.coroutines.launch @@ -15,7 +16,6 @@ import java.io.File class DocumentsActivity : BaseGalleryActivity(), FileProcessCallback { override val fileType = FileManager.FileType.DOCUMENT private lateinit var pickLauncher: ActivityResultLauncher - private var selectedUri: Uri? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -40,19 +40,21 @@ class DocumentsActivity : BaseGalleryActivity(), FileProcessCallback { FileManager(this@DocumentsActivity, this@DocumentsActivity).processMultipleFiles(uriList, fileType,this@DocumentsActivity ) } } else { - Toast.makeText(this, "No files selected", Toast.LENGTH_SHORT).show() + Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show() } } } } override fun onFilesProcessedSuccessfully(copiedFiles: List) { - Toast.makeText(this@DocumentsActivity, "${copiedFiles.size} Documents hidden successfully", Toast.LENGTH_SHORT).show() + Toast.makeText(this@DocumentsActivity,copiedFiles.size.toString() + + getString(R.string.documents_hidden_successfully ), Toast.LENGTH_SHORT).show() loadFiles() } override fun onFileProcessFailed() { - Toast.makeText(this@DocumentsActivity, "Failed to hide Documents", Toast.LENGTH_SHORT).show() + Toast.makeText(this@DocumentsActivity, + getString(R.string.failed_to_hide_documents), Toast.LENGTH_SHORT).show() } private fun setupFabButton() { @@ -70,6 +72,6 @@ class DocumentsActivity : BaseGalleryActivity(), FileProcessCallback { } override fun openPreview() { - // Implement document preview + //Not implemented document preview } } \ No newline at end of file diff --git a/app/src/main/java/devs/org/calculator/activities/ImageGalleryActivity.kt b/app/src/main/java/devs/org/calculator/activities/ImageGalleryActivity.kt index 2f4b3d8..695feff 100644 --- a/app/src/main/java/devs/org/calculator/activities/ImageGalleryActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/ImageGalleryActivity.kt @@ -18,6 +18,7 @@ import devs.org.calculator.utils.FileManager import kotlinx.coroutines.launch import java.io.File import android.Manifest +import devs.org.calculator.R import devs.org.calculator.callbacks.FileProcessCallback class ImageGalleryActivity : BaseGalleryActivity(), FileProcessCallback { @@ -33,7 +34,8 @@ class ImageGalleryActivity : BaseGalleryActivity(), FileProcessCallback { setupFabButton() intentSenderLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()){ - if (it.resultCode != RESULT_OK) Toast.makeText(this, "Failed to hide/unhide photo", Toast.LENGTH_SHORT).show() + if (it.resultCode != RESULT_OK) Toast.makeText(this, + getString(R.string.failed_to_hide_unhide_photo), Toast.LENGTH_SHORT).show() } pickImageLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> @@ -56,7 +58,7 @@ class ImageGalleryActivity : BaseGalleryActivity(), FileProcessCallback { .processMultipleFiles(uriList, fileType,this@ImageGalleryActivity ) } } else { - Toast.makeText(this, "No files selected", Toast.LENGTH_SHORT).show() + Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show() } } } @@ -64,12 +66,14 @@ class ImageGalleryActivity : BaseGalleryActivity(), FileProcessCallback { } override fun onFilesProcessedSuccessfully(copiedFiles: List) { - Toast.makeText(this@ImageGalleryActivity, "${copiedFiles.size} Images hidden successfully", Toast.LENGTH_SHORT).show() + Toast.makeText(this@ImageGalleryActivity, copiedFiles.size.toString() + + getString(R.string.images_hidden_successfully), Toast.LENGTH_SHORT).show() loadFiles() } override fun onFileProcessFailed() { - Toast.makeText(this@ImageGalleryActivity, "Failed to hide images", Toast.LENGTH_SHORT).show() + Toast.makeText(this@ImageGalleryActivity, + getString(R.string.failed_to_hide_images), Toast.LENGTH_SHORT).show() } private fun setupIntentSenderLauncher() { @@ -125,9 +129,11 @@ class ImageGalleryActivity : BaseGalleryActivity(), FileProcessCallback { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == STORAGE_PERMISSION_CODE) { if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - Toast.makeText(this, "Storage permissions granted", Toast.LENGTH_SHORT).show() + Toast.makeText(this, + getString(R.string.storage_permissions_granted), Toast.LENGTH_SHORT).show() } else { - Toast.makeText(this, "Storage permissions denied", Toast.LENGTH_SHORT).show() + Toast.makeText(this, + getString(R.string.storage_permissions_denied), Toast.LENGTH_SHORT).show() } } } diff --git a/app/src/main/java/devs/org/calculator/activities/MainActivity.kt b/app/src/main/java/devs/org/calculator/activities/MainActivity.kt index 81888f4..beedc3b 100644 --- a/app/src/main/java/devs/org/calculator/activities/MainActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/MainActivity.kt @@ -1,5 +1,6 @@ package devs.org.calculator.activities +import android.annotation.SuppressLint import android.content.Intent import android.content.pm.PackageManager import android.net.Uri @@ -20,12 +21,14 @@ import devs.org.calculator.utils.DialogUtil import devs.org.calculator.utils.FileManager import devs.org.calculator.utils.PrefsUtil import net.objecthunter.exp4j.ExpressionBuilder +import java.util.regex.Pattern class MainActivity : AppCompatActivity(), DialogActionsCallback { private lateinit var binding: ActivityMainBinding private var currentExpression = "0" private var lastWasOperator = false private var hasDecimal = false + private var lastWasPercent = false private lateinit var launcher: ActivityResultLauncher private lateinit var baseDocumentTreeUri: Uri private val dialogUtil = DialogUtil(this) @@ -71,7 +74,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback { binding.btnClear.setOnClickListener { clearDisplay() } binding.btnDot.setOnClickListener { addDecimal() } binding.btnEquals.setOnClickListener { calculateResult() } - binding.btnPercent.setOnClickListener { calculatePercentage() } + binding.btnPercent.setOnClickListener { addPercentage() } binding.cut.setOnClickListener { cutNumbers() } } @@ -98,41 +101,57 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback { currentExpression += number } lastWasOperator = false + lastWasPercent = false updateDisplay() } } private fun setupOperatorButton(button: MaterialButton, operator: String) { button.setOnClickListener { - if (!lastWasOperator) { + if (lastWasOperator) { + currentExpression = currentExpression.substring(0, currentExpression.length - 1) + + when (operator) { + "×" -> "*" + else -> operator + } + } else if (!lastWasPercent) { currentExpression += when (operator) { "×" -> "*" else -> operator } lastWasOperator = true + lastWasPercent = false hasDecimal = false } updateDisplay() } } - private fun clearDisplay() { currentExpression = "0" binding.total.text = "" lastWasOperator = false + lastWasPercent = false hasDecimal = false updateDisplay() } private fun addDecimal() { - if (!hasDecimal && !lastWasOperator) { + if (!hasDecimal && !lastWasOperator && !lastWasPercent) { currentExpression += "." hasDecimal = true updateDisplay() } } + private fun addPercentage() { + if (!lastWasOperator && !lastWasPercent) { + currentExpression += "%" + lastWasPercent = true + updateDisplay() + } + } + private fun calculatePercentage() { try { val value = currentExpression.toDouble() @@ -143,6 +162,126 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback { } } + private fun preprocessExpression(expression: String): String { + val percentagePattern = Pattern.compile("(\\d+\\.?\\d*)%") + val operatorPercentPattern = Pattern.compile("([+\\-*/])(\\d+\\.?\\d*)%") + + var processedExpression = expression + + // Replace standalone percentages (like "50%") with their decimal form (0.5) + val matcher = percentagePattern.matcher(processedExpression) + while (matcher.find()) { + val fullMatch = matcher.group(0) + val number = matcher.group(1) + + // Check if it's a standalone percentage or part of an operation + val start = matcher.start() + if (start == 0 || !isOperator(processedExpression[start-1].toString())) { + val percentageValue = number.toDouble() / 100 + processedExpression = processedExpression.replace(fullMatch, percentageValue.toString()) + } + } + + // Handle operator-percentage combinations (like "100-20%") + val opMatcher = operatorPercentPattern.matcher(processedExpression) + val sb = StringBuilder(processedExpression) + + // We need to process matches from right to left to maintain indices + val matches = mutableListOf>() + + while (opMatcher.find()) { + val operator = opMatcher.group(1) + val percentValue = opMatcher.group(2)!!.toDouble() + val start = opMatcher.start() + val end = opMatcher.end() + + matches.add(Triple(start, end, "$operator$percentValue%")) + } + + // Process matches from right to left + for (match in matches.reversed()) { + val (start, end, fullMatch) = match + + // Find the number before this operator + var leftNumberEnd = start + var leftNumberStart = start - 1 + + // Skip parentheses and move to the actual number + if (leftNumberStart >= 0 && sb[leftNumberStart] == ')') { + var openParens = 1 + leftNumberStart-- + + while (leftNumberStart >= 0 && openParens > 0) { + if (sb[leftNumberStart] == ')') openParens++ + else if (sb[leftNumberStart] == '(') openParens-- + leftNumberStart-- + } + + // Now we need to find the start of the expression + if (leftNumberStart >= 0) { + while (leftNumberStart >= 0 && (isDigit(sb[leftNumberStart].toString()) || sb[leftNumberStart] == '.' || sb[leftNumberStart] == '-')) { + leftNumberStart-- + } + leftNumberStart++ + } else { + leftNumberStart = 0 + } + } else { + // For simple numbers, just find the start of the number + while (leftNumberStart >= 0 && (isDigit(sb[leftNumberStart].toString()) || sb[leftNumberStart] == '.')) { + leftNumberStart-- + } + leftNumberStart++ + } + + if (leftNumberStart < leftNumberEnd) { + val leftPart = sb.substring(leftNumberStart, leftNumberEnd) + + try { + // Extract the numerical values + val baseNumber = evaluateExpression(leftPart) + val operator = fullMatch.substring(0, 1) + val percentNumber = fullMatch.substring(1, fullMatch.length - 1).toDouble() + + // Calculate the percentage of the base number + val percentValue = baseNumber * (percentNumber / 100) + + // Calculate the new value based on the operator + val newValue = when (operator) { + "+" -> baseNumber + percentValue + "-" -> baseNumber - percentValue + "*" -> baseNumber * (percentNumber / 100) + "/" -> baseNumber / (percentNumber / 100) + else -> baseNumber + } + + // Replace the entire expression "number operator percent%" with the result + sb.replace(leftNumberStart, end, newValue.toString()) + } catch (e: Exception) { + Log.e("Calculator", "Error processing percentage expression: $e") + } + } + } + + return sb.toString() + } + + private fun isOperator(char: String): Boolean { + return char == "+" || char == "-" || char == "*" || char == "/" + } + + private fun isDigit(char: String): Boolean { + return char.matches(Regex("[0-9]")) + } + + private fun evaluateExpression(expression: String): Double { + return try { + ExpressionBuilder(expression).build().evaluate() + } catch (e: Exception) { + expression.toDouble() + } + } + private fun calculateResult() { if (currentExpression == "123456") { val intent = Intent(this, SetupPasswordActivity::class.java) @@ -161,8 +300,15 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback { } try { - currentExpression = currentExpression.replace("×", "*") - val expression = ExpressionBuilder(currentExpression).build() + // Replace '×' with '*' for the expression evaluator + var processedExpression = currentExpression.replace("×", "*") + + // Process percentages in the expression + if (processedExpression.contains("%")) { + processedExpression = preprocessExpression(processedExpression) + } + + val expression = ExpressionBuilder(processedExpression).build() val result = expression.evaluate() currentExpression = if (result.toLong().toDouble() == result) { @@ -172,16 +318,17 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback { } lastWasOperator = false + lastWasPercent = false hasDecimal = currentExpression.contains(".") updateDisplay() binding.total.text = "" } catch (e: Exception) { - binding.display.text = getString(R.string.invalid_message) + e.printStackTrace() } } - + @SuppressLint("DefaultLocale") private fun updateDisplay() { binding.display.text = currentExpression.replace("*", "×") @@ -191,8 +338,24 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback { } try { - val expression = ExpressionBuilder(currentExpression).build() + // Don't show preview result if the expression ends with an operator + // (but allow percentage at the end) + if (currentExpression.isEmpty() || + (isOperator(currentExpression.last().toString()) && currentExpression.last() != '%')) { + binding.total.text = "" + return + } + + // Process the expression for preview calculation + var processedExpression = currentExpression.replace("×", "*") + + if (processedExpression.contains("%")) { + processedExpression = preprocessExpression(processedExpression) + } + + val expression = ExpressionBuilder(processedExpression).build() val result = expression.evaluate() + val formattedResult = if (result.toLong().toDouble() == result) { result.toLong().toString() } else { @@ -205,16 +368,27 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback { } } - private fun cutNumbers() { if (currentExpression.isNotEmpty()){ if (currentExpression.length == 1){ - currentExpression = currentExpression.substring(0, currentExpression.length - 1) currentExpression = "0" - }else currentExpression = currentExpression.substring(0, currentExpression.length - 1) - }else currentExpression = "0" - updateDisplay() + } else { + val lastChar = currentExpression.last() + currentExpression = currentExpression.substring(0, currentExpression.length - 1) + // Update flags based on what was removed + if (lastChar == '%') { + lastWasPercent = false + } else if (isOperator(lastChar.toString())) { + lastWasOperator = false + } else if (lastChar == '.') { + hasDecimal = false + } + } + } else { + currentExpression = "0" + } + updateDisplay() } override fun onPositiveButtonClicked() { @@ -239,6 +413,4 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback { } } } -} - - +} \ No newline at end of file diff --git a/app/src/main/java/devs/org/calculator/activities/PreviewActivity.kt b/app/src/main/java/devs/org/calculator/activities/PreviewActivity.kt index e6ba208..61879d6 100644 --- a/app/src/main/java/devs/org/calculator/activities/PreviewActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/PreviewActivity.kt @@ -4,8 +4,8 @@ import android.net.Uri import android.os.Bundle import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope -import androidx.viewpager.widget.ViewPager import androidx.viewpager2.widget.ViewPager2 +import devs.org.calculator.R import devs.org.calculator.adapters.ImagePreviewAdapter import devs.org.calculator.callbacks.DialogActionsCallback import devs.org.calculator.databinding.ActivityPreviewBinding @@ -13,7 +13,6 @@ import devs.org.calculator.utils.DialogUtil import devs.org.calculator.utils.FileManager import kotlinx.coroutines.launch import java.io.File -import devs.org.calculator.R class PreviewActivity : AppCompatActivity() { @@ -55,19 +54,19 @@ class PreviewActivity : AppCompatActivity() { when (type) { "IMAGE" -> { filetype = FileManager.FileType.IMAGE - binding.title.text = "Preview Images" + binding.title.text = getString(R.string.preview_images) } "VIDEO" -> { filetype = FileManager.FileType.VIDEO - binding.title.text = "Preview Videos" + binding.title.text = getString(R.string.preview_videos) } "AUDIO" -> { filetype = FileManager.FileType.AUDIO - binding.title.text = "Preview Audios" + binding.title.text = getString(R.string.preview_audios) } else -> { filetype = FileManager.FileType.DOCUMENT - binding.title.text = "Preview Documents" + binding.title.text = getString(R.string.preview_documents) } } } @@ -107,10 +106,10 @@ class PreviewActivity : AppCompatActivity() { val fileUri = FileManager.FileManager().getContentUriImage(this, files[binding.viewPager.currentItem], filetype) if (fileUri != null) { dialogUtil.showMaterialDialog( - "Delete File", - "Are you sure to Delete this file permanently?", - "Delete Permanently", - "Cancel", + getString(R.string.delete_file), + getString(R.string.are_you_sure_to_delete_this_file_permanently), + getString(R.string.delete_permanently), + getString(R.string.cancel), object : DialogActionsCallback{ override fun onPositiveButtonClicked() { lifecycleScope.launch { @@ -136,10 +135,10 @@ class PreviewActivity : AppCompatActivity() { val fileUri = FileManager.FileManager().getContentUriImage(this, files[binding.viewPager.currentItem], filetype) if (fileUri != null) { dialogUtil.showMaterialDialog( - "Unhide File", - "Are you sure you want to Unhide this file?", - "Unhide", - "Cancel", + getString(R.string.un_hide_file), + getString(R.string.are_you_sure_you_want_to_un_hide_this_file), + getString(R.string.un_hide), + getString(R.string.cancel), object : DialogActionsCallback{ override fun onPositiveButtonClicked() { lifecycleScope.launch { @@ -168,7 +167,7 @@ class PreviewActivity : AppCompatActivity() { adapter.images = updatedFiles // Update adapter with the new list // Update the ViewPager's position - if (!updatedFiles.isNotEmpty()) finish() + if (updatedFiles.isEmpty()) finish() } diff --git a/app/src/main/java/devs/org/calculator/activities/SetupPasswordActivity.kt b/app/src/main/java/devs/org/calculator/activities/SetupPasswordActivity.kt index 5d6a070..431068e 100644 --- a/app/src/main/java/devs/org/calculator/activities/SetupPasswordActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/SetupPasswordActivity.kt @@ -45,28 +45,28 @@ class SetupPasswordActivity : AppCompatActivity() { val securityAnswer = binding.etSecurityAnswer.text.toString() if (password.isEmpty()){ - binding.etPassword.error = "Enter password" + binding.etPassword.error = getString(R.string.enter_password) return@setOnClickListener } if (confirmPassword.isEmpty()){ - binding.etConfirmPassword.error = "Confirm password" + binding.etConfirmPassword.error = getString(R.string.confirm_password) return@setOnClickListener } if (securityQuestion.isEmpty()){ - binding.etSecurityQuestion.error = "Enter security question" + binding.etSecurityQuestion.error = getString(R.string.enter_security_question) return@setOnClickListener } if (securityAnswer.isEmpty()){ - binding.etSecurityAnswer.error = "Enter security answer" + binding.etSecurityAnswer.error = getString(R.string.enter_security_answer) return@setOnClickListener } if (password != confirmPassword) { - binding.etPassword.error = "Passwords don't match" + binding.etPassword.error = getString(R.string.passwords_don_t_match) return@setOnClickListener } prefsUtil.savePassword(password) prefsUtil.saveSecurityQA(securityQuestion, securityAnswer) - Toast.makeText(this, "Password set successfully", Toast.LENGTH_SHORT).show() + Toast.makeText(this, getString(R.string.password_set_successfully), Toast.LENGTH_SHORT).show() startActivity(Intent(this, MainActivity::class.java)) finish() } @@ -75,40 +75,43 @@ class SetupPasswordActivity : AppCompatActivity() { // 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() + else Toast.makeText(this, + getString(R.string.security_question_not_set_yet), Toast.LENGTH_SHORT).show() } binding2.btnChangePassword.setOnClickListener{ val oldPassword = binding2.etOldPassword.text.toString() val newPassword = binding2.etNewPassword.text.toString() if (oldPassword.isEmpty()) { - binding2.etOldPassword.error = "This field can't be empty" + binding2.etOldPassword.error = getString(R.string.this_field_can_t_be_empty) return@setOnClickListener } if (newPassword.isEmpty()) { - binding2.etNewPassword.error = "This field can't be empty" + binding2.etNewPassword.error = getString(R.string.this_field_can_t_be_empty) return@setOnClickListener } if (prefsUtil.validatePassword(oldPassword)){ if (oldPassword != newPassword){ prefsUtil.savePassword(newPassword) - Toast.makeText(this, "Password reset successfully", Toast.LENGTH_SHORT).show() + Toast.makeText(this, + getString(R.string.password_reset_successfully), Toast.LENGTH_SHORT).show() startActivity(Intent(this, MainActivity::class.java)) finish() }else { - Toast.makeText(this, "Old Password And New Password Not Be Same", Toast.LENGTH_SHORT).show() - binding2.etNewPassword.error = "Old Password And New Password Not Be Same" + Toast.makeText(this, + getString(R.string.old_password_and_new_password_not_be_same), Toast.LENGTH_SHORT).show() + binding2.etNewPassword.error = getString(R.string.old_password_and_new_password_not_be_same) } }else { - Toast.makeText(this, "Wrong password entered", Toast.LENGTH_SHORT).show() - binding2.etOldPassword.error = "Old Password Not Matching" + Toast.makeText(this, getString(R.string.wrong_password_entered), Toast.LENGTH_SHORT).show() + binding2.etOldPassword.error = getString(R.string.old_password_not_matching) } } binding2.btnResetPassword.setOnClickListener{ if (prefsUtil.getSecurityQuestion() != null) showSecurityQuestionDialog(prefsUtil.getSecurityQuestion().toString()) - else Toast.makeText(this, "Security question not set yet.", Toast.LENGTH_SHORT).show() + else Toast.makeText(this, getString(R.string.this_field_can_t_be_empty), Toast.LENGTH_SHORT).show() } } @@ -120,26 +123,28 @@ class SetupPasswordActivity : AppCompatActivity() { MaterialAlertDialogBuilder(this) - .setTitle("Answer the Security Question!") + .setTitle(getString(R.string.answer_the_security_question)) .setView(dialogView) - .setPositiveButton("Verify") { dialog, _ -> + .setPositiveButton(getString(R.string.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() + Toast.makeText(this, + getString(R.string.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() + Toast.makeText(this, + getString(R.string.password_successfully_reset), Toast.LENGTH_SHORT).show() dialog.dismiss() }else { - Toast.makeText(this, "Invalid answer!", Toast.LENGTH_SHORT).show() + Toast.makeText(this, getString(R.string.invalid_answer), Toast.LENGTH_SHORT).show() } } } - .setNegativeButton("Cancel") { dialog, _ -> + .setNegativeButton(getString(R.string.cancel)) { dialog, _ -> dialog.dismiss() } diff --git a/app/src/main/java/devs/org/calculator/activities/VideoGalleryActivity.kt b/app/src/main/java/devs/org/calculator/activities/VideoGalleryActivity.kt index 290e0d9..dcfc848 100644 --- a/app/src/main/java/devs/org/calculator/activities/VideoGalleryActivity.kt +++ b/app/src/main/java/devs/org/calculator/activities/VideoGalleryActivity.kt @@ -7,6 +7,7 @@ import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts import androidx.lifecycle.lifecycleScope +import devs.org.calculator.R import devs.org.calculator.utils.FileManager import devs.org.calculator.callbacks.FileProcessCallback import kotlinx.coroutines.launch @@ -15,7 +16,6 @@ import java.io.File class VideoGalleryActivity : BaseGalleryActivity(), FileProcessCallback { override val fileType = FileManager.FileType.VIDEO private lateinit var pickLauncher: ActivityResultLauncher - private var selectedUri: Uri? = null override fun onCreate(savedInstanceState: Bundle?) { @@ -42,19 +42,21 @@ class VideoGalleryActivity : BaseGalleryActivity(), FileProcessCallback { .processMultipleFiles(uriList, fileType,this@VideoGalleryActivity ) } } else { - Toast.makeText(this, "No files selected", Toast.LENGTH_SHORT).show() + Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show() } } } } override fun onFilesProcessedSuccessfully(copiedFiles: List) { - Toast.makeText(this@VideoGalleryActivity, "${copiedFiles.size} Videos hidden successfully", Toast.LENGTH_SHORT).show() + Toast.makeText(this@VideoGalleryActivity, copiedFiles.size.toString() + + getString(R.string.videos_hidden_successfully), Toast.LENGTH_SHORT).show() loadFiles() } override fun onFileProcessFailed() { - Toast.makeText(this@VideoGalleryActivity, "Failed to hide videos", Toast.LENGTH_SHORT).show() + Toast.makeText(this@VideoGalleryActivity, + getString(R.string.failed_to_hide_videos), Toast.LENGTH_SHORT).show() } private fun setupFabButton() { diff --git a/app/src/main/java/devs/org/calculator/adapters/FileAdapter.kt b/app/src/main/java/devs/org/calculator/adapters/FileAdapter.kt index 15340b5..6b1fc0e 100644 --- a/app/src/main/java/devs/org/calculator/adapters/FileAdapter.kt +++ b/app/src/main/java/devs/org/calculator/adapters/FileAdapter.kt @@ -13,7 +13,6 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.Glide -import com.google.android.material.dialog.MaterialAlertDialogBuilder import devs.org.calculator.R import devs.org.calculator.activities.PreviewActivity import devs.org.calculator.callbacks.DialogActionsCallback @@ -35,18 +34,18 @@ class FileAdapter( private var fileTypes = when (fileType) { FileManager.FileType.IMAGE -> { - "IMAGE" + context.getString(R.string.image) } FileManager.FileType.VIDEO -> { - "VIDEO" + context.getString(R.string.video) } FileManager.FileType.AUDIO -> { - "AUDIO" + context.getString(R.string.audio) } - else -> "DOCUMENT" + else -> context.getString(R.string.document) } @@ -93,7 +92,8 @@ class FileAdapter( try { context.startActivity(intent) } catch (e: Exception) { - Toast.makeText(context, "No audio player found!", Toast.LENGTH_SHORT).show() + Toast.makeText(context, + context.getString(R.string.no_audio_player_found), Toast.LENGTH_SHORT).show() } } @@ -106,7 +106,8 @@ class FileAdapter( try { context.startActivity(intent) } catch (e: Exception) { - Toast.makeText(context, "No suitable app found to open this document!", Toast.LENGTH_SHORT).show() + Toast.makeText(context, + context.getString(R.string.no_suitable_app_found_to_open_this_document), Toast.LENGTH_SHORT).show() } } else -> { @@ -131,14 +132,14 @@ class FileAdapter( } fileName = FileManager.FileName(context).getFileNameFromUri(fileUri)?.toString() - ?: "Unknown File" + ?: context.getString(R.string.unknown_file) DialogUtil(context).showMaterialDialogWithNaturalButton( - "$fileTypes DETAILS", - "File Name: $fileName\n\nFile Path: $file\n\nYou can permanently delete or unhide this file.", - "Delete Permanently", - "Unhide", - "Cancel", + context.getString(R.string.details, fileTypes), + "File Name: $fileName\n\nFile Path: $file\n\nYou can permanently delete or un-hide this file.", + context.getString(R.string.delete_permanently), + context.getString(R.string.un_hide), + context.getString(R.string.cancel), object : DialogActionsCallback { override fun onPositiveButtonClicked() { lifecycleOwner.lifecycleScope.launch { diff --git a/app/src/main/java/devs/org/calculator/utils/DialogUtil.kt b/app/src/main/java/devs/org/calculator/utils/DialogUtil.kt index 5561c2a..94179b6 100644 --- a/app/src/main/java/devs/org/calculator/utils/DialogUtil.kt +++ b/app/src/main/java/devs/org/calculator/utils/DialogUtil.kt @@ -1,22 +1,10 @@ package devs.org.calculator.utils -import android.app.RecoverableSecurityException import android.content.Context -import android.net.Uri -import android.os.Build -import android.provider.MediaStore -import android.view.View -import android.widget.Toast import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.IntentSenderRequest -import androidx.documentfile.provider.DocumentFile -import androidx.lifecycle.LifecycleOwner import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.textfield.TextInputEditText -import devs.org.calculator.R import devs.org.calculator.callbacks.DialogActionsCallback -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext class DialogUtil(private val context: Context) { private lateinit var intentSenderLauncher: ActivityResultLauncher @@ -57,7 +45,7 @@ class DialogUtil(private val context: Context) { MaterialAlertDialogBuilder(context) .setTitle(title) .setMessage(message) - .setPositiveButton(positiveButton) { dialog, _ -> + .setPositiveButton(positiveButton) { _, _ -> // Handle positive button click callback.onPositiveButtonClicked() } diff --git a/app/src/main/java/devs/org/calculator/utils/FileManager.kt b/app/src/main/java/devs/org/calculator/utils/FileManager.kt index b47ddc6..3030495 100644 --- a/app/src/main/java/devs/org/calculator/utils/FileManager.kt +++ b/app/src/main/java/devs/org/calculator/utils/FileManager.kt @@ -188,7 +188,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life withContext(Dispatchers.Main) { Toast.makeText( context, - "Error hiding/unhiding file: ${e.message}", + "Error hiding/un-hiding file: ${e.message}", Toast.LENGTH_LONG ).show() } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 6a554eb..6f3f941 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -1,39 +1,45 @@ - - + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintHeight_percent="0.3"> + - - - - - + - + app:columnCount="4" + app:rowCount="5" + app:layout_constraintTop_toBottomOf="@id/displayContainer" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintEnd_toEndOf="parent"> + - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a2624b9..2eeaf9c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,4 +1,54 @@ Calculator Invalid Value Entered + Add Image + Add Audio + Add Video + Add Files + Failed to hide Documents + No files selected + %1$s Documents hidden successfully + Failed to hide/unhide photo + %1$s Images hidden successfully + Failed to hide images + Storage permissions granted + Storage permissions denied + Preview Images + Preview Videos + Preview Audios + Preview Documents + Delete File + Are you sure to Delete this file permanently? + Delete Permanently + Cancel + Un-hide File + Are you sure you want to Un-hide this file? + Un-hide + Enter password + Confirm password + Enter security question + Enter security answer + Passwords don\'t match + Password set successfully + Security question not set yet. + This field can\'t be empty + Password reset successfully + Old Password And New Password Not Be Same + Wrong password entered + Old Password Not Matching + Answer the Security Question! + Verify + Answer cannot be empty! + Password successfully reset. + Invalid answer! + %1$s Videos hidden successfully + Failed to hide videos + IMAGE + VIDEO + AUDIO + DOCUMENT + No audio player found! + No suitable app found to open this document! + Unknown File + %1$s DETAILS \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 41d6fca..53d4946 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,8 @@ [versions] -agp = "8.7.2" +agp = "8.7.3" +documentfile = "1.0.1" exp4j = "0.4.8" +glide = "4.16.0" kotlin = "1.9.24" coreKtx = "1.15.0" junit = "4.13.2" @@ -11,10 +13,17 @@ material = "1.12.0" activity = "1.9.3" constraintlayout = "2.2.0" materialColorUtilities = "1.3.0" +gridlayout = "1.0.0" +photoview = "2.3.0" +viewpager = "1.1.0" +zoomage = "1.3.1" [libraries] androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } +androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" } +androidx-viewpager = { module = "androidx.viewpager:viewpager", version.ref = "viewpager" } exp4j = { module = "net.objecthunter:exp4j", version.ref = "exp4j" } +glide = { module = "com.github.bumptech.glide:glide", version.ref = "glide" } junit = { group = "junit", name = "junit", version.ref = "junit" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espressoCore" } @@ -23,6 +32,9 @@ material = { group = "com.google.android.material", name = "material", version.r androidx-activity = { group = "androidx.activity", name = "activity", version.ref = "activity" } androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" } material-color-utilities = { module = "com.google.android.material:material-color-utilities", version.ref = "materialColorUtilities" } +androidx-gridlayout = { group = "androidx.gridlayout", name = "gridlayout", version.ref = "gridlayout" } +photoview = { module = "com.github.chrisbanes:PhotoView", version.ref = "photoview" } +zoomage = { module = "com.jsibbold:zoomage", version.ref = "zoomage" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" }