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
[
](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
[
](https://play.google.com/store/apps/details?id=com.gustavoas.noti)
## 💜 Help Translate
## 🔑 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