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"> + + + + + + + + +