51 Commits
1.0 ... 1.4.0

Author SHA1 Message Date
Binondi
cf192b6ab9 Added - file encryption, custom key creation. 2025-06-06 16:09:09 +05:30
Binondi
191368bdf8 Added - file encryption, custom key creation. 2025-06-06 15:16:21 +05:30
Binondi
5aafc7e411 Added - file encryption using room db. 2025-06-05 21:34:23 +05:30
Binondi
1f81cccd96 Added - file encryption using room db. 2025-06-05 21:27:28 +05:30
Binondi
eea0f56379 Added - file encryption using room db. 2025-06-05 21:22:26 +05:30
Binondi
e90e3c438f Added - relatable dialog text. 2025-06-04 12:22:43 +05:30
Binondi
480a90b64c Added - relatable dialog text. 2025-06-04 12:20:51 +05:30
Binondi
90a252f227 Added - relatable dialog text. 2025-06-04 12:12:40 +05:30
Binondi
08cdd747ca Added - relatable dialog text. 2025-06-04 12:10:33 +05:30
Binondi
2d10c2f941 Added - relatable dialog text. 2025-06-04 12:03:48 +05:30
Binondi
d840732305 Added - relatable dialog text. 2025-06-04 12:02:24 +05:30
Binondi
7da5885931 Fixed - icon color. 2025-06-04 11:51:01 +05:30
Binondi
082dbf17aa Added - padding in the document icon. 2025-06-04 11:50:29 +05:30
Binondi
093c419f34 Fixed - icon color 2025-06-04 11:49:25 +05:30
Binondi
8649ec9ac2 Merge remote-tracking branch 'origin/master' 2025-06-04 11:39:57 +05:30
Binondi
527f377ebd Fixed resource mipmap/ic_launcher_monochrome not found problem 2025-06-04 11:38:45 +05:30
Binondi Borthakur
e1627c426f Add files via upload 2025-06-03 20:44:28 +05:30
Binondi Borthakur
092219d487 Add files via upload 2025-06-03 20:43:34 +05:30
Binondi Borthakur
7803ed2589 Add files via upload 2025-06-03 20:40:43 +05:30
Binondi
e40b59c460 2025-06-03 20:35:46 +05:30
Binondi
c25ac613c2 Merge remote-tracking branch 'origin/master' 2025-06-03 20:31:13 +05:30
Binondi
bfc339c101 Changes For Folder Feature 2025-06-03 20:29:39 +05:30
Binondi Borthakur
657b674ff3 Update README.md 2025-06-03 16:38:25 +05:30
Binondi Borthakur
ea7c6ea18a Add files via upload 2025-06-03 16:32:56 +05:30
Binondi Borthakur
c97f38ae53 Add files via upload 2025-06-03 16:30:21 +05:30
Binondi Borthakur
0ba60f2bae Update README.md 2025-06-03 15:59:23 +05:30
Binondi
970cde737a Changes For Folder Feature 2025-06-03 15:25:24 +05:30
Binondi
eb7f61fbc8 Changes For Folder Feature 2025-06-03 15:09:19 +05:30
Binondi
4069ddc200 Changes For Folder Feature 2025-06-03 00:52:41 +05:30
Binondi
88dcc844c8 Changes For Folder Feature 2025-06-02 12:44:48 +05:30
Binondi
f8575da2a9 Changes For Folder Feature 2025-06-01 21:37:13 +05:30
Binondi
ab737511a7 Changes For Folder Feature 2025-06-01 16:47:45 +05:30
Binondi
48c4e04a28 Bug Fixes, Added Empty item message. 2025-04-14 21:10:51 +05:30
Binondi
713fd3540f Bug Fixes, Added Empty item message. 2025-04-14 12:44:06 +05:30
Binondi
2e10cc5d11 Bug Fixes, Added Empty item message. 2025-04-14 12:15:14 +05:30
Binondi
f06b6d442f Merge remote-tracking branch 'origin/master' 2025-04-14 12:07:22 +05:30
Binondi
4fd8832d90 Bug Fixes, Added Empty item message. 2025-04-14 12:06:56 +05:30
Binondi Borthakur
61e3d49670 Update README.md 2025-04-12 01:59:08 +05:30
Binondi Borthakur
8610ad9a03 Update README.md 2025-04-09 01:12:15 +05:30
Binondi
83f9bcdf88 Bug Fix, Added Some More Calculation Features in Calculator Part. 2025-04-04 15:23:27 +05:30
Binondi Borthakur
0d7ea6174d Update issue templates 2025-04-03 19:55:06 +05:30
Binondi Borthakur
5aab372920 Update README.md 2025-04-03 19:44:07 +05:30
Binondi Borthakur
32e5bfa36d Update README.md 2025-04-03 19:39:38 +05:30
Binondi Borthakur
3a90b17301 Update README.md 2025-04-03 19:30:44 +05:30
Binondi Borthakur
b706f679a9 Update README.md 2025-04-03 19:23:12 +05:30
Binondi
6f4cf5674e Bug Fix, Added Some More Calculation Features in Calculator Part. 2025-04-03 19:02:30 +05:30
Binondi
3ffba02332 Merge remote-tracking branch 'origin/master'
# Conflicts:
#	app/src/main/java/devs/org/calculator/activities/MainActivity.kt
2025-04-03 18:51:18 +05:30
Binondi
937791eb5c Bug Fix, Added Some More Calculation Features in Calculator Part. 2025-04-03 18:50:12 +05:30
Binondi
568c0044a4 Bug Fix, Added Some More Calculation Features in Calculator Part. 2025-04-03 18:41:16 +05:30
Binondi Borthakur
070fe0a620 2024-12-16 21:45:56 +05:30
Binondi Borthakur
38d78a8e6a Update README.md 2024-12-16 21:42:35 +05:30
127 changed files with 6691 additions and 1860 deletions

