├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml └── workflows │ ├── debug.yml │ └── release.yml ├── .gitignore ├── .idea ├── .gitignore ├── .name ├── AndroidProjectSystem.xml ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── compiler.xml ├── deploymentTargetSelector.xml ├── gradle.xml ├── kotlinc.xml ├── migrations.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── LICENSE ├── Readme.md ├── app ├── .gitignore ├── build.gradle.kts ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── nethical │ │ └── digipaws │ │ └── ExampleInstrumentedTest.kt │ ├── main │ ├── AndroidManifest.xml │ ├── java │ │ └── nethical │ │ │ └── digipaws │ │ │ ├── Constants.kt │ │ │ ├── CrashLogger.kt │ │ │ ├── Digipaws.kt │ │ │ ├── blockers │ │ │ ├── AppBlocker.kt │ │ │ ├── BaseBlocker.kt │ │ │ ├── FocusModeBlocker.kt │ │ │ ├── KeywordBlocker.kt │ │ │ └── ViewBlocker.kt │ │ │ ├── data │ │ │ └── blockers │ │ │ │ ├── KeywordPacks.kt │ │ │ │ └── PackageWand.kt │ │ │ ├── receivers │ │ │ └── AdminReceiver.kt │ │ │ ├── services │ │ │ ├── AppBlockerService.kt │ │ │ ├── BaseBlockingService.kt │ │ │ ├── GeneralFeaturesService.kt │ │ │ ├── KeywordBlockerService.kt │ │ │ ├── UsageTrackingService.kt │ │ │ └── ViewBlockerService.kt │ │ │ ├── ui │ │ │ ├── activity │ │ │ │ ├── FragmentActivity.kt │ │ │ │ ├── LauncherActivity.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── ManageKeywordsActivity.kt │ │ │ │ ├── SelectAppsActivity.kt │ │ │ │ ├── ShortcutActivity.kt │ │ │ │ ├── TimedActionActivity.kt │ │ │ │ ├── UsageMetricsActivity.kt │ │ │ │ └── WarningActivity.kt │ │ │ ├── dialogs │ │ │ │ ├── BaseDialog.kt │ │ │ │ ├── StartFocusMode.kt │ │ │ │ ├── TweakAppBlockerWarning.kt │ │ │ │ ├── TweakGrayScaleMode.kt │ │ │ │ ├── TweakKeywordBlocker.kt │ │ │ │ ├── TweakKeywordPack.kt │ │ │ │ ├── TweakUsageTracker.kt │ │ │ │ ├── TweakViewBlockerCheatHours.kt │ │ │ │ └── TweakViewBlockerWarning.kt │ │ │ ├── fragments │ │ │ │ ├── anti_uninstall │ │ │ │ │ ├── ChooseModeFragment.kt │ │ │ │ │ ├── SetupPasswordModeFragment.kt │ │ │ │ │ └── SetupTimedModeFragment.kt │ │ │ │ ├── installation │ │ │ │ │ ├── AccessibilityGuide.kt │ │ │ │ │ ├── PermissionsFragment.kt │ │ │ │ │ └── WelcomeFragment.kt │ │ │ │ └── usage │ │ │ │ │ ├── AllAppsUsageFragment.kt │ │ │ │ │ └── AppUsageBreakdown.kt │ │ │ ├── overlay │ │ │ │ └── UsageStatOverlayManager.kt │ │ │ └── widgets │ │ │ │ ├── ReelsWidgetProvider.kt │ │ │ │ └── ScreentimeWidgetProvider.kt │ │ │ ├── utils │ │ │ ├── AnimTools.kt │ │ │ ├── BackupTools.kt │ │ │ ├── CommonUtils.kt │ │ │ ├── MonochromeTools.kt │ │ │ ├── NotificationTimerManager.kt │ │ │ ├── SavedPreferencesLoader.kt │ │ │ ├── ShizukuRunner.kt │ │ │ ├── TimeTools.kt │ │ │ └── UsageStatsHelper.kt │ │ │ └── views │ │ │ ├── CustomMarkerView.kt │ │ │ └── HorizontalNumberPicker.kt │ └── res │ │ ├── anim │ │ ├── fade_in.xml │ │ └── fade_out.xml │ │ ├── drawable │ │ ├── baseline_add_24.xml │ │ ├── baseline_android_24.xml │ │ ├── baseline_app_shortcut_24.xml │ │ ├── baseline_auto_fix_high_24.xml │ │ ├── baseline_close_24.xml │ │ ├── baseline_date_range_24.xml │ │ ├── baseline_delete_24.xml │ │ ├── baseline_done_24.xml │ │ ├── baseline_edit_24.xml │ │ ├── baseline_help_outline_24.xml │ │ ├── baseline_info_24.xml │ │ ├── baseline_lock_24.xml │ │ ├── baseline_query_stats_24.xml │ │ ├── baseline_read_more_24.xml │ │ ├── baseline_refresh_24.xml │ │ ├── baseline_remove_24.xml │ │ ├── baseline_share_24.xml │ │ ├── baseline_start_24.xml │ │ ├── baseline_stop_24.xml │ │ ├── baseline_task_alt_24.xml │ │ ├── baseline_warning_24.xml │ │ ├── focus_mode_icon.jpg │ │ ├── ic_launcher_background.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_launcher_monochrome.xml │ │ └── widget_background.xml │ │ ├── layout │ │ ├── activity_add_timed_action_activity.xml │ │ ├── activity_fragment.xml │ │ ├── activity_launcher.xml │ │ ├── activity_main.xml │ │ ├── activity_manage_keywords.xml │ │ ├── activity_select_apps.xml │ │ ├── activity_usage_metrics.xml │ │ ├── app_usage_item.xml │ │ ├── cheat_hour_item.xml │ │ ├── custom_marker_view.xml │ │ ├── dialog_add_keyword.xml │ │ ├── dialog_add_timed_action.xml │ │ ├── dialog_config_tracker.xml │ │ ├── dialog_focus_mode.xml │ │ ├── dialog_grayscale.xml │ │ ├── dialog_keyword_blocker_config.xml │ │ ├── dialog_keyword_package.xml │ │ ├── dialog_permission_info.xml │ │ ├── dialog_remove_anti_uninstall.xml │ │ ├── dialog_tweak_blocker_warning.xml │ │ ├── dialog_warning_overlay.xml │ │ ├── fragment_accessibility_guide.xml │ │ ├── fragment_all_app_usage.xml │ │ ├── fragment_app_usage_breakdown.xml │ │ ├── fragment_chose_anti_uninstall_mode.xml │ │ ├── fragment_permissions.xml │ │ ├── fragment_setup_password_mode.xml │ │ ├── fragment_setup_timed_mode.xml │ │ ├── fragment_welcome.xml │ │ ├── horizontal_number_picker.xml │ │ ├── keyword_item.xml │ │ ├── launcher_item.xml │ │ ├── overlay_usage_stat.xml │ │ ├── reel_stats.xml │ │ ├── select_apps_item.xml │ │ ├── widget_app_stats.xml │ │ └── widget_reels_count.xml │ │ ├── menu │ │ ├── app_wand.xml │ │ └── usage_tracker_options.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.webp │ │ └── ic_launcher_round.webp │ │ ├── values-de │ │ └── strings.xml │ │ ├── values-fa │ │ └── strings.xml │ │ ├── values-fr │ │ └── strings.xml │ │ ├── values-hi │ │ └── strings.xml │ │ ├── values-night │ │ ├── colors.xml │ │ └── themes.xml │ │ ├── values-pt-rBR │ │ └── strings.xml │ │ ├── values │ │ ├── colors.xml │ │ ├── strings.xml │ │ └── themes.xml │ │ └── xml │ │ ├── app_blocker_service_config.xml │ │ ├── device_admin_receiver_info.xml │ │ ├── file_paths.xml │ │ ├── general_features_service_config.xml │ │ ├── keyword_blocker_service_config.xml │ │ ├── shortcuts.xml │ │ ├── usage_tracker_service_config.xml │ │ ├── view_blocker_service_config.xml │ │ ├── widget_reels_metadata.xml │ │ └── widget_screentime_metadata.xml │ └── test │ └── java │ └── nethical │ └── digipaws │ └── ExampleUnitTest.kt ├── build.gradle.kts ├── fastlane └── metadata │ └── android │ └── en-US │ ├── full_description.txt │ ├── images │ ├── featureGraphic.png │ ├── icon.png │ └── phoneScreenshots │ │ ├── 1.png │ │ ├── 2.png │ │ ├── 3.png │ │ ├── 4.png │ │ ├── 5.png │ │ ├── 6.png │ │ ├── 7.png │ │ └── 8.png │ ├── short_description.txt │ └── title.txt ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── local └── settings.gradle.kts /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: nethical 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Report a reproducible bug or unexpected behavior in DigiPaws. 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thank you for reporting an issue. Please provide as much detail as possible to help us investigate. 12 | 13 | - type: input 14 | id: summary 15 | attributes: 16 | label: Summary 17 | description: A short description of the issue. 18 | placeholder: e.g., "Focus Mode timer does not start on Android 14." 19 | 20 | - type: textarea 21 | id: steps 22 | attributes: 23 | label: Steps to Reproduce 24 | description: Clear steps to reproduce the issue. 25 | placeholder: | 26 | 1. Go to Focus Mode 27 | 2. Set timer 28 | 3. Observe that timer does not start 29 | 30 | - type: textarea 31 | id: expected 32 | attributes: 33 | label: Expected Behavior 34 | description: Describe what you expected to happen. 35 | 36 | - type: textarea 37 | id: actual 38 | attributes: 39 | label: Actual Behavior 40 | description: Describe what actually happened. 41 | 42 | - type: input 43 | id: environment 44 | attributes: 45 | label: Environment 46 | description: Device model, OS version, DigiPaws version. 47 | placeholder: e.g., OnePlus 9, Android 14, DigiPaws 1.2.1 48 | 49 | - type: textarea 50 | id: additional 51 | attributes: 52 | label: Additional Context (optional) 53 | description: Add logs, screenshots, or any other helpful information. 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest a new feature or improvement for DigiPaws. 3 | title: "[Feature]: " 4 | labels: ["enhancement"] 5 | assignees: [] 6 | 7 | body: 8 | - type: markdown 9 | attributes: 10 | value: | 11 | Thank you for suggesting a feature. Please describe it clearly and explain the problem it would solve. 12 | 13 | - type: input 14 | id: feature 15 | attributes: 16 | label: Feature Summary 17 | description: A clear and concise summary of the feature. 18 | placeholder: e.g., "Add challenge mode that unlocks apps after completing a quiz." 19 | 20 | - type: textarea 21 | id: problem 22 | attributes: 23 | label: Problem Statement 24 | description: Describe the problem this feature would address. 25 | 26 | - type: textarea 27 | id: solution 28 | attributes: 29 | label: Proposed Solution 30 | description: Describe how you would like the feature to work. 31 | 32 | - type: input 33 | id: user-impact 34 | attributes: 35 | label: Target Users 36 | description: Who will benefit from this feature? 37 | 38 | - type: textarea 39 | id: additional 40 | attributes: 41 | label: Additional Context (optional) 42 | description: Any other information, mockups, or references. 43 | -------------------------------------------------------------------------------- /.github/workflows/debug.yml: -------------------------------------------------------------------------------- 1 | name: Debug CI 2 | 3 | on: 4 | workflow_dispatch: # Manual trigger 5 | 6 | jobs: 7 | build: 8 | 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout repository 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up JDK 17 16 | uses: actions/setup-java@v4.2.1 17 | with: 18 | distribution: 'temurin' 19 | java-version: '17' 20 | 21 | - name: Give permission to executable 22 | run: chmod +x gradlew 23 | 24 | - name: Clear build cache 25 | uses: gradle/gradle-build-action@v3.1.0 26 | with: 27 | gradle-version: nightly 28 | arguments: clean 29 | 30 | - name: Build Lite Debug APK 31 | run: ./gradlew assembleLiteDebug 32 | 33 | - name: Upload Lite Debug APK 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: lite-debug-apk 37 | path: app/build/outputs/apk/lite/debug 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | DigiPaws -------------------------------------------------------------------------------- /.idea/AndroidProjectSystem.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 119 | 120 | 122 | 123 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/deploymentTargetSelector.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/migrations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 17 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.android.application) 3 | alias(libs.plugins.kotlin.android) 4 | } 5 | 6 | android { 7 | namespace = "nethical.digipaws" 8 | compileSdk = 34 9 | flavorDimensions += "version" 10 | 11 | defaultConfig { 12 | applicationId = "nethical.digipaws" 13 | minSdk = 26 14 | targetSdk = 34 15 | versionCode = 23 16 | versionName = "2.3-alpha" 17 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 18 | } 19 | 20 | productFlavors { 21 | create("lite") { 22 | dimension = "version" 23 | versionNameSuffix = "-lite" 24 | buildConfigField("Boolean", "FDROID_VARIANT", "true") 25 | } 26 | 27 | create("play-store") { 28 | dimension = "version" 29 | versionNameSuffix = "-full" 30 | buildConfigField("Boolean", "FDROID_VARIANT", "false") 31 | } 32 | } 33 | 34 | 35 | splits { 36 | abi { 37 | isEnable = false 38 | 39 | } 40 | } 41 | 42 | buildTypes { 43 | release { 44 | isMinifyEnabled = false 45 | proguardFiles( 46 | getDefaultProguardFile("proguard-android-optimize.txt"), 47 | "proguard-rules.pro" 48 | ) 49 | 50 | // required because of hardcoded f-droid values 51 | applicationVariants.all { 52 | val variant = this 53 | if (variant.flavorName == "lite") { 54 | variant.outputs 55 | .map { it as com.android.build.gradle.internal.api.BaseVariantOutputImpl } 56 | .forEach { output -> 57 | val outputFileName = "app-lite-universal-release-unsigned.apk" 58 | println("OutputFileName: $outputFileName") 59 | output.outputFileName = outputFileName 60 | } 61 | } 62 | } 63 | } 64 | } 65 | compileOptions { 66 | sourceCompatibility = JavaVersion.VERSION_1_8 67 | targetCompatibility = JavaVersion.VERSION_1_8 68 | } 69 | kotlinOptions { 70 | jvmTarget = "1.8" 71 | } 72 | buildFeatures { 73 | viewBinding = true 74 | buildConfig = true 75 | } 76 | } 77 | 78 | 79 | 80 | dependencies { 81 | 82 | implementation(libs.androidx.core.ktx) 83 | implementation(libs.androidx.appcompat) 84 | implementation(libs.material) 85 | implementation(libs.androidx.activity) 86 | implementation(libs.androidx.constraintlayout) 87 | 88 | // Shizuku dependecies 89 | implementation (libs.api) 90 | implementation (libs.provider) 91 | 92 | implementation(libs.gson) 93 | 94 | testImplementation(libs.junit) 95 | androidTestImplementation(libs.androidx.junit) 96 | androidTestImplementation(libs.androidx.espresso.core) 97 | 98 | implementation(libs.mpandroidchart) 99 | implementation(libs.timerangepicker) 100 | 101 | } -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /app/src/androidTest/java/nethical/digipaws/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws 2 | 3 | import androidx.test.platform.app.InstrumentationRegistry 4 | import androidx.test.ext.junit.runners.AndroidJUnit4 5 | 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | import org.junit.Assert.* 10 | 11 | /** 12 | * Instrumented test, which will execute on an Android device. 13 | * 14 | * See [testing documentation](http://d.android.com/tools/testing). 15 | */ 16 | @RunWith(AndroidJUnit4::class) 17 | class ExampleInstrumentedTest { 18 | @Test 19 | fun useAppContext() { 20 | // Context of the app under test. 21 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 22 | assertEquals("nethical.digipaws", appContext.packageName) 23 | } 24 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/Constants.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws 2 | 3 | class Constants { 4 | companion object { 5 | // available modes for setting up anti-uninstall 6 | const val ANTI_UNINSTALL_PASSWORD_MODE = 1 7 | const val ANTI_UNINSTALL_TIMED_MODE = 2 8 | 9 | // available types of warning screen 10 | const val WARNING_SCREEN_MODE_VIEW_BLOCKER = 1 11 | const val WARNING_SCREEN_MODE_APP_BLOCKER = 2 12 | 13 | // available types for focus mode 14 | const val FOCUS_MODE_BLOCK_ALL_EX_SELECTED = 1 15 | const val FOCUS_MODE_BLOCK_SELECTED = 2 16 | 17 | // available types for focus mode 18 | const val GRAYSCALE_MODE_ALL = 1 // apply to all apps 19 | const val GRAYSCALE_MODE_ONLY_SELECTED = 2 // apply to only selected 20 | const val GRAYSCALE_MODE_ALL_EXCEPT_SELECTED = 3 // apply to all except selected 21 | const val GRAYSCALE_MODE_OFF = 4 // turned off 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/CrashLogger.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws 2 | 3 | import android.content.Context 4 | import android.os.Build 5 | import java.io.File 6 | import java.io.FileWriter 7 | import java.io.PrintWriter 8 | import java.text.SimpleDateFormat 9 | import java.util.Date 10 | import java.util.Locale 11 | 12 | class CrashLogger(private val context: Context) : Thread.UncaughtExceptionHandler { 13 | 14 | private val logFile = File(context.filesDir, "crash_log.txt") 15 | private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() 16 | 17 | override fun uncaughtException(thread: Thread, throwable: Throwable) { 18 | try { 19 | val timeStamp = 20 | SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date()) 21 | val writer = FileWriter(logFile, true) // Append mode 22 | writer.append("\n--- Crash at $timeStamp ---\n") 23 | writer.append("Device: ${Build.MANUFACTURER} ${Build.MODEL} (Android ${Build.VERSION.RELEASE})\n") 24 | val printWriter = PrintWriter(writer) 25 | throwable.printStackTrace(printWriter) 26 | printWriter.flush() 27 | printWriter.close() 28 | } catch (e: Exception) { 29 | e.printStackTrace() 30 | } 31 | 32 | defaultHandler?.uncaughtException(thread, throwable) // Let the system handle the crash 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/Digipaws.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws 2 | 3 | import android.app.Application 4 | import com.google.android.material.color.DynamicColors 5 | 6 | class Digipaws: Application() { 7 | override fun onCreate() { 8 | DynamicColors.applyToActivitiesIfAvailable(this) 9 | Thread.setDefaultUncaughtExceptionHandler(CrashLogger(this)) 10 | super.onCreate() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/blockers/BaseBlocker.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.blockers 2 | 3 | import android.os.SystemClock 4 | 5 | 6 | abstract class BaseBlocker{ 7 | fun isDelayOver(lastTimeStamp: Float): Boolean { 8 | val currentTime = SystemClock.uptimeMillis().toFloat() 9 | return currentTime - lastTimeStamp > 30000 10 | } 11 | 12 | fun isDelayOver(lastTimeStamp: Float, delay: Int): Boolean { 13 | val currentTime = SystemClock.uptimeMillis().toFloat() 14 | return currentTime - lastTimeStamp > delay 15 | } 16 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/blockers/ViewBlocker.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.blockers 2 | 3 | import android.os.SystemClock 4 | import android.view.accessibility.AccessibilityNodeInfo 5 | import nethical.digipaws.utils.TimeTools 6 | import java.util.Calendar 7 | 8 | class ViewBlocker : BaseBlocker() { 9 | companion object { 10 | fun findElementById(node: AccessibilityNodeInfo?, id: String?): AccessibilityNodeInfo? { 11 | if (node == null) return null 12 | var targetNode: AccessibilityNodeInfo? = null 13 | try { 14 | targetNode = node.findAccessibilityNodeInfosByViewId(id!!)[0] 15 | } catch (e: Exception) { 16 | // e.printStackTrace(); 17 | } 18 | return targetNode 19 | } 20 | 21 | val TIKTOK_PACKAGE_NAMES = hashSetOf( 22 | "com.ss.android.ugc.trill", 23 | "com.zhiliaoapp.musically", 24 | "com.ss.android.ugc.aweme" 25 | ) 26 | val BLOCKED_VIEW_ID_LIST = mutableListOf( 27 | "com.instagram.android:id/root_clips_layout", 28 | "com.google.android.youtube:id/reel_recycler", 29 | "app.revanced.android.youtube:id/reel_recycler" 30 | ) 31 | 32 | } 33 | private val cooldownViewIdsList = mutableMapOf() 34 | 35 | 36 | var isIGInboxReelAllowed = false 37 | var isFirstReelInFeedAllowed = false 38 | 39 | var cheatMinuteStartTime: Int? = null 40 | var cheatMinutesEndTIme: Int? = null 41 | 42 | fun doesViewNeedToBeBlocked( 43 | node: AccessibilityNodeInfo, 44 | packageName: String 45 | ): ViewBlockerResult? { 46 | if (isCheatHourActive()) { 47 | return null 48 | } 49 | if (TIKTOK_PACKAGE_NAMES.contains(packageName)) { 50 | if (isCooldownActive(packageName)) { 51 | return ViewBlockerResult( 52 | isReelFoundInCooldownState = true, 53 | viewId = packageName, 54 | requestHomePressInstead = true 55 | ) 56 | } 57 | return ViewBlockerResult( 58 | isBlocked = true, 59 | viewId = packageName, 60 | requestHomePressInstead = true 61 | ) 62 | } 63 | 64 | if (isIGInboxReelAllowed && isViewOpened( 65 | node, 66 | "com.instagram.android:id/reply_bar_container" 67 | ) 68 | ) { 69 | return null 70 | } 71 | 72 | 73 | BLOCKED_VIEW_ID_LIST.forEach { viewId -> 74 | if(isViewOpened(node,viewId)){ 75 | if (isCooldownActive(viewId)) { 76 | return ViewBlockerResult(isReelFoundInCooldownState = true, viewId = viewId) 77 | } 78 | return ViewBlockerResult(isBlocked = true, viewId = viewId) 79 | } 80 | } 81 | return null 82 | } 83 | 84 | fun applyCooldown(viewId: String, endTime: Long) { 85 | cooldownViewIdsList[viewId] = endTime 86 | } 87 | 88 | private fun isCooldownActive(viewId: String): Boolean { 89 | val cooldownEnd = cooldownViewIdsList[viewId] ?: return false 90 | if (SystemClock.uptimeMillis() > cooldownEnd) { 91 | cooldownViewIdsList.remove(viewId) 92 | return false 93 | } 94 | return true 95 | } 96 | 97 | private fun isViewOpened(rootNode: AccessibilityNodeInfo, viewId: String): Boolean { 98 | val viewNode = 99 | findElementById(rootNode, viewId) 100 | return viewNode != null 101 | } 102 | 103 | private fun isCheatHourActive(): Boolean { 104 | 105 | val currentTime = Calendar.getInstance() 106 | val currentHour = currentTime.get(Calendar.HOUR_OF_DAY) 107 | val currentMinute = currentTime.get(Calendar.MINUTE) 108 | 109 | val currentMinutes = TimeTools.convertToMinutesFromMidnight(currentHour, currentMinute) 110 | 111 | // If cheat hours are not set, treat as inactive 112 | if (cheatMinuteStartTime == null || cheatMinutesEndTIme == null || cheatMinuteStartTime == -1 || cheatMinutesEndTIme == -1) { 113 | return false 114 | } 115 | 116 | return if (cheatMinuteStartTime!! <= cheatMinutesEndTIme!!) { 117 | // Regular case: start time is before or equal to end time 118 | currentMinutes in cheatMinuteStartTime!!..cheatMinutesEndTIme!! 119 | } else { 120 | // Wraparound case: time range spans midnight 121 | currentMinutes in cheatMinuteStartTime!!..1439 || currentMinutes in 0..cheatMinutesEndTIme!! 122 | } 123 | } 124 | data class ViewBlockerResult( 125 | val isBlocked: Boolean = false, 126 | val requestHomePressInstead: Boolean = false, 127 | val isReelFoundInCooldownState: Boolean = false, 128 | val viewId: String = "" 129 | ) 130 | 131 | } 132 | -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/data/blockers/KeywordPacks.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.data.blockers 2 | 3 | class KeywordPacks { 4 | companion object { 5 | val adultKeywords = setOf( 6 | "porn", 7 | "adult", 8 | "xxx", 9 | "erotic", 10 | "nude", 11 | "sex", 12 | "fetish", 13 | "nsfw", 14 | "camgirl", 15 | "explicit", 16 | "hardcore", 17 | "lingerie", 18 | "escort", 19 | "bdsm", 20 | "taboo", 21 | "kink", 22 | "lust", 23 | "voyeur", 24 | "amateur", 25 | "hentai", 26 | "orgy", 27 | "strip", 28 | "milf", 29 | "teens", 30 | "anal", 31 | "lesbian", 32 | "gay", 33 | "blowjob", 34 | "threesome", 35 | "incest", 36 | "submission", 37 | "dominatrix", 38 | "masturbation", 39 | "webcam", 40 | "provocative", 41 | "intercourse", 42 | "hookup", 43 | "seduction", 44 | "scandal", 45 | "temptation", 46 | "nudity", 47 | "orgasm", 48 | "prostitute", 49 | "sultry", 50 | "affair", 51 | "arousal", 52 | "sensual", 53 | "lustful", 54 | "peeping", 55 | "stripper", 56 | "playmate", 57 | "infidelity", 58 | "desire", 59 | "bondage", 60 | "swingers", 61 | "provocateur", 62 | "buceta", 63 | "pussy", 64 | "pelada", 65 | "siririca", 66 | "gozo", 67 | "gozei", 68 | "gozar", 69 | "masturbate" 70 | ) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/receivers/AdminReceiver.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.receivers 2 | 3 | import android.app.admin.DeviceAdminReceiver 4 | 5 | class AdminReceiver : DeviceAdminReceiver() -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/services/BaseBlockingService.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.services 2 | 3 | import android.accessibilityservice.AccessibilityService 4 | import android.os.SystemClock 5 | import android.view.accessibility.AccessibilityEvent 6 | import nethical.digipaws.utils.SavedPreferencesLoader 7 | 8 | open class BaseBlockingService : AccessibilityService() { 9 | val savedPreferencesLoader: SavedPreferencesLoader by lazy { 10 | SavedPreferencesLoader(this) 11 | } 12 | 13 | var lastBackPressTimeStamp: Long = 14 | SystemClock.uptimeMillis() // prevents repetitive global actions 15 | 16 | override fun onAccessibilityEvent(event: AccessibilityEvent?) { 17 | } 18 | 19 | override fun onInterrupt() { 20 | } 21 | 22 | private fun isDelayOver(): Boolean { 23 | return isDelayOver(1000) 24 | } 25 | 26 | fun isDelayOver(delay: Int): Boolean { 27 | val currentTime = SystemClock.uptimeMillis().toFloat() 28 | return currentTime - lastBackPressTimeStamp > delay 29 | } 30 | 31 | fun isDelayOver(lastTimestamp: Long, delay: Int): Boolean { 32 | val currentTime = SystemClock.uptimeMillis().toFloat() 33 | return currentTime - lastTimestamp > delay 34 | } 35 | 36 | fun pressHome() { 37 | if (isDelayOver()) { 38 | performGlobalAction(GLOBAL_ACTION_HOME) 39 | lastBackPressTimeStamp = SystemClock.uptimeMillis() 40 | } 41 | } 42 | 43 | fun pressBack() { 44 | if (isDelayOver()) { 45 | performGlobalAction(GLOBAL_ACTION_BACK) 46 | lastBackPressTimeStamp = SystemClock.uptimeMillis() 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/services/KeywordBlockerService.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.services 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.BroadcastReceiver 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.IntentFilter 8 | import android.os.Build 9 | import android.os.SystemClock 10 | import android.util.Log 11 | import android.view.accessibility.AccessibilityEvent 12 | import android.view.accessibility.AccessibilityNodeInfo 13 | import android.widget.Toast 14 | import nethical.digipaws.blockers.KeywordBlocker 15 | import nethical.digipaws.data.blockers.KeywordPacks 16 | 17 | class KeywordBlockerService : BaseBlockingService() { 18 | 19 | private var refreshCooldown = 1000 20 | private var lastEventTimeStamp = 0L 21 | companion object { 22 | const val INTENT_ACTION_REFRESH_BLOCKED_KEYWORD_LIST = 23 | "nethical.digipaws.refresh.keywordblocker.blockedwords" 24 | 25 | const val INTENT_ACTION_REFRESH_CONFIG = 26 | "nethical.digipaws.refresh.keywordblocker.config" 27 | } 28 | 29 | private val keywordBlocker = KeywordBlocker(this) 30 | 31 | private var KbIgnoredApps: HashSet = hashSetOf() 32 | 33 | override fun onAccessibilityEvent(event: AccessibilityEvent?) { 34 | 35 | if (!isDelayOver( 36 | lastEventTimeStamp, 37 | refreshCooldown 38 | ) || event == null || event.packageName == "nethical.digipaws" || KbIgnoredApps.contains( 39 | event.packageName 40 | ) 41 | ) { 42 | return 43 | } 44 | val rootnode: AccessibilityNodeInfo? = rootInActiveWindow 45 | Log.d("KeywordBlocker", "Searching Keywords") 46 | handleKeywordBlockerResult(keywordBlocker.checkIfUserGettingFreaky(rootnode, event)) 47 | 48 | lastEventTimeStamp = SystemClock.uptimeMillis() 49 | 50 | } 51 | 52 | override fun onInterrupt() { 53 | } 54 | 55 | @SuppressLint("UnspecifiedRegisterReceiverFlag") 56 | override fun onServiceConnected() { 57 | super.onServiceConnected() 58 | setupBlockedWords() 59 | setupConfig() 60 | 61 | val filter = IntentFilter().apply { 62 | addAction(INTENT_ACTION_REFRESH_BLOCKED_KEYWORD_LIST) 63 | addAction(INTENT_ACTION_REFRESH_CONFIG) 64 | } 65 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { 66 | registerReceiver(refreshReceiver, filter, RECEIVER_EXPORTED) 67 | } else { 68 | registerReceiver(refreshReceiver, filter) 69 | } 70 | } 71 | 72 | 73 | private val refreshReceiver = object : BroadcastReceiver() { 74 | override fun onReceive(context: Context?, intent: Intent?) { 75 | when (intent?.action) { 76 | INTENT_ACTION_REFRESH_BLOCKED_KEYWORD_LIST -> setupBlockedWords() 77 | INTENT_ACTION_REFRESH_CONFIG -> setupConfig() 78 | } 79 | } 80 | } 81 | 82 | private fun handleKeywordBlockerResult(result: KeywordBlocker.KeywordBlockerResult) { 83 | if (result.resultDetectWord == null) return 84 | Toast.makeText( 85 | this, 86 | "Blocked keyword ${result.resultDetectWord} was found.", 87 | Toast.LENGTH_LONG 88 | ).show() 89 | if (result.isHomePressRequested) { 90 | pressHome() 91 | } 92 | } 93 | 94 | 95 | private fun setupBlockedWords() { 96 | val keywords = savedPreferencesLoader.loadBlockedKeywords().toMutableSet() 97 | val sp = getSharedPreferences("keyword_blocker_packs", Context.MODE_PRIVATE) 98 | val isAdultBlockerOn = sp.getBoolean("adult_blocker", false) 99 | if (isAdultBlockerOn) { 100 | keywords.addAll(KeywordPacks.adultKeywords) 101 | } 102 | keywordBlocker.blockedKeyword = keywords.toHashSet() 103 | } 104 | 105 | private fun setupConfig() { 106 | val sp = getSharedPreferences("keyword_blocker_configs", Context.MODE_PRIVATE) 107 | 108 | keywordBlocker.isSearchAllTextFields = sp.getBoolean("search_all_text_fields", false) 109 | keywordBlocker.redirectUrl = 110 | sp.getString("redirect_url", "https://www.youtube.com/watch?v=x31tDT-4fQw&t=1s") 111 | .toString() 112 | 113 | if (keywordBlocker.isSearchAllTextFields) { 114 | refreshCooldown = 5000 115 | } 116 | 117 | KbIgnoredApps = savedPreferencesLoader.getKeywordBlockerIgnoredApps().toHashSet() 118 | 119 | } 120 | 121 | 122 | 123 | override fun onDestroy() { 124 | super.onDestroy() 125 | unregisterReceiver(refreshReceiver) 126 | } 127 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/activity/FragmentActivity.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.activity 2 | 3 | import android.os.Bundle 4 | import androidx.activity.enableEdgeToEdge 5 | import androidx.appcompat.app.AppCompatActivity 6 | import androidx.core.view.ViewCompat 7 | import androidx.core.view.WindowInsetsCompat 8 | import androidx.fragment.app.Fragment 9 | import nethical.digipaws.R 10 | import nethical.digipaws.ui.fragments.anti_uninstall.ChooseModeFragment 11 | import nethical.digipaws.ui.fragments.installation.AccessibilityGuide 12 | import nethical.digipaws.ui.fragments.installation.WelcomeFragment 13 | import nethical.digipaws.ui.fragments.usage.AllAppsUsageFragment 14 | 15 | class FragmentActivity : AppCompatActivity() { 16 | 17 | override fun onCreate(savedInstanceState: Bundle?) { 18 | super.onCreate(savedInstanceState) 19 | enableEdgeToEdge() 20 | setContentView(R.layout.activity_fragment) 21 | ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets -> 22 | val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()) 23 | v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom) 24 | insets 25 | } 26 | 27 | var fragment: Fragment? = null 28 | if (intent.getStringExtra("fragment") != null) { 29 | when (intent.getStringExtra("fragment")) { 30 | ChooseModeFragment.FRAGMENT_ID -> { 31 | fragment = ChooseModeFragment() 32 | } 33 | AllAppsUsageFragment.FRAGMENT_ID -> { 34 | fragment = AllAppsUsageFragment() 35 | } 36 | WelcomeFragment.FRAGMENT_ID -> { 37 | fragment = WelcomeFragment() 38 | } 39 | AccessibilityGuide.FRAGMENT_ID -> 40 | fragment = AccessibilityGuide() 41 | } 42 | supportFragmentManager.beginTransaction() 43 | .replace( 44 | R.id.fragment_holder, 45 | fragment!! 46 | ) // Add or replace the fragment in the container 47 | .commit() // Commit the transaction 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/activity/ShortcutActivity.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.activity 2 | 3 | import android.accessibilityservice.AccessibilityService 4 | import android.content.ComponentName 5 | import android.content.Intent 6 | import android.os.Bundle 7 | import android.provider.Settings 8 | import android.widget.Toast 9 | import androidx.appcompat.app.AppCompatActivity 10 | import nethical.digipaws.services.GeneralFeaturesService 11 | import nethical.digipaws.ui.dialogs.StartFocusMode 12 | import nethical.digipaws.utils.SavedPreferencesLoader 13 | 14 | class ShortcutActivity : AppCompatActivity() { 15 | 16 | private val savedPreferencesLoader = SavedPreferencesLoader(this) 17 | 18 | override fun onCreate(savedInstanceState: Bundle?) { 19 | super.onCreate(savedInstanceState) 20 | 21 | val isFocusedModeOn = savedPreferencesLoader.getFocusModeData().isTurnedOn 22 | if(isFocusedModeOn){ 23 | Toast.makeText(this,"Focus Mode is already active",Toast.LENGTH_SHORT).show() 24 | finish() 25 | } 26 | 27 | val isGeneralSettingsOn = isAccessibilityServiceEnabled(GeneralFeaturesService::class.java) 28 | if(!isGeneralSettingsOn){ 29 | Toast.makeText(this,"Find 'General Features' and press enable",Toast.LENGTH_LONG).show() 30 | openAccessibilityServiceScreen(cls = GeneralFeaturesService::class.java) 31 | finish() 32 | } 33 | StartFocusMode(savedPreferencesLoader, onPositiveButtonPressed = { 34 | finish() 35 | }).show( 36 | supportFragmentManager, 37 | "start_focus_mode_from_shortcut" 38 | ) 39 | } 40 | 41 | 42 | private fun openAccessibilityServiceScreen(cls: Class<*>) { 43 | try { 44 | val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) 45 | val componentName = ComponentName(this, cls) 46 | 47 | intent.putExtra(":settings:fragment_args_key", componentName.flattenToString()) 48 | startActivity(intent) 49 | } catch (e: Exception) { 50 | e.printStackTrace() 51 | // Fallback to general Accessibility Settings 52 | startActivity(Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS)) 53 | } 54 | } 55 | 56 | private fun isAccessibilityServiceEnabled(serviceClass: Class): Boolean { 57 | val serviceName = ComponentName(this, serviceClass).flattenToString() 58 | val enabledServices = Settings.Secure.getString( 59 | contentResolver, 60 | Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES 61 | ) ?: return false 62 | val isAccessibilityEnabled = Settings.Secure.getInt( 63 | contentResolver, 64 | Settings.Secure.ACCESSIBILITY_ENABLED, 65 | 0 66 | ) 67 | return isAccessibilityEnabled == 1 && enabledServices.contains(serviceName) 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/dialogs/BaseDialog.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.dialogs 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Intent 5 | import android.view.MotionEvent 6 | import android.widget.ScrollView 7 | import androidx.fragment.app.DialogFragment 8 | import nethical.digipaws.utils.SavedPreferencesLoader 9 | import nl.joery.timerangepicker.TimeRangePicker 10 | 11 | open class BaseDialog(val savedPreferencesLoader: SavedPreferencesLoader? = null) : 12 | DialogFragment() { 13 | fun sendRefreshRequest(action: String) { 14 | val intent = Intent(action) 15 | context?.sendBroadcast(intent) 16 | } 17 | 18 | 19 | @SuppressLint("ClickableViewAccessibility") 20 | fun fixPickerInterceptBug(scrollview: ScrollView, picker: TimeRangePicker) { 21 | picker.setOnTouchListener { v, event -> 22 | // Disable ScrollView's touch interception when interacting with the picker 23 | when (event.action) { 24 | MotionEvent.ACTION_DOWN -> scrollview.requestDisallowInterceptTouchEvent(true) 25 | MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> scrollview.requestDisallowInterceptTouchEvent( 26 | false 27 | ) 28 | } 29 | v.onTouchEvent(event) // Pass the event to the picker 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/dialogs/StartFocusMode.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.dialogs 2 | 3 | import android.app.Dialog 4 | import android.os.Bundle 5 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 6 | import nethical.digipaws.Constants 7 | import nethical.digipaws.R 8 | import nethical.digipaws.blockers.FocusModeBlocker 9 | import nethical.digipaws.databinding.DialogFocusModeBinding 10 | import nethical.digipaws.services.AppBlockerService 11 | import nethical.digipaws.utils.NotificationTimerManager 12 | import nethical.digipaws.utils.SavedPreferencesLoader 13 | 14 | class StartFocusMode(savedPreferencesLoader: SavedPreferencesLoader,private val onPositiveButtonPressed: () -> Unit) : BaseDialog( 15 | savedPreferencesLoader 16 | ) { 17 | 18 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 19 | // Inflate the custom dialog layout 20 | val dialogFocusModeBinding = DialogFocusModeBinding.inflate(layoutInflater) 21 | val previousData = savedPreferencesLoader?.getFocusModeData() 22 | dialogFocusModeBinding.focusModeMinsPicker.setValue(3) 23 | dialogFocusModeBinding.focusModeMinsPicker.minValue = 2 24 | 25 | var selectedMode = previousData?.modeType 26 | if (previousData != null) { 27 | when (previousData.modeType) { 28 | Constants.FOCUS_MODE_BLOCK_SELECTED -> dialogFocusModeBinding.blockSelected.isChecked = 29 | true 30 | 31 | Constants.FOCUS_MODE_BLOCK_ALL_EX_SELECTED -> dialogFocusModeBinding.blockAll.isChecked = 32 | true 33 | } 34 | } 35 | 36 | dialogFocusModeBinding.modeType.setOnCheckedChangeListener { _, checkedId -> 37 | when (checkedId) { 38 | dialogFocusModeBinding.blockAll.id -> selectedMode = 39 | Constants.FOCUS_MODE_BLOCK_ALL_EX_SELECTED 40 | 41 | dialogFocusModeBinding.blockSelected.id -> selectedMode = 42 | Constants.FOCUS_MODE_BLOCK_SELECTED 43 | } 44 | } 45 | return MaterialAlertDialogBuilder(requireContext()) 46 | .setView(dialogFocusModeBinding.root) 47 | .setPositiveButton(getString(R.string.start)) { _, _ -> 48 | val totalMillis = dialogFocusModeBinding.focusModeMinsPicker.getValue() * 60000 49 | savedPreferencesLoader?.saveFocusModeData( 50 | FocusModeBlocker.FocusModeData( 51 | true, 52 | System.currentTimeMillis() + totalMillis, 53 | selectedMode!! 54 | ) 55 | ) 56 | sendRefreshRequest(AppBlockerService.INTENT_ACTION_REFRESH_FOCUS_MODE) 57 | val timer = NotificationTimerManager(requireContext()) 58 | // TODO: add notification permission check 59 | timer.startTimer(totalMillis.toLong()) 60 | onPositiveButtonPressed() 61 | } 62 | .setNegativeButton(getString(R.string.cancel), null) 63 | .show() 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/dialogs/TweakAppBlockerWarning.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.dialogs 2 | 3 | import android.animation.LayoutTransition 4 | import android.app.Dialog 5 | import android.os.Bundle 6 | import android.util.Log 7 | import com.google.android.material.chip.Chip 8 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 9 | import nethical.digipaws.R 10 | import nethical.digipaws.databinding.DialogTweakBlockerWarningBinding 11 | import nethical.digipaws.services.AppBlockerService 12 | import nethical.digipaws.ui.activity.MainActivity 13 | import nethical.digipaws.utils.AnimTools.Companion.animateVisibility 14 | import nethical.digipaws.utils.SavedPreferencesLoader 15 | 16 | class TweakAppBlockerWarning(savedPreferencesLoader: SavedPreferencesLoader) : BaseDialog( 17 | savedPreferencesLoader 18 | ) { 19 | 20 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 21 | // Inflate the custom dialog layout 22 | val binding = DialogTweakBlockerWarningBinding.inflate(layoutInflater) 23 | 24 | // Set up number picker 25 | binding.selectMins.minValue = 1 26 | binding.selectMins.maxValue = 240 27 | 28 | binding.cbProceedBtn.setOnCheckedChangeListener { _, isChecked -> 29 | val viewsToToggle = listOf( 30 | binding.cbDynamicWarning, 31 | binding.selectMins, 32 | binding.info, 33 | binding.proceedDelay 34 | ) 35 | viewsToToggle.forEach { it.animateVisibility(!isChecked) } 36 | 37 | } 38 | 39 | binding.cbBackWithoutWarning.setOnCheckedChangeListener { _, isChecked -> 40 | val viewsToToggle = listOf( 41 | binding.cbDynamicWarning, 42 | binding.selectMins, 43 | binding.textInputLayout2, 44 | binding.info, 45 | binding.cbProceedBtn, 46 | binding.proceedDelay 47 | ) 48 | viewsToToggle.forEach { it.animateVisibility(!isChecked) } 49 | } 50 | 51 | // Load previous data from preferences 52 | val previousData = savedPreferencesLoader!!.loadAppBlockerWarningInfo() 53 | var proceedDelay = previousData.proceedDelayInSecs 54 | 55 | when (proceedDelay) { 56 | 3 -> binding.proceedDelayChips.check(R.id.three_sec_chip) 57 | 9 -> binding.proceedDelayChips.check(R.id.nine_sec_chip) 58 | 30 -> binding.proceedDelayChips.check(R.id.thirty_sec_chip) 59 | 15 -> binding.proceedDelayChips.check(R.id.fifteen_sec_chip) 60 | } 61 | 62 | binding.proceedDelayChips.setOnCheckedStateChangeListener { group, checkedIds -> 63 | 64 | val chip = group.findViewById(checkedIds[0]) 65 | proceedDelay = chip.text.toString().slice(IntRange(0, 1)).trim().toInt() 66 | Log.d("proceedDelay", "onCreateDialog: $proceedDelay") 67 | } 68 | 69 | previousData.let { 70 | binding.selectMins.setValue(it.timeInterval / 60000) 71 | binding.warningMsgEdit.setText(it.message) 72 | binding.cbProceedBtn.isChecked = it.isProceedDisabled 73 | binding.cbBackWithoutWarning.isChecked = it.isWarningDialogHidden 74 | } 75 | 76 | binding.root.layoutTransition = LayoutTransition().apply { 77 | enableTransitionType(LayoutTransition.CHANGING) 78 | setDuration(300) // Set animation duration in ms 79 | } 80 | 81 | // Build and return the dialog 82 | return MaterialAlertDialogBuilder(requireContext()) 83 | .setView(binding.root) 84 | .setPositiveButton(getString(R.string.save)) { dialog, _ -> 85 | val selectedMinInMs = binding.selectMins.getValue() * 60000 86 | savedPreferencesLoader.saveAppBlockerWarningInfo( 87 | MainActivity.WarningData( 88 | binding.warningMsgEdit.text.toString(), 89 | selectedMinInMs, 90 | binding.cbDynamicWarning.isChecked, 91 | binding.cbProceedBtn.isChecked, 92 | binding.cbBackWithoutWarning.isChecked, 93 | proceedDelay 94 | ) 95 | ) 96 | sendRefreshRequest(AppBlockerService.INTENT_ACTION_REFRESH_APP_BLOCKER) 97 | dialog.dismiss() 98 | } 99 | .setCancelable(false) 100 | .setNegativeButton(getString(R.string.cancel)) { dialog, _ -> 101 | dialog.dismiss() 102 | } 103 | .create() 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/dialogs/TweakGrayScaleMode.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.dialogs 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Dialog 5 | import android.content.Context 6 | import android.content.SharedPreferences 7 | import android.os.Bundle 8 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 9 | import nethical.digipaws.Constants 10 | import nethical.digipaws.R 11 | import nethical.digipaws.databinding.DialogGrayscaleBinding 12 | import nethical.digipaws.services.GeneralFeaturesService 13 | import nethical.digipaws.utils.GrayscaleControl 14 | import nethical.digipaws.utils.SavedPreferencesLoader 15 | 16 | class TweakGrayScaleMode( 17 | savedPreferencesLoader: SavedPreferencesLoader 18 | ) : BaseDialog(savedPreferencesLoader) { 19 | 20 | private lateinit var trackerPreferences: SharedPreferences 21 | 22 | @SuppressLint("ApplySharedPref") 23 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 24 | val dialogGrayscaleBinding = DialogGrayscaleBinding.inflate(layoutInflater) 25 | 26 | // Load tracker preferences 27 | trackerPreferences = 28 | requireContext().getSharedPreferences("grayscale", Context.MODE_PRIVATE) 29 | val getMode = trackerPreferences.getInt("mode",Constants.GRAYSCALE_MODE_ONLY_SELECTED) 30 | 31 | 32 | 33 | when(getMode){ 34 | Constants.GRAYSCALE_MODE_ONLY_SELECTED -> { 35 | dialogGrayscaleBinding.blockSelected.isChecked = true 36 | } 37 | Constants.GRAYSCALE_MODE_ALL -> { 38 | dialogGrayscaleBinding.blockAll.isChecked = true 39 | } 40 | Constants.GRAYSCALE_MODE_ALL_EXCEPT_SELECTED -> { 41 | dialogGrayscaleBinding.blockExceptSelected.isChecked = true 42 | } 43 | } 44 | 45 | // Build and display dialog 46 | return MaterialAlertDialogBuilder(requireContext()) 47 | .setView(dialogGrayscaleBinding.root) 48 | .setCancelable(false) 49 | .setPositiveButton(getString(R.string.save)) { dialog, _ -> 50 | when(dialogGrayscaleBinding.modeType.checkedRadioButtonId){ 51 | dialogGrayscaleBinding.blockAll.id -> { 52 | trackerPreferences.edit().putInt("mode",Constants.GRAYSCALE_MODE_ALL).commit() 53 | val grayscaleControl = GrayscaleControl() 54 | grayscaleControl.enableGrayscale() 55 | } 56 | dialogGrayscaleBinding.turnOff.id -> { 57 | trackerPreferences.edit().putInt("mode",Constants.GRAYSCALE_MODE_OFF).commit() 58 | val grayscaleControl = GrayscaleControl() 59 | grayscaleControl.disableGrayscale() 60 | } 61 | dialogGrayscaleBinding.blockSelected.id -> { 62 | trackerPreferences.edit().putInt("mode",Constants.GRAYSCALE_MODE_ONLY_SELECTED).commit() 63 | } 64 | dialogGrayscaleBinding.blockExceptSelected.id -> { 65 | trackerPreferences.edit().putInt("mode",Constants.GRAYSCALE_MODE_ALL_EXCEPT_SELECTED).commit() 66 | } 67 | 68 | } 69 | // Send broadcast to refresh UsageTrackingService 70 | sendRefreshRequest(GeneralFeaturesService.INTENT_ACTION_REFRESH_GRAYSCALE) 71 | dialog.dismiss() 72 | } 73 | .setNegativeButton(getString(R.string.cancel)) { dialog, _ -> 74 | dialog.dismiss() 75 | } 76 | .create() 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/dialogs/TweakKeywordBlocker.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.dialogs 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Dialog 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.content.SharedPreferences 8 | import android.os.Bundle 9 | import android.view.View 10 | import androidx.activity.result.ActivityResultLauncher 11 | import androidx.activity.result.contract.ActivityResultContracts 12 | import androidx.appcompat.app.AppCompatActivity.RESULT_OK 13 | import androidx.core.app.ActivityOptionsCompat 14 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 15 | import nethical.digipaws.R 16 | import nethical.digipaws.databinding.DialogKeywordBlockerConfigBinding 17 | import nethical.digipaws.services.KeywordBlockerService 18 | import nethical.digipaws.ui.activity.SelectAppsActivity 19 | import nethical.digipaws.utils.SavedPreferencesLoader 20 | 21 | class TweakKeywordBlocker(savedPreferencesLoader: SavedPreferencesLoader) : 22 | BaseDialog(savedPreferencesLoader) { 23 | 24 | private lateinit var sharedPreferences: SharedPreferences 25 | 26 | private var ignoredAppsSize = 0 27 | 28 | @SuppressLint("ApplySharedPref", "SetTextI18n") 29 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 30 | val dialogManageKeywordBlocker = DialogKeywordBlockerConfigBinding.inflate(layoutInflater) 31 | 32 | 33 | val selectIgnoredApps: ActivityResultLauncher = 34 | registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> 35 | if (result.resultCode == RESULT_OK) { 36 | val selectedApps = result.data?.getStringArrayListExtra("SELECTED_APPS") 37 | selectedApps?.let { 38 | ignoredAppsSize = selectedApps.size 39 | dialogManageKeywordBlocker.ignoredKbApps.setText(dialogManageKeywordBlocker.ignoredKbApps.text.toString() + " " + "($ignoredAppsSize)") 40 | savedPreferencesLoader?.saveKeywordBlockerIgnoredApps(selectedApps) 41 | } 42 | } 43 | } 44 | if (!dialogManageKeywordBlocker.cbSearchTextField.isChecked) { 45 | dialogManageKeywordBlocker.ignoredKbApps.visibility = View.GONE 46 | } 47 | dialogManageKeywordBlocker.cbSearchTextField.setOnCheckedChangeListener { _, isChecked -> 48 | if (isChecked) { 49 | dialogManageKeywordBlocker.ignoredKbApps.visibility = View.VISIBLE 50 | } else { 51 | dialogManageKeywordBlocker.ignoredKbApps.visibility = View.GONE 52 | } 53 | } 54 | 55 | dialogManageKeywordBlocker.ignoredKbApps.setOnClickListener { 56 | 57 | val intent = Intent(requireContext(), SelectAppsActivity::class.java) 58 | intent.putStringArrayListExtra( 59 | "PRE_SELECTED_APPS", 60 | ArrayList(savedPreferencesLoader?.getKeywordBlockerIgnoredApps() ?: emptyList()) 61 | ) 62 | selectIgnoredApps.launch( 63 | intent, 64 | ActivityOptionsCompat.makeCustomAnimation( 65 | requireContext(), 66 | R.anim.fade_in, 67 | R.anim.fade_out 68 | ) 69 | ) 70 | } 71 | 72 | // Initialize SharedPreferences 73 | sharedPreferences = 74 | requireContext().getSharedPreferences("keyword_blocker_configs", Context.MODE_PRIVATE) 75 | 76 | // Load current preferences into dialog 77 | dialogManageKeywordBlocker.cbSearchTextField.isChecked = 78 | sharedPreferences.getBoolean("search_all_text_fields", false) 79 | dialogManageKeywordBlocker.redirectUrl.setText( 80 | sharedPreferences.getString( 81 | "redirect_url", 82 | "https://www.youtube.com/watch?v=x31tDT-4fQw&t=1s" 83 | ) 84 | ) 85 | 86 | // Build and show the dialog 87 | return MaterialAlertDialogBuilder(requireContext()) 88 | .setView(dialogManageKeywordBlocker.root) 89 | .setCancelable(false) 90 | .setPositiveButton(getString(R.string.ok)) { _, _ -> 91 | // Save changes to SharedPreferences 92 | with(sharedPreferences.edit()) { 93 | putBoolean( 94 | "search_all_text_fields", 95 | dialogManageKeywordBlocker.cbSearchTextField.isChecked 96 | ) 97 | putString( 98 | "redirect_url", 99 | dialogManageKeywordBlocker.redirectUrl.text.toString() 100 | ) 101 | commit() // Save changes immediately 102 | } 103 | 104 | // Send broadcast to refresh the KeywordBlockerService 105 | sendRefreshRequest(KeywordBlockerService.INTENT_ACTION_REFRESH_CONFIG) 106 | } 107 | .setNegativeButton(getString(R.string.close)) { _, _ -> 108 | // Do nothing on cancel 109 | } 110 | .create() 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/dialogs/TweakKeywordPack.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.dialogs 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Dialog 5 | import android.content.Context 6 | import android.content.SharedPreferences 7 | import android.os.Bundle 8 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 9 | import nethical.digipaws.R 10 | import nethical.digipaws.databinding.DialogKeywordPackageBinding 11 | import nethical.digipaws.services.KeywordBlockerService 12 | 13 | class TweakKeywordPack : BaseDialog() { 14 | 15 | private lateinit var sharedPreferences: SharedPreferences 16 | 17 | @SuppressLint("ApplySharedPref") 18 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 19 | val dialogManageKeywordPacks = DialogKeywordPackageBinding.inflate(layoutInflater) 20 | 21 | // Initialize SharedPreferences 22 | sharedPreferences = 23 | requireContext().getSharedPreferences("keyword_blocker_packs", Context.MODE_PRIVATE) 24 | 25 | // Load current preferences into dialog 26 | dialogManageKeywordPacks.cbAdultKeywords.isChecked = 27 | sharedPreferences.getBoolean("adult_blocker", false) 28 | 29 | // Build and show the dialog 30 | return MaterialAlertDialogBuilder(requireContext()) 31 | .setTitle(getString(R.string.manage_keyword_blockers)) 32 | .setView(dialogManageKeywordPacks.root) 33 | .setCancelable(false) 34 | .setPositiveButton(getString(R.string.save)) { _, _ -> 35 | // Save changes to SharedPreferences 36 | with(sharedPreferences.edit()) { 37 | putBoolean( 38 | "adult_blocker", 39 | dialogManageKeywordPacks.cbAdultKeywords.isChecked 40 | ) 41 | commit() // Save changes immediately 42 | } 43 | 44 | // Send broadcast to refresh the KeywordBlockerService 45 | sendRefreshRequest(KeywordBlockerService.INTENT_ACTION_REFRESH_BLOCKED_KEYWORD_LIST) 46 | } 47 | .setNegativeButton(getString(R.string.cancel)) { dialog, _ -> 48 | dialog.dismiss() 49 | } 50 | .create() 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/dialogs/TweakUsageTracker.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.dialogs 2 | 3 | import android.annotation.SuppressLint 4 | import android.app.Dialog 5 | import android.content.Context 6 | import android.content.SharedPreferences 7 | import android.os.Bundle 8 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 9 | import nethical.digipaws.R 10 | import nethical.digipaws.databinding.DialogConfigTrackerBinding 11 | import nethical.digipaws.services.UsageTrackingService 12 | import nethical.digipaws.utils.SavedPreferencesLoader 13 | 14 | class TweakUsageTracker( 15 | savedPreferencesLoader: SavedPreferencesLoader 16 | ) : BaseDialog(savedPreferencesLoader) { 17 | 18 | private lateinit var trackerPreferences: SharedPreferences 19 | 20 | @SuppressLint("ApplySharedPref") 21 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 22 | val dialogConfigurationTracker = DialogConfigTrackerBinding.inflate(layoutInflater) 23 | 24 | // Load tracker preferences 25 | trackerPreferences = 26 | requireContext().getSharedPreferences("config_tracker", Context.MODE_PRIVATE) 27 | dialogConfigurationTracker.cbReelCounter.isChecked = 28 | trackerPreferences.getBoolean("is_reel_counter", true) 29 | dialogConfigurationTracker.cbTimeElapsed.isChecked = 30 | trackerPreferences.getBoolean("is_time_elapsed", false) 31 | 32 | // Build and display dialog 33 | return MaterialAlertDialogBuilder(requireContext()) 34 | .setView(dialogConfigurationTracker.root) 35 | .setCancelable(false) 36 | .setPositiveButton(getString(R.string.save)) { dialog, _ -> 37 | 38 | // Save updated settings 39 | with(trackerPreferences.edit()) { 40 | putBoolean( 41 | "is_reel_counter", 42 | dialogConfigurationTracker.cbReelCounter.isChecked 43 | ) 44 | putBoolean( 45 | "is_time_elapsed", 46 | dialogConfigurationTracker.cbTimeElapsed.isChecked 47 | ) 48 | commit() // Apply changes immediately 49 | } 50 | 51 | // Send broadcast to refresh UsageTrackingService 52 | sendRefreshRequest(UsageTrackingService.INTENT_ACTION_REFRESH_USAGE_TRACKER) 53 | dialog.dismiss() 54 | } 55 | .setNegativeButton(getString(R.string.cancel)) { dialog, _ -> 56 | dialog.dismiss() 57 | } 58 | .create() 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/dialogs/TweakViewBlockerCheatHours.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.dialogs 2 | 3 | import android.app.Dialog 4 | import android.content.Context 5 | import android.os.Bundle 6 | import android.view.View 7 | import android.widget.Toast 8 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 9 | import nethical.digipaws.R 10 | import nethical.digipaws.databinding.DialogAddTimedActionBinding 11 | import nethical.digipaws.services.ViewBlockerService 12 | import nethical.digipaws.utils.SavedPreferencesLoader 13 | import nl.joery.timerangepicker.TimeRangePicker 14 | 15 | class TweakViewBlockerCheatHours(savedPreferencesLoader: SavedPreferencesLoader) : BaseDialog( 16 | savedPreferencesLoader 17 | ) { 18 | 19 | private var startTimeInMins: Int? = null 20 | private var endTimeInMins: Int? = null 21 | 22 | override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { 23 | 24 | val dialogAddToCheatHoursBinding = DialogAddTimedActionBinding.inflate(layoutInflater) 25 | 26 | // Hide unused UI elements 27 | dialogAddToCheatHoursBinding.btnSelectUnblockedApps.visibility = View.GONE 28 | dialogAddToCheatHoursBinding.cheatHourTitle.visibility = View.GONE 29 | 30 | // Configure time picker 31 | dialogAddToCheatHoursBinding.picker.hourFormat = TimeRangePicker.HourFormat.FORMAT_24 32 | fixPickerInterceptBug( 33 | dialogAddToCheatHoursBinding.scrollview, 34 | dialogAddToCheatHoursBinding.picker 35 | ) 36 | 37 | val viewBlockerCheatHours = 38 | requireContext().getSharedPreferences("cheat_hours", Context.MODE_PRIVATE) 39 | val savedEndTimeInMinutes = viewBlockerCheatHours.getInt("view_blocker_end_time", -1) 40 | val savedStartTimeInMinutes = viewBlockerCheatHours.getInt("view_blocker_start_time", -1) 41 | 42 | // Set saved times if available 43 | if (savedStartTimeInMinutes != -1 || savedEndTimeInMinutes != -1) { 44 | dialogAddToCheatHoursBinding.picker.startTimeMinutes = savedStartTimeInMinutes 45 | dialogAddToCheatHoursBinding.picker.endTimeMinutes = savedEndTimeInMinutes 46 | startTimeInMins = savedStartTimeInMinutes 47 | endTimeInMins = savedEndTimeInMinutes 48 | } else { 49 | dialogAddToCheatHoursBinding.picker.startTimeMinutes = 0 50 | dialogAddToCheatHoursBinding.picker.endTimeMinutes = 0 51 | dialogAddToCheatHoursBinding.fromTime.text = getString(R.string.from) 52 | dialogAddToCheatHoursBinding.endTime.text = getString(R.string.end) 53 | } 54 | 55 | // Handle time changes 56 | dialogAddToCheatHoursBinding.picker.setOnTimeChangeListener(object : 57 | TimeRangePicker.OnTimeChangeListener { 58 | override fun onStartTimeChange(startTime: TimeRangePicker.Time) { 59 | dialogAddToCheatHoursBinding.fromTime.text = 60 | dialogAddToCheatHoursBinding.picker.startTime.toString() 61 | startTimeInMins = dialogAddToCheatHoursBinding.picker.startTimeMinutes 62 | endTimeInMins = dialogAddToCheatHoursBinding.picker.endTimeMinutes 63 | } 64 | 65 | override fun onEndTimeChange(endTime: TimeRangePicker.Time) { 66 | dialogAddToCheatHoursBinding.endTime.text = 67 | dialogAddToCheatHoursBinding.picker.endTime.toString() 68 | startTimeInMins = dialogAddToCheatHoursBinding.picker.startTimeMinutes 69 | endTimeInMins = dialogAddToCheatHoursBinding.picker.endTimeMinutes 70 | } 71 | 72 | override fun onDurationChange(duration: TimeRangePicker.TimeDuration) { 73 | // No action needed 74 | } 75 | }) 76 | 77 | // Show dialog 78 | return MaterialAlertDialogBuilder(requireContext()) 79 | .setView(dialogAddToCheatHoursBinding.root) 80 | .setCancelable(false) 81 | .setPositiveButton(getString(R.string.save)) { dialog, _ -> 82 | if (startTimeInMins == null || endTimeInMins == null) { 83 | Toast.makeText( 84 | requireContext(), 85 | getString(R.string.please_specify_time), 86 | Toast.LENGTH_SHORT 87 | ).show() 88 | return@setPositiveButton 89 | } 90 | savedPreferencesLoader!!.saveCheatHoursForViewBlocker( 91 | startTimeInMins!!, 92 | endTimeInMins!! 93 | ) 94 | sendRefreshRequest(ViewBlockerService.INTENT_ACTION_REFRESH_VIEW_BLOCKER) 95 | dialog.dismiss() 96 | } 97 | .setNegativeButton(getString(R.string.cancel)) { dialog, _ -> 98 | dialog.dismiss() 99 | } 100 | .create() 101 | } 102 | } 103 | 104 | -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/fragments/anti_uninstall/ChooseModeFragment.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.fragments.anti_uninstall 2 | 3 | import android.os.Bundle 4 | import android.view.LayoutInflater 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import androidx.fragment.app.Fragment 8 | import nethical.digipaws.R 9 | import nethical.digipaws.databinding.FragmentChoseAntiUninstallModeBinding 10 | 11 | class ChooseModeFragment : Fragment() { 12 | 13 | companion object { 14 | const val FRAGMENT_ID = "choose_anti_uninstall_mode" 15 | } 16 | private var _binding: FragmentChoseAntiUninstallModeBinding? = null 17 | private val binding get() = _binding!! // Safe getter for binding 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, 21 | container: ViewGroup?, 22 | savedInstanceState: Bundle? 23 | ): View { 24 | _binding = FragmentChoseAntiUninstallModeBinding.inflate(inflater, container, false) 25 | return binding.root 26 | } 27 | 28 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 29 | super.onViewCreated(view, savedInstanceState) 30 | binding.btnNext.setOnClickListener { 31 | when (binding.radioGroup.checkedRadioButtonId) { 32 | binding.passMode.id -> { 33 | requireActivity().supportFragmentManager.beginTransaction() 34 | .replace( 35 | R.id.fragment_holder, 36 | SetupPasswordModeFragment() 37 | ) // Replace with FragmentB 38 | .addToBackStack(null) 39 | .commit() 40 | } 41 | 42 | binding.timedMode.id -> { 43 | requireActivity().supportFragmentManager.beginTransaction() 44 | .replace( 45 | R.id.fragment_holder, 46 | SetupTimedModeFragment() 47 | ) // Replace with FragmentB 48 | .addToBackStack(null) 49 | .commit() 50 | } 51 | } 52 | } 53 | } 54 | 55 | override fun onDestroyView() { 56 | super.onDestroyView() 57 | _binding = null 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/fragments/anti_uninstall/SetupPasswordModeFragment.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.fragments.anti_uninstall 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.fragment.app.Fragment 10 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 11 | import nethical.digipaws.Constants 12 | import nethical.digipaws.R 13 | import nethical.digipaws.databinding.FragmentSetupPasswordModeBinding 14 | import nethical.digipaws.services.GeneralFeaturesService 15 | 16 | class SetupPasswordModeFragment : Fragment() { 17 | 18 | private var _binding: FragmentSetupPasswordModeBinding? = null 19 | private val binding get() = _binding!! // Safe getter for binding 20 | 21 | override fun onCreateView( 22 | inflater: LayoutInflater, 23 | container: ViewGroup?, 24 | savedInstanceState: Bundle? 25 | ): View { 26 | _binding = FragmentSetupPasswordModeBinding.inflate(inflater, container, false) 27 | return binding.root 28 | } 29 | 30 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 31 | super.onViewCreated(view, savedInstanceState) 32 | 33 | binding.btnNextPass.setOnClickListener { 34 | MaterialAlertDialogBuilder(requireContext()) 35 | .setTitle(getString(R.string.alert)) 36 | .setMessage(getString(R.string.are_you_sure_you_want_to_turn_on_anti_uninstall_there_is_no_turning_back)) 37 | 38 | .setPositiveButton(getString(R.string.i_understand)) { _, _ -> 39 | setupPasswordMode() 40 | } 41 | .setNegativeButton(getString(R.string.cancel)) { _, dialog -> 42 | requireActivity().finish() 43 | } 44 | .show() 45 | } 46 | 47 | binding.cbBlockChanges.setOnCheckedChangeListener { _, isChecked -> 48 | if (isChecked) { 49 | MaterialAlertDialogBuilder(requireContext()) 50 | .setTitle(getString(R.string.alert)) 51 | .setMessage(getString(R.string.if_you_enable_this_you_won_t_be_able_to_change_configurations_such_as_adding_blocked_apps_keywords_and_more)) 52 | .setPositiveButton(getString(R.string.i_understand), null) 53 | .show() 54 | } 55 | } 56 | 57 | } 58 | 59 | private fun setupPasswordMode() { 60 | val editor = 61 | activity?.getSharedPreferences("anti_uninstall", Context.MODE_PRIVATE)?.edit() 62 | editor?.apply() { 63 | putBoolean("is_anti_uninstall_on", true) 64 | putString("password", binding.password.text.toString()) 65 | putInt("mode", Constants.ANTI_UNINSTALL_PASSWORD_MODE) 66 | putBoolean("is_configuring_blocked", binding.cbBlockChanges.isChecked) 67 | commit() 68 | } 69 | 70 | val intent = Intent(GeneralFeaturesService.INTENT_ACTION_REFRESH_ANTI_UNINSTALL) 71 | activity?.sendBroadcast(intent) 72 | 73 | activity?.finish() 74 | } 75 | 76 | override fun onDestroyView() { 77 | super.onDestroyView() 78 | _binding = null 79 | } 80 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/fragments/anti_uninstall/SetupTimedModeFragment.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.fragments.anti_uninstall 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.os.Bundle 6 | import android.view.LayoutInflater 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import androidx.fragment.app.Fragment 10 | import com.google.android.material.dialog.MaterialAlertDialogBuilder 11 | import nethical.digipaws.Constants 12 | import nethical.digipaws.R 13 | import nethical.digipaws.databinding.FragmentSetupTimedModeBinding 14 | import nethical.digipaws.services.GeneralFeaturesService 15 | 16 | class SetupTimedModeFragment : Fragment() { 17 | 18 | private var _binding: FragmentSetupTimedModeBinding? = null 19 | private val binding get() = _binding!! // Safe getter for binding 20 | 21 | override fun onCreateView( 22 | inflater: LayoutInflater, 23 | container: ViewGroup?, 24 | savedInstanceState: Bundle? 25 | ): View { 26 | _binding = FragmentSetupTimedModeBinding.inflate(inflater, container, false) 27 | return binding.root 28 | } 29 | 30 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 31 | super.onViewCreated(view, savedInstanceState) 32 | binding.calendarView.minDate = binding.calendarView.date 33 | var selectedDate: String? = null 34 | binding.calendarView.setOnDateChangeListener { _, year, month, dayOfMonth -> 35 | selectedDate = "${month + 1}/$dayOfMonth/$year" 36 | } 37 | binding.turnOnTimed.setOnClickListener { 38 | 39 | MaterialAlertDialogBuilder(requireContext()) 40 | .setTitle(getString(R.string.alert)) 41 | .setMessage(getString(R.string.are_you_sure_you_want_to_turn_on_anti_uninstall_there_is_no_turning_back)) 42 | .setPositiveButton(getString(R.string.i_understand)) { _, dialog -> 43 | selectedDate?.let { it1 -> turnOnTimedMode(it1) } 44 | } 45 | .setNegativeButton(getString(R.string.cancel)) { _, dialog -> 46 | requireActivity().finish() 47 | } 48 | .show() 49 | } 50 | binding.blockChanges.setOnCheckedChangeListener { _, isChecked -> 51 | if (isChecked) { 52 | MaterialAlertDialogBuilder(requireContext()) 53 | .setTitle(getString(R.string.alert)) 54 | .setMessage(getString(R.string.if_you_enable_this_you_won_t_be_able_to_change_configurations_such_as_adding_blocked_apps_keywords_and_more)) 55 | .setPositiveButton(getString(R.string.i_understand), null) 56 | .show() 57 | } 58 | } 59 | 60 | } 61 | 62 | private fun turnOnTimedMode(selectedDate: String) { 63 | 64 | val editor = 65 | activity?.getSharedPreferences("anti_uninstall", Context.MODE_PRIVATE)?.edit() 66 | editor?.apply() { 67 | putBoolean("is_anti_uninstall_on", true) 68 | putString("date", selectedDate) 69 | putBoolean("is_configuring_blocked", binding.blockChanges.isChecked) 70 | putInt("mode", Constants.ANTI_UNINSTALL_TIMED_MODE) 71 | commit() 72 | } 73 | 74 | val intent = Intent(GeneralFeaturesService.INTENT_ACTION_REFRESH_ANTI_UNINSTALL) 75 | activity?.sendBroadcast(intent) 76 | 77 | activity?.finish() 78 | } 79 | 80 | override fun onDestroyView() { 81 | super.onDestroyView() 82 | _binding = null 83 | } 84 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/fragments/installation/AccessibilityGuide.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.fragments.installation 2 | 3 | import android.annotation.SuppressLint 4 | import android.os.Bundle 5 | import android.view.LayoutInflater 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.fragment.app.Fragment 9 | import nethical.digipaws.databinding.FragmentAccessibilityGuideBinding 10 | 11 | class AccessibilityGuide : Fragment() { 12 | companion object { 13 | const val FRAGMENT_ID = "accessibility_guide_fragment" 14 | } 15 | 16 | private var _binding: FragmentAccessibilityGuideBinding? = null 17 | private val binding get() = _binding!! // Safe getter for binding 18 | 19 | override fun onCreateView( 20 | inflater: LayoutInflater, 21 | container: ViewGroup?, 22 | savedInstanceState: Bundle? 23 | ): View { 24 | _binding = FragmentAccessibilityGuideBinding.inflate(inflater, container, false) 25 | return binding.root 26 | } 27 | 28 | @SuppressLint("CommitPrefEdits") 29 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 30 | super.onViewCreated(view, savedInstanceState) 31 | binding.btnNext.setOnClickListener { 32 | requireActivity().finish() 33 | } 34 | } 35 | 36 | 37 | override fun onDestroyView() { 38 | super.onDestroyView() 39 | _binding = null 40 | } 41 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/fragments/installation/WelcomeFragment.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.fragments.installation 2 | 3 | import android.content.ActivityNotFoundException 4 | import android.content.Intent 5 | import android.net.Uri 6 | import android.os.Bundle 7 | import android.view.LayoutInflater 8 | import android.view.View 9 | import android.view.ViewGroup 10 | import android.widget.Toast 11 | import androidx.fragment.app.Fragment 12 | import nethical.digipaws.R 13 | import nethical.digipaws.databinding.FragmentWelcomeBinding 14 | 15 | class WelcomeFragment : Fragment() { 16 | 17 | companion object { 18 | const val FRAGMENT_ID = "welcome_fragment" 19 | } 20 | 21 | private var _binding: FragmentWelcomeBinding? = null 22 | private val binding get() = _binding!! // Safe getter for binding 23 | 24 | override fun onCreateView( 25 | inflater: LayoutInflater, 26 | container: ViewGroup?, 27 | savedInstanceState: Bundle? 28 | ): View { 29 | _binding = FragmentWelcomeBinding.inflate(inflater, container, false) 30 | return binding.root 31 | } 32 | 33 | override fun onViewCreated(view: View, savedInstanceState: Bundle?) { 34 | super.onViewCreated(view, savedInstanceState) 35 | 36 | binding.cbTos.setOnCheckedChangeListener { _, isChecked -> 37 | binding.btnNext.isEnabled = isChecked 38 | } 39 | binding.openTos.setOnClickListener { 40 | val intent = 41 | Intent(Intent.ACTION_VIEW, Uri.parse("https://digipaws.life/terms-and-conditions")) 42 | try { 43 | startActivity(intent) 44 | } catch (e: ActivityNotFoundException) { 45 | Toast.makeText( 46 | requireContext(), 47 | "No application found to open the link", 48 | Toast.LENGTH_SHORT 49 | ).show() 50 | } 51 | } 52 | 53 | binding.btnNext.setOnClickListener { 54 | requireActivity().supportFragmentManager.beginTransaction() 55 | .replace( 56 | R.id.fragment_holder, 57 | PermissionsFragment() 58 | ) // Replace with FragmentB 59 | .addToBackStack(null) 60 | .commit() 61 | } 62 | } 63 | 64 | 65 | override fun onDestroyView() { 66 | super.onDestroyView() 67 | _binding = null 68 | } 69 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/ui/overlay/UsageStatOverlayManager.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.ui.overlay 2 | 3 | import android.annotation.SuppressLint 4 | import android.content.Context 5 | import android.graphics.PixelFormat 6 | import android.os.CountDownTimer 7 | import android.util.Log 8 | import android.view.Gravity 9 | import android.view.LayoutInflater 10 | import android.view.View 11 | import android.view.WindowManager 12 | import android.view.WindowManager.LayoutParams 13 | import nethical.digipaws.databinding.OverlayUsageStatBinding 14 | 15 | class UsageStatOverlayManager(private val context: Context) { 16 | 17 | private var overlayView: View? = null 18 | var binding: OverlayUsageStatBinding? = null 19 | var isOverlayVisible = false 20 | private var windowManager: WindowManager? = null 21 | 22 | var reelsScrolledThisSession = 0 23 | 24 | @SuppressLint("InlinedApi") 25 | fun startDisplaying() { 26 | if (overlayView != null || isOverlayVisible) return 27 | 28 | binding = OverlayUsageStatBinding.inflate(LayoutInflater.from(context)) 29 | isOverlayVisible = true 30 | overlayView = binding?.root 31 | 32 | // Set up WindowManager.LayoutParams for the overlay 33 | val layoutParams = LayoutParams( 34 | LayoutParams.MATCH_PARENT, 35 | LayoutParams.MATCH_PARENT, 36 | LayoutParams.TYPE_ACCESSIBILITY_OVERLAY, 37 | LayoutParams.FLAG_NOT_FOCUSABLE or 38 | LayoutParams.FLAG_NOT_TOUCHABLE or 39 | LayoutParams.FLAG_LAYOUT_IN_SCREEN 40 | or LayoutParams.FLAG_LAYOUT_NO_LIMITS, 41 | PixelFormat.TRANSLUCENT 42 | ) 43 | layoutParams.gravity = Gravity.CENTER 44 | layoutParams.layoutInDisplayCutoutMode = LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES 45 | 46 | windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager 47 | 48 | windowManager?.addView(overlayView, layoutParams) 49 | } 50 | 51 | fun removeOverlay() { 52 | if (overlayView != null && windowManager != null) { 53 | Log.d("UsageStatOverlayManager", "Removing overlay.") 54 | windowManager?.removeView(overlayView) 55 | overlayView = null 56 | binding = null 57 | isOverlayVisible = false 58 | } else { 59 | Log.d("UsageStatOverlayManager", "No overlay to remove.") 60 | } 61 | } 62 | 63 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/utils/AnimTools.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.utils 2 | 3 | import android.view.View 4 | 5 | class AnimTools { 6 | companion object { 7 | fun View.animateVisibility(show: Boolean, duration: Long = 300) { 8 | if (show) { 9 | this.apply { 10 | alpha = 0f 11 | visibility = View.VISIBLE 12 | animate().alpha(1f).setDuration(duration).start() 13 | } 14 | } else { 15 | this.animate() 16 | .alpha(0f) 17 | .setDuration(duration) 18 | .withEndAction { visibility = View.GONE } 19 | .start() 20 | } 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/utils/CommonUtils.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.utils 2 | 3 | import android.content.Context 4 | import android.content.Intent 5 | import android.content.pm.PackageManager 6 | import android.view.inputmethod.InputMethodManager 7 | 8 | 9 | fun getDefaultLauncherPackageName(packageManager: PackageManager): String? { 10 | val intent = Intent(Intent.ACTION_MAIN).apply { 11 | addCategory(Intent.CATEGORY_HOME) 12 | } 13 | 14 | val resolveInfo = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) 15 | return resolveInfo?.activityInfo?.packageName 16 | } 17 | 18 | 19 | fun getCurrentKeyboardPackageName(context: Context): String? { 20 | context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager 21 | val currentInputMethodId = android.provider.Settings.Secure.getString( 22 | context.contentResolver, 23 | android.provider.Settings.Secure.DEFAULT_INPUT_METHOD 24 | ) 25 | return currentInputMethodId?.substringBefore('/') 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/utils/MonochromeTools.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.utils 2 | 3 | import android.util.Log 4 | 5 | class GrayscaleControl { 6 | private val commandListener = object : ShizukuRunner.CommandResultListener { 7 | override fun onCommandResult(output: String, done: Boolean) { 8 | Log.d("monochrome output: ",output) 9 | // Handle successful command execution if needed 10 | } 11 | 12 | override fun onCommandError(error: String) { 13 | // Handle command errors if needed 14 | 15 | Log.d("monochrome error: ",error) 16 | } 17 | } 18 | 19 | /** 20 | * Enable grayscale mode 21 | */ 22 | fun enableGrayscale() { 23 | ShizukuRunner.executeCommand( 24 | "settings put secure accessibility_display_daltonizer 0 && " + 25 | "settings put secure accessibility_display_daltonizer_enabled 1", 26 | commandListener 27 | ) 28 | } 29 | 30 | /** 31 | * Disable grayscale mode 32 | */ 33 | fun disableGrayscale() { 34 | ShizukuRunner.executeCommand( 35 | "settings put secure accessibility_display_daltonizer_enabled 0", 36 | commandListener 37 | ) 38 | } 39 | 40 | /** 41 | * Toggle grayscale mode based on current state 42 | */ 43 | fun toggleGrayscale() { 44 | ShizukuRunner.executeCommand( 45 | "if [ $(settings get secure accessibility_display_daltonizer_enabled) = \"1\" ]; then " + 46 | "settings put secure accessibility_display_daltonizer_enabled 0; " + 47 | "else " + 48 | "settings put secure accessibility_display_daltonizer 0 && " + 49 | "settings put secure accessibility_display_daltonizer_enabled 1; " + 50 | "fi", 51 | commandListener 52 | ) 53 | } 54 | 55 | /** 56 | * Get current grayscale state 57 | * @param callback Lambda that receives the current state (true if enabled, false if disabled) 58 | */ 59 | fun isGrayscaleEnabled(callback: (Boolean) -> Unit) { 60 | ShizukuRunner.executeCommand( 61 | "settings get secure accessibility_display_daltonizer_enabled", 62 | object : ShizukuRunner.CommandResultListener { 63 | override fun onCommandResult(output: String, done: Boolean) { 64 | if (done) { 65 | callback(output.trim() == "1") 66 | } 67 | } 68 | 69 | override fun onCommandError(error: String) { 70 | callback(false) 71 | } 72 | } 73 | ) 74 | } 75 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/utils/NotificationTimerManager.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.utils 2 | 3 | 4 | import android.app.NotificationChannel 5 | import android.app.NotificationManager 6 | import android.content.Context 7 | import android.os.CountDownTimer 8 | import androidx.core.app.NotificationCompat 9 | 10 | class NotificationTimerManager(private val context: Context) { 11 | 12 | companion object { 13 | private const val CHANNEL_ID = "TimerNotificationChannel" 14 | private const val NOTIFICATION_ID = 1001 15 | } 16 | 17 | private var countDownTimer: CountDownTimer? = null 18 | private val notificationManager: NotificationManager by lazy { 19 | context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager 20 | } 21 | 22 | init { 23 | createNotificationChannel() 24 | } 25 | 26 | private fun createNotificationChannel() { 27 | val channel = NotificationChannel( 28 | CHANNEL_ID, 29 | "Timer Notifications", 30 | NotificationManager.IMPORTANCE_LOW 31 | ).apply { 32 | description = "Timer progress notifications" 33 | setSound(null, null) 34 | enableVibration(false) 35 | } 36 | notificationManager.createNotificationChannel(channel) 37 | } 38 | 39 | fun startTimer( 40 | totalMillis: Long, 41 | isCountdown: Boolean = true, 42 | onTickCallback: ((Long) -> Unit)? = null, 43 | onFinishCallback: (() -> Unit)? = null 44 | ) { 45 | // Cancel any existing timer 46 | countDownTimer?.cancel() 47 | 48 | countDownTimer = object : CountDownTimer(totalMillis, 1000) { 49 | override fun onTick(millisUntilFinished: Long) { 50 | val displayMillis = 51 | if (isCountdown) millisUntilFinished else totalMillis - millisUntilFinished 52 | 53 | // Update notification with current timer value 54 | updateTimerNotification(displayMillis) 55 | 56 | // Optional tick callback 57 | onTickCallback?.invoke(displayMillis) 58 | } 59 | 60 | override fun onFinish() { 61 | // Remove the notification when timer completes 62 | notificationManager.cancel(NOTIFICATION_ID) 63 | 64 | // Optional finish callback 65 | onFinishCallback?.invoke() 66 | } 67 | }.start() 68 | } 69 | 70 | private fun updateTimerNotification(remainingMillis: Long) { 71 | // Convert milliseconds to hours, minutes, seconds 72 | val hours = remainingMillis / 3600000 73 | val minutes = (remainingMillis % 3600000) / 60000 74 | val seconds = (remainingMillis % 60000) / 1000 75 | 76 | // Format time display 77 | val timeString = String.format("%02d:%02d:%02d", hours, minutes, seconds) 78 | 79 | val notification = NotificationCompat.Builder(context, CHANNEL_ID) 80 | .setContentTitle("Timer") 81 | .setContentText(timeString) 82 | .setSmallIcon(android.R.drawable.ic_dialog_info) 83 | .setPriority(NotificationCompat.PRIORITY_LOW) 84 | .setOngoing(true) // Makes notification persistent 85 | .setCategory(NotificationCompat.CATEGORY_PROGRESS) 86 | .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) 87 | .build() 88 | 89 | notificationManager.notify(NOTIFICATION_ID, notification) 90 | } 91 | 92 | fun stopTimer() { 93 | countDownTimer?.cancel() 94 | notificationManager.cancel(NOTIFICATION_ID) 95 | } 96 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/utils/ShizukuRunner.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.utils 2 | import moe.shizuku.server.IShizukuService 3 | import rikka.shizuku.Shizuku 4 | import java.io.BufferedReader 5 | import java.io.FileInputStream 6 | import java.io.InputStreamReader 7 | 8 | /** 9 | * Utility class for executing shell commands using Shizuku 10 | */ 11 | class ShizukuRunner { 12 | 13 | interface CommandResultListener { 14 | /** 15 | * Called when the command produces output. 16 | * @param output The output from the command execution 17 | * @param done True if the command execution is complete, false otherwise 18 | */ 19 | fun onCommandResult(output: String, done: Boolean) {} 20 | 21 | /** 22 | * Called when an error occurs during command execution. 23 | * @param error The error message 24 | */ 25 | fun onCommandError(error: String) {} 26 | } 27 | 28 | companion object { 29 | 30 | /** 31 | * Executes a shell command using Shizuku. 32 | * @param command The shell command to execute 33 | * @param listener Listener for command results and errors 34 | * @param lineBundle Number of lines to batch before invoking the listener 35 | */ 36 | fun executeCommand(command: String, listener: CommandResultListener, lineBundle: Int = 50) { 37 | Thread { 38 | try { 39 | // Initialize the Shizuku process 40 | val process = IShizukuService.Stub.asInterface(Shizuku.getBinder()) 41 | .newProcess(arrayOf("sh", "-c", command), null, null) 42 | 43 | // Readers for command output and error streams 44 | val outputReader = BufferedReader(InputStreamReader(FileInputStream(process.inputStream.fileDescriptor))) 45 | val errorReader = BufferedReader(InputStreamReader(FileInputStream(process.errorStream.fileDescriptor))) 46 | 47 | // StringBuilders for capturing output and errors 48 | val outputBuffer = StringBuilder() 49 | val errorBuffer = StringBuilder() 50 | 51 | var line: String? 52 | var lineCount = 0 53 | 54 | // Read output stream 55 | while (outputReader.readLine().also { line = it } != null) { 56 | lineCount++ 57 | outputBuffer.append(line).append("\n") 58 | 59 | // Send partial results if lineBundle is reached 60 | if (lineCount == lineBundle) { 61 | lineCount = 0 62 | listener.onCommandResult(outputBuffer.toString(), false) 63 | outputBuffer.clear() 64 | } 65 | } 66 | 67 | // Read error stream 68 | while (errorReader.readLine().also { line = it } != null) { 69 | errorBuffer.append(line).append("\n") 70 | } 71 | 72 | // Notify listener of results or errors 73 | if (errorBuffer.isNotBlank()) { 74 | listener.onCommandError(errorBuffer.toString()) 75 | } else { 76 | listener.onCommandResult(outputBuffer.toString(), true) 77 | } 78 | 79 | // Wait for process to complete 80 | process.waitFor() 81 | 82 | } catch (e: Exception) { 83 | // Notify listener of exceptions 84 | listener.onCommandError(e.message ?: "An unexpected error occurred while executing the command.") 85 | } 86 | }.start() 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/utils/TimeTools.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.utils 2 | 3 | import java.text.SimpleDateFormat 4 | import java.time.LocalDate 5 | import java.time.LocalTime 6 | import java.time.format.DateTimeFormatter 7 | import java.util.Date 8 | import java.util.Locale 9 | 10 | class TimeTools { 11 | companion object { 12 | fun convertToMinutesFromMidnight(hour: Int, minute: Int): Int { 13 | return (hour * 60) + minute 14 | } 15 | 16 | fun convertMinutesTo24Hour(minutes: Int): Pair { 17 | return Pair(minutes / 60, minutes % 60) 18 | } 19 | 20 | fun getCurrentDate(): String { 21 | val currentDate = LocalDate.now() 22 | 23 | val formatter = DateTimeFormatter.ofPattern("dd MMMM yyyy") 24 | return currentDate.format(formatter) 25 | } 26 | fun getPreviousDate(daysAgo:Long = 1): String { 27 | val previousDate = LocalDate.now().minusDays(daysAgo) 28 | 29 | val formatter = DateTimeFormatter.ofPattern("dd MMMM yyyy") 30 | return previousDate.format(formatter) 31 | } 32 | 33 | 34 | fun getCurrentTime(): String { 35 | val currentTime = LocalTime.now() 36 | 37 | val formatter = DateTimeFormatter.ofPattern("HH:mm:ss") 38 | 39 | return currentTime.format(formatter) 40 | } 41 | 42 | fun shortenDate(dateString: String): String { 43 | val parts = dateString.split(" ") 44 | 45 | if (parts.size >= 2) { 46 | val day = parts[0] 47 | val month = parts[1].take(3) 48 | return "$day $month" 49 | } 50 | 51 | return dateString 52 | } 53 | 54 | fun formatDate(timestamp: Long): String { 55 | val dateFormat = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()) 56 | return dateFormat.format(Date(timestamp)) 57 | } 58 | 59 | 60 | 61 | fun formatTime(timeInMillis: Long, showSeconds: Boolean = true): String { 62 | val hours = timeInMillis / (1000 * 60 * 60) 63 | val minutes = (timeInMillis % (1000 * 60 * 60)) / (1000 * 60) 64 | val seconds = (timeInMillis % (1000 * 60)) / 1000 65 | 66 | return buildString { 67 | if (hours > 0) append("$hours hr") 68 | if (minutes > 0) append(" $minutes mins") 69 | if (showSeconds && seconds > 0) append(" $seconds secs") 70 | }.trim() 71 | } 72 | 73 | fun formatTimeForWidget(timeInMillis: Long): String { 74 | val hours = timeInMillis / (1000 * 60 * 60) 75 | val minutes = (timeInMillis % (1000 * 60 * 60)) / (1000 * 60) 76 | 77 | return buildString { 78 | if (hours > 0) append("${hours}h") 79 | if (minutes > 0L) append("${minutes}m") 80 | if (hours == 0L && minutes == 0L) append("<1m") // Handle case for less than 1 minute 81 | }.trim() 82 | } 83 | 84 | 85 | } 86 | } -------------------------------------------------------------------------------- /app/src/main/java/nethical/digipaws/views/CustomMarkerView.kt: -------------------------------------------------------------------------------- 1 | package nethical.digipaws.views 2 | 3 | import android.content.Context 4 | import android.widget.TextView 5 | import com.github.mikephil.charting.components.MarkerView 6 | import com.github.mikephil.charting.data.Entry 7 | import com.github.mikephil.charting.highlight.Highlight 8 | import com.github.mikephil.charting.utils.MPPointF 9 | import nethical.digipaws.R 10 | 11 | class CustomMarkerView(context: Context, layoutResource: Int) : 12 | MarkerView(context, layoutResource) { 13 | 14 | private val tvContent: TextView = findViewById(R.id.tvContent) 15 | 16 | var showDecimal = true 17 | override fun refreshContent(e: Entry?, highlight: Highlight?) { 18 | e?.y?.let { value -> 19 | tvContent.text = if (showDecimal) { 20 | "%.2f".format(value) // Efficient decimal formatting 21 | } else { 22 | value.toInt().toString() // Convert to integer for non-decimal 23 | } 24 | } 25 | 26 | super.refreshContent(e, highlight) 27 | } 28 | 29 | override fun getOffset(): MPPointF { 30 | // Adjust marker position 31 | return MPPointF(-(width / 2).toFloat(), -height.toFloat()) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/res/anim/fade_in.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/anim/fade_out.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_add_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_android_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_app_shortcut_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_auto_fix_high_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_close_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_date_range_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_delete_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_done_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_edit_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_help_outline_24.xml: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_info_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_lock_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_query_stats_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_read_more_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_refresh_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_remove_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_share_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_start_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_stop_24.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_task_alt_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/baseline_warning_24.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/focus_mode_icon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nethical6/digipaws/3e57d27af103a2421ec70d3f96da212acaaec47b/app/src/main/res/drawable/focus_mode_icon.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 8 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 8 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_monochrome.xml: -------------------------------------------------------------------------------- 1 | 8 | 13 | 18 | 19 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/widget_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_add_timed_action_activity.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 17 | 18 | 28 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_fragment.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 17 | 18 | 27 | 28 | 36 | 37 |