From 7209026fcba17130c22d273b4666253dceb312fa Mon Sep 17 00:00:00 2001 From: Akilesh Date: Mon, 13 Jan 2020 20:25:08 +0530 Subject: [PATCH] v1.50 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Redesigned UI. Create separate accent colour for dark theme [Android 10 only]. Resolves #2 Customise Hue, saturation and lightness of chosen colours. Reduced app size (42816ce).
 Overlay is now installed as a normal app on Oreo [Untested].
 Added support for Oxygen OS [Untested].
 Accents can be deleted by swiping from either side.
 Added colour preview from Styles and wallpapers app.
 New Info screen with links and app version.
 Fragment transition animations from Android 10. 
 --- app/build.gradle.kts | 4 +- app/src/main/AndroidManifest.xml | 9 +- app/src/main/java/app/akilesh/qacc/App.kt | 6 +- app/src/main/java/app/akilesh/qacc/Const.kt | 86 ++-- .../java/app/akilesh/qacc/MainActivity.kt | 405 ------------------ .../java/app/akilesh/qacc/SettingsActivity.kt | 57 --- .../app/akilesh/qacc/db/AccentDatabase.kt | 14 +- .../java/app/akilesh/qacc/model/Accent.kt | 3 +- .../akilesh/qacc/signing/ByteArrayStream.java | 2 +- .../LaunchActivity.kt} | 9 +- .../java/app/akilesh/qacc/ui/MainActivity.kt | 102 +++++ .../fragments/ColorCustomisationFragment.kt | 185 ++++++++ .../qacc/ui/fragments/ColorPickerFragment.kt | 287 +++++++++++++ .../ui/fragments/DarkColorPickerFragment.kt | 245 +++++++++++ .../akilesh/qacc/ui/fragments/HomeFragment.kt | 67 +++ .../akilesh/qacc/ui/fragments/InfoFragment.kt | 117 +++++ .../qacc/ui/fragments/SettingsFragment.kt | 27 ++ .../java/app/akilesh/qacc/utils/AppUtils.kt | 238 ++++++++++ .../akilesh/qacc/viewmodel/AccentViewModel.kt | 2 +- .../qacc/viewmodel/CustomisationViewModel.kt | 16 + app/src/main/res/anim/fragment_enter.xml | 15 + app/src/main/res/anim/fragment_enter_pop.xml | 15 + app/src/main/res/anim/fragment_exit.xml | 15 + app/src/main/res/anim/fragment_exit_pop.xml | 15 + app/src/main/res/anim/slide_in_left.xml | 7 + app/src/main/res/anim/slide_in_right.xml | 7 + app/src/main/res/anim/slide_out_left.xml | 7 + app/src/main/res/anim/slide_out_right.xml | 7 + app/src/main/res/drawable/circular_bg.xml | 4 + app/src/main/res/drawable/ic_bluetooth.xml | 10 + app/src/main/res/drawable/ic_colorize.xml | 5 + app/src/main/res/drawable/ic_flashlight.xml | 13 + app/src/main/res/drawable/ic_github.xml | 11 + .../main/res/drawable/ic_invert_colors.xml | 5 + .../res/drawable/ic_outline_autorenew.xml | 10 + .../main/res/drawable/ic_outline_get_app.xml | 5 + .../main/res/drawable/ic_outline_group.xml | 5 + app/src/main/res/drawable/ic_outline_info.xml | 8 + .../res/drawable/ic_plus_google_colors.xml | 8 + app/src/main/res/drawable/ic_settings.xml | 2 +- app/src/main/res/drawable/ic_twotone_home.xml | 7 + app/src/main/res/drawable/ic_wallpaper.xml | 12 +- app/src/main/res/drawable/ic_wifi.xml | 26 ++ app/src/main/res/drawable/ic_xda.xml | 10 + app/src/main/res/layout/activity_main.xml | 222 ++-------- .../layout/color_customisation_fragment.xml | 195 +++++++++ .../main/res/layout/color_picker_fragment.xml | 157 +++++++ app/src/main/res/layout/home_fragment.xml | 12 + .../main/res/layout/preview_color_content.xml | 153 +++++++ app/src/main/res/layout/recyclerview_item.xml | 71 +-- app/src/main/res/menu/main_menu.xml | 18 + app/src/main/res/navigation/nav_graph.xml | 167 ++++++++ app/src/main/res/values/dimens.xml | 5 + app/src/main/res/values/strings.xml | 19 +- app/src/main/res/xml/root_preferences.xml | 35 +- 55 files changed, 2426 insertions(+), 738 deletions(-) delete mode 100644 app/src/main/java/app/akilesh/qacc/MainActivity.kt delete mode 100644 app/src/main/java/app/akilesh/qacc/SettingsActivity.kt rename app/src/main/java/app/akilesh/qacc/{SplashActivity.kt => ui/LaunchActivity.kt} (89%) create mode 100644 app/src/main/java/app/akilesh/qacc/ui/MainActivity.kt create mode 100644 app/src/main/java/app/akilesh/qacc/ui/fragments/ColorCustomisationFragment.kt create mode 100644 app/src/main/java/app/akilesh/qacc/ui/fragments/ColorPickerFragment.kt create mode 100644 app/src/main/java/app/akilesh/qacc/ui/fragments/DarkColorPickerFragment.kt create mode 100644 app/src/main/java/app/akilesh/qacc/ui/fragments/HomeFragment.kt create mode 100644 app/src/main/java/app/akilesh/qacc/ui/fragments/InfoFragment.kt create mode 100644 app/src/main/java/app/akilesh/qacc/ui/fragments/SettingsFragment.kt create mode 100644 app/src/main/java/app/akilesh/qacc/utils/AppUtils.kt create mode 100644 app/src/main/java/app/akilesh/qacc/viewmodel/CustomisationViewModel.kt create mode 100644 app/src/main/res/anim/fragment_enter.xml create mode 100644 app/src/main/res/anim/fragment_enter_pop.xml create mode 100644 app/src/main/res/anim/fragment_exit.xml create mode 100644 app/src/main/res/anim/fragment_exit_pop.xml create mode 100644 app/src/main/res/anim/slide_in_left.xml create mode 100644 app/src/main/res/anim/slide_in_right.xml create mode 100644 app/src/main/res/anim/slide_out_left.xml create mode 100644 app/src/main/res/anim/slide_out_right.xml create mode 100644 app/src/main/res/drawable/circular_bg.xml create mode 100644 app/src/main/res/drawable/ic_bluetooth.xml create mode 100644 app/src/main/res/drawable/ic_colorize.xml create mode 100644 app/src/main/res/drawable/ic_flashlight.xml create mode 100644 app/src/main/res/drawable/ic_github.xml create mode 100644 app/src/main/res/drawable/ic_invert_colors.xml create mode 100644 app/src/main/res/drawable/ic_outline_autorenew.xml create mode 100644 app/src/main/res/drawable/ic_outline_get_app.xml create mode 100644 app/src/main/res/drawable/ic_outline_group.xml create mode 100644 app/src/main/res/drawable/ic_outline_info.xml create mode 100644 app/src/main/res/drawable/ic_plus_google_colors.xml create mode 100644 app/src/main/res/drawable/ic_twotone_home.xml create mode 100644 app/src/main/res/drawable/ic_wifi.xml create mode 100644 app/src/main/res/drawable/ic_xda.xml create mode 100644 app/src/main/res/layout/color_customisation_fragment.xml create mode 100644 app/src/main/res/layout/color_picker_fragment.xml create mode 100644 app/src/main/res/layout/home_fragment.xml create mode 100644 app/src/main/res/layout/preview_color_content.xml create mode 100644 app/src/main/res/menu/main_menu.xml create mode 100644 app/src/main/res/navigation/nav_graph.xml create mode 100644 app/src/main/res/values/dimens.xml 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"> + + + + + + + + +