38
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,38 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Browser [e.g. chrome, safari]
- Version [e.g. 22]
**Smartphone (please complete the following information):**
- Device: [e.g. iPhone6]
- OS: [e.g. iOS8.1]
- Browser [e.g. stock browser, safari]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

View File

@@ -0,0 +1,11 @@
---
name: Give Me Some Idea For This App
about: If your idea is unique i will definitely implement the Idea and mention you
in my repo and add the feature to the app
title: ''
labels: ''
assignees: ''
---

6
.idea/AndroidProjectSystem.xml generated Normal file
View 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>

View File

@@ -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
View File

@@ -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>

180
README.md
View File

@@ -1,15 +1,14 @@
<div align="center">
<img src="app/src/main/assets/logo.png" alt="Calculator Hide File App Logo" width="200" />
# Calculator Hide File App
# 📂 Calculator Hide File App for Android 📂
<a href="https://github.com/Binondi/Calculator-Hide-Files/releases/latest">
<img alt="Latest release" src="https://img.shields.io/badge/Releases-v1.0-blue?logo=github&style=for-the-badge">
</a>
<a href="https://github.com/Binondi/Calculator-Hide-Files/releases/latest">
<img alt="Downloads" src="https://img.shields.io/badge/Downloads-1.2k-blue?logo=github&style=for-the-badge">
<img alt="Downloads" src="https://img.shields.io/badge/Downloads-1.3k-blue?logo=github&style=for-the-badge">
</a>
<a href="LICENSE">
@@ -19,132 +18,151 @@
</div>
---
Welcome to the **Calculator Hide File App**! This app is a unique and secure way to hide your sensitive files under the disguise of a fully functional calculator.
## 😍 Why Choose This App?
The **Calculator Hide File App** is an **open-source** application, allowing you to inspect the code yourself. This ensures **complete transparency** and guarantees that your **privacy remains uncompromised**. 🔒✅
---
## Features
## 🔥 About Calculator Hide File App
- **Dual Functionality**: Operates as a regular calculator and a file hiding app.
- **User-Friendly Interface**: Simple and intuitive design for easy use.
- **Secure File Storage**: Protects your hidden files with a passcode.
- **Passcode Protection**: Access the hidden files by entering a secret passcode in the calculator.
- **File Management**: Easily hide, unhide, and manage files within the app.
The **Calculator Hide File App** is an innovative **Android file-hiding app** that disguises itself as a **fully functional calculator**. It helps you **securely store** private files and protect them with a **hidden passcode**.
> **⭐ Why Choose This App?**
> - Hide images, videos, documents & other files securely.
> - Works like a **real calculator** with hidden storage mode.
> - No one will suspect its a file vault!
---
## Screenshots
<div>
<img src="app/src/main/assets/Screenshot_1.jpg" alt="Screenshot 1" width="24%">
<img src="app/src/main/assets/Screenshot_2.jpg" alt="Screenshot 2" width="24%">
<img src="app/src/main/assets/Screenshot_3.jpg" alt="Screenshot 3" width="24%">
<img src="app/src/main/assets/Screenshot_4.jpg" alt="Screenshot 4" width="24%">
## 🚀 Features
</div>
<div>
<img src="app/src/main/assets/Screenshot_5.jpg" alt="Screenshot 5" width="24%">
<img src="app/src/main/assets/Screenshot_6.jpg" alt="Screenshot 6" width="24%">
<img src="app/src/main/assets/Screenshot_7.jpg" alt="Screenshot 7" width="24%">
<img src="app/src/main/assets/Screenshot_8.jpg" alt="Screenshot 8" width="24%">
**Dual Functionality** A working **calculator** & a **file vault** in one app.
**Secret Passcode** Unlock hidden files by entering a secret code.
**Secure File Manager** Hide/unhide files easily.
**Fast & Lightweight** Smooth performance on all Android devices.
**No Root Required** Works without rooting your phone.
---
## 🖼️ Screenshots
<div align="center">
<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>
---
## How It Works
<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>
1. **Calculator Mode**:
- Perform basic arithmetic operations just like any regular calculator.
2. **Setup Password**:
- Enter `123456=` to setup your password.
3. **Hidden Mode**:
- Enter your secret passcode and hit the `=` button to unlock the hidden file manager.
5. **File Management**:
- Add files to hide them securely.
- Retrieve or unhide files as needed.
<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>
---
## ☕ Support
## 🔑 How It Works
Support My development by donating money. Thank you very much for your help! ❤️
1. **Use as a Regular Calculator**
- Perform standard arithmetic operations like a normal calculator.
[<img src="https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA"
alt="Sponsor the project on GitHub"
height="40">](https://github.com/sponsors/Binondi/Calculator-Hide-Files) [<img src="https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white"
alt="Donate with PayPal"
height="40">](https://www.paypal.me/BinondiBorthakur56) [<img src="https://img.shields.io/badge/Buy%20Me%20a%20Coffee-ffdd00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black"
alt="Donate with buymeacoffee"
height="40">](https://buymeacoffee.com/binondi)
2. **Enter Secret Passcode**
- Type `123456` and press `=` to set up your password.
- Enter your **custom passcode** and hit `=` to unlock the hidden file manager.
---
## Installation
### Prerequisites
- Android Studio (for development)
- A device or emulator running Android 6.0 or higher
### Steps
1. Clone the repository:
```bash
git clone https://github.com/YourUsername/CalculatorHideFileApp.git
```
2. Open the project in Android Studio.
3. Build and run the app on your device or emulator.
3. **Manage Hidden Files**
- Add, remove, and restore hidden files.
- Files stay protected even after closing the app.
- Create Hidden Folders.
- Move & Copy Hidden Files Between Hidden Folders.
---
## Technologies Used
## 📥 Download & Installation
- **Programming Language**: Java/Kotlin
- **Development Platform**: Android Studio
- **UI Framework**: Android XML layouts
- **File Storage**: Secure internal storage and MediaStore API
### 🔗 **[Download the Latest Version Here](https://github.com/Binondi/Calculator-Hide-Files/releases/latest)**
### 🔹 Prerequisites
- **Android 6.0 or higher**
- **Storage permissions enabled**
- **Need Manage All Files Permissons For Android 11 and Higher Versions.**
### 🔹 Installation Steps
```bash
git clone https://github.com/Binondi/Calculator-Hide-Files.git
```
- Open **Android Studio** and build the project.
- Install the APK on your **Android device or emulator**.
---
## Usage Instructions
## 🛠️ Technologies Used
1. Open the app.
2. Use the calculator as normal.
3. Enter the secret passcode and press `=` to switch to the hidden file manager.
4. Add or manage your hidden files.
- **Programming Language**: Kotlin
- **UI Framework**: XML (For UI)
- **File Storage**: Secure internal storage & MediaStore API
---
## Contributing
## 🎯 Why Use Calculator Hide File App?
- **No One Can Detect Your Files** Works like a real calculator.
- **100% Secure** Your private files stay hidden, even if someone opens the app.
- **Completely Free & Open Source** Modify or contribute to the project.
---
## ❤️ Support the Project
If you find this app useful, please consider supporting the development. 🙏
[![Sponsor on GitHub](https://img.shields.io/badge/sponsor-30363D?style=for-the-badge&logo=GitHub-Sponsors&logoColor=#EA4AAA)](https://github.com/sponsors/Binondi)
[![Donate via PayPal](https://img.shields.io/badge/PayPal-00457C?style=for-the-badge&logo=paypal&logoColor=white)](https://paypal.me/BinondiBorthakur56)
---
## 🔧 Contributing
We welcome contributions! To contribute:
1. Fork the repository.
2. Create a new branch for your feature or bugfix:
1. **Fork the repository**
2. **Create a new branch**
```bash
git checkout -b feature-name
```
3. Commit your changes:
3. **Commit your changes**
```bash
git commit -m "Add a new feature"
```
4. Push to the branch:
4. **Push to GitHub**
```bash
git push origin feature-name
```
5. Open a Pull Request.
5. **Create a Pull Request**
---
## License
## 📜 License
This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details.
This project is licensed under the **Apache License 2.0**.
See the full license [here](LICENSE).
---
## Contact
## 📧 Contact
For any inquiries or feedback, reach out to:
- **Email**: binondiborthakur56@gmail.com
- **GitHub**: [Binondi](https://github.com/Binondi)
For any questions or feedback:
📩 **Email**: binondiborthakur56@gmail.com
🐙 **GitHub**: [Binondi](https://github.com/Binondi)
---
Thank you for using the **Calculator Hide File App**! We hope you find it secure and easy to use.
### 🎉 Thank You for Using Calculator Hide File App! 🎉
🚀 **Keep your files secure and hidden!** 🚀

View File

@@ -1,6 +1,7 @@
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
id("kotlin-kapt")
}
android {
@@ -11,21 +12,28 @@ android {
applicationId = "devs.org.calculator"
minSdk = 26
targetSdk = 34
versionCode = 1
versionName = "1.0"
versionCode = 5
versionName = "1.4.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
debug {
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
@@ -36,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 {
@@ -45,15 +68,22 @@ dependencies {
implementation(libs.material)
implementation(libs.androidx.activity)
implementation(libs.androidx.constraintlayout)
implementation(libs.androidx.gridlayout)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
//custom dependencies
implementation(libs.exp4j)
implementation("com.github.bumptech.glide:glide:4.16.0")
implementation("androidx.documentfile:documentfile:1.0.1")
implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("androidx.viewpager:viewpager:1.0.0")
implementation("com.jsibbold:zoomage:1.3.1")
implementation(libs.glide)
implementation(libs.androidx.documentfile)
implementation(libs.photoview)
implementation(libs.androidx.viewpager)
implementation(libs.zoomage)
implementation(libs.lottie)
// Room dependencies
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.room.ktx)
kapt(libs.androidx.room.compiler)
}

View File

@@ -1,21 +1,70 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Keep your MainActivity and Application class
-keep public class devs.org.calculator.activities.MainActivity
-keep public class devs.org.calculator.activities.SetupPasswordActivity
-keep public class devs.org.calculator.activities.HiddenVaultActivity
-keep public class devs.org.calculator.** { *; }
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Keep exp4j library since it's used for expression evaluation
-keep class net.objecthunter.exp4j.** { *; }
-dontwarn net.objecthunter.exp4j.**
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# Keep Google Material components
-keep class com.google.android.material.** { *; }
-dontwarn com.google.android.material.**
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# Keep Android X components
-keep class androidx.** { *; }
-keep interface androidx.** { *; }
# Keep any classes with ViewBinding
-keep class devs.org.calculator.databinding.** { *; }
# Keep any callback interfaces
-keep class devs.org.calculator.callbacks.** { *; }
-keep interface devs.org.calculator.callbacks.** { *; }
# Keep classes used for regex pattern matching
-keep class java.util.regex.** { *; }
# Keep annotation classes
-keepattributes *Annotation*
-keepattributes Signature
-keepattributes SourceFile,LineNumberTable
# Keep Parcelable classes (might be needed for Intent extras)
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# Keep FileManager classes since they work with storage permissions
-keep class devs.org.calculator.utils.FileManager { *; }
# Keep DialogUtil since it's used for permission dialogs
-keep class devs.org.calculator.utils.DialogUtil { *; }
# Keep PrefsUtil since it's used for password validation
-keep class devs.org.calculator.utils.PrefsUtil { *; }
# General Android rules
-keepclassmembers class * extends android.app.Activity {
public void *(android.view.View);
}
# Keep any classes that use reflection
-keepattributes InnerClasses
# Keep R classes and their fields
-keepclassmembers class **.R$* {
public static <fields>;
}
# Keep enums
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# Keep specific activities with special code in onCreate
-keepclassmembers class * extends android.app.Activity {
public void onCreate(android.os.Bundle);
}

View 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
}

View File

@@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 159 KiB

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 469 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 557 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 228 KiB

File diff suppressed because one or more lines are too long

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

View File

@@ -1,85 +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.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, "No files selected", Toast.LENGTH_SHORT).show()
}
}
}
}
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
Toast.makeText(
this@AudioGalleryActivity,
"${copiedFiles.size} Audios hidden 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() {
// Implement audio preview
}
}

View File

@@ -1,139 +0,0 @@
package devs.org.calculator.activities
import android.Manifest
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 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.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
protected lateinit var fileManager: FileManager
protected lateinit var adapter: FileAdapter
protected 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
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupIntentSenderLauncher()
binding = ActivityGalleryBinding.inflate(layoutInflater)
setContentView(binding.root)
fileManager = FileManager(this, this)
binding.fabAdd.text = when(fileType){
FileManager.FileType.IMAGE -> {
"Add Image"
}
FileManager.FileType.AUDIO -> {
"Add Audio"
}
FileManager.FileType.VIDEO -> {
"Add Video"
}
FileManager.FileType.DOCUMENT -> {
"Add Files"
}
}
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)
}
override fun onResume() {
super.onResume()
loadFiles()
}
abstract fun openPreview()
private fun showPermissionDeniedDialog() {
// permission denied
}
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()
}
}
}
}

