diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e6077cc..d735f5e 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -13,8 +13,8 @@ android {
applicationId = "app.akilesh.qacc"
minSdkVersion(AndroidSdk.min)
targetSdkVersion(AndroidSdk.target)
- versionCode = 6
- versionName = "1.40"
+ versionCode = 8
+ versionName = "1.50"
vectorDrawables.useSupportLibrary = true
}
buildFeatures {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3756dc2..c51f4c4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -13,8 +13,7 @@
android:supportsRtl="true"
android:theme="@style/AppTheme">
-
@@ -23,11 +22,7 @@
-
-
-
+
diff --git a/app/src/main/java/app/akilesh/qacc/App.kt b/app/src/main/java/app/akilesh/qacc/App.kt
index 59ac8e0..c077ef5 100644
--- a/app/src/main/java/app/akilesh/qacc/App.kt
+++ b/app/src/main/java/app/akilesh/qacc/App.kt
@@ -2,7 +2,7 @@ package app.akilesh.qacc
import android.app.Application
import androidx.preference.PreferenceManager
-import app.akilesh.qacc.utils.ThemeUtil
+import app.akilesh.qacc.utils.AppUtils
import com.topjohnwu.superuser.Shell
class App: Application() {
@@ -17,8 +17,8 @@ class App: Application() {
override fun onCreate() {
super.onCreate()
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
- val theme = sharedPreferences.getString("themePref", ThemeUtil.default)
- ThemeUtil.applyTheme(theme)
+ val theme = sharedPreferences.getString("themePref", AppUtils.default)
+ AppUtils.applyTheme(theme)
}
}
diff --git a/app/src/main/java/app/akilesh/qacc/Const.kt b/app/src/main/java/app/akilesh/qacc/Const.kt
index 670b285..9744831 100644
--- a/app/src/main/java/app/akilesh/qacc/Const.kt
+++ b/app/src/main/java/app/akilesh/qacc/Const.kt
@@ -1,34 +1,70 @@
package app.akilesh.qacc
+import android.os.Build
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.Q
import app.akilesh.qacc.model.Colour
+import com.topjohnwu.superuser.Shell
object Const {
//Credits to AEX
- val presetColors = listOf(
- Colour("#FFC107", "Amber"),
- Colour("#448AFF", "Blue"),
- Colour("#607D8B", "Blue Grey"),
- Colour("#795548", "Brown"),
- Colour("#FF1744", "Candy Red"),
- Colour("#00BCD4", "Cyan"),
- Colour("#FF5722", "Deep Orange"),
- Colour("#7C4DFF", "Deep Purple"),
- Colour("#47AE84", "Elegant Green"),
- Colour("#21EF8B", "Extended Green"),
- Colour("#9E9E9E", "Grey"),
- Colour("#536DFE", "Indigo"),
- Colour("#9ABC98", "Jade Green"),
- Colour("#03A9F4", "Light Blue"),
- Colour("#8BC34A", "Light Green"),
- Colour("#CDDC39", "Lime"),
- Colour("#FF9800", "Orange"),
- Colour("#A1B6ED", "Pale Blue"),
- Colour("#F05361", "Pale Red"),
- Colour("#FF4081", "Pink"),
- Colour("#FF5252", "Red"),
- Colour("#009688", "Teal"),
- Colour("#FFEB3B", "Yellow")
- )
+ object Colors {
+ val presets = listOf(
+ Colour("#FFC107", "Amber"),
+ Colour("#448AFF", "Blue"),
+ Colour("#607D8B", "Blue Grey"),
+ Colour("#795548", "Brown"),
+ Colour("#FF1744", "Candy Red"),
+ Colour("#00BCD4", "Cyan"),
+ Colour("#FF5722", "Deep Orange"),
+ Colour("#7C4DFF", "Deep Purple"),
+ Colour("#47AE84", "Elegant Green"),
+ Colour("#21EF8B", "Extended Green"),
+ Colour("#9E9E9E", "Grey"),
+ Colour("#536DFE", "Indigo"),
+ Colour("#9ABC98", "Jade Green"),
+ Colour("#03A9F4", "Light Blue"),
+ Colour("#8BC34A", "Light Green"),
+ Colour("#CDDC39", "Lime"),
+ Colour("#FF9800", "Orange"),
+ Colour("#A1B6ED", "Pale Blue"),
+ Colour("#F05361", "Pale Red"),
+ Colour("#FF4081", "Pink"),
+ Colour("#FF5252", "Red"),
+ Colour("#009688", "Teal"),
+ Colour("#FFEB3B", "Yellow")
+ )
+
+ }
+
+ object Links {
+ const val telegramGroup = "https://t.me/AccentColourCreator"
+ const val xdaThread =
+ "https://forum.xda-developers.com/android/apps-games/app-magisk-module-qacc-custom-accent-t4011747"
+ const val githubRepo = "https://github.com/Akilesh-T/ACC"
+ const val telegramChannel = "https://t.me/ACC_Releases"
+ const val githubReleases = "$githubRepo/releases/latest"
+ }
+
+ val overlayPath = if (SDK_INT == Q) "/data/adb/modules/qacc-mobile/system/product/overlay"
+ else "/data/adb/modules/qacc-mobile/system/vendor/overlay"
+
+ const val prefix = "com.android.theme.color.custom."
+
+ val isOOS = Shell.sh("getprop ro.oxygen.version").exec().out.component1().isNotBlank()
+
+ fun getAssetFiles(): MutableList {
+
+ val assetFiles = mutableListOf()
+
+ val arch = if (listOf(Build.SUPPORTED_64_BIT_ABIS).isNotEmpty()) "arm64" else "arm"
+ if (arch == "arm64")
+ assetFiles.addAll(listOf("aapt64", "zipalign64"))
+ else
+ assetFiles.addAll(listOf("aapt", "zipalign"))
+
+ return assetFiles
+ }
}
diff --git a/app/src/main/java/app/akilesh/qacc/MainActivity.kt b/app/src/main/java/app/akilesh/qacc/MainActivity.kt
deleted file mode 100644
index e351b8f..0000000
--- a/app/src/main/java/app/akilesh/qacc/MainActivity.kt
+++ /dev/null
@@ -1,405 +0,0 @@
-package app.akilesh.qacc
-
-import android.app.WallpaperColors
-import android.app.WallpaperManager
-import android.app.WallpaperManager.FLAG_SYSTEM
-import android.content.Intent
-import android.content.res.ColorStateList
-import android.content.res.Configuration
-import android.graphics.Color
-import android.os.Build
-import android.os.Build.VERSION.SDK_INT
-import android.os.Build.VERSION_CODES.O
-import android.os.Build.VERSION_CODES.Q
-import android.os.Bundle
-import android.os.Handler
-import android.util.Log
-import android.view.View
-import android.view.View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
-import android.view.View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
-import android.view.WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
-import android.widget.Toast
-import androidx.appcompat.app.AppCompatActivity
-import androidx.core.widget.doAfterTextChanged
-import androidx.lifecycle.Observer
-import androidx.lifecycle.ViewModelProvider
-import androidx.palette.graphics.Palette
-import androidx.recyclerview.widget.ItemTouchHelper
-import androidx.recyclerview.widget.LinearLayoutManager
-import androidx.recyclerview.widget.RecyclerView
-import app.akilesh.qacc.Const.presetColors
-import app.akilesh.qacc.adapter.AccentListAdapter
-import app.akilesh.qacc.adapter.ColorListAdapter
-import app.akilesh.qacc.databinding.ActivityMainBinding
-import app.akilesh.qacc.databinding.ColorPreviewBinding
-import app.akilesh.qacc.databinding.DialogTitleBinding
-import app.akilesh.qacc.model.Accent
-import app.akilesh.qacc.model.Colour
-import app.akilesh.qacc.signing.ByteArrayStream
-import app.akilesh.qacc.signing.JarMap
-import app.akilesh.qacc.signing.SignAPK
-import app.akilesh.qacc.utils.SwipeToDeleteCallback
-import app.akilesh.qacc.viewmodel.AccentViewModel
-import com.afollestad.assent.Permission
-import com.afollestad.assent.rationale.createDialogRationale
-import com.afollestad.assent.runWithPermissions
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import com.google.android.material.snackbar.Snackbar
-import com.topjohnwu.superuser.Shell
-import me.priyesh.chroma.ChromaDialog
-import me.priyesh.chroma.ColorMode
-import me.priyesh.chroma.ColorSelectListener
-import org.bouncycastle.asn1.ASN1InputStream
-import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
-import java.io.*
-import java.security.GeneralSecurityException
-import java.security.KeyFactory
-import java.security.PrivateKey
-import java.security.cert.CertificateFactory
-import java.security.cert.X509Certificate
-import java.security.spec.PKCS8EncodedKeySpec
-
-
-class MainActivity : AppCompatActivity() {
-
- private val assetFiles = mutableListOf(
- "AndroidManifest.xml",
- "src/values/colors.xml",
- "src/values/strings.xml"
- )
-
- private lateinit var binding: ActivityMainBinding
- private lateinit var accentViewModel: AccentViewModel
- private lateinit var path: String
- private val prefix = "com.android.theme.color.custom."
- private var accentColor = ""
- private var accentName = ""
- private var createHint = "Tap to create accent"
-
-
- override fun onCreate(savedInstanceState: Bundle?) {
-
- super.onCreate(savedInstanceState)
- binding = ActivityMainBinding.inflate(layoutInflater)
- setContentView(binding.root)
-
- copyAssets()
- path = if (SDK_INT == Q) "/data/adb/modules/qacc-mobile/system/product/overlay"
- else "/data/adb/modules/qacc-mobile/system/vendor/overlay"
-
- val adapter = AccentListAdapter(this)
- binding.recyclerView.adapter = adapter
- binding.recyclerView.layoutManager = LinearLayoutManager(this)
-
- accentViewModel = ViewModelProvider(this).get(AccentViewModel::class.java)
- accentViewModel.allAccents.observe(this, Observer { accents ->
- accents?.let { adapter.setAccents(it) }
- })
-
- val swipeHandler = object : SwipeToDeleteCallback(this) {
- override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
- val accent = adapter.getAccentAndRemoveAt(viewHolder.adapterPosition)
- accentViewModel.delete(accent)
- val appName = accent.pkgName.substringAfter(prefix)
- val result = Shell.su("rm -f $path/$appName.apk").exec()
- if (result.isSuccess)
- showSnackbar("${accent.name} removed.")
- }
- }
- val itemTouchHelper = ItemTouchHelper(swipeHandler)
- itemTouchHelper.attachToRecyclerView(binding.recyclerView)
-
- if (SDK_INT == O)
- binding.wallFrame.visibility = View.GONE
-
- val decorView = window.decorView
- decorView.systemUiVisibility = FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
-
- when(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
- Configuration.UI_MODE_NIGHT_NO -> {
- decorView.systemUiVisibility = SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
- }
- }
-
- binding.custom.setOnClickListener { setCustomColor() }
- binding.preset.setOnClickListener { chooseFromPresets() }
- binding.create.setOnClickListener { createAccent() }
- if (SDK_INT > O) binding.wallColors.setOnClickListener { chooseFromWallpaperColors() }
-
- binding.fab.setOnClickListener {
- val settingsIntent = Intent(this, SettingsActivity::class.java)
- startActivity(settingsIntent)
- }
-
- binding.name.doAfterTextChanged {
- accentName = it.toString().trim()
- binding.previewSelectedText.text = String.format(resources.getString(R.string.create_accent), accentName, accentColor, createHint)
- }
-
- }
-
- private fun copyAssets() {
- val arch = if ( listOf(Build.SUPPORTED_64_BIT_ABIS).isNotEmpty() ) "arm64" else "arm"
- if (arch == "arm64")
- assetFiles.addAll( listOf("aapt64", "xmlstarlet64", "zipalign64") )
- else
- assetFiles.addAll( listOf("aapt", "xmlstarlet", "zipalign") )
-
- Log.d("assets", assetFiles.toString())
- assetFiles.forEach {
- val file = it.removeSuffix("64")
- copyFromAsset(file)
- }
- }
-
- private fun copyFromAsset(filename: String) {
- if( !File("$filesDir/src/values").exists() )
- File("$filesDir/src/values").mkdirs()
- assets.open(filename).use { stream ->
- File("${filesDir}/$filename").outputStream().use {
- stream.copyTo(it)
- }
- }
- }
-
- private fun setCustomColor() {
-
- ChromaDialog.Builder()
- .initialColor(Color.parseColor("#FF2800"))
- .colorMode(ColorMode.RGB)
- .onColorSelected(object : ColorSelectListener {
- override fun onColorSelected(color: Int) {
- accentColor = toHex(color)
- accentName = ""
- binding.create.backgroundTintList = ColorStateList.valueOf(color)
- val backgroundColor = Color.parseColor(accentColor)
- val textColor = Palette.Swatch(backgroundColor, 1).bodyTextColor
- binding.previewSelectedText.setTextColor(textColor)
- binding.previewSelectedText.text = String.format(resources.getString(R.string.create_accent), accentName, accentColor, createHint)
- }
- })
- .create()
- .show(supportFragmentManager, "ChromaDialog")
-
- }
-
- private fun chooseFromPresets() {
-
- val colorPreviewBinding = ColorPreviewBinding.inflate(layoutInflater)
- val dialogTitleBinding = DialogTitleBinding.inflate(layoutInflater)
- dialogTitleBinding.titleText.text = String.format(resources.getString(R.string.presets))
- dialogTitleBinding.titleIcon.setImageResource(R.drawable.ic_preset)
- val builder = MaterialAlertDialogBuilder(this)
- .setCustomTitle(dialogTitleBinding.root)
- .setView(colorPreviewBinding.root)
- val dialog = builder.create()
-
- val adapter = ColorListAdapter(this, presetColors) { colour ->
- accentColor = colour.hex
- accentName = colour.name
- val backgroundColor = Color.parseColor(accentColor)
- val textColor = Palette.Swatch(backgroundColor, 1).bodyTextColor
- binding.create.backgroundTintList = ColorStateList.valueOf(backgroundColor)
- binding.previewSelectedText.setTextColor(textColor)
- binding.previewSelectedText.text = String.format(resources.getString(R.string.create_accent), accentName, accentColor, createHint)
- dialog.cancel()
- }
-
- colorPreviewBinding.recyclerViewColor.adapter = adapter
- colorPreviewBinding.recyclerViewColor.layoutManager = LinearLayoutManager(this)
-
- dialog.show()
- }
-
-
- private fun chooseFromWallpaperColors() {
- if (SDK_INT > O) {
-
- val rationaleHandler = createDialogRationale(R.string.app_name_full) {
- onPermission(
- Permission.READ_EXTERNAL_STORAGE,
- "Storage permission is required to get wallpaper colours."
- )
- }
-
- runWithPermissions(
- Permission.READ_EXTERNAL_STORAGE,
- rationaleHandler = rationaleHandler
- ) {
- if (it.isAllGranted()) {
- val wallpaperManager = WallpaperManager.getInstance(this)
- val wallDrawable = wallpaperManager.drawable
- var wallColors = wallpaperManager.getWallpaperColors(FLAG_SYSTEM)!!
-
- val colorsChangedListener = WallpaperManager.OnColorsChangedListener { colors, _ ->
- wallColors = colors ?: WallpaperColors.fromDrawable(wallDrawable)
- }
- wallpaperManager.addOnColorsChangedListener(colorsChangedListener, Handler())
-
- val primary = wallColors.primaryColor.toArgb()
- val secondary = wallColors.secondaryColor?.toArgb()
- val tertiary = wallColors.tertiaryColor?.toArgb()
-
- val primaryHex = toHex(primary)
- val wallpaperColours = mutableListOf(Colour(primaryHex, "Wallpaper primary"))
- if (secondary != null) {
- val secondaryHex = toHex(secondary)
- wallpaperColours.add(Colour(secondaryHex, "Wallpaper secondary"))
- }
- if (tertiary != null) {
- val tertiaryHex = toHex(tertiary)
- wallpaperColours.add(Colour(tertiaryHex, "Wallpaper tertiary"))
- }
-
- val colorPreviewBinding = ColorPreviewBinding.inflate(layoutInflater)
- val dialogTitleBinding = DialogTitleBinding.inflate(layoutInflater)
- dialogTitleBinding.titleText.text = String.format(resources.getString(R.string.color_wallpaper))
- dialogTitleBinding.titleIcon.setImageResource(R.drawable.ic_wallpaper)
- val builder = MaterialAlertDialogBuilder(this)
- .setCustomTitle(dialogTitleBinding.root)
- .setView(colorPreviewBinding.root)
- val dialog = builder.create()
-
- val adapter = ColorListAdapter(this, wallpaperColours) { colour ->
- accentColor = colour.hex
- accentName = colour.name
- val backgroundColor = Color.parseColor(accentColor)
- val textColor = Palette.Swatch(backgroundColor, 1).bodyTextColor
- binding.create.backgroundTintList = ColorStateList.valueOf(backgroundColor)
- binding.previewSelectedText.setTextColor(textColor)
- binding.previewSelectedText.text = String.format(resources.getString(R.string.create_accent), accentName, accentColor, createHint)
- dialog.cancel()
- }
-
- colorPreviewBinding.recyclerViewColor.adapter = adapter
- colorPreviewBinding.recyclerViewColor.layoutManager = LinearLayoutManager(this)
-
- dialog.show()
- }
- }
- }
- }
-
- private fun createAccent() {
- if (accentColor.isNotBlank() && accentName.isNotBlank()) {
-
- val suffix = "hex" + accentColor.removePrefix("#")
- val pkgName = prefix + suffix
- Log.d("pkg-name", pkgName)
-
- val xmlRes = Shell.su(
- "cd ${filesDir.absolutePath}",
- "chmod +x xmlstarlet",
- "./xmlstarlet ed -L -u '/manifest/@package' -v \"$pkgName\" AndroidManifest.xml",
- "./xmlstarlet ed -L -u '/resources/color[@name=\"accent_device_default_light\"]' -v \"$accentColor\" src/values/colors.xml",
- "./xmlstarlet ed -L -u '/resources/color[@name=\"accent_device_default_dark\"]' -v \"$accentColor\" src/values/colors.xml",
- "./xmlstarlet ed -L -u '/resources/color[@name=\"accent_device_default_700\"]' -v \"$accentColor\" src/values/colors.xml",
- "./xmlstarlet ed -L -u '/resources/string[@name=\"accent_color_custom_overlay\"]' -v \"$accentName\" src/values/strings.xml",
- "cd /"
- ).exec()
- Log.d("ACC-xml", xmlRes.out.toString())
-
- if (xmlRes.isSuccess) {
- //Toast.makeText(this, "Building overlay apk", Toast.LENGTH_SHORT).show()
- Shell.su("cd ${filesDir.absolutePath}").exec()
- val ovrRes = Shell.su(resources.openRawResource(R.raw.create_overlay)).exec()
- Log.d("ACC-ovr", ovrRes.out.toString())
-
- if (ovrRes.isSuccess) {
- val certFile = assets.open("testkey.x509.pem")
- val keyFile = assets.open("testkey.pk8")
- val out = FileOutputStream(File(filesDir, "signed.apk").absolutePath)
-
- val cert = readCertificate(certFile)
- val key = readPrivateKey(keyFile)
-
- val jar = JarMap.open("$filesDir/qacc.apk")
-
- SignAPK.sign(cert, key, jar, out.buffered())
-
- Shell.su("cd ${filesDir.absolutePath}").exec()
- val zipalignRes = Shell.su(resources.openRawResource(R.raw.zipalign)).exec()
- Log.d("ACC-zip", zipalignRes.out.toString())
-
- if (zipalignRes.isSuccess) {
- //Toast.makeText(this, "Creating Magisk module", Toast.LENGTH_SHORT).show()
-
- Shell.su("mkdir -p $path").exec()
- Shell.su(resources.openRawResource(R.raw.create_module)).exec()
- val result = Shell.su(
- "cp -f $filesDir/aligned.apk $path/$suffix.apk",
- "chmod 644 $path/$suffix.apk"
- ).exec()
- Log.d("ACC-MM", result.out.toString())
-
- if (result.isSuccess) {
-
- val createdApks = listOf("qacc.apk", "signed.apk", "aligned.apk")
- createdApks.forEach {
- File(filesDir, it).delete()
- }
- val accent = Accent(pkgName, accentName, accentColor)
- accentViewModel.insert(accent)
- showSnackbar("$accentName created.")
-
- }
- }
- }
- }
- }
-
- else {
- if (accentColor.isBlank()) Toast.makeText(this, "Accent color is not selected", Toast.LENGTH_SHORT).show()
- if (accentName.isBlank()) Toast.makeText(this, "Accent name is not set", Toast.LENGTH_SHORT).show()
- }
- }
-
- private fun showSnackbar(text: String) {
-
- Snackbar.make(
- binding.root,
- text,
- Snackbar.LENGTH_INDEFINITE
- )
- .setAction("Reboot") {
- Shell.su("/system/bin/svc power reboot || /system/bin/reboot")
- .submit()
- }
- .show()
- }
-
- private fun toHex(color: Int): String {
- return String.format("#%06X", (0xFFFFFF and color))
- }
-
- @Throws(IOException::class, GeneralSecurityException::class)
- fun readCertificate(inputStream: InputStream): X509Certificate {
- inputStream.use { stream ->
- val cf = CertificateFactory.getInstance("X.509")
- return cf.generateCertificate(stream) as X509Certificate
- }
- }
-
-
- @Throws(IOException::class, GeneralSecurityException::class)
- fun readPrivateKey(inputStream: InputStream): PrivateKey {
- inputStream.use { stream ->
- val buf = ByteArrayStream()
- buf.readFrom(stream)
- val bytes = buf.toByteArray()
- // Check to see if this is in an EncryptedPrivateKeyInfo structure.
- val spec = PKCS8EncodedKeySpec(bytes)
- /*
- * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
- * OID and use that to construct a KeyFactory.
- */
- val bIn = ASN1InputStream(ByteArrayInputStream(spec.encoded))
- val pki = PrivateKeyInfo.getInstance(bIn.readObject())
- val algOid = pki.privateKeyAlgorithm.algorithm.id
- return KeyFactory.getInstance(algOid).generatePrivate(spec)
- }
- }
-}
-
-
-
diff --git a/app/src/main/java/app/akilesh/qacc/SettingsActivity.kt b/app/src/main/java/app/akilesh/qacc/SettingsActivity.kt
deleted file mode 100644
index 47db7c2..0000000
--- a/app/src/main/java/app/akilesh/qacc/SettingsActivity.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package app.akilesh.qacc
-
-import android.content.res.Configuration
-import android.os.Bundle
-import android.view.View
-import android.view.WindowManager
-import androidx.appcompat.app.AppCompatActivity
-import androidx.preference.ListPreference
-import androidx.preference.Preference
-import androidx.preference.PreferenceFragmentCompat
-import app.akilesh.qacc.databinding.SettingsActivityBinding
-import app.akilesh.qacc.utils.ThemeUtil
-
-class SettingsActivity : AppCompatActivity() {
-
- private lateinit var settingsActivityBinding: SettingsActivityBinding
-
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- settingsActivityBinding = SettingsActivityBinding.inflate(layoutInflater)
- setContentView(settingsActivityBinding.root)
-
- val decorView = window.decorView
- decorView.systemUiVisibility = WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
-
- when(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
- Configuration.UI_MODE_NIGHT_NO -> {
- decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
- }
- }
-
- supportFragmentManager
- .beginTransaction()
- .replace(settingsActivityBinding.settings.id, SettingsFragment())
- .commit()
- }
-
- override fun onSupportNavigateUp(): Boolean {
- onBackPressed()
- return true
- }
-
- class SettingsFragment : PreferenceFragmentCompat() {
- override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
- setPreferencesFromResource(R.xml.root_preferences, rootKey)
-
- val themePreference = findPreference("themePref")
- if (themePreference != null) {
- themePreference.onPreferenceChangeListener = Preference.OnPreferenceChangeListener { _, newValue ->
- val theme = newValue as String
- ThemeUtil.applyTheme(theme)
- true
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/app/akilesh/qacc/db/AccentDatabase.kt b/app/src/main/java/app/akilesh/qacc/db/AccentDatabase.kt
index 5c0811d..9dace1b 100644
--- a/app/src/main/java/app/akilesh/qacc/db/AccentDatabase.kt
+++ b/app/src/main/java/app/akilesh/qacc/db/AccentDatabase.kt
@@ -4,10 +4,12 @@ import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
+import androidx.room.migration.Migration
+import androidx.sqlite.db.SupportSQLiteDatabase
import app.akilesh.qacc.db.dao.AccentDao
import app.akilesh.qacc.model.Accent
-@Database(entities = [Accent::class], version = 1, exportSchema = false)
+@Database(entities = [Accent::class], version = 2, exportSchema = false)
abstract class AccentDatabase: RoomDatabase() {
abstract fun accentDao(): AccentDao
@@ -17,6 +19,12 @@ abstract class AccentDatabase: RoomDatabase() {
@Volatile
private var INSTANCE: AccentDatabase? = null
+ val MIGRATION_1_2 = object : Migration(1, 2) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL("ALTER TABLE accent_colors ADD COLUMN color_dark TEXT NOT NULL DEFAULT ''")
+ }
+ }
+
fun getDatabase(context: Context): AccentDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
@@ -27,7 +35,9 @@ abstract class AccentDatabase: RoomDatabase() {
context.applicationContext,
AccentDatabase::class.java,
"accent_database"
- ).build()
+ )
+ .addMigrations(MIGRATION_1_2)
+ .build()
INSTANCE = instance
return instance
}
diff --git a/app/src/main/java/app/akilesh/qacc/model/Accent.kt b/app/src/main/java/app/akilesh/qacc/model/Accent.kt
index 7a1f648..57777d1 100644
--- a/app/src/main/java/app/akilesh/qacc/model/Accent.kt
+++ b/app/src/main/java/app/akilesh/qacc/model/Accent.kt
@@ -8,5 +8,6 @@ import androidx.room.PrimaryKey
data class Accent(
@PrimaryKey @ColumnInfo(name = "package_name") val pkgName: String,
@ColumnInfo(name = "name") val name: String,
- @ColumnInfo(name = "color") val color: String
+ @ColumnInfo(name = "color") val colorLight: String,
+ @ColumnInfo(name = "color_dark") val colorDark: String
)
diff --git a/app/src/main/java/app/akilesh/qacc/signing/ByteArrayStream.java b/app/src/main/java/app/akilesh/qacc/signing/ByteArrayStream.java
index 036771d..991628b 100644
--- a/app/src/main/java/app/akilesh/qacc/signing/ByteArrayStream.java
+++ b/app/src/main/java/app/akilesh/qacc/signing/ByteArrayStream.java
@@ -18,7 +18,7 @@ public synchronized void readFrom(InputStream is) {
public synchronized void readFrom(InputStream is, int len) {
int read;
- byte buffer[] = new byte[4096];
+ byte[] buffer = new byte[4096];
try {
while ((read = is.read(buffer, 0, Math.min(len, buffer.length))) > 0) {
write(buffer, 0, read);
diff --git a/app/src/main/java/app/akilesh/qacc/SplashActivity.kt b/app/src/main/java/app/akilesh/qacc/ui/LaunchActivity.kt
similarity index 89%
rename from app/src/main/java/app/akilesh/qacc/SplashActivity.kt
rename to app/src/main/java/app/akilesh/qacc/ui/LaunchActivity.kt
index 7c424ba..3d7fac3 100644
--- a/app/src/main/java/app/akilesh/qacc/SplashActivity.kt
+++ b/app/src/main/java/app/akilesh/qacc/ui/LaunchActivity.kt
@@ -1,14 +1,14 @@
-package app.akilesh.qacc
+package app.akilesh.qacc.ui
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
+import app.akilesh.qacc.R
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.topjohnwu.superuser.Shell
-
-class SplashActivity : AppCompatActivity() {
+class LaunchActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -25,7 +25,8 @@ class SplashActivity : AppCompatActivity() {
*/
MaterialAlertDialogBuilder(this)
.setTitle("Unable to get root access")
- .setMessage("Please ensure that:\n1. Your device is rooted using Magisk.\n2. ${getString(R.string.app_name)} is granted root access.")
+ .setMessage("Please ensure that:\n1. Your device is rooted using Magisk.\n2. ${getString(
+ R.string.app_name)} is granted root access.")
.setCancelable(false)
.setNegativeButton("Exit") { _, _ ->
finish()
diff --git a/app/src/main/java/app/akilesh/qacc/ui/MainActivity.kt b/app/src/main/java/app/akilesh/qacc/ui/MainActivity.kt
new file mode 100644
index 0000000..dc5e2c9
--- /dev/null
+++ b/app/src/main/java/app/akilesh/qacc/ui/MainActivity.kt
@@ -0,0 +1,102 @@
+package app.akilesh.qacc.ui
+
+import android.content.res.Configuration
+import android.os.Bundle
+import android.util.Log
+import android.view.View
+import android.view.WindowManager
+import androidx.appcompat.app.AppCompatActivity
+import androidx.navigation.fragment.NavHostFragment
+import androidx.navigation.navOptions
+import app.akilesh.qacc.Const.getAssetFiles
+import app.akilesh.qacc.R
+import app.akilesh.qacc.databinding.ActivityMainBinding
+import java.io.File
+
+class MainActivity: AppCompatActivity() {
+
+ private lateinit var binding: ActivityMainBinding
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ binding = ActivityMainBinding.inflate(layoutInflater)
+ setContentView(binding.root)
+
+ val decorView = window.decorView
+ decorView.systemUiVisibility = WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS
+
+ when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
+ Configuration.UI_MODE_NIGHT_NO -> {
+ decorView.systemUiVisibility =
+ View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR
+ }
+ }
+
+ val navHostFragment = supportFragmentManager.findFragmentById(R.id.fragment) as NavHostFragment
+ val navController = navHostFragment.navController
+
+
+ // Hide bottom app bar & ext. fab while creating an accent
+ navController.addOnDestinationChangedListener { _, destination, _ ->
+ when(destination.id) {
+ R.id.color_picker, R.id.dark_accent, R.id.customisation -> {
+ binding.bottomAppBar.visibility = View.GONE
+ binding.xFab.visibility = View.GONE
+ }
+ else -> {
+ binding.bottomAppBar.visibility = View.VISIBLE
+ binding.xFab.visibility = View.VISIBLE
+ }
+ }
+ }
+
+ val navOptions = navOptions {
+ anim {
+ // Animations from Android 10
+ enter = R.anim.fragment_enter
+ exit = R.anim.fragment_exit
+ popEnter = R.anim.fragment_enter_pop
+ popExit = R.anim.fragment_exit_pop
+ }
+ }
+
+ binding.xFab.setOnClickListener {
+ navController.navigate(R.id.color_picker, null, navOptions)
+ }
+
+ binding.bottomAppBar.setOnMenuItemClickListener {
+ when(it.itemId) {
+ R.id.settings -> navController.navigate(R.id.settings, null, navOptions)
+ R.id.info -> navController.navigate(R.id.info, null, navOptions)
+ }
+ true
+ }
+
+ /*
+ * Use navigation icon to navigate home.
+ * May not be the correct way, but convenient.
+ */
+ binding.bottomAppBar.setNavigationOnClickListener {
+ navController.navigate(R.id.home, null, navOptions)
+ }
+
+ copyAssets()
+ }
+
+ private fun copyAssets() {
+ if( !File("$filesDir/src/values").exists() )
+ File("$filesDir/src/values").mkdirs()
+
+ val assetFiles = getAssetFiles()
+ Log.d("assets", assetFiles.toString())
+ assetFiles.forEach {
+ val file = it.removeSuffix("64")
+ assets.open(file).use { stream ->
+ File("${filesDir}/$file").outputStream().use { fileOutputStream ->
+ stream.copyTo(fileOutputStream)
+ }
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/akilesh/qacc/ui/fragments/ColorCustomisationFragment.kt b/app/src/main/java/app/akilesh/qacc/ui/fragments/ColorCustomisationFragment.kt
new file mode 100644
index 0000000..113b424
--- /dev/null
+++ b/app/src/main/java/app/akilesh/qacc/ui/fragments/ColorCustomisationFragment.kt
@@ -0,0 +1,185 @@
+package app.akilesh.qacc.ui.fragments
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.os.Bundle
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.core.graphics.ColorUtils
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
+import androidx.palette.graphics.Palette
+import androidx.preference.PreferenceManager
+import app.akilesh.qacc.Const.isOOS
+import app.akilesh.qacc.Const.prefix
+import app.akilesh.qacc.R
+import app.akilesh.qacc.databinding.ColorCustomisationFragmentBinding
+import app.akilesh.qacc.model.Accent
+import app.akilesh.qacc.utils.AppUtils.createAccent
+import app.akilesh.qacc.utils.AppUtils.showSnackbar
+import app.akilesh.qacc.utils.AppUtils.toHex
+import app.akilesh.qacc.viewmodel.AccentViewModel
+import app.akilesh.qacc.viewmodel.CustomisationViewModel
+import com.topjohnwu.superuser.Shell
+import kotlin.properties.Delegates
+
+class ColorCustomisationFragment: Fragment() {
+
+ private lateinit var binding: ColorCustomisationFragmentBinding
+ private lateinit var model: CustomisationViewModel
+ private lateinit var accentViewModel: AccentViewModel
+ private val args: ColorCustomisationFragmentArgs by navArgs()
+ private var colorLight by Delegates.notNull()
+ private var colorDark by Delegates.notNull()
+ private var separateAccents by Delegates.notNull()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ binding = ColorCustomisationFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+ separateAccents = sharedPreferences.getBoolean("separate_accent", false)
+
+ var accentLight = args.lightAccent
+ var accentDark = args.darkAccent
+ colorLight = Color.parseColor(accentLight)
+ setPreviewLight(colorLight, accentLight)
+ model = ViewModelProvider(this).get(CustomisationViewModel::class.java)
+ accentViewModel = ViewModelProvider(this).get(AccentViewModel::class.java)
+
+ val lightAccentObserver = Observer {
+ accentLight = it
+ colorLight = Color.parseColor(accentLight)
+ setPreviewLight(colorLight, accentLight)
+ }
+ model.lightAccent.observe(viewLifecycleOwner, lightAccentObserver)
+
+ if (!separateAccents) {
+ binding.chipGroup.visibility = View.GONE
+ binding.previewDark.root.visibility = View.GONE
+ updateColor(false)
+
+ }
+ else {
+ colorDark = Color.parseColor(accentDark)
+ setPreviewDark(colorDark, accentDark)
+
+ val darkAccentObserver = Observer {
+ accentDark = it
+ colorDark = Color.parseColor(accentDark)
+ setPreviewDark(colorDark, accentDark)
+ }
+ model.darkAccent.observe(viewLifecycleOwner, darkAccentObserver)
+ }
+
+ binding.chipGroup.setOnCheckedChangeListener { _, checkedId ->
+ when(checkedId) {
+ binding.lightChip.id -> {
+ updateColor( false)
+ }
+ binding.darkChip.id -> {
+ updateColor( true)
+ }
+ }
+ }
+
+ binding.resetChip.setOnClickListener {
+ val action = ColorCustomisationFragmentDirections.reset(args.lightAccent, args.darkAccent, args.accentName)
+ findNavController().navigate(action)
+ }
+
+ binding.buttonPrevious.setOnClickListener {
+ findNavController().navigateUp()
+ }
+
+ binding.buttonNext.setOnClickListener {
+
+ if (isOOS) {
+ val result = Shell.su("settings put system oem_black_mode_accent_color \'$accentLight\'")
+ .exec()
+ if (result.isSuccess) {
+ showSnackbar(view, "$accentLight set")
+ findNavController().navigate(R.id.back_home)
+ }
+ }
+
+ var suffix = "hex_" + accentLight.removePrefix("#")
+ val dark: String
+ if (separateAccents) {
+ suffix += "_" + accentDark.removePrefix("#")
+ dark = accentDark
+ }
+ else dark = accentLight
+ val pkgName = prefix + suffix
+ val accent = Accent(pkgName, args.accentName, accentLight, dark)
+ Log.d("accent", accent.toString())
+ if (createAccent(context!!, accentViewModel, accent)) {
+ showSnackbar(view, "${args.accentName} created")
+ findNavController().navigate(R.id.to_home)
+ }
+ }
+
+ }
+
+ private fun setPreviewLight(color: Int, hex: String) {
+ val colorName = if (separateAccents) context!!.resources.getString(R.string.light) else args.accentName
+ binding.previewLight.colorName.text = String.format(context!!.resources.getString(R.string.colour), colorName, hex)
+ val textColorLight = Palette.Swatch(color, 1).bodyTextColor
+ binding.previewLight.colorName.setTextColor(textColorLight)
+ binding.previewLight.colorCard.backgroundTintList = ColorStateList.valueOf(color)
+ }
+
+ private fun setPreviewDark(color: Int, hex: String) {
+ binding.previewDark.colorName.text = String.format(context!!.resources.getString(R.string.colour), context!!.resources.getString(R.string.dark), hex)
+ val textColorDark = Palette.Swatch(color, 1).bodyTextColor
+ binding.previewDark.colorName.setTextColor(textColorDark)
+ binding.previewDark.colorCard.backgroundTintList = ColorStateList.valueOf(color)
+ }
+
+ private fun updateColor(isDark: Boolean) {
+
+ var newColor: Int
+ val hsl = FloatArray(3)
+ ColorUtils.colorToHSL(if (isDark) colorDark else colorLight, hsl)
+
+ binding.hue.setOnChangeListener { _, value ->
+ hsl[0] = value
+ newColor = ColorUtils.HSLToColor(hsl)
+ if (isDark)
+ model.darkAccent.value = toHex(newColor)
+ else
+ model.lightAccent.value = toHex(newColor)
+ }
+
+ binding.saturation.setOnChangeListener { _, value ->
+ hsl[1] = value
+ newColor = ColorUtils.HSLToColor(hsl)
+ if (isDark)
+ model.darkAccent.value = toHex(newColor)
+ else
+ model.lightAccent.value = toHex(newColor)
+ }
+
+ binding.lightness.setOnChangeListener { _, value ->
+ hsl[2] = value
+ newColor = ColorUtils.HSLToColor(hsl)
+ if (isDark)
+ model.darkAccent.value = toHex(newColor)
+ else
+ model.lightAccent.value = toHex(newColor)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/akilesh/qacc/ui/fragments/ColorPickerFragment.kt b/app/src/main/java/app/akilesh/qacc/ui/fragments/ColorPickerFragment.kt
new file mode 100644
index 0000000..d97d265
--- /dev/null
+++ b/app/src/main/java/app/akilesh/qacc/ui/fragments/ColorPickerFragment.kt
@@ -0,0 +1,287 @@
+package app.akilesh.qacc.ui.fragments
+
+import android.app.WallpaperColors
+import android.app.WallpaperManager
+import android.app.WallpaperManager.FLAG_SYSTEM
+import android.graphics.Color
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.O
+import android.os.Build.VERSION_CODES.Q
+import android.os.Bundle
+import android.os.Handler
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.core.widget.doAfterTextChanged
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import androidx.preference.PreferenceManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import app.akilesh.qacc.Const.Colors.presets
+import app.akilesh.qacc.Const.isOOS
+import app.akilesh.qacc.Const.prefix
+import app.akilesh.qacc.R
+import app.akilesh.qacc.databinding.ColorPickerFragmentBinding
+import app.akilesh.qacc.databinding.ColorPreviewBinding
+import app.akilesh.qacc.databinding.DialogTitleBinding
+import app.akilesh.qacc.model.Accent
+import app.akilesh.qacc.model.Colour
+import app.akilesh.qacc.ui.adapter.ColorListAdapter
+import app.akilesh.qacc.utils.AppUtils.createAccent
+import app.akilesh.qacc.utils.AppUtils.getColorAccent
+import app.akilesh.qacc.utils.AppUtils.setPreview
+import app.akilesh.qacc.utils.AppUtils.showSnackbar
+import app.akilesh.qacc.utils.AppUtils.toHex
+import app.akilesh.qacc.viewmodel.AccentViewModel
+import com.afollestad.assent.Permission
+import com.afollestad.assent.rationale.createDialogRationale
+import com.afollestad.assent.runWithPermissions
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import com.topjohnwu.superuser.Shell
+import me.priyesh.chroma.ChromaDialog
+import me.priyesh.chroma.ColorMode
+import me.priyesh.chroma.ColorSelectListener
+
+class ColorPickerFragment: Fragment() {
+
+ var accentColor = ""
+ private var accentName = ""
+ private lateinit var binding: ColorPickerFragmentBinding
+ private lateinit var accentViewModel: AccentViewModel
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ binding = ColorPickerFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val systemAccentColor = this.context!!.getColorAccent()
+ setPreview(binding, systemAccentColor)
+
+ accentViewModel = ViewModelProvider(this).get(AccentViewModel::class.java)
+
+ val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+ var separateAccents = sharedPreferences.getBoolean("separate_accent", false)
+ val customise = sharedPreferences.getBoolean("customise", false)
+ if (SDK_INT < Q || isOOS) separateAccents = false
+
+ if (separateAccents) {
+ binding.title.text = String.format(
+ context!!.resources.getString(R.string.picker_title_text),
+ "for light theme"
+ )
+ binding.buttonNext.text = context!!.resources.getString(R.string.next)
+ binding.textInputLayout.visibility = View.INVISIBLE
+ }
+ else
+ binding.title.text = String.format(context!!.resources.getString(R.string.picker_title_text), "")
+
+ if (customise)
+ binding.buttonNext.text = context!!.resources.getString(R.string.next)
+
+ binding.buttonNext.setOnClickListener {
+
+ if (separateAccents) {
+ if (accentColor.isNotBlank()) {
+ val action = ColorPickerFragmentDirections.toDark(accentColor)
+ findNavController().navigate(action)
+ }
+ else
+ Toast.makeText(context, "Accent color for light theme is not selected",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ else if (customise) {
+ if (accentColor.isNotBlank() && accentName.isNotBlank()) {
+ val action = ColorPickerFragmentDirections.toCustomise(accentColor, accentName, accentColor)
+ findNavController().navigate(action)
+ }
+ else {
+ if (accentColor.isBlank()) Toast.makeText(
+ context,
+ "Accent color is not selected",
+ Toast.LENGTH_SHORT
+ ).show()
+ if (accentName.isBlank()) Toast.makeText(
+ context,
+ "Accent name is not set",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ else {
+ if (isOOS) {
+ if (accentColor.isNotBlank()) {
+ val result = Shell.su("settings put system oem_black_mode_accent_color \'$accentColor\'")
+ .exec()
+ if (result.isSuccess) {
+ showSnackbar(view, "$accentColor set")
+ findNavController().navigate(R.id.back_home)
+ }
+ }
+ else
+ Toast.makeText(
+ context,
+ "Accent color is not selected",
+ Toast.LENGTH_SHORT
+ ).show()
+ } else {
+ if (accentColor.isNotBlank() && accentName.isNotBlank()) {
+ val suffix = "hex_" + accentColor.removePrefix("#")
+ val pkgName = prefix + suffix
+ val accent = Accent(pkgName, accentName, accentColor, accentColor)
+ Log.d("accent", accent.toString())
+ if (createAccent(context!!, accentViewModel, accent)) {
+ showSnackbar(view, "$accentName created")
+ findNavController().navigate(R.id.back_home)
+ }
+ } else {
+ if (accentColor.isBlank()) Toast.makeText(
+ context,
+ "Accent color is not selected",
+ Toast.LENGTH_SHORT
+ ).show()
+ if (accentName.isBlank()) Toast.makeText(
+ context,
+ "Accent name is not set",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+ }
+
+ binding.buttonPrevious.setOnClickListener {
+ findNavController().navigateUp()
+ }
+
+ binding.custom.setOnClickListener { setCustomColor() }
+ binding.preset.setOnClickListener { chooseFromPresets() }
+ if (SDK_INT > O)
+ binding.wallColors.setOnClickListener { chooseFromWallpaperColors() }
+ else
+ binding.wallFrame.visibility = View.GONE
+
+ binding.name.doAfterTextChanged {
+ accentName = it.toString().trim()
+ }
+
+ }
+
+ private fun setCustomColor() {
+
+ ChromaDialog.Builder()
+ .initialColor(Color.parseColor("#FF2800"))
+ .colorMode(ColorMode.RGB)
+ .onColorSelected(object : ColorSelectListener {
+ override fun onColorSelected(color: Int) {
+ accentColor = toHex(color)
+ setPreview(binding, color)
+ binding.name.text = null
+ }
+ })
+ .create()
+ .show(parentFragmentManager, "ChromaDialog")
+
+ }
+
+ private fun chooseFromPresets() {
+
+ val colorPreviewBinding = ColorPreviewBinding.inflate(layoutInflater)
+ val dialogTitleBinding = DialogTitleBinding.inflate(layoutInflater)
+ dialogTitleBinding.titleText.text = String.format(resources.getString(R.string.presets))
+ dialogTitleBinding.titleIcon.setImageResource(R.drawable.ic_preset)
+ val builder = MaterialAlertDialogBuilder(context)
+ .setCustomTitle(dialogTitleBinding.root)
+ .setView(colorPreviewBinding.root)
+ val dialog = builder.create()
+
+ val adapter = ColorListAdapter(context!!, presets) { colour ->
+ accentColor = colour.hex
+ accentName = colour.name
+ binding.name.setText(colour.name)
+ setPreview(binding, Color.parseColor(accentColor))
+ dialog.cancel()
+ }
+
+ colorPreviewBinding.recyclerViewColor.adapter = adapter
+ colorPreviewBinding.recyclerViewColor.layoutManager = LinearLayoutManager(context)
+
+ dialog.show()
+ }
+
+
+ private fun chooseFromWallpaperColors() {
+ if (SDK_INT > O) {
+
+ val rationaleHandler = createDialogRationale(R.string.app_name_full) {
+ onPermission(
+ Permission.READ_EXTERNAL_STORAGE,
+ "Storage permission is required to get wallpaper colours."
+ )
+ }
+
+ runWithPermissions(
+ Permission.READ_EXTERNAL_STORAGE,
+ rationaleHandler = rationaleHandler
+ ) {
+ if (it.isAllGranted()) {
+ val wallpaperManager = WallpaperManager.getInstance(context)
+ val wallDrawable = wallpaperManager.drawable
+ var wallColors = wallpaperManager.getWallpaperColors(FLAG_SYSTEM)!!
+
+ val colorsChangedListener = WallpaperManager.OnColorsChangedListener { colors, _ ->
+ wallColors = colors ?: WallpaperColors.fromDrawable(wallDrawable)
+ }
+ wallpaperManager.addOnColorsChangedListener(colorsChangedListener, Handler())
+
+ val primary = wallColors.primaryColor.toArgb()
+ val secondary = wallColors.secondaryColor?.toArgb()
+ val tertiary = wallColors.tertiaryColor?.toArgb()
+
+ val primaryHex = toHex(primary)
+ val wallpaperColours = mutableListOf(Colour(primaryHex, "Wallpaper primary"))
+ if (secondary != null) {
+ val secondaryHex = toHex(secondary)
+ wallpaperColours.add(Colour(secondaryHex, "Wallpaper secondary"))
+ }
+ if (tertiary != null) {
+ val tertiaryHex = toHex(tertiary)
+ wallpaperColours.add(Colour(tertiaryHex, "Wallpaper tertiary"))
+ }
+
+ val colorPreviewBinding = ColorPreviewBinding.inflate(layoutInflater)
+ val dialogTitleBinding = DialogTitleBinding.inflate(layoutInflater)
+ dialogTitleBinding.titleText.text = String.format(resources.getString(R.string.color_wallpaper))
+ dialogTitleBinding.titleIcon.setImageResource(R.drawable.ic_wallpaper)
+ val builder = MaterialAlertDialogBuilder(context)
+ .setCustomTitle(dialogTitleBinding.root)
+ .setView(colorPreviewBinding.root)
+ val dialog = builder.create()
+
+ val adapter = ColorListAdapter(context!!, wallpaperColours) { colour ->
+ accentColor = colour.hex
+ accentName = colour.name
+ setPreview(binding, Color.parseColor(accentColor))
+ binding.name.text = null
+ dialog.cancel()
+ }
+
+ colorPreviewBinding.recyclerViewColor.adapter = adapter
+ colorPreviewBinding.recyclerViewColor.layoutManager = LinearLayoutManager(context)
+
+ dialog.show()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/akilesh/qacc/ui/fragments/DarkColorPickerFragment.kt b/app/src/main/java/app/akilesh/qacc/ui/fragments/DarkColorPickerFragment.kt
new file mode 100644
index 0000000..ff4dae2
--- /dev/null
+++ b/app/src/main/java/app/akilesh/qacc/ui/fragments/DarkColorPickerFragment.kt
@@ -0,0 +1,245 @@
+package app.akilesh.qacc.ui.fragments
+
+import android.app.WallpaperColors
+import android.app.WallpaperManager
+import android.graphics.Color
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.util.Log
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.Toast
+import androidx.core.widget.doAfterTextChanged
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProvider
+import androidx.navigation.fragment.findNavController
+import androidx.navigation.fragment.navArgs
+import androidx.preference.PreferenceManager
+import androidx.recyclerview.widget.LinearLayoutManager
+import app.akilesh.qacc.Const
+import app.akilesh.qacc.R
+import app.akilesh.qacc.ui.adapter.ColorListAdapter
+import app.akilesh.qacc.databinding.ColorPickerFragmentBinding
+import app.akilesh.qacc.databinding.ColorPreviewBinding
+import app.akilesh.qacc.databinding.DialogTitleBinding
+import app.akilesh.qacc.model.Accent
+import app.akilesh.qacc.model.Colour
+import app.akilesh.qacc.utils.AppUtils.createAccent
+import app.akilesh.qacc.utils.AppUtils.getColorAccent
+import app.akilesh.qacc.utils.AppUtils.setPreview
+import app.akilesh.qacc.utils.AppUtils.showSnackbar
+import app.akilesh.qacc.utils.AppUtils.toHex
+import app.akilesh.qacc.viewmodel.AccentViewModel
+import com.afollestad.assent.Permission
+import com.afollestad.assent.rationale.createDialogRationale
+import com.afollestad.assent.runWithPermissions
+import com.google.android.material.dialog.MaterialAlertDialogBuilder
+import me.priyesh.chroma.ChromaDialog
+import me.priyesh.chroma.ColorMode
+import me.priyesh.chroma.ColorSelectListener
+
+class DarkColorPickerFragment: Fragment() {
+
+ private var accentColor = ""
+ private var accentName = ""
+ private lateinit var binding: ColorPickerFragmentBinding
+ private lateinit var accentViewModel: AccentViewModel
+ private val args: DarkColorPickerFragmentArgs by navArgs()
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ binding = ColorPickerFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val systemAccentColor = this.context!!.getColorAccent()
+ setPreview(binding, systemAccentColor)
+
+ val accentColorLight = args.lightAccent
+ accentViewModel = ViewModelProvider(this).get(AccentViewModel::class.java)
+
+ val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
+ val customise = sharedPreferences.getBoolean("customise", false)
+ binding.title.text = String.format(context!!.resources.getString(R.string.picker_title_text), "for dark theme")
+
+ if (customise) binding.buttonNext.text = context!!.resources.getString(R.string.next)
+
+
+ binding.buttonNext.setOnClickListener {
+
+ if (customise) {
+ if (accentName.isNotBlank() && accentColor.isNotBlank()) {
+ val action =
+ DarkColorPickerFragmentDirections.toCustomise(accentColorLight, accentColor, accentName)
+ findNavController().navigate(action)
+ } else {
+ if (accentColor.isBlank()) Toast.makeText(
+ context,
+ "Accent color is not selected",
+ Toast.LENGTH_SHORT
+ ).show()
+ if (accentName.isBlank()) Toast.makeText(
+ context,
+ "Accent name is not set",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ } else {
+
+ if (accentColor.isNotBlank() && accentName.isNotBlank()) {
+ val suffix = "hex_" + accentColorLight.removePrefix("#")
+ val pkgName = Const.prefix + suffix
+ val accent = Accent(pkgName, accentName, accentColorLight, accentColor)
+ Log.d("accent", accent.toString())
+ if (createAccent(context!!, accentViewModel, accent)) {
+ showSnackbar(view, "$accentName created")
+ findNavController().navigate(R.id.back_home)
+ }
+ } else {
+ if (accentColor.isBlank()) Toast.makeText(
+ context,
+ "Accent color is not selected",
+ Toast.LENGTH_SHORT
+ ).show()
+ if (accentName.isBlank()) Toast.makeText(
+ context,
+ "Accent name is not set",
+ Toast.LENGTH_SHORT
+ ).show()
+ }
+ }
+ }
+
+ binding.buttonPrevious.setOnClickListener {
+ findNavController().navigateUp()
+ }
+
+ binding.custom.setOnClickListener { setCustomColor() }
+ binding.preset.setOnClickListener { chooseFromPresets() }
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O)
+ binding.wallColors.setOnClickListener { chooseFromWallpaperColors() }
+ else
+ binding.wallFrame.visibility = View.GONE
+
+ binding.name.doAfterTextChanged {
+ accentName = it.toString().trim()
+ }
+
+ }
+
+ private fun setCustomColor() {
+
+ ChromaDialog.Builder()
+ .initialColor(Color.parseColor("#FF2800"))
+ .colorMode(ColorMode.RGB)
+ .onColorSelected(object : ColorSelectListener {
+ override fun onColorSelected(color: Int) {
+ accentColor = toHex(color)
+ setPreview(binding, color)
+ binding.name.text = null
+ }
+ })
+ .create()
+ .show(parentFragmentManager, "ChromaDialog")
+
+ }
+
+ private fun chooseFromPresets() {
+
+ val colorPreviewBinding = ColorPreviewBinding.inflate(layoutInflater)
+ val dialogTitleBinding = DialogTitleBinding.inflate(layoutInflater)
+ dialogTitleBinding.titleText.text = String.format(resources.getString(R.string.presets))
+ dialogTitleBinding.titleIcon.setImageResource(R.drawable.ic_preset)
+ val builder = MaterialAlertDialogBuilder(context)
+ .setCustomTitle(dialogTitleBinding.root)
+ .setView(colorPreviewBinding.root)
+ val dialog = builder.create()
+
+ val adapter = ColorListAdapter(context!!, Const.Colors.presets) { colour ->
+ accentColor = colour.hex
+ accentName = colour.name
+ binding.name.setText(colour.name)
+ setPreview(binding, Color.parseColor(accentColor))
+ dialog.cancel()
+ }
+
+ colorPreviewBinding.recyclerViewColor.adapter = adapter
+ colorPreviewBinding.recyclerViewColor.layoutManager = LinearLayoutManager(context)
+
+ dialog.show()
+ }
+
+
+ private fun chooseFromWallpaperColors() {
+ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) {
+
+ val rationaleHandler = createDialogRationale(R.string.app_name_full) {
+ onPermission(
+ Permission.READ_EXTERNAL_STORAGE,
+ "Storage permission is required to get wallpaper colours."
+ )
+ }
+
+ runWithPermissions(
+ Permission.READ_EXTERNAL_STORAGE,
+ rationaleHandler = rationaleHandler
+ ) {
+ if (it.isAllGranted()) {
+ val wallpaperManager = WallpaperManager.getInstance(context)
+ val wallDrawable = wallpaperManager.drawable
+ var wallColors = wallpaperManager.getWallpaperColors(WallpaperManager.FLAG_SYSTEM)!!
+
+ val colorsChangedListener = WallpaperManager.OnColorsChangedListener { colors, _ ->
+ wallColors = colors ?: WallpaperColors.fromDrawable(wallDrawable)
+ }
+ wallpaperManager.addOnColorsChangedListener(colorsChangedListener, Handler())
+
+ val primary = wallColors.primaryColor.toArgb()
+ val secondary = wallColors.secondaryColor?.toArgb()
+ val tertiary = wallColors.tertiaryColor?.toArgb()
+
+ val primaryHex = toHex(primary)
+ val wallpaperColours = mutableListOf(Colour(primaryHex, "Wallpaper primary"))
+ if (secondary != null) {
+ val secondaryHex = toHex(secondary)
+ wallpaperColours.add(Colour(secondaryHex, "Wallpaper secondary"))
+ }
+ if (tertiary != null) {
+ val tertiaryHex = toHex(tertiary)
+ wallpaperColours.add(Colour(tertiaryHex, "Wallpaper tertiary"))
+ }
+
+ val colorPreviewBinding = ColorPreviewBinding.inflate(layoutInflater)
+ val dialogTitleBinding = DialogTitleBinding.inflate(layoutInflater)
+ dialogTitleBinding.titleText.text = String.format(resources.getString(R.string.color_wallpaper))
+ dialogTitleBinding.titleIcon.setImageResource(R.drawable.ic_wallpaper)
+ val builder = MaterialAlertDialogBuilder(context)
+ .setCustomTitle(dialogTitleBinding.root)
+ .setView(colorPreviewBinding.root)
+ val dialog = builder.create()
+
+ val adapter = ColorListAdapter(context!!, wallpaperColours) { colour ->
+ accentColor = colour.hex
+ accentName = colour.name
+ setPreview(binding, Color.parseColor(accentColor))
+ binding.name.text = null
+ dialog.cancel()
+ }
+
+ colorPreviewBinding.recyclerViewColor.adapter = adapter
+ colorPreviewBinding.recyclerViewColor.layoutManager = LinearLayoutManager(context)
+
+ dialog.show()
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/akilesh/qacc/ui/fragments/HomeFragment.kt b/app/src/main/java/app/akilesh/qacc/ui/fragments/HomeFragment.kt
new file mode 100644
index 0000000..09a3b52
--- /dev/null
+++ b/app/src/main/java/app/akilesh/qacc/ui/fragments/HomeFragment.kt
@@ -0,0 +1,67 @@
+package app.akilesh.qacc.ui.fragments
+
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.P
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.Observer
+import androidx.lifecycle.ViewModelProvider
+import androidx.recyclerview.widget.ItemTouchHelper
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import app.akilesh.qacc.Const.overlayPath
+import app.akilesh.qacc.Const.prefix
+import app.akilesh.qacc.ui.adapter.AccentListAdapter
+import app.akilesh.qacc.databinding.HomeFragmentBinding
+import app.akilesh.qacc.utils.AppUtils.showSnackbar
+import app.akilesh.qacc.utils.SwipeToDeleteCallback
+import app.akilesh.qacc.viewmodel.AccentViewModel
+import com.topjohnwu.superuser.Shell
+
+
+class HomeFragment: Fragment() {
+
+ private lateinit var accentViewModel: AccentViewModel
+ private lateinit var binding: HomeFragmentBinding
+
+ override fun onCreateView(
+ inflater: LayoutInflater,
+ container: ViewGroup?,
+ savedInstanceState: Bundle?
+ ): View? {
+ binding = HomeFragmentBinding.inflate(inflater, container, false)
+ return binding.root
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+
+ val adapter = AccentListAdapter(context!!)
+ binding.recyclerView.adapter = adapter
+ binding.recyclerView.layoutManager = LinearLayoutManager(context!!)
+
+ accentViewModel = ViewModelProvider(this).get(AccentViewModel::class.java)
+ accentViewModel.allAccents.observe(viewLifecycleOwner, Observer { accents ->
+ accents?.let { adapter.setAccents(it) }
+ })
+
+ val swipeHandler = object : SwipeToDeleteCallback(context!!) {
+ override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
+ val accent = adapter.getAccentAndRemoveAt(viewHolder.adapterPosition)
+ accentViewModel.delete(accent)
+ val appName = accent.pkgName.substringAfter(prefix)
+ val result = if (SDK_INT >= P)
+ Shell.su("rm -f $overlayPath/$appName.apk").exec()
+ else
+ Shell.su("pm uninstall ${accent.pkgName}").exec()
+ if (result.isSuccess)
+ showSnackbar(view, "${accent.name} removed.")
+ }
+ }
+ val itemTouchHelper = ItemTouchHelper(swipeHandler)
+ itemTouchHelper.attachToRecyclerView(binding.recyclerView)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/akilesh/qacc/ui/fragments/InfoFragment.kt b/app/src/main/java/app/akilesh/qacc/ui/fragments/InfoFragment.kt
new file mode 100644
index 0000000..3230cf8
--- /dev/null
+++ b/app/src/main/java/app/akilesh/qacc/ui/fragments/InfoFragment.kt
@@ -0,0 +1,117 @@
+package app.akilesh.qacc.ui.fragments
+
+import android.content.Context
+import android.content.res.Configuration
+import android.net.Uri
+import androidx.core.content.res.ResourcesCompat
+import app.akilesh.qacc.Const.Links.githubReleases
+import app.akilesh.qacc.Const.Links.githubRepo
+import app.akilesh.qacc.Const.Links.telegramChannel
+import app.akilesh.qacc.Const.Links.telegramGroup
+import app.akilesh.qacc.Const.Links.xdaThread
+import app.akilesh.qacc.R
+import com.danielstone.materialaboutlibrary.ConvenienceBuilder
+import com.danielstone.materialaboutlibrary.MaterialAboutFragment
+import com.danielstone.materialaboutlibrary.items.MaterialAboutActionItem
+import com.danielstone.materialaboutlibrary.items.MaterialAboutTitleItem
+import com.danielstone.materialaboutlibrary.model.MaterialAboutCard
+import com.danielstone.materialaboutlibrary.model.MaterialAboutList
+
+class InfoFragment: MaterialAboutFragment() {
+
+ override fun getTheme(): Int {
+ var theme: Int = R.style.AppTheme
+ when (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
+ Configuration.UI_MODE_NIGHT_NO -> {
+ theme = R.style.Theme_Mal_Light
+ }
+ Configuration.UI_MODE_NIGHT_YES -> {
+ theme = R.style.Theme_Mal_Dark
+ }
+ }
+ return theme
+ }
+
+ override fun getMaterialAboutList(context: Context?): MaterialAboutList {
+
+ val appInfoCard = MaterialAboutCard.Builder()
+ .addItem(
+ MaterialAboutTitleItem.Builder()
+ .text(context!!.resources.getString(R.string.app_name_full))
+ .desc("By Akilesh")
+ .icon(R.mipmap.ic_launcher)
+ .build()
+ )
+ .addItem(
+ ConvenienceBuilder.createVersionActionItem(context,
+ ResourcesCompat.getDrawable(context.resources, R.drawable.ic_outline_info, context.theme),
+ "Version",
+ false
+ )
+ )
+ .build()
+
+
+ val linksCard = MaterialAboutCard.Builder()
+ .title("Links")
+ .titleColor(ResourcesCompat.getColor(context.resources, R.color.colorPrimary, context.theme))
+ .addItem(
+ MaterialAboutActionItem.Builder()
+ .text("Github repo")
+ .icon(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_github, context.theme))
+ .setOnClickAction(
+ ConvenienceBuilder.createWebsiteOnClickAction(context, Uri.parse(githubRepo))
+ )
+ .build()
+ )
+ .addItem(
+ MaterialAboutActionItem.Builder()
+ .text("Telegram group")
+ .icon(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_outline_group, context.theme))
+ .setOnClickAction(
+ ConvenienceBuilder.createWebsiteOnClickAction(context, Uri.parse(telegramGroup))
+ )
+ .build()
+ )
+ .addItem(
+ MaterialAboutActionItem.Builder()
+ .text("XDA thread")
+ .icon(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_xda, context.theme))
+ .setOnClickAction(
+ ConvenienceBuilder.createWebsiteOnClickAction(context, Uri.parse(xdaThread))
+ )
+ .build()
+ )
+ .build()
+
+ val downloadsCard = MaterialAboutCard.Builder()
+ .title("Downloads")
+ .titleColor(ResourcesCompat.getColor(context.resources, R.color.colorPrimary, context.theme))
+ .addItem(
+ MaterialAboutActionItem.Builder()
+ .text("Github releases")
+ .icon(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_outline_get_app, context.theme))
+ .setOnClickAction(
+ ConvenienceBuilder.createWebsiteOnClickAction(context, Uri.parse(githubReleases))
+ )
+ .build()
+ )
+ .addItem(
+ MaterialAboutActionItem.Builder()
+ .text("Telegram channel (includes beta releases)")
+ .icon(ResourcesCompat.getDrawable(context.resources, R.drawable.ic_outline_get_app, context.theme))
+ .setOnClickAction(
+ ConvenienceBuilder.createWebsiteOnClickAction(context, Uri.parse(telegramChannel))
+ )
+ .build()
+ )
+ .build()
+
+
+ return MaterialAboutList.Builder()
+ .addCard(appInfoCard)
+ .addCard(linksCard)
+ .addCard(downloadsCard)
+ .build()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/akilesh/qacc/ui/fragments/SettingsFragment.kt b/app/src/main/java/app/akilesh/qacc/ui/fragments/SettingsFragment.kt
new file mode 100644
index 0000000..68b02fa
--- /dev/null
+++ b/app/src/main/java/app/akilesh/qacc/ui/fragments/SettingsFragment.kt
@@ -0,0 +1,27 @@
+package app.akilesh.qacc.ui.fragments
+
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.Q
+import android.os.Bundle
+import androidx.preference.ListPreference
+import androidx.preference.PreferenceFragmentCompat
+import androidx.preference.SwitchPreferenceCompat
+import app.akilesh.qacc.Const.isOOS
+import app.akilesh.qacc.R
+import app.akilesh.qacc.utils.AppUtils
+
+class SettingsFragment: PreferenceFragmentCompat() {
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.root_preferences, rootKey)
+
+ findPreference("themePref")?.setOnPreferenceChangeListener { _, newValue ->
+ val theme = newValue as String
+ AppUtils.applyTheme(theme)
+ true
+ }
+
+ if (SDK_INT < Q || isOOS)
+ findPreference("separate_accent")?.isVisible = false
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/app/akilesh/qacc/utils/AppUtils.kt b/app/src/main/java/app/akilesh/qacc/utils/AppUtils.kt
new file mode 100644
index 0000000..b68d737
--- /dev/null
+++ b/app/src/main/java/app/akilesh/qacc/utils/AppUtils.kt
@@ -0,0 +1,238 @@
+package app.akilesh.qacc.utils
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.os.Build.VERSION.SDK_INT
+import android.os.Build.VERSION_CODES.P
+import android.os.Build.VERSION_CODES.Q
+import android.util.Log
+import android.view.View
+import androidx.annotation.AttrRes
+import androidx.annotation.ColorInt
+import androidx.appcompat.app.AppCompatDelegate
+import androidx.core.graphics.ColorUtils
+import app.akilesh.qacc.Const.overlayPath
+import app.akilesh.qacc.Const.prefix
+import app.akilesh.qacc.R
+import app.akilesh.qacc.databinding.ColorPickerFragmentBinding
+import app.akilesh.qacc.model.Accent
+import app.akilesh.qacc.signing.ByteArrayStream
+import app.akilesh.qacc.signing.JarMap
+import app.akilesh.qacc.signing.SignAPK
+import app.akilesh.qacc.utils.XmlUtils.createColors
+import app.akilesh.qacc.utils.XmlUtils.createOverlayManifest
+import app.akilesh.qacc.viewmodel.AccentViewModel
+import com.google.android.material.snackbar.Snackbar
+import com.topjohnwu.superuser.Shell
+import org.bouncycastle.asn1.ASN1InputStream
+import org.bouncycastle.asn1.pkcs.PrivateKeyInfo
+import java.io.*
+import java.security.GeneralSecurityException
+import java.security.KeyFactory
+import java.security.PrivateKey
+import java.security.cert.CertificateFactory
+import java.security.cert.X509Certificate
+import java.security.spec.PKCS8EncodedKeySpec
+
+
+object AppUtils {
+ private const val lightMode = "light"
+ private const val darkMode = "dark"
+ private const val batterySaverMode = "battery"
+ const val default = "default"
+
+ fun applyTheme(theme: String?) {
+ when (theme) {
+ lightMode -> {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
+ }
+
+ darkMode -> {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
+ }
+
+ batterySaverMode -> {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY)
+ }
+
+ default -> {
+ AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
+ }
+ }
+ }
+
+ /* fun getDesaturatedColor(color: Int, ratio: Float): String {
+ val hsv = FloatArray(3)
+ Color.colorToHSV(color, hsv)
+
+ hsv[1] = hsv[1] / 1 * ratio + 0.2f * (1.0f - ratio)
+
+ return toHex(Color.HSVToColor(hsv))
+ }*/
+
+ fun toHex(color: Int): String {
+ return String.format("#%06X", (0xFFFFFF and color))
+ }
+
+ @ColorInt
+ fun Context.getColorAccent(): Int {
+ @AttrRes val attr = android.R.attr.colorAccent
+
+ val ta = if (SDK_INT == Q) {
+ obtainStyledAttributes(android.R.style.ThemeOverlay_DeviceDefault_Accent_DayNight, intArrayOf(attr))
+ } else {
+ obtainStyledAttributes(android.R.style.Theme_DeviceDefault, intArrayOf(attr))
+ }
+ @ColorInt val colorAccent = ta.getColor(0, 0)
+ ta.recycle()
+ return colorAccent
+ }
+
+ fun showSnackbar(view: View, text: String) {
+
+ Snackbar.make(
+ view,
+ text,
+ Snackbar.LENGTH_LONG
+ )
+ .setAnchorView(R.id.x_fab)
+ .setAction("Reboot") {
+ Shell.su("/system/bin/svc power reboot || /system/bin/reboot")
+ .submit()
+ }
+ .show()
+ }
+
+
+ fun setPreview(binding: ColorPickerFragmentBinding, accentColor: Int) {
+
+ val accentTintList = ColorStateList.valueOf(accentColor)
+
+ binding.include.apply {
+ previewColorQs0Bg.backgroundTintList = accentTintList
+ previewColorQs1Bg.backgroundTintList = accentTintList
+ previewColorQs2Bg.backgroundTintList = accentTintList
+
+ previewSeekbar.thumbTintList = accentTintList
+ previewSeekbar.progressTintList = accentTintList
+ previewSeekbar.progressBackgroundTintList = accentTintList
+
+ previewCheckSelected.buttonTintList = accentTintList
+ previewRadioSelected.buttonTintList = accentTintList
+ previewToggleSelected.buttonTintList = accentTintList
+ previewToggleSelected.thumbTintList = accentTintList
+ previewToggleSelected.trackTintList = ColorStateList.valueOf(ColorUtils.setAlphaComponent(accentColor, 51))
+ }
+
+ binding.apply {
+ buttonPrevious.setTextColor(accentColor)
+ buttonNext.setBackgroundColor(accentColor)
+ }
+ }
+
+ fun createAccent(context: Context, accentViewModel: AccentViewModel, accent: Accent): Boolean {
+ var created = false
+ val appName = accent.pkgName.substringAfter(prefix)
+ val filesDir = context.filesDir
+
+ val manifest = File("$filesDir", "AndroidManifest.xml")
+ val values = File("$filesDir/src/values")
+ val colors = File(values, "colors.xml")
+ manifest.createNewFile()
+ colors.createNewFile()
+
+ createOverlayManifest(manifest, accent.pkgName, accent.name)
+ createColors(colors, accent.colorLight, accent.colorDark)
+
+ if (manifest.exists() && colors.exists()) {
+ Shell.su("cd ${filesDir.absolutePath}").exec()
+ val ovrRes = Shell.su(context.resources.openRawResource(R.raw.create_overlay)).exec()
+ Log.d("ACC-ovr", ovrRes.out.toString())
+
+ if (ovrRes.isSuccess) {
+ val certFile = context.assets.open("testkey.x509.pem")
+ val keyFile = context.assets.open("testkey.pk8")
+ val out = FileOutputStream(File(filesDir, "signed.apk").absolutePath)
+
+ val cert = readCertificate(certFile)
+ val key = readPrivateKey(keyFile)
+
+ val jar = JarMap.open("$filesDir/qacc.apk")
+
+ SignAPK.sign(cert, key, jar, out.buffered())
+
+ Shell.su("cd ${filesDir.absolutePath}").exec()
+ val zipalignRes = Shell.su(context.resources.openRawResource(R.raw.zipalign)).exec()
+ Log.d("ACC-zip", zipalignRes.out.toString())
+
+ if (zipalignRes.isSuccess) {
+
+ if (SDK_INT >= P) {
+ Shell.su("mkdir -p $overlayPath").exec()
+ Shell.su(context.resources.openRawResource(R.raw.create_module)).exec()
+ val result = Shell.su(
+ "cp -f $filesDir/aligned.apk $overlayPath/$appName.apk",
+ "chmod 644 $overlayPath/$appName.apk"
+ ).exec()
+ Log.d("ACC-MM", result.out.toString())
+
+ if (result.isSuccess) {
+ created = true
+ val createdApks = listOf("qacc.apk", "signed.apk", "aligned.apk")
+ createdApks.forEach {
+ File(filesDir, it).delete()
+ }
+ accentViewModel.insert(accent)
+ }
+ }
+ else {
+ val result = Shell.su(
+ "chmod 644 $filesDir/aligned.apk",
+ "pm install -r $filesDir/aligned.apk"
+ ).exec()
+
+ if (result.isSuccess) {
+ created = true
+ val createdApks = listOf("qacc.apk", "signed.apk", "aligned.apk")
+ createdApks.forEach {
+ File(filesDir, it).delete()
+ }
+ accentViewModel.insert(accent)
+ }
+ }
+ }
+ }
+ }
+ return created
+ }
+
+
+ @Throws(IOException::class, GeneralSecurityException::class)
+ fun readCertificate(inputStream: InputStream): X509Certificate {
+ inputStream.use { stream ->
+ val cf = CertificateFactory.getInstance("X.509")
+ return cf.generateCertificate(stream) as X509Certificate
+ }
+ }
+
+
+ @Throws(IOException::class, GeneralSecurityException::class)
+ fun readPrivateKey(inputStream: InputStream): PrivateKey {
+ inputStream.use { stream ->
+ val buf = ByteArrayStream()
+ buf.readFrom(stream)
+ val bytes = buf.toByteArray()
+ // Check to see if this is in an EncryptedPrivateKeyInfo structure.
+ val spec = PKCS8EncodedKeySpec(bytes)
+ /*
+ * Now it's in a PKCS#8 PrivateKeyInfo structure. Read its Algorithm
+ * OID and use that to construct a KeyFactory.
+ */
+ val bIn = ASN1InputStream(ByteArrayInputStream(spec.encoded))
+ val pki = PrivateKeyInfo.getInstance(bIn.readObject())
+ val algOid = pki.privateKeyAlgorithm.algorithm.id
+ return KeyFactory.getInstance(algOid).generatePrivate(spec)
+ }
+ }
+
+}
diff --git a/app/src/main/java/app/akilesh/qacc/viewmodel/AccentViewModel.kt b/app/src/main/java/app/akilesh/qacc/viewmodel/AccentViewModel.kt
index 1988935..687e340 100644
--- a/app/src/main/java/app/akilesh/qacc/viewmodel/AccentViewModel.kt
+++ b/app/src/main/java/app/akilesh/qacc/viewmodel/AccentViewModel.kt
@@ -4,7 +4,7 @@ import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
-import app.akilesh.qacc.AccentRepository
+import app.akilesh.qacc.db.AccentRepository
import app.akilesh.qacc.db.AccentDatabase
import app.akilesh.qacc.model.Accent
import kotlinx.coroutines.launch
diff --git a/app/src/main/java/app/akilesh/qacc/viewmodel/CustomisationViewModel.kt b/app/src/main/java/app/akilesh/qacc/viewmodel/CustomisationViewModel.kt
new file mode 100644
index 0000000..8d99972
--- /dev/null
+++ b/app/src/main/java/app/akilesh/qacc/viewmodel/CustomisationViewModel.kt
@@ -0,0 +1,16 @@
+package app.akilesh.qacc.viewmodel
+
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+
+class CustomisationViewModel: ViewModel() {
+
+ val lightAccent: MutableLiveData by lazy {
+ MutableLiveData()
+ }
+
+ val darkAccent: MutableLiveData by lazy {
+ MutableLiveData()
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/anim/fragment_enter.xml b/app/src/main/res/anim/fragment_enter.xml
new file mode 100644
index 0000000..affbb54
--- /dev/null
+++ b/app/src/main/res/anim/fragment_enter.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/fragment_enter_pop.xml b/app/src/main/res/anim/fragment_enter_pop.xml
new file mode 100644
index 0000000..6a1d9f3
--- /dev/null
+++ b/app/src/main/res/anim/fragment_enter_pop.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/fragment_exit.xml b/app/src/main/res/anim/fragment_exit.xml
new file mode 100644
index 0000000..59ca284
--- /dev/null
+++ b/app/src/main/res/anim/fragment_exit.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/fragment_exit_pop.xml b/app/src/main/res/anim/fragment_exit_pop.xml
new file mode 100644
index 0000000..c39aaf6
--- /dev/null
+++ b/app/src/main/res/anim/fragment_exit_pop.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_in_left.xml b/app/src/main/res/anim/slide_in_left.xml
new file mode 100644
index 0000000..4d27dbd
--- /dev/null
+++ b/app/src/main/res/anim/slide_in_left.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_in_right.xml b/app/src/main/res/anim/slide_in_right.xml
new file mode 100644
index 0000000..bd87a44
--- /dev/null
+++ b/app/src/main/res/anim/slide_in_right.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_out_left.xml b/app/src/main/res/anim/slide_out_left.xml
new file mode 100644
index 0000000..b1b8e18
--- /dev/null
+++ b/app/src/main/res/anim/slide_out_left.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_out_right.xml b/app/src/main/res/anim/slide_out_right.xml
new file mode 100644
index 0000000..5ab1372
--- /dev/null
+++ b/app/src/main/res/anim/slide_out_right.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/circular_bg.xml b/app/src/main/res/drawable/circular_bg.xml
new file mode 100644
index 0000000..fc132c9
--- /dev/null
+++ b/app/src/main/res/drawable/circular_bg.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_bluetooth.xml b/app/src/main/res/drawable/ic_bluetooth.xml
new file mode 100644
index 0000000..8b79aef
--- /dev/null
+++ b/app/src/main/res/drawable/ic_bluetooth.xml
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_colorize.xml b/app/src/main/res/drawable/ic_colorize.xml
new file mode 100644
index 0000000..430dfe7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_colorize.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_flashlight.xml b/app/src/main/res/drawable/ic_flashlight.xml
new file mode 100644
index 0000000..4c99669
--- /dev/null
+++ b/app/src/main/res/drawable/ic_flashlight.xml
@@ -0,0 +1,13 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_github.xml b/app/src/main/res/drawable/ic_github.xml
new file mode 100644
index 0000000..773c25a
--- /dev/null
+++ b/app/src/main/res/drawable/ic_github.xml
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_invert_colors.xml b/app/src/main/res/drawable/ic_invert_colors.xml
new file mode 100644
index 0000000..57ae264
--- /dev/null
+++ b/app/src/main/res/drawable/ic_invert_colors.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_autorenew.xml b/app/src/main/res/drawable/ic_outline_autorenew.xml
new file mode 100644
index 0000000..52f5094
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_autorenew.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_get_app.xml b/app/src/main/res/drawable/ic_outline_get_app.xml
new file mode 100644
index 0000000..1c554ff
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_get_app.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_group.xml b/app/src/main/res/drawable/ic_outline_group.xml
new file mode 100644
index 0000000..1adab43
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_group.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_outline_info.xml b/app/src/main/res/drawable/ic_outline_info.xml
new file mode 100644
index 0000000..3631663
--- /dev/null
+++ b/app/src/main/res/drawable/ic_outline_info.xml
@@ -0,0 +1,8 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_plus_google_colors.xml b/app/src/main/res/drawable/ic_plus_google_colors.xml
new file mode 100644
index 0000000..7225aa3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_plus_google_colors.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml
index bf293e1..6036b65 100644
--- a/app/src/main/res/drawable/ic_settings.xml
+++ b/app/src/main/res/drawable/ic_settings.xml
@@ -3,7 +3,7 @@
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
- android:tint="?colorOnSecondary">
+ android:tint="?colorPrimary">
+
+
+
diff --git a/app/src/main/res/drawable/ic_wallpaper.xml b/app/src/main/res/drawable/ic_wallpaper.xml
index 71c5e58..96f9b67 100644
--- a/app/src/main/res/drawable/ic_wallpaper.xml
+++ b/app/src/main/res/drawable/ic_wallpaper.xml
@@ -1,9 +1,9 @@
-
-
-
+ xmlns:android="http://schemas.android.com/apk/res/android">
+
diff --git a/app/src/main/res/drawable/ic_wifi.xml b/app/src/main/res/drawable/ic_wifi.xml
new file mode 100644
index 0000000..ce2aeff
--- /dev/null
+++ b/app/src/main/res/drawable/ic_wifi.xml
@@ -0,0 +1,26 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_xda.xml b/app/src/main/res/drawable/ic_xda.xml
new file mode 100644
index 0000000..6454230
--- /dev/null
+++ b/app/src/main/res/drawable/ic_xda.xml
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 215a93c..ba0d20a 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -1,193 +1,45 @@
-
+ android:orientation="vertical"
+ android:animateLayoutChanges="true">
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="match_parent"
+ app:defaultNavHost="true"
+ app:navGraph="@navigation/nav_graph"
+ tools:ignore="FragmentTagUsage" />
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/color_customisation_fragment.xml b/app/src/main/res/layout/color_customisation_fragment.xml
new file mode 100644
index 0000000..99e34bb
--- /dev/null
+++ b/app/src/main/res/layout/color_customisation_fragment.xml
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/color_picker_fragment.xml b/app/src/main/res/layout/color_picker_fragment.xml
new file mode 100644
index 0000000..f3cf382
--- /dev/null
+++ b/app/src/main/res/layout/color_picker_fragment.xml
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/home_fragment.xml b/app/src/main/res/layout/home_fragment.xml
new file mode 100644
index 0000000..ef693a1
--- /dev/null
+++ b/app/src/main/res/layout/home_fragment.xml
@@ -0,0 +1,12 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/preview_color_content.xml b/app/src/main/res/layout/preview_color_content.xml
new file mode 100644
index 0000000..e32de59
--- /dev/null
+++ b/app/src/main/res/layout/preview_color_content.xml
@@ -0,0 +1,153 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/recyclerview_item.xml b/app/src/main/res/layout/recyclerview_item.xml
index 951631d..cc9b5c5 100644
--- a/app/src/main/res/layout/recyclerview_item.xml
+++ b/app/src/main/res/layout/recyclerview_item.xml
@@ -5,36 +5,47 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="horizontal">
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="wrap_content"
+ app:cardElevation="2dp"
+ app:cardCornerRadius="8dp"
+ app:cardUseCompatPadding="true">
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/menu/main_menu.xml b/app/src/main/res/menu/main_menu.xml
new file mode 100644
index 0000000..b4ff5c4
--- /dev/null
+++ b/app/src/main/res/menu/main_menu.xml
@@ -0,0 +1,18 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml
new file mode 100644
index 0000000..e8a1724
--- /dev/null
+++ b/app/src/main/res/navigation/nav_graph.xml
@@ -0,0 +1,167 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..91c73f2
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,5 @@
+
+
+ 30dp
+ 16dp
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 5b3c87b..ac3e952 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -9,11 +9,24 @@
Preset Colours
Tap to use preset colours
Tap to use colours from your wallpaper
- Selected colour
- Settings
Display
Dark theme
%1$s - %2$s
- %1$s %2$s\n%3$s
+ Accents
+ Separate accents
+ Create
+ Choose your accent %1$s
+ Previous
+ Next
+ Light
+ Dark
+ Info
+ Same accent colour will be used for both light & dark themes
+ You can now set different accents for light & dark themes
+ Hue
+ Saturation
+ Lightness
+ Reset
+ Settings
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index 9327adf..b31d5a0 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -1,14 +1,15 @@
-
+
+ app:key="display_category"
+ app:title="@string/display_header"
+ app:allowDividerBelow="false">
+
+
+
+
+
+
+
+
+