Repository: GustavoASantos/Noti Branch: main Commit: 7f4c1fbac4e0 Files: 136 Total size: 337.8 KB Directory structure: gitextract_91b8wrhl/ ├── .gitignore ├── README.md ├── app/ │ ├── .gitignore │ ├── build.gradle │ ├── proguard-rules.pro │ └── src/ │ └── main/ │ ├── AndroidManifest.xml │ ├── java/ │ │ └── com/ │ │ └── gustavoas/ │ │ └── noti/ │ │ ├── ProgressBarAppsAdapter.kt │ │ ├── ProgressBarAppsRepository.kt │ │ ├── SettingsActivity.kt │ │ ├── Utils.kt │ │ ├── fragments/ │ │ │ ├── BasePreferenceFragment.kt │ │ │ ├── CircularBarFragment.kt │ │ │ ├── LinearBarFragment.kt │ │ │ ├── PerAppSettingsFragment.kt │ │ │ └── SettingsFragment.kt │ │ ├── model/ │ │ │ ├── DeviceConfiguration.kt │ │ │ ├── ProgressBarApp.kt │ │ │ └── ProgressNotification.kt │ │ ├── notifications/ │ │ │ ├── DownloadProgressBar.kt │ │ │ ├── GoogleTimerProgressBar.kt │ │ │ ├── MediaProgressBar.kt │ │ │ ├── PercentageProgressBar.kt │ │ │ ├── ProgressBarNotification.kt │ │ │ └── TimedProgressBar.kt │ │ ├── preferences/ │ │ │ ├── BannerPreference.kt │ │ │ ├── BarStylesListPreference.kt │ │ │ └── SeekBarPreference.kt │ │ └── services/ │ │ ├── AccessibilityService.kt │ │ ├── FullscreenDetectionService.kt │ │ └── NotificationListenerService.kt │ └── res/ │ ├── color/ │ │ └── outlined_button_selector.xml │ ├── drawable/ │ │ ├── circular_mask.xml │ │ ├── ic_accessibility.xml │ │ ├── ic_apps.xml │ │ ├── ic_bug_report.xml │ │ ├── ic_calentile.xml │ │ ├── ic_circular_progress_bar.xml │ │ ├── ic_close.xml │ │ ├── ic_colors.xml │ │ ├── ic_contrast.xml │ │ ├── ic_download.xml │ │ ├── ic_download_filled.xml │ │ ├── ic_fullscreen.xml │ │ ├── ic_height.xml │ │ ├── ic_horizontal.xml │ │ ├── ic_info.xml │ │ ├── ic_launcher_foreground.xml │ │ ├── ic_linear_progress_bar.xml │ │ ├── ic_lockscreen.xml │ │ ├── ic_margin_top.xml │ │ ├── ic_music.xml │ │ ├── ic_music_filled.xml │ │ ├── ic_notification.xml │ │ ├── ic_notification_bar.xml │ │ ├── ic_overlay.xml │ │ ├── ic_palette.xml │ │ ├── ic_preview.xml │ │ ├── ic_progress_bar_style.xml │ │ ├── ic_rounded_corners.xml │ │ ├── ic_share.xml │ │ ├── ic_size.xml │ │ ├── ic_sumup.xml │ │ ├── layout_bg.xml │ │ ├── selector_download.xml │ │ ├── selector_music.xml │ │ └── splashscreen.xml │ ├── drawable-v31/ │ │ └── splashscreen.xml │ ├── layout/ │ │ ├── activity_main.xml │ │ ├── advanced_style_dialog.xml │ │ ├── app_item.xml │ │ ├── banner_preference.xml │ │ ├── button_toggle_group.xml │ │ ├── fragment_per_app_settings.xml │ │ ├── material_switch.xml │ │ ├── per_app_settings_footer.xml │ │ ├── progress_bar.xml │ │ └── seekbar_preference.xml │ ├── layout-land/ │ │ └── button_toggle_group.xml │ ├── menu/ │ │ └── settings_menu.xml │ ├── mipmap-anydpi-v26/ │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ ├── resources.properties │ ├── values/ │ │ ├── arrays.xml │ │ ├── attrs.xml │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── themes.xml │ ├── values-ar/ │ │ └── strings.xml │ ├── values-ar-rSA/ │ │ └── strings.xml │ ├── values-bg/ │ │ └── strings.xml │ ├── values-cs/ │ │ └── strings.xml │ ├── values-de/ │ │ └── strings.xml │ ├── values-es/ │ │ └── strings.xml │ ├── values-es-rMX/ │ │ └── strings.xml │ ├── values-fr/ │ │ └── strings.xml │ ├── values-fr-rCA/ │ │ └── strings.xml │ ├── values-hu/ │ │ └── strings.xml │ ├── values-in/ │ │ └── strings.xml │ ├── values-it/ │ │ └── strings.xml │ ├── values-iw/ │ │ └── strings.xml │ ├── values-ja/ │ │ └── strings.xml │ ├── values-ka/ │ │ └── strings.xml │ ├── values-lt/ │ │ └── strings.xml │ ├── values-ml/ │ │ └── strings.xml │ ├── values-nb-rNO/ │ │ └── strings.xml │ ├── values-night/ │ │ ├── colors.xml │ │ └── themes.xml │ ├── values-pl/ │ │ └── strings.xml │ ├── values-pt-rPT/ │ │ └── strings.xml │ ├── values-ro/ │ │ └── strings.xml │ ├── values-ru/ │ │ └── strings.xml │ ├── values-sk/ │ │ └── strings.xml │ ├── values-sl/ │ │ └── strings.xml │ ├── values-ta/ │ │ └── strings.xml │ ├── values-tr/ │ │ └── strings.xml │ ├── values-uk/ │ │ └── strings.xml │ ├── values-v31/ │ │ └── arrays.xml │ ├── values-vi/ │ │ └── strings.xml │ ├── values-zh-rCN/ │ │ └── strings.xml │ ├── values-zh-rTW/ │ │ └── strings.xml │ └── xml/ │ ├── accessibility_service_config.xml │ ├── backup_rules.xml │ ├── circular_bar_preferences.xml │ ├── data_extraction_rules.xml │ ├── linear_bar_preferences.xml │ └── preferences.xml ├── build.gradle ├── gradle/ │ └── wrapper/ │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── gradlew ├── gradlew.bat ├── renovate.json └── settings.gradle ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ *.iml .gradle /local.properties /.idea .DS_Store /build /captures .externalNativeBuild .cxx local.properties ================================================ FILE: README.md ================================================ # Noti Progress Bar [banner](https://play.google.com/store/apps/details?id=com.gustavoas.noti) > Noti lets you easily track the progress of downloads, music and more directly from your status bar. ## ✨ Features + Track downloads, music, and timers from Google Clock. + Customizable progress bar or progress circle. + Adjustable progress circle to fit your phone's hole punch camera. + Automatically hides when in full screen mode. + Display Noti in the lockscreen by enabling the accessibility service. + Filter which apps you want to track. + Customize Noti on a per-app basis. ## ⬇️ Download [Get it on Google Play](https://play.google.com/store/apps/details?id=com.gustavoas.noti) ## 💜 Help Translate Translation status ## 🔑 License The source code for Noti is available under the GPL-3.0 License. However, distributing the compiled application anywhere, including on Google Play, requires explicit written permission from the original author. ================================================ FILE: app/.gitignore ================================================ /build /release/ /src/debug/ /google-services.json ================================================ FILE: app/build.gradle ================================================ plugins { id 'com.android.application' id 'org.jetbrains.kotlin.android' id 'com.google.gms.google-services' id 'kotlin-parcelize' } android { namespace 'com.gustavoas.noti' compileSdk 35 defaultConfig { applicationId "com.gustavoas.noti" minSdk 21 targetSdk 34 versionCode 90 versionName "2.0" versionCode 95 versionName "2.2" vectorDrawables { useSupportLibrary true } } buildTypes { release { minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } debug { applicationIdSuffix ".dogfood" } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = '1.8' } packagingOptions { resources { excludes += '/META-INF/{AL2.0,LGPL2.1}' } } androidResources { generateLocaleConfig true } } dependencies { implementation platform('com.google.firebase:firebase-bom:33.13.0') implementation 'androidx.core:core-ktx:1.16.0' implementation 'androidx.appcompat:appcompat:1.7.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.1' implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.palette:palette-ktx:1.0.0' implementation 'androidx.preference:preference-ktx:1.2.1' implementation 'androidx.core:core-splashscreen:1.0.1' implementation 'androidx.recyclerview:recyclerview:1.4.0' implementation 'com.github.kizitonwose.colorpreference:support:1.1.0' implementation 'com.github.kizitonwose.colorpreference:core:1.1.0' implementation 'com.github.eltos:simpledialogfragments:v3.7' implementation 'com.google.firebase:firebase-storage' } ================================================ FILE: app/proguard-rules.pro ================================================ # Add project specific ProGuard rules here. # You can control the set of applied configuration files using the # proguardFiles setting in build.gradle. # # For more details, see # http://developer.android.com/guide/developing/tools/proguard.html # If your project uses WebView with JS, uncomment the following # and specify the fully qualified class name to the JavaScript interface # class: #-keepclassmembers class fqcn.of.javascript.interface.for.webview { # public *; #} # Uncomment this to preserve the line number information for # debugging stack traces. #-keepattributes SourceFile,LineNumberTable # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile ================================================ FILE: app/src/main/AndroidManifest.xml ================================================ ================================================ FILE: app/src/main/java/com/gustavoas/noti/ProgressBarAppsAdapter.kt ================================================ package com.gustavoas.noti import android.content.Context import android.content.Intent import android.graphics.Color import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.CheckBox import android.widget.LinearLayout import android.widget.TextView import androidx.core.content.edit import androidx.core.graphics.drawable.toDrawable import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButtonToggleGroup import com.gustavoas.noti.Utils.dpToPx import com.gustavoas.noti.Utils.getApplicationIcon import com.gustavoas.noti.Utils.getApplicationName import com.gustavoas.noti.Utils.getColorForApp import com.gustavoas.noti.Utils.showColorDialog import com.gustavoas.noti.model.ProgressBarApp import com.gustavoas.noti.services.AccessibilityService class ProgressBarAppsAdapter( private val fragment: Fragment, private val context: Context, private val apps: ArrayList, private val appsRepository: ProgressBarAppsRepository ) : RecyclerView.Adapter() { private val VIEW_TYPE_HEADER = 0 private val VIEW_TYPE_ITEM = 1 private val VIEW_TYPE_FOOTER = 2 override fun getItemViewType(position: Int): Int { return when (position) { 0 -> VIEW_TYPE_HEADER itemCount - 1 -> VIEW_TYPE_FOOTER else -> VIEW_TYPE_ITEM } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { return when (viewType) { VIEW_TYPE_HEADER -> { val view = LayoutInflater.from(context).inflate(R.layout.button_toggle_group, parent, false) HeaderViewHolder(view) } VIEW_TYPE_FOOTER -> { val view = LayoutInflater.from(context).inflate(R.layout.per_app_settings_footer, parent, false) FooterViewHolder(view) } else -> { val view = LayoutInflater.from(context).inflate(R.layout.app_item, parent, false) ItemViewHolder(view) } } } override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { if (holder !is ItemViewHolder) return with(apps[position - 1]) { holder.appName.text = getApplicationName(context, packageName) ?: packageName val appIcon = getApplicationIcon(context, packageName) ?: Color.TRANSPARENT.toDrawable() val appIconSize = dpToPx(context, 36) appIcon.setBounds(0, 0, appIconSize, appIconSize) holder.appName.setCompoundDrawables(appIcon, null, null, null) holder.toggle.isChecked = showProgressBar holder.toggle.setOnCheckedChangeListener { _, isChecked -> showProgressBar = isChecked appsRepository.updateApp(this) if (!isChecked) { val intent = Intent(context, AccessibilityService::class.java) intent.putExtra("packageName", packageName) intent.putExtra("removal", true) context.startService(intent) } } holder.background.setOnClickListener { holder.toggle.toggle() } val barColor = getColorForApp(context, this) holder.colorPicker.setBackgroundColor(barColor) holder.colorPicker.setOnClickListener { showColorDialog( fragment, barColor, (position - 1).toString(), !useDefaultColor, ) } } } override fun onViewRecycled(holder: RecyclerView.ViewHolder) { super.onViewRecycled(holder) if (holder !is ItemViewHolder) return holder.toggle.setOnCheckedChangeListener(null) } override fun getItemCount(): Int = apps.size + 2 class HeaderViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { private val toggleGroup: MaterialButtonToggleGroup = itemView.findViewById(R.id.toggleGroup) init { val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(this.itemView.context) val enableDownloads = sharedPrefs.getBoolean("showForDownloads", true) val enableMedia = sharedPrefs.getBoolean("showForMedia", true) if (enableDownloads) { toggleGroup.check(R.id.toggleDownloads) } if (enableMedia) { toggleGroup.check(R.id.toggleMedia) } toggleGroup.addOnButtonCheckedListener { _, checkedId, isChecked -> when (checkedId) { R.id.toggleDownloads -> { sharedPrefs.edit { putBoolean("showForDownloads", isChecked) } } R.id.toggleMedia -> { sharedPrefs.edit { putBoolean("showForMedia", isChecked) } } } } } } class ItemViewHolder(view: View) : RecyclerView.ViewHolder(view) { val appName: TextView = view.findViewById(R.id.app_name) val toggle: CheckBox = view.findViewById(R.id.checkbox) val background: LinearLayout = view.findViewById(R.id.item_container) val colorPicker: Button = view.findViewById(R.id.color_picker) } class FooterViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) } ================================================ FILE: app/src/main/java/com/gustavoas/noti/ProgressBarAppsRepository.kt ================================================ package com.gustavoas.noti import android.content.Context import android.database.sqlite.SQLiteDatabase import android.database.sqlite.SQLiteOpenHelper import com.gustavoas.noti.model.ProgressBarApp class ProgressBarAppsRepository(context: Context) : SQLiteOpenHelper(context, DATABASE_NAME, null, 3) { companion object { const val DATABASE_NAME = "progressBarApps" const val TABLE_NAME = "apps" const val COLUMN_PACKAGE = "package_name" const val COLUMN_SHOW_PROGRESS = "show_progress" const val COLUMN_COLOR = "color" const val COLUMN_USE_DEFAULT = "default_color" const val COLUMN_USE_MATERIAL_YOU = "material_you_color" private val apps: MutableList = mutableListOf() } init { if (apps.isEmpty()) { apps.addAll(getAllApps()) } } override fun onCreate(db: SQLiteDatabase?) { db?.execSQL( "CREATE TABLE IF NOT EXISTS $TABLE_NAME (" + "$COLUMN_PACKAGE TEXT PRIMARY KEY," + "$COLUMN_SHOW_PROGRESS INTEGER NOT NULL DEFAULT 1," + "$COLUMN_COLOR INTEGER DEFAULT NULL," + "$COLUMN_USE_DEFAULT INTEGER NOT NULL DEFAULT 1," + "$COLUMN_USE_MATERIAL_YOU INTEGER NOT NULL DEFAULT 0" + ")" ) val knownApps = listOf( "com.google.android.deskclock", "com.android.chrome", "com.duckduckgo.mobile.android", "com.android.vending", "com.epicgames.portal", "code.name.monkey.retromusic", "com.google.android.apps.youtube.music", "com.spotify.music", ) knownApps.forEach { packageName -> db?.execSQL( "INSERT INTO $TABLE_NAME ($COLUMN_PACKAGE) VALUES ('$packageName')" ) } } override fun onUpgrade(db: SQLiteDatabase?, oldVersion: Int, newVersion: Int) { if (oldVersion < 2) { db?.execSQL("ALTER TABLE $TABLE_NAME ADD COLUMN $COLUMN_COLOR INTEGER NOT NULL DEFAULT 1") } if (oldVersion < 3) { db?.execSQL("ALTER TABLE $TABLE_NAME RENAME TO ${TABLE_NAME}_old") db?.execSQL( "CREATE TABLE $TABLE_NAME (" + "$COLUMN_PACKAGE TEXT PRIMARY KEY," + "$COLUMN_SHOW_PROGRESS INTEGER NOT NULL DEFAULT 1," + "$COLUMN_COLOR INTEGER DEFAULT NULL," + "$COLUMN_USE_DEFAULT INTEGER NOT NULL DEFAULT 1," + "$COLUMN_USE_MATERIAL_YOU INTEGER NOT NULL DEFAULT 0" + ")" ) db?.execSQL( "INSERT INTO $TABLE_NAME ($COLUMN_PACKAGE, $COLUMN_SHOW_PROGRESS, $COLUMN_COLOR) " + "SELECT $COLUMN_PACKAGE, $COLUMN_SHOW_PROGRESS, $COLUMN_COLOR FROM ${TABLE_NAME}_old" ) db?.execSQL("DROP TABLE ${TABLE_NAME}_old") db?.execSQL("UPDATE $TABLE_NAME SET " + "$COLUMN_USE_DEFAULT = CASE WHEN $COLUMN_COLOR = 1 THEN 1 ELSE 0 END, " + "$COLUMN_USE_MATERIAL_YOU = CASE WHEN $COLUMN_COLOR = 2 THEN 1 ELSE 0 END, " + "$COLUMN_COLOR = CASE WHEN $COLUMN_COLOR IN (1, 2) THEN NULL ELSE $COLUMN_COLOR END" ) } } fun addApp(app: ProgressBarApp): ProgressBarApp { writableDatabase.execSQL( "INSERT OR IGNORE INTO $TABLE_NAME ($COLUMN_PACKAGE) VALUES (?)", arrayOf(app.packageName) ) apps.firstOrNull { it.packageName == app.packageName }?.let { return it } apps.add(app) return app } fun updateApp(app: ProgressBarApp) { writableDatabase.execSQL( "UPDATE $TABLE_NAME " + "SET $COLUMN_SHOW_PROGRESS = ?, " + "$COLUMN_COLOR = ?, " + "$COLUMN_USE_DEFAULT = ?, " + "$COLUMN_USE_MATERIAL_YOU = ? " + "WHERE $COLUMN_PACKAGE = ?", arrayOf( if (app.showProgressBar) 1 else 0, app.color, if (app.useDefaultColor) 1 else 0, if (app.useMaterialYouColor) 1 else 0, app.packageName ) ) apps.indexOfFirst { it.packageName == app.packageName }.let { index -> if (index != -1) { apps[index] = app } } } fun showProgressForApp(packageName: String): Boolean? { apps.firstOrNull { it.packageName == packageName }?.let { return it.showProgressBar } readableDatabase.rawQuery( "SELECT $COLUMN_SHOW_PROGRESS FROM $TABLE_NAME WHERE $COLUMN_PACKAGE = ?", arrayOf(packageName) ).use { cursor -> if (cursor.moveToFirst()) { return cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_SHOW_PROGRESS)) == 1 } } return null } fun getApp(packageName: String): ProgressBarApp? { apps.firstOrNull { it.packageName == packageName }?.let { return it } readableDatabase.rawQuery( "SELECT * FROM $TABLE_NAME WHERE $COLUMN_PACKAGE = ?", arrayOf(packageName) ).use { cursor -> if (cursor.moveToFirst()) { val showProgress = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_SHOW_PROGRESS)) val colorIndex = cursor.getColumnIndexOrThrow(COLUMN_COLOR) val color = if (cursor.isNull(colorIndex)) { null } else { cursor.getInt(colorIndex) } val useDefault = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_USE_DEFAULT)) val useMaterialYou = cursor.getInt(cursor.getColumnIndexOrThrow(COLUMN_USE_MATERIAL_YOU)) return ProgressBarApp( packageName, showProgress == 1, color, useDefault == 1, useMaterialYou == 1 ) } } return null } private fun getAllApps(): MutableList { val apps = mutableListOf() readableDatabase.rawQuery( "SELECT * FROM $TABLE_NAME", null ).use { cursor -> val packageIndex = cursor.getColumnIndexOrThrow(COLUMN_PACKAGE) val showProgressIndex = cursor.getColumnIndexOrThrow(COLUMN_SHOW_PROGRESS) val colorIndex = cursor.getColumnIndexOrThrow(COLUMN_COLOR) val useDefaultIndex = cursor.getColumnIndexOrThrow(COLUMN_USE_DEFAULT) val useMaterialYouIndex = cursor.getColumnIndexOrThrow(COLUMN_USE_MATERIAL_YOU) while (cursor.moveToNext()) { val packageName = cursor.getString(packageIndex) val showProgress = cursor.getInt(showProgressIndex) val color = if (cursor.isNull(colorIndex)) { null } else { cursor.getInt(colorIndex) } val useDefault = cursor.getInt(useDefaultIndex) val useMaterialYou = cursor.getInt(useMaterialYouIndex) apps.add( ProgressBarApp( packageName, showProgress == 1, color, useDefault == 1, useMaterialYou == 1 ) ) } } return apps } fun getAll(): List { return apps.toList() } } ================================================ FILE: app/src/main/java/com/gustavoas/noti/SettingsActivity.kt ================================================ package com.gustavoas.noti import android.content.Intent import android.content.SharedPreferences import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler import android.os.Looper import android.util.Xml import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat import androidx.core.content.edit import androidx.core.net.toUri import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceManager import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.AppBarLayout.OnOffsetChangedListener import com.google.android.material.appbar.CollapsingToolbarLayout import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.gustavoas.noti.Utils.dpToPx import com.gustavoas.noti.Utils.getFirebaseConfigStorageReference import com.gustavoas.noti.Utils.getScreenLargeSide import com.gustavoas.noti.Utils.getScreenSmallSide import com.gustavoas.noti.Utils.hasAccessibilityPermission import com.gustavoas.noti.Utils.hasNotificationListenerPermission import com.gustavoas.noti.Utils.hasSystemAlertWindowPermission import com.gustavoas.noti.fragments.CircularBarFragment import com.gustavoas.noti.fragments.LinearBarFragment import com.gustavoas.noti.fragments.PerAppSettingsFragment import com.gustavoas.noti.fragments.SettingsFragment import com.gustavoas.noti.model.DeviceConfiguration import com.gustavoas.noti.model.ProgressBarApp import com.gustavoas.noti.model.ProgressNotification import com.gustavoas.noti.services.AccessibilityService import kotlinx.coroutines.runBlocking import kotlinx.coroutines.tasks.await import org.xmlpull.v1.XmlPullParser import java.io.InputStream import kotlin.math.abs import kotlin.math.roundToInt import kotlin.math.sqrt class SettingsActivity : AppCompatActivity(), PreferenceFragmentCompat.OnPreferenceStartFragmentCallback, SharedPreferences.OnSharedPreferenceChangeListener { private val previewFab by lazy { findViewById(R.id.previewFab) } private val topAppBar by lazy { findViewById(R.id.topAppBar) } private val appBarLayout by lazy { findViewById(R.id.appBarLayout) } private var offsetChangeListener: OnOffsetChangedListener? = null private val handler = Handler(Looper.getMainLooper()) private val sizeDependentPrefs = arrayOf( Pair("advancedProgressBarStyle", false), Pair("progressBarStyle", "linear"), Pair("progressBarStylePortrait", "linear"), Pair("progressBarStyleLandscape", "linear"), Pair("circularProgressBarThickness", 15), Pair("circularProgressBarSize", 65), Pair("circularProgressBarTopOffset", 60), Pair("circularProgressBarHorizontalOffset", 0), Pair("linearProgressBarSize", 15), Pair("matchStatusBarHeight", false), Pair("linearProgressBarMarginTop", 0), Pair("showBelowNotch", false), ) override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { if (sizeDependentPrefs.any { it.first == key }) { val defaultValue = sizeDependentPrefs.first { it.first == key }.second val width = getScreenSmallSide(this) val height = getScreenLargeSide(this) moveSharedPreferenceValue(key + height + "x" + width, key!!, defaultValue) } } override fun onCreate(savedInstanceState: Bundle?) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { setTheme(R.style.SplashScreen) installSplashScreen() } else { setTheme(R.style.Theme_NotiProgressBar) } super.onCreate(savedInstanceState) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) if (!sharedPreferences.contains("progressBarStyle")) { runBlocking { setupDeviceConfiguration() } } if (!sharedPreferences.contains("progressBarColor")) { setMaterialYouAsDefault() } setupSizeDependentPrefs() sharedPreferences .registerOnSharedPreferenceChangeListener(this) setContentView(R.layout.activity_main) if (savedInstanceState == null) { supportFragmentManager.beginTransaction() .replace(R.id.settings, SettingsFragment()) .commitNow() } updateUpNavigationVisibility() ViewCompat.setOnApplyWindowInsetsListener(window.decorView.rootView) { _, insets -> val keyboardInset = insets.getInsets(WindowInsetsCompat.Type.ime()).bottom if (keyboardInset > 0) { appBarLayout.setExpanded(false, true) } insets } } private fun setupSizeDependentPrefs() { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) val width = getScreenSmallSide(this) val height = getScreenLargeSide(this) for (pref in sizeDependentPrefs) { val relatedPrefs = sharedPreferences.all.filterKeys { it.startsWith(pref.first) } val sameRatio = relatedPrefs.filterKeys { it.length > pref.first.length && it[pref.first.length].isDigit() && it.drop(pref.first.length).split("x")[0].toInt() / it.drop(pref.first.length).split("x")[1].toInt() == height / width } if (sameRatio.isEmpty()) { continue } val closest = sameRatio.keys.minByOrNull { abs(it.drop(pref.first.length).split("x")[0].toInt() * it.drop(pref.first.length).split("x")[1].toInt() - height * width) } var reductionRatio = 1f if (pref.second is Int) { reductionRatio = sqrt((height * width).div(closest!!.drop(pref.first.length).split("x")[0].toFloat() * closest.drop(pref.first.length).split("x")[1].toFloat())) } moveSharedPreferenceValue(pref.first, closest!!, pref.second, reductionRatio) } } private fun moveSharedPreferenceValue(key: String, oldKey: String, defaultValue: Any, reductionRatio: Float = 1f) { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) sharedPreferences.edit { when (defaultValue) { is Boolean -> putBoolean(key, sharedPreferences.getBoolean(oldKey, defaultValue)) is Int -> putInt( key, sharedPreferences.getInt(oldKey, defaultValue).times(reductionRatio) .roundToInt() ) is String -> putString(key, sharedPreferences.getString(oldKey, defaultValue)) } } } override fun onStart() { super.onStart() val collapsingToolbarLayout = findViewById(R.id.collapsingToolbar) var isVisible = true var scrollRange = -1 offsetChangeListener = OnOffsetChangedListener { barLayout, verticalOffset -> if (scrollRange == -1) { scrollRange = barLayout?.totalScrollRange!! } if (scrollRange + verticalOffset < dpToPx(this, 25)) { collapsingToolbarLayout.title = resources.getString(R.string.app_name_short) isVisible = true } else if (isVisible) { collapsingToolbarLayout.title = resources.getString(R.string.app_name) isVisible = false } } appBarLayout.addOnOffsetChangedListener(offsetChangeListener) if (hasNotificationListenerPermission(this) && (hasAccessibilityPermission(this) || hasSystemAlertWindowPermission(this))) { previewFab.visibility = View.VISIBLE } else { previewFab.visibility = View.GONE } previewFab.setOnClickListener { simulateDownload() } supportFragmentManager.addOnBackStackChangedListener { updateUpNavigationVisibility() } topAppBar.setNavigationOnClickListener { supportFragmentManager.popBackStack() } topAppBar.setOnMenuItemClickListener { menuItem -> when (menuItem.itemId) { R.id.bug_report -> { val sendEmail = Intent(Intent.ACTION_SENDTO).apply { data = ("mailto:gustavoasgas1+noti@gmail.com" + "?subject=" + Uri.encode("Noti")).toUri() } sendEmail.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(sendEmail) true } else -> false } } } override fun onStop() { super.onStop() if (offsetChangeListener != null) { appBarLayout.removeOnOffsetChangedListener(offsetChangeListener!!) } } override fun onDestroy() { super.onDestroy() PreferenceManager.getDefaultSharedPreferences(this) .unregisterOnSharedPreferenceChangeListener(this) } override fun onPreferenceStartFragment( caller: PreferenceFragmentCompat, pref: Preference ): Boolean { val fragment = when (pref.key) { "CircularBarFragment" -> CircularBarFragment() "LinearBarFragment" -> LinearBarFragment() "PerAppSettingsFragment" -> PerAppSettingsFragment() else -> null } supportFragmentManager.beginTransaction() .replace(R.id.settings, fragment ?: return false) .addToBackStack(null) .commit() return true } private fun simulateDownload() { val intent = Intent(this, AccessibilityService::class.java) handler.removeCallbacksAndMessages(null) val maxProgress = this.resources.getInteger(R.integer.progress_bar_max) val numberOfSteps = 4 val stepSize = maxProgress / numberOfSteps for (i in stepSize..(maxProgress + stepSize) step stepSize) { handler.postDelayed({ intent.putExtra("removal", i > maxProgress) intent.putExtra("id", packageName) intent.putExtra( "progressNotification", ProgressNotification( ProgressBarApp( packageName, true ), i, 10 ) ) startService(intent) }, ((i - stepSize) * 1000 / stepSize).toLong()) } } private suspend fun setupDeviceConfiguration() { if (!Utils.isInternetAvailable(this)) { return } val configRef = getFirebaseConfigStorageReference() try { val taskSnapshot = configRef.stream.await() val inputStream: InputStream = taskSnapshot.stream val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) sharedPreferences.edit { putString("progressBarStyle", "circular") } parseDeviceConfiguration(inputStream) } catch (_: Exception) { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) sharedPreferences.edit { putString("progressBarStyle", "linear") } } } private fun parseDeviceConfiguration(input: InputStream) { val parser: XmlPullParser = Xml.newPullParser() parser.setInput(input, null) while (parser.eventType != XmlPullParser.END_DOCUMENT) { when (parser.eventType) { XmlPullParser.START_TAG -> { if (parser.name == "display") { val displayConfig = DeviceConfiguration() displayConfig.deviceWidth = parser.getAttributeValue(null, "width") displayConfig.deviceHeight = parser.getAttributeValue(null, "height") displayConfig.configuration = parser.getAttributeValue(null, "configuration") parseDisplay(displayConfig, parser) } } } parser.next() } } private fun parseDisplay(config: DeviceConfiguration, parser: XmlPullParser) { while (parser.eventType != XmlPullParser.END_DOCUMENT) { when (parser.eventType) { XmlPullParser.START_TAG -> { when (parser.name) { "size" -> config.size = parser.nextText() "marginTop" -> config.marginTop = parser.nextText() "topOffset" -> config.topOffset = parser.nextText() "offset" -> config.horizontalOffset = parser.nextText() } } XmlPullParser.END_TAG -> { if (parser.name == "display") { break } } } parser.next() } val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) val currentWidth = getScreenSmallSide(this) val currentHeight = getScreenLargeSide(this) if (config.deviceWidth == "" || config.deviceHeight == "") { if ((config.deviceWidth != "" && currentWidth == config.deviceWidth?.toInt()) || (config.deviceHeight != "" && currentHeight == config.deviceHeight?.toInt())) { config.deviceWidth = currentWidth.toString() config.deviceHeight = currentHeight.toString() } if (config.deviceWidth == "" && config.deviceHeight != "") { config.deviceWidth = (currentWidth * config.deviceHeight!!.toInt() / currentHeight).toString() } else if (config.deviceHeight == "" && config.deviceWidth != "") { config.deviceHeight = (currentHeight * config.deviceWidth!!.toInt() / currentWidth).toString() } if (config.deviceWidth == "" || config.deviceHeight == "") { config.deviceWidth = currentWidth.toString() config.deviceHeight = currentHeight.toString() } } val appendix = config.deviceHeight + "x" + config.deviceWidth if (config.configuration == "circular") { sharedPreferences.edit { putString("progressBarStyle$appendix", "circular") putBoolean("blackBackground", true) putInt("circularProgressBarSize$appendix", config.size?.toIntOrNull() ?: 65) putInt("circularProgressBarTopOffset$appendix", config.topOffset?.toIntOrNull() ?: 60) putInt("circularProgressBarHorizontalOffset$appendix", config.horizontalOffset?.toIntOrNull() ?: 0) } } else { sharedPreferences.edit { putString("progressBarStyle$appendix", "linear") } } } private fun setMaterialYouAsDefault() { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { sharedPreferences.edit { putInt( "progressBarColor", ContextCompat.getColor( this@SettingsActivity, R.color.system_accent_color ) ) putBoolean("usingMaterialYouColor", true) } } else { sharedPreferences.edit { putInt( "progressBarColor", ContextCompat.getColor( this@SettingsActivity, R.color.purple_500 ) ) } } } private fun updateUpNavigationVisibility() { if (supportFragmentManager.backStackEntryCount > 0) { topAppBar.setNavigationIcon(androidx.appcompat.R.drawable.abc_ic_ab_back_material) topAppBar.setNavigationIconTint(ContextCompat.getColor(this, R.color.text)) topAppBar.setTitleMargin(0, 0, dpToPx(this, 40), 0) } else { topAppBar.navigationIcon = null topAppBar.setTitleMargin(0, 0, 0, 0) } } } ================================================ FILE: app/src/main/java/com/gustavoas/noti/Utils.kt ================================================ package com.gustavoas.noti import android.content.ComponentName import android.content.Context import android.content.pm.ApplicationInfo import android.content.pm.PackageManager.NameNotFoundException import android.graphics.drawable.Drawable import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.os.Build import android.os.VibrationEffect import android.os.Vibrator import android.os.VibratorManager import android.provider.Settings import android.util.DisplayMetrics import android.view.Display import android.widget.Toast import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager import com.google.firebase.ktx.Firebase import com.google.firebase.storage.StorageReference import com.google.firebase.storage.ktx.storage import com.gustavoas.noti.model.ProgressBarApp import com.gustavoas.noti.services.AccessibilityService import com.gustavoas.noti.services.NotificationListenerService import eltos.simpledialogfragment.color.SimpleColorDialog import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream import java.io.InputStream import java.nio.charset.StandardCharsets object Utils { fun hasAccessibilityPermission(context: Context): Boolean { val accessibilityServiceComponentName = ComponentName(context, AccessibilityService::class.java) val enabledServices = Settings.Secure.getString(context.contentResolver, Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES) return enabledServices.orEmpty().let { it.contains(accessibilityServiceComponentName.flattenToString()) || it.contains(accessibilityServiceComponentName.flattenToShortString()) } } fun hasNotificationListenerPermission(context: Context): Boolean { val notificationListenerComponentName = ComponentName(context, NotificationListenerService::class.java) val enabledServices = Settings.Secure.getString(context.contentResolver, "enabled_notification_listeners") return enabledServices?.contains(notificationListenerComponentName.flattenToString()) ?: false } fun hasSystemAlertWindowPermission(context: Context): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { Settings.canDrawOverlays(context) } else { true } } fun dpToPx(context: Context, dp: Int): Int { return (dp * context.resources.displayMetrics.density).toInt() } fun isInternetAvailable(context: Context): Boolean { val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { val network = connectivityManager.activeNetwork ?: return false val capabilities = connectivityManager.getNetworkCapabilities(network) ?: return false return capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) && capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) } else { val activeNetworkInfo = connectivityManager.activeNetworkInfo return activeNetworkInfo != null && activeNetworkInfo.isConnected } } fun getFirebaseConfigStorageReference(): StorageReference { val storage = Firebase.storage val storageRef = storage.reference val brand = Build.BRAND.lowercase() var model = Build.DEVICE.lowercase().replace("\\W".toRegex(), "") if (brand == "xiaomi" || brand == "redmi" || brand == "poco") { while (model.endsWith("in")) { model = model.dropLast(2) } } return storageRef.child("configs/$brand/$model.xml") } fun shareConfigToFirebase(context: Context) { if (!isInternetAvailable(context)) { Toast.makeText(context, context.getString(R.string.shareConfigNoInternetMessage), Toast.LENGTH_SHORT).show() return } val configRef = getFirebaseConfigStorageReference() configRef.stream.addOnSuccessListener { taskSnapshot -> val inputStream: InputStream = taskSnapshot.stream val outputStream = ByteArrayOutputStream() val buffer = ByteArray(1024) var length: Int while (inputStream.read(buffer).also { length = it } != -1) { outputStream.write(buffer, 0, length) } val existingFileContent = outputStream.toString(StandardCharsets.UTF_8.name()) if (existingFileContent != buildConfig(context)) { uploadConfigToStorageRef(context, configRef) } }.addOnFailureListener { uploadConfigToStorageRef(context, configRef) } Toast.makeText(context, context.getString(R.string.shareConfigPositiveMessage), Toast.LENGTH_SHORT).show() } private fun uploadConfigToStorageRef(context: Context, storageRef: StorageReference) { val config = buildConfig(context) val xmlInputStream = ByteArrayInputStream(config.toByteArray(StandardCharsets.UTF_8)) storageRef.putStream(xmlInputStream) } private fun buildConfig(context: Context): String { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val stylePrefs = sharedPreferences.all.filterKeys { it.startsWith("progressBarStyle") && it.last().isDigit() } if (stylePrefs.isEmpty() || stylePrefs.none { it.value == "circular" }) { return "" } val config = StringBuilder() config.append("\n") config.append("\n") val sizes = stylePrefs.keys.map { str -> str.dropWhile { !it.isDigit() } }.distinct() sizes.forEach { config.append(buildConfigForDisplay(context, it)).append("\n") } config.append("") return config.toString() } private fun buildConfigForDisplay(context: Context, display: String): String { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val height = display.split("x")[0].toInt() val width = display.split("x")[1].toInt() val configuration = sharedPreferences.getString("progressBarStyle$display", "linear") val displayConfig = StringBuilder() displayConfig.append("\t\n") if (configuration == "circular") { val size = sharedPreferences.getInt("circularProgressBarSize$display", 65) val marginTop = sharedPreferences.getInt("circularProgressBarTopOffset$display", 60) val offset = sharedPreferences.getInt("circularProgressBarHorizontalOffset$display", 0) displayConfig.append("\t\t$size\n") displayConfig.append("\t\t$marginTop\n") displayConfig.append("\t\t$offset\n") } displayConfig.append("\t") return displayConfig.toString() } fun showColorDialog(fragment: Fragment, color: Int, tag: String, reset: Boolean = false) { if (reset) { SimpleColorDialog.build() .colorPreset(color) .colors(fragment.context, R.array.colorsArrayValues) .allowCustom(true) .showOutline(0x46000000) .gridNumColumn(5) .choiceMode(SimpleColorDialog.SINGLE_CHOICE) .neg() .neut(R.string.reset) .show(fragment, tag) } else { SimpleColorDialog.build() .colorPreset(color) .colors(fragment.context, R.array.colorsArrayValues) .allowCustom(true) .showOutline(0x46000000) .gridNumColumn(5) .choiceMode(SimpleColorDialog.SINGLE_CHOICE) .neg() .show(fragment, tag) } } fun getRealDisplayHeight(context: Context): Int { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager windowManager.currentWindowMetrics.bounds.height() } else { val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as android.hardware.display.DisplayManager val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)!! val metrics = DisplayMetrics() display.getRealMetrics(metrics) metrics.heightPixels } } private fun getRealDisplayWidth(context: Context): Int { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as android.view.WindowManager windowManager.currentWindowMetrics.bounds.width() } else { val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as android.hardware.display.DisplayManager val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY)!! val metrics = DisplayMetrics() display.getRealMetrics(metrics) metrics.widthPixels } } fun getScreenSmallSide(context: Context): Int { return minOf(getRealDisplayHeight(context), getRealDisplayWidth(context)) } fun getScreenLargeSide(context: Context): Int { return maxOf(getRealDisplayHeight(context), getRealDisplayWidth(context)) } fun vibrate(context: Context) { val vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val vibratorManager = ContextCompat.getSystemService(context, VibratorManager::class.java) vibratorManager?.defaultVibrator } else { @Suppress("DEPRECATION") context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator } if (vibrator == null) { return } vibrator.cancel() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { vibrator.vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK)) } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { vibrator.vibrate(VibrationEffect.createOneShot(1, 200)) } else { @Suppress("DEPRECATION") vibrator.vibrate(1) } } fun getStatusBarHeight(context: Context): Int { return context.resources.getDimensionPixelSize( (context.resources.getIdentifier( "status_bar_height", "dimen", "android" ))) } fun getApplicationInfo(context: Context, packageName: String): ApplicationInfo? { return try { context.packageManager.getApplicationInfo(packageName, 0) } catch (e: NameNotFoundException) { null } } fun getApplicationName(context: Context, packageName: String): String? { return context.packageManager.getApplicationLabel( getApplicationInfo(context, packageName) ?: return null ).toString() } fun getApplicationIcon(context: Context, packageName: String): Drawable? { return context.packageManager.getApplicationIcon( getApplicationInfo(context, packageName) ?: return null ) } fun getColorForApp(context: Context, progressBarApp: ProgressBarApp): Int { val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(context) val useNotificationColor = sharedPrefs.getBoolean("useNotificationColor", false) val useMaterialYou = sharedPrefs.getBoolean("usingMaterialYouColor", false) if ((progressBarApp.useDefaultColor && useNotificationColor) || (!progressBarApp.useDefaultColor && !progressBarApp.useMaterialYouColor)) { progressBarApp.color?.let { return it } } if ((progressBarApp.useMaterialYouColor) || (progressBarApp.useDefaultColor && useMaterialYou)) { return ContextCompat.getColor(context, R.color.system_accent_color) } return sharedPrefs.getInt( "progressBarColor", ContextCompat.getColor(context, R.color.purple_500) ) } } ================================================ FILE: app/src/main/java/com/gustavoas/noti/fragments/BasePreferenceFragment.kt ================================================ package com.gustavoas.noti.fragments import android.os.Bundle import android.view.View import androidx.preference.PreferenceFragmentCompat import com.gustavoas.noti.Utils abstract class BasePreferenceFragment : PreferenceFragmentCompat() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) val preferencesView = listView preferencesView.setPadding(0, 0, 0, Utils.dpToPx(requireContext(), 100)) preferencesView.isVerticalScrollBarEnabled = false } } ================================================ FILE: app/src/main/java/com/gustavoas/noti/fragments/CircularBarFragment.kt ================================================ package com.gustavoas.noti.fragments import android.content.SharedPreferences import android.os.Bundle import androidx.preference.Preference import androidx.preference.PreferenceManager import com.gustavoas.noti.R import com.gustavoas.noti.Utils class CircularBarFragment : BasePreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { // TODO: If offset is not zero and hole punch size changes toast suggesting a 0.5x change } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.circular_bar_preferences, rootKey) findPreference("shareConfig")?.setOnPreferenceClickListener { Utils.shareConfigToFirebase(requireContext()) true } val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences.registerOnSharedPreferenceChangeListener(this) } override fun onDestroy() { super.onDestroy() PreferenceManager.getDefaultSharedPreferences(requireContext()) .unregisterOnSharedPreferenceChangeListener(this) } } ================================================ FILE: app/src/main/java/com/gustavoas/noti/fragments/LinearBarFragment.kt ================================================ package com.gustavoas.noti.fragments import android.content.SharedPreferences import android.os.Build import android.os.Bundle import androidx.preference.Preference import androidx.preference.PreferenceManager import com.gustavoas.noti.R import com.gustavoas.noti.Utils.getStatusBarHeight class LinearBarFragment : BasePreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { if (key == "linearProgressBarSize") { val size = sharedPreferences?.getInt(key, 15) ?: 15 sharedPreferences?.edit() ?.putBoolean("matchStatusBarHeight", size == getStatusBarHeight(context ?: return)) ?.apply() } } override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.linear_bar_preferences, rootKey) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) findPreference("statusBarHeightCard")?.summary = getString(R.string.prefsStatusBarHeightInfo, getStatusBarHeight(requireContext())) if (sharedPreferences.getBoolean("matchStatusBarHeight", false)) { sharedPreferences.edit() .putInt("linearProgressBarSize", getStatusBarHeight(requireContext())).apply() } sharedPreferences.registerOnSharedPreferenceChangeListener(this) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (savedInstanceState == null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || !hasDisplayCutout()) { findPreference("showBelowNotch")?.isVisible = false } } } private fun hasDisplayCutout(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { val windowInsets = requireActivity().window?.decorView?.rootWindowInsets val displayCutout = windowInsets?.displayCutout displayCutout != null } else { false } } } ================================================ FILE: app/src/main/java/com/gustavoas/noti/fragments/PerAppSettingsFragment.kt ================================================ package com.gustavoas.noti.fragments import android.os.Build import android.os.Build.VERSION_CODES import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.TextView import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.gustavoas.noti.ProgressBarAppsAdapter import com.gustavoas.noti.ProgressBarAppsRepository import com.gustavoas.noti.R import com.gustavoas.noti.Utils.getApplicationInfo import com.gustavoas.noti.Utils.getApplicationName import com.gustavoas.noti.Utils.getColorForApp import com.gustavoas.noti.model.ProgressBarApp import eltos.simpledialogfragment.SimpleDialog import eltos.simpledialogfragment.SimpleDialog.OnDialogResultListener.BUTTON_NEUTRAL import eltos.simpledialogfragment.SimpleDialog.OnDialogResultListener.BUTTON_POSITIVE import eltos.simpledialogfragment.color.SimpleColorDialog class PerAppSettingsFragment : Fragment(), SimpleDialog.OnDialogResultListener { private val apps = ArrayList() private val appsRepository by lazy { ProgressBarAppsRepository(requireContext()) } private val recyclerView by lazy { requireView().findViewById(R.id.apps_recycler_view) } override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View = inflater.inflate(R.layout.fragment_per_app_settings, container, false) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) updateAppsFromDatabase() recyclerView.layoutManager = LinearLayoutManager(requireContext()) recyclerView.adapter = ProgressBarAppsAdapter(this, requireContext(), apps, appsRepository) updateRecyclerViewVisibility() } override fun onResult(dialogTag: String, which: Int, extras: Bundle): Boolean { val progressBarApp = apps[dialogTag.toInt()].copy() val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) val defaultColor = sharedPrefs.getInt( "progressBarColor", ContextCompat.getColor(requireContext(), R.color.purple_500) ) val existingSelectedColor = getColorForApp(requireContext(), progressBarApp) val useMaterialYou = sharedPrefs.getBoolean("usingMaterialYouColor", false) val color = extras.getInt(SimpleColorDialog.COLOR) val selectedPosition = extras.getInt(SimpleColorDialog.SELECTED_SINGLE_POSITION) if (which == BUTTON_NEUTRAL || (color == defaultColor && (!useMaterialYou || selectedPosition != 19))) { progressBarApp.useDefaultColor = true progressBarApp.useMaterialYouColor = false progressBarApp.color = null } else if (which == BUTTON_POSITIVE) { if ( Build.VERSION.SDK_INT >= VERSION_CODES.S && selectedPosition != 19 && color == ContextCompat.getColor(requireContext(), R.color.system_accent_color) ) { progressBarApp.useDefaultColor = false progressBarApp.useMaterialYouColor = true progressBarApp.color = null } else if (color != existingSelectedColor || (progressBarApp.useMaterialYouColor && selectedPosition == 19)) { progressBarApp.useDefaultColor = false progressBarApp.useMaterialYouColor = false progressBarApp.color = color } } if (apps[dialogTag.toInt()] != progressBarApp) { apps[dialogTag.toInt()] = progressBarApp appsRepository.updateApp(apps[dialogTag.toInt()]) recyclerView.adapter?.notifyItemChanged(dialogTag.toInt() + 1) } return true } private fun updateAppsFromDatabase() { apps.clear() apps.addAll(appsRepository.getAll()) removeUnavailableApps() alphabetizeApps() } private fun removeUnavailableApps() { apps.removeAll { app -> getApplicationInfo(requireContext(), app.packageName)?.enabled != true } } private fun alphabetizeApps() { apps.sortBy { app -> (getApplicationName(requireContext(), app.packageName) ?: app.packageName).lowercase() } } private fun updateRecyclerViewVisibility() { val emptyRecyclerView = requireView().findViewById(R.id.empty_view) if (apps.isEmpty()) { recyclerView.visibility = View.GONE emptyRecyclerView.visibility = View.VISIBLE } else { recyclerView.visibility = View.VISIBLE emptyRecyclerView.visibility = View.GONE } } override fun onDestroy() { super.onDestroy() appsRepository.close() } } ================================================ FILE: app/src/main/java/com/gustavoas/noti/fragments/SettingsFragment.kt ================================================ package com.gustavoas.noti.fragments import android.content.ComponentName import android.content.Intent import android.content.SharedPreferences import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.os.Build import android.os.Bundle import android.provider.Settings import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat import androidx.core.content.edit import androidx.core.net.toUri import androidx.preference.Preference import androidx.preference.PreferenceCategory import androidx.preference.PreferenceManager import androidx.preference.children import com.gustavoas.noti.R import com.gustavoas.noti.Utils.hasAccessibilityPermission import com.gustavoas.noti.Utils.hasNotificationListenerPermission import com.gustavoas.noti.Utils.hasSystemAlertWindowPermission import com.gustavoas.noti.Utils.showColorDialog import com.gustavoas.noti.preferences.BannerPreference import com.gustavoas.noti.services.NotificationListenerService import com.kizitonwose.colorpreferencecompat.ColorPreferenceCompat import eltos.simpledialogfragment.SimpleDialog import eltos.simpledialogfragment.SimpleDialog.OnDialogResultListener.BUTTON_POSITIVE import eltos.simpledialogfragment.color.SimpleColorDialog class SettingsFragment : BasePreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener, SimpleDialog.OnDialogResultListener { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { if (key == "progressBarStyle" || key == "progressBarStylePortrait" || key == "progressBarStyleLandscape" || key == "advancedProgressBarStyle") { if (key != "advancedProgressBarStyle" && sharedPreferences?.getString( key, "linear" ) == "circular" && sharedPreferences.getBoolean("showHolePunchInstruction", true) ) { Toast.makeText( requireContext(), getString(R.string.holePunchInstruction), Toast.LENGTH_LONG ).show() sharedPreferences.edit { putBoolean("showHolePunchInstruction", false) } } updateProgressBarStyle() } else if (key == "progressBarColor") { updateColorPreferenceSummary() } else if (key == "useNotificationColor") { updateColorPreferenceState() } } private val myApps = listOf( Triple("CalenTile", "https://play.google.com/store/apps/details?id=com.gustavoas.calendarqst", R.drawable.ic_calentile), Triple("Sum Up", "https://play.google.com/store/apps/details?id=com.gustavoas.sumup", R.drawable.ic_sumup), ) override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.preferences, rootKey) updateSetupVisibility() updateProgressBarStyle() updatePermissionDependentPreferences() updateColorPreferenceState() val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) sharedPreferences.registerOnSharedPreferenceChangeListener(this) val appIndex = myApps.indices.random() val appPromo = findPreference("selfPromo") appPromo?.title = getString(R.string.prefsCalentileTitle, myApps[appIndex].first) appPromo?.intent = Intent(Intent.ACTION_VIEW, myApps[appIndex].second.toUri()) appPromo?.icon = ContextCompat.getDrawable(requireContext(), myApps[appIndex].third) if (!sharedPreferences.contains("batteryOptimizations")) { sharedPreferences.edit { putBoolean( "batteryOptimizations", Build.BRAND.lowercase() != "google" ) } } val batterOptimizationsBanner = findPreference("batteryOptimizations") if (!sharedPreferences.getBoolean("batteryOptimizations", true)) { batterOptimizationsBanner?.isVisible = false } findPreference("accessibilityPermission")?.setOnPreferenceClickListener { showAccessibilityDialog() true } findPreference("progressBarColor")?.setOnPreferenceClickListener { val color = PreferenceManager.getDefaultSharedPreferences(requireContext()).getInt( "progressBarColor", ContextCompat.getColor(requireContext(), R.color.purple_500) ) showColorDialog(this, color, "colorPicker") true } findPreference("notificationPermission")?.setOnPreferenceClickListener { requestNotificationAccess() true } } override fun onStart() { super.onStart() updateSetupVisibility() updateColorPreferenceSummary() updatePermissionDependentPreferences() } override fun onDestroy() { super.onDestroy() PreferenceManager.getDefaultSharedPreferences(requireContext()) .unregisterOnSharedPreferenceChangeListener(this) } override fun onResult(dialogTag: String, which: Int, extras: Bundle): Boolean { if (which == BUTTON_POSITIVE) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PreferenceManager.getDefaultSharedPreferences(requireContext()).edit { putBoolean( "usingMaterialYouColor", extras.getInt(SimpleColorDialog.COLOR) == ContextCompat.getColor( requireContext(), R.color.system_accent_color ) && extras.getInt(SimpleColorDialog.SELECTED_SINGLE_POSITION) != 19 ) } } findPreference("progressBarColor")?.value = extras.getInt( SimpleColorDialog.COLOR, ContextCompat.getColor(requireContext(), R.color.purple_500) ) } return true } private fun updateColorPreferenceState() { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val colorPreference = findPreference("progressBarColor") val useNotificationColor = sharedPreferences.getBoolean("useNotificationColor", false) colorPreference?.isEnabled = !useNotificationColor colorPreference?.icon?.alpha = if (useNotificationColor) 80 else 255 } private fun updateColorPreferenceSummary() { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val color = sharedPreferences.getInt( "progressBarColor", ContextCompat.getColor(requireContext(), R.color.purple_500) ) val colorPosition = resources.getIntArray(R.array.colorsArrayValues).indexOf(color) var colorName = resources.getStringArray(R.array.colorsArray).getOrNull(colorPosition) val useMaterialYou = sharedPreferences.getBoolean("usingMaterialYouColor", false) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && colorName == null && useMaterialYou) { sharedPreferences.edit { putInt( "progressBarColor", ContextCompat.getColor( requireContext(), R.color.system_accent_color ) ) } findPreference("progressBarColor")?.value = ContextCompat.getColor(requireContext(), R.color.system_accent_color) colorName = resources.getString(R.string.colorMaterialYou) } findPreference("progressBarColor")?.summary = colorName ?: "#${Integer.toHexString(color).drop(2).uppercase()}" } private fun updateProgressBarStyle() { val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val advancedStyleOptions = sharedPreferences.getBoolean("advancedProgressBarStyle", false) val progressBarStyle = sharedPreferences.getString("progressBarStyle", "linear") val progressBarStylePortrait = sharedPreferences.getString("progressBarStylePortrait", "linear") val progressBarStyleLandscape = sharedPreferences.getString("progressBarStyleLandscape", "linear") val anyLinear = (!advancedStyleOptions && progressBarStyle == "linear") || (advancedStyleOptions && (progressBarStylePortrait == "linear" || progressBarStyleLandscape == "linear")) val anyCircular = (!advancedStyleOptions && progressBarStyle == "circular") || (advancedStyleOptions && (progressBarStylePortrait == "circular" || progressBarStyleLandscape == "circular")) findPreference("CircularBarFragment")?.isVisible = anyCircular findPreference("LinearBarFragment")?.isVisible = anyLinear findPreference("progressBarStyle")?.summary = if (advancedStyleOptions) { if (resources.configuration.orientation == ORIENTATION_LANDSCAPE) { resources.getStringArray(R.array.progressBarStyle).getOrNull( resources.getStringArray(R.array.progressBarStyleValues) .indexOf(progressBarStyleLandscape) ) } else { resources.getStringArray(R.array.progressBarStyle).getOrNull( resources.getStringArray(R.array.progressBarStyleValues) .indexOf(progressBarStylePortrait) ) } } else { resources.getStringArray(R.array.progressBarStyle).getOrNull( resources.getStringArray(R.array.progressBarStyleValues).indexOf(progressBarStyle) ) } } private fun updatePermissionDependentPreferences() { findPreference("showInLockScreen")?.isVisible = (hasAccessibilityPermission(requireContext()) || Build.VERSION.SDK_INT < Build.VERSION_CODES.M) findPreference("disableInFullScreen")?.isVisible = hasSystemAlertWindowPermission(requireContext()) } private fun updateSetupVisibility() { val hasAccessibilityPermission = hasAccessibilityPermission(requireContext()) val hasNotificationListenerPermission = hasNotificationListenerPermission(requireContext()) val hasSystemAlertWindowPermission = hasSystemAlertWindowPermission(requireContext()) findPreference("accessibilityPermission")?.isVisible = !hasAccessibilityPermission findPreference("notificationPermission")?.isVisible = !hasNotificationListenerPermission findPreference("systemAlertWindowPermission")?.isVisible = !hasSystemAlertWindowPermission findPreference("setup")?.isVisible = findPreference("setup")?.children?.any { it.isVisible } == true } private fun showAccessibilityDialog() { AlertDialog.Builder(requireContext()) .setTitle(R.string.prefsAccessibilityPermissionTitle) .setMessage(R.string.prefsAccessibilityPermissionSummary) .setPositiveButton(android.R.string.ok) { _, _ -> val intent = Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS) startActivity(intent) } .setNegativeButton(android.R.string.cancel, null) .show() } private fun requestNotificationAccess() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val intent = Intent(Settings.ACTION_NOTIFICATION_LISTENER_DETAIL_SETTINGS) intent.putExtra( Settings.EXTRA_NOTIFICATION_LISTENER_COMPONENT_NAME, ComponentName( requireContext(), NotificationListenerService::class.java ).flattenToString() ) try { startActivity(intent) } catch (e: Exception) { startActivity(Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")) } } else { startActivity(Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS")) } } } ================================================ FILE: app/src/main/java/com/gustavoas/noti/model/DeviceConfiguration.kt ================================================ package com.gustavoas.noti.model data class DeviceConfiguration ( var configuration: String? = null, var deviceWidth: String? = null, var deviceHeight: String? = null, var size: String? = null, var marginTop: String? = null, var topOffset: String? = null, var horizontalOffset: String? = null, ) ================================================ FILE: app/src/main/java/com/gustavoas/noti/model/ProgressBarApp.kt ================================================ package com.gustavoas.noti.model import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize data class ProgressBarApp( val packageName: String, var showProgressBar: Boolean = true, var color: Int? = null, var useDefaultColor: Boolean = true, var useMaterialYouColor: Boolean = false ) : Parcelable ================================================ FILE: app/src/main/java/com/gustavoas/noti/model/ProgressNotification.kt ================================================ package com.gustavoas.noti.model import android.os.Parcelable import kotlinx.parcelize.Parcelize @Parcelize data class ProgressNotification ( val progressBarApp: ProgressBarApp, val progress: Int, val priority: Int ) : Parcelable ================================================ FILE: app/src/main/java/com/gustavoas/noti/notifications/DownloadProgressBar.kt ================================================ package com.gustavoas.noti.notifications import android.app.Notification import android.content.Context import android.service.notification.StatusBarNotification import androidx.preference.PreferenceManager import com.gustavoas.noti.ProgressBarAppsRepository class DownloadProgressBar( ctx: Context, sbn: StatusBarNotification, appsRepository: ProgressBarAppsRepository ): ProgressBarNotification(ctx, sbn, appsRepository) { override val priorityLevel: Int = 2 init { updateNotification(sbn) } override fun updateNotification(sbn: StatusBarNotification) { super.updateNotification(sbn) val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(ctx) val enabledForDownloads = sharedPrefs.getBoolean("showForDownloads", true) if (!enabledForDownloads) { cancel() return } val (progress, progressMax) = getProgressBarValues(sbn) if (progress == 0 && progressMax == 0) { cancel() return } sendProgressToAccessibilityService(progress, progressMax) } private fun getProgressBarValues(sbn: StatusBarNotification): Pair { val progress = sbn.notification.extras.getInt(Notification.EXTRA_PROGRESS) val progressMax = sbn.notification.extras.getInt(Notification.EXTRA_PROGRESS_MAX) return Pair(progress, progressMax) } } ================================================ FILE: app/src/main/java/com/gustavoas/noti/notifications/GoogleTimerProgressBar.kt ================================================ package com.gustavoas.noti.notifications import android.content.Context import android.service.notification.StatusBarNotification import com.gustavoas.noti.ProgressBarAppsRepository class GoogleTimerProgressBar( ctx: Context, sbn: StatusBarNotification, appsRepository: ProgressBarAppsRepository ): TimedProgressBar(ctx, sbn, appsRepository) { override val priorityLevel: Int = 1 init { updateNotification(sbn) } override fun updateNotification(sbn: StatusBarNotification) { super.updateNotification(sbn) val sortKey = sbn.notification.sortKey if (sortKey == null || !sortKey.contains("RUNNING")) { cancel() return } val splitKey = sortKey.split("|") val timeLeft = splitKey.firstOrNull { it.contains("⏳") } ?: return val totalTime = splitKey.firstOrNull { it.contains("Σ") } ?: return val timeLeftMillis = parseTimeStringToMillis(timeLeft.substringAfter("⏳")) val totalTimeMillis = parseTimeStringToMillis(totalTime.substringAfter("Σ")) startUpdatingTimedPosition(timeLeftMillis, totalTimeMillis, -1f) } private fun parseTimeStringToMillis(time: String): Long { val timeSplit = time.trim().split(":") val hours = timeSplit.getOrNull(0)?.toLongOrNull() ?: 0 val minutes = timeSplit.getOrNull(1)?.toLongOrNull() ?: 0 val seconds = timeSplit.getOrNull(2)?.toLongOrNull() ?: 0 return (hours * 3600 + minutes * 60 + seconds) * 1000 } } ================================================ FILE: app/src/main/java/com/gustavoas/noti/notifications/MediaProgressBar.kt ================================================ package com.gustavoas.noti.notifications import android.content.Context import android.media.MediaMetadata import android.media.session.MediaController import android.media.session.MediaSession import android.media.session.PlaybackState import android.service.notification.StatusBarNotification import androidx.core.app.NotificationCompat import androidx.preference.PreferenceManager import com.gustavoas.noti.ProgressBarAppsRepository class MediaProgressBar( ctx: Context, sbn: StatusBarNotification, appsRepository: ProgressBarAppsRepository ): TimedProgressBar(ctx, sbn, appsRepository) { override val priorityLevel: Int = 0 private var mediaController: MediaController? = null private var mediaCallback: MediaController.Callback? = null init { updateNotification(sbn) } override fun updateNotification(sbn: StatusBarNotification) { super.updateNotification(sbn) val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(ctx) val enabledForMedia = sharedPrefs.getBoolean("showForMedia", true) if (!enabledForMedia) { cancel() return } val mediaSession = sbn.notification.extras.get(NotificationCompat.EXTRA_MEDIA_SESSION) as? MediaSession.Token if (mediaSession == null) { cancel() return } val newMediaController = MediaController(ctx, mediaSession) if (mediaController == null) { createProgressBarFromMedia(newMediaController) } } private fun createProgressBarFromMedia(newMediaController: MediaController) { mediaController = newMediaController if (mediaController?.playbackState?.state == PlaybackState.STATE_PLAYING) { startTrackingMediaPosition() } mediaCallback = object : MediaController.Callback() { override fun onPlaybackStateChanged(state: PlaybackState?) { super.onPlaybackStateChanged(state) when (state?.state) { PlaybackState.STATE_PLAYING -> { startTrackingMediaPosition() } PlaybackState.STATE_NONE, PlaybackState.STATE_STOPPED, PlaybackState.STATE_PAUSED, PlaybackState.STATE_ERROR -> { cancel() } else -> { stopUpdatingTimedPosition() } } } } mediaController?.registerCallback(mediaCallback as MediaController.Callback) } private fun startTrackingMediaPosition() { val artwork = mediaController?.metadata?.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART) artwork?.let { getColorFromBitmap(it)?.let { color -> notificationColor = color } } startUpdatingTimedPosition( mediaController?.playbackState?.position ?: 0, mediaController?.metadata?.getLong(MediaMetadata.METADATA_KEY_DURATION) ?: 0, mediaController?.playbackState?.playbackSpeed ?: 1f ) } override fun cancel() { super.cancel() if (mediaCallback != null) { mediaController?.unregisterCallback(mediaCallback as MediaController.Callback) mediaCallback = null } mediaController = null } } ================================================ FILE: app/src/main/java/com/gustavoas/noti/notifications/PercentageProgressBar.kt ================================================ package com.gustavoas.noti.notifications import android.app.Notification import android.content.Context import android.service.notification.StatusBarNotification import androidx.preference.PreferenceManager import com.gustavoas.noti.ProgressBarAppsRepository import kotlin.math.roundToInt class PercentageProgressBar( ctx: Context, sbn: StatusBarNotification, appsRepository: ProgressBarAppsRepository ): ProgressBarNotification(ctx, sbn, appsRepository) { override val priorityLevel: Int = 2 private var initialValue: Int? = null init { updateNotification(sbn) } override fun updateNotification(sbn: StatusBarNotification) { super.updateNotification(sbn) val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(ctx) val enabledForDownloads = sharedPrefs.getBoolean("showForDownloads", true) if (!enabledForDownloads) { cancel() return } val percentageProgress = getProgressFromPercentage(sbn) if (percentageProgress == 0) { cancel() return } if (initialValue != null && initialValue != percentageProgress) { sendProgressToAccessibilityService(percentageProgress, 100) } if (initialValue == null) { initialValue = percentageProgress } } private fun getProgressFromPercentage(sbn: StatusBarNotification): Int { val extras = sbn.notification.extras val title = extras.getCharSequence(Notification.EXTRA_TITLE)?.toString() ?: "" val text = extras.getCharSequence(Notification.EXTRA_TEXT)?.toString() ?: "" val subText = extras.getCharSequence(Notification.EXTRA_SUB_TEXT)?.toString() ?: "" val bigText = extras.getCharSequence(Notification.EXTRA_BIG_TEXT)?.toString() ?: "" val textLines = extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES) val percentageProgress = title.substringBefore("%").toFloatOrNull() ?: text.substringBefore("%").toFloatOrNull() ?: subText.substringBefore("%").toFloatOrNull() ?: bigText.split("\n").firstOrNull { it.contains("%") }?.toString() ?.substringBefore("%")?.toFloatOrNull() ?: textLines?.firstOrNull { it.contains("%") }?.toString() ?.substringBefore("%")?.toFloatOrNull() if (percentageProgress == null || percentageProgress.isNaN()) { return 0 } return percentageProgress.roundToInt() } } ================================================ FILE: app/src/main/java/com/gustavoas/noti/notifications/ProgressBarNotification.kt ================================================ package com.gustavoas.noti.notifications import android.app.Notification import android.content.Context import android.content.Intent import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable import android.os.Handler import android.os.Looper import android.service.notification.StatusBarNotification import androidx.core.graphics.createBitmap import androidx.palette.graphics.Palette import androidx.preference.PreferenceManager import com.gustavoas.noti.ProgressBarAppsRepository import com.gustavoas.noti.R import com.gustavoas.noti.Utils.getApplicationIcon import com.gustavoas.noti.model.ProgressBarApp import com.gustavoas.noti.model.ProgressNotification import com.gustavoas.noti.services.AccessibilityService import kotlin.math.roundToInt abstract class ProgressBarNotification( protected val ctx: Context, private var sbn: StatusBarNotification, private val appsRepository: ProgressBarAppsRepository ) { protected abstract val priorityLevel: Int protected var notificationColor = sbn.notification.color private val removalHandler = Handler(Looper.getMainLooper()) open fun updateNotification(sbn: StatusBarNotification) { this.sbn = sbn } open fun cancel() { removalHandler.removeCallbacksAndMessages(null) sendRemovalRequestToAccessibilityService() } protected fun sendProgressToAccessibilityService( progress: Int = 0, progressMax: Int = 0 ) { if (progressMax <= 0 || progress !in 0..progressMax) { return } val appInDatabase = getOrCreateAppInDatabase() updateProgressBarColor(appInDatabase) if (!appInDatabase.showProgressBar) { cancel() return } val progressBarMax = ctx.resources.getInteger(R.integer.progress_bar_max) val progressNormalized = if (progress == 0) { 0 } else { (progress.toFloat() / progressMax.toFloat() * progressBarMax).roundToInt() } val intent = Intent(ctx, AccessibilityService::class.java) intent.putExtra("id", sbn.key ?: "") intent.putExtra( "progressNotification", ProgressNotification( appInDatabase, progressNormalized, priorityLevel ) ) ctx.startService(intent) removalHandler.removeCallbacksAndMessages(null) removalHandler.postDelayed({ sendRemovalRequestToAccessibilityService() }, 10000) } private fun sendRemovalRequestToAccessibilityService() { val intent = Intent(ctx, AccessibilityService::class.java) intent.putExtra("id", sbn.key ?: "") intent.putExtra("removal", true) ctx.startService(intent) } private fun updateProgressBarColor(progressBarApp: ProgressBarApp) { val sharedPrefs = PreferenceManager.getDefaultSharedPreferences(ctx) val useNotificationColor = sharedPrefs.getBoolean("useNotificationColor", false) if (!progressBarApp.useDefaultColor || !useNotificationColor) { return } if (notificationColor == Notification.COLOR_DEFAULT) { val appIcon = getApplicationIcon(ctx, sbn.packageName) appIcon?.let { getColorFromBitmap(drawableToBitmap(it))?.let { color -> notificationColor = color } } } if (notificationColor != Notification.COLOR_DEFAULT && progressBarApp.color != notificationColor) { progressBarApp.color = notificationColor appsRepository.updateApp(progressBarApp) } } protected fun getColorFromBitmap(bitmap: Bitmap): Int? { val palette = Palette.from(bitmap).generate() val swatch = palette.lightMutedSwatch ?: palette.vibrantSwatch ?: palette.dominantSwatch return swatch?.rgb } private fun drawableToBitmap(drawable: Drawable): Bitmap { if (drawable is BitmapDrawable) return drawable.bitmap val width = drawable.intrinsicWidth.takeIf { it > 0 } ?: 1 val height = drawable.intrinsicHeight.takeIf { it > 0 } ?: 1 val bitmap = createBitmap(width, height) val canvas = Canvas(bitmap) drawable.setBounds(0, 0, canvas.width, canvas.height) drawable.draw(canvas) return bitmap } private fun getOrCreateAppInDatabase(): ProgressBarApp { return appsRepository.let { it.getApp(sbn.packageName ?: "") ?: it.addApp(ProgressBarApp(sbn.packageName, true)) } } } ================================================ FILE: app/src/main/java/com/gustavoas/noti/notifications/TimedProgressBar.kt ================================================ package com.gustavoas.noti.notifications import android.content.Context import android.os.Handler import android.os.Looper import android.service.notification.StatusBarNotification import com.gustavoas.noti.ProgressBarAppsRepository import kotlin.math.abs abstract class TimedProgressBar( ctx: Context, sbn: StatusBarNotification, appsRepository: ProgressBarAppsRepository ): ProgressBarNotification(ctx, sbn, appsRepository) { private val updateInterval = 1000 private val handler = Handler(Looper.getMainLooper()) private var updatesRunnable: Runnable? = null protected fun startUpdatingTimedPosition( initialPosition: Long, duration: Long, speed: Float ) { stopUpdatingTimedPosition() val initialTime = System.currentTimeMillis() updatesRunnable = object : Runnable { override fun run() { val currTime = System.currentTimeMillis() var currProgress = (initialPosition + (currTime - initialTime) * speed).toLong() if (currProgress !in 0..duration) { return } val updateStep = abs(updateInterval * speed).toInt() if (duration - currProgress < updateStep) { currProgress = duration } else if (currProgress < updateStep) { currProgress = 0 } sendProgressToAccessibilityService( currProgress.toInt(), duration.toInt(), ) updatesRunnable?.let { handler.postDelayed(it, updateInterval.toLong()) } } } updatesRunnable?.let { handler.post(it) } } protected fun stopUpdatingTimedPosition() { handler.removeCallbacksAndMessages(null) updatesRunnable = null } override fun cancel() { super.cancel() stopUpdatingTimedPosition() } } ================================================ FILE: app/src/main/java/com/gustavoas/noti/preferences/BannerPreference.kt ================================================ package com.gustavoas.noti.preferences import android.content.Context import android.util.AttributeSet import android.widget.Button import android.widget.ImageButton import androidx.preference.Preference import androidx.preference.PreferenceViewHolder import com.gustavoas.noti.R class BannerPreference(context: Context, attrs: AttributeSet): Preference(context, attrs) { var onBtnClick = { intent?.let { context.startActivity(intent) } } init { layoutResource = R.layout.banner_preference isSelectable = false } override fun onBindViewHolder(holder: PreferenceViewHolder) { super.onBindViewHolder(holder) val persistedValue = getPersistedBoolean(true) val preferenceHolder = holder.itemView if (!persistedValue) { this.isVisible = false return } val btn = preferenceHolder.findViewById