View File

@@ -1,75 +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.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>
private var selectedUri: Uri? = null
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, "No files selected", Toast.LENGTH_SHORT).show()
}
}
}
}
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
Toast.makeText(this@DocumentsActivity, "${copiedFiles.size} Documents hidden successfully", Toast.LENGTH_SHORT).show()
loadFiles()
}
override fun onFileProcessFailed() {
Toast.makeText(this@DocumentsActivity, "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() {
// Implement document preview
}
}

View File

@@ -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()
}
}
}
}

View File

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

View File

@@ -1,155 +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.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, "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, "No files selected", Toast.LENGTH_SHORT).show()
}
}
}
askPermissiom()
}
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
Toast.makeText(this@ImageGalleryActivity, "${copiedFiles.size} Images hidden successfully", Toast.LENGTH_SHORT).show()
loadFiles()
}
override fun onFileProcessFailed() {
Toast.makeText(this@ImageGalleryActivity, "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, "Storage permissions granted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "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)
}
}

View File

@@ -1,5 +1,6 @@
package devs.org.calculator.activities
import android.annotation.SuppressLint
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
@@ -8,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
@@ -20,40 +22,64 @@ import devs.org.calculator.utils.DialogUtil
import devs.org.calculator.utils.FileManager
import devs.org.calculator.utils.PrefsUtil
import net.objecthunter.exp4j.ExpressionBuilder
import java.util.regex.Pattern
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
private var hasDecimal = false
private var lastWasPercent = false
private lateinit var launcher: ActivityResultLauncher<Intent>
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")
@@ -71,10 +97,11 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
binding.btnClear.setOnClickListener { clearDisplay() }
binding.btnDot.setOnClickListener { addDecimal() }
binding.btnEquals.setOnClickListener { calculateResult() }
binding.btnPercent.setOnClickListener { calculatePercentage() }
binding.btnPercent.setOnClickListener { addPercentage() }
binding.cut.setOnClickListener { cutNumbers() }
}
private fun handleActivityResult(result: androidx.activity.result.ActivityResult) {
if (result.resultCode == RESULT_OK) {
result.data?.data?.let { uri ->
@@ -83,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")
}
}
@@ -98,54 +123,167 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
currentExpression += number
}
lastWasOperator = false
lastWasPercent = false
updateDisplay()
}
}
private fun setupOperatorButton(button: MaterialButton, operator: String) {
button.setOnClickListener {
if (!lastWasOperator) {
if (lastWasOperator) {
currentExpression = currentExpression.substring(0, currentExpression.length - 1) +
when (operator) {
"×" -> "*"
else -> operator
}
} else if (!lastWasPercent) {
currentExpression += when (operator) {
"×" -> "*"
else -> operator
}
lastWasOperator = true
lastWasPercent = false
hasDecimal = false
}
updateDisplay()
}
}
private fun clearDisplay() {
currentExpression = "0"
currentExpression = ""
binding.total.text = ""
lastWasOperator = false
lastWasPercent = false
hasDecimal = false
updateDisplay()
}
private fun addDecimal() {
if (!hasDecimal && !lastWasOperator) {
if (!hasDecimal && !lastWasOperator && !lastWasPercent) {
currentExpression += "."
hasDecimal = true
updateDisplay()
}
}
private fun calculatePercentage() {
try {
val value = currentExpression.toDouble()
currentExpression = (value / 100).toString()
private fun addPercentage() {
if (!lastWasOperator && !lastWasPercent) {
currentExpression += "%"
lastWasPercent = true
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
val matcher = percentagePattern.matcher(processedExpression)
while (matcher.find()) {
val fullMatch = matcher.group(0)
val number = matcher.group(1)
val start = matcher.start()
if (start == 0 || !isOperator(processedExpression[start-1].toString())) {
val percentageValue = number!!.toDouble() / 100
processedExpression = processedExpression.replace(fullMatch!!.toString(), percentageValue.toString())
}
}
val opMatcher = operatorPercentPattern.matcher(processedExpression)
val sb = StringBuilder(processedExpression)
val matches = mutableListOf<Triple<Int, Int, String>>()
while (opMatcher.find()) {
val operator = opMatcher.group(1)
val percentValue = opMatcher.group(2)!!.toDouble()
val start = opMatcher.start()
val end = opMatcher.end()
matches.add(Triple(start, end, "$operator$percentValue%"))
}
for (match in matches.reversed()) {
val (start, end, fullMatch) = match
var leftNumberStart = start - 1
if (leftNumberStart >= 0 && sb[leftNumberStart] == ')') {
var openParens = 1
leftNumberStart--
while (leftNumberStart >= 0 && openParens > 0) {
if (sb[leftNumberStart] == ')') openParens++
else if (sb[leftNumberStart] == '(') openParens--
leftNumberStart--
}
if (leftNumberStart >= 0) {
while (leftNumberStart >= 0 && (isDigit(sb[leftNumberStart].toString()) || sb[leftNumberStart] == '.' || sb[leftNumberStart] == '-')) {
leftNumberStart--
}
leftNumberStart++
} else {
leftNumberStart = 0
}
} else {
while (leftNumberStart >= 0 && (isDigit(sb[leftNumberStart].toString()) || sb[leftNumberStart] == '.')) {
leftNumberStart--
}
leftNumberStart++
}
if (leftNumberStart < start) {
val leftPart = sb.substring(leftNumberStart, start)
try {
val baseNumber = evaluateExpression(leftPart)
val operator = fullMatch.substring(0, 1)
val percentNumber = fullMatch.substring(1, fullMatch.length - 1).toDouble()
val percentValue = baseNumber * (percentNumber / 100)
val newValue = when (operator) {
"+" -> baseNumber + percentValue
"-" -> baseNumber - percentValue
"*" -> baseNumber * (percentNumber / 100)
"/" -> baseNumber / (percentNumber / 100)
else -> baseNumber
}
sb.replace(leftNumberStart, end, newValue.toString())
} catch (e: Exception) {
Log.e("Calculator", "Error processing percentage expression: $e")
}
}
}
return sb.toString()
}
private fun isOperator(char: String): Boolean {
return char == "+" || char == "-" || char == "*" || char == "/"
}
private fun isDigit(char: String): Boolean {
return char.matches(Regex("[0-9]"))
}
private fun evaluateExpression(expression: String): Double {
return try {
ExpressionBuilder(expression).build().evaluate()
} catch (_: 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()
@@ -153,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()
@@ -161,8 +299,13 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
}
try {
currentExpression = currentExpression.replace("×", "*")
val expression = ExpressionBuilder(currentExpression).build()
var processedExpression = currentExpression.replace("×", "*")
if (processedExpression.contains("%")) {
processedExpression = preprocessExpression(processedExpression)
}
val expression = ExpressionBuilder(processedExpression).build()
val result = expression.evaluate()
currentExpression = if (result.toLong().toDouble() == result) {
@@ -172,16 +315,17 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
}
lastWasOperator = false
lastWasPercent = false
hasDecimal = currentExpression.contains(".")
updateDisplay()
binding.total.text = ""
} catch (e: Exception) {
binding.display.text = getString(R.string.invalid_message)
e.printStackTrace()
}
}
@SuppressLint("DefaultLocale")
private fun updateDisplay() {
binding.display.text = currentExpression.replace("*", "×")
@@ -191,30 +335,53 @@ class MainActivity : AppCompatActivity(), DialogActionsCallback {
}
try {
val expression = ExpressionBuilder(currentExpression).build()
if (currentExpression.isEmpty() ||
(isOperator(currentExpression.last().toString()) && currentExpression.last() != '%')) {
binding.total.text = ""
return
}
var processedExpression = currentExpression.replace("×", "*")
if (processedExpression.contains("%")) {
processedExpression = preprocessExpression(processedExpression)
}
val expression = ExpressionBuilder(processedExpression).build()
val result = expression.evaluate()
val formattedResult = if (result.toLong().toDouble() == result) {
result.toLong().toString()
} else {
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 = ""
}
}
private fun cutNumbers() {
if (currentExpression.isNotEmpty()){
if (currentExpression.length == 1){
currentExpression = currentExpression.substring(0, currentExpression.length - 1)
currentExpression = "0"
}else currentExpression = currentExpression.substring(0, currentExpression.length - 1)
}else currentExpression = "0"
updateDisplay()
} else {
val lastChar = currentExpression.last()
currentExpression = currentExpression.substring(0, currentExpression.length - 1)
if (lastChar == '%') {
lastWasPercent = false
} else if (isOperator(lastChar.toString())) {
lastWasOperator = false
} else if (lastChar == '.') {
hasDecimal = false
}
}
} else {
currentExpression = "0"
}
updateDisplay()
}
override fun onPositiveButtonClicked() {
@@ -222,23 +389,21 @@ 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()
}
}
}
}
}

