Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf192b6ab9 | ||
|
|
191368bdf8 | ||
|
|
5aafc7e411 | ||
|
|
1f81cccd96 | ||
|
|
eea0f56379 | ||
|
|
e90e3c438f | ||
|
|
480a90b64c | ||
|
|
90a252f227 | ||
|
|
08cdd747ca | ||
|
|
2d10c2f941 | ||
|
|
d840732305 | ||
|
|
7da5885931 | ||
|
|
082dbf17aa | ||
|
|
093c419f34 | ||
|
|
8649ec9ac2 | ||
|
|
527f377ebd | ||
|
|
e1627c426f | ||
|
|
092219d487 | ||
|
|
7803ed2589 | ||
|
|
e40b59c460 | ||
|
|
c25ac613c2 | ||
|
|
bfc339c101 | ||
|
|
657b674ff3 | ||
|
|
ea7c6ea18a | ||
|
|
c97f38ae53 | ||
|
|
0ba60f2bae | ||
|
|
970cde737a | ||
|
|
eb7f61fbc8 | ||
|
|
4069ddc200 | ||
|
|
88dcc844c8 | ||
|
|
f8575da2a9 | ||
|
|
ab737511a7 | ||
|
|
48c4e04a28 | ||
|
|
713fd3540f |
6
.idea/AndroidProjectSystem.xml
generated
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AndroidProjectSystem">
|
||||
<option name="providerId" value="com.android.tools.idea.GradleProjectSystem" />
|
||||
</component>
|
||||
</project>
|
||||
1
.idea/appInsightsSettings.xml
generated
@@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AppInsightsSettings">
|
||||
<option name="selectedTabId" value="Firebase Crashlytics" />
|
||||
<option name="tabSettings">
|
||||
<map>
|
||||
<entry key="Firebase Crashlytics">
|
||||
|
||||
2
.idea/kotlinc.xml
generated
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="1.9.24" />
|
||||
<option name="version" value="2.0.0" />
|
||||
</component>
|
||||
</project>
|
||||
26
README.md
@@ -49,10 +49,21 @@ The **Calculator Hide File App** is an innovative **Android file-hiding app** th
|
||||
## 🖼️ Screenshots
|
||||
|
||||
<div align="center">
|
||||
<img src="app/src/main/assets/Screenshot_1.jpg" alt="Calculator Hide File App - Home Screen" width="24%">
|
||||
<img src="app/src/main/assets/Screenshot_2.jpg" alt="Calculator Hide File App - Secure File Storage" width="24%">
|
||||
<img src="app/src/main/assets/Screenshot_3.jpg" alt="Calculator Hide File App - Passcode Protection" width="24%">
|
||||
<img src="app/src/main/assets/Screenshot_4.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="24%">
|
||||
<img src="app/src/main/assets/Screenshot_1.jpg" alt="Calculator Hide File App - Home Screen" width="32%">
|
||||
<img src="app/src/main/assets/Screenshot_2.jpg" alt="Calculator Hide File App - Secure File Storage" width="32%">
|
||||
<img src="app/src/main/assets/Screenshot_3.jpg" alt="Calculator Hidle File App - Passcode Protection" width="32%">
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img src="app/src/main/assets/Screenshot_4.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
|
||||
<img src="app/src/main/assets/Screenshot_5.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
|
||||
<img src="app/src/main/assets/Screenshot_6.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img src="app/src/main/assets/Screenshot_7.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
|
||||
<img src="app/src/main/assets/Screenshot_8.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
|
||||
<img src="app/src/main/assets/Screenshot_9.jpg" alt="Calculator Hide File App - Hidden Files Manager" width="32%">
|
||||
</div>
|
||||
|
||||
---
|
||||
@@ -68,7 +79,9 @@ The **Calculator Hide File App** is an innovative **Android file-hiding app** th
|
||||
|
||||
3. **Manage Hidden Files**
|
||||
- Add, remove, and restore hidden files.
|
||||
- Files stay protected even after closing the app.
|
||||
- Files stay protected even after closing the app.
|
||||
- Create Hidden Folders.
|
||||
- Move & Copy Hidden Files Between Hidden Folders.
|
||||
|
||||
---
|
||||
|
||||
@@ -78,7 +91,8 @@ The **Calculator Hide File App** is an innovative **Android file-hiding app** th
|
||||
|
||||
### 🔹 Prerequisites
|
||||
- **Android 6.0 or higher**
|
||||
- **Storage permissions enabled**
|
||||
- **Storage permissions enabled**
|
||||
- **Need Manage All Files Permissons For Android 11 and Higher Versions.**
|
||||
|
||||
### 🔹 Installation Steps
|
||||
```bash
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application)
|
||||
alias(libs.plugins.kotlin.android)
|
||||
id("kotlin-kapt")
|
||||
}
|
||||
|
||||
android {
|
||||
@@ -11,28 +12,28 @@ android {
|
||||
applicationId = "devs.org.calculator"
|
||||
minSdk = 26
|
||||
targetSdk = 34
|
||||
versionCode = 3
|
||||
versionName = "1.2"
|
||||
versionCode = 5
|
||||
versionName = "1.4.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
debug {
|
||||
isMinifyEnabled = true
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
@@ -43,6 +44,21 @@ android {
|
||||
buildFeatures{
|
||||
viewBinding = true
|
||||
}
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
excludes += listOf(
|
||||
"META-INF/DEPENDENCIES",
|
||||
"META-INF/LICENSE",
|
||||
"META-INF/LICENSE.txt",
|
||||
"META-INF/license.txt",
|
||||
"META-INF/NOTICE",
|
||||
"META-INF/NOTICE.txt",
|
||||
"META-INF/notice.txt",
|
||||
"META-INF/ASL2.0"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -64,4 +80,10 @@ dependencies {
|
||||
implementation(libs.photoview)
|
||||
implementation(libs.androidx.viewpager)
|
||||
implementation(libs.zoomage)
|
||||
implementation(libs.lottie)
|
||||
|
||||
// Room dependencies
|
||||
implementation(libs.androidx.room.runtime)
|
||||
implementation(libs.androidx.room.ktx)
|
||||
kapt(libs.androidx.room.compiler)
|
||||
}
|
||||
2
app/proguard-rules.pro
vendored
@@ -67,4 +67,4 @@
|
||||
# Keep specific activities with special code in onCreate
|
||||
-keepclassmembers class * extends android.app.Activity {
|
||||
public void onCreate(android.os.Bundle);
|
||||
}
|
||||
}
|
||||
|
||||
37
app/release/output-metadata.json
Normal file
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"version": 3,
|
||||
"artifactType": {
|
||||
"type": "APK",
|
||||
"kind": "Directory"
|
||||
},
|
||||
"applicationId": "devs.org.calculator",
|
||||
"variantName": "release",
|
||||
"elements": [
|
||||
{
|
||||
"type": "SINGLE",
|
||||
"filters": [],
|
||||
"attributes": [],
|
||||
"versionCode": 4,
|
||||
"versionName": "1.3",
|
||||
"outputFile": "app-release.apk"
|
||||
}
|
||||
],
|
||||
"elementType": "File",
|
||||
"baselineProfiles": [
|
||||
{
|
||||
"minApi": 28,
|
||||
"maxApi": 30,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/1/app-release.dm"
|
||||
]
|
||||
},
|
||||
{
|
||||
"minApi": 31,
|
||||
"maxApi": 2147483647,
|
||||
"baselineProfiles": [
|
||||
"baselineProfiles/0/app-release.dm"
|
||||
]
|
||||
}
|
||||
],
|
||||
"minSdkVersionForDexing": 26
|
||||
}
|
||||
@@ -1,68 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
package="devs.org.calculator"
|
||||
>
|
||||
package="devs.org.calculator">
|
||||
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
<uses-permission
|
||||
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="29" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_MEDIA_IMAGES"
|
||||
tools:ignore="SelectedPhotoAccess" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO"
|
||||
<uses-permission
|
||||
android:name="android.permission.READ_MEDIA_VIDEO"
|
||||
tools:ignore="SelectedPhotoAccess" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />
|
||||
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
<uses-permission
|
||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
||||
<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"
|
||||
android:label="@string/app_name"
|
||||
android:requestLegacyExternalStorage="true"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Calculator"
|
||||
tools:targetApi="31">
|
||||
<activity
|
||||
android:name=".activities.DocumentsActivity"
|
||||
android:name=".activities.ViewFolderActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".activities.AudioGalleryActivity"
|
||||
android:name=".activities.SettingsActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".activities.VideoGalleryActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".activities.ImageGalleryActivity"
|
||||
android:name=".activities.HiddenActivity"
|
||||
android:exported="false" />
|
||||
<activity
|
||||
android:name=".activities.SetupPasswordActivity"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.MainActivity"
|
||||
android:exported="true" >
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activities.HiddenVaultActivity"
|
||||
android:exported="true" />
|
||||
<activity
|
||||
android:name=".activities.PreviewActivity"
|
||||
android:configChanges="orientation|screenSize"/>
|
||||
android:configChanges="orientation|screenSize" />
|
||||
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="devs.org.calculator.fileprovider"
|
||||
android:authorities="devs.org.calculator.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
@@ -70,8 +67,9 @@
|
||||
android:resource="@xml/file_paths" />
|
||||
</provider>
|
||||
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.icon"
|
||||
android:resource="@mipmap/ic_launcher_monochrome" />
|
||||
</application>
|
||||
|
||||
|
||||
</manifest>
|
||||
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 183 KiB |
|
Before Width: | Height: | Size: 164 KiB After Width: | Height: | Size: 99 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 154 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 101 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 406 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 372 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 469 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 557 KiB |
BIN
app/src/main/assets/Screenshot_9.jpg
Normal file
|
After Width: | Height: | Size: 228 KiB |
1
app/src/main/assets/hiding_files.json
Normal file
@@ -1,12 +1,18 @@
|
||||
package devs.org.calculator
|
||||
|
||||
import android.app.Application
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import devs.org.calculator.utils.PrefsUtil
|
||||
|
||||
class CalculatorApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
// Apply dynamic colors to enable Material You theming
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
val prefs = PrefsUtil(this)
|
||||
val themeMode = prefs.getInt("theme_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
AppCompatDelegate.setDefaultNightMode(themeMode)
|
||||
if (prefs.getBoolean("dynamic_theme", true)) {
|
||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
package devs.org.calculator.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
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.callbacks.FileProcessCallback
|
||||
import devs.org.calculator.utils.FileManager
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
class AudioGalleryActivity : BaseGalleryActivity(), FileProcessCallback {
|
||||
override val fileType = FileManager.FileType.AUDIO
|
||||
private lateinit var pickAudioLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setupFabButton()
|
||||
|
||||
pickAudioLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
val clipData = result.data?.clipData
|
||||
val uriList = mutableListOf<Uri>()
|
||||
|
||||
if (clipData != null) {
|
||||
for (i in 0 until clipData.itemCount) {
|
||||
val uri = clipData.getItemAt(i).uri
|
||||
uriList.add(uri)
|
||||
}
|
||||
} else {
|
||||
result.data?.data?.let { uriList.add(it) }
|
||||
}
|
||||
|
||||
if (uriList.isNotEmpty()) {
|
||||
lifecycleScope.launch {
|
||||
FileManager(
|
||||
this@AudioGalleryActivity,
|
||||
this@AudioGalleryActivity
|
||||
).processMultipleFiles(uriList, fileType, this@AudioGalleryActivity)
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
|
||||
Toast.makeText(
|
||||
this@AudioGalleryActivity,
|
||||
"${copiedFiles.size} ${getString(R.string.audio_hidded_successfully)} ",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
loadFiles()
|
||||
}
|
||||
|
||||
override fun onFileProcessFailed() {
|
||||
Toast.makeText(this@AudioGalleryActivity, "Failed to hide Audios", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun setupFabButton() {
|
||||
binding.fabAdd.setOnClickListener {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
type = "audio/*"
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
}
|
||||
pickAudioLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun openPreview() {
|
||||
// Not implemented audio preview
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,162 +0,0 @@
|
||||
package devs.org.calculator.activities
|
||||
|
||||
import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
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.view.View
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
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
|
||||
import java.io.File
|
||||
|
||||
abstract class BaseGalleryActivity : AppCompatActivity() {
|
||||
protected lateinit var binding: ActivityGalleryBinding
|
||||
private lateinit var fileManager: FileManager
|
||||
private lateinit var adapter: FileAdapter
|
||||
private lateinit var files: List<File>
|
||||
|
||||
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
|
||||
private val storagePermissionLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.RequestMultiplePermissions()
|
||||
) { permissions ->
|
||||
val granted = permissions.values.all { it }
|
||||
if (granted || Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && Environment.isExternalStorageManager()) {
|
||||
loadFiles()
|
||||
} else {
|
||||
showPermissionDeniedDialog()
|
||||
}
|
||||
}
|
||||
|
||||
abstract val fileType: FileManager.FileType
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setupIntentSenderLauncher()
|
||||
binding = ActivityGalleryBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
fileManager = FileManager(this, this)
|
||||
|
||||
when(fileType){
|
||||
FileManager.FileType.IMAGE -> {
|
||||
val image = getString(R.string.add_image)
|
||||
binding.fabAdd.text = image
|
||||
binding.noItemsTxt.text = "${getString(R.string.no_items_available_add_one_by_clicking_on_the_plus_button)} '$image' button"
|
||||
}
|
||||
FileManager.FileType.AUDIO -> {
|
||||
val text = getString(R.string.add_audio)
|
||||
binding.fabAdd.text = text
|
||||
binding.noItemsTxt.text = "${getString(R.string.no_items_available_add_one_by_clicking_on_the_plus_button)} '$text' button"
|
||||
|
||||
}
|
||||
FileManager.FileType.VIDEO -> {
|
||||
val text = getString(R.string.add_video)
|
||||
binding.fabAdd.text = text
|
||||
binding.noItemsTxt.text = "${getString(R.string.no_items_available_add_one_by_clicking_on_the_plus_button)} '$text' button"
|
||||
}
|
||||
FileManager.FileType.DOCUMENT -> {
|
||||
val text = getString(R.string.add_files)
|
||||
binding.fabAdd.text = text
|
||||
binding.noItemsTxt.text = "${getString(R.string.no_items_available_add_one_by_clicking_on_the_plus_button)} '$text' button"
|
||||
}
|
||||
}
|
||||
binding.recyclerView.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY ->
|
||||
if (scrollY > oldScrollY && binding.fabAdd.isExtended) {
|
||||
|
||||
binding.fabAdd.shrink()
|
||||
} else if (scrollY < oldScrollY && !binding.fabAdd.isExtended) {
|
||||
|
||||
binding.fabAdd.extend()
|
||||
}
|
||||
}
|
||||
setupRecyclerView()
|
||||
checkPermissionsAndLoadFiles()
|
||||
}
|
||||
|
||||
private fun setupIntentSenderLauncher() {
|
||||
intentSenderLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartIntentSenderForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
loadFiles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupRecyclerView() {
|
||||
binding.recyclerView.layoutManager = GridLayoutManager(this, 3)
|
||||
adapter = FileAdapter(fileType, this, this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
private fun checkPermissionsAndLoadFiles() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
if (!Environment.isExternalStorageManager()) {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||
.addCategory("android.intent.category.DEFAULT")
|
||||
.setData(Uri.parse("package:${applicationContext.packageName}"))
|
||||
startActivityForResult(intent, 2296)
|
||||
} else {
|
||||
loadFiles()
|
||||
}
|
||||
} else {
|
||||
val permissions = arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
)
|
||||
if (permissions.any { checkSelfPermission(it) != PackageManager.PERMISSION_GRANTED }) {
|
||||
storagePermissionLauncher.launch(permissions)
|
||||
} else {
|
||||
loadFiles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun loadFiles() {
|
||||
files = fileManager.getFilesInHiddenDir(fileType)
|
||||
adapter.submitList(files)
|
||||
if (files.isEmpty()){
|
||||
binding.recyclerView.visibility = View.GONE
|
||||
binding.loading.visibility = View.GONE
|
||||
binding.noItems.visibility = View.VISIBLE
|
||||
}else{
|
||||
binding.recyclerView.visibility = View.VISIBLE
|
||||
binding.loading.visibility = View.GONE
|
||||
binding.noItems.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
loadFiles()
|
||||
}
|
||||
|
||||
abstract fun openPreview()
|
||||
|
||||
private fun showPermissionDeniedDialog() {
|
||||
// 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) {
|
||||
if (Environment.isExternalStorageManager()) {
|
||||
loadFiles()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package devs.org.calculator.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
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
|
||||
import java.io.File
|
||||
|
||||
class DocumentsActivity : BaseGalleryActivity(), FileProcessCallback {
|
||||
override val fileType = FileManager.FileType.DOCUMENT
|
||||
private lateinit var pickLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setupFabButton()
|
||||
|
||||
pickLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
val clipData = result.data?.clipData
|
||||
val uriList = mutableListOf<Uri>()
|
||||
|
||||
if (clipData != null) {
|
||||
for (i in 0 until clipData.itemCount) {
|
||||
val uri = clipData.getItemAt(i).uri
|
||||
uriList.add(uri)
|
||||
}
|
||||
} else {
|
||||
result.data?.data?.let { uriList.add(it) } // Single file selected
|
||||
}
|
||||
|
||||
if (uriList.isNotEmpty()) {
|
||||
lifecycleScope.launch {
|
||||
FileManager(this@DocumentsActivity, this@DocumentsActivity).processMultipleFiles(uriList, fileType,this@DocumentsActivity )
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
|
||||
Toast.makeText(this@DocumentsActivity,"${copiedFiles.size} ${getString(R.string.documents_hidden_successfully )}"
|
||||
, Toast.LENGTH_SHORT).show()
|
||||
loadFiles()
|
||||
}
|
||||
|
||||
override fun onFileProcessFailed() {
|
||||
Toast.makeText(this@DocumentsActivity,
|
||||
getString(R.string.failed_to_hide_documents), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun setupFabButton() {
|
||||
binding.fabAdd.setOnClickListener {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
type = "*/*"
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
}
|
||||
pickLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun openPreview() {
|
||||
//Not implemented document preview
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,501 @@
|
||||
package devs.org.calculator.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import devs.org.calculator.R
|
||||
import devs.org.calculator.adapters.FolderAdapter
|
||||
import devs.org.calculator.adapters.ListFolderAdapter
|
||||
import devs.org.calculator.databinding.ActivityHiddenBinding
|
||||
import devs.org.calculator.utils.DialogUtil
|
||||
import devs.org.calculator.utils.FileManager
|
||||
import devs.org.calculator.utils.FileManager.Companion.HIDDEN_DIR
|
||||
import devs.org.calculator.utils.FolderManager
|
||||
import devs.org.calculator.utils.PrefsUtil
|
||||
import java.io.File
|
||||
|
||||
class HiddenActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityHiddenBinding
|
||||
private lateinit var fileManager: FileManager
|
||||
private lateinit var folderManager: FolderManager
|
||||
private lateinit var dialogUtil: DialogUtil
|
||||
private var currentFolder: File? = null
|
||||
private var folderAdapter: FolderAdapter? = null
|
||||
private var listFolderAdapter: ListFolderAdapter? = null
|
||||
private val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
|
||||
private val prefs:PrefsUtil by lazy { PrefsUtil(this) }
|
||||
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityHiddenBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
fileManager = FileManager(this, this)
|
||||
folderManager = FolderManager()
|
||||
dialogUtil = DialogUtil(this)
|
||||
|
||||
|
||||
setupInitialUIState()
|
||||
setupClickListeners()
|
||||
setupBackPressedHandler()
|
||||
|
||||
fileManager.askPermission(this)
|
||||
|
||||
refreshCurrentView()
|
||||
}
|
||||
|
||||
private fun setupInitialUIState() {
|
||||
|
||||
binding.addFolder.visibility = View.VISIBLE
|
||||
binding.settings.visibility = View.VISIBLE
|
||||
binding.folderOrientation.visibility = View.VISIBLE
|
||||
binding.deleteSelected.visibility = View.GONE
|
||||
binding.delete.visibility = View.GONE
|
||||
binding.menuButton.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun setupClickListeners() {
|
||||
|
||||
|
||||
binding.settings.setOnClickListener {
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
}
|
||||
|
||||
|
||||
binding.back.setOnClickListener {
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
binding.addFolder.setOnClickListener {
|
||||
createNewFolder()
|
||||
}
|
||||
|
||||
binding.deleteSelected.setOnClickListener {
|
||||
deleteSelectedItems()
|
||||
}
|
||||
|
||||
binding.delete.setOnClickListener {
|
||||
deleteSelectedItems()
|
||||
}
|
||||
|
||||
binding.edit.setOnClickListener {
|
||||
editSelectedFolder()
|
||||
}
|
||||
|
||||
binding.folderOrientation.setOnClickListener {
|
||||
val currentIsList = PrefsUtil(this).getBoolean("isList", false)
|
||||
val newIsList = !currentIsList
|
||||
|
||||
if (newIsList) {
|
||||
showListUI()
|
||||
PrefsUtil(this).setBoolean("isList", true)
|
||||
binding.folderOrientation.setIconResource(R.drawable.ic_grid)
|
||||
} else {
|
||||
showGridUI()
|
||||
PrefsUtil(this).setBoolean("isList", false)
|
||||
binding.folderOrientation.setIconResource(R.drawable.ic_list)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showGridUI() {
|
||||
listFoldersInHiddenDirectory()
|
||||
}
|
||||
|
||||
private fun showListUI() {
|
||||
listFoldersInHiddenDirectoryListStyle()
|
||||
}
|
||||
|
||||
private fun listFoldersInHiddenDirectoryListStyle() {
|
||||
try {
|
||||
if (!hiddenDir.exists()) {
|
||||
fileManager.getHiddenDirectory()
|
||||
}
|
||||
|
||||
if (hiddenDir.exists() && hiddenDir.isDirectory) {
|
||||
val folders = folderManager.getFoldersInDirectory(hiddenDir)
|
||||
|
||||
if (folders.isNotEmpty()) {
|
||||
showFolderListStyle(folders)
|
||||
} else {
|
||||
showEmptyState()
|
||||
}
|
||||
} else {
|
||||
showEmptyState()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
|
||||
showEmptyState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupBackPressedHandler() {
|
||||
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||
override fun handleOnBackPressed() {
|
||||
handleBackPress()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private fun createNewFolder() {
|
||||
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_input, null)
|
||||
val inputEditText = dialogView.findViewById<EditText>(R.id.editText)
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.enter_folder_name_to_create))
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(getString(R.string.create)) { dialog, _ ->
|
||||
val newName = inputEditText.text.toString().trim()
|
||||
if (newName.isNotEmpty()) {
|
||||
try {
|
||||
folderManager.createFolder(hiddenDir, newName)
|
||||
refreshCurrentView()
|
||||
} catch (_: Exception) {
|
||||
Toast.makeText(
|
||||
this@HiddenActivity,
|
||||
getString(R.string.failed_to_create_folder),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||
dialog.cancel()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
setupFlagSecure()
|
||||
}
|
||||
|
||||
private fun setupFlagSecure() {
|
||||
if (prefs.getBoolean("screenshot_restriction", true)) {
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun listFoldersInHiddenDirectory() {
|
||||
try {
|
||||
if (!hiddenDir.exists()) {
|
||||
fileManager.getHiddenDirectory()
|
||||
}
|
||||
|
||||
if (hiddenDir.exists() && hiddenDir.isDirectory) {
|
||||
val folders = folderManager.getFoldersInDirectory(hiddenDir)
|
||||
|
||||
if (folders.isNotEmpty()) {
|
||||
showFolderList(folders)
|
||||
} else {
|
||||
showEmptyState()
|
||||
}
|
||||
} else {
|
||||
showEmptyState()
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
showEmptyState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFolderList(folders: List<File>) {
|
||||
binding.noItems.visibility = View.GONE
|
||||
binding.recyclerView.visibility = View.VISIBLE
|
||||
listFolderAdapter = null
|
||||
|
||||
binding.recyclerView.layoutManager = GridLayoutManager(this, 2)
|
||||
folderAdapter = FolderAdapter(
|
||||
onFolderClick = { clickedFolder ->
|
||||
startActivity(Intent(this,ViewFolderActivity::class.java).putExtra("folder",clickedFolder.toString()))
|
||||
},
|
||||
onFolderLongClick = {
|
||||
enterFolderSelectionMode()
|
||||
},
|
||||
onSelectionModeChanged = { isSelectionMode ->
|
||||
handleFolderSelectionModeChange(isSelectionMode)
|
||||
},
|
||||
onSelectionCountChanged = { _ ->
|
||||
updateEditButtonVisibility()
|
||||
}
|
||||
)
|
||||
binding.recyclerView.adapter = folderAdapter
|
||||
folderAdapter?.submitList(folders)
|
||||
|
||||
if (folderAdapter?.isInSelectionMode() != true) {
|
||||
showFolderViewIcons()
|
||||
}
|
||||
}
|
||||
private fun showFolderListStyle(folders: List<File>) {
|
||||
binding.noItems.visibility = View.GONE
|
||||
binding.recyclerView.visibility = View.VISIBLE
|
||||
folderAdapter = null
|
||||
|
||||
binding.recyclerView.layoutManager = GridLayoutManager(this, 1)
|
||||
listFolderAdapter = ListFolderAdapter(
|
||||
onFolderClick = { clickedFolder ->
|
||||
startActivity(Intent(this,ViewFolderActivity::class.java).putExtra("folder",clickedFolder.toString()))
|
||||
},
|
||||
onFolderLongClick = {
|
||||
enterFolderSelectionMode()
|
||||
},
|
||||
onSelectionModeChanged = { isSelectionMode ->
|
||||
handleFolderSelectionModeChange(isSelectionMode)
|
||||
},
|
||||
onSelectionCountChanged = { _ ->
|
||||
updateEditButtonVisibility()
|
||||
}
|
||||
)
|
||||
binding.recyclerView.adapter = listFolderAdapter
|
||||
listFolderAdapter?.submitList(folders)
|
||||
|
||||
if (listFolderAdapter?.isInSelectionMode() != true) {
|
||||
showFolderViewIcons()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateEditButtonVisibility() {
|
||||
val selectedCount = when {
|
||||
folderAdapter != null -> folderAdapter?.getSelectedItems()?.size ?: 0
|
||||
listFolderAdapter != null -> listFolderAdapter?.getSelectedItems()?.size ?: 0
|
||||
else -> 0
|
||||
}
|
||||
binding.edit.visibility = if (selectedCount == 1) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun showEmptyState() {
|
||||
binding.noItems.visibility = View.VISIBLE
|
||||
binding.recyclerView.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun enterFolderSelectionMode() {
|
||||
showFolderSelectionIcons()
|
||||
}
|
||||
|
||||
|
||||
private fun refreshCurrentView() {
|
||||
val isList = PrefsUtil(this).getBoolean("isList", false)
|
||||
if (isList) {
|
||||
binding.folderOrientation.setIconResource(R.drawable.ic_grid)
|
||||
listFoldersInHiddenDirectoryListStyle()
|
||||
} else {
|
||||
binding.folderOrientation.setIconResource(R.drawable.ic_list)
|
||||
listFoldersInHiddenDirectory()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun deleteSelectedItems() {
|
||||
deleteSelectedFolders()
|
||||
}
|
||||
|
||||
private fun deleteSelectedFolders() {
|
||||
val selectedFolders = when {
|
||||
folderAdapter != null -> folderAdapter?.getSelectedItems() ?: emptyList()
|
||||
listFolderAdapter != null -> listFolderAdapter?.getSelectedItems() ?: emptyList()
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
if (selectedFolders.isNotEmpty()) {
|
||||
dialogUtil.showMaterialDialog(
|
||||
getString(R.string.delete_items),
|
||||
"${getString(R.string.are_you_sure_you_want_to_delete_selected_items)}\n ${getString(R.string.folder_will_be_deleted_permanently)}" ,
|
||||
getString(R.string.delete),
|
||||
getString(R.string.cancel),
|
||||
object : DialogUtil.DialogCallback {
|
||||
override fun onPositiveButtonClicked() {
|
||||
performFolderDeletion(selectedFolders)
|
||||
}
|
||||
|
||||
override fun onNegativeButtonClicked() {
|
||||
|
||||
}
|
||||
|
||||
override fun onNaturalButtonClicked() {
|
||||
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun performFolderDeletion(selectedFolders: List<File>) {
|
||||
var allDeleted = true
|
||||
selectedFolders.forEach { folder ->
|
||||
if (!folderManager.deleteFolder(folder)) {
|
||||
allDeleted = false
|
||||
}
|
||||
}
|
||||
|
||||
val message = if (allDeleted) {
|
||||
getString(R.string.folder_deleted_successfully)
|
||||
} else {
|
||||
getString(R.string.some_items_could_not_be_deleted)
|
||||
}
|
||||
|
||||
Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
|
||||
folderAdapter?.clearSelection()
|
||||
listFolderAdapter?.clearSelection()
|
||||
|
||||
exitFolderSelectionMode()
|
||||
|
||||
refreshCurrentView()
|
||||
}
|
||||
|
||||
private fun handleBackPress() {
|
||||
|
||||
if (folderAdapter?.onBackPressed() == true || listFolderAdapter?.onBackPressed() == true) {
|
||||
return
|
||||
}
|
||||
|
||||
if (currentFolder != null) {
|
||||
navigateBackToFolders()
|
||||
} else {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
private fun navigateBackToFolders() {
|
||||
currentFolder = null
|
||||
|
||||
refreshCurrentView()
|
||||
|
||||
binding.folderName.text = getString(R.string.hidden_space)
|
||||
|
||||
showFolderViewIcons()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
mainHandler.removeCallbacksAndMessages(null)
|
||||
}
|
||||
|
||||
private fun showFolderViewIcons() {
|
||||
binding.folderOrientation.visibility = View.VISIBLE
|
||||
binding.settings.visibility = View.VISIBLE
|
||||
binding.delete.visibility = View.GONE
|
||||
binding.deleteSelected.visibility = View.GONE
|
||||
binding.menuButton.visibility = View.GONE
|
||||
binding.addFolder.visibility = View.VISIBLE
|
||||
binding.edit.visibility = View.GONE
|
||||
if (currentFolder == null) {
|
||||
|
||||
binding.addFolder.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
private fun showFolderSelectionIcons() {
|
||||
binding.folderOrientation.visibility = View.GONE
|
||||
binding.settings.visibility = View.GONE
|
||||
binding.delete.visibility = View.VISIBLE
|
||||
binding.deleteSelected.visibility = View.VISIBLE
|
||||
binding.menuButton.visibility = View.GONE
|
||||
binding.addFolder.visibility = View.GONE
|
||||
updateEditButtonVisibility()
|
||||
}
|
||||
private fun exitFolderSelectionMode() {
|
||||
showFolderViewIcons()
|
||||
}
|
||||
|
||||
private fun handleFolderSelectionModeChange(isSelectionMode: Boolean) {
|
||||
if (!isSelectionMode) {
|
||||
exitFolderSelectionMode()
|
||||
} else {
|
||||
enterFolderSelectionMode()
|
||||
}
|
||||
updateEditButtonVisibility()
|
||||
}
|
||||
|
||||
private fun editSelectedFolder() {
|
||||
val selectedFolders = when {
|
||||
folderAdapter != null -> folderAdapter?.getSelectedItems() ?: emptyList()
|
||||
listFolderAdapter != null -> listFolderAdapter?.getSelectedItems() ?: emptyList()
|
||||
else -> emptyList()
|
||||
}
|
||||
|
||||
if (selectedFolders.size != 1) {
|
||||
Toast.makeText(this,
|
||||
getString(R.string.please_select_exactly_one_folder_to_edit), Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val folder = selectedFolders[0]
|
||||
showEditFolderDialog(folder)
|
||||
}
|
||||
|
||||
private fun showEditFolderDialog(folder: File) {
|
||||
val dialogView = LayoutInflater.from(this).inflate(R.layout.dialog_input, null)
|
||||
val inputEditText = dialogView.findViewById<EditText>(R.id.editText)
|
||||
inputEditText.setText(folder.name)
|
||||
inputEditText.selectAll()
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.rename_folder))
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(getString(R.string.rename)) { dialog, _ ->
|
||||
val newName = inputEditText.text.toString().trim()
|
||||
if (newName.isNotEmpty() && newName != folder.name) {
|
||||
if (isValidFolderName(newName)) {
|
||||
renameFolder(folder, newName)
|
||||
} else {
|
||||
Toast.makeText(this,
|
||||
getString(R.string.invalid_folder_name), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
|
||||
dialog.cancel()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun isValidFolderName(folderName: String): Boolean {
|
||||
val forbiddenChars = charArrayOf('/', '\\', ':', '*', '?', '"', '<', '>', '|')
|
||||
return folderName.isNotBlank() &&
|
||||
folderName.none { it in forbiddenChars } &&
|
||||
!folderName.startsWith(".") &&
|
||||
folderName.length <= 255
|
||||
}
|
||||
|
||||
private fun renameFolder(oldFolder: File, newName: String) {
|
||||
val parentDir = oldFolder.parentFile
|
||||
if (parentDir != null) {
|
||||
val newFolder = File(parentDir, newName)
|
||||
if (newFolder.exists()) {
|
||||
Toast.makeText(this,
|
||||
getString(R.string.folder_with_this_name_already_exists), Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
if (oldFolder.renameTo(newFolder)) {
|
||||
folderAdapter?.clearSelection()
|
||||
listFolderAdapter?.clearSelection()
|
||||
exitFolderSelectionMode()
|
||||
|
||||
refreshCurrentView()
|
||||
} else {
|
||||
Toast.makeText(this, getString(R.string.failed_to_rename_folder), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
package devs.org.calculator.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import devs.org.calculator.databinding.ActivityHiddenVaultBinding
|
||||
import devs.org.calculator.utils.FileManager
|
||||
|
||||
class HiddenVaultActivity : AppCompatActivity() {
|
||||
private lateinit var binding: ActivityHiddenVaultBinding
|
||||
private lateinit var fileManager: FileManager
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityHiddenVaultBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
fileManager = FileManager(this, this)
|
||||
setupNavigation()
|
||||
}
|
||||
|
||||
private fun setupNavigation() {
|
||||
binding.btnImages.setOnClickListener {
|
||||
startActivity(Intent(this, ImageGalleryActivity::class.java))
|
||||
}
|
||||
binding.btnVideos.setOnClickListener {
|
||||
startActivity(Intent(this, VideoGalleryActivity::class.java))
|
||||
}
|
||||
binding.btnAudio.setOnClickListener {
|
||||
startActivity(Intent(this, AudioGalleryActivity::class.java))
|
||||
}
|
||||
binding.btnDocs.setOnClickListener {
|
||||
startActivity(Intent(this, DocumentsActivity::class.java))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
package devs.org.calculator.activities
|
||||
|
||||
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.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.IntentSenderRequest
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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 {
|
||||
override val fileType = FileManager.FileType.IMAGE
|
||||
private val STORAGE_PERMISSION_CODE = 100
|
||||
|
||||
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
|
||||
private lateinit var pickImageLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setupIntentSenderLauncher()
|
||||
setupFabButton()
|
||||
|
||||
intentSenderLauncher = registerForActivityResult(ActivityResultContracts.StartIntentSenderForResult()){
|
||||
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 ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
val clipData = result.data?.clipData
|
||||
val uriList = mutableListOf<Uri>()
|
||||
|
||||
if (clipData != null) {
|
||||
for (i in 0 until clipData.itemCount) {
|
||||
val uri = clipData.getItemAt(i).uri
|
||||
uriList.add(uri)
|
||||
}
|
||||
} else {
|
||||
result.data?.data?.let { uriList.add(it) }
|
||||
}
|
||||
|
||||
if (uriList.isNotEmpty()) {
|
||||
lifecycleScope.launch {
|
||||
FileManager(this@ImageGalleryActivity, this@ImageGalleryActivity)
|
||||
.processMultipleFiles(uriList, fileType,this@ImageGalleryActivity )
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
askPermissiom()
|
||||
}
|
||||
|
||||
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
|
||||
Toast.makeText(this@ImageGalleryActivity, "${copiedFiles.size} ${getString(R.string.images_hidden_successfully)}", Toast.LENGTH_SHORT).show()
|
||||
loadFiles()
|
||||
}
|
||||
|
||||
override fun onFileProcessFailed() {
|
||||
Toast.makeText(this@ImageGalleryActivity,
|
||||
getString(R.string.failed_to_hide_images), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun setupIntentSenderLauncher() {
|
||||
intentSenderLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartIntentSenderForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
loadFiles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun askPermissiom() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R){
|
||||
if (!Environment.isExternalStorageManager()){
|
||||
val intent = Intent().setAction(Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
else {
|
||||
checkAndRequestStoragePermission()
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkAndRequestStoragePermission() {
|
||||
if (ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED ||
|
||||
ContextCompat.checkSelfPermission(
|
||||
this,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
ActivityCompat.requestPermissions(
|
||||
this,
|
||||
arrayOf(
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
),
|
||||
STORAGE_PERMISSION_CODE
|
||||
)
|
||||
} else {
|
||||
//storage permission granted
|
||||
}
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
if (requestCode == STORAGE_PERMISSION_CODE) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Toast.makeText(this,
|
||||
getString(R.string.storage_permissions_granted), Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(this,
|
||||
getString(R.string.storage_permissions_denied), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupFabButton() {
|
||||
binding.fabAdd.setOnClickListener {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
type = "image/*"
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
}
|
||||
pickImageLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun openPreview() {
|
||||
val intent = Intent(this, PreviewActivity::class.java).apply {
|
||||
putExtra("type", fileType)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.RequiresApi
|
||||
@@ -22,8 +23,9 @@ import devs.org.calculator.utils.FileManager
|
||||
import devs.org.calculator.utils.PrefsUtil
|
||||
import net.objecthunter.exp4j.ExpressionBuilder
|
||||
import java.util.regex.Pattern
|
||||
import androidx.core.content.edit
|
||||
|
||||
class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
class MainActivity : AppCompatActivity(), DialogActionsCallback, DialogUtil.DialogCallback {
|
||||
private lateinit var binding: ActivityMainBinding
|
||||
private var currentExpression = "0"
|
||||
private var lastWasOperator = false
|
||||
@@ -33,30 +35,51 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
private lateinit var baseDocumentTreeUri: Uri
|
||||
private val dialogUtil = DialogUtil(this)
|
||||
private val fileManager = FileManager(this, this)
|
||||
private val sp by lazy { getSharedPreferences("app", MODE_PRIVATE) }
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.R)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivityMainBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
enableEdgeToEdge()
|
||||
launcher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
handleActivityResult(result)
|
||||
}
|
||||
|
||||
// Ask permission
|
||||
if (sp.getBoolean("isFirst", true)){
|
||||
binding.display.text = getString(R.string.enter_123456)
|
||||
}
|
||||
|
||||
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" +
|
||||
getString(R.string.storage_permission),
|
||||
getString(R.string.to_ensure_the_app_works_properly_and_allows_you_to_easily_hide_or_un_hide_your_private_files_please_grant_storage_access_permission) +
|
||||
"\n" +
|
||||
"For devices running Android 11 or higher, you'll need to grant the 'All Files Access' permission.",
|
||||
"Grant",
|
||||
"Cancel",
|
||||
this
|
||||
getString(R.string.for_devices_running_android_11_or_higher_you_ll_need_to_grant_the_all_files_access_permission),
|
||||
getString(R.string.grant_permission),
|
||||
getString(R.string.later),
|
||||
object : DialogUtil.DialogCallback {
|
||||
override fun onPositiveButtonClicked() {
|
||||
fileManager.askPermission(this@MainActivity)
|
||||
}
|
||||
|
||||
override fun onNegativeButtonClicked() {
|
||||
Toast.makeText(this@MainActivity,
|
||||
getString(R.string.storage_permission_is_required_for_the_app_to_function_properly),
|
||||
Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onNaturalButtonClicked() {
|
||||
Toast.makeText(this@MainActivity,
|
||||
getString(R.string.you_can_grant_permission_later_from_settings),
|
||||
Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
setupNumberButton(binding.btn0, "0")
|
||||
setupNumberButton(binding.btn00, "00")
|
||||
setupNumberButton(binding.btn1, "1")
|
||||
setupNumberButton(binding.btn2, "2")
|
||||
setupNumberButton(binding.btn3, "3")
|
||||
@@ -78,6 +101,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
binding.cut.setOnClickListener { cutNumbers() }
|
||||
}
|
||||
|
||||
|
||||
private fun handleActivityResult(result: androidx.activity.result.ActivityResult) {
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
result.data?.data?.let { uri ->
|
||||
@@ -86,10 +110,8 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
contentResolver.takePersistableUriPermission(uri, takeFlags)
|
||||
|
||||
val preferences = getSharedPreferences("com.example.fileutility", MODE_PRIVATE)
|
||||
preferences.edit().putString("filestorageuri", uri.toString()).apply()
|
||||
preferences.edit { putString("filestorageuri", uri.toString()) }
|
||||
}
|
||||
} else {
|
||||
Log.e("FileUtility", "Error occurred or operation cancelled: $result")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,7 +150,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
}
|
||||
|
||||
private fun clearDisplay() {
|
||||
currentExpression = "0"
|
||||
currentExpression = ""
|
||||
binding.total.text = ""
|
||||
lastWasOperator = false
|
||||
lastWasPercent = false
|
||||
@@ -152,41 +174,24 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculatePercentage() {
|
||||
try {
|
||||
val value = currentExpression.toDouble()
|
||||
currentExpression = (value / 100).toString()
|
||||
updateDisplay()
|
||||
} catch (e: Exception) {
|
||||
binding.display.text = getString(R.string.invalid_message)
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
val percentageValue = number!!.toDouble() / 100
|
||||
processedExpression = processedExpression.replace(fullMatch!!.toString(), 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<Triple<Int, Int, String>>()
|
||||
|
||||
while (opMatcher.find()) {
|
||||
@@ -198,15 +203,11 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
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--
|
||||
@@ -217,7 +218,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
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--
|
||||
@@ -227,26 +227,24 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
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)
|
||||
if (leftNumberStart < start) {
|
||||
val leftPart = sb.substring(leftNumberStart, start)
|
||||
|
||||
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
|
||||
@@ -255,7 +253,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
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")
|
||||
@@ -277,14 +274,16 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
private fun evaluateExpression(expression: String): Double {
|
||||
return try {
|
||||
ExpressionBuilder(expression).build().evaluate()
|
||||
} catch (e: Exception) {
|
||||
} catch (_: Exception) {
|
||||
expression.toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("DefaultLocale")
|
||||
private fun calculateResult() {
|
||||
if (currentExpression == "123456") {
|
||||
val intent = Intent(this, SetupPasswordActivity::class.java)
|
||||
sp.edit { putBoolean("isFirst", false) }
|
||||
intent.putExtra("password", currentExpression)
|
||||
startActivity(intent)
|
||||
clearDisplay()
|
||||
@@ -292,7 +291,7 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
}
|
||||
|
||||
if (PrefsUtil(this).validatePassword(currentExpression)) {
|
||||
val intent = Intent(this, HiddenVaultActivity::class.java)
|
||||
val intent = Intent(this, HiddenActivity::class.java)
|
||||
intent.putExtra("password", currentExpression)
|
||||
startActivity(intent)
|
||||
clearDisplay()
|
||||
@@ -300,10 +299,8 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
}
|
||||
|
||||
try {
|
||||
// Replace '×' with '*' for the expression evaluator
|
||||
var processedExpression = currentExpression.replace("×", "*")
|
||||
|
||||
// Process percentages in the expression
|
||||
if (processedExpression.contains("%")) {
|
||||
processedExpression = preprocessExpression(processedExpression)
|
||||
}
|
||||
@@ -338,15 +335,11 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
}
|
||||
|
||||
try {
|
||||
// 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("%")) {
|
||||
@@ -361,9 +354,10 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
} else {
|
||||
String.format("%.2f", result)
|
||||
}
|
||||
|
||||
binding.total.text = formattedResult
|
||||
} catch (e: Exception) {
|
||||
if (sp.getBoolean("isFirst", true) && (currentExpression == "123456" || binding.display.text.toString() == "123456")){
|
||||
binding.total.text = getString(R.string.now_enter_button)
|
||||
}else binding.total.text = formattedResult
|
||||
} catch (_: Exception) {
|
||||
binding.total.text = ""
|
||||
}
|
||||
}
|
||||
@@ -376,7 +370,6 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
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())) {
|
||||
@@ -396,20 +389,20 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
|
||||
}
|
||||
|
||||
override fun onNegativeButtonClicked() {
|
||||
|
||||
Toast.makeText(this, getString(R.string.storage_permission_is_required_for_the_app_to_function_properly), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
override fun onNaturalButtonClicked() {
|
||||
|
||||
Toast.makeText(this, getString(R.string.you_can_grant_permission_later_from_settings), Toast.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
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()
|
||||
Toast.makeText(this, getString(R.string.permission_granted), Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show()
|
||||
Toast.makeText(this, getString(R.string.permission_denied), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,15 +2,22 @@ package devs.org.calculator.activities
|
||||
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.WindowManager
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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.database.AppDatabase
|
||||
import devs.org.calculator.database.HiddenFileRepository
|
||||
import devs.org.calculator.databinding.ActivityPreviewBinding
|
||||
import devs.org.calculator.utils.DialogUtil
|
||||
import devs.org.calculator.utils.FileManager
|
||||
import devs.org.calculator.utils.PrefsUtil
|
||||
import devs.org.calculator.utils.SecurityUtils
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
@@ -18,12 +25,18 @@ class PreviewActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivityPreviewBinding
|
||||
private var currentPosition: Int = 0
|
||||
private lateinit var files: List<File>
|
||||
private var files: MutableList<File> = mutableListOf()
|
||||
private lateinit var type: String
|
||||
private lateinit var folder: String
|
||||
private lateinit var filetype: FileManager.FileType
|
||||
private lateinit var adapter: ImagePreviewAdapter
|
||||
private lateinit var fileManager: FileManager
|
||||
private val dialogUtil = DialogUtil(this)
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private val prefs: PrefsUtil by lazy { PrefsUtil(this) }
|
||||
private val hiddenFileRepository: HiddenFileRepository by lazy {
|
||||
HiddenFileRepository(AppDatabase.getDatabase(this).hiddenFileDao())
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -33,21 +46,38 @@ class PreviewActivity : AppCompatActivity() {
|
||||
fileManager = FileManager(this, this)
|
||||
|
||||
currentPosition = intent.getIntExtra("position", 0)
|
||||
type = intent.getStringExtra("type").toString()
|
||||
type = intent.getStringExtra("type") ?: "IMAGE"
|
||||
folder = intent.getStringExtra("folder") ?: ""
|
||||
|
||||
setupFileType()
|
||||
files = fileManager.getFilesInHiddenDir(filetype)
|
||||
|
||||
loadFiles()
|
||||
setupImagePreview()
|
||||
clickListeners()
|
||||
setupClickListeners()
|
||||
setupPageChangeCallback()
|
||||
}
|
||||
|
||||
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
|
||||
}
|
||||
})
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
setupFlagSecure()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
adapter.releaseAllResources()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
adapter.releaseAllResources()
|
||||
}
|
||||
|
||||
private fun setupFlagSecure() {
|
||||
if (prefs.getBoolean("screenshot_restriction", true)) {
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupFileType() {
|
||||
@@ -56,14 +86,17 @@ class PreviewActivity : AppCompatActivity() {
|
||||
filetype = FileManager.FileType.IMAGE
|
||||
binding.title.text = getString(R.string.preview_images)
|
||||
}
|
||||
|
||||
"VIDEO" -> {
|
||||
filetype = FileManager.FileType.VIDEO
|
||||
binding.title.text = getString(R.string.preview_videos)
|
||||
}
|
||||
|
||||
"AUDIO" -> {
|
||||
filetype = FileManager.FileType.AUDIO
|
||||
binding.title.text = getString(R.string.preview_audios)
|
||||
}
|
||||
|
||||
else -> {
|
||||
filetype = FileManager.FileType.DOCUMENT
|
||||
binding.title.text = getString(R.string.preview_documents)
|
||||
@@ -71,110 +104,243 @@ class PreviewActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFiles() {
|
||||
try {
|
||||
val filesList = fileManager.getFilesInHiddenDirFromFolder(filetype, folder = folder)
|
||||
files = filesList.toMutableList()
|
||||
|
||||
if (currentPosition >= files.size) {
|
||||
currentPosition = 0
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
files = mutableListOf()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupImagePreview() {
|
||||
adapter = ImagePreviewAdapter(this, filetype)
|
||||
if (files.isEmpty()) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
adapter = ImagePreviewAdapter(this, this)
|
||||
adapter.images = files
|
||||
binding.viewPager.adapter = adapter
|
||||
|
||||
binding.viewPager.setCurrentItem(currentPosition, false)
|
||||
|
||||
val fileUri = Uri.fromFile(files[currentPosition])
|
||||
val fileName = FileManager.FileName(this).getFileNameFromUri(fileUri).toString()
|
||||
}
|
||||
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
if (filetype == FileManager.FileType.AUDIO) {
|
||||
(binding.viewPager.adapter as? ImagePreviewAdapter)?.currentMediaPlayer?.pause()
|
||||
if (currentPosition < files.size) {
|
||||
binding.viewPager.setCurrentItem(currentPosition, false)
|
||||
}
|
||||
|
||||
updateFileInfo()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
if (filetype == FileManager.FileType.AUDIO) {
|
||||
(binding.viewPager.adapter as? ImagePreviewAdapter)?.let { adapter ->
|
||||
adapter.currentMediaPlayer?.release()
|
||||
adapter.currentMediaPlayer = null
|
||||
private fun setupPageChangeCallback() {
|
||||
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
currentPosition = position
|
||||
updateFileInfo()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun updateFileInfo() {
|
||||
if (files.isNotEmpty() && currentPosition < files.size) {
|
||||
val fileUri = Uri.fromFile(files[currentPosition])
|
||||
val fileName = FileManager.FileName(this).getFileNameFromUri(fileUri) ?: "Unknown"
|
||||
//For Now File Name not Needed, i am keeping it for later use
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupClickListeners() {
|
||||
binding.back.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun clickListeners() {
|
||||
binding.delete.setOnClickListener {
|
||||
val fileUri = FileManager.FileManager().getContentUriImage(this, files[binding.viewPager.currentItem], filetype)
|
||||
if (fileUri != null) {
|
||||
dialogUtil.showMaterialDialog(
|
||||
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 {
|
||||
FileManager(this@PreviewActivity, this@PreviewActivity).deletePhotoFromExternalStorage(fileUri)
|
||||
removeFileFromList(binding.viewPager.currentItem)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNegativeButtonClicked() {
|
||||
|
||||
}
|
||||
|
||||
override fun onNaturalButtonClicked() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
handleDeleteFile()
|
||||
}
|
||||
|
||||
binding.unHide.setOnClickListener {
|
||||
val fileUri = FileManager.FileManager().getContentUriImage(this, files[binding.viewPager.currentItem], filetype)
|
||||
if (fileUri != null) {
|
||||
dialogUtil.showMaterialDialog(
|
||||
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 {
|
||||
FileManager(this@PreviewActivity, this@PreviewActivity).copyFileToNormalDir(fileUri)
|
||||
removeFileFromList(binding.viewPager.currentItem)
|
||||
performFileUnHiding()
|
||||
}
|
||||
binding.viewPager.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
|
||||
override fun onPageSelected(position: Int) {
|
||||
super.onPageSelected(position)
|
||||
adapter.onItemScrolledAway(currentPosition)
|
||||
currentPosition = position
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun handleDeleteFile() {
|
||||
if (files.isEmpty() || currentPosition >= files.size) return
|
||||
|
||||
val currentFile = files[currentPosition]
|
||||
val fileUri = FileManager.FileManager().getContentUriImage(this, currentFile)
|
||||
|
||||
if (fileUri != null) {
|
||||
dialogUtil.showMaterialDialog(
|
||||
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 : DialogUtil.DialogCallback {
|
||||
override fun onPositiveButtonClicked() {
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val hiddenFile =
|
||||
hiddenFileRepository.getHiddenFileByPath(currentFile.absolutePath)
|
||||
hiddenFile?.let {
|
||||
hiddenFileRepository.deleteHiddenFile(it)
|
||||
}
|
||||
|
||||
fileManager.deletePhotoFromExternalStorage(fileUri)
|
||||
removeFileFromList(currentPosition)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNegativeButtonClicked() {
|
||||
|
||||
}
|
||||
|
||||
override fun onNaturalButtonClicked() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onNegativeButtonClicked() {}
|
||||
|
||||
override fun onNaturalButtonClicked() {}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun performFileUnHiding() {
|
||||
if (files.isEmpty() || currentPosition >= files.size) return
|
||||
val file = files[currentPosition]
|
||||
|
||||
dialogUtil.showMaterialDialog(
|
||||
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 : DialogUtil.DialogCallback {
|
||||
override fun onPositiveButtonClicked() {
|
||||
lifecycleScope.launch {
|
||||
var allUnhidden = true
|
||||
try {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
if (hiddenFile != null){
|
||||
if (hiddenFile.isEncrypted) {
|
||||
val originalExtension = hiddenFile.originalExtension
|
||||
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
|
||||
|
||||
if (SecurityUtils.decryptFile(this@PreviewActivity, file, decryptedFile)) {
|
||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
val fileUri = FileManager.FileManager()
|
||||
.getContentUriImage(this@PreviewActivity, decryptedFile)
|
||||
if (fileUri != null) {
|
||||
val result = fileManager.copyFileToNormalDir(fileUri)
|
||||
if (result != null) {
|
||||
hiddenFile.let {
|
||||
hiddenFileRepository.deleteHiddenFile(it)
|
||||
}
|
||||
file.delete()
|
||||
decryptedFile.delete()
|
||||
removeFileFromList(currentPosition)
|
||||
} else {
|
||||
decryptedFile.delete()
|
||||
allUnhidden = false
|
||||
}
|
||||
} else {
|
||||
decryptedFile.delete()
|
||||
allUnhidden = false
|
||||
}
|
||||
} else {
|
||||
decryptedFile.delete()
|
||||
allUnhidden = false
|
||||
}
|
||||
} else {
|
||||
if (decryptedFile.exists()) {
|
||||
decryptedFile.delete()
|
||||
}
|
||||
allUnhidden = false
|
||||
}
|
||||
} else {
|
||||
val fileUri = FileManager.FileManager().getContentUriImage(this@PreviewActivity, file)
|
||||
if (fileUri != null) {
|
||||
val result = fileManager.copyFileToNormalDir(fileUri)
|
||||
if (result != null) {
|
||||
hiddenFile.let {
|
||||
hiddenFileRepository.deleteHiddenFile(it)
|
||||
}
|
||||
file.delete()
|
||||
removeFileFromList(currentPosition)
|
||||
} else {
|
||||
allUnhidden = false
|
||||
}
|
||||
} else {
|
||||
allUnhidden = false
|
||||
}
|
||||
}
|
||||
}else{
|
||||
val fileUri = FileManager.FileManager().getContentUriImage(this@PreviewActivity, file)
|
||||
if (fileUri != null) {
|
||||
val result = fileManager.copyFileToNormalDir(fileUri)
|
||||
if (result != null) {
|
||||
file.delete()
|
||||
removeFileFromList(currentPosition)
|
||||
} else {
|
||||
allUnhidden = false
|
||||
}
|
||||
} else {
|
||||
allUnhidden = false
|
||||
}
|
||||
}
|
||||
|
||||
} catch (_: Exception) {
|
||||
|
||||
allUnhidden = false
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
val message = if (allUnhidden) {
|
||||
getString(R.string.file_unhidden_successfully)
|
||||
} else {
|
||||
getString(R.string.something_went_wrong)
|
||||
}
|
||||
|
||||
Toast.makeText(this@PreviewActivity, message, Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNegativeButtonClicked() {}
|
||||
|
||||
override fun onNaturalButtonClicked() {}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
}
|
||||
|
||||
private fun removeFileFromList(position: Int) {
|
||||
val updatedFiles = files.toMutableList().apply { removeAt(position) }
|
||||
files = updatedFiles
|
||||
adapter.images = updatedFiles // Update adapter with the new list
|
||||
|
||||
// Update the ViewPager's position
|
||||
if (updatedFiles.isEmpty()) finish()
|
||||
if (position < 0 || position >= files.size) return
|
||||
adapter.releaseAllResources()
|
||||
files.removeAt(position)
|
||||
adapter.images = files
|
||||
if (files.isEmpty()) {
|
||||
finish()
|
||||
return
|
||||
}
|
||||
currentPosition = if (position >= files.size) {
|
||||
files.size - 1
|
||||
} else {
|
||||
position
|
||||
}
|
||||
|
||||
binding.viewPager.setCurrentItem(currentPosition, false)
|
||||
updateFileInfo()
|
||||
}
|
||||
|
||||
override fun onSupportNavigateUp(): Boolean {
|
||||
onBackPressed()
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package devs.org.calculator.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.WindowManager
|
||||
import android.widget.EditText
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.net.toUri
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import devs.org.calculator.R
|
||||
import devs.org.calculator.databinding.ActivitySettingsBinding
|
||||
import devs.org.calculator.utils.PrefsUtil
|
||||
import devs.org.calculator.utils.SecurityUtils
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var binding: ActivitySettingsBinding
|
||||
private lateinit var prefs: PrefsUtil
|
||||
private var DEV_GITHUB_URL = ""
|
||||
private var GITHUB_URL = ""
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = ActivitySettingsBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
prefs = PrefsUtil(this)
|
||||
DEV_GITHUB_URL = getString(R.string.github_profile)
|
||||
GITHUB_URL = getString(R.string.calculator_hide_files, DEV_GITHUB_URL)
|
||||
setupUI()
|
||||
loadSettings()
|
||||
setupListeners()
|
||||
}
|
||||
|
||||
private fun setupUI() {
|
||||
binding.back.setOnClickListener {
|
||||
onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun loadSettings() {
|
||||
|
||||
binding.dynamicThemeSwitch.isChecked = prefs.getBoolean("dynamic_theme", true)
|
||||
|
||||
val themeMode = prefs.getInt("theme_mode", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
binding.themeModeSwitch.isChecked = themeMode != AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
|
||||
when (themeMode) {
|
||||
AppCompatDelegate.MODE_NIGHT_YES -> binding.darkThemeRadio.isChecked = true
|
||||
AppCompatDelegate.MODE_NIGHT_NO -> binding.lightThemeRadio.isChecked = true
|
||||
else -> binding.systemThemeRadio.isChecked = true
|
||||
}
|
||||
|
||||
val isUsingCustomKey = SecurityUtils.isUsingCustomKey(this)
|
||||
binding.customKeyStatus.isChecked = isUsingCustomKey
|
||||
binding.screenshotRestrictionSwitch.isChecked = prefs.getBoolean("screenshot_restriction", true)
|
||||
binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true)
|
||||
binding.encryptionSwitch.isChecked = prefs.getBoolean("encryption", false)
|
||||
|
||||
updateThemeModeVisibility()
|
||||
}
|
||||
|
||||
private fun setupListeners() {
|
||||
|
||||
binding.githubButton.setOnClickListener {
|
||||
openUrl(GITHUB_URL)
|
||||
}
|
||||
|
||||
binding.devGithubButton.setOnClickListener {
|
||||
openUrl(DEV_GITHUB_URL)
|
||||
}
|
||||
|
||||
binding.dynamicThemeSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.setBoolean("dynamic_theme", isChecked)
|
||||
if (!isChecked) {
|
||||
showThemeModeDialog()
|
||||
}else{
|
||||
showThemeModeDialog()
|
||||
if (!prefs.getBoolean("isAppReopened",false)){
|
||||
DynamicColors.applyToActivityIfAvailable(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
binding.encryptionSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.setBoolean("encryption", isChecked)
|
||||
}
|
||||
|
||||
|
||||
binding.themeModeSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
binding.themeRadioGroup.visibility = if (isChecked) View.VISIBLE else View.GONE
|
||||
if (!isChecked) {
|
||||
binding.systemThemeRadio.isChecked = true
|
||||
applyThemeMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
}
|
||||
|
||||
binding.themeRadioGroup.setOnCheckedChangeListener { _, checkedId ->
|
||||
val themeMode = when (checkedId) {
|
||||
R.id.lightThemeRadio -> AppCompatDelegate.MODE_NIGHT_NO
|
||||
R.id.darkThemeRadio -> AppCompatDelegate.MODE_NIGHT_YES
|
||||
else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||
}
|
||||
applyThemeMode(themeMode)
|
||||
}
|
||||
|
||||
binding.screenshotRestrictionSwitch.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.setBoolean("screenshot_restriction", isChecked)
|
||||
if (isChecked) {
|
||||
enableScreenshotRestriction()
|
||||
} else {
|
||||
disableScreenshotRestriction()
|
||||
}
|
||||
}
|
||||
binding.showFileNames.setOnCheckedChangeListener { _, isChecked ->
|
||||
prefs.setBoolean("showFileName", isChecked)
|
||||
}
|
||||
|
||||
binding.customKeyStatus.setOnClickListener {
|
||||
showCustomKeyDialog()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateThemeModeVisibility() {
|
||||
binding.themeRadioGroup.visibility = if (binding.themeModeSwitch.isChecked) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun showThemeModeDialog() {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.attention))
|
||||
.setMessage(getString(R.string.if_you_turn_on_off_this_option_dynamic_theme_changes_will_be_visible_after_you_reopen_the_app))
|
||||
.setPositiveButton(getString(R.string.ok)) { _, _ ->
|
||||
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun applyThemeMode(themeMode: Int) {
|
||||
prefs.setInt("theme_mode", themeMode)
|
||||
AppCompatDelegate.setDefaultNightMode(themeMode)
|
||||
}
|
||||
|
||||
private fun enableScreenshotRestriction() {
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE
|
||||
)
|
||||
}
|
||||
|
||||
private fun disableScreenshotRestriction() {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
}
|
||||
|
||||
private fun openUrl(url: String) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
|
||||
startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Snackbar.make(binding.root,
|
||||
getString(R.string.could_not_open_url), Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCustomKeyDialog() {
|
||||
val dialogView = layoutInflater.inflate(R.layout.dialog_custom_key, null)
|
||||
val keyInput = dialogView.findViewById<EditText>(R.id.keyInput)
|
||||
val confirmKeyInput = dialogView.findViewById<EditText>(R.id.confirmKeyInput)
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.set_custom_encryption_key))
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(getString(R.string.set)) { dialog, _ ->
|
||||
val key = keyInput.text.toString()
|
||||
val confirmKey = confirmKeyInput.text.toString()
|
||||
|
||||
if (key.isEmpty()) {
|
||||
Toast.makeText(this, getString(R.string.key_cannot_be_empty), Toast.LENGTH_SHORT).show()
|
||||
updateUI()
|
||||
return@setPositiveButton
|
||||
}
|
||||
|
||||
if (key != confirmKey) {
|
||||
Toast.makeText(this, getString(R.string.keys_do_not_match), Toast.LENGTH_SHORT).show()
|
||||
updateUI()
|
||||
return@setPositiveButton
|
||||
}
|
||||
|
||||
if (SecurityUtils.setCustomKey(this, key)) {
|
||||
Toast.makeText(this,
|
||||
getString(R.string.custom_key_set_successfully), Toast.LENGTH_SHORT).show()
|
||||
updateUI()
|
||||
} else {
|
||||
Toast.makeText(this,
|
||||
getString(R.string.failed_to_set_custom_key), Toast.LENGTH_SHORT).show()
|
||||
updateUI()
|
||||
}
|
||||
}
|
||||
.setNegativeButton(getString(R.string.cancel)){ _, _ ->
|
||||
updateUI()
|
||||
}
|
||||
.setNeutralButton(getString(R.string.delete_key)) { _, _ ->
|
||||
SecurityUtils.clearCustomKey(this)
|
||||
Toast.makeText(this,
|
||||
getString(R.string.custom_encryption_key_cleared), Toast.LENGTH_SHORT).show()
|
||||
updateUI()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun updateUI() {
|
||||
binding.showFileNames.isChecked = prefs.getBoolean("showFileName", true)
|
||||
binding.encryptionSwitch.isChecked = prefs.getBoolean("encryption", false)
|
||||
val isUsingCustomKey = SecurityUtils.isUsingCustomKey(this)
|
||||
binding.customKeyStatus.isChecked = isUsingCustomKey
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import android.view.LayoutInflater
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.TextInputEditText
|
||||
import devs.org.calculator.databinding.ActivitySetupPasswordBinding
|
||||
@@ -18,6 +19,7 @@ class SetupPasswordActivity : AppCompatActivity() {
|
||||
private lateinit var binding2: ActivityChangePasswordBinding
|
||||
private lateinit var prefsUtil: PrefsUtil
|
||||
private var hasPassword = false
|
||||
private val prefs:PrefsUtil by lazy { PrefsUtil(this) }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
@@ -72,8 +74,6 @@ class SetupPasswordActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
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,
|
||||
getString(R.string.security_question_not_set_yet), Toast.LENGTH_SHORT).show()
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
package devs.org.calculator.activities
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
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
|
||||
import java.io.File
|
||||
|
||||
class VideoGalleryActivity : BaseGalleryActivity(), FileProcessCallback {
|
||||
override val fileType = FileManager.FileType.VIDEO
|
||||
private lateinit var pickLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
setupFabButton()
|
||||
|
||||
pickLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
val clipData = result.data?.clipData
|
||||
val uriList = mutableListOf<Uri>()
|
||||
|
||||
if (clipData != null) {
|
||||
for (i in 0 until clipData.itemCount) {
|
||||
val uri = clipData.getItemAt(i).uri
|
||||
uriList.add(uri)
|
||||
}
|
||||
} else {
|
||||
result.data?.data?.let { uriList.add(it) } // Single file selected
|
||||
}
|
||||
|
||||
if (uriList.isNotEmpty()) {
|
||||
lifecycleScope.launch {
|
||||
FileManager(this@VideoGalleryActivity, this@VideoGalleryActivity)
|
||||
.processMultipleFiles(uriList, fileType,this@VideoGalleryActivity )
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
|
||||
Toast.makeText(this@VideoGalleryActivity, "${copiedFiles.size} ${getString(R.string.videos_hidden_successfully)}"
|
||||
, Toast.LENGTH_SHORT).show()
|
||||
loadFiles()
|
||||
}
|
||||
|
||||
override fun onFileProcessFailed() {
|
||||
Toast.makeText(this@VideoGalleryActivity,
|
||||
getString(R.string.failed_to_hide_videos), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
|
||||
private fun setupFabButton() {
|
||||
binding.fabAdd.setOnClickListener {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
type = "video/*"
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
}
|
||||
pickLauncher.launch(intent)
|
||||
}
|
||||
}
|
||||
|
||||
override fun openPreview() {
|
||||
val intent = Intent(this, PreviewActivity::class.java).apply {
|
||||
putExtra("type", fileType)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,955 @@
|
||||
package devs.org.calculator.activities
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Environment
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import devs.org.calculator.R
|
||||
import devs.org.calculator.adapters.FileAdapter
|
||||
import devs.org.calculator.adapters.FolderSelectionAdapter
|
||||
import devs.org.calculator.callbacks.FileProcessCallback
|
||||
import devs.org.calculator.database.HiddenFileEntity
|
||||
import devs.org.calculator.databinding.ActivityViewFolderBinding
|
||||
import devs.org.calculator.databinding.ProccessingDialogBinding
|
||||
import devs.org.calculator.utils.DialogUtil
|
||||
import devs.org.calculator.utils.FileManager
|
||||
import devs.org.calculator.utils.FileManager.Companion.ENCRYPTED_EXTENSION
|
||||
import devs.org.calculator.utils.FileManager.Companion.HIDDEN_DIR
|
||||
import devs.org.calculator.utils.FolderManager
|
||||
import devs.org.calculator.utils.PrefsUtil
|
||||
import devs.org.calculator.utils.SecurityUtils
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import android.widget.CheckBox
|
||||
import android.widget.CompoundButton
|
||||
import android.app.AlertDialog
|
||||
|
||||
class ViewFolderActivity : AppCompatActivity() {
|
||||
|
||||
private var isFabOpen = false
|
||||
private lateinit var fabOpen: Animation
|
||||
private lateinit var fabClose: Animation
|
||||
private lateinit var rotateOpen: Animation
|
||||
private lateinit var rotateClose: Animation
|
||||
private lateinit var binding: ActivityViewFolderBinding
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
private lateinit var fileManager: FileManager
|
||||
private lateinit var folderManager: FolderManager
|
||||
private lateinit var dialogUtil: DialogUtil
|
||||
private var fileAdapter: FileAdapter? = null
|
||||
private var currentFolder: File? = null
|
||||
private val hiddenDir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
|
||||
private lateinit var pickImageLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
private var customDialog: androidx.appcompat.app.AlertDialog? = null
|
||||
|
||||
private var dialogShowTime: Long = 0
|
||||
private val MINIMUM_DIALOG_DURATION = 1200L
|
||||
private val prefs:PrefsUtil by lazy { PrefsUtil(this) }
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
binding = ActivityViewFolderBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setupAnimations()
|
||||
initialize()
|
||||
setupClickListeners()
|
||||
closeFabs()
|
||||
val folder = intent.getStringExtra("folder").toString()
|
||||
currentFolder = File(folder)
|
||||
if (currentFolder != null){
|
||||
openFolder(currentFolder!!)
|
||||
}else {
|
||||
showEmptyState()
|
||||
}
|
||||
|
||||
setupActivityResultLaunchers()
|
||||
}
|
||||
|
||||
private fun initialize() {
|
||||
fileManager = FileManager(this, this)
|
||||
folderManager = FolderManager()
|
||||
dialogUtil = DialogUtil(this)
|
||||
|
||||
}
|
||||
|
||||
private fun setupActivityResultLaunchers() {
|
||||
pickImageLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == RESULT_OK) {
|
||||
handleFilePickerResult(result.data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleFilePickerResult(data: Intent?) {
|
||||
val clipData = data?.clipData
|
||||
val uriList = mutableListOf<Uri>()
|
||||
|
||||
if (clipData != null) {
|
||||
for (i in 0 until clipData.itemCount) {
|
||||
val uri = clipData.getItemAt(i).uri
|
||||
uriList.add(uri)
|
||||
}
|
||||
} else {
|
||||
data?.data?.let { uriList.add(it) }
|
||||
}
|
||||
|
||||
if (uriList.isNotEmpty()) {
|
||||
processSelectedFiles(uriList)
|
||||
} else {
|
||||
Toast.makeText(this, getString(R.string.no_files_selected), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
private fun showCustomDialog(count: Int) {
|
||||
val dialogView = ProccessingDialogBinding.inflate(layoutInflater)
|
||||
customDialog = MaterialAlertDialogBuilder(this)
|
||||
.setView(dialogView.root)
|
||||
.setCancelable(false)
|
||||
.create()
|
||||
dialogView.title.text = "Hiding $count files"
|
||||
customDialog?.show()
|
||||
dialogShowTime = System.currentTimeMillis()
|
||||
}
|
||||
private fun dismissCustomDialog() {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val elapsedTime = currentTime - dialogShowTime
|
||||
|
||||
if (elapsedTime < MINIMUM_DIALOG_DURATION) {
|
||||
val remainingTime = MINIMUM_DIALOG_DURATION - elapsedTime
|
||||
mainHandler.postDelayed({
|
||||
customDialog?.dismiss()
|
||||
customDialog = null
|
||||
updateFilesToAdapter()
|
||||
}, remainingTime)
|
||||
} else {
|
||||
customDialog?.dismiss()
|
||||
customDialog = null
|
||||
updateFilesToAdapter()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateFilesToAdapter() {
|
||||
openFolder(currentFolder!!)
|
||||
}
|
||||
|
||||
|
||||
private fun processSelectedFiles(uriList: List<Uri>) {
|
||||
val targetFolder = currentFolder ?: hiddenDir
|
||||
if (!targetFolder.exists()) {
|
||||
targetFolder.mkdirs()
|
||||
File(targetFolder, ".nomedia").createNewFile()
|
||||
}
|
||||
|
||||
showCustomDialog(uriList.size)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
fileManager.processMultipleFiles(uriList, targetFolder,
|
||||
object : FileProcessCallback {
|
||||
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
|
||||
mainHandler.post {
|
||||
copiedFiles.forEach { file ->
|
||||
val fileType = fileManager.getFileType(file)
|
||||
var finalFile = file
|
||||
val extension = ".${file.extension}"
|
||||
val isEncrypted = prefs.getBoolean("encryption",false)
|
||||
|
||||
if (isEncrypted) {
|
||||
val encryptedFile = SecurityUtils.changeFileExtension(file, ENCRYPTED_EXTENSION)
|
||||
if (SecurityUtils.encryptFile(this@ViewFolderActivity, file, encryptedFile)) {
|
||||
finalFile = encryptedFile
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
|
||||
HiddenFileEntity(
|
||||
filePath = finalFile.absolutePath,
|
||||
fileName = file.name,
|
||||
fileType = fileType,
|
||||
originalExtension = extension,
|
||||
isEncrypted = isEncrypted,
|
||||
encryptedFileName = finalFile.name
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
mainHandler.postDelayed({
|
||||
dismissCustomDialog()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFileProcessFailed() {
|
||||
mainHandler.post {
|
||||
Toast.makeText(
|
||||
this@ViewFolderActivity,
|
||||
getString(R.string.failed_to_hide_files),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
dismissCustomDialog()
|
||||
}
|
||||
}
|
||||
})
|
||||
} catch (e: Exception) {
|
||||
mainHandler.post {
|
||||
Toast.makeText(
|
||||
this@ViewFolderActivity,
|
||||
getString(R.string.there_was_a_problem_in_the_folder),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
dismissCustomDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
refreshCurrentFolder()
|
||||
}
|
||||
|
||||
private fun openFolder(folder: File) {
|
||||
if (!folder.exists()) {
|
||||
folder.mkdirs()
|
||||
File(folder, ".nomedia").createNewFile()
|
||||
}
|
||||
|
||||
val files = folderManager.getFilesInFolder(folder)
|
||||
binding.folderName.text = folder.name
|
||||
|
||||
if (files.isNotEmpty()) {
|
||||
showFileList(files, folder)
|
||||
} else {
|
||||
showEmptyState()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEmptyState() {
|
||||
binding.noItems.visibility = View.VISIBLE
|
||||
binding.recyclerView.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showFileList(files: List<File>, folder: File) {
|
||||
binding.recyclerView.layoutManager = GridLayoutManager(this, 3)
|
||||
fileAdapter?.cleanup()
|
||||
|
||||
fileAdapter = FileAdapter(this, this, folder, prefs.getBoolean("showFileName", true),
|
||||
onFolderLongClick = { isSelected ->
|
||||
handleFileSelectionModeChange(isSelected)
|
||||
}).apply {
|
||||
setFileOperationCallback(object : FileAdapter.FileOperationCallback {
|
||||
override fun onFileDeleted(file: File) {
|
||||
refreshCurrentFolder()
|
||||
}
|
||||
|
||||
override fun onFileRenamed(oldFile: File, newFile: File) {
|
||||
refreshCurrentFolder()
|
||||
}
|
||||
|
||||
override fun onRefreshNeeded() {
|
||||
refreshCurrentFolder()
|
||||
}
|
||||
|
||||
override fun onSelectionModeChanged(isSelectionMode: Boolean, selectedCount: Int) {
|
||||
handleFileSelectionModeChange(isSelectionMode)
|
||||
}
|
||||
|
||||
override fun onSelectionCountChanged(selectedCount: Int) {
|
||||
updateSelectionCountDisplay(selectedCount)
|
||||
}
|
||||
})
|
||||
|
||||
submitList(files)
|
||||
}
|
||||
|
||||
binding.recyclerView.adapter = fileAdapter
|
||||
binding.recyclerView.visibility = View.VISIBLE
|
||||
binding.noItems.visibility = View.GONE
|
||||
|
||||
binding.menuButton.setOnClickListener {
|
||||
fileAdapter?.let { adapter ->
|
||||
showFileOptionsMenu(adapter.getSelectedItems())
|
||||
}
|
||||
}
|
||||
showFileViewIcons()
|
||||
}
|
||||
|
||||
|
||||
@Deprecated("This method has been deprecated in favor of using the\n {@link OnBackPressedDispatcher} via {@link #getOnBackPressedDispatcher()}.\n The OnBackPressedDispatcher controls how back button events are dispatched\n to one or more {@link OnBackPressedCallback} objects.")
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun onBackPressed() {
|
||||
handleBackPress()
|
||||
}
|
||||
|
||||
private fun handleBackPress() {
|
||||
if (fileAdapter?.onBackPressed() == true) {
|
||||
return
|
||||
}
|
||||
|
||||
super.onBackPressed()
|
||||
}
|
||||
|
||||
private fun handleFileSelectionModeChange(isSelectionMode: Boolean) {
|
||||
if (isSelectionMode) {
|
||||
showFileSelectionIcons()
|
||||
} else {
|
||||
showFileViewIcons()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelectionCountDisplay(selectedCount: Int) {
|
||||
if (selectedCount > 0) {
|
||||
showFileSelectionIcons()
|
||||
} else {
|
||||
showFileViewIcons()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFileOptionsMenu(selectedFiles: List<File>) {
|
||||
if (selectedFiles.isEmpty()) return
|
||||
|
||||
lifecycleScope.launch {
|
||||
var hasEncryptedFiles = false
|
||||
var hasDecryptedFiles = false
|
||||
var hasEncFilesWithoutMetadata = false
|
||||
|
||||
for (file in selectedFiles) {
|
||||
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||
|
||||
if (file.name.endsWith(ENCRYPTED_EXTENSION)) {
|
||||
if (hiddenFile?.isEncrypted == true) {
|
||||
hasEncryptedFiles = true
|
||||
} else {
|
||||
hasEncFilesWithoutMetadata = true
|
||||
}
|
||||
} else {
|
||||
hasDecryptedFiles = true
|
||||
}
|
||||
}
|
||||
|
||||
val options = mutableListOf(
|
||||
getString(R.string.un_hide),
|
||||
getString(R.string.delete),
|
||||
getString(R.string.copy_to_another_folder),
|
||||
getString(R.string.move_to_another_folder)
|
||||
)
|
||||
|
||||
if (hasDecryptedFiles) {
|
||||
options.add(getString(R.string.encrypt_file))
|
||||
}
|
||||
if (hasEncryptedFiles || hasEncFilesWithoutMetadata) {
|
||||
options.add(getString(R.string.decrypt_file))
|
||||
}
|
||||
|
||||
|
||||
MaterialAlertDialogBuilder(this@ViewFolderActivity)
|
||||
.setTitle(getString(R.string.file_options))
|
||||
.setItems(options.toTypedArray()) { _, which ->
|
||||
when (which) {
|
||||
0 -> unhideSelectedFiles(selectedFiles)
|
||||
1 -> deleteSelectedFiles(selectedFiles)
|
||||
2 -> copyToAnotherFolder(selectedFiles)
|
||||
3 -> moveToAnotherFolder(selectedFiles)
|
||||
else -> {
|
||||
val option = options[which]
|
||||
when (option) {
|
||||
getString(R.string.encrypt_file) -> fileAdapter?.encryptSelectedFiles()
|
||||
getString(R.string.decrypt_file) -> {
|
||||
lifecycleScope.launch {
|
||||
val filesWithoutMetadata = selectedFiles.filter { file ->
|
||||
file.name.endsWith(ENCRYPTED_EXTENSION) &&
|
||||
fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)?.isEncrypted != true
|
||||
}
|
||||
|
||||
if (filesWithoutMetadata.isNotEmpty()) {
|
||||
showDecryptionTypeDialog(filesWithoutMetadata)
|
||||
} else {
|
||||
fileAdapter?.decryptSelectedFiles()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showDecryptionTypeDialog(selectedFiles: List<File>) {
|
||||
val dialogView = layoutInflater.inflate(R.layout.dialog_file_type_selection, null)
|
||||
val imageCheckbox = dialogView.findViewById<CheckBox>(R.id.checkboxImage)
|
||||
val videoCheckbox = dialogView.findViewById<CheckBox>(R.id.checkboxVideo)
|
||||
val audioCheckbox = dialogView.findViewById<CheckBox>(R.id.checkboxAudio)
|
||||
val checkboxes = listOf(imageCheckbox, videoCheckbox, audioCheckbox)
|
||||
checkboxes.forEach { checkbox ->
|
||||
checkbox.setOnCheckedChangeListener { _, isChecked ->
|
||||
if (isChecked) {
|
||||
checkboxes.filter { it != checkbox }.forEach { it.isChecked = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(this)
|
||||
.setTitle(getString(R.string.select_file_type))
|
||||
.setMessage(getString(R.string.please_select_the_type_of_file_to_decrypt))
|
||||
.setView(dialogView)
|
||||
.setNegativeButton(getString(R.string.cancel), null)
|
||||
.setPositiveButton(getString(R.string.decrypt), null)
|
||||
.create()
|
||||
|
||||
dialog.setOnShowListener {
|
||||
val positiveButton = dialog.getButton(AlertDialog.BUTTON_POSITIVE)
|
||||
positiveButton.isEnabled = false
|
||||
val checkboxListener = CompoundButton.OnCheckedChangeListener { _, _ ->
|
||||
positiveButton.isEnabled = checkboxes.any { it.isChecked }
|
||||
}
|
||||
checkboxes.forEach { it.setOnCheckedChangeListener(checkboxListener) }
|
||||
}
|
||||
|
||||
dialog.setOnDismissListener {
|
||||
checkboxes.forEach { it.setOnCheckedChangeListener(null) }
|
||||
}
|
||||
|
||||
dialog.show()
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).setOnClickListener {
|
||||
val selectedType = when {
|
||||
imageCheckbox.isChecked -> FileManager.FileType.IMAGE
|
||||
videoCheckbox.isChecked -> FileManager.FileType.VIDEO
|
||||
audioCheckbox.isChecked -> FileManager.FileType.AUDIO
|
||||
else -> return@setOnClickListener
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
performDecryptionWithType(selectedFiles, selectedType)
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun performDecryptionWithType(selectedFiles: List<File>, fileType: FileManager.FileType) {
|
||||
lifecycleScope.launch {
|
||||
var successCount = 0
|
||||
var failCount = 0
|
||||
|
||||
for (file in selectedFiles) {
|
||||
try {
|
||||
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||
|
||||
|
||||
if (hiddenFile?.isEncrypted == true) {
|
||||
|
||||
val originalExtension = hiddenFile.originalExtension
|
||||
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
|
||||
|
||||
|
||||
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
|
||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
|
||||
hiddenFile.let {
|
||||
fileAdapter?.hiddenFileRepository?.updateEncryptionStatus(
|
||||
filePath = file.absolutePath,
|
||||
newFilePath = decryptedFile.absolutePath,
|
||||
encryptedFileName = decryptedFile.name,
|
||||
isEncrypted = false
|
||||
)
|
||||
}
|
||||
if (file.delete()) {
|
||||
|
||||
successCount++
|
||||
} else {
|
||||
|
||||
decryptedFile.delete()
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
|
||||
decryptedFile.delete()
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
|
||||
if (decryptedFile.exists()) {
|
||||
decryptedFile.delete()
|
||||
}
|
||||
failCount++
|
||||
}
|
||||
} else if (file.name.endsWith(ENCRYPTED_EXTENSION) && hiddenFile == null) {
|
||||
|
||||
val extension = when (fileType) {
|
||||
FileManager.FileType.IMAGE -> ".jpg"
|
||||
FileManager.FileType.VIDEO -> ".mp4"
|
||||
FileManager.FileType.AUDIO -> ".mp3"
|
||||
else -> ".txt"
|
||||
}
|
||||
|
||||
val decryptedFile = SecurityUtils.changeFileExtension(file, extension)
|
||||
|
||||
|
||||
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
|
||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
|
||||
|
||||
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
|
||||
HiddenFileEntity(
|
||||
filePath = decryptedFile.absolutePath,
|
||||
fileName = decryptedFile.name,
|
||||
encryptedFileName = file.name,
|
||||
fileType = fileType,
|
||||
originalExtension = extension,
|
||||
isEncrypted = false
|
||||
)
|
||||
)
|
||||
if (file.delete()) {
|
||||
|
||||
successCount++
|
||||
} else {
|
||||
|
||||
decryptedFile.delete()
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
|
||||
decryptedFile.delete()
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
|
||||
if (decryptedFile.exists()) {
|
||||
decryptedFile.delete()
|
||||
}
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
|
||||
failCount++
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
fileAdapter?.exitSelectionMode()
|
||||
when {
|
||||
successCount > 0 && failCount == 0 -> {
|
||||
Toast.makeText(this@ViewFolderActivity, "Decrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
successCount > 0 && failCount > 0 -> {
|
||||
Toast.makeText(this@ViewFolderActivity, "Decrypted $successCount file(s), failed to decrypt $failCount", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
failCount > 0 -> {
|
||||
Toast.makeText(this@ViewFolderActivity, "Failed to decrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
refreshCurrentFolder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveToAnotherFolder(selectedFiles: List<File>) {
|
||||
showFolderSelectionDialog { destinationFolder ->
|
||||
moveFilesToFolder(selectedFiles, destinationFolder)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun unhideSelectedFiles(selectedFiles: List<File>) {
|
||||
dialogUtil.showMaterialDialog(
|
||||
getString(R.string.un_hide_files),
|
||||
getString(R.string.are_you_sure_you_want_to_un_hide_selected_files),
|
||||
getString(R.string.un_hide),
|
||||
getString(R.string.cancel),
|
||||
object : DialogUtil.DialogCallback {
|
||||
override fun onPositiveButtonClicked() {
|
||||
performFileUnhiding(selectedFiles)
|
||||
|
||||
}
|
||||
|
||||
override fun onNegativeButtonClicked() {}
|
||||
|
||||
override fun onNaturalButtonClicked() {}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun deleteSelectedFiles(selectedFiles: List<File>) {
|
||||
dialogUtil.showMaterialDialog(
|
||||
getString(R.string.delete_file),
|
||||
getString(R.string.are_you_sure_to_delete_selected_files_permanently),
|
||||
getString(R.string.delete),
|
||||
getString(R.string.cancel),
|
||||
object : DialogUtil.DialogCallback {
|
||||
override fun onPositiveButtonClicked() {
|
||||
performFileDeletion(selectedFiles)
|
||||
}
|
||||
|
||||
override fun onNegativeButtonClicked() {}
|
||||
|
||||
override fun onNaturalButtonClicked() {}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
|
||||
private fun refreshCurrentFolder() {
|
||||
currentFolder?.let { folder ->
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val files = folderManager.getFilesInFolder(folder)
|
||||
mainHandler.post {
|
||||
if (files.isNotEmpty()) {
|
||||
binding.recyclerView.visibility = View.VISIBLE
|
||||
binding.noItems.visibility = View.GONE
|
||||
|
||||
fileAdapter?.submitList(files.toMutableList())
|
||||
fileAdapter?.let { adapter ->
|
||||
if (adapter.isInSelectionMode()) {
|
||||
showFileSelectionIcons()
|
||||
} else {
|
||||
showFileViewIcons()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showEmptyState()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
mainHandler.post {
|
||||
showEmptyState()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun setupClickListeners() {
|
||||
binding.fabExpend.setOnClickListener {
|
||||
if (isFabOpen) closeFabs()
|
||||
else openFabs()
|
||||
}
|
||||
binding.back.setOnClickListener {
|
||||
finish()
|
||||
}
|
||||
|
||||
binding.addImage.setOnClickListener { openFilePicker("image/*") }
|
||||
binding.addVideo.setOnClickListener { openFilePicker("video/*") }
|
||||
binding.addAudio.setOnClickListener { openFilePicker("audio/*") }
|
||||
binding.addDocument.setOnClickListener { openFilePicker("*/*") }
|
||||
}
|
||||
|
||||
private fun setupAnimations() {
|
||||
fabOpen = AnimationUtils.loadAnimation(this, R.anim.fab_open)
|
||||
fabClose = AnimationUtils.loadAnimation(this, R.anim.fab_close)
|
||||
rotateOpen = AnimationUtils.loadAnimation(this, R.anim.rotate_open)
|
||||
rotateClose = AnimationUtils.loadAnimation(this, R.anim.rotate_close)
|
||||
}
|
||||
|
||||
private fun openFilePicker(mimeType: String) {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
type = mimeType
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
|
||||
addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
}
|
||||
closeFabs()
|
||||
pickImageLauncher.launch(intent)
|
||||
}
|
||||
|
||||
private fun openFabs() {
|
||||
if (!isFabOpen) {
|
||||
binding.addImage.startAnimation(fabOpen)
|
||||
binding.addVideo.startAnimation(fabOpen)
|
||||
binding.addAudio.startAnimation(fabOpen)
|
||||
binding.addDocument.startAnimation(fabOpen)
|
||||
binding.fabExpend.startAnimation(rotateOpen)
|
||||
|
||||
binding.addImage.visibility = View.VISIBLE
|
||||
binding.addVideo.visibility = View.VISIBLE
|
||||
binding.addAudio.visibility = View.VISIBLE
|
||||
binding.addDocument.visibility = View.VISIBLE
|
||||
|
||||
isFabOpen = true
|
||||
mainHandler.postDelayed({
|
||||
binding.fabExpend.setImageResource(R.drawable.wrong)
|
||||
}, 200)
|
||||
}
|
||||
}
|
||||
|
||||
private fun closeFabs() {
|
||||
if (isFabOpen) {
|
||||
binding.addImage.startAnimation(fabClose)
|
||||
binding.addVideo.startAnimation(fabClose)
|
||||
binding.addAudio.startAnimation(fabClose)
|
||||
binding.addDocument.startAnimation(fabClose)
|
||||
binding.fabExpend.startAnimation(rotateClose)
|
||||
|
||||
binding.addImage.visibility = View.INVISIBLE
|
||||
binding.addVideo.visibility = View.INVISIBLE
|
||||
binding.addAudio.visibility = View.INVISIBLE
|
||||
binding.addDocument.visibility = View.INVISIBLE
|
||||
|
||||
isFabOpen = false
|
||||
binding.fabExpend.setImageResource(R.drawable.ic_add)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFileViewIcons() {
|
||||
binding.menuButton.visibility = View.GONE
|
||||
binding.fabExpend.visibility = View.VISIBLE
|
||||
binding.addImage.visibility = View.INVISIBLE
|
||||
binding.addVideo.visibility = View.INVISIBLE
|
||||
binding.addAudio.visibility = View.INVISIBLE
|
||||
binding.addDocument.visibility = View.INVISIBLE
|
||||
isFabOpen = false
|
||||
binding.fabExpend.setImageResource(R.drawable.ic_add)
|
||||
}
|
||||
|
||||
private fun showFileSelectionIcons() {
|
||||
binding.menuButton.visibility = View.VISIBLE
|
||||
binding.fabExpend.visibility = View.GONE
|
||||
binding.addImage.visibility = View.INVISIBLE
|
||||
binding.addVideo.visibility = View.INVISIBLE
|
||||
binding.addAudio.visibility = View.INVISIBLE
|
||||
binding.addDocument.visibility = View.INVISIBLE
|
||||
isFabOpen = false
|
||||
}
|
||||
|
||||
private fun performFileUnhiding(selectedFiles: List<File>) {
|
||||
lifecycleScope.launch {
|
||||
var allUnhidden = true
|
||||
selectedFiles.forEach { file ->
|
||||
try {
|
||||
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||
|
||||
if (hiddenFile?.isEncrypted == true) {
|
||||
val originalExtension = hiddenFile.originalExtension
|
||||
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
|
||||
|
||||
if (SecurityUtils.decryptFile(this@ViewFolderActivity, file, decryptedFile)) {
|
||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
val fileUri = FileManager.FileManager().getContentUriImage(this@ViewFolderActivity, decryptedFile)
|
||||
if (fileUri != null) {
|
||||
val result = fileManager.copyFileToNormalDir(fileUri)
|
||||
if (result != null) {
|
||||
hiddenFile.let {
|
||||
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
|
||||
}
|
||||
file.delete()
|
||||
decryptedFile.delete()
|
||||
} else {
|
||||
decryptedFile.delete()
|
||||
allUnhidden = false
|
||||
}
|
||||
} else {
|
||||
decryptedFile.delete()
|
||||
allUnhidden = false
|
||||
}
|
||||
} else {
|
||||
decryptedFile.delete()
|
||||
allUnhidden = false
|
||||
}
|
||||
} else {
|
||||
if (decryptedFile.exists()) {
|
||||
decryptedFile.delete()
|
||||
}
|
||||
allUnhidden = false
|
||||
}
|
||||
} else {
|
||||
val fileUri = FileManager.FileManager().getContentUriImage(this@ViewFolderActivity, file)
|
||||
if (fileUri != null) {
|
||||
val result = fileManager.copyFileToNormalDir(fileUri)
|
||||
if (result != null) {
|
||||
hiddenFile?.let {
|
||||
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
|
||||
}
|
||||
file.delete()
|
||||
} else {
|
||||
allUnhidden = false
|
||||
}
|
||||
} else {
|
||||
allUnhidden = false
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
allUnhidden = false
|
||||
}
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
val message = if (allUnhidden) {
|
||||
getString(R.string.files_unhidden_successfully)
|
||||
} else {
|
||||
getString(R.string.some_files_could_not_be_unhidden)
|
||||
}
|
||||
|
||||
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||
fileAdapter?.exitSelectionMode()
|
||||
refreshCurrentFolder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun performFileDeletion(selectedFiles: List<File>) {
|
||||
lifecycleScope.launch {
|
||||
var allDeleted = true
|
||||
selectedFiles.forEach { file ->
|
||||
try {
|
||||
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||
hiddenFile?.let {
|
||||
fileAdapter?.hiddenFileRepository?.deleteHiddenFile(it)
|
||||
}
|
||||
if (!file.delete()) {
|
||||
allDeleted = false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
allDeleted = false
|
||||
}
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
val message = if (allDeleted) {
|
||||
getString(R.string.files_deleted_successfully)
|
||||
} else {
|
||||
getString(R.string.some_items_could_not_be_deleted)
|
||||
}
|
||||
|
||||
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||
fileAdapter?.exitSelectionMode()
|
||||
refreshCurrentFolder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyToAnotherFolder(selectedFiles: List<File>) {
|
||||
showFolderSelectionDialog { destinationFolder ->
|
||||
copyFilesToFolder(selectedFiles, destinationFolder)
|
||||
}
|
||||
}
|
||||
|
||||
private fun copyFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
|
||||
lifecycleScope.launch {
|
||||
var allCopied = true
|
||||
selectedFiles.forEach { file ->
|
||||
try {
|
||||
val newFile = File(destinationFolder, file.name)
|
||||
file.copyTo(newFile, overwrite = true)
|
||||
|
||||
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||
hiddenFile?.let {
|
||||
fileAdapter?.hiddenFileRepository?.insertHiddenFile(
|
||||
HiddenFileEntity(
|
||||
filePath = newFile.absolutePath,
|
||||
fileName = it.fileName,
|
||||
fileType = it.fileType,
|
||||
originalExtension = it.originalExtension,
|
||||
isEncrypted = it.isEncrypted,
|
||||
encryptedFileName = it.encryptedFileName
|
||||
)
|
||||
)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
|
||||
allCopied = false
|
||||
}
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
val message = if (allCopied) getString(R.string.files_copied_successfully) else getString(R.string.some_files_could_not_be_copied)
|
||||
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||
fileAdapter?.exitSelectionMode()
|
||||
refreshCurrentFolder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun moveFilesToFolder(selectedFiles: List<File>, destinationFolder: File) {
|
||||
lifecycleScope.launch {
|
||||
var allMoved = true
|
||||
selectedFiles.forEach { file ->
|
||||
try {
|
||||
val newFile = File(destinationFolder, file.name)
|
||||
file.copyTo(newFile, overwrite = true)
|
||||
|
||||
val hiddenFile = fileAdapter?.hiddenFileRepository?.getHiddenFileByPath(file.absolutePath)
|
||||
hiddenFile?.let {
|
||||
fileAdapter?.hiddenFileRepository?.updateEncryptionStatus(
|
||||
filePath = file.absolutePath,
|
||||
newFilePath = newFile.absolutePath,
|
||||
encryptedFileName = it.encryptedFileName,
|
||||
isEncrypted = it.isEncrypted
|
||||
)
|
||||
}
|
||||
|
||||
file.delete()
|
||||
} catch (e: Exception) {
|
||||
|
||||
allMoved = false
|
||||
}
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
val message = if (allMoved) getString(R.string.files_moved_successfully) else getString(R.string.some_files_could_not_be_moved)
|
||||
Toast.makeText(this@ViewFolderActivity, message, Toast.LENGTH_SHORT).show()
|
||||
fileAdapter?.exitSelectionMode()
|
||||
refreshCurrentFolder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showFolderSelectionDialog(onFolderSelected: (File) -> Unit) {
|
||||
val folders = folderManager.getFoldersInDirectory(hiddenDir)
|
||||
.filter { it != currentFolder }
|
||||
|
||||
if (folders.isEmpty()) {
|
||||
Toast.makeText(this, getString(R.string.no_folders_available), Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
val bottomSheetView = LayoutInflater.from(this).inflate(R.layout.bottom_sheet_folder_selection, null)
|
||||
val recyclerView = bottomSheetView.findViewById<RecyclerView>(R.id.folderRecyclerView)
|
||||
|
||||
val bottomSheetDialog = BottomSheetDialog(this)
|
||||
bottomSheetDialog.setContentView(bottomSheetView)
|
||||
|
||||
recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
recyclerView.adapter = FolderSelectionAdapter(folders) { selectedFolder ->
|
||||
bottomSheetDialog.dismiss()
|
||||
onFolderSelected(selectedFolder)
|
||||
}
|
||||
|
||||
bottomSheetDialog.show()
|
||||
}
|
||||
}
|
||||
@@ -1,197 +1,846 @@
|
||||
package devs.org.calculator.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.EditText
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import android.widget.Toast
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import devs.org.calculator.R
|
||||
import devs.org.calculator.activities.PreviewActivity
|
||||
import devs.org.calculator.callbacks.DialogActionsCallback
|
||||
import devs.org.calculator.utils.DialogUtil
|
||||
import devs.org.calculator.database.AppDatabase
|
||||
import devs.org.calculator.database.HiddenFileEntity
|
||||
import devs.org.calculator.database.HiddenFileRepository
|
||||
import devs.org.calculator.utils.FileManager
|
||||
import devs.org.calculator.utils.SecurityUtils
|
||||
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
|
||||
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
class FileAdapter(
|
||||
private val fileType: FileManager.FileType,
|
||||
var context: Context,
|
||||
private var lifecycleOwner: LifecycleOwner
|
||||
) :
|
||||
ListAdapter<File, FileAdapter.FileViewHolder>(FileDiffCallback()) {
|
||||
private val context: Context,
|
||||
private val lifecycleOwner: LifecycleOwner,
|
||||
private val currentFolder: File,
|
||||
private val showFileName: Boolean,
|
||||
private val onFolderLongClick: (Boolean) -> Unit
|
||||
) : ListAdapter<File, FileAdapter.FileViewHolder>(FileDiffCallback()) {
|
||||
|
||||
private val selectedItems = mutableSetOf<Int>()
|
||||
private var isSelectionMode = false
|
||||
private var fileName = "Unknown File"
|
||||
private var fileTypes = when (fileType) {
|
||||
|
||||
FileManager.FileType.IMAGE -> {
|
||||
context.getString(R.string.image)
|
||||
}
|
||||
private var fileOperationCallback: WeakReference<FileOperationCallback>? = null
|
||||
|
||||
FileManager.FileType.VIDEO -> {
|
||||
context.getString(R.string.video)
|
||||
}
|
||||
private val fileExecutor = Executors.newSingleThreadExecutor()
|
||||
private val mainHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
FileManager.FileType.AUDIO -> {
|
||||
context.getString(R.string.audio)
|
||||
}
|
||||
val hiddenFileRepository: HiddenFileRepository by lazy {
|
||||
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
||||
}
|
||||
|
||||
else -> context.getString(R.string.document)
|
||||
interface FileOperationCallback {
|
||||
fun onFileDeleted(file: File)
|
||||
fun onFileRenamed(oldFile: File, newFile: File)
|
||||
fun onRefreshNeeded()
|
||||
fun onSelectionModeChanged(isSelectionMode: Boolean, selectedCount: Int)
|
||||
fun onSelectionCountChanged(selectedCount: Int)
|
||||
}
|
||||
|
||||
fun setFileOperationCallback(callback: FileOperationCallback?) {
|
||||
fileOperationCallback = callback?.let { WeakReference(it) }
|
||||
}
|
||||
|
||||
inner class FileViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
private val imageView: ImageView = view.findViewById(R.id.imageView)
|
||||
val imageView: ImageView = view.findViewById(R.id.fileIconImageView)
|
||||
val fileNameTextView: TextView = view.findViewById(R.id.fileNameTextView)
|
||||
val playIcon: ImageView = view.findViewById(R.id.videoPlay)
|
||||
val selectedLayer: View = view.findViewById(R.id.selectedLayer)
|
||||
val shade: View = view.findViewById(R.id.shade)
|
||||
val selected: ImageView = view.findViewById(R.id.selected)
|
||||
val encryptedIcon: ImageView = view.findViewById(R.id.encrypted)
|
||||
|
||||
fun bind(file: File) {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
val fileType = if (hiddenFile?.fileType != null) hiddenFile.fileType
|
||||
else {
|
||||
FileManager(context, lifecycleOwner).getFileType(file)
|
||||
}
|
||||
|
||||
setupFileDisplay(file, fileType, hiddenFile?.isEncrypted == true,hiddenFile)
|
||||
setupClickListeners(file, fileType)
|
||||
fileNameTextView.visibility = if (showFileName) View.VISIBLE else View.GONE
|
||||
shade.visibility = if (showFileName) View.VISIBLE else View.GONE
|
||||
|
||||
val position = adapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
val isSelected = selectedItems.contains(position)
|
||||
updateSelectionUI(isSelected)
|
||||
}
|
||||
encryptedIcon.visibility = if (hiddenFile?.isEncrypted == true) View.VISIBLE else View.GONE
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun bind(file: File, payloads: List<Any>) {
|
||||
if (payloads.isEmpty()) {
|
||||
bind(file)
|
||||
return
|
||||
}
|
||||
|
||||
val changes = payloads.firstOrNull() as? List<String>
|
||||
changes?.forEach { change ->
|
||||
when (change) {
|
||||
"NAME_CHANGED" -> {
|
||||
fileNameTextView.text = file.name
|
||||
}
|
||||
"SIZE_CHANGED", "MODIFIED_DATE_CHANGED" -> {
|
||||
|
||||
}
|
||||
"SELECTION_CHANGED" -> {
|
||||
val position = adapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
val isSelected = selectedItems.contains(position)
|
||||
updateSelectionUI(isSelected)
|
||||
notifySelectionModeChange()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSelectionUI(isSelected: Boolean) {
|
||||
selectedLayer.visibility = if (isSelected) View.VISIBLE else View.GONE
|
||||
selected.visibility = if (isSelected) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun setupFileDisplay(file: File, fileType: FileManager.FileType, isEncrypted: Boolean, metadata: HiddenFileEntity?) {
|
||||
fileNameTextView.text = metadata?.fileName ?: file.name
|
||||
|
||||
when (fileType) {
|
||||
FileManager.FileType.IMAGE -> {
|
||||
Glide.with(imageView)
|
||||
.load(file)
|
||||
.centerCrop()
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
FileManager.FileType.VIDEO -> {
|
||||
Glide.with(imageView)
|
||||
.asBitmap()
|
||||
.load(file)
|
||||
.centerCrop()
|
||||
.into(imageView)
|
||||
}
|
||||
|
||||
else -> {
|
||||
val resourceId = when (fileType) {
|
||||
FileManager.FileType.AUDIO -> R.drawable.ic_audio
|
||||
FileManager.FileType.DOCUMENT -> R.drawable.ic_document
|
||||
else -> R.drawable.ic_file
|
||||
}
|
||||
imageView.setImageResource(resourceId)
|
||||
}
|
||||
}
|
||||
itemView.setOnClickListener {
|
||||
|
||||
|
||||
when(fileType){
|
||||
FileManager.FileType.AUDIO -> {
|
||||
// Create an intent to play audio using available audio players
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(FileManager.FileManager().getContentUriImage(context, file, fileType), "audio/*")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
playIcon.visibility = View.GONE
|
||||
if (isEncrypted) {
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.no_audio_player_found), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
|
||||
FileManager.FileType.DOCUMENT -> {
|
||||
// Create an intent to open the document using available viewers or file managers
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(FileManager.FileManager().getContentUriImage(context, file, fileType), "*/*")
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
try {
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.no_suitable_app_found_to_open_this_document), Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val intent = Intent(context, PreviewActivity::class.java).apply {
|
||||
putExtra("type", fileTypes)
|
||||
putExtra("position", position)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
itemView.setOnLongClickListener {
|
||||
|
||||
val fileUri = FileManager.FileManager().getContentUriImage(context, file, fileType)
|
||||
if (fileUri == null) {
|
||||
Toast.makeText(context, "Unable to access file: $file", Toast.LENGTH_SHORT)
|
||||
.show()
|
||||
|
||||
return@setOnLongClickListener true
|
||||
|
||||
}
|
||||
fileName = FileManager.FileName(context).getFileNameFromUri(fileUri)?.toString()
|
||||
?: context.getString(R.string.unknown_file)
|
||||
|
||||
DialogUtil(context).showMaterialDialogWithNaturalButton(
|
||||
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 {
|
||||
FileManager(context, lifecycleOwner).deletePhotoFromExternalStorage(
|
||||
fileUri
|
||||
)
|
||||
val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
|
||||
if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
val uri = getUriForPreviewFile(context, decryptedFile)
|
||||
if (uri != null) {
|
||||
Glide.with(context)
|
||||
.load(uri)
|
||||
.centerCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.encrypted)
|
||||
.into(imageView)
|
||||
imageView.setPadding(0, 0, 0, 0)
|
||||
} else {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} else {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
val currentList = currentList.toMutableList()
|
||||
currentList.remove(file)
|
||||
submitList(currentList)
|
||||
}
|
||||
|
||||
override fun onNegativeButtonClicked() {
|
||||
FileManager(context, lifecycleOwner).copyFileToNormalDir(fileUri)
|
||||
val currentList = currentList.toMutableList()
|
||||
currentList.remove(file)
|
||||
submitList(currentList)
|
||||
}
|
||||
|
||||
override fun onNaturalButtonClicked() {
|
||||
|
||||
} catch (e: Exception) {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} else {
|
||||
Glide.with(context)
|
||||
.load(file)
|
||||
.centerCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.ic_image)
|
||||
.into(imageView)
|
||||
imageView.setPadding(0, 0, 0, 0)
|
||||
}
|
||||
)
|
||||
}
|
||||
FileManager.FileType.VIDEO -> {
|
||||
playIcon.visibility = View.VISIBLE
|
||||
if (isEncrypted) {
|
||||
try {
|
||||
val decryptedFile = getDecryptedPreviewFile(context, metadata!!)
|
||||
if (decryptedFile != null && decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
val uri = getUriForPreviewFile(context, decryptedFile)
|
||||
if (uri != null) {
|
||||
Glide.with(context)
|
||||
.load(uri)
|
||||
.centerCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.encrypted)
|
||||
.into(imageView)
|
||||
imageView.setPadding(0, 0, 0, 0)
|
||||
} else {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} else {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
showEncryptedIcon()
|
||||
}
|
||||
} else {
|
||||
Glide.with(context)
|
||||
.load(file)
|
||||
.centerCrop()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(true)
|
||||
.error(R.drawable.ic_video)
|
||||
.into(imageView)
|
||||
}
|
||||
}
|
||||
FileManager.FileType.AUDIO -> {
|
||||
playIcon.visibility = View.GONE
|
||||
if (isEncrypted) {
|
||||
imageView.setImageResource(R.drawable.encrypted)
|
||||
} else {
|
||||
imageView.setImageResource(R.drawable.ic_audio)
|
||||
}
|
||||
imageView.setPadding(50, 50, 50, 50)
|
||||
}
|
||||
else -> {
|
||||
playIcon.visibility = View.GONE
|
||||
if (isEncrypted) {
|
||||
imageView.setImageResource(R.drawable.encrypted)
|
||||
} else {
|
||||
imageView.setImageResource(R.drawable.ic_document)
|
||||
}
|
||||
imageView.setPadding(50, 50, 50, 50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return@setOnLongClickListener true
|
||||
private fun setupClickListeners(file: File, fileType: FileManager.FileType) {
|
||||
itemView.setOnClickListener {
|
||||
val position = adapterPosition
|
||||
if (position == RecyclerView.NO_POSITION) return@setOnClickListener
|
||||
|
||||
if (isSelectionMode) {
|
||||
toggleSelection(position)
|
||||
} else {
|
||||
openFile(file, fileType)
|
||||
}
|
||||
}
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
val position = adapterPosition
|
||||
if (position == RecyclerView.NO_POSITION) return@setOnLongClickListener false
|
||||
|
||||
if (!isSelectionMode) {
|
||||
enterSelectionMode()
|
||||
toggleSelection(position)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun openFile(file: File, fileType: FileManager.FileType) {
|
||||
if (!file.exists()) {
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.file_no_longer_exists), Toast.LENGTH_SHORT).show()
|
||||
return
|
||||
}
|
||||
|
||||
when (fileType) {
|
||||
FileManager.FileType.AUDIO -> openAudioFile(file)
|
||||
FileManager.FileType.IMAGE, FileManager.FileType.VIDEO -> {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
if (hiddenFile?.isEncrypted == true || file.extension == FileManager.ENCRYPTED_EXTENSION) {
|
||||
if (file.extension == FileManager.ENCRYPTED_EXTENSION && hiddenFile == null) {
|
||||
showDecryptionTypeDialog(file)
|
||||
} else {
|
||||
val tempFile = File(context.cacheDir, "preview_${file.name}")
|
||||
|
||||
if (SecurityUtils.decryptFile(context, file, tempFile)) {
|
||||
if (tempFile.exists() && tempFile.length() > 0) {
|
||||
mainHandler.post {
|
||||
val fileTypeString = when (fileType) {
|
||||
FileManager.FileType.IMAGE -> context.getString(R.string.image)
|
||||
FileManager.FileType.VIDEO -> context.getString(R.string.video)
|
||||
else -> "unknown"
|
||||
}
|
||||
|
||||
val intent = Intent(context, PreviewActivity::class.java).apply {
|
||||
putExtra("type", fileTypeString)
|
||||
putExtra("folder", currentFolder.toString())
|
||||
putExtra("position", adapterPosition)
|
||||
putExtra("isEncrypted", true)
|
||||
putExtra("tempFile", tempFile.absolutePath)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
} else {
|
||||
mainHandler.post {
|
||||
Toast.makeText(context, "Failed to prepare file for preview", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
mainHandler.post {
|
||||
Toast.makeText(context, "Failed to decrypt file for preview", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
openInPreview(fileType)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
mainHandler.post {
|
||||
Toast.makeText(context, "Error preparing file for preview", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
FileManager.FileType.DOCUMENT -> openDocumentFile(file)
|
||||
else -> openDocumentFile(file)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openAudioFile(file: File) {
|
||||
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
|
||||
try {
|
||||
val fileTypeString = when (fileType) {
|
||||
FileManager.FileType.IMAGE -> context.getString(R.string.image)
|
||||
FileManager.FileType.VIDEO -> context.getString(R.string.video)
|
||||
else -> "unknown"
|
||||
}
|
||||
|
||||
val intent = Intent(context, PreviewActivity::class.java).apply {
|
||||
putExtra("type", fileTypeString)
|
||||
putExtra("folder", currentFolder.toString())
|
||||
putExtra("position", adapterPosition)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.no_audio_player_found),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openDocumentFile(file: File) {
|
||||
try {
|
||||
val uri = FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.fileprovider",
|
||||
file
|
||||
)
|
||||
val intent = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "*/*")
|
||||
putExtra("folder", currentFolder.toString())
|
||||
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.no_suitable_app_found_to_open_this_document),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun openInPreview(fileType: FileManager.FileType) {
|
||||
val fileTypeString = when (fileType) {
|
||||
FileManager.FileType.IMAGE -> context.getString(R.string.image)
|
||||
FileManager.FileType.VIDEO -> context.getString(R.string.video)
|
||||
else -> "unknown"
|
||||
}
|
||||
|
||||
val intent = Intent(context, PreviewActivity::class.java).apply {
|
||||
putExtra("type", fileTypeString)
|
||||
putExtra("folder", currentFolder.toString())
|
||||
putExtra("position", adapterPosition)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
@SuppressLint("MissingInflatedId")
|
||||
private fun renameFile(file: File) {
|
||||
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_input, null)
|
||||
val inputEditText = dialogView.findViewById<EditText>(R.id.editText)
|
||||
inputEditText.setText(file.name)
|
||||
inputEditText.selectAll()
|
||||
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(context.getString(R.string.rename_file))
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(context.getString(R.string.rename)) { dialog, _ ->
|
||||
val newName = inputEditText.text.toString().trim()
|
||||
if (newName.isNotEmpty() && newName != file.name) {
|
||||
if (isValidFileName(newName)) {
|
||||
renameFileAsync(file, newName)
|
||||
} else {
|
||||
Toast.makeText(context, "Invalid file name", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setNegativeButton(context.getString(R.string.cancel)) { dialog, _ ->
|
||||
dialog.cancel()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun isValidFileName(fileName: String): Boolean {
|
||||
val forbiddenChars = charArrayOf('/', '\\', ':', '*', '?', '"', '<', '>', '|')
|
||||
return fileName.isNotBlank() &&
|
||||
fileName.none { it in forbiddenChars } &&
|
||||
!fileName.startsWith(".") &&
|
||||
fileName.length <= 255
|
||||
}
|
||||
|
||||
private fun renameFileAsync(file: File, newName: String) {
|
||||
fileExecutor.execute {
|
||||
val parentDir = file.parentFile
|
||||
if (parentDir != null) {
|
||||
val newFile = File(parentDir, newName)
|
||||
if (newFile.exists()) {
|
||||
mainHandler.post {
|
||||
Toast.makeText(context, "File with this name already exists", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
return@execute
|
||||
}
|
||||
|
||||
val success = try {
|
||||
file.renameTo(newFile)
|
||||
} catch (e: Exception) {
|
||||
false
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
if (success) {
|
||||
fileOperationCallback?.get()?.onFileRenamed(file, newFile)
|
||||
Toast.makeText(context, "File renamed", Toast.LENGTH_SHORT).show()
|
||||
} else {
|
||||
Toast.makeText(context, "Failed to rename file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun toggleSelection(position: Int) {
|
||||
if (selectedItems.contains(position)) {
|
||||
selectedItems.remove(position)
|
||||
if (selectedItems.isEmpty()) {
|
||||
exitSelectionMode()
|
||||
}
|
||||
} else {
|
||||
selectedItems.add(position)
|
||||
}
|
||||
onSelectionCountChanged(selectedItems.size)
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
|
||||
private fun showEncryptedIcon() {
|
||||
imageView.setImageResource(R.drawable.encrypted)
|
||||
imageView.setPadding(50, 50, 50, 50)
|
||||
}
|
||||
|
||||
private fun showDecryptionTypeDialog(file: File) {
|
||||
val options = arrayOf("Image", "Video", "Audio")
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle("Select File Type")
|
||||
.setMessage("Please select the type of file to decrypt")
|
||||
.setItems(options) { _, which ->
|
||||
val selectedType = when (which) {
|
||||
0 -> FileManager.FileType.IMAGE
|
||||
1 -> FileManager.FileType.VIDEO
|
||||
2 -> FileManager.FileType.AUDIO
|
||||
else -> FileManager.FileType.DOCUMENT
|
||||
}
|
||||
performDecryptionWithType(file, selectedType)
|
||||
}
|
||||
.setNegativeButton("Cancel", null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun performDecryptionWithType(file: File, fileType: FileManager.FileType) {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
try {
|
||||
val extension = when (fileType) {
|
||||
FileManager.FileType.IMAGE -> ".jpg"
|
||||
FileManager.FileType.VIDEO -> ".mp4"
|
||||
FileManager.FileType.AUDIO -> ".mp3"
|
||||
else -> ".txt"
|
||||
}
|
||||
|
||||
val decryptedFile = SecurityUtils.changeFileExtension(file, extension)
|
||||
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
|
||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
hiddenFileRepository.insertHiddenFile(
|
||||
HiddenFileEntity(
|
||||
filePath = decryptedFile.absolutePath,
|
||||
fileName = decryptedFile.name,
|
||||
encryptedFileName = file.name,
|
||||
fileType = fileType,
|
||||
originalExtension = extension,
|
||||
isEncrypted = false
|
||||
)
|
||||
)
|
||||
|
||||
when (fileType) {
|
||||
FileManager.FileType.IMAGE, FileManager.FileType.VIDEO -> {
|
||||
val intent = Intent(context, PreviewActivity::class.java).apply {
|
||||
putExtra("type", if (fileType == FileManager.FileType.IMAGE) "image" else "video")
|
||||
putExtra("folder", currentFolder.toString())
|
||||
putExtra("position", adapterPosition)
|
||||
putExtra("isEncrypted", false)
|
||||
putExtra("file", decryptedFile.absolutePath)
|
||||
}
|
||||
context.startActivity(intent)
|
||||
}
|
||||
FileManager.FileType.AUDIO -> openAudioFile(decryptedFile)
|
||||
else -> openDocumentFile(decryptedFile)
|
||||
}
|
||||
|
||||
file.delete()
|
||||
} else {
|
||||
decryptedFile.delete()
|
||||
mainHandler.post {
|
||||
Toast.makeText(context, "Failed to decrypt file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (decryptedFile.exists()) {
|
||||
decryptedFile.delete()
|
||||
}
|
||||
mainHandler.post {
|
||||
Toast.makeText(context, "Failed to decrypt file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
mainHandler.post {
|
||||
Toast.makeText(context, "Error decrypting file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FileViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_file, parent, false)
|
||||
.inflate(R.layout.list_item_file, parent, false)
|
||||
return FileViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: FileViewHolder, position: Int) {
|
||||
holder.bind(getItem(position))
|
||||
if (position < itemCount) {
|
||||
val file = getItem(position)
|
||||
holder.bind(file)
|
||||
}
|
||||
}
|
||||
|
||||
class FileDiffCallback : DiffUtil.ItemCallback<File>() {
|
||||
override fun areItemsTheSame(oldItem: File, newItem: File): Boolean {
|
||||
return oldItem.path == newItem.path
|
||||
override fun onBindViewHolder(holder: FileViewHolder, position: Int, payloads: MutableList<Any>) {
|
||||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position)
|
||||
} else {
|
||||
if (position < itemCount) {
|
||||
val file = getItem(position)
|
||||
holder.bind(file, payloads)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: File, newItem: File): Boolean {
|
||||
return oldItem == newItem
|
||||
override fun submitList(list: List<File>?) {
|
||||
val currentList = currentList.toMutableList()
|
||||
if (list == null) {
|
||||
currentList.clear()
|
||||
super.submitList(null)
|
||||
} else {
|
||||
val newList = list.filter { it.name != ".nomedia" }.toMutableList()
|
||||
super.submitList(newList)
|
||||
}
|
||||
}
|
||||
|
||||
fun enterSelectionMode() {
|
||||
if (!isSelectionMode) {
|
||||
isSelectionMode = true
|
||||
notifySelectionModeChange()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun exitSelectionMode() {
|
||||
if (isSelectionMode) {
|
||||
isSelectionMode = false
|
||||
selectedItems.clear()
|
||||
notifySelectionModeChange()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
if (selectedItems.isNotEmpty()) {
|
||||
val previouslySelected = selectedItems.toSet()
|
||||
selectedItems.clear()
|
||||
fileOperationCallback?.get()?.onSelectionCountChanged(0)
|
||||
previouslySelected.forEach { position ->
|
||||
if (position < itemCount) {
|
||||
notifyItemChanged(position, listOf("SELECTION_CHANGED"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAll() {
|
||||
if (!isSelectionMode) {
|
||||
enterSelectionMode()
|
||||
}
|
||||
val previouslySelected = selectedItems.toSet()
|
||||
selectedItems.clear()
|
||||
for (i in 0 until itemCount) {
|
||||
selectedItems.add(i)
|
||||
}
|
||||
fileOperationCallback?.get()?.onSelectionCountChanged(selectedItems.size)
|
||||
updateSelectionItems(selectedItems.toSet(), previouslySelected)
|
||||
}
|
||||
|
||||
private fun updateSelectionItems(newSelections: Set<Int>, oldSelections: Set<Int>) {
|
||||
val changedItems = (oldSelections - newSelections) + (newSelections - oldSelections)
|
||||
changedItems.forEach { position ->
|
||||
if (position < itemCount) {
|
||||
notifyItemChanged(position, listOf("SELECTION_CHANGED"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifySelectionModeChange() {
|
||||
fileOperationCallback?.get()?.onSelectionModeChanged(isSelectionMode, selectedItems.size)
|
||||
onFolderLongClick(isSelectionMode)
|
||||
}
|
||||
|
||||
fun getSelectedItems(): List<File> {
|
||||
return selectedItems.mapNotNull { position ->
|
||||
if (position < itemCount) getItem(position) else null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
fun getSelectedCount(): Int = selectedItems.size
|
||||
|
||||
|
||||
fun isInSelectionMode(): Boolean = isSelectionMode
|
||||
|
||||
|
||||
fun onBackPressed(): Boolean {
|
||||
return if (isSelectionMode) {
|
||||
exitSelectionMode()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun cleanup() {
|
||||
try {
|
||||
if (!fileExecutor.isShutdown) {
|
||||
fileExecutor.shutdown()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
}
|
||||
|
||||
fileOperationCallback?.clear()
|
||||
fileOperationCallback = null
|
||||
}
|
||||
override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onDetachedFromRecyclerView(recyclerView)
|
||||
cleanup()
|
||||
}
|
||||
|
||||
private fun onSelectionCountChanged(count: Int) {
|
||||
fileOperationCallback?.get()?.onSelectionCountChanged(count)
|
||||
}
|
||||
|
||||
fun encryptSelectedFiles() {
|
||||
val selectedFiles = getSelectedItems()
|
||||
if (selectedFiles.isEmpty()) return
|
||||
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(context.getString(R.string.encrypt_files))
|
||||
.setMessage(context.getString(R.string.encryption_disclaimer))
|
||||
.setPositiveButton(context.getString(R.string.encrypt)) { _, _ ->
|
||||
performEncryption(selectedFiles)
|
||||
}
|
||||
.setNegativeButton(context.getString(R.string.cancel), null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun performEncryption(selectedFiles: List<File>) {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
var successCount = 0
|
||||
var failCount = 0
|
||||
val updatedFiles = mutableListOf<File>()
|
||||
|
||||
for (file in selectedFiles) {
|
||||
try {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
if (hiddenFile?.isEncrypted == true) continue
|
||||
val originalExtension = ".${file.extension.lowercase()}"
|
||||
val fileType = FileManager(context,lifecycleOwner).getFileType(file)
|
||||
val encryptedFile = SecurityUtils.changeFileExtension(file, FileManager.ENCRYPTED_EXTENSION)
|
||||
if (SecurityUtils.encryptFile(context, file, encryptedFile)) {
|
||||
if (encryptedFile.exists()) {
|
||||
if (hiddenFile == null){
|
||||
hiddenFileRepository.insertHiddenFile(
|
||||
HiddenFileEntity(
|
||||
filePath = encryptedFile.absolutePath,
|
||||
isEncrypted = true,
|
||||
encryptedFileName = encryptedFile.name,
|
||||
fileType = fileType,
|
||||
fileName = file.name,
|
||||
originalExtension = originalExtension
|
||||
)
|
||||
)
|
||||
}else{
|
||||
hiddenFile.let {
|
||||
hiddenFileRepository.updateEncryptionStatus(
|
||||
filePath = hiddenFile.filePath,
|
||||
newFilePath = encryptedFile.absolutePath,
|
||||
encryptedFileName = encryptedFile.name,
|
||||
isEncrypted = true
|
||||
)
|
||||
}
|
||||
}
|
||||
if (file.delete()) {
|
||||
updatedFiles.add(encryptedFile)
|
||||
successCount++
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
failCount++
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
exitSelectionMode()
|
||||
when {
|
||||
successCount > 0 && failCount == 0 -> {
|
||||
Toast.makeText(context, "Encrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
successCount > 0 && failCount > 0 -> {
|
||||
Toast.makeText(context, "Encrypted $successCount file(s), failed to encrypt $failCount", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
failCount > 0 -> {
|
||||
Toast.makeText(context, "Failed to encrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
val currentFiles = currentFolder.listFiles()?.toList() ?: emptyList()
|
||||
submitList(currentFiles)
|
||||
fileOperationCallback?.get()?.onRefreshNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun decryptSelectedFiles() {
|
||||
val selectedFiles = getSelectedItems()
|
||||
if (selectedFiles.isEmpty()) return
|
||||
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(context.getString(R.string.decrypt_files))
|
||||
.setMessage(context.getString(R.string.decryption_disclaimer))
|
||||
.setPositiveButton(context.getString(R.string.decrypt)) { _, _ ->
|
||||
performDecryption(selectedFiles)
|
||||
}
|
||||
.setNegativeButton(context.getString(R.string.cancel), null)
|
||||
.show()
|
||||
}
|
||||
|
||||
private fun performDecryption(selectedFiles: List<File>) {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
var successCount = 0
|
||||
var failCount = 0
|
||||
val updatedFiles = mutableListOf<File>()
|
||||
|
||||
for (file in selectedFiles) {
|
||||
try {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
if (hiddenFile?.isEncrypted != true) continue
|
||||
val originalExtension = hiddenFile.originalExtension
|
||||
val decryptedFile = SecurityUtils.changeFileExtension(file, originalExtension)
|
||||
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
|
||||
if (decryptedFile.exists() && decryptedFile.length() > 0) {
|
||||
hiddenFile.let {
|
||||
hiddenFileRepository.updateEncryptionStatus(
|
||||
filePath = file.absolutePath,
|
||||
newFilePath = decryptedFile.absolutePath,
|
||||
encryptedFileName = decryptedFile.name,
|
||||
isEncrypted = false
|
||||
)
|
||||
}
|
||||
if (file.delete()) {
|
||||
updatedFiles.add(decryptedFile)
|
||||
successCount++
|
||||
} else {
|
||||
decryptedFile.delete()
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
decryptedFile.delete()
|
||||
failCount++
|
||||
}
|
||||
} else {
|
||||
if (decryptedFile.exists()) {
|
||||
decryptedFile.delete()
|
||||
}
|
||||
failCount++
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
failCount++
|
||||
}
|
||||
}
|
||||
|
||||
mainHandler.post {
|
||||
exitSelectionMode()
|
||||
when {
|
||||
successCount > 0 && failCount == 0 -> {
|
||||
Toast.makeText(context, "Decrypted $successCount file(s)", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
successCount > 0 && failCount > 0 -> {
|
||||
Toast.makeText(context, "Decrypted $successCount file(s), failed to decrypt $failCount", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
failCount > 0 -> {
|
||||
Toast.makeText(context, "Failed to decrypt $failCount file(s)", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
val currentFiles = currentFolder.listFiles()?.toList() ?: emptyList()
|
||||
submitList(currentFiles)
|
||||
fileOperationCallback?.get()?.onRefreshNeeded()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package devs.org.calculator.adapters
|
||||
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import java.io.File
|
||||
|
||||
class FileDiffCallback : DiffUtil.ItemCallback<File>() {
|
||||
|
||||
override fun areItemsTheSame(oldItem: File, newItem: File): Boolean {
|
||||
return oldItem.absolutePath == newItem.absolutePath
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: File, newItem: File): Boolean {
|
||||
return oldItem.name == newItem.name &&
|
||||
oldItem.length() == newItem.length() &&
|
||||
oldItem.lastModified() == newItem.lastModified() &&
|
||||
oldItem.canRead() == newItem.canRead() &&
|
||||
oldItem.canWrite() == newItem.canWrite() &&
|
||||
oldItem.exists() == newItem.exists()
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItem: File, newItem: File): Any? {
|
||||
val changes = mutableListOf<String>()
|
||||
|
||||
if (oldItem.name != newItem.name) {
|
||||
changes.add("NAME_CHANGED")
|
||||
}
|
||||
|
||||
if (oldItem.length() != newItem.length()) {
|
||||
changes.add("SIZE_CHANGED")
|
||||
}
|
||||
|
||||
if (oldItem.lastModified() != newItem.lastModified()) {
|
||||
changes.add("MODIFIED_DATE_CHANGED")
|
||||
}
|
||||
|
||||
if (oldItem.exists() != newItem.exists()) {
|
||||
changes.add("EXISTENCE_CHANGED")
|
||||
}
|
||||
|
||||
return changes.ifEmpty { null }
|
||||
}
|
||||
}
|
||||
127
app/src/main/java/devs/org/calculator/adapters/FolderAdapter.kt
Normal file
@@ -0,0 +1,127 @@
|
||||
package devs.org.calculator.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import devs.org.calculator.R
|
||||
import java.io.File
|
||||
|
||||
class FolderAdapter(
|
||||
private val onFolderClick: (File) -> Unit,
|
||||
private val onFolderLongClick: (File) -> Unit,
|
||||
private val onSelectionModeChanged: (Boolean) -> Unit,
|
||||
private val onSelectionCountChanged: (Int) -> Unit
|
||||
) : ListAdapter<File, FolderAdapter.FolderViewHolder>(FolderDiffCallback()) {
|
||||
|
||||
private val selectedItems = mutableSetOf<Int>()
|
||||
private var isSelectionMode = false
|
||||
|
||||
inner class FolderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val folderNameTextView: TextView = itemView.findViewById(R.id.folderName)
|
||||
val selectedView: ImageView = itemView.findViewById(R.id.selected)
|
||||
val selectedLayer: View = itemView.findViewById(R.id.selectedLayer)
|
||||
|
||||
fun bind(folder: File, onFolderClick: (File) -> Unit, onFolderLongClick: (File) -> Unit, isSelected: Boolean) {
|
||||
folderNameTextView.text = folder.name
|
||||
|
||||
selectedView.visibility = if (isSelected) View.VISIBLE else View.GONE
|
||||
selectedLayer.visibility = if (isSelected) View.VISIBLE else View.GONE
|
||||
|
||||
itemView.setOnClickListener {
|
||||
if (isSelectionMode) {
|
||||
toggleSelection(adapterPosition)
|
||||
} else {
|
||||
onFolderClick(folder)
|
||||
}
|
||||
}
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
if (!isSelectionMode) {
|
||||
enterSelectionMode()
|
||||
onFolderLongClick(folder)
|
||||
toggleSelection(adapterPosition)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSelection(position: Int) {
|
||||
if (selectedItems.contains(position)) {
|
||||
selectedItems.remove(position)
|
||||
if (selectedItems.isEmpty()) {
|
||||
exitSelectionMode()
|
||||
}
|
||||
} else {
|
||||
selectedItems.add(position)
|
||||
}
|
||||
onSelectionCountChanged(selectedItems.size)
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_folder, parent, false)
|
||||
return FolderViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
|
||||
val folder = getItem(position)
|
||||
holder.bind(folder, onFolderClick, onFolderLongClick, selectedItems.contains(position))
|
||||
}
|
||||
|
||||
fun getSelectedItems(): List<File> {
|
||||
return selectedItems.mapNotNull { position ->
|
||||
if (position < itemCount) getItem(position) else null
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun clearSelection() {
|
||||
val wasInSelectionMode = isSelectionMode
|
||||
selectedItems.clear()
|
||||
if (wasInSelectionMode) {
|
||||
exitSelectionMode()
|
||||
}
|
||||
onSelectionCountChanged(0)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun onBackPressed(): Boolean {
|
||||
return if (isInSelectionMode()) {
|
||||
clearSelection()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun isInSelectionMode(): Boolean {
|
||||
return isSelectionMode
|
||||
}
|
||||
|
||||
private fun enterSelectionMode() {
|
||||
isSelectionMode = true
|
||||
onSelectionModeChanged(true)
|
||||
}
|
||||
|
||||
private fun exitSelectionMode() {
|
||||
isSelectionMode = false
|
||||
onSelectionModeChanged(false)
|
||||
}
|
||||
|
||||
private class FolderDiffCallback : DiffUtil.ItemCallback<File>() {
|
||||
override fun areItemsTheSame(oldItem: File, newItem: File): Boolean {
|
||||
return oldItem.absolutePath == newItem.absolutePath
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: File, newItem: File): Boolean {
|
||||
return oldItem.name == newItem.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package devs.org.calculator.adapters
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import devs.org.calculator.R
|
||||
import java.io.File
|
||||
|
||||
class FolderSelectionAdapter(
|
||||
private val folders: List<File>,
|
||||
private val onFolderSelected: (File) -> Unit
|
||||
) : RecyclerView.Adapter<FolderSelectionAdapter.FolderViewHolder>() {
|
||||
|
||||
inner class FolderViewHolder(view: View) : RecyclerView.ViewHolder(view) {
|
||||
val folderName: TextView = view.findViewById(R.id.folderName)
|
||||
|
||||
init {
|
||||
view.setOnClickListener {
|
||||
val position = adapterPosition
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
onFolderSelected(folders[position])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
|
||||
val view = LayoutInflater.from(parent.context)
|
||||
.inflate(R.layout.item_folder_selection, parent, false)
|
||||
return FolderViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
|
||||
val folder = folders[position]
|
||||
holder.folderName.text = folder.name
|
||||
}
|
||||
|
||||
override fun getItemCount() = folders.size
|
||||
}
|
||||
@@ -9,27 +9,32 @@ import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.MediaController
|
||||
import android.widget.SeekBar
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import devs.org.calculator.adapters.FileAdapter.FileDiffCallback
|
||||
import devs.org.calculator.R
|
||||
import devs.org.calculator.database.AppDatabase
|
||||
import devs.org.calculator.database.HiddenFileRepository
|
||||
import devs.org.calculator.databinding.ViewpagerItemsBinding
|
||||
import devs.org.calculator.utils.FileManager
|
||||
import devs.org.calculator.utils.SecurityUtils.getDecryptedPreviewFile
|
||||
import devs.org.calculator.utils.SecurityUtils.getUriForPreviewFile
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import devs.org.calculator.R
|
||||
|
||||
class ImagePreviewAdapter(
|
||||
private val context: Context,
|
||||
private var fileType: FileManager.FileType
|
||||
private var lifecycleOwner: LifecycleOwner
|
||||
) : RecyclerView.Adapter<ImagePreviewAdapter.ImageViewHolder>() {
|
||||
|
||||
private val differ = AsyncListDiffer(this, FileDiffCallback())
|
||||
var currentMediaPlayer: MediaPlayer? = null
|
||||
var isMediaPlayerPrepared = false
|
||||
var currentViewHolder: ImageViewHolder? = null
|
||||
private var currentPlayingPosition = -1
|
||||
private var isPlaying = false
|
||||
private var currentViewHolder: ImageViewHolder? = null
|
||||
private val hiddenFileRepository: HiddenFileRepository by lazy {
|
||||
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
||||
}
|
||||
|
||||
var images: List<File>
|
||||
get() = differ.currentList
|
||||
@@ -42,39 +47,78 @@ class ImagePreviewAdapter(
|
||||
|
||||
override fun onBindViewHolder(holder: ImageViewHolder, position: Int) {
|
||||
val imageUrl = images[position]
|
||||
holder.bind(imageUrl)
|
||||
currentViewHolder = holder
|
||||
val fileType = FileManager(context,lifecycleOwner).getFileType(imageUrl)
|
||||
stopAndResetCurrentAudio()
|
||||
|
||||
currentMediaPlayer?.let {
|
||||
if (it.isPlaying) it.pause()
|
||||
it.seekTo(0)
|
||||
}
|
||||
currentMediaPlayer = null
|
||||
isMediaPlayerPrepared = false
|
||||
|
||||
if (currentMediaPlayer?.isPlaying == true) {
|
||||
currentMediaPlayer?.stop()
|
||||
currentMediaPlayer?.release()
|
||||
}
|
||||
currentMediaPlayer = null
|
||||
holder.bind(imageUrl, position,fileType)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int = images.size
|
||||
|
||||
private fun stopAndResetCurrentAudio() {
|
||||
currentViewHolder?.stopAndResetAudio()
|
||||
currentPlayingPosition = -1
|
||||
currentViewHolder = null
|
||||
}
|
||||
|
||||
inner class ImageViewHolder(private val binding: ViewpagerItemsBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
private var seekHandler = Handler(Looper.getMainLooper())
|
||||
private var seekRunnable: Runnable? = null
|
||||
private var mediaPlayer: MediaPlayer? = null
|
||||
private var isMediaPlayerPrepared = false
|
||||
private var isPlaying = false
|
||||
private var currentPosition = 0
|
||||
private var tempDecryptedFile: File? = null
|
||||
|
||||
fun bind(file: File) {
|
||||
fun bind(file: File, position: Int,decryptedFileType: FileManager.FileType) {
|
||||
currentPosition = position
|
||||
|
||||
releaseMediaPlayer()
|
||||
resetAudioUI()
|
||||
cleanupTempFile()
|
||||
try {
|
||||
lifecycleOwner.lifecycleScope.launch {
|
||||
val hiddenFile = hiddenFileRepository.getHiddenFileByPath(file.absolutePath)
|
||||
if (hiddenFile != null){
|
||||
val isEncrypted = hiddenFile.isEncrypted
|
||||
val fileType = hiddenFile.fileType
|
||||
if (isEncrypted) {
|
||||
val tempDecryptedFile = getDecryptedPreviewFile(context, hiddenFile)
|
||||
if (tempDecryptedFile != null && tempDecryptedFile.exists() && tempDecryptedFile.length() > 0) {
|
||||
displayFile(tempDecryptedFile, fileType,true)
|
||||
} else {
|
||||
showEncryptedError()
|
||||
}
|
||||
} else {
|
||||
displayFile(file, decryptedFileType,false)
|
||||
}
|
||||
}else{
|
||||
displayFile(file, decryptedFileType,false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} catch (_: Exception) {
|
||||
|
||||
displayFile(file, decryptedFileType,false)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun displayFile(file: File, fileType: FileManager.FileType,isEncrypted: Boolean = false) {
|
||||
val uri = getUriForPreviewFile(context, file)
|
||||
when (fileType) {
|
||||
FileManager.FileType.VIDEO -> {
|
||||
binding.imageView.visibility = View.GONE
|
||||
binding.audioBg.visibility = View.GONE
|
||||
binding.videoView.visibility = View.VISIBLE
|
||||
|
||||
val videoUri = Uri.fromFile(file)
|
||||
val videoUri = if (isEncrypted){
|
||||
uri
|
||||
}else{
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
binding.videoView.setVideoURI(videoUri)
|
||||
binding.videoView.start()
|
||||
|
||||
@@ -99,21 +143,30 @@ class ImagePreviewAdapter(
|
||||
}
|
||||
}
|
||||
FileManager.FileType.IMAGE -> {
|
||||
val imageUri = if (isEncrypted){
|
||||
uri
|
||||
}else{
|
||||
Uri.fromFile(file)
|
||||
}
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
binding.videoView.visibility = View.GONE
|
||||
binding.audioBg.visibility = View.GONE
|
||||
Glide.with(context)
|
||||
.load(file)
|
||||
.load(imageUri)
|
||||
.into(binding.imageView)
|
||||
}
|
||||
FileManager.FileType.AUDIO -> {
|
||||
val audioFile: File? = if (isEncrypted) {
|
||||
getFileFromUri(context, uri!!)
|
||||
} else {
|
||||
file
|
||||
}
|
||||
binding.imageView.visibility = View.GONE
|
||||
binding.audioBg.visibility = View.VISIBLE
|
||||
binding.videoView.visibility = View.GONE
|
||||
binding.audioTitle.text = file.name
|
||||
|
||||
setupAudioPlayer(file)
|
||||
setupSeekBar()
|
||||
setupAudioPlayer(audioFile!!)
|
||||
setupPlaybackControls()
|
||||
}
|
||||
else -> {
|
||||
@@ -124,40 +177,87 @@ class ImagePreviewAdapter(
|
||||
}
|
||||
}
|
||||
|
||||
fun getFileFromUri(context: Context, uri: Uri): File? {
|
||||
return try {
|
||||
val inputStream = context.contentResolver.openInputStream(uri)
|
||||
val tempFile = File.createTempFile("temp_audio", null, context.cacheDir)
|
||||
tempFile.outputStream().use { output ->
|
||||
inputStream?.copyTo(output)
|
||||
}
|
||||
tempFile
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun showEncryptedError() {
|
||||
binding.imageView.visibility = View.VISIBLE
|
||||
binding.videoView.visibility = View.GONE
|
||||
binding.audioBg.visibility = View.GONE
|
||||
binding.imageView.setImageResource(R.drawable.encrypted)
|
||||
}
|
||||
|
||||
private fun cleanupTempFile() {
|
||||
tempDecryptedFile?.let { file ->
|
||||
if (file.exists()) {
|
||||
try {
|
||||
file.delete()
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
}
|
||||
tempDecryptedFile = null
|
||||
}
|
||||
}
|
||||
|
||||
private fun resetAudioUI() {
|
||||
binding.playPause.setImageResource(R.drawable.play)
|
||||
binding.audioSeekBar.value = 0f
|
||||
binding.audioSeekBar.valueTo = 100f
|
||||
seekRunnable?.let { seekHandler.removeCallbacks(it) }
|
||||
}
|
||||
|
||||
private fun setupAudioPlayer(file: File) {
|
||||
mediaPlayer = MediaPlayer().apply {
|
||||
setDataSource(file.absolutePath)
|
||||
setOnPreparedListener { mp ->
|
||||
binding.audioSeekBar.max = mp.duration
|
||||
isMediaPlayerPrepared = true
|
||||
try {
|
||||
mediaPlayer = MediaPlayer().apply {
|
||||
setDataSource(file.absolutePath)
|
||||
setOnPreparedListener { mp ->
|
||||
binding.audioSeekBar.valueTo = mp.duration.toFloat()
|
||||
binding.audioSeekBar.value = 0f
|
||||
setupSeekBar()
|
||||
isMediaPlayerPrepared = true
|
||||
}
|
||||
setOnCompletionListener {
|
||||
stopAndResetAudio()
|
||||
}
|
||||
setOnErrorListener { _, _, _ ->
|
||||
releaseMediaPlayer()
|
||||
true
|
||||
}
|
||||
prepareAsync()
|
||||
}
|
||||
setOnCompletionListener {
|
||||
// isPlaying = false
|
||||
binding.playPause.setImageResource(R.drawable.play)
|
||||
binding.audioSeekBar.progress = 0
|
||||
seekHandler.removeCallbacks(seekRunnable!!)
|
||||
}
|
||||
prepareAsync()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
releaseMediaPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSeekBar() {
|
||||
binding.audioSeekBar.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
mediaPlayer?.seekTo(progress)
|
||||
}
|
||||
binding.audioSeekBar.addOnChangeListener { _, value, fromUser ->
|
||||
if (fromUser && mediaPlayer != null && isMediaPlayerPrepared) {
|
||||
mediaPlayer?.seekTo(value.toInt())
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar?) {}
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar?) {}
|
||||
})
|
||||
}
|
||||
|
||||
seekRunnable = Runnable {
|
||||
mediaPlayer?.let { mp ->
|
||||
if (mp.isPlaying) {
|
||||
binding.audioSeekBar.progress = mp.currentPosition
|
||||
seekHandler.postDelayed(seekRunnable!!, 100)
|
||||
if (mp.isPlaying && isMediaPlayerPrepared) {
|
||||
try {
|
||||
binding.audioSeekBar.value = mp.currentPosition.toFloat()
|
||||
seekHandler.postDelayed(seekRunnable!!, 100)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -174,64 +274,127 @@ class ImagePreviewAdapter(
|
||||
|
||||
binding.preview.setOnClickListener {
|
||||
mediaPlayer?.let { mp ->
|
||||
val newPosition = mp.currentPosition - 10000
|
||||
mp.seekTo(maxOf(0, newPosition))
|
||||
binding.audioSeekBar.progress = mp.currentPosition
|
||||
if (isMediaPlayerPrepared) {
|
||||
try {
|
||||
val newPosition = mp.currentPosition - 10000
|
||||
mp.seekTo(maxOf(0, newPosition))
|
||||
binding.audioSeekBar.value = mp.currentPosition.toFloat()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.next.setOnClickListener {
|
||||
mediaPlayer?.let { mp ->
|
||||
val newPosition = mp.currentPosition + 10000
|
||||
mp.seekTo(minOf(mp.duration, newPosition))
|
||||
binding.audioSeekBar.progress = mp.currentPosition
|
||||
if (isMediaPlayerPrepared) {
|
||||
try {
|
||||
val newPosition = mp.currentPosition + 10000
|
||||
mp.seekTo(minOf(mp.duration, newPosition))
|
||||
binding.audioSeekBar.value = mp.currentPosition.toFloat()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun playAudio() {
|
||||
mediaPlayer?.let { mp ->
|
||||
if (currentPlayingPosition != -1 && currentPlayingPosition != adapterPosition) {
|
||||
currentViewHolder?.pauseAudio()
|
||||
if (isMediaPlayerPrepared) {
|
||||
try {
|
||||
if (currentPlayingPosition != currentPosition) {
|
||||
stopAndResetCurrentAudio()
|
||||
}
|
||||
|
||||
mp.start()
|
||||
isPlaying = true
|
||||
binding.playPause.setImageResource(R.drawable.pause)
|
||||
seekRunnable?.let { seekHandler.post(it) }
|
||||
|
||||
currentPlayingPosition = currentPosition
|
||||
currentViewHolder = this@ImageViewHolder
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
releaseMediaPlayer()
|
||||
}
|
||||
}
|
||||
mp.start()
|
||||
isPlaying = true
|
||||
binding.playPause.setImageResource(R.drawable.pause)
|
||||
seekHandler.post(seekRunnable!!)
|
||||
currentPlayingPosition = adapterPosition
|
||||
currentViewHolder = this
|
||||
}
|
||||
}
|
||||
|
||||
private fun pauseAudio() {
|
||||
mediaPlayer?.let { mp ->
|
||||
if (mp.isPlaying) {
|
||||
mp.pause()
|
||||
isPlaying = false
|
||||
binding.playPause.setImageResource(R.drawable.play)
|
||||
seekHandler.removeCallbacks(seekRunnable!!)
|
||||
try {
|
||||
if (mp.isPlaying) {
|
||||
mp.pause()
|
||||
isPlaying = false
|
||||
binding.playPause.setImageResource(R.drawable.play)
|
||||
seekRunnable?.let { seekHandler.removeCallbacks(it) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
releaseMediaPlayer()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopAndResetAudio() {
|
||||
try {
|
||||
mediaPlayer?.let { mp ->
|
||||
if (mp.isPlaying) {
|
||||
mp.stop()
|
||||
mp.prepare()
|
||||
} else if (isMediaPlayerPrepared) {
|
||||
mp.seekTo(0)
|
||||
}
|
||||
}
|
||||
isPlaying = false
|
||||
resetAudioUI()
|
||||
|
||||
if (currentPlayingPosition == currentPosition) {
|
||||
currentPlayingPosition = -1
|
||||
currentViewHolder = null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
releaseMediaPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
fun releaseMediaPlayer() {
|
||||
mediaPlayer?.let { mp ->
|
||||
if (mp.isPlaying) {
|
||||
mp.stop()
|
||||
try {
|
||||
mediaPlayer?.let { mp ->
|
||||
if (mp.isPlaying) {
|
||||
mp.stop()
|
||||
}
|
||||
mp.release()
|
||||
}
|
||||
mp.release()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
} finally {
|
||||
mediaPlayer = null
|
||||
isPlaying = false
|
||||
seekHandler.removeCallbacks(seekRunnable!!)
|
||||
isMediaPlayerPrepared = false
|
||||
seekRunnable?.let { seekHandler.removeCallbacks(it) }
|
||||
|
||||
if (currentPlayingPosition == currentPosition) {
|
||||
currentPlayingPosition = -1
|
||||
currentViewHolder = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun playVideoAtPosition(position: Int) {
|
||||
val nextFile = images[position]
|
||||
if (fileType == FileManager.FileType.VIDEO) {
|
||||
val videoUri = Uri.fromFile(nextFile)
|
||||
binding.videoView.setVideoURI(videoUri)
|
||||
binding.videoView.start()
|
||||
if (position < images.size) {
|
||||
val nextFile = images[position]
|
||||
val fileType = FileManager(context, lifecycleOwner).getFileType(images[position])
|
||||
if (fileType == FileManager.FileType.VIDEO) {
|
||||
val videoUri = Uri.fromFile(nextFile)
|
||||
binding.videoView.setVideoURI(videoUri)
|
||||
binding.videoView.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -240,5 +403,14 @@ class ImagePreviewAdapter(
|
||||
super.onViewRecycled(holder)
|
||||
holder.releaseMediaPlayer()
|
||||
}
|
||||
}
|
||||
|
||||
fun onItemScrolledAway(position: Int) {
|
||||
if (currentPlayingPosition == position) {
|
||||
stopAndResetCurrentAudio()
|
||||
}
|
||||
}
|
||||
|
||||
fun releaseAllResources() {
|
||||
stopAndResetCurrentAudio()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package devs.org.calculator.adapters
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.TextView
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import devs.org.calculator.R
|
||||
import java.io.File
|
||||
|
||||
class ListFolderAdapter(
|
||||
private val onFolderClick: (File) -> Unit,
|
||||
private val onFolderLongClick: (File) -> Unit,
|
||||
private val onSelectionModeChanged: (Boolean) -> Unit,
|
||||
private val onSelectionCountChanged: (Int) -> Unit
|
||||
) : ListAdapter<File, ListFolderAdapter.FolderViewHolder>(FolderDiffCallback()) {
|
||||
|
||||
private val selectedItems = mutableSetOf<Int>()
|
||||
private var isSelectionMode = false
|
||||
|
||||
inner class FolderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val folderNameTextView: TextView = itemView.findViewById(R.id.folderName)
|
||||
|
||||
private val selectedLayer: View = itemView.findViewById(R.id.selectedLayer)
|
||||
|
||||
fun bind(folder: File, onFolderClick: (File) -> Unit, onFolderLongClick: (File) -> Unit, isSelected: Boolean) {
|
||||
folderNameTextView.text = folder.name
|
||||
|
||||
selectedLayer.visibility = if (isSelected) View.VISIBLE else View.GONE
|
||||
|
||||
itemView.setOnClickListener {
|
||||
if (isSelectionMode) {
|
||||
toggleSelection(adapterPosition)
|
||||
} else {
|
||||
onFolderClick(folder)
|
||||
}
|
||||
}
|
||||
|
||||
itemView.setOnLongClickListener {
|
||||
if (!isSelectionMode) {
|
||||
enterSelectionMode()
|
||||
onFolderLongClick(folder)
|
||||
toggleSelection(adapterPosition)
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleSelection(position: Int) {
|
||||
if (selectedItems.contains(position)) {
|
||||
selectedItems.remove(position)
|
||||
if (selectedItems.isEmpty()) {
|
||||
exitSelectionMode()
|
||||
}
|
||||
} else {
|
||||
selectedItems.add(position)
|
||||
}
|
||||
onSelectionCountChanged(selectedItems.size)
|
||||
notifyItemChanged(position)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enterSelectionMode() {
|
||||
isSelectionMode = true
|
||||
onSelectionModeChanged(true)
|
||||
}
|
||||
|
||||
private fun exitSelectionMode() {
|
||||
isSelectionMode = false
|
||||
onSelectionModeChanged(false)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): FolderViewHolder {
|
||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_folder_list_style, parent, false)
|
||||
return FolderViewHolder(view)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: FolderViewHolder, position: Int) {
|
||||
val folder = getItem(position)
|
||||
holder.bind(folder, onFolderClick, onFolderLongClick, selectedItems.contains(position))
|
||||
}
|
||||
|
||||
fun getSelectedItems(): List<File> {
|
||||
return selectedItems.mapNotNull { position ->
|
||||
if (position < itemCount) getItem(position) else null
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
fun clearSelection() {
|
||||
val wasInSelectionMode = isSelectionMode
|
||||
selectedItems.clear()
|
||||
if (wasInSelectionMode) {
|
||||
exitSelectionMode()
|
||||
}
|
||||
onSelectionCountChanged(0)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun onBackPressed(): Boolean {
|
||||
return if (isInSelectionMode()) {
|
||||
clearSelection()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun isInSelectionMode(): Boolean {
|
||||
return isSelectionMode
|
||||
}
|
||||
|
||||
private class FolderDiffCallback : DiffUtil.ItemCallback<File>() {
|
||||
override fun areItemsTheSame(oldItem: File, newItem: File): Boolean {
|
||||
return oldItem.absolutePath == newItem.absolutePath
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(oldItem: File, newItem: File): Boolean {
|
||||
return oldItem.name == newItem.name
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package devs.org.calculator.database
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
|
||||
@Database(entities = [HiddenFileEntity::class], version = 1)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
abstract fun hiddenFileDao(): HiddenFileDao
|
||||
|
||||
companion object {
|
||||
@Volatile
|
||||
private var INSTANCE: AppDatabase? = null
|
||||
|
||||
fun getDatabase(context: Context): AppDatabase {
|
||||
return INSTANCE ?: synchronized(this) {
|
||||
val instance = Room.databaseBuilder(
|
||||
context.applicationContext,
|
||||
AppDatabase::class.java,
|
||||
"calculator_database"
|
||||
).build()
|
||||
INSTANCE = instance
|
||||
instance
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package devs.org.calculator.database
|
||||
|
||||
import androidx.room.*
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface HiddenFileDao {
|
||||
@Query("SELECT * FROM hidden_files")
|
||||
fun getAllHiddenFiles(): Flow<List<HiddenFileEntity>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
suspend fun insertHiddenFile(hiddenFile: HiddenFileEntity)
|
||||
|
||||
@Delete
|
||||
suspend fun deleteHiddenFile(hiddenFile: HiddenFileEntity)
|
||||
|
||||
@Query("SELECT * FROM hidden_files WHERE filePath = :filePath")
|
||||
suspend fun getHiddenFileByPath(filePath: String): HiddenFileEntity?
|
||||
|
||||
@Query("SELECT * FROM hidden_files WHERE fileName = :fileName")
|
||||
suspend fun getHiddenFileByOriginalName(fileName: String): HiddenFileEntity?
|
||||
|
||||
@Query("UPDATE hidden_files SET isEncrypted = :isEncrypted, filePath = :newFilePath, encryptedFileName = :encryptedFileName WHERE filePath = :filePath")
|
||||
suspend fun updateEncryptionStatus(
|
||||
filePath: String,
|
||||
newFilePath: String,
|
||||
encryptedFileName: String?,
|
||||
isEncrypted: Boolean
|
||||
)
|
||||
|
||||
@Update
|
||||
suspend fun updateHiddenFile(hiddenFile: HiddenFileEntity)
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package devs.org.calculator.database
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import devs.org.calculator.utils.FileManager
|
||||
|
||||
@Entity(tableName = "hidden_files")
|
||||
data class HiddenFileEntity(
|
||||
@PrimaryKey
|
||||
val filePath: String, //absolute path of the file
|
||||
val fileName: String, // Original filename with extension
|
||||
val encryptedFileName: String, // Encrypted filename
|
||||
val fileType: FileManager.FileType, //type of the file
|
||||
val originalExtension: String, // original file extension
|
||||
val isEncrypted: Boolean, // is the file encrypted or not
|
||||
var dateAdded: Long = System.currentTimeMillis()
|
||||
)
|
||||
@@ -0,0 +1,27 @@
|
||||
package devs.org.calculator.database
|
||||
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class HiddenFileRepository(private val hiddenFileDao: HiddenFileDao) {
|
||||
|
||||
fun getAllHiddenFiles(): Flow<List<HiddenFileEntity>> {
|
||||
return hiddenFileDao.getAllHiddenFiles()
|
||||
}
|
||||
|
||||
suspend fun insertHiddenFile(hiddenFile: HiddenFileEntity) {
|
||||
hiddenFileDao.insertHiddenFile(hiddenFile)
|
||||
}
|
||||
|
||||
suspend fun deleteHiddenFile(hiddenFile: HiddenFileEntity) {
|
||||
hiddenFileDao.deleteHiddenFile(hiddenFile)
|
||||
}
|
||||
|
||||
suspend fun getHiddenFileByPath(filePath: String): HiddenFileEntity? {
|
||||
return hiddenFileDao.getHiddenFileByPath(filePath)
|
||||
}
|
||||
|
||||
suspend fun updateEncryptionStatus(filePath: String, newFilePath: String,encryptedFileName: String, isEncrypted: Boolean) {
|
||||
hiddenFileDao.updateEncryptionStatus(filePath,newFilePath, encryptedFileName = encryptedFileName, isEncrypted)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,59 +1,57 @@
|
||||
package devs.org.calculator.utils
|
||||
|
||||
import android.content.Context
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.IntentSenderRequest
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.EditText
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import devs.org.calculator.callbacks.DialogActionsCallback
|
||||
import devs.org.calculator.R
|
||||
|
||||
class DialogUtil(private val context: Context) {
|
||||
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
|
||||
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()
|
||||
}
|
||||
|
||||
fun showMaterialDialog(
|
||||
title: String,
|
||||
message: String,
|
||||
positiveButton: String,
|
||||
negativeButton: String,
|
||||
callback: DialogActionsCallback
|
||||
positiveButtonText: String,
|
||||
neutralButtonText: String,
|
||||
callback: DialogCallback
|
||||
) {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(title)
|
||||
.setMessage(message)
|
||||
.setPositiveButton(positiveButton) { _, _ ->
|
||||
// Handle positive button click
|
||||
callback.onPositiveButtonClicked()
|
||||
}
|
||||
.setNegativeButton(negativeButton) { dialog, _ ->
|
||||
// Handle negative button click
|
||||
callback.onNegativeButtonClicked()
|
||||
dialog.dismiss()
|
||||
}
|
||||
.setPositiveButton(positiveButtonText) { _, _ -> callback.onPositiveButtonClicked() }
|
||||
.setNegativeButton(neutralButtonText) { _, _ -> callback.onNegativeButtonClicked() }
|
||||
.show()
|
||||
}
|
||||
|
||||
fun createInputDialog(
|
||||
title: String,
|
||||
hint: String,
|
||||
callback: InputDialogCallback
|
||||
) {
|
||||
val dialogView = LayoutInflater.from(context).inflate(R.layout.dialog_input, null)
|
||||
val editText = dialogView.findViewById<EditText>(R.id.editText)
|
||||
editText.hint = hint
|
||||
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(title)
|
||||
.setView(dialogView)
|
||||
.setPositiveButton(R.string.create) { _, _ ->
|
||||
callback.onPositiveButtonClicked(editText.text.toString())
|
||||
}
|
||||
.setNegativeButton(R.string.cancel) { dialog, _ ->
|
||||
dialog.dismiss()
|
||||
}
|
||||
.create()
|
||||
.show()
|
||||
}
|
||||
|
||||
interface DialogCallback {
|
||||
fun onPositiveButtonClicked()
|
||||
fun onNegativeButtonClicked()
|
||||
fun onNaturalButtonClicked()
|
||||
}
|
||||
|
||||
interface InputDialogCallback {
|
||||
fun onPositiveButtonClicked(input: String)
|
||||
}
|
||||
}
|
||||
@@ -20,15 +20,27 @@ import androidx.lifecycle.LifecycleOwner
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import devs.org.calculator.callbacks.FileProcessCallback
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import android.Manifest
|
||||
import androidx.core.content.FileProvider
|
||||
import devs.org.calculator.R
|
||||
import devs.org.calculator.database.AppDatabase
|
||||
import devs.org.calculator.database.HiddenFileRepository
|
||||
import devs.org.calculator.utils.PrefsUtil
|
||||
import android.util.Log
|
||||
import devs.org.calculator.database.HiddenFileEntity
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class FileManager(private val context: Context, private val lifecycleOwner: LifecycleOwner) {
|
||||
private lateinit var intentSenderLauncher: ActivityResultLauncher<IntentSenderRequest>
|
||||
val intent = Intent()
|
||||
private val prefs: PrefsUtil by lazy { PrefsUtil(context) }
|
||||
val hiddenFileRepository: HiddenFileRepository by lazy {
|
||||
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val HIDDEN_DIR = ".CalculatorHide"
|
||||
@@ -36,45 +48,62 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
const val VIDEOS_DIR = "videos"
|
||||
const val AUDIO_DIR = "audio"
|
||||
const val DOCS_DIR = "documents"
|
||||
const val ENCRYPTED_EXTENSION = ".enc"
|
||||
}
|
||||
|
||||
|
||||
fun getHiddenDirectory(): File {
|
||||
val dir = File(Environment.getExternalStorageDirectory(), HIDDEN_DIR)
|
||||
if (!dir.exists()) {
|
||||
dir.mkdirs()
|
||||
// Create .nomedia file to hide from media scanners
|
||||
File(dir, ".nomedia").createNewFile()
|
||||
val created = dir.mkdirs()
|
||||
if (!created) {
|
||||
throw RuntimeException("Failed to create hidden directory: ${dir.absolutePath}")
|
||||
}
|
||||
val nomediaFile = File(dir, ".nomedia")
|
||||
if (!nomediaFile.exists()) {
|
||||
nomediaFile.createNewFile()
|
||||
}
|
||||
}
|
||||
return dir
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun getFilesInHiddenDir(type: FileType): List<File> {
|
||||
val hiddenDir = getHiddenDirectory()
|
||||
val typeDir = File(hiddenDir, type.dirName)
|
||||
return if (typeDir.exists()) {
|
||||
typeDir.listFiles()?.filterNotNull()?.filter { it.name != ".nomedia" } ?: emptyList()
|
||||
} else {
|
||||
emptyList()
|
||||
if (!typeDir.exists()) {
|
||||
typeDir.mkdirs()
|
||||
File(typeDir, ".nomedia").createNewFile()
|
||||
}
|
||||
return typeDir.listFiles()?.filterNotNull()?.filter { it.name != ".nomedia" } ?: emptyList()
|
||||
}
|
||||
fun getFilesInHiddenDirFromFolder(type: FileType, folder: String): List<File> {
|
||||
val typeDir = File(folder)
|
||||
if (!typeDir.exists()) {
|
||||
typeDir.mkdirs()
|
||||
File(typeDir, ".nomedia").createNewFile()
|
||||
}
|
||||
return typeDir.listFiles()?.filterNotNull()?.filter { it.name != ".nomedia" } ?: emptyList()
|
||||
}
|
||||
|
||||
private fun copyFileToHiddenDir(uri: Uri, type: FileType): File? {
|
||||
private fun copyFileToHiddenDir(uri: Uri, folderName: File, currentDir: File? = null): File? {
|
||||
return try {
|
||||
val contentResolver = context.contentResolver
|
||||
|
||||
// Get the target directory
|
||||
val targetDir = File(Environment.getExternalStorageDirectory(), "$HIDDEN_DIR/${type.dirName}")
|
||||
targetDir.mkdirs()
|
||||
File(targetDir, ".nomedia").createNewFile()
|
||||
// Get the target directory (i am using the current opened folder as target folder)
|
||||
val targetDir = folderName
|
||||
|
||||
// Ensure target directory exists and has .nomedia file
|
||||
if (!targetDir.exists()) {
|
||||
targetDir.mkdirs()
|
||||
File(targetDir, ".nomedia").createNewFile()
|
||||
}
|
||||
|
||||
// Create target file
|
||||
val mimeType = contentResolver.getType(uri)
|
||||
val extension = MimeTypeMap.getSingleton()
|
||||
.getExtensionFromMimeType(mimeType) ?: ""
|
||||
val fileName = "${System.currentTimeMillis()}.${extension}"
|
||||
val targetFile = File(targetDir, fileName)
|
||||
var targetFile = File(targetDir, fileName)
|
||||
|
||||
// Copy file using DocumentFile
|
||||
contentResolver.openInputStream(uri)?.use { input ->
|
||||
@@ -88,6 +117,8 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
throw Exception("File copy failed")
|
||||
}
|
||||
|
||||
// Encrypt file if encryption is enabled
|
||||
|
||||
// Media scan the new file to hide it
|
||||
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
|
||||
mediaScanIntent.data = Uri.fromFile(targetDir)
|
||||
@@ -141,6 +172,62 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun unHideFile(file: File, onSuccess: (() -> Unit)? = null, onError: ((String) -> Unit)? = null) {
|
||||
lifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
// Create target directory (Downloads)
|
||||
val targetDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
|
||||
targetDir.mkdirs()
|
||||
|
||||
// Create target file with same name or timestamp
|
||||
val targetFile = File(targetDir, file.name)
|
||||
|
||||
// If file with same name exists, add timestamp
|
||||
val finalTargetFile = if (targetFile.exists()) {
|
||||
val nameWithoutExt = file.nameWithoutExtension
|
||||
val extension = file.extension
|
||||
File(targetDir, "${nameWithoutExt}_${System.currentTimeMillis()}.${extension}")
|
||||
} else {
|
||||
targetFile
|
||||
}
|
||||
|
||||
// Check if file is encrypted
|
||||
if (file.extension == ENCRYPTED_EXTENSION) {
|
||||
// Decrypt file
|
||||
val decryptedFile = SecurityUtils.changeFileExtension(file, SecurityUtils.getFileExtension(file))
|
||||
if (SecurityUtils.decryptFile(context, file, decryptedFile)) {
|
||||
decryptedFile.copyTo(finalTargetFile, overwrite = false)
|
||||
decryptedFile.delete()
|
||||
} else {
|
||||
throw Exception("Failed to decrypt file")
|
||||
}
|
||||
} else {
|
||||
// Copy file content
|
||||
file.copyTo(finalTargetFile, overwrite = false)
|
||||
}
|
||||
|
||||
// Verify copy success
|
||||
if (!finalTargetFile.exists() || finalTargetFile.length() == 0L) {
|
||||
throw Exception("File copy failed")
|
||||
}
|
||||
|
||||
// Media scan the new file
|
||||
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
|
||||
mediaScanIntent.data = Uri.fromFile(finalTargetFile)
|
||||
context.sendBroadcast(mediaScanIntent)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
onSuccess?.invoke()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
withContext(Dispatchers.Main) {
|
||||
onError?.invoke(e.message ?: "Unknown error occurred")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun deletePhotoFromExternalStorage(photoUri: Uri) {
|
||||
withContext(Dispatchers.IO) {
|
||||
@@ -150,11 +237,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
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 hide/unhide file", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
return@withContext
|
||||
}
|
||||
@@ -163,7 +246,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
try {
|
||||
context.contentResolver.delete(photoUri, null, null)
|
||||
withContext(Dispatchers.Main) {
|
||||
// Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show()
|
||||
// Toast.makeText(context, "File deleted successfully", Toast.LENGTH_SHORT).show()
|
||||
}
|
||||
} catch (e: SecurityException) {
|
||||
// Handle security exception for Android 10 and above
|
||||
@@ -221,8 +304,8 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
}
|
||||
|
||||
}
|
||||
class FileManager(){
|
||||
fun getContentUriImage(context: Context, file: File, fileType: FileType): Uri? {
|
||||
class FileManager{
|
||||
fun getContentUriImage(context: Context, file: File): Uri? {
|
||||
|
||||
// Query MediaStore for the file
|
||||
val projection = arrayOf(MediaStore.MediaColumns._ID)
|
||||
@@ -258,8 +341,6 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
} 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
|
||||
@@ -274,19 +355,22 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
|
||||
suspend fun processMultipleFiles(
|
||||
uriList: List<Uri>,
|
||||
fileType: FileType,
|
||||
callback: FileProcessCallback
|
||||
targetFolder: File,
|
||||
callback: FileProcessCallback,
|
||||
currentDir: File? = null
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val copiedFiles = mutableListOf<File>()
|
||||
for (uri in uriList) {
|
||||
try {
|
||||
val file = copyFileToHiddenDir(uri, fileType)
|
||||
val file = copyFileToHiddenDir(uri, targetFolder, currentDir)
|
||||
file?.let { copiedFiles.add(it) }
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
delay(500)
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
if (copiedFiles.isNotEmpty()) {
|
||||
callback.onFilesProcessedSuccessfully(copiedFiles)
|
||||
@@ -297,12 +381,24 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
|
||||
}
|
||||
}
|
||||
|
||||
fun getFileType(file: File): FileType {
|
||||
val extension = file.extension.lowercase()
|
||||
return when (extension) {
|
||||
"jpg", "jpeg", "png", "gif", "bmp", "webp" -> FileType.IMAGE
|
||||
"mp4", "avi", "mkv", "mov", "wmv", "flv", "webm", "3gp" -> FileType.VIDEO
|
||||
"mp3", "wav", "flac", "aac", "ogg", "m4a" -> FileType.AUDIO
|
||||
else -> FileType.DOCUMENT
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
enum class FileType(val dirName: String) {
|
||||
IMAGE(IMAGES_DIR),
|
||||
VIDEO(VIDEOS_DIR),
|
||||
AUDIO(AUDIO_DIR),
|
||||
DOCUMENT(DOCS_DIR)
|
||||
DOCUMENT(DOCS_DIR),
|
||||
ALL("all")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
52
app/src/main/java/devs/org/calculator/utils/FolderManager.kt
Normal file
@@ -0,0 +1,52 @@
|
||||
package devs.org.calculator.utils
|
||||
|
||||
import java.io.File
|
||||
|
||||
class FolderManager {
|
||||
|
||||
|
||||
fun createFolder(parentDir: File, folderName: String): Boolean {
|
||||
val newFolder = File(parentDir, folderName)
|
||||
return if (!newFolder.exists()) {
|
||||
newFolder.mkdirs()
|
||||
File(newFolder, ".nomedia").createNewFile()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFolder(folder: File): Boolean {
|
||||
return try {
|
||||
if (folder.exists() && folder.isDirectory) {
|
||||
folder.listFiles()?.forEach { file ->
|
||||
if (file.isFile) {
|
||||
file.delete()
|
||||
}
|
||||
}
|
||||
folder.delete()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun getFoldersInDirectory(directory: File): List<File> {
|
||||
return if (directory.exists() && directory.isDirectory) {
|
||||
directory.listFiles()?.filter { it.isDirectory && it.name != ".nomedia" } ?: emptyList()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
fun getFilesInFolder(folder: File): List<File> {
|
||||
return if (folder.exists() && folder.isDirectory) {
|
||||
folder.listFiles()?.filter { it.isFile && it.name != ".nomedia" } ?: emptyList()
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package devs.org.calculator.utils
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import java.security.MessageDigest
|
||||
import androidx.core.content.edit
|
||||
|
||||
class PrefsUtil(context: Context) {
|
||||
private val prefs: SharedPreferences = context.getSharedPreferences("Calculator", Context.MODE_PRIVATE)
|
||||
@@ -13,17 +14,33 @@ class PrefsUtil(context: Context) {
|
||||
|
||||
fun savePassword(password: String) {
|
||||
val hashedPassword = hashPassword(password)
|
||||
prefs.edit()
|
||||
.putString("password", hashedPassword)
|
||||
.apply()
|
||||
prefs.edit {
|
||||
putString("password", hashedPassword)
|
||||
}
|
||||
}
|
||||
|
||||
fun setBoolean(key:String, value: Boolean){
|
||||
return prefs.edit { putBoolean(key, value) }
|
||||
|
||||
}
|
||||
fun setInt(key:String, value: Int){
|
||||
return prefs.edit { putInt(key, value) }
|
||||
|
||||
}
|
||||
|
||||
fun getBoolean(key: String, defValue: Boolean = false): Boolean{
|
||||
return prefs.getBoolean(key,defValue)
|
||||
}
|
||||
fun getInt(key: String, defValue: Int): Int{
|
||||
return prefs.getInt(key,defValue)
|
||||
}
|
||||
|
||||
fun resetPassword(){
|
||||
prefs.edit()
|
||||
.remove("password")
|
||||
.remove("security_question")
|
||||
.remove("security_answer")
|
||||
.apply()
|
||||
prefs.edit {
|
||||
remove("password")
|
||||
.remove("security_question")
|
||||
.remove("security_answer")
|
||||
}
|
||||
}
|
||||
|
||||
fun validatePassword(input: String): Boolean {
|
||||
@@ -31,11 +48,15 @@ class PrefsUtil(context: Context) {
|
||||
return stored == hashPassword(input)
|
||||
}
|
||||
|
||||
fun getPassword(): String{
|
||||
return prefs.getString("password", "") ?: ""
|
||||
}
|
||||
|
||||
fun saveSecurityQA(question: String, answer: String) {
|
||||
prefs.edit()
|
||||
.putString("security_question", question)
|
||||
.putString("security_answer", hashPassword(answer))
|
||||
.apply()
|
||||
prefs.edit {
|
||||
putString("security_question", question)
|
||||
.putString("security_answer", hashPassword(answer))
|
||||
}
|
||||
}
|
||||
|
||||
fun validateSecurityAnswer(answer: String): Boolean {
|
||||
|
||||
@@ -5,60 +5,237 @@ import android.net.Uri
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.CipherInputStream
|
||||
import javax.crypto.CipherOutputStream
|
||||
import javax.crypto.KeyGenerator
|
||||
import javax.crypto.SecretKey
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import android.content.SharedPreferences
|
||||
import androidx.core.content.FileProvider
|
||||
import devs.org.calculator.database.HiddenFileEntity
|
||||
import androidx.core.content.edit
|
||||
|
||||
class SecurityUtils {
|
||||
companion object {
|
||||
private const val ALGORITHM = "AES"
|
||||
private const val HIDDEN_FOLDER = "Calculator_Data"
|
||||
object SecurityUtils {
|
||||
private const val ALGORITHM = "AES"
|
||||
private const val TRANSFORMATION = "AES/CBC/PKCS5Padding"
|
||||
private const val KEY_SIZE = 256
|
||||
|
||||
private fun getSecretKey(context: Context): SecretKey {
|
||||
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
|
||||
val useCustomKey = keyStore.getBoolean("use_custom_key", false)
|
||||
|
||||
fun validatePassword(input: String, storedHash: String): Boolean {
|
||||
return input.hashCode().toString() == storedHash
|
||||
}
|
||||
|
||||
fun encryptFile(context: Context, sourceUri: Uri, password: String): File {
|
||||
val inputStream = context.contentResolver.openInputStream(sourceUri)
|
||||
val secretKey = generateKey(password)
|
||||
val cipher = Cipher.getInstance(ALGORITHM)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey)
|
||||
|
||||
val hiddenDir = File(context.getExternalFilesDir(null), HIDDEN_FOLDER)
|
||||
if (!hiddenDir.exists()) hiddenDir.mkdirs()
|
||||
|
||||
val encryptedFile = File(hiddenDir, "${System.currentTimeMillis()}_encrypted")
|
||||
val outputStream = FileOutputStream(encryptedFile)
|
||||
|
||||
inputStream?.use { input ->
|
||||
val buffer = ByteArray(1024)
|
||||
var read: Int
|
||||
while (input.read(buffer).also { read = it } != -1) {
|
||||
val encrypted = cipher.update(buffer, 0, read)
|
||||
outputStream.write(encrypted)
|
||||
if (useCustomKey) {
|
||||
val customKey = keyStore.getString("custom_key", null)
|
||||
if (customKey != null) {
|
||||
try {
|
||||
val messageDigest = java.security.MessageDigest.getInstance("SHA-256")
|
||||
val keyBytes = messageDigest.digest(customKey.toByteArray())
|
||||
return SecretKeySpec(keyBytes, ALGORITHM)
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
val finalBlock = cipher.doFinal()
|
||||
outputStream.write(finalBlock)
|
||||
}
|
||||
outputStream.close()
|
||||
return encryptedFile
|
||||
}
|
||||
|
||||
fun decryptFile(file: File, password: String): ByteArray {
|
||||
val secretKey = generateKey(password)
|
||||
val cipher = Cipher.getInstance(ALGORITHM)
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey)
|
||||
|
||||
val inputStream = FileInputStream(file)
|
||||
val bytes = inputStream.readBytes()
|
||||
inputStream.close()
|
||||
|
||||
return cipher.doFinal(bytes)
|
||||
}
|
||||
|
||||
private fun generateKey(password: String): SecretKey {
|
||||
val keyBytes = password.toByteArray().copyOf(16)
|
||||
return SecretKeySpec(keyBytes, ALGORITHM)
|
||||
val encodedKey = keyStore.getString("secret_key", null)
|
||||
return if (encodedKey != null) {
|
||||
try {
|
||||
val decodedKey = android.util.Base64.decode(encodedKey, android.util.Base64.DEFAULT)
|
||||
SecretKeySpec(decodedKey, ALGORITHM)
|
||||
} catch (_: Exception) {
|
||||
generateAndStoreNewKey(keyStore)
|
||||
}
|
||||
} else {
|
||||
generateAndStoreNewKey(keyStore)
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateAndStoreNewKey(keyStore: SharedPreferences): SecretKey {
|
||||
val keyGenerator = KeyGenerator.getInstance(ALGORITHM)
|
||||
keyGenerator.init(KEY_SIZE, SecureRandom())
|
||||
val key = keyGenerator.generateKey()
|
||||
val encodedKey = android.util.Base64.encodeToString(key.encoded, android.util.Base64.DEFAULT)
|
||||
keyStore.edit { putString("secret_key", encodedKey) }
|
||||
return key
|
||||
}
|
||||
|
||||
fun encryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
|
||||
return try {
|
||||
if (!inputFile.exists()) {
|
||||
return false
|
||||
}
|
||||
|
||||
val secretKey = getSecretKey(context)
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
val iv = ByteArray(16)
|
||||
SecureRandom().nextBytes(iv)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey, IvParameterSpec(iv))
|
||||
|
||||
FileInputStream(inputFile).use { input ->
|
||||
FileOutputStream(outputFile).use { output ->
|
||||
output.write(iv)
|
||||
CipherOutputStream(output, cipher).use { cipherOutput ->
|
||||
input.copyTo(cipherOutput)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!outputFile.exists() || outputFile.length() == 0L) {
|
||||
return false
|
||||
}
|
||||
|
||||
FileInputStream(outputFile).use { input ->
|
||||
val iv = ByteArray(16)
|
||||
val bytesRead = input.read(iv)
|
||||
if (bytesRead != 16) {
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
} catch (_: Exception) {
|
||||
|
||||
if (outputFile.exists()) {
|
||||
outputFile.delete()
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun getDecryptedPreviewFile(context: Context, meta: HiddenFileEntity): File? {
|
||||
try {
|
||||
val encryptedFile = File(meta.filePath)
|
||||
if (!encryptedFile.exists()) {
|
||||
return null
|
||||
}
|
||||
|
||||
val tempDir = File(context.cacheDir, "preview_temp")
|
||||
if (!tempDir.exists()) tempDir.mkdirs()
|
||||
|
||||
val tempFile = File(tempDir, "preview_${System.currentTimeMillis()}_${meta.fileName}")
|
||||
|
||||
tempDir.listFiles()?.forEach { it.delete() }
|
||||
|
||||
val success = decryptFile(context, encryptedFile, tempFile)
|
||||
|
||||
if (success && tempFile.exists() && tempFile.length() > 0) {
|
||||
return tempFile
|
||||
} else {
|
||||
if (tempFile.exists()) tempFile.delete()
|
||||
return null
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun getUriForPreviewFile(context: Context, file: File): Uri? {
|
||||
return try {
|
||||
FileProvider.getUriForFile(
|
||||
context,
|
||||
"${context.packageName}.provider", // Must match AndroidManifest
|
||||
file
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
fun decryptFile(context: Context, inputFile: File, outputFile: File): Boolean {
|
||||
return try {
|
||||
if (!inputFile.exists()) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (inputFile.length() == 0L) {
|
||||
return false
|
||||
}
|
||||
|
||||
val secretKey = getSecretKey(context)
|
||||
val cipher = Cipher.getInstance(TRANSFORMATION)
|
||||
FileInputStream(inputFile).use { input ->
|
||||
val iv = ByteArray(16)
|
||||
val bytesRead = input.read(iv)
|
||||
if (bytesRead != 16) {
|
||||
return false
|
||||
}
|
||||
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKey, IvParameterSpec(iv))
|
||||
|
||||
FileInputStream(inputFile).use { decInput ->
|
||||
decInput.skip(16)
|
||||
|
||||
FileOutputStream(outputFile).use { output ->
|
||||
CipherInputStream(decInput, cipher).use { cipherInput ->
|
||||
cipherInput.copyTo(output)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!outputFile.exists() || outputFile.length() == 0L) {
|
||||
return false
|
||||
}
|
||||
|
||||
true
|
||||
} catch (_: Exception) {
|
||||
if (outputFile.exists()) {
|
||||
outputFile.delete()
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun getFileExtension(file: File): String {
|
||||
val name = file.name
|
||||
val lastDotIndex = name.lastIndexOf('.')
|
||||
return if (lastDotIndex > 0) {
|
||||
name.substring(lastDotIndex)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fun changeFileExtension(file: File, newExtension: String): File {
|
||||
val name = file.name
|
||||
val lastDotIndex = name.lastIndexOf('.')
|
||||
val newName = if (lastDotIndex > 0) {
|
||||
name.substring(0, lastDotIndex) + newExtension
|
||||
} else {
|
||||
name + newExtension
|
||||
}
|
||||
return File(file.parent, newName)
|
||||
}
|
||||
|
||||
fun setCustomKey(context: Context, key: String): Boolean {
|
||||
return try {
|
||||
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
|
||||
keyStore.edit {
|
||||
putString("custom_key", key)
|
||||
putBoolean("use_custom_key", true)
|
||||
}
|
||||
true
|
||||
} catch (_: Exception) {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCustomKey(context: Context) {
|
||||
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
|
||||
keyStore.edit {
|
||||
remove("custom_key")
|
||||
putBoolean("use_custom_key", false)
|
||||
}
|
||||
}
|
||||
|
||||
fun isUsingCustomKey(context: Context): Boolean {
|
||||
val keyStore = context.getSharedPreferences("keystore", Context.MODE_PRIVATE)
|
||||
return keyStore.getBoolean("use_custom_key", false)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.content.PermissionChecker
|
||||
import androidx.core.net.toUri
|
||||
|
||||
class StoragePermissionUtil(private val activity: AppCompatActivity) {
|
||||
|
||||
@@ -34,7 +35,7 @@ class StoragePermissionUtil(private val activity: AppCompatActivity) {
|
||||
onGranted()
|
||||
} else {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION).apply {
|
||||
data = Uri.parse("package:${activity.packageName}")
|
||||
data = "package:${activity.packageName}".toUri()
|
||||
}
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
9
app/src/main/res/anim/fab_close.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<scale xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="200"
|
||||
android:fromXScale="1.0"
|
||||
android:fromYScale="1.0"
|
||||
android:toXScale="0.0"
|
||||
android:toYScale="0.0"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%" />
|
||||
9
app/src/main/res/anim/fab_open.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<scale xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="200"
|
||||
android:fromXScale="0.0"
|
||||
android:fromYScale="0.0"
|
||||
android:toXScale="1.0"
|
||||
android:toYScale="1.0"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%" />
|
||||
7
app/src/main/res/anim/rotate_close.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="200"
|
||||
android:fromDegrees="45"
|
||||
android:toDegrees="0"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%" />
|
||||
7
app/src/main/res/anim/rotate_open.xml
Normal file
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:duration="200"
|
||||
android:fromDegrees="0"
|
||||
android:toDegrees="45"
|
||||
android:pivotX="50%"
|
||||
android:pivotY="50%" />
|
||||
9
app/src/main/res/drawable/add_image.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="50dp"
|
||||
android:height="50dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M23,4v2h-3v3h-2L18,6h-3L15,4h3L18,1h2v3h3zM14.5,11c0.828,0 1.5,-0.672 1.5,-1.5S15.328,8 14.5,8 13,8.672 13,9.5s0.672,1.5 1.5,1.5zM18,14.234l-0.513,-0.57c-0.794,-0.885 -2.18,-0.885 -2.976,0l-0.655,0.73L9,9l-3,3.333L6,6h7L13,4L6,4c-1.105,0 -2,0.895 -2,2v12c0,1.105 0.895,2 2,2h12c1.105,0 2,-0.895 2,-2v-7h-2v3.234z"/>
|
||||
</vector>
|
||||
@@ -2,12 +2,12 @@
|
||||
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M7.7,6.36L3.533,11.36C3.224,11.731 3.224,12.269 3.533,12.64L7.7,17.64C7.89,17.868 8.172,18 8.468,18H18C19.657,18 21,16.657 21,15V9C21,7.343 19.657,6 18,6H8.468C8.172,6 7.89,6.132 7.7,6.36Z"
|
||||
android:strokeColor="@color/textColor" android:strokeLineCap="round"
|
||||
android:strokeColor="@color/white" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M15,10L13,12M13,12L11,14M13,12L11,10M13,12L15,14"
|
||||
android:strokeColor="@color/textColor"
|
||||
android:strokeColor="@color/white"
|
||||
android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"/>
|
||||
|
||||
6
app/src/main/res/drawable/bottom_corner.xml
Normal file
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<corners android:bottomLeftRadius="15dp" android:bottomRightRadius="15dp"/>
|
||||
<solid android:color="?attr/colorSecondaryContainer"/>
|
||||
</shape>
|
||||
5
app/src/main/res/drawable/bottom_shade.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<gradient android:startColor="#CC000000" android:centerColor="#00ffffff" android:endColor="#00ffffff" android:angle="90"/>
|
||||
</shape>
|
||||
9
app/src/main/res/drawable/document_add.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="205dp"
|
||||
android:height="205dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M15,12h-2v-2c0,-0.553 -0.447,-1 -1,-1s-1,0.447 -1,1v2h-2c-0.553,0 -1,0.447 -1,1s0.447,1 1,1h2v2c0,0.553 0.447,1 1,1s1,-0.447 1,-1v-2h2c0.553,0 1,-0.447 1,-1s-0.447,-1 -1,-1zM19.707,7.293l-4,-4c-0.187,-0.188 -0.441,-0.293 -0.707,-0.293h-8c-1.654,0 -3,1.346 -3,3v12c0,1.654 1.346,3 3,3h10c1.654,0 3,-1.346 3,-3v-10c0,-0.266 -0.105,-0.52 -0.293,-0.707zM17.586,8h-1.086c-0.827,0 -1.5,-0.673 -1.5,-1.5v-1.086l2.586,2.586zM17,19h-10c-0.552,0 -1,-0.448 -1,-1v-12c0,-0.552 0.448,-1 1,-1h7v1.5c0,1.379 1.121,2.5 2.5,2.5h1.5v9c0,0.552 -0.448,1 -1,1z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
||||
17
app/src/main/res/drawable/encrypted.xml
Normal file
@@ -0,0 +1,17 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="50dp" android:viewportHeight="73" android:viewportWidth="73" android:width="50dp">
|
||||
|
||||
<path android:fillColor="#00FFFFFF" android:fillType="nonZero" android:pathData="M15,1L58,1A14,14 0,0 1,72 15L72,58A14,14 0,0 1,58 72L15,72A14,14 0,0 1,1 58L1,15A14,14 0,0 1,15 1z" android:strokeColor="#0004673E" android:strokeWidth="2"/>
|
||||
|
||||
<path android:fillColor="#00DD80" android:fillType="nonZero" android:pathData="M54.191,43.754C52.953,47.108 51.081,50.025 48.626,52.423C45.832,55.151 42.173,57.319 37.751,58.866C37.605,58.917 37.454,58.958 37.302,58.989C37.101,59.028 36.896,59.05 36.694,59.053L36.654,59.053C36.438,59.053 36.221,59.031 36.005,58.989C35.853,58.958 35.704,58.917 35.56,58.867C31.132,57.323 27.469,55.156 24.671,52.427C22.215,50.03 20.344,47.115 19.108,43.76C16.86,37.66 16.988,30.941 17.091,25.542L17.093,25.459C17.113,25.013 17.127,24.544 17.134,24.027C17.172,21.488 19.191,19.387 21.73,19.246C27.025,18.95 31.121,17.223 34.621,13.812L34.652,13.784C35.233,13.251 35.965,12.989 36.694,13C37.396,13.009 38.096,13.271 38.657,13.784L38.687,13.812C42.187,17.223 46.283,18.95 51.578,19.246C54.118,19.387 56.137,21.488 56.174,24.027C56.182,24.548 56.195,25.016 56.216,25.459L56.217,25.494C56.319,30.904 56.446,37.636 54.191,43.754Z" android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
|
||||
<path android:fillColor="#03D078" android:fillType="nonZero" android:pathData="M54.191,43.754C52.953,47.108 51.081,50.025 48.626,52.423C45.832,55.151 42.173,57.319 37.751,58.866C37.605,58.917 37.454,58.958 37.302,58.989C37.101,59.028 36.896,59.05 36.694,59.053L36.694,13C37.396,13.009 38.096,13.271 38.657,13.784L38.687,13.812C42.187,17.223 46.283,18.95 51.578,19.246C54.118,19.387 56.137,21.488 56.174,24.027C56.182,24.548 56.195,25.016 56.216,25.459L56.217,25.494C56.319,30.904 56.446,37.636 54.191,43.754Z" android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
|
||||
<path android:fillColor="#00FFFFFF" android:fillType="nonZero" android:pathData="M48.131,36.026C48.131,42.341 43.003,47.482 36.694,47.504L36.653,47.504C30.325,47.504 25.176,42.355 25.176,36.026C25.176,29.698 30.325,24.549 36.653,24.549L36.694,24.549C43.003,24.571 48.131,29.712 48.131,36.026Z" android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
|
||||
<path android:fillColor="#00E1EBF0" android:fillType="nonZero" android:pathData="M48.131,36.026C48.131,42.341 43.003,47.482 36.694,47.504L36.694,24.549C43.003,24.571 48.131,29.712 48.131,36.026Z" android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
|
||||
<path android:fillColor="#FFFFFF" android:fillType="nonZero" android:pathData="M41.863,34.374L36.694,39.543L35.577,40.66C35.313,40.924 34.967,41.056 34.621,41.056C34.275,41.056 33.929,40.924 33.665,40.66L31.264,38.258C30.736,37.73 30.736,36.875 31.264,36.347C31.791,35.819 32.646,35.819 33.174,36.347L34.621,37.794L39.952,32.463C40.48,31.935 41.336,31.935 41.863,32.463C42.391,32.991 42.391,33.847 41.863,34.374Z" android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
|
||||
<path android:fillColor="#F8F6F6" android:fillType="nonZero" android:pathData="M41.863,34.374L36.694,39.543L36.694,35.721L39.952,32.463C40.48,31.935 41.336,31.935 41.863,32.463C42.391,32.991 42.391,33.847 41.863,34.374Z" android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
|
||||
</vector>
|
||||
5
app/src/main/res/drawable/gradient_bg.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<corners android:radius="15dp"/>
|
||||
<gradient android:startColor="?attr/colorPrimary" android:endColor="?attr/colorPrimary" android:angle="75"/>
|
||||
</shape>
|
||||
10
app/src/main/res/drawable/ic_add.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/>
|
||||
</vector>
|
||||
@@ -1,9 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/textColor"
|
||||
android:pathData="M12,3v9.28c-0.47,-0.17 -0.97,-0.28 -1.5,-0.28C8.01,12 6,14.01 6,16.5S8.01,21 10.5,21c2.31,0 4.2,-1.75 4.45,-4H15V6h4V3h-7z"/>
|
||||
</vector>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="?attr/colorPrimary" android:pathData="M13.75,2C13.75,1.586 13.414,1.25 13,1.25C12.586,1.25 12.25,1.586 12.25,2V14.536C11.4,13.738 10.257,13.25 9,13.25C6.377,13.25 4.25,15.377 4.25,18C4.25,20.623 6.377,22.75 9,22.75C11.623,22.75 13.75,20.623 13.75,18V6.243C14.988,7.772 16.879,8.75 19,8.75C19.414,8.75 19.75,8.414 19.75,8C19.75,7.586 19.414,7.25 19,7.25C16.101,7.25 13.75,4.899 13.75,2Z"/>
|
||||
|
||||
</vector>
|
||||
|
||||
12
app/src/main/res/drawable/ic_back.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="800dp"
|
||||
android:height="800dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M224,480h640a32,32 0,1 1,0 64H224a32,32 0,0 1,0 -64z"
|
||||
android:fillColor="@color/svgTintColor"/>
|
||||
<path
|
||||
android:pathData="m237.2,512 l265.4,265.3a32,32 0,0 1,-45.3 45.3l-288,-288a32,32 0,0 1,0 -45.3l288,-288a32,32 0,1 1,45.3 45.3L237.2,512z"
|
||||
android:fillColor="@color/svgTintColor"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_close.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_create_new_folder.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M20,6h-8l-2,-2L4,4c-1.11,0 -1.99,0.89 -1.99,2L2,18c0,1.11 0.89,2 2,2h16c1.11,0 2,-0.89 2,-2L22,8c0,-1.11 -0.89,-2 -2,-2zM20,18L4,18L4,8h16v10zM14,12h-2v-2h-2v2L8,12v2h2v2h2v-2h2z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_delete.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
|
||||
</vector>
|
||||
@@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/textColor"
|
||||
android:pathData="M14,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8L14,2zM16,18H8v-2h8V18zM16,14H8v-2h8V14zM13,9V3.5L18.5,9H13z"/>
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM16,18L8,18v-2h8v2zM16,14L8,14v-2h8v2zM13,9L13,3.5L18.5,9L13,9z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_edit.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="800dp"
|
||||
android:height="800dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M3,17.25V21h3.75L17.81,9.94l-3.75,-3.75L3,17.25zM20.71,7.04c0.39,-0.39 0.39,-1.02 0,-1.41l-2.34,-2.34c-0.39,-0.39 -1.02,-0.39 -1.41,0l-1.83,1.83 3.75,3.75 1.83,-1.83z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
@@ -1,9 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:width="50dp"
|
||||
android:height="50dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/textColor"
|
||||
android:pathData="M6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2H18c1.1,0 2,-0.9 2,-2V8l-6,-6H6zM13,9V3.5L18.5,9H13z"/>
|
||||
</vector>
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M13.172,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8.828c0,-0.53 -0.211,-1.039 -0.586,-1.414l-4.828,-4.828C14.211,2.211 13.702,2 13.172,2zM15,18H9c-0.552,0 -1,-0.448 -1,-1v0c0,-0.552 0.448,-1 1,-1h6c0.552,0 1,0.448 1,1v0C16,17.552 15.552,18 15,18zM15,14H9c-0.552,0 -1,-0.448 -1,-1v0c0,-0.552 0.448,-1 1,-1h6c0.552,0 1,0.448 1,1v0C16,13.552 15.552,14 15,14zM13,9V3.5L18.5,9H13z"/>
|
||||
</vector>
|
||||
|
||||
9
app/src/main/res/drawable/ic_file_no_item.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="50dp"
|
||||
android:height="50dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#757575"
|
||||
android:pathData="M13.172,2H6C4.9,2 4,2.9 4,4v16c0,1.1 0.9,2 2,2h12c1.1,0 2,-0.9 2,-2V8.828c0,-0.53 -0.211,-1.039 -0.586,-1.414l-4.828,-4.828C14.211,2.211 13.702,2 13.172,2zM15,18H9c-0.552,0 -1,-0.448 -1,-1v0c0,-0.552 0.448,-1 1,-1h6c0.552,0 1,0.448 1,1v0C16,17.552 15.552,18 15,18zM15,14H9c-0.552,0 -1,-0.448 -1,-1v0c0,-0.552 0.448,-1 1,-1h6c0.552,0 1,0.448 1,1v0C16,13.552 15.552,14 15,14zM13,9V3.5L18.5,9H13z"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_folder.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="?attr/colorPrimary"
|
||||
android:pathData="M20,6h-8l-2,-2L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8h16v10z"/>
|
||||
</vector>
|
||||
13
app/src/main/res/drawable/ic_folder_add.xml
Normal file
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M20,6h-8l-2,-2L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8h16v10z"/>
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M15,13h-2v2h-2v-2H9v-2h2V9h2v2h2v2z"/>
|
||||
</vector>
|
||||
37
app/src/main/res/drawable/ic_folder_yellow.xml
Normal file
@@ -0,0 +1,37 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:aapt="http://schemas.android.com/aapt"
|
||||
android:width="800dp"
|
||||
android:height="800dp"
|
||||
android:viewportWidth="32"
|
||||
android:viewportHeight="32">
|
||||
<path
|
||||
android:pathData="M2.909,3.5L11.283,3.5C12.441,3.5 13.551,3.96 14.369,4.778L14.722,5.131C15.54,5.949 16.65,6.409 17.807,6.409L23.273,6.409C24.076,6.409 24.762,6.693 25.33,7.261C25.898,7.829 26.182,8.515 26.182,9.318L26.182,25.318C26.182,26.122 25.898,26.807 25.33,27.375C24.762,27.943 24.076,28.227 23.273,28.227L2.909,28.227C2.106,28.227 1.42,27.943 0.852,27.375C0.284,26.807 0,26.122 0,25.318L0,6.409C0,5.606 0.284,4.92 0.852,4.352C1.42,3.784 2.106,3.5 2.909,3.5Z"
|
||||
android:fillType="evenOdd">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="16"
|
||||
android:startY="3.5"
|
||||
android:endX="16"
|
||||
android:endY="19.854"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFFBA200"/>
|
||||
<item android:offset="1" android:color="#FFFF7300"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
<path
|
||||
android:pathData="M5.818,25.318L5.818,12.227C5.818,11.424 6.102,10.738 6.67,10.17C7.238,9.602 7.924,9.318 8.727,9.318L29.091,9.318C29.894,9.318 30.58,9.602 31.148,10.17C31.716,10.738 32,11.424 32,12.227L32,25.318C32,26.122 31.716,26.807 31.148,27.375C30.58,27.943 29.894,28.227 29.091,28.227L2.909,28.227C3.712,28.227 4.398,27.943 4.966,27.375C5.534,26.807 5.818,26.122 5.818,25.318Z"
|
||||
android:fillType="evenOdd">
|
||||
<aapt:attr name="android:fillColor">
|
||||
<gradient
|
||||
android:startX="17.454"
|
||||
android:startY="9.318"
|
||||
android:endX="17.454"
|
||||
android:endY="28.227"
|
||||
android:type="linear">
|
||||
<item android:offset="0" android:color="#FFFAC227"/>
|
||||
<item android:offset="1" android:color="#FFFAA627"/>
|
||||
</gradient>
|
||||
</aapt:attr>
|
||||
</path>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_github.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<vector
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="20"
|
||||
android:viewportWidth="20"
|
||||
android:width="24dp">
|
||||
|
||||
<path android:fillColor="@color/black" android:fillType="evenOdd" android:pathData="M10,0C15.523,0 20,4.59 20,10.253C20,14.782 17.138,18.624 13.167,19.981C12.66,20.082 12.48,19.762 12.48,19.489C12.48,19.151 12.492,18.047 12.492,16.675C12.492,15.719 12.172,15.095 11.813,14.777C14.04,14.523 16.38,13.656 16.38,9.718C16.38,8.598 15.992,7.684 15.35,6.966C15.454,6.707 15.797,5.664 15.252,4.252C15.252,4.252 14.414,3.977 12.505,5.303C11.706,5.076 10.85,4.962 10,4.958C9.15,4.962 8.295,5.076 7.497,5.303C5.586,3.977 4.746,4.252 4.746,4.252C4.203,5.664 4.546,6.707 4.649,6.966C4.01,7.684 3.619,8.598 3.619,9.718C3.619,13.646 5.954,14.526 8.175,14.785C7.889,15.041 7.63,15.493 7.54,16.156C6.97,16.418 5.522,16.871 4.63,15.304C4.63,15.304 4.101,14.319 3.097,14.247C3.097,14.247 2.122,14.234 3.029,14.87C3.029,14.87 3.684,15.185 4.139,16.37C4.139,16.37 4.726,18.2 7.508,17.58C7.513,18.437 7.522,19.245 7.522,19.489C7.522,19.76 7.338,20.077 6.839,19.982C2.865,18.627 0,14.783 0,10.253C0,4.59 4.478,0 10,0" android:strokeColor="#00000000" android:strokeWidth="1"/>
|
||||
|
||||
</vector>
|
||||
12
app/src/main/res/drawable/ic_grid.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="169dp"
|
||||
android:height="169dp"
|
||||
android:viewportWidth="35.28"
|
||||
android:viewportHeight="35.28">
|
||||
<path
|
||||
android:pathData="M27.64,3.64L23.64,3.64C21.431,3.64 19.64,5.431 19.64,7.64L19.64,11.64C19.64,13.849 21.431,15.64 23.64,15.64L27.64,15.64C29.849,15.64 31.64,13.849 31.64,11.64L31.64,7.64C31.64,5.431 29.849,3.64 27.64,3.64L27.64,3.64ZM27.64,19.64L23.64,19.64C21.431,19.64 19.64,21.431 19.64,23.64L19.64,27.64C19.64,29.849 21.431,31.64 23.64,31.64L27.64,31.64C29.849,31.64 31.64,29.849 31.64,27.64L31.64,23.64C31.64,21.431 29.849,19.64 27.64,19.64L27.64,19.64ZM11.64,19.64L7.64,19.64C5.431,19.64 3.64,21.431 3.64,23.64L3.64,27.64C3.64,29.849 5.431,31.64 7.64,31.64L11.64,31.64C13.849,31.64 15.64,29.849 15.64,27.64L15.64,23.64C15.64,21.431 13.849,19.64 11.64,19.64L11.64,19.64ZM11.64,3.64L7.64,3.64C5.431,3.64 3.64,5.431 3.64,7.64L3.64,11.64C3.64,13.849 5.431,15.64 7.64,15.64L11.64,15.64C13.849,15.64 15.64,13.849 15.64,11.64L15.64,7.64C15.64,5.431 13.849,3.64 11.64,3.64L11.64,3.64Z"
|
||||
android:strokeWidth="1"
|
||||
android:fillColor="?attr/colorPrimary"
|
||||
android:fillType="evenOdd"
|
||||
android:strokeColor="#00000000"/>
|
||||
</vector>
|
||||
@@ -1,9 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/textColor"
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M21,19V5c0,-1.1 -0.9,-2 -2,-2H5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h14c1.1,0 2,-0.9 2,-2zM8.5,13.5l2.5,3.01L14.5,12l4.5,6H5l3.5,-4.5z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_list.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="800dp"
|
||||
android:height="800dp"
|
||||
android:viewportWidth="16"
|
||||
android:viewportHeight="16">
|
||||
<path
|
||||
android:pathData="M4,13L2,13L2,11L4,11ZM14,11L6,11v2h8ZM4,7L2,7L2,9L4,9ZM14,7L6,7L6,9h8ZM4,3L2,3L2,5L4,5ZM14,3L6,3L6,5h8Z"
|
||||
android:fillColor="#ffffff"/>
|
||||
</vector>
|
||||
8
app/src/main/res/drawable/ic_more.xml
Normal file
@@ -0,0 +1,8 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillColor="#fff"
|
||||
android:pathData="M12,12H12.01M12,6H12.01M12,18H12.01M13,12C13,12.552 12.552,13 12,13C11.448,13 11,12.552 11,12C11,11.448 11.448,11 12,11C12.552,11 13,11.448 13,12ZM13,18C13,18.552 12.552,19 12,19C11.448,19 11,18.552 11,18C11,17.448 11.448,17 12,17C12.552,17 13,17.448 13,18ZM13,6C13,6.552 12.552,7 12,7C11.448,7 11,6.552 11,6C11,5.448 11.448,5 12,5C12.552,5 13,5.448 13,6Z" android:strokeColor="#fff" android:strokeLineCap="round" android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_no_items.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#757575"
|
||||
android:pathData="M20,6h-8l-2,-2L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8h16v10z"/>
|
||||
</vector>
|
||||
12
app/src/main/res/drawable/ic_play_circle.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="800dp"
|
||||
android:height="800dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="?attr/colorPrimary"
|
||||
android:pathData="M34.6,23.3 L18.1,13.2c-0.6,-0.4 -1.1,-0.1 -1.1,0.6V34.2c0,0.7 0.5,1 1.1,0.6L34.6,24.7A0.8,0.8 0,0 0,34.6 23.3Z"/>
|
||||
<path
|
||||
android:fillColor="?attr/colorPrimary"
|
||||
android:pathData="M24,2A22,22 0,1 0,46 24,21.9 21.9,0 0,0 24,2ZM24,42A18,18 0,1 1,42 24,18.1 18.1,0 0,1 24,42Z"/>
|
||||
</vector>
|
||||
11
app/src/main/res/drawable/ic_setting.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
|
||||
<path android:fillAlpha="1" android:fillColor="?attr/colorPrimary" android:pathData="M17.88,18.6C17.88,19.926 16.806,21 15.48,21C14.155,21 13.08,19.926 13.08,18.6C13.08,17.274 14.155,16.2 15.48,16.2C16.806,16.2 17.88,17.274 17.88,18.6Z"/>
|
||||
|
||||
<path android:fillAlpha="1" android:fillColor="?attr/colorPrimary" android:pathData="M9.48,12C9.48,13.325 8.405,14.4 7.08,14.4C5.755,14.4 4.68,13.325 4.68,12C4.68,10.675 5.755,9.6 7.08,9.6C8.405,9.6 9.48,10.675 9.48,12Z"/>
|
||||
|
||||
<path android:fillAlpha="1" android:fillColor="?attr/colorPrimary" android:pathData="M17.88,5.4C17.88,6.725 16.806,7.8 15.48,7.8C14.155,7.8 13.08,6.725 13.08,5.4C13.08,4.075 14.155,3 15.48,3C16.806,3 17.88,4.075 17.88,5.4Z"/>
|
||||
|
||||
<path android:fillColor="#00000000" android:pathData="M18.48,18.537H21M4.68,12L3,12.044M4.68,12C4.68,13.325 5.755,14.4 7.08,14.4C8.405,14.4 9.48,13.325 9.48,12C9.48,10.675 8.405,9.6 7.08,9.6C5.755,9.6 4.68,10.675 4.68,12ZM10.169,12.044H21M12.801,5.551L3,5.551M21,5.551H18.48M3,18.537H12.801M17.88,18.6C17.88,19.926 16.806,21 15.48,21C14.155,21 13.08,19.926 13.08,18.6C13.08,17.274 14.155,16.2 15.48,16.2C16.806,16.2 17.88,17.274 17.88,18.6ZM17.88,5.4C17.88,6.725 16.806,7.8 15.48,7.8C14.155,7.8 13.08,6.725 13.08,5.4C13.08,4.075 14.155,3 15.48,3C16.806,3 17.88,4.075 17.88,5.4Z" android:strokeColor="?attr/colorPrimary" android:strokeLineCap="round" android:strokeWidth="1.5"/>
|
||||
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/ic_settings.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="800dp"
|
||||
android:height="800dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M4,13.743l-1,0.579a1,1 0,0 0,-0.366 1.366l1.488,2.578a1,1 0,0 0,1.366 0.366L6.5,18.05a1.987,1.987 0,0 1,1.986 0l0.02,0.011a1.989,1.989 0,0 1,1 1.724V21a1,1 0,0 0,1 1h3a1,1 0,0 0,1 -1V19.782a1.985,1.985 0,0 1,0.995 -1.721l0.021,-0.012a1.987,1.987 0,0 1,1.986 0l1.008,0.582a1,1 0,0 0,1.366 -0.366l1.488,-2.578A1,1 0,0 0,21 14.322l-1,-0.579a1.994,1.994 0,0 1,-1 -1.733v-0.021a1.991,1.991 0,0 1,1 -1.732l1,-0.579a1,1 0,0 0,0.366 -1.366L19.876,5.734a1,1 0,0 0,-1.366 -0.366L17.5,5.95a1.987,1.987 0,0 1,-1.986 0L15.5,5.94a1.989,1.989 0,0 1,-1 -1.724V3a1,1 0,0 0,-1 -1h-3a1,1 0,0 0,-1 1V4.294A1.856,1.856 0,0 1,8.57 5.9l-0.153,0.088a1.855,1.855 0,0 1,-1.853 0L5.49,5.368a1,1 0,0 0,-1.366 0.366L2.636,8.312A1,1 0,0 0,3 9.678l1,0.579A1.994,1.994 0,0 1,5 11.99v0.021A1.991,1.991 0,0 1,4 13.743ZM12,9a3,3 0,1 1,-3 3A3,3 0,0 1,12 9Z"
|
||||
android:fillColor="#fff"/>
|
||||
</vector>
|
||||
10
app/src/main/res/drawable/ic_video.xml
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FFFFFF"
|
||||
android:pathData="M17,10.5V7c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l4,4v-11l-4,4z"/>
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M13.75,2C13.75,1.586 13.414,1.25 13,1.25C12.586,1.25 12.25,1.586 12.25,2V14.536C11.4,13.738 10.257,13.25 9,13.25C6.377,13.25 4.25,15.377 4.25,18C4.25,20.623 6.377,22.75 9,22.75C11.623,22.75 13.75,20.623 13.75,18V6.243C14.988,7.772 16.879,8.75 19,8.75C19.414,8.75 19.75,8.414 19.75,8C19.75,7.586 19.414,7.25 19,7.25C16.101,7.25 13.75,4.899 13.75,2Z"
|
||||
android:fillColor="@color/textColor"/>
|
||||
</vector>
|
||||
41
app/src/main/res/drawable/music_add.xml
Normal file
@@ -0,0 +1,41 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="800dp"
|
||||
android:height="800dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M17,6.5H20M23,6.5H20M20,6.5V3.5M20,6.5V9.5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M6,16V5L14,4"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M15,14V10"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,19H13C14.105,19 15,18.105 15,17V14H12C10.895,14 10,14.895 10,16V17C10,18.105 10.895,19 12,19Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M3,21H4C5.105,21 6,20.105 6,19V16H3C1.895,16 1,16.895 1,18V19C1,20.105 1.895,21 3,21Z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
||||
@@ -5,6 +5,6 @@
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M5,7.766c0,-1.554 1.696,-2.515 3.029,-1.715l7.056,4.234c1.295,0.777 1.295,2.653 0,3.43l-7.056,4.234c-1.333,0.8 -3.029,-0.16 -3.029,-1.715V7.766zM14.056,12L7,7.766v8.468L14.056,12zM18,6a1,1 0,0 0,-1 1v10a1,1 0,1 0,2 0V7a1,1 0,0 0,-1 -1z"
|
||||
android:fillColor="@color/textColor"
|
||||
android:fillColor="?attr/colorPrimary"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M2,6C2,4.114 2,3.172 2.586,2.586C3.172,2 4.114,2 6,2C7.886,2 8.828,2 9.414,2.586C10,3.172 10,4.114 10,6V18C10,19.886 10,20.828 9.414,21.414C8.828,22 7.886,22 6,22C4.114,22 3.172,22 2.586,21.414C2,20.828 2,19.886 2,18V6Z"
|
||||
android:fillColor="@color/textColor"/>
|
||||
android:fillColor="?attr/colorPrimary"/>
|
||||
<path
|
||||
android:pathData="M14,6C14,4.114 14,3.172 14.586,2.586C15.172,2 16.114,2 18,2C19.886,2 20.828,2 21.414,2.586C22,3.172 22,4.114 22,6V18C22,19.886 22,20.828 21.414,21.414C20.828,22 19.886,22 18,22C16.114,22 15.172,22 14.586,21.414C14,20.828 14,19.886 14,18V6Z"
|
||||
android:fillColor="@color/textColor"/>
|
||||
android:fillColor="?attr/colorPrimary"/>
|
||||
</vector>
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:viewportHeight="512" android:viewportWidth="512" android:width="24dp">
|
||||
|
||||
<path android:fillColor="@color/textColor"
|
||||
android:pathData="M21.409,9.353C23.531,10.507 23.531,13.493 21.409,14.647L8.597,21.615C6.534,22.736 4,21.276 4,18.967L4,5.033C4,2.724 6.534,1.264 8.597,2.385L21.409,9.353Z"/>
|
||||
<path android:fillColor="?attr/colorPrimary" android:pathData="M464.7,221.5L86.1,7.3C52.5,-11.7 25,7.5 25,50v412c0,42.5 27.5,61.7 61.1,42.7l378.6,-214.1C498.2,271.5 498.2,240.5 464.7,221.5z"/>
|
||||
|
||||
</vector>
|
||||
|
||||
@@ -5,6 +5,6 @@
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M19,7.766c0,-1.554 -1.696,-2.515 -3.029,-1.715l-7.056,4.234c-1.295,0.777 -1.295,2.653 0,3.43l7.056,4.234c1.333,0.8 3.029,-0.16 3.029,-1.715V7.766zM9.944,12L17,7.766v8.468L9.944,12zM6,6a1,1 0,0 1,1 1v10a1,1 0,1 1,-2 0V7a1,1 0,0 1,1 -1z"
|
||||
android:fillColor="@color/textColor"
|
||||
android:fillColor="?attr/colorPrimary"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
||||
12
app/src/main/res/drawable/selected.xml
Normal file
@@ -0,0 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="800dp"
|
||||
android:height="800dp"
|
||||
android:viewportWidth="1024"
|
||||
android:viewportHeight="1024">
|
||||
<path
|
||||
android:pathData="M512,512m-448,0a448,448 0,1 0,896 0,448 448,0 1,0 -896,0Z"
|
||||
android:fillColor="#00ffffff"/>
|
||||
<path
|
||||
android:pathData="M738.1,311.5L448,601.6l-119.5,-119.5 -59.7,59.7 179.2,179.2 349.9,-349.9z"
|
||||
android:fillColor="@color/black"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/video_add.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="800dp"
|
||||
android:height="800dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M16,4c0.552,0 1,0.448 1,1v4.2l5.213,-3.65c0.226,-0.158 0.538,-0.103 0.697,0.124 0.058,0.084 0.09,0.184 0.09,0.286v12.08c0,0.276 -0.224,0.5 -0.5,0.5 -0.103,0 -0.203,-0.032 -0.287,-0.09L17,14.8V19c0,0.552 -0.448,1 -1,1H2c-0.552,0 -1,-0.448 -1,-1V5c0,-0.552 0.448,-1 1,-1h14zM8,8v3H5v2h2.999L8,16h2l-0.001,-3H13v-2h-3V8H8z"/>
|
||||
</vector>
|
||||
9
app/src/main/res/drawable/wrong.xml
Normal file
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="800dp"
|
||||
android:height="800dp"
|
||||
android:viewportWidth="256"
|
||||
android:viewportHeight="256">
|
||||
<path
|
||||
android:pathData="M142,128l49,-49a9.9,9.9 0,0 0,-14 -14L128,114 79,65A9.9,9.9 0,0 0,65 79l49,49L65,177a9.9,9.9 0,0 0,14 14l49,-49 49,49a9.9,9.9 0,0 0,14 -14Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".activities.AudioGalleryActivity">
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -12,7 +12,7 @@
|
||||
android:id="@+id/tvTitle"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Change Password"
|
||||
android:text="@string/change_password"
|
||||
android:textSize="24sp"
|
||||
android:textStyle="bold"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@@ -24,7 +24,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="32dp"
|
||||
android:hint="Enter Old Password"
|
||||
android:hint="@string/enter_old_password"
|
||||
app:endIconMode="password_toggle"
|
||||
app:layout_constraintTop_toBottomOf="@id/tvTitle">
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="16dp"
|
||||
android:hint="Enter New Password"
|
||||
android:hint="@string/enter_new_password"
|
||||
app:endIconMode="password_toggle"
|
||||
app:layout_constraintTop_toBottomOf="@id/tilOldPassword">
|
||||
|
||||
@@ -58,10 +58,10 @@
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnChangePassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_height="50dp"
|
||||
android:layout_marginTop="32dp"
|
||||
android:padding="12dp"
|
||||
android:text="Change Password"
|
||||
android:text="@string/change_password"
|
||||
app:layout_constraintTop_toBottomOf="@id/tilNewPassword" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
@@ -70,7 +70,7 @@
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:text="Forgot Password?"
|
||||
android:text="@string/forgot_password"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/btnChangePassword" />
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".activities.DocumentsActivity">
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,60 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
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">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:visibility="gone"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="8dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/loading"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="visible"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical">
|
||||
<ProgressBar
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/noItems"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:gravity="center"
|
||||
android:visibility="gone"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:id="@+id/noItemsTxt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textSize="16sp"
|
||||
android:gravity="center"
|
||||
android:padding="25dp"
|
||||
android:text="@string/no_items_available_add_one_by_clicking_on_the_plus_button"/>
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/fabAdd"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:layout_marginBottom="35dp"
|
||||
android:contentDescription="Compose"
|
||||
app:iconPadding="8dp"
|
||||
app:icon="@android:drawable/ic_input_add"
|
||||
android:text="Add file"/>
|
||||
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
@@ -1,25 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
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">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="8dp" />
|
||||
|
||||
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/fabAdd"
|
||||
android:layout_width="60dp"
|
||||
android:layout_height="60dp"
|
||||
android:layout_gravity="bottom|end"
|
||||
android:layout_marginEnd="25dp"
|
||||
android:layout_marginBottom="35dp"
|
||||
android:src="@android:drawable/ic_input_add" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
145
app/src/main/res/layout/activity_hidden.xml
Normal file
@@ -0,0 +1,145 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".activities.HiddenActivity">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:id="@+id/toolBar"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_back"
|
||||
app:tint="@color/svgTintColor"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="#00000000"
|
||||
android:padding="8dp"
|
||||
android:id="@+id/back"/>
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/hidden_space"
|
||||
android:textSize="22sp"
|
||||
android:singleLine="true"
|
||||
android:padding="4dp"
|
||||
android:textStyle="bold"
|
||||
android:layout_weight="1"
|
||||
android:id="@+id/folderName"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_edit"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:id="@+id/edit"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/delete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_delete"
|
||||
style="@style/Widget.Material3.Button.IconButton" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/menuButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_more"
|
||||
style="@style/Widget.Material3.Button.IconButton"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_list"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:id="@+id/folderOrientation"/>
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
app:icon="@drawable/ic_settings"
|
||||
style="@style/Widget.Material3.Button.IconButton"
|
||||
android:id="@+id/settings"/>
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/toolBar">
|
||||
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/noItems"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<ImageView
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:src="@drawable/ic_no_items" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/noItemsTxt"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:gravity="center"
|
||||
android:padding="10dp"
|
||||
android:text="@string/there_is_no_folders_available_create_one_by_clicking_on_the_add_folder_button_showing_in_the_bottom"
|
||||
android:textSize="16sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/addFolder"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_folder_add"
|
||||
android:text="@string/add_image"
|
||||
android:backgroundTint="?attr/colorPrimary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginEnd="16dp"
|
||||
app:tint="@color/white"
|
||||
android:layout_marginBottom="20dp"
|
||||
app:fabCustomSize="57dp" />
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/deleteSelected"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:src="@drawable/ic_delete"
|
||||
android:visibility="gone"
|
||||
app:tint="@color/white"
|
||||
android:backgroundTint="?attr/colorPrimary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginBottom="20dp"
|
||||
app:fabCustomSize="57dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -1,121 +0,0 @@
|
||||
<?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"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/linearLayout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="2"
|
||||
android:minHeight="300dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_horizontal|bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/btnImages"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="150dp"
|
||||
android:layout_margin="8dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/btnAudio"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnVideos"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/linearLayout">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="Images"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/btnVideos"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="150dp"
|
||||
android:layout_margin="8dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/btnDocs"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/btnImages"
|
||||
app:layout_constraintTop_toBottomOf="@+id/linearLayout">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="Videos"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:gravity="center_horizontal|top"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/btnAudio"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="150dp"
|
||||
android:layout_margin="8dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/btnDocs"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/btnImages">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="Audio"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:id="@+id/btnDocs"
|
||||
android:layout_width="150dp"
|
||||
android:layout_height="150dp"
|
||||
android:layout_margin="8dp"
|
||||
app:cardCornerRadius="12dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/btnAudio"
|
||||
app:layout_constraintTop_toBottomOf="@id/btnVideos">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:text="Documents"
|
||||
android:textSize="18sp"
|
||||
android:textStyle="bold" />
|
||||
</com.google.android.material.card.MaterialCardView>
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
@@ -1,10 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".activities.ImageGalleryActivity">
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -7,14 +7,14 @@
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".activities.MainActivity">
|
||||
|
||||
<!-- Calculator Display -->
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/displayContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_margin="0dp"
|
||||
android:background="@drawable/bottom_corner"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintHeight_percent="0.3"
|
||||
app:layout_constraintHeight_percent="0.4"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:scrollbars="none"
|
||||
android:paddingTop="27dp"
|
||||
android:gravity="end|bottom"
|
||||
app:layout_constraintBottom_toTopOf="@+id/total"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@@ -41,33 +42,32 @@
|
||||
android:id="@+id/display"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoSizeMaxTextSize="48sp"
|
||||
android:autoSizeMaxTextSize="70sp"
|
||||
android:autoSizeMinTextSize="16sp"
|
||||
android:autoSizeStepGranularity="2sp"
|
||||
android:gravity="end|bottom"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:padding="10dp"
|
||||
android:text="0"
|
||||
android:textSize="48sp"
|
||||
android:text=""
|
||||
android:textSize="70sp"
|
||||
tools:ignore="Suspicious0dp" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/total"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:autoSizeMaxTextSize="26sp"
|
||||
android:autoSizeMinTextSize="24sp"
|
||||
android:autoSizeMaxTextSize="40sp"
|
||||
android:autoSizeMinTextSize="12sp"
|
||||
android:autoSizeStepGranularity="2sp"
|
||||
android:autoSizeTextType="uniform"
|
||||
android:gravity="end|bottom"
|
||||
android:paddingRight="10dp"
|
||||
android:paddingBottom="10dp"
|
||||
android:text=""
|
||||
android:textSize="26sp"
|
||||
android:textSize="40sp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
@@ -75,255 +75,386 @@
|
||||
tools:ignore="Suspicious0dp" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<!-- Calculator Buttons -->
|
||||
<androidx.gridlayout.widget.GridLayout
|
||||
android:id="@+id/buttonGrid"
|
||||
<LinearLayout
|
||||
android:id="@+id/buttonContainer"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="8dp"
|
||||
app:columnCount="4"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/displayContainer"
|
||||
app:rowCount="5">
|
||||
app:layout_constraintTop_toBottomOf="@id/displayContainer">
|
||||
|
||||
<!-- Row 1 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnClear"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="C"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnPercent"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnClear"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:backgroundTint="?attr/colorSecondaryContainer"
|
||||
android:text="C"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnPercent"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnPercent"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:backgroundTint="?attr/colorSecondaryContainer"
|
||||
android:text="%"
|
||||
android:textSize="30sp"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnDivide"
|
||||
app:layout_constraintStart_toEndOf="@+id/btnClear"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnDivide"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:backgroundTint="?attr/colorSecondaryContainer"
|
||||
android:text="÷"
|
||||
android:textSize="30sp"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/cut"
|
||||
app:layout_constraintStart_toEndOf="@+id/btnPercent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/cut"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:background="@drawable/gradient_bg"
|
||||
android:scaleType="centerInside"
|
||||
android:src="@drawable/backspace"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/btnDivide"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="%"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnDivide"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn7"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:text="7"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn8"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn8"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:text="8"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn9"
|
||||
app:layout_constraintStart_toEndOf="@+id/btn7"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn9"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:text="9"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnMultiply"
|
||||
app:layout_constraintStart_toEndOf="@+id/btn8"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnMultiply"
|
||||
style="@style/CustomMaterialButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:text="×"
|
||||
android:textColor="@color/white"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/btn9"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="÷"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/cut"
|
||||
android:layout_width="0dp"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn4"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:layout_weight="1"
|
||||
android:text="4"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn5"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn5"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:layout_weight="1"
|
||||
android:text="5"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn6"
|
||||
app:layout_constraintStart_toEndOf="@+id/btn4"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn6"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:layout_weight="1"
|
||||
android:text="6"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnMinus"
|
||||
app:layout_constraintStart_toEndOf="@+id/btn5"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnMinus"
|
||||
style="@style/CustomMaterialButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:layout_weight="1"
|
||||
android:textColor="@color/white"
|
||||
android:text="-"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/btn6"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:gravity="center"
|
||||
android:textAlignment="center"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:icon="@drawable/backspace"
|
||||
app:iconSize="32dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<!-- Row 2 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn7"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn1"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:text="1"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn2"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn2"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:text="2"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn3"
|
||||
app:layout_constraintStart_toEndOf="@+id/btn1"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn3"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:text="3"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnPlus"
|
||||
app:layout_constraintStart_toEndOf="@+id/btn2"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnPlus"
|
||||
style="@style/CustomMaterialButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:text="+"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/btn3"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="7"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
android:layout_weight="1"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn8"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="8"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn00"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:text="00"
|
||||
android:layout_weight="1"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btn0"
|
||||
app:layout_constraintHorizontal_chainStyle="spread"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn9"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="9"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
<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_weight="1"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:text="0"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnDot"
|
||||
app:layout_constraintStart_toEndOf="@+id/btn00"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnMultiply"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="×"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnDot"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:text="."
|
||||
android:layout_weight="1"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1.15"
|
||||
app:layout_constraintEnd_toStartOf="@+id/btnEquals"
|
||||
app:layout_constraintStart_toEndOf="@+id/btn0"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<!-- Row 3 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn4"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="4"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnEquals"
|
||||
style="@style/CustomMaterialButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1"
|
||||
android:layout_marginHorizontal="3dp"
|
||||
android:text="="
|
||||
android:textColor="@color/white"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="1:1"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@+id/btnDot"
|
||||
app:layout_constraintTop_toTopOf="parent"/>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn5"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="5"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn6"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="6"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnMinus"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="-"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
|
||||
<!-- Row 4 -->
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn1"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="1"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn2"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="2"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btn3"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="3"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnPlus"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="+"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
|
||||
<!-- 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_margin="4dp"
|
||||
android:text="0"
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnSpan="2"
|
||||
app:layout_columnWeight="2"
|
||||
app:layout_rowWeight="1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnDot"
|
||||
style="@style/Widget.MaterialComponents.Button.OutlinedButton"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="."
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnEquals"
|
||||
style="@style/Widget.MaterialComponents.Button"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="4dp"
|
||||
android:text="="
|
||||
android:textSize="30sp"
|
||||
app:cornerRadius="15dp"
|
||||
app:layout_columnWeight="1"
|
||||
app:layout_rowWeight="1" />
|
||||
|
||||
</androidx.gridlayout.widget.GridLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
@@ -15,13 +15,21 @@
|
||||
android:paddingLeft="15dp"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" >
|
||||
<androidx.appcompat.widget.AppCompatImageButton
|
||||
android:layout_width="40dp"
|
||||
android:layout_height="40dp"
|
||||
android:src="@drawable/ic_back"
|
||||
android:scaleType="fitCenter"
|
||||
android:background="#00000000"
|
||||
android:padding="8dp"
|
||||
android:id="@+id/back"/>
|
||||
<TextView
|
||||
android:id="@+id/title"
|
||||
android:layout_width="wrap_content"
|
||||
android:textSize="22sp"
|
||||
android:layout_height="wrap_content"
|
||||
android:textStyle="bold"
|
||||
android:text="Preview File"/>
|
||||
android:text="@string/preview_file"/>
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.viewpager2.widget.ViewPager2
|
||||
@@ -46,19 +54,22 @@
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/unHide"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="6dp"
|
||||
android:minWidth="120dp"
|
||||
android:text="Unhide" />
|
||||
android:layout_weight="1"
|
||||
style="@style/Widget.Material3.Button.OutlinedButton"
|
||||
android:text="@string/un_hide" />
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/delete"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:layout_margin="6dp"
|
||||
android:minWidth="120dp"
|
||||
android:text="Delete" />
|
||||
android:text="@string/delete" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||