View File

@@ -2,29 +2,41 @@ 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.viewpager.widget.ViewPager
import androidx.viewpager2.widget.ViewPager2
import devs.org.calculator.R
import devs.org.calculator.adapters.ImagePreviewAdapter
import devs.org.calculator.callbacks.DialogActionsCallback
import devs.org.calculator.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
import devs.org.calculator.R
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)
@@ -34,148 +46,301 @@ 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() {
when (type) {
"IMAGE" -> {
filetype = FileManager.FileType.IMAGE
binding.title.text = "Preview Images"
binding.title.text = getString(R.string.preview_images)
}
"VIDEO" -> {
filetype = FileManager.FileType.VIDEO
binding.title.text = "Preview Videos"
binding.title.text = getString(R.string.preview_videos)
}
"AUDIO" -> {
filetype = FileManager.FileType.AUDIO
binding.title.text = "Preview Audios"
binding.title.text = getString(R.string.preview_audios)
}
else -> {
filetype = FileManager.FileType.DOCUMENT
binding.title.text = "Preview Documents"
binding.title.text = getString(R.string.preview_documents)
}
}
}
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(
"Delete File",
"Are you sure to Delete this file permanently?",
"Delete Permanently",
"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(
"Unhide File",
"Are you sure you want to Unhide this file?",
"Unhide",
"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.isNotEmpty()) 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
}
}
}

View File

@@ -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
}
}

View File

@@ -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)
@@ -45,70 +47,71 @@ class SetupPasswordActivity : AppCompatActivity() {
val securityAnswer = binding.etSecurityAnswer.text.toString()
if (password.isEmpty()){
binding.etPassword.error = "Enter password"
binding.etPassword.error = getString(R.string.enter_password)
return@setOnClickListener
}
if (confirmPassword.isEmpty()){
binding.etConfirmPassword.error = "Confirm password"
binding.etConfirmPassword.error = getString(R.string.confirm_password)
return@setOnClickListener
}
if (securityQuestion.isEmpty()){
binding.etSecurityQuestion.error = "Enter security question"
binding.etSecurityQuestion.error = getString(R.string.enter_security_question)
return@setOnClickListener
}
if (securityAnswer.isEmpty()){
binding.etSecurityAnswer.error = "Enter security answer"
binding.etSecurityAnswer.error = getString(R.string.enter_security_answer)
return@setOnClickListener
}
if (password != confirmPassword) {
binding.etPassword.error = "Passwords don't match"
binding.etPassword.error = getString(R.string.passwords_don_t_match)
return@setOnClickListener
}
prefsUtil.savePassword(password)
prefsUtil.saveSecurityQA(securityQuestion, securityAnswer)
Toast.makeText(this, "Password set successfully", Toast.LENGTH_SHORT).show()
Toast.makeText(this, getString(R.string.password_set_successfully), Toast.LENGTH_SHORT).show()
startActivity(Intent(this, MainActivity::class.java))
finish()
}
binding.btnResetPassword.setOnClickListener {
// Implement password reset logic
// Could use security questions or email verification
if (prefsUtil.getSecurityQuestion() != null) showSecurityQuestionDialog(prefsUtil.getSecurityQuestion().toString())
else Toast.makeText(this, "Security question not set yet.", Toast.LENGTH_SHORT).show()
else Toast.makeText(this,
getString(R.string.security_question_not_set_yet), Toast.LENGTH_SHORT).show()
}
binding2.btnChangePassword.setOnClickListener{
val oldPassword = binding2.etOldPassword.text.toString()
val newPassword = binding2.etNewPassword.text.toString()
if (oldPassword.isEmpty()) {
binding2.etOldPassword.error = "This field can't be empty"
binding2.etOldPassword.error = getString(R.string.this_field_can_t_be_empty)
return@setOnClickListener
}
if (newPassword.isEmpty()) {
binding2.etNewPassword.error = "This field can't be empty"
binding2.etNewPassword.error = getString(R.string.this_field_can_t_be_empty)
return@setOnClickListener
}
if (prefsUtil.validatePassword(oldPassword)){
if (oldPassword != newPassword){
prefsUtil.savePassword(newPassword)
Toast.makeText(this, "Password reset successfully", Toast.LENGTH_SHORT).show()
Toast.makeText(this,
getString(R.string.password_reset_successfully), Toast.LENGTH_SHORT).show()
startActivity(Intent(this, MainActivity::class.java))
finish()
}else {
Toast.makeText(this, "Old Password And New Password Not Be Same", Toast.LENGTH_SHORT).show()
binding2.etNewPassword.error = "Old Password And New Password Not Be Same"
Toast.makeText(this,
getString(R.string.old_password_and_new_password_not_be_same), Toast.LENGTH_SHORT).show()
binding2.etNewPassword.error = getString(R.string.old_password_and_new_password_not_be_same)
}
}else {
Toast.makeText(this, "Wrong password entered", Toast.LENGTH_SHORT).show()
binding2.etOldPassword.error = "Old Password Not Matching"
Toast.makeText(this, getString(R.string.wrong_password_entered), Toast.LENGTH_SHORT).show()
binding2.etOldPassword.error = getString(R.string.old_password_not_matching)
}
}
binding2.btnResetPassword.setOnClickListener{
if (prefsUtil.getSecurityQuestion() != null) showSecurityQuestionDialog(prefsUtil.getSecurityQuestion().toString())
else Toast.makeText(this, "Security question not set yet.", Toast.LENGTH_SHORT).show()
else Toast.makeText(this, getString(R.string.this_field_can_t_be_empty), Toast.LENGTH_SHORT).show()
}
}
@@ -120,26 +123,28 @@ class SetupPasswordActivity : AppCompatActivity() {
MaterialAlertDialogBuilder(this)
.setTitle("Answer the Security Question!")
.setTitle(getString(R.string.answer_the_security_question))
.setView(dialogView)
.setPositiveButton("Verify") { dialog, _ ->
.setPositiveButton(getString(R.string.verify)) { dialog, _ ->
val inputEditText: TextInputEditText = dialogView.findViewById(R.id.text_input_edit_text)
val userAnswer = inputEditText.text.toString().trim()
if (userAnswer.isEmpty()) {
Toast.makeText(this, "Answer cannot be empty!", Toast.LENGTH_SHORT).show()
Toast.makeText(this,
getString(R.string.answer_cannot_be_empty), Toast.LENGTH_SHORT).show()
} else {
if (prefsUtil.validateSecurityAnswer(userAnswer)){
prefsUtil.resetPassword()
Toast.makeText(this, "Password successfully reset.", Toast.LENGTH_SHORT).show()
Toast.makeText(this,
getString(R.string.password_successfully_reset), Toast.LENGTH_SHORT).show()
dialog.dismiss()
}else {
Toast.makeText(this, "Invalid answer!", Toast.LENGTH_SHORT).show()
Toast.makeText(this, getString(R.string.invalid_answer), Toast.LENGTH_SHORT).show()
}
}
}
.setNegativeButton("Cancel") { dialog, _ ->
.setNegativeButton(getString(R.string.cancel)) { dialog, _ ->
dialog.dismiss()
}

View File

@@ -1,80 +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.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>
private var selectedUri: Uri? = null
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, "No files selected", Toast.LENGTH_SHORT).show()
}
}
}
}
override fun onFilesProcessedSuccessfully(copiedFiles: List<File>) {
Toast.makeText(this@VideoGalleryActivity, "${copiedFiles.size} Videos hidden successfully", Toast.LENGTH_SHORT).show()
loadFiles()
}
override fun onFileProcessFailed() {
Toast.makeText(this@VideoGalleryActivity, "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)
}
}

View File

@@ -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()
}
}

View File

@@ -1,196 +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 -> {
"IMAGE"
}
private var fileOperationCallback: WeakReference<FileOperationCallback>? = null
FileManager.FileType.VIDEO -> {
"VIDEO"
}
private val fileExecutor = Executors.newSingleThreadExecutor()
private val mainHandler = Handler(Looper.getMainLooper())
FileManager.FileType.AUDIO -> {
"AUDIO"
}
val hiddenFileRepository: HiddenFileRepository by lazy {
HiddenFileRepository(AppDatabase.getDatabase(context).hiddenFileDao())
}
else -> "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, "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, "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()
?: "Unknown File"
DialogUtil(context).showMaterialDialogWithNaturalButton(
"$fileTypes DETAILS",
"File Name: $fileName\n\nFile Path: $file\n\nYou can permanently delete or unhide this file.",
"Delete Permanently",
"Unhide",
"Cancel",
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()
}
}
}
}

View File

@@ -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 }
}
}

View 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
}
}
}

View File

@@ -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
}

View File

@@ -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()
}
}

View File

@@ -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
}
}
}

View File

@@ -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
}
}
}
}

View File

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

View File

@@ -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()
)

View File

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

View File

@@ -1,71 +1,57 @@
package devs.org.calculator.utils
import android.app.RecoverableSecurityException
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import android.view.View
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.IntentSenderRequest
import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.LifecycleOwner
import android.view.LayoutInflater
import android.widget.EditText
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.TextInputEditText
import devs.org.calculator.R
import devs.org.calculator.callbacks.DialogActionsCallback
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
class DialogUtil(private val context: Context) {
private lateinit var intentSenderLauncher: ActivityResultLauncher<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) { dialog, _ ->
// 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)
}
}

View File

@@ -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()
}
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
@@ -188,7 +271,7 @@ class FileManager(private val context: Context, private val lifecycleOwner: Life
withContext(Dispatchers.Main) {
Toast.makeText(
context,
"Error hiding/unhiding file: ${e.message}",
"Error hiding/un-hiding file: ${e.message}",
Toast.LENGTH_LONG
).show()
}
@@ -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")
}
}

View 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()
}
}
}

View File

@@ -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 {

View File

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

View File

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

View 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%" />

View 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%" />

View 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%" />

View 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%" />

View 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>

View File

@@ -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"/>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View 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>

View File

@@ -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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View 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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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>

View File

@@ -1,39 +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.ExtendedFloatingActionButton-->
<!-- android:id="@+id/fabAdd"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:layout_gravity="bottom|end"-->
<!-- android:layout_margin="16dp"-->
<!-- android:contentDescription="Compose"-->
<!-- android:icon="@drawable/plus"-->
<!-- android:text="Add File"-->
<!-- app:elevation="6dp"/>-->
<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>

View File

@@ -1,23 +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>

View 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>

View File

@@ -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>

Some files were not shown because too many files have changed in this diff Show More