├── .gitignore
├── .idea
├── gradle.xml
└── vcs.xml
├── Feature.afphoto
├── Feature.png
├── IconBackground.afphoto
├── IconBackground.png
├── IconForeground.afphoto
├── IconForeground.png
├── app
├── .gitignore
├── build.gradle.kts
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── ic_launcher-playstore.png
│ ├── ic_launcher-web.png
│ ├── java
│ └── tk
│ │ └── zwander
│ │ └── sprviewer
│ │ ├── data
│ │ ├── AppData.kt
│ │ ├── BaseData.kt
│ │ ├── BatchExportSessionData.kt
│ │ ├── CustomPackageInfo.kt
│ │ ├── DrawableData.kt
│ │ ├── ResTableConfigParcelable.kt
│ │ ├── StringData.kt
│ │ ├── StringXmlData.kt
│ │ └── ValueData.kt
│ │ ├── ui
│ │ ├── App.kt
│ │ ├── activities
│ │ │ ├── BaseActivity.kt
│ │ │ ├── BaseListActivity.kt
│ │ │ ├── BatchExportDialogActivity.kt
│ │ │ ├── DrawableListActivity.kt
│ │ │ ├── DrawableViewActivity.kt
│ │ │ ├── MainActivity.kt
│ │ │ ├── StringsListActivity.kt
│ │ │ └── StringsViewActivity.kt
│ │ ├── adapters
│ │ │ ├── AppListAdapter.kt
│ │ │ ├── BaseListAdapter.kt
│ │ │ ├── DrawableListAdapter.kt
│ │ │ ├── StringsListAdapter.kt
│ │ │ └── StringsViewAdapter.kt
│ │ └── services
│ │ │ └── BatchExportService.kt
│ │ ├── util
│ │ ├── APKUtils.kt
│ │ ├── BatchExportListener.kt
│ │ ├── Constants.kt
│ │ ├── ContextUtils.kt
│ │ ├── IntentUtils.kt
│ │ ├── ListUtils.kt
│ │ ├── PackageUtils.kt
│ │ ├── ResourceUtils.kt
│ │ ├── TextWatcherAdapter.kt
│ │ └── Utils.kt
│ │ └── views
│ │ ├── AnimatedImageView.kt
│ │ ├── BaseDimensionInputDialog.kt
│ │ ├── CircularProgressDialog.kt
│ │ ├── DimensionInputDialog.kt
│ │ └── ExtensionIndicator.kt
│ └── res
│ ├── anim
│ ├── slide_in_down.xml
│ └── slide_out_up.xml
│ ├── drawable
│ ├── action_bar_background.xml
│ ├── corner_left.xml
│ ├── dialog_background.xml
│ ├── ic_add_black_24dp.xml
│ ├── ic_baseline_folder_open_24.xml
│ ├── ic_baseline_image_24.xml
│ ├── ic_baseline_list_24.xml
│ ├── outlined_circle.xml
│ ├── snackbar_background.xml
│ └── solid_circle.xml
│ ├── layout
│ ├── activity_drawable_view.xml
│ ├── activity_main.xml
│ ├── app_info_layout.xml
│ ├── batch_export_notification_content_layout.xml
│ ├── determinate_progress.xml
│ ├── dimension_input.xml
│ ├── drawable_info_layout.xml
│ ├── pixel_input.xml
│ ├── progress.xml
│ └── string_list_info_layout.xml
│ ├── menu
│ ├── batch.xml
│ ├── import_apk.xml
│ ├── save.xml
│ └── search.xml
│ ├── mipmap-anydpi-v26
│ ├── ic_launcher.xml
│ └── ic_launcher_round.xml
│ ├── mipmap-hdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ ├── mipmap-mdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ ├── mipmap-xhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ ├── mipmap-xxxhdpi
│ ├── ic_launcher.png
│ ├── ic_launcher_background.png
│ ├── ic_launcher_foreground.png
│ └── ic_launcher_round.png
│ └── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
├── build.gradle.kts
├── gradle.properties
├── gradle
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
└── settings.gradle.kts
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | /app/release/app-release.apk
16 | /.idea/appInsightsSettings.xml
17 | /.idea/compiler.xml
18 | /.idea/deploymentTargetDropDown.xml
19 | /.idea/deploymentTargetSelector.xml
20 | /.idea/jarRepositories.xml
21 | /.idea/kotlinc.xml
22 | /.idea/migrations.xml
23 | /.idea/misc.xml
24 | /app/release/output-metadata.json
25 | /.idea/runConfigurations.xml
26 |
--------------------------------------------------------------------------------
/.idea/gradle.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
20 |
21 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/Feature.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/Feature.afphoto
--------------------------------------------------------------------------------
/Feature.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/Feature.png
--------------------------------------------------------------------------------
/IconBackground.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/IconBackground.afphoto
--------------------------------------------------------------------------------
/IconBackground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/IconBackground.png
--------------------------------------------------------------------------------
/IconForeground.afphoto:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/IconForeground.afphoto
--------------------------------------------------------------------------------
/IconForeground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/IconForeground.png
--------------------------------------------------------------------------------
/app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 |
--------------------------------------------------------------------------------
/app/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application)
3 | alias(libs.plugins.kotlin.android)
4 | alias(libs.plugins.kotlin.parcelize)
5 | }
6 |
7 | android {
8 | compileSdk = 34
9 | defaultConfig {
10 | applicationId = "tk.zwander.sprviewer"
11 | minSdk = 23
12 | targetSdk = 34
13 | versionCode = 14
14 | versionName = versionCode.toString()
15 | }
16 | buildTypes {
17 | release {
18 | isMinifyEnabled = false
19 | proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
20 | }
21 | }
22 | compileOptions {
23 | sourceCompatibility = JavaVersion.VERSION_11
24 | targetCompatibility = JavaVersion.VERSION_11
25 | }
26 | kotlinOptions {
27 | jvmTarget = "11"
28 | }
29 | viewBinding {
30 | enable = true
31 | }
32 | namespace = "tk.zwander.sprviewer"
33 | }
34 |
35 | dependencies {
36 | implementation(fileTree("libs") { include("*.jar") })
37 | implementation(libs.kotlin.stdlib)
38 | implementation(libs.kotlinx.coroutines.core)
39 | implementation(libs.kotlinx.coroutines.android)
40 |
41 | implementation(libs.appcompat)
42 | implementation(libs.core.ktx)
43 | implementation(libs.constraintlayout)
44 | implementation(libs.recyclerview)
45 | implementation(libs.material)
46 | implementation(libs.documentfile)
47 | implementation(libs.lifecycle.viewmodel.ktx)
48 |
49 | implementation(libs.progresscircula)
50 | implementation(libs.picasso)
51 | implementation(libs.apk.parser)
52 | implementation(libs.photoview)
53 | implementation(libs.pngj)
54 | implementation(libs.numberprogressbar)
55 | implementation(libs.indicatorfastscroll)
56 | implementation(libs.balloon)
57 | implementation(libs.colorpicker)
58 | implementation(libs.hiddenapibypass)
59 |
60 | implementation(libs.bugsnag.android)
61 | }
62 |
--------------------------------------------------------------------------------
/app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
22 |
--------------------------------------------------------------------------------
/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
8 |
10 |
11 |
20 |
23 |
26 |
29 |
32 |
37 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
50 |
51 |
54 |
55 |
56 |
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-playstore.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/ic_launcher-playstore.png
--------------------------------------------------------------------------------
/app/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/data/AppData.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.data
2 |
3 | import android.graphics.drawable.Drawable
4 | import java.util.*
5 |
6 | data class AppData(
7 | val pkg: String,
8 | val label: String,
9 | val icon: Drawable,
10 | var expanded: Boolean = false
11 | ) : BaseData(), Comparable {
12 | override fun compareTo(other: AppData): Int {
13 | return pkg.compareTo(other.pkg)
14 | }
15 |
16 | override fun constructLabel(): String {
17 | return label
18 | }
19 |
20 | override fun equals(other: Any?): Boolean {
21 | return other is AppData
22 | && pkg == other.pkg
23 | && label == other.label
24 | }
25 |
26 | override fun hashCode(): Int {
27 | return Objects.hash(pkg, label)
28 | }
29 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/data/BaseData.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.data
2 |
3 | abstract class BaseData {
4 | abstract fun constructLabel(): String
5 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/data/BatchExportSessionData.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.data
2 |
3 | import android.content.res.Resources
4 | import android.net.Uri
5 | import net.dongliu.apk.parser.ApkFile
6 | import net.dongliu.apk.parser.struct.resource.ResourceTable
7 | import tk.zwander.sprviewer.views.ExportInfo
8 | import java.io.File
9 |
10 | data class BatchExportSessionData(
11 | val uri: Uri,
12 | val drawables: List,
13 | val exportInfo: ExportInfo,
14 | val appName: String,
15 | val appFile: File,
16 | val apkFile: ApkFile,
17 | val appPkg: String,
18 | val remRes: Resources,
19 | val apkResourceTable: ResourceTable = apkFile.resourceTable
20 | )
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/data/CustomPackageInfo.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("DEPRECATION")
2 |
3 | package tk.zwander.sprviewer.data
4 |
5 | import android.content.pm.ApplicationInfo
6 | import android.content.pm.PackageInfo
7 | import android.content.pm.PackageParser
8 |
9 | data class CustomPackageInfo(
10 | val packageName: String,
11 | val appInfo: ApplicationInfo? = null
12 | ) {
13 | constructor(pkg: PackageParser.Package) : this(pkg.packageName, pkg.applicationInfo)
14 | constructor(info: PackageInfo) : this(info.packageName, info.applicationInfo)
15 | constructor(result: Any) : this(
16 | result::class.java.getMethod("getPackageName").invoke(result)!!.toString(),
17 | result::class.java.getMethod("toAppInfoWithoutState").invoke(result) as ApplicationInfo
18 | )
19 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/data/DrawableData.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.data
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 | import net.dongliu.apk.parser.ApkFile
6 | import net.dongliu.apk.parser.struct.resource.ResTableConfig
7 |
8 | @Parcelize
9 | data class DrawableData(
10 | val type: String,
11 | val name: String,
12 | val ext: String?,
13 | val path: String,
14 | val id: Int,
15 | val resTableConfig: ResTableConfigParcelable
16 | ) : Parcelable
17 |
18 | data class UDrawableData(
19 | val type: String,
20 | val name: String,
21 | val ext: String?,
22 | val path: String,
23 | val id: Int,
24 | val file: ApkFile,
25 | val packageInfo: CustomPackageInfo,
26 | val resTableConfig: ResTableConfig
27 | ) : BaseData() {
28 | fun toDrawableData(): DrawableData {
29 | return DrawableData(
30 | type, name, ext, path, id,
31 | ResTableConfigParcelable(resTableConfig)
32 | )
33 | }
34 |
35 | override fun constructLabel(): String {
36 | return "$name.$ext"
37 | }
38 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/data/ResTableConfigParcelable.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.data
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 | import net.dongliu.apk.parser.struct.resource.ResTableConfig
6 |
7 | @Parcelize
8 | data class ResTableConfigParcelable(
9 | val size: Int,
10 | val mcc: Short,
11 | val mnc: Short,
12 | val language: String,
13 | val country: String,
14 | val orientation: Short,
15 | val touchscreen: Short,
16 | val density: Int,
17 | val navigation: Short,
18 | val inputFlags: Short,
19 | val screenWidth: Int,
20 | val screenHeight: Int,
21 | val sdkVersion: Int,
22 | val minorVersion: Int,
23 | val screenLayout: Short,
24 | val uiMode: Short,
25 | val screenConfigPad1: Short,
26 | val screenConfigPad2: Short
27 | ) : Parcelable {
28 | constructor(config: ResTableConfig) : this(
29 | config.size,
30 | config.mcc,
31 | config.mnc,
32 | config.language,
33 | config.country,
34 | config.orientation,
35 | config.touchscreen,
36 | config.density,
37 | config.navigation,
38 | config.inputFlags,
39 | config.screenWidth,
40 | config.screenHeight,
41 | config.sdkVersion,
42 | config.minorVersion,
43 | config.screenLayout,
44 | config.uiMode,
45 | config.screenConfigPad1,
46 | config.screenConfigPad2
47 | )
48 |
49 | fun constructPrefix(): String {
50 | val builder = ArrayList()
51 |
52 | if (mcc != 0.toShort()) {
53 | builder.add("mcc$mcc")
54 |
55 | if (mnc != 0.toShort()) {
56 | builder.add("mnc$mnc")
57 | }
58 | }
59 |
60 | if (language.isNotBlank() && language != "0") {
61 | builder.add(language)
62 |
63 | if (country.isNotBlank() && country != "0") {
64 | builder.add("r$country")
65 | }
66 | }
67 |
68 | if (screenWidth != 0) {
69 | builder.add("w${screenWidth}dp")
70 | }
71 |
72 | if (screenHeight != 0) {
73 | builder.add("h${screenHeight}dp")
74 | }
75 |
76 | if (orientation != 0.toShort()) {
77 | builder.add(if (orientation == 1.toShort()) "port" else "land")
78 | }
79 |
80 | if (uiMode != 0.toShort()) {
81 | builder.add(
82 | when (uiMode.toInt()) {
83 | 1 -> "normal"
84 | 2 -> "desk"
85 | 3 -> "car"
86 | 4 -> "television"
87 | 5 -> "appliance"
88 | 6 -> "watch"
89 | else -> "vrheadset"
90 | }
91 | )
92 | }
93 |
94 | if (density != 0) {
95 | builder.add(
96 | when (density) {
97 | 120 -> "ldpi"
98 | 160 -> "mdpi"
99 | 213 -> "tvdpi"
100 | 240 -> "hdpi"
101 | 320 -> "xhdpi"
102 | 480 -> "xxhdpi"
103 | 640 -> "xxxhdpi"
104 | else -> "${density}dpi"
105 | }
106 | )
107 | }
108 |
109 | if (touchscreen != 0.toShort()) {
110 | builder.add(
111 | when (touchscreen.toInt()) {
112 | 1 -> "notouch"
113 | 2 -> "stylus"
114 | else -> "finger"
115 | }
116 | )
117 | }
118 |
119 | if (sdkVersion != 0) {
120 | builder.add("v$sdkVersion")
121 | }
122 |
123 | return builder.joinToString("-")
124 | }
125 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/data/StringData.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.data
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 |
6 | @Parcelize
7 | data class StringData(
8 | val key: String,
9 | val value: String
10 | ) : Parcelable, Comparable, BaseData() {
11 | override fun constructLabel(): String {
12 | return key
13 | }
14 |
15 | override fun compareTo(other: StringData): Int {
16 | val k = key.compareTo(other.key)
17 | val v = value.compareTo(other.value)
18 |
19 | return if (k == 0) v else k
20 | }
21 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/data/StringXmlData.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.data
2 |
3 | import android.os.Parcel
4 | import android.os.Parcelable
5 | import kotlinx.parcelize.Parceler
6 | import kotlinx.parcelize.Parcelize
7 | import kotlinx.parcelize.TypeParceler
8 | import java.lang.StringBuilder
9 | import java.util.*
10 | import kotlin.collections.HashMap
11 |
12 | @Parcelize
13 | @TypeParceler, MapParceler>
14 | data class StringXmlData(
15 | val locale: Locale,
16 | val values: MutableSet = TreeSet()
17 | ) : Parcelable, BaseData() {
18 | override fun constructLabel(): String {
19 | return "${locale.toString().run { this.ifBlank { "DEFAULT" } }}/strings.xml"
20 | }
21 |
22 | fun asXmlString(): String {
23 | val builder = StringBuilder()
24 |
25 | builder.appendLine("")
26 | builder.appendLine("")
27 | values.forEach { (t, u) ->
28 | builder.appendLine(" $u")
29 | }
30 | builder.appendLine("")
31 |
32 | return builder.toString()
33 | }
34 | }
35 |
36 | object MapParceler : Parceler> {
37 | override fun MutableMap.write(parcel: Parcel, flags: Int) {
38 | parcel.writeInt(size)
39 |
40 | forEach { (k, v) ->
41 | parcel.writeString(k)
42 | parcel.writeString(v)
43 | }
44 | }
45 |
46 | override fun create(parcel: Parcel): MutableMap {
47 | val size = parcel.readInt()
48 |
49 | return HashMap().apply {
50 | for (i in 0 until size) {
51 | val key = parcel.readString()
52 | val value = parcel.readString()
53 |
54 | this[key] = value
55 | }
56 | }
57 | }
58 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/data/ValueData.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.data
2 |
3 | import android.os.Parcelable
4 | import kotlinx.parcelize.Parcelize
5 | import net.dongliu.apk.parser.ApkFile
6 | import java.util.*
7 |
8 | @Parcelize
9 | data class LocalizedValueData(
10 | val locale: Locale,
11 | val value: String?
12 | ) : Parcelable
13 |
14 | @Parcelize
15 | data class ValueData(
16 | val type: String,
17 | val name: String,
18 | val path: String,
19 | val id: Int,
20 | val defaultValue: String?,
21 | val values: MutableList
22 | ) : Parcelable
23 |
24 | data class UValueData(
25 | val type: String,
26 | val name: String,
27 | val path: String,
28 | val id: Int,
29 | val file: ApkFile,
30 | val packageInfo: CustomPackageInfo,
31 | val defaultValue: String?,
32 | val values: MutableList
33 | ) : BaseData() {
34 | override fun constructLabel(): String {
35 | return name
36 | }
37 |
38 | override fun equals(other: Any?): Boolean {
39 | return other is UValueData
40 | && name == other.name
41 | && defaultValue == other.defaultValue
42 | }
43 |
44 | override fun hashCode(): Int {
45 | return Objects.hash(name, defaultValue)
46 | }
47 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/ui/App.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.ui
2 |
3 | import android.app.Application
4 | import android.os.Build
5 | import com.bugsnag.android.Bugsnag
6 | import com.squareup.picasso.Picasso
7 | import org.lsposed.hiddenapibypass.HiddenApiBypass
8 | import tk.zwander.sprviewer.util.BatchExportListener
9 |
10 | class App : Application() {
11 | val batchExportListeners = ArrayList()
12 |
13 | override fun onCreate() {
14 | super.onCreate()
15 |
16 | Bugsnag.start(this)
17 |
18 | Picasso.setSingletonInstance(Picasso.Builder(this).build())
19 |
20 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
21 | HiddenApiBypass.setHiddenApiExemptions("L")
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/ui/activities/BaseActivity.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.ui.activities
2 |
3 | import android.os.Bundle
4 | import android.view.Menu
5 | import android.view.MenuItem
6 | import android.view.View
7 | import androidx.activity.OnBackPressedCallback
8 | import androidx.annotation.CallSuper
9 | import androidx.appcompat.app.AppCompatActivity
10 | import androidx.appcompat.widget.SearchView
11 | import androidx.core.view.isVisible
12 | import androidx.recyclerview.widget.LinearLayoutManager
13 | import androidx.recyclerview.widget.RecyclerView
14 | import com.hmomeni.progresscircula.ProgressCircula
15 | import com.reddit.indicatorfastscroll.FastScrollItemIndicator
16 | import com.reddit.indicatorfastscroll.FastScrollerThumbView
17 | import com.reddit.indicatorfastscroll.FastScrollerView
18 | import kotlinx.coroutines.*
19 | import tk.zwander.sprviewer.R
20 | import tk.zwander.sprviewer.data.BaseData
21 | import tk.zwander.sprviewer.ui.adapters.BaseListAdapter
22 | import tk.zwander.sprviewer.util.showTitleSnackBar
23 | import java.util.*
24 | import kotlin.math.absoluteValue
25 |
26 | abstract class BaseActivity : AppCompatActivity(), CoroutineScope by MainScope() {
27 | abstract val contentView: View
28 | abstract val adapter: BaseListAdapter
29 |
30 | internal abstract val recycler: RecyclerView
31 | internal abstract val scroller: FastScrollerView
32 | internal abstract val scrollerThumb: FastScrollerThumbView
33 |
34 | internal var progressItem: MenuItem? = null
35 | internal var progress: ProgressCircula? = null
36 |
37 | private var searchItem: MenuItem? = null
38 | private var searchView: SearchView? = null
39 |
40 | private var doneLoading = false
41 |
42 | internal open val hasBackButton = false
43 |
44 | private val backPressedCallback = object : OnBackPressedCallback(false) {
45 | override fun handleOnBackPressed() {
46 | searchView?.onActionViewCollapsed()
47 | adapter.onQueryTextSubmit(null)
48 | }
49 | }
50 |
51 | @CallSuper
52 | override fun onCreate(savedInstanceState: Bundle?) {
53 | super.onCreate(savedInstanceState)
54 | setContentView(contentView)
55 |
56 | if (hasBackButton) {
57 | supportActionBar?.setDisplayHomeAsUpEnabled(true)
58 | supportActionBar?.setDisplayShowHomeEnabled(true)
59 | }
60 |
61 | recycler.layoutManager = object : LinearLayoutManager(this) {
62 | override fun supportsPredictiveItemAnimations(): Boolean {
63 | return true
64 | }
65 | }
66 |
67 | adapter.setHasStableIds(true)
68 |
69 | recycler.adapter = adapter
70 |
71 | scroller.setupWithRecyclerView(
72 | recycler,
73 | { position ->
74 | val item = adapter.getInfo(position)
75 |
76 | FastScrollItemIndicator.Text(
77 | item.constructLabel()
78 | .run {
79 | if (isBlank()) "?"
80 | else substring(0, 1)
81 | }
82 | .uppercase(Locale.getDefault())
83 | )
84 | }
85 | )
86 | scrollerThumb.setupWithFastScroller(scroller)
87 |
88 | scroller.itemIndicatorSelectedCallbacks += object : FastScrollerView.ItemIndicatorSelectedCallback {
89 | override fun onItemIndicatorSelected(
90 | indicator: FastScrollItemIndicator,
91 | indicatorCenterY: Int,
92 | itemPosition: Int
93 | ) {
94 | scrollToPosition(itemPosition)
95 | }
96 | }
97 |
98 | window.decorView?.findViewById(R.id.action_bar)?.apply {
99 | setOnClickListener {
100 | scrollToPosition()
101 | }
102 |
103 | setOnLongClickListener {
104 | showTitleSnackBar(it)
105 |
106 | true
107 | }
108 | }
109 |
110 | onBackPressedDispatcher.addCallback(this, backPressedCallback)
111 | }
112 |
113 | override fun onDestroy() {
114 | super.onDestroy()
115 |
116 | cancel()
117 | }
118 |
119 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
120 | menuInflater.inflate(R.menu.search, menu)
121 |
122 | searchItem = menu.findItem(R.id.action_search)
123 | searchView = searchItem?.actionView as SearchView?
124 |
125 | searchView?.setOnQueryTextListener(adapter)
126 | searchView?.isSubmitButtonEnabled = true
127 |
128 | progressItem = menu.findItem(R.id.status_progress)
129 | progress = progressItem?.actionView as ProgressCircula?
130 |
131 | if (doneLoading) {
132 | progressItem?.isVisible = false
133 | }
134 |
135 | searchView?.setOnCloseListener {
136 | backPressedCallback.isEnabled = false
137 | adapter.onQueryTextSubmit(null)
138 | false
139 | }
140 |
141 | searchItem?.setOnMenuItemClickListener {
142 | backPressedCallback.isEnabled = true
143 | false
144 | }
145 |
146 | checkCount()
147 |
148 | return true
149 | }
150 |
151 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
152 | return when (item.itemId) {
153 | android.R.id.home -> {
154 | onBackPressedDispatcher.onBackPressed()
155 | true
156 | }
157 | else -> super.onOptionsItemSelected(item)
158 | }
159 | }
160 |
161 | open fun onLoadFinished() {
162 | doneLoading = true
163 | progressItem?.isVisible = false
164 | recycler.isVisible = true
165 |
166 | checkCount()
167 | }
168 |
169 | open fun checkCount() {
170 | if (isReadyForMenus() && searchItem?.isVisible == false) {
171 | searchItem?.isVisible = true
172 | }
173 | }
174 |
175 | fun scrollToPosition(position: Int = 0) {
176 | recycler.apply {
177 | val lin = (layoutManager as LinearLayoutManager)
178 | val firstPos = lin.findFirstVisibleItemPosition()
179 | val smooth = (position - firstPos).absoluteValue < 50
180 |
181 |
182 | if (smooth) smoothScrollToPosition(position)
183 | else scrollToPosition(position)
184 | }
185 | }
186 |
187 | internal fun isReadyForMenus(): Boolean {
188 | return doneLoading && adapter.itemCount > 0
189 | }
190 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/ui/activities/BaseListActivity.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.ui.activities
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Intent
5 | import android.net.Uri
6 | import android.os.Bundle
7 | import android.view.Menu
8 | import android.view.MenuItem
9 | import androidx.activity.result.ActivityResultLauncher
10 | import androidx.recyclerview.widget.RecyclerView
11 | import com.reddit.indicatorfastscroll.FastScrollerThumbView
12 | import com.reddit.indicatorfastscroll.FastScrollerView
13 | import kotlinx.coroutines.Dispatchers
14 | import kotlinx.coroutines.launch
15 | import kotlinx.coroutines.withContext
16 | import net.dongliu.apk.parser.ApkFile
17 | import tk.zwander.sprviewer.R
18 | import tk.zwander.sprviewer.data.BaseData
19 | import tk.zwander.sprviewer.databinding.ActivityMainBinding
20 | import tk.zwander.sprviewer.ui.adapters.BaseListAdapter
21 | import tk.zwander.sprviewer.util.*
22 | import java.io.File
23 | import java.util.*
24 |
25 | @SuppressLint("InlinedApi")
26 | abstract class BaseListActivity :
27 | BaseActivity() {
28 | companion object {
29 | const val EXTRA_FILE = "file"
30 | const val EXTRA_APP_LABEL = "app_label"
31 | }
32 |
33 | internal abstract val saveAllAct: ActivityResultLauncher
34 |
35 | override val contentView by lazy { binding.root }
36 | override val hasBackButton = true
37 |
38 | override val recycler: RecyclerView
39 | get() = binding.recycler
40 | override val scrollerThumb: FastScrollerThumbView
41 | get() = binding.scrollerThumb
42 | override val scroller: FastScrollerView
43 | get() = binding.scroller
44 |
45 | internal val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
46 |
47 | private val apkPath by lazy {
48 | if (pkg != null) {
49 | File(packageManager.getApplicationInfoCompat(pkg!!, 0).sourceDir)
50 | } else {
51 | file
52 | }
53 | }
54 | internal val apk by lazy {
55 | ApkFile(apkPath)
56 | .apply { preferredLocale = Locale.getDefault() }
57 | }
58 |
59 | protected val pkg: String? by lazy { intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME) }
60 | protected val appLabel: String by lazy {
61 | intent.getStringExtra(EXTRA_APP_LABEL)
62 | ?: run {
63 | val l = packageInfo.appInfo?.loadLabel(packageManager)?.toString()
64 | if (l == packageInfo.appInfo?.name) null else l
65 | }
66 | ?: try {
67 | packageManager.getPackageInfoCompat(
68 | pkg ?: apk.apkMeta.packageName, 0
69 | ).applicationInfo.loadLabel(packageManager).toString()
70 | } catch (e: Exception) {
71 | apk.apkMeta.label ?: pkg ?: apk.apkMeta.packageName
72 | }
73 | }
74 | internal val file by lazy { intent.getSerializableExtra(EXTRA_FILE) as File? }
75 | internal val remRes by lazy { getAppRes(apk.getFile()) }
76 |
77 | protected val packageInfo by lazy {
78 | parsePackageCompat(apk.getFile(), pkg ?: apk.apkMeta.packageName, 0, true)
79 | }
80 |
81 | private var saveAll: MenuItem? = null
82 |
83 | override fun onCreate(savedInstanceState: Bundle?) {
84 | super.onCreate(savedInstanceState)
85 |
86 | if (pkg == null && file == null) {
87 | finish()
88 | return
89 | }
90 |
91 | updateTitle()
92 | }
93 |
94 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
95 | menuInflater.inflate(R.menu.batch, menu)
96 |
97 | saveAll = menu.findItem(R.id.all)
98 | saveAll?.setOnMenuItemClickListener {
99 | saveAllAct.launch(null)
100 |
101 | // val openIntent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
102 | // startActivityForResult(openIntent, REQ_CHOOSE_OUTPUT_DIR)
103 |
104 | true
105 | }
106 |
107 | return super.onCreateOptionsMenu(menu)
108 | }
109 |
110 | override fun checkCount() {
111 | super.checkCount()
112 |
113 | if (isReadyForMenus()) {
114 | saveAll?.isVisible = true
115 | }
116 | }
117 |
118 | override fun onLoadFinished() {
119 | super.onLoadFinished()
120 |
121 | updateTitle(adapter.itemCount)
122 | }
123 |
124 | override fun onDestroy() {
125 | super.onDestroy()
126 |
127 | destroyAppRes(apk.getFile())
128 | }
129 |
130 | private fun updateTitle(numberItems: Int = -1) = launch {
131 | title = withContext(Dispatchers.IO) {
132 | appLabel
133 | } + if (numberItems > -1) " ($numberItems)" else ""
134 | }
135 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/ui/activities/BatchExportDialogActivity.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.ui.activities
2 |
3 | import android.content.Intent
4 | import android.os.Bundle
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.core.content.ContextCompat
7 | import tk.zwander.sprviewer.ui.services.BatchExportService
8 | import tk.zwander.sprviewer.util.BatchExportListener
9 | import tk.zwander.sprviewer.util.app
10 | import tk.zwander.sprviewer.views.CircularProgressDialog
11 |
12 | class BatchExportDialogActivity : AppCompatActivity() {
13 | private val exportListener = object : BatchExportListener {
14 | override fun onBaseFilenameUpdate(filename: String) {
15 | dialogBuilder.setBaseFileName(filename)
16 | }
17 |
18 | override fun onCurrentFilenameUpdate(filename: String) {
19 | dialogBuilder.setCurrentFileName(filename)
20 | }
21 |
22 | override fun onProgressUpdate(current: Int, max: Int) {
23 | dialogBuilder.updateProgress(current, max)
24 | }
25 |
26 | override fun onSubProgressUpdate(current: Int, max: Int) {
27 | dialogBuilder.updateSubProgress(current, max)
28 | }
29 |
30 | override fun onExportComplete() {
31 | finish()
32 | }
33 | }
34 |
35 | private val dialogBuilder by lazy { CircularProgressDialog(this) }
36 | private val dialog by lazy { dialogBuilder.show() }
37 |
38 | override fun onCreate(savedInstanceState: Bundle?) {
39 | super.onCreate(savedInstanceState)
40 |
41 | app.batchExportListeners.add(exportListener)
42 |
43 | dialogBuilder.onCancelListener = {
44 | ContextCompat.startForegroundService(
45 | this,
46 | Intent(this, BatchExportService::class.java)
47 | .setAction(BatchExportService.ACTION_CANCEL_CURRENT_EXPORT)
48 | )
49 | finish()
50 | }
51 |
52 | dialog.setOnDismissListener {
53 | if (!isDestroyed) {
54 | finish()
55 | }
56 | }
57 | }
58 |
59 | override fun onDestroy() {
60 | super.onDestroy()
61 |
62 | app.batchExportListeners.remove(exportListener)
63 | if (dialog.isShowing) {
64 | dialog.dismiss()
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/ui/activities/DrawableListActivity.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.ui.activities
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import androidx.activity.result.ActivityResultLauncher
7 | import androidx.activity.result.contract.ActivityResultContracts
8 | import kotlinx.coroutines.launch
9 | import tk.zwander.sprviewer.data.UDrawableData
10 | import tk.zwander.sprviewer.ui.adapters.DrawableListAdapter
11 | import tk.zwander.sprviewer.ui.services.BatchExportService
12 | import tk.zwander.sprviewer.util.getFile
13 | import tk.zwander.sprviewer.views.BaseDimensionInputDialog
14 | import tk.zwander.sprviewer.views.ExportInfo
15 |
16 | class DrawableListActivity : BaseListActivity() {
17 | override val adapter by lazy {
18 | DrawableListAdapter(remRes) {
19 | val viewIntent = Intent(this, DrawableViewActivity::class.java)
20 | viewIntent.putExtra(DrawableViewActivity.EXTRA_DRAWABLE_INFO, it.toDrawableData())
21 | viewIntent.putExtras(intent)
22 |
23 | startActivity(viewIntent)
24 | }
25 | }
26 |
27 | override val saveAllAct: ActivityResultLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
28 | BaseDimensionInputDialog(this) { info ->
29 | handleBatchExport(info, result)
30 | }.show()
31 | }
32 |
33 | override fun onCreate(savedInstanceState: Bundle?) {
34 | super.onCreate(savedInstanceState)
35 |
36 | adapter.loadItemsAsync(apk, packageInfo, this::onLoadFinished) { size, count ->
37 | progress?.progress = (count.toFloat() / size.toFloat() * 100f).toInt()
38 | }
39 | }
40 |
41 | private fun handleBatchExport(info: ExportInfo, uri: Uri?) = launch {
42 | if (uri == null) return@launch
43 |
44 | contentResolver.takePersistableUriPermission(uri,
45 | Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
46 |
47 | BatchExportService.startBatchExport(
48 | this@DrawableListActivity,
49 | uri,
50 | adapter.allItemsCopy,
51 | info,
52 | appLabel,
53 | apk.getFile(),
54 | packageInfo.packageName
55 | )
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/ui/activities/DrawableViewActivity.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.ui.activities
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Activity
5 | import android.content.ContentResolver
6 | import android.content.Intent
7 | import android.graphics.Bitmap
8 | import android.graphics.Color
9 | import android.graphics.drawable.Drawable
10 | import android.net.Uri
11 | import android.os.Bundle
12 | import android.util.Log
13 | import android.view.Menu
14 | import android.view.MenuItem
15 | import android.view.View
16 | import android.widget.Toast
17 | import androidx.activity.result.contract.ActivityResultContracts
18 | import androidx.appcompat.app.AppCompatActivity
19 | import androidx.core.content.res.ResourcesCompat
20 | import androidx.core.graphics.drawable.toBitmap
21 | import androidx.core.view.isVisible
22 | import ar.com.hjg.pngj.ImageInfo
23 | import ar.com.hjg.pngj.ImageLineHelper
24 | import ar.com.hjg.pngj.ImageLineInt
25 | import ar.com.hjg.pngj.PngWriter
26 | import com.squareup.picasso.Callback
27 | import com.squareup.picasso.Picasso
28 | import kotlinx.coroutines.*
29 | import net.dongliu.apk.parser.ApkFile
30 | import tk.zwander.sprviewer.R
31 | import tk.zwander.sprviewer.data.DrawableData
32 | import tk.zwander.sprviewer.databinding.ActivityDrawableViewBinding
33 | import tk.zwander.sprviewer.util.*
34 | import tk.zwander.sprviewer.views.DimensionInputDialog
35 | import java.io.ByteArrayInputStream
36 | import java.io.File
37 | import java.io.OutputStream
38 | import java.util.*
39 |
40 | @SuppressLint("InlinedApi")
41 | @Suppress("DeferredResultUnused")
42 | class DrawableViewActivity : AppCompatActivity(), CoroutineScope by MainScope() {
43 | companion object {
44 | const val EXTRA_DRAWABLE_INFO = "drawable_info"
45 | }
46 |
47 | private val apkPath by lazy {
48 | if (pkg != null) {
49 | File(packageManager.getApplicationInfoCompat(pkg).sourceDir)
50 | } else {
51 | file
52 | }
53 | }
54 | private val apk: ApkFile by lazy {
55 | ApkFile(apkPath)
56 | .apply { preferredLocale = Locale.getDefault() }
57 | }
58 | private val drawableXml by lazyDeferred(context = Dispatchers.IO) {
59 | try {
60 | if (ext == "xml") apk.transBinaryXml(path)
61 | else null
62 | } catch (e: Exception) {
63 | null
64 | }
65 | }
66 | private val path by lazy { paths.last() }
67 | private val drawableInfo by lazy { intent.getParcelableExtraCompat(EXTRA_DRAWABLE_INFO) }
68 | private val pkg by lazy { intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME) }
69 | private val file by lazy { intent.getSerializableExtra(BaseListActivity.EXTRA_FILE) as File? }
70 | private val remRes by lazy { getAppRes(apkPath!!) }
71 | private val table by lazy {
72 | apk.resourceTable
73 | }
74 | private val paths by lazy {
75 | table.getResourcesById(drawableId.toLong()).map {
76 | it.resourceEntry.toStringValue(table, Locale.getDefault())
77 | }
78 | }
79 | private val packageInfo by lazy { parsePackageCompat(apk.getFile(), pkg, 0, true) }
80 | private val drawableName: String
81 | get() = drawableInfo!!.name
82 | private val drawableId: Int
83 | get() = drawableInfo!!.id
84 | private val ext: String?
85 | get() = drawableInfo!!.ext
86 |
87 | private val isViewingAnimatedImage: Boolean
88 | get() = binding.image.anim != null
89 |
90 | private val binding by lazy { ActivityDrawableViewBinding.inflate(layoutInflater) }
91 |
92 | private val savePngRequester = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
93 | if (result.resultCode == Activity.RESULT_OK) {
94 | val os = contentResolver.openOutputStream(result.data!!.data!!)!!
95 |
96 | saveAsPng(os)
97 | }
98 | }
99 |
100 | @OptIn(ExperimentalCoroutinesApi::class)
101 | private val saveXmlRequester = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
102 | if (result.resultCode == Activity.RESULT_OK) {
103 | val os = contentResolver.openOutputStream(result.data!!.data!!)!!
104 |
105 | saveXmlAsync(os)
106 | }
107 | }
108 |
109 | private val saveOrigRequester = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
110 | if (result.resultCode == Activity.RESULT_OK) {
111 | val os = contentResolver.openOutputStream(result.data!!.data!!)!!
112 |
113 | saveOrigAsync(os)
114 | }
115 | }
116 |
117 | private var saveImg: MenuItem? = null
118 | private var saveOrig: MenuItem? = null
119 | private var saveXml: MenuItem? = null
120 |
121 | @OptIn(ExperimentalCoroutinesApi::class)
122 | override fun onCreate(savedInstanceState: Bundle?) {
123 | super.onCreate(savedInstanceState)
124 | setContentView(binding.root)
125 |
126 | if (drawableInfo == null || (pkg.isNullOrBlank() && file == null)) {
127 | finish()
128 | return
129 | }
130 |
131 | supportActionBar?.setDisplayHomeAsUpEnabled(true)
132 | supportActionBar?.setDisplayShowHomeEnabled(true)
133 |
134 | title = "$drawableName.$ext"
135 |
136 | window.decorView?.findViewById(R.id.action_bar)?.apply {
137 | setOnLongClickListener {
138 | showTitleSnackBar(it)
139 |
140 | true
141 | }
142 | }
143 |
144 | launch {
145 | val drawableXml = drawableXml.getOrAwaitResult()
146 |
147 | if (drawableXml == null) {
148 | try {
149 | Picasso.get().load(
150 | Uri.parse(
151 | "${ContentResolver.SCHEME_ANDROID_RESOURCE}://" +
152 | "${packageInfo.packageName}/" +
153 | "${remRes.getResourceTypeName(drawableId)}/" +
154 | "$drawableId"
155 | )
156 | ).into(binding.image, object : Callback {
157 | override fun onError(e: Exception?) {
158 | try {
159 | binding.image.setImageDrawable(ResourcesCompat.getDrawable(remRes, drawableId, remRes.newTheme()))
160 |
161 | saveImg?.isVisible = !isViewingAnimatedImage
162 | } catch (e: Exception) {
163 | binding.image.isVisible = false
164 | binding.text.isVisible = true
165 |
166 | binding.text.text = drawableXml
167 | }
168 |
169 | imageLoadingDone()
170 | }
171 |
172 | override fun onSuccess() {
173 | saveImg?.isVisible = !isViewingAnimatedImage
174 | imageLoadingDone()
175 | }
176 | })
177 | } catch (e: Exception) {
178 | Log.e("SPRViewer", "ERR", e)
179 | Toast.makeText(this@DrawableViewActivity, R.string.load_image_error, Toast.LENGTH_SHORT).show()
180 | imageLoadingDone()
181 | }
182 | } else {
183 | try {
184 | binding.image.setImageDrawable(ResourcesCompat.getDrawable(remRes, drawableId, remRes.newTheme()))
185 |
186 | saveImg?.isVisible = !isViewingAnimatedImage
187 | } catch (e: Exception) {
188 | binding.image.isVisible = false
189 | binding.text.isVisible = true
190 |
191 | binding.text.text = drawableXml
192 | }
193 |
194 | imageLoadingDone()
195 | }
196 | }
197 | }
198 |
199 | private fun imageLoadingDone() {
200 | binding.loadingProgress.isVisible = false
201 | binding.imageTextWrapper.isVisible = true
202 | }
203 |
204 | override fun onDestroy() {
205 | super.onDestroy()
206 |
207 | cancel()
208 | destroyAppRes(apk.getFile())
209 | }
210 |
211 | @OptIn(ExperimentalCoroutinesApi::class)
212 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
213 | menuInflater.inflate(R.menu.save, menu)
214 |
215 | saveImg = menu.findItem(R.id.action_save_png)
216 | saveXml = menu.findItem(R.id.action_save_xml)
217 | saveOrig = menu.findItem(R.id.action_save_orig)
218 |
219 | saveOrig?.isVisible = ext == "spr" || ext == "astc"
220 |
221 | launch {
222 | saveXml?.isVisible = drawableXml.getOrAwaitResult() != null
223 | }
224 |
225 | return true
226 | }
227 |
228 | override fun onOptionsItemSelected(item: MenuItem): Boolean {
229 | return when (item.itemId) {
230 | R.id.action_save_png -> {
231 | startPngSave()
232 | true
233 | }
234 | R.id.action_save_xml -> {
235 | startXmlSave()
236 | true
237 | }
238 | R.id.action_save_orig -> {
239 | startOrigSave()
240 | true
241 | }
242 | android.R.id.home -> {
243 | onBackPressedDispatcher.onBackPressed()
244 | true
245 | }
246 | else -> super.onOptionsItemSelected(item)
247 | }
248 | }
249 |
250 | private fun startPngSave() {
251 | val saveIntent = Intent(Intent.ACTION_CREATE_DOCUMENT)
252 | val extension = if (extensionsToRasterize.contains(ext)) "png" else ext
253 |
254 | saveIntent.addCategory(Intent.CATEGORY_OPENABLE)
255 | saveIntent.type = "images/$extension"
256 | saveIntent.putExtra(Intent.EXTRA_TITLE, "$drawableName.$extension")
257 |
258 | savePngRequester.launch(saveIntent)
259 | }
260 |
261 | private fun startXmlSave() {
262 | val saveIntent = Intent(Intent.ACTION_CREATE_DOCUMENT)
263 |
264 | saveIntent.addCategory(Intent.CATEGORY_OPENABLE)
265 | saveIntent.type = "text/xml"
266 | saveIntent.putExtra(Intent.EXTRA_TITLE, "$drawableName.xml")
267 |
268 | saveXmlRequester.launch(saveIntent)
269 | }
270 |
271 | private fun startOrigSave() {
272 | val saveIntent = Intent(Intent.ACTION_CREATE_DOCUMENT)
273 |
274 | saveIntent.addCategory(Intent.CATEGORY_OPENABLE)
275 | saveIntent.type = "image/$ext"
276 | saveIntent.putExtra(Intent.EXTRA_TITLE, "$drawableName.$ext")
277 |
278 | saveOrigRequester.launch(saveIntent)
279 | }
280 |
281 | private fun saveAsPng(os: OutputStream) {
282 | if (!extensionsToRasterize.contains(ext)) {
283 | savePngDirectAsync(os)
284 | } else {
285 | handlePngSave(os)
286 | }
287 | }
288 |
289 | private fun handlePngSave(os: OutputStream) {
290 | binding.image.drawable.run {
291 | if (!extensionsToRasterize.contains(ext)) {
292 | compressPngAsync(toBitmap(), os)
293 | } else {
294 | getDimensions(this, os)
295 | }
296 | }
297 | }
298 |
299 | private fun compressPngAsync(bmp: Bitmap, os: OutputStream) = async(context = Dispatchers.IO) {
300 | setProgressVisible(true, indeterminate = false).join()
301 |
302 | try {
303 | val info = ImageInfo(bmp.width, bmp.height, 8, bmp.hasAlpha())
304 | val writer = PngWriter(os, info)
305 |
306 | writer.pixelsWriter.deflaterCompLevel = 0
307 |
308 | for (row in 0 until bmp.height) {
309 | val line = ImageLineInt(info)
310 |
311 | for (col in 0 until bmp.width) {
312 | if (bmp.hasAlpha()) {
313 | ImageLineHelper.setPixelRGBA8(line, col, bmp.getPixel(col, row))
314 | } else {
315 | ImageLineHelper.setPixelRGB8(line, col, bmp.getPixel(col, row))
316 | }
317 | }
318 |
319 | writer.writeRow(line)
320 | setCurrentProgress(row + 1, bmp.height)
321 | }
322 |
323 | writer.end()
324 | } finally {
325 | os.close()
326 | setProgressVisible(false).join()
327 | }
328 | }
329 |
330 | private fun savePngDirectAsync(os: OutputStream, bytes: ByteArray? = null) = async(context = Dispatchers.IO) {
331 | val res = if (bytes != null) ByteArrayInputStream(bytes) else remRes.openRawResource(drawableId)
332 |
333 | val buffer = ByteArray(16384)
334 |
335 | setProgressVisible(true).join()
336 |
337 | val max = res.available()
338 | var n: Int
339 |
340 | try {
341 | while (true) {
342 | n = res.read(buffer)
343 |
344 | if (n <= 0) break
345 |
346 | os.write(buffer, 0, n)
347 | setCurrentProgress(max - res.available(), max).join()
348 | }
349 | } finally {
350 | res.close()
351 | os.close()
352 |
353 | setProgressVisible(false).join()
354 | }
355 | }
356 |
357 | @ExperimentalCoroutinesApi
358 | private fun saveXmlAsync(os: OutputStream) = async(context = Dispatchers.IO) {
359 | setProgressVisible(visible = true, indeterminate = false)
360 |
361 | os.use { output ->
362 | drawableXml.getOrAwaitResult()?.byteInputStream()?.use { input ->
363 | val buffer = ByteArray(16384)
364 | val max = input.available()
365 |
366 | var n: Int
367 |
368 | while (true) {
369 | n = input.read(buffer)
370 |
371 | if (n <= 0) break
372 |
373 | output.write(buffer, 0, n)
374 |
375 | val avail = input.available()
376 | setCurrentProgress(max - avail).join()
377 | }
378 | }
379 | }
380 |
381 | setProgressVisible(false)
382 | }
383 |
384 | private fun saveOrigAsync(os: OutputStream) = async(context = Dispatchers.IO) {
385 | setProgressVisible(visible = true, indeterminate = false)
386 |
387 | os.use { output ->
388 | remRes.openRawResource(drawableId).use { input ->
389 | val buffer = ByteArray(16384)
390 | val max = input.available()
391 |
392 | var n: Int
393 |
394 | while (true) {
395 | n = input.read(buffer)
396 |
397 | if (n <= 0) break
398 |
399 | output.write(buffer, 0, n)
400 |
401 | val avail = input.available()
402 | setCurrentProgress(max - avail).join()
403 | }
404 | }
405 | }
406 |
407 | setProgressVisible(false).join()
408 | }
409 |
410 | private fun setProgressVisible(visible: Boolean, indeterminate: Boolean = false) = launch {
411 | binding.exportProgress.isVisible = visible
412 | binding.exportProgress.isIndeterminate = indeterminate
413 | binding.exportProgress.progress = 0
414 | }
415 |
416 | private fun setMaxProgress(max: Int) = launch {
417 | binding.exportProgress.max = max
418 | }
419 |
420 | private fun setCurrentProgress(current: Int, max: Int = 100) = launch {
421 | setMaxProgress(max)
422 | binding.exportProgress.progress = current
423 | }
424 |
425 | private fun getDimensions(drawable: Drawable, os: OutputStream) = launch {
426 | DimensionInputDialog(this@DrawableViewActivity, drawable)
427 | .apply {
428 | saveListener = { width, height, tint -> compressPngAsync(
429 | drawable.mutate().apply {
430 | if (tint != Color.TRANSPARENT) {
431 | setTint(tint)
432 | }
433 | }.toBitmap(width, height), os)
434 | }
435 | }
436 | .show()
437 | }
438 | }
439 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/ui/activities/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.ui.activities
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Activity
5 | import android.content.Intent
6 | import android.content.pm.PackageManager
7 | import android.os.Build
8 | import android.os.Bundle
9 | import android.view.Menu
10 | import android.view.MenuItem
11 | import androidx.activity.result.contract.ActivityResultContracts
12 | import androidx.recyclerview.widget.RecyclerView
13 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
14 | import com.reddit.indicatorfastscroll.FastScrollerThumbView
15 | import com.reddit.indicatorfastscroll.FastScrollerView
16 | import tk.zwander.sprviewer.R
17 | import tk.zwander.sprviewer.data.AppData
18 | import tk.zwander.sprviewer.databinding.ActivityMainBinding
19 | import tk.zwander.sprviewer.ui.adapters.AppListAdapter
20 | import java.io.File
21 |
22 | class MainActivity : BaseActivity() {
23 | override val contentView by lazy { binding.root }
24 | override val adapter = AppListAdapter(
25 | itemSelectedListener = {
26 | openDrawableActivity(it.pkg, it.label)
27 | },
28 | valuesSelectedListener = {
29 | openValuesActivity(it.pkg, it.label)
30 | }
31 | )
32 |
33 | override val recycler: RecyclerView
34 | get() = binding.recycler
35 | override val scrollerThumb: FastScrollerThumbView
36 | get() = binding.scrollerThumb
37 | override val scroller: FastScrollerView
38 | get() = binding.scroller
39 |
40 | internal val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
41 |
42 | private val notifRequester =
43 | registerForActivityResult(ActivityResultContracts.RequestPermission()) {
44 | if (!it) {
45 | finish()
46 | }
47 | }
48 | private val apkImportReq = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
49 | if (result.resultCode == Activity.RESULT_OK) {
50 | result?.data?.data?.also { uri ->
51 | contentResolver.openInputStream(uri).use { inputStream ->
52 | val apk = File(cacheDir, "temp_apk.apk")
53 |
54 | apk.outputStream().use { outputStream ->
55 | inputStream.copyTo(outputStream)
56 | }
57 |
58 | MaterialAlertDialogBuilder(this)
59 | .setTitle(R.string.import_apk)
60 | .setMessage(R.string.import_apk_choice)
61 | .setPositiveButton(R.string.view_images) { _, _ ->
62 | openDrawableActivity(apk)
63 | }
64 | .setNegativeButton(R.string.view_strings) { _, _ ->
65 | openValuesActivity(apk)
66 | }
67 | .setNeutralButton(android.R.string.cancel, null)
68 | .show()
69 | }
70 | }
71 | }
72 | }
73 |
74 | private var importItem: MenuItem? = null
75 |
76 | override fun onCreate(savedInstanceState: Bundle?) {
77 | super.onCreate(savedInstanceState)
78 |
79 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
80 | checkCallingOrSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED
81 | ) {
82 | notifRequester.launch(android.Manifest.permission.POST_NOTIFICATIONS)
83 | }
84 |
85 | adapter.loadItemsAsync(this, this::onLoadFinished) { size, count ->
86 | progress?.apply {
87 | progress = (count.toFloat() / size.toFloat() * 100f).toInt()
88 | }
89 | }
90 | }
91 |
92 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
93 | menuInflater.inflate(R.menu.import_apk, menu)
94 |
95 | importItem = menu.findItem(R.id.action_import)
96 | importItem?.setOnMenuItemClickListener {
97 | val openIntent = Intent(Intent.ACTION_OPEN_DOCUMENT)
98 |
99 | openIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
100 | openIntent.addCategory(Intent.CATEGORY_OPENABLE)
101 | openIntent.type = "application/vnd.android.package-archive"
102 |
103 | apkImportReq.launch(openIntent)
104 |
105 | true
106 | }
107 |
108 | return super.onCreateOptionsMenu(menu)
109 | }
110 |
111 | override fun checkCount() {
112 | super.checkCount()
113 |
114 | if (isReadyForMenus()) {
115 | importItem?.isVisible = true
116 | }
117 | }
118 |
119 | @SuppressLint("InlinedApi")
120 | private fun openDrawableActivity(pkg: String, label: String) {
121 | val drawableIntent = Intent(this, DrawableListActivity::class.java)
122 | drawableIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, pkg)
123 | drawableIntent.putExtra(BaseListActivity.EXTRA_APP_LABEL, label)
124 |
125 | startActivity(drawableIntent)
126 | }
127 |
128 | private fun openDrawableActivity(apk: File) {
129 | val drawableIntent = Intent(this, DrawableListActivity::class.java)
130 | drawableIntent.putExtra(BaseListActivity.EXTRA_FILE, apk)
131 |
132 | startActivity(drawableIntent)
133 | }
134 |
135 | @SuppressLint("InlinedApi")
136 | private fun openValuesActivity(pkg: String, label: String) {
137 | val valueIntent = Intent(this, StringsListActivity::class.java)
138 | valueIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, pkg)
139 | valueIntent.putExtra(BaseListActivity.EXTRA_APP_LABEL, label)
140 |
141 | startActivity(valueIntent)
142 | }
143 |
144 | private fun openValuesActivity(apk: File) {
145 | val valueIntent = Intent(this, StringsListActivity::class.java)
146 | valueIntent.putExtra(BaseListActivity.EXTRA_FILE, apk)
147 |
148 | startActivity(valueIntent)
149 | }
150 | }
151 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/ui/activities/StringsListActivity.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.ui.activities
2 |
3 | import android.content.Intent
4 | import android.net.Uri
5 | import android.os.Bundle
6 | import androidx.activity.result.ActivityResultLauncher
7 | import androidx.activity.result.contract.ActivityResultContracts
8 | import androidx.documentfile.provider.DocumentFile
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.launch
11 | import kotlinx.coroutines.withContext
12 | import tk.zwander.sprviewer.data.StringXmlData
13 | import tk.zwander.sprviewer.ui.adapters.StringsListAdapter
14 |
15 |
16 | class StringsListActivity : BaseListActivity() {
17 | override val adapter = StringsListAdapter {
18 | StringsViewActivity.start(this, it, packageInfo.packageName)
19 | }
20 |
21 | override val saveAllAct: ActivityResultLauncher = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { result ->
22 | if (result == null) return@registerForActivityResult
23 |
24 | contentResolver.takePersistableUriPermission(result,
25 | Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
26 |
27 | progressItem?.isVisible = true
28 | progress?.progress = 0
29 |
30 | launch {
31 | withContext(Dispatchers.IO) {
32 | val parentDir = DocumentFile.fromTreeUri(this@StringsListActivity, result)
33 | val dir = parentDir?.createDirectory(packageInfo.packageName)
34 | val items = adapter.allItemsCopy
35 |
36 | items.forEachIndexed { index, stringXmlData ->
37 | val file = dir?.createFile("text/xml", "strings_${stringXmlData.locale}.xml")
38 |
39 | contentResolver.openOutputStream(file!!.uri).bufferedWriter().use { writer ->
40 | writer.write(stringXmlData.asXmlString())
41 | }
42 |
43 | withContext(Dispatchers.Main) {
44 | progress?.progress = ((index + 1f) / items.size.toFloat() * 100f).toInt()
45 | }
46 | }
47 | }
48 |
49 | progressItem?.isVisible = false
50 | }
51 | }
52 |
53 | override fun onCreate(savedInstanceState: Bundle?) {
54 | super.onCreate(savedInstanceState)
55 |
56 | adapter.loadItemsAsync(apk, this::onLoadFinished) { size, count ->
57 | progress?.progress = (count.toFloat() / size.toFloat() * 100f).toInt()
58 | }
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/ui/activities/StringsViewActivity.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.ui.activities
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.*
5 | import android.os.Bundle
6 | import android.view.Menu
7 | import android.view.MenuItem
8 | import android.widget.Toast
9 | import androidx.activity.result.contract.ActivityResultContracts
10 | import androidx.recyclerview.widget.RecyclerView
11 | import com.reddit.indicatorfastscroll.FastScrollerThumbView
12 | import com.reddit.indicatorfastscroll.FastScrollerView
13 | import kotlinx.coroutines.Dispatchers
14 | import kotlinx.coroutines.launch
15 | import kotlinx.coroutines.withContext
16 | import tk.zwander.sprviewer.R
17 | import tk.zwander.sprviewer.data.StringData
18 | import tk.zwander.sprviewer.data.StringXmlData
19 | import tk.zwander.sprviewer.databinding.ActivityMainBinding
20 | import tk.zwander.sprviewer.ui.adapters.StringsViewAdapter
21 |
22 | @SuppressLint("InlinedApi")
23 | class StringsViewActivity : BaseActivity() {
24 | companion object {
25 | var stringInfo: StringXmlData? = null
26 |
27 | fun start(context: Context, info: StringXmlData, pkg: String) {
28 | val intent = Intent(context, StringsViewActivity::class.java)
29 | intent.putExtra(Intent.EXTRA_PACKAGE_NAME, pkg)
30 |
31 | stringInfo = info
32 |
33 | context.startActivity(intent)
34 | }
35 | }
36 |
37 | override val contentView by lazy { binding.root }
38 | override val hasBackButton = true
39 |
40 | override val recycler: RecyclerView
41 | get() = binding.recycler
42 | override val scrollerThumb: FastScrollerThumbView
43 | get() = binding.scrollerThumb
44 | override val scroller: FastScrollerView
45 | get() = binding.scroller
46 |
47 | internal val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
48 |
49 | override val adapter = StringsViewAdapter {
50 | val cm = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
51 | cm.primaryClip = ClipData.newPlainText(
52 | it.value, it.value
53 | )
54 | Toast.makeText(this, R.string.copied, Toast.LENGTH_SHORT).show()
55 | }
56 |
57 | private val pkgName by lazy { intent.getStringExtra(Intent.EXTRA_PACKAGE_NAME) }
58 | private val saveLauncher = registerForActivityResult(ActivityResultContracts.CreateDocument("text/xml")) { result ->
59 | if (result != null) {
60 | contentResolver.openOutputStream(result).bufferedWriter().use {
61 | val xml = stringInfo ?: return@use
62 | it.write(xml.asXmlString())
63 | }
64 | }
65 | }
66 |
67 | private var saveAll: MenuItem? = null
68 |
69 | override fun onCreate(savedInstanceState: Bundle?) {
70 | super.onCreate(savedInstanceState)
71 |
72 | if (stringInfo == null || pkgName == null) {
73 | finish()
74 | return
75 | }
76 |
77 | adapter.loadItemsAsync(stringInfo!!, this::onLoadFinished)
78 |
79 | updateTitle()
80 | }
81 |
82 | override fun onDestroy() {
83 | stringInfo = null
84 | super.onDestroy()
85 | }
86 |
87 | override fun onLoadFinished() {
88 | super.onLoadFinished()
89 |
90 | if (isReadyForMenus()) {
91 | saveAll?.isVisible = true
92 | }
93 |
94 | updateTitle(adapter.itemCount)
95 | }
96 |
97 | override fun onCreateOptionsMenu(menu: Menu): Boolean {
98 | menuInflater.inflate(R.menu.batch, menu)
99 |
100 | saveAll = menu.findItem(R.id.all)
101 | saveAll?.setOnMenuItemClickListener {
102 | saveLauncher.launch("${pkgName}-strings_${stringInfo!!.locale}.xml")
103 |
104 | true
105 | }
106 | if (isReadyForMenus()) {
107 | saveAll?.isVisible = true
108 | }
109 |
110 | return super.onCreateOptionsMenu(menu)
111 | }
112 |
113 | private fun updateTitle(numberStrings: Int = -1) = launch {
114 | title = withContext(Dispatchers.IO) {
115 | stringInfo!!.constructLabel()
116 | } + if (numberStrings > -1) " ($numberStrings)" else ""
117 | }
118 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/ui/adapters/AppListAdapter.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.ui.adapters
2 |
3 | import android.content.Context
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.core.view.isVisible
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.launch
10 | import kotlinx.coroutines.withContext
11 | import tk.zwander.sprviewer.R
12 | import tk.zwander.sprviewer.data.AppData
13 | import tk.zwander.sprviewer.databinding.AppInfoLayoutBinding
14 | import tk.zwander.sprviewer.util.getInstalledApps
15 |
16 | class AppListAdapter(private val itemSelectedListener: (AppData) -> Unit, private val valuesSelectedListener: (AppData) -> Unit) : BaseListAdapter() {
17 | override val viewRes = R.layout.app_info_layout
18 |
19 | override fun onCreateViewHolder(parent: ViewGroup, position: Int): AppVH {
20 | return AppVH(
21 | LayoutInflater.from(parent.context).inflate(
22 | viewRes, parent, false
23 | )
24 | )
25 | }
26 |
27 | override fun onBindViewHolder(holder: AppVH, position: Int, info: AppData) {
28 | holder.bind(info)
29 | }
30 |
31 | override fun matches(query: String, data: AppData): Boolean {
32 | return data.pkg.contains(query, true) || data.constructLabel().contains(query, true)
33 | }
34 |
35 | override fun compare(o1: AppData, o2: AppData): Int {
36 | val initialCompare = o1.constructLabel().compareTo(o2.constructLabel(), true)
37 | return if (initialCompare != 0) initialCompare else
38 | o1.pkg.compareTo(o2.pkg)
39 | }
40 |
41 | override fun areContentsTheSame(oldItem: AppData, newItem: AppData): Boolean {
42 | return oldItem.pkg == newItem.pkg
43 | }
44 |
45 | fun loadItemsAsync(context: Context, listener: () -> Unit, progressListener: (Int, Int) -> Unit) = launch {
46 | val apps = withContext(Dispatchers.IO) {
47 | context.getInstalledApps { _, size, count ->
48 | progressListener(size, count)
49 | }
50 | }
51 |
52 | addAll(apps)
53 | listener()
54 | }
55 |
56 | inner class AppVH(view: View) : BaseVH(view) {
57 | private val binding = AppInfoLayoutBinding.bind(itemView)
58 |
59 | fun bind(info: AppData) {
60 | binding.apply {
61 | icon.setImageDrawable(info.icon)
62 | appName.text = info.constructLabel()
63 | appPkg.text = info.pkg
64 |
65 | extrasLayout.isVisible = info.expanded
66 | viewImages.setOnClickListener {
67 | itemSelectedListener(getInfo(bindingAdapterPosition))
68 | }
69 | viewStrings.setOnClickListener {
70 | valuesSelectedListener(getInfo(bindingAdapterPosition))
71 | }
72 | }
73 |
74 | itemView.setOnClickListener {
75 | getInfo(bindingAdapterPosition).apply {
76 | expanded = !expanded
77 | notifyItemChanged(bindingAdapterPosition)
78 | }
79 | }
80 | }
81 | }
82 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/ui/adapters/BaseListAdapter.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.ui.adapters
2 |
3 | import android.annotation.SuppressLint
4 | import android.view.View
5 | import androidx.appcompat.widget.SearchView
6 | import androidx.recyclerview.widget.RecyclerView
7 | import androidx.recyclerview.widget.SortedList
8 | import kotlinx.coroutines.*
9 | import tk.zwander.sprviewer.data.BaseData
10 | import tk.zwander.sprviewer.util.get
11 | import java.util.*
12 |
13 | abstract class BaseListAdapter : RecyclerView.Adapter(), SearchView.OnQueryTextListener, CoroutineScope by MainScope() {
14 | private val batchedCallback = SortedList.BatchedCallback(InnerSortCallback())
15 | internal val actuallyVisible = TreeSet { o1, o2 -> compare(o1, o2) }
16 |
17 | private val orig = object : ArrayList() {
18 | override fun add(element: T): Boolean {
19 | launch {
20 | if (matches(currentQuery, element)) {
21 | actuallyVisible.add(element)
22 | notifyItemInserted(actuallyVisible.indexOf(element))
23 | }
24 | }
25 | return super.add(element)
26 | }
27 |
28 | override fun addAll(elements: Collection): Boolean {
29 | replaceAll(elements)
30 | return super.addAll(elements)
31 | }
32 |
33 | override fun remove(element: T): Boolean {
34 | actuallyVisible.remove(element)
35 | return super.remove(element)
36 | }
37 | }
38 |
39 | private var currentQuery = ""
40 | private var recyclerView: RecyclerView? = null
41 |
42 | internal abstract val viewRes: Int
43 |
44 | val allItemsCopy: MutableList
45 | get() = ArrayList(orig)
46 |
47 | val fullItemCount: Int
48 | get() = orig.size
49 |
50 | override fun getItemCount() = actuallyVisible.size
51 |
52 | override fun getItemId(position: Int): Long {
53 | return actuallyVisible.get(position).hashCode().toLong()
54 | }
55 |
56 | override fun onQueryTextChange(newText: String?): Boolean {
57 | return false
58 | }
59 |
60 | override fun onQueryTextSubmit(query: String?): Boolean {
61 | doFilter(query)
62 | return true
63 | }
64 |
65 | override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
66 | this.recyclerView = recyclerView
67 | super.onAttachedToRecyclerView(recyclerView)
68 | }
69 |
70 | override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
71 | this.recyclerView = null
72 | cancel()
73 | super.onDetachedFromRecyclerView(recyclerView)
74 | }
75 |
76 | final override fun onBindViewHolder(holder: VH, position: Int) {
77 | onBindViewHolder(holder, position, actuallyVisible.get(position))
78 | }
79 |
80 | private fun doFilter(newText: String?) {
81 | runBlocking {
82 | currentQuery = newText ?: ""
83 |
84 | val filtered = withContext(Dispatchers.IO) {
85 | filter(currentQuery)
86 | }
87 |
88 | replaceAll(filtered)
89 | recyclerView?.scrollToPosition(0)
90 | }
91 | }
92 |
93 | abstract fun compare(o1: T, o2: T): Int
94 | abstract fun areContentsTheSame(oldItem: T, newItem: T): Boolean
95 | abstract fun matches(query: String, data: T): Boolean
96 | abstract fun onBindViewHolder(holder: VH, position: Int, info: T)
97 |
98 | open fun areItemsTheSame(item1: T, item2: T): Boolean {
99 | return item1 == item2
100 | }
101 |
102 | fun add(item: T) {
103 | orig.add(item)
104 | }
105 |
106 | fun addAll(items: Collection) {
107 | orig.addAll(items)
108 | }
109 |
110 | fun remove(item: T) {
111 | orig.remove(item)
112 | }
113 |
114 | fun indexOf(item: T) = actuallyVisible.indexOf(item)
115 |
116 | fun addToActuallyVisible(items: Collection) {
117 | actuallyVisible.addAll(items)
118 | }
119 |
120 | internal fun getInfo(position: Int) = actuallyVisible.get(position)
121 |
122 | private fun filter(query: String): List {
123 | val lowerCaseQuery = query.lowercase(Locale.getDefault())
124 |
125 | val filteredModelList = LinkedList()
126 |
127 | orig.forEach { item ->
128 | if (matches(lowerCaseQuery, item)) filteredModelList.add(item)
129 | }
130 |
131 | return filteredModelList
132 | }
133 |
134 | @SuppressLint("NotifyDataSetChanged")
135 | private fun replaceAll(newItems: Collection) = runBlocking {
136 | actuallyVisible.clear()
137 | actuallyVisible.addAll(newItems)
138 | notifyDataSetChanged()
139 | }
140 |
141 | inner class InnerSortCallback : SortedList.Callback() {
142 | override fun compare(o1: T, o2: T): Int {
143 | return this@BaseListAdapter.compare(o1, o2)
144 | }
145 |
146 | override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
147 | return this@BaseListAdapter.areContentsTheSame(oldItem, newItem)
148 | }
149 |
150 | override fun areItemsTheSame(item1: T, item2: T): Boolean {
151 | return this@BaseListAdapter.areItemsTheSame(item1, item2)
152 | }
153 |
154 | override fun onInserted(position: Int, count: Int) {
155 | notifyItemRangeInserted(position, count)
156 | }
157 |
158 | override fun onRemoved(position: Int, count: Int) {
159 | notifyItemRangeRemoved(position, count)
160 | }
161 |
162 | override fun onChanged(position: Int, count: Int) {
163 | notifyItemChanged(position, count)
164 | }
165 |
166 | override fun onMoved(fromPosition: Int, toPosition: Int) {
167 | notifyItemMoved(fromPosition, toPosition)
168 | }
169 | }
170 |
171 | open class BaseVH(view: View) : RecyclerView.ViewHolder(view)
172 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/ui/adapters/DrawableListAdapter.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.ui.adapters
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.ContentResolver
5 | import android.content.res.Resources
6 | import android.net.Uri
7 | import android.util.Log
8 | import android.view.LayoutInflater
9 | import android.view.View
10 | import android.view.ViewGroup
11 | import androidx.core.content.res.ResourcesCompat
12 | import androidx.core.view.isVisible
13 | import com.squareup.picasso.Callback
14 | import com.squareup.picasso.Picasso
15 | import kotlinx.coroutines.Dispatchers
16 | import kotlinx.coroutines.launch
17 | import kotlinx.coroutines.withContext
18 | import net.dongliu.apk.parser.ApkFile
19 | import tk.zwander.sprviewer.R
20 | import tk.zwander.sprviewer.data.CustomPackageInfo
21 | import tk.zwander.sprviewer.data.UDrawableData
22 | import tk.zwander.sprviewer.databinding.DrawableInfoLayoutBinding
23 | import tk.zwander.sprviewer.util.getAppDrawables
24 |
25 | class DrawableListAdapter(private val remRes: Resources, private val itemSelectedListener: (UDrawableData) -> Unit) : BaseListAdapter() {
26 | override val viewRes = R.layout.drawable_info_layout
27 |
28 | @SuppressLint("SetTextI18n")
29 | override fun onBindViewHolder(holder: ListVH, position: Int, info: UDrawableData) {
30 | holder.onBind(info)
31 | }
32 |
33 | override fun onCreateViewHolder(parent: ViewGroup, position: Int): ListVH {
34 | return ListVH(
35 | LayoutInflater.from(parent.context)
36 | .inflate(viewRes, parent, false)
37 | )
38 | }
39 |
40 | override fun compare(o1: UDrawableData, o2: UDrawableData): Int {
41 | val names = o1.name.compareTo(o2.name, true)
42 | val paths = o1.path.compareTo(o2.path, true)
43 | return if (names == 0) paths else names
44 | }
45 |
46 | override fun areContentsTheSame(oldItem: UDrawableData, newItem: UDrawableData): Boolean {
47 | return oldItem.path == newItem.path
48 | }
49 |
50 | override fun matches(query: String, data: UDrawableData): Boolean {
51 | return data.path.contains(query, true) || "${data.name}.${data.ext}".contains(query, true)
52 | }
53 |
54 | fun loadItemsAsync(
55 | apk: ApkFile,
56 | packageInfo: CustomPackageInfo,
57 | listener: () -> Unit,
58 | progressListener: (Int, Int) -> Unit
59 | ) = launch {
60 | val drawables = withContext(Dispatchers.IO) {
61 | getAppDrawables(apk, packageInfo) { _, size, count ->
62 | launch(Dispatchers.Main) {
63 | progressListener(size, count)
64 | }
65 | }
66 | }
67 |
68 | withContext(Dispatchers.Main) {
69 | addAll(drawables)
70 | listener()
71 | }
72 | }
73 |
74 | inner class ListVH(view: View) : BaseVH(view) {
75 | private val binding = DrawableInfoLayoutBinding.bind(itemView)
76 |
77 | @SuppressLint("SetTextI18n")
78 | fun onBind(info: UDrawableData) {
79 | itemView.apply {
80 | binding.extIndicator.isVisible = true
81 | binding.imgPreview.isVisible = true
82 |
83 | try {
84 | Picasso.get().cancelRequest(binding.imgPreview)
85 |
86 | Picasso.get().load(
87 | Uri.parse(
88 | "${ContentResolver.SCHEME_ANDROID_RESOURCE}://" +
89 | "${info.packageInfo.packageName}/" +
90 | "${remRes.getResourceTypeName(info.id)}/" +
91 | "${info.id}"
92 | )
93 | ).into(binding.imgPreview, object : Callback {
94 | override fun onError(e: Exception?) {
95 | launch {
96 | withContext(Dispatchers.Main) {
97 | try {
98 | binding.imgPreview.setImageDrawable(
99 | ResourcesCompat.getDrawable(remRes, info.id, remRes.newTheme()))
100 | binding.extIndicator.isVisible = false
101 | } catch (e: Exception) {
102 | binding.imgPreview.isVisible = false
103 | }
104 | }
105 | }
106 | }
107 |
108 | override fun onSuccess() {
109 | binding.extIndicator.isVisible = false
110 | }
111 | })
112 |
113 | } catch (e: Exception) {
114 | Log.e("SPRViewer", "ERR", e)
115 | }
116 |
117 | binding.drawableName.text = "${info.name}.${info.ext}"
118 | binding.extIndicator.setText(info.ext)
119 | binding.drawablePath.text = info.path
120 |
121 | setOnClickListener {
122 | itemSelectedListener(getInfo(bindingAdapterPosition))
123 | }
124 | }
125 | }
126 | }
127 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/ui/adapters/StringsListAdapter.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.ui.adapters
2 |
3 | import android.annotation.SuppressLint
4 | import android.view.LayoutInflater
5 | import android.view.View
6 | import android.view.ViewGroup
7 | import androidx.core.view.isVisible
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.launch
10 | import kotlinx.coroutines.withContext
11 | import net.dongliu.apk.parser.ApkFile
12 | import tk.zwander.sprviewer.R
13 | import tk.zwander.sprviewer.data.StringXmlData
14 | import tk.zwander.sprviewer.databinding.StringListInfoLayoutBinding
15 | import tk.zwander.sprviewer.util.*
16 |
17 | class StringsListAdapter(private val itemSelectedListener: (StringXmlData) -> Unit) : BaseListAdapter() {
18 | override val viewRes = R.layout.string_list_info_layout
19 |
20 | @SuppressLint("SetTextI18n")
21 | override fun onBindViewHolder(holder: StringListVH, position: Int, info: StringXmlData) {
22 | holder.onBind(info)
23 | }
24 |
25 | override fun onCreateViewHolder(parent: ViewGroup, position: Int): StringListVH {
26 | return StringListVH(
27 | LayoutInflater.from(parent.context)
28 | .inflate(viewRes, parent, false)
29 | )
30 | }
31 |
32 | override fun compare(o1: StringXmlData, o2: StringXmlData): Int {
33 | val names = o1.locale.toString().compareTo(o2.locale.toString(), true)
34 | val paths = o1.locale.toString().compareTo(o2.locale.toString(), true)
35 | return if (names == 0) paths else names
36 | }
37 |
38 | override fun areContentsTheSame(oldItem: StringXmlData, newItem: StringXmlData): Boolean {
39 | return oldItem.locale == newItem.locale
40 | }
41 |
42 | override fun matches(query: String, data: StringXmlData): Boolean {
43 | return data.constructLabel().contains(query, true)
44 | }
45 |
46 | fun loadItemsAsync(
47 | apk: ApkFile,
48 | listener: () -> Unit,
49 | progressListener: (Int, Int) -> Unit
50 | ) = launch {
51 | val values = withContext(Dispatchers.IO) {
52 | getAppStringXmls(apk) { _, size, count ->
53 | launch(Dispatchers.Main) {
54 | progressListener(size, count)
55 | }
56 | }
57 | }
58 |
59 | withContext(Dispatchers.Main) {
60 | addAll(values)
61 |
62 | listener()
63 | }
64 | }
65 |
66 | inner class StringListVH(view: View) : BaseVH(view) {
67 | private val binding = StringListInfoLayoutBinding.bind(itemView)
68 |
69 | @SuppressLint("SetTextI18n")
70 | fun onBind(info: StringXmlData) {
71 | binding.apply {
72 | extIndicator.isVisible = true
73 | imgPreview.isVisible = true
74 |
75 | extIndicator.setText(info.locale.toString().run {
76 | if (isBlank()) {
77 | "DEF"
78 | } else {
79 | substring(0, 4.coerceAtMost(length))
80 | }
81 | })
82 |
83 | stringFileName.text = info.constructLabel()
84 | stringCount.text = info.values.size.toString()
85 | }
86 |
87 | itemView.setOnClickListener {
88 | itemSelectedListener(getInfo(bindingAdapterPosition))
89 | }
90 | }
91 | }
92 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/ui/adapters/StringsViewAdapter.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.ui.adapters
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import kotlinx.coroutines.*
7 | import tk.zwander.sprviewer.R
8 | import tk.zwander.sprviewer.data.StringData
9 | import tk.zwander.sprviewer.data.StringXmlData
10 | import tk.zwander.sprviewer.databinding.DrawableInfoLayoutBinding
11 |
12 | class StringsViewAdapter(private val itemSelectedCallback: (item: StringData) -> Unit) : BaseListAdapter() {
13 | override val viewRes = R.layout.drawable_info_layout
14 |
15 | override fun compare(o1: StringData, o2: StringData): Int {
16 | return o1.compareTo(o2)
17 | }
18 |
19 | override fun areContentsTheSame(oldItem: StringData, newItem: StringData): Boolean {
20 | return oldItem == newItem
21 | }
22 |
23 | override fun matches(query: String, data: StringData): Boolean {
24 | return data.key.contains(query, true) || data.value.contains(query, true)
25 | }
26 |
27 | override fun onCreateViewHolder(parent: ViewGroup, position: Int): StringViewVH {
28 | return StringViewVH(
29 | LayoutInflater.from(parent.context)
30 | .inflate(viewRes, parent, false)
31 | )
32 | }
33 |
34 | override fun onBindViewHolder(holder: StringViewVH, position: Int, info: StringData) {
35 | holder.onBind(info)
36 | }
37 |
38 | fun loadItemsAsync(
39 | info: StringXmlData,
40 | listener: () -> Unit
41 | ) = launch {
42 | withContext(Dispatchers.Main) {
43 | delay(100)
44 |
45 | addAll(info.values)
46 |
47 | listener()
48 | }
49 | }
50 |
51 | inner class StringViewVH(view: View) : BaseVH(view) {
52 | private val binding = DrawableInfoLayoutBinding.bind(itemView)
53 |
54 | fun onBind(info: StringData) {
55 | binding.apply {
56 | drawableName.text = info.key
57 | drawablePath.text = info.value
58 |
59 | extIndicator.setText("STR")
60 | }
61 |
62 | itemView.setOnClickListener {
63 | itemSelectedCallback(getInfo(bindingAdapterPosition))
64 | }
65 | }
66 | }
67 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/util/APKUtils.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("DEPRECATION")
2 |
3 | package tk.zwander.sprviewer.util
4 |
5 | import android.annotation.SuppressLint
6 | import android.content.pm.PackageManager.*
7 | import android.content.pm.PackageParser
8 | import android.content.pm.parsing.result.ParseInput
9 | import android.content.pm.parsing.result.ParseTypeImpl
10 | import android.content.pm.pkg.FrameworkPackageUserState
11 | import android.os.Build
12 | import android.permission.PermissionManager
13 | import net.dongliu.apk.parser.ApkFile
14 | import tk.zwander.sprviewer.data.CustomPackageInfo
15 | import java.io.File
16 | import java.util.zip.ZipFile
17 |
18 |
19 | fun ApkFile.getFile(): File {
20 | return ApkFile::class.java
21 | .getDeclaredField("apkFile")
22 | .apply { isAccessible = true }
23 | .get(this) as File
24 | }
25 |
26 | fun ApkFile.getZipFile(): ZipFile {
27 | return ApkFile::class.java
28 | .getDeclaredField("zf")
29 | .apply { isAccessible = true }
30 | .get(this) as ZipFile
31 | }
32 |
33 | @SuppressLint("PrivateApi")
34 | fun parsePackageCompat(
35 | packageFile: File,
36 | packageName: String,
37 | flags: Int,
38 | useCaches: Boolean
39 | ): CustomPackageInfo {
40 | return when {
41 | Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> {
42 | try {
43 | @Suppress("DEPRECATION")
44 | val parser = PackageParser()
45 | var flagsBits = flags
46 | if (flagsBits and (MATCH_DIRECT_BOOT_UNAWARE or MATCH_DIRECT_BOOT_AWARE) != 0) {
47 | // Caller expressed an explicit opinion about what encryption
48 | // aware/unaware components they want to see, so fall through and
49 | // give them what they want
50 | } else {
51 | // Caller expressed no opinion, so match everything
52 | flagsBits = flagsBits or (MATCH_DIRECT_BOOT_AWARE or MATCH_DIRECT_BOOT_UNAWARE)
53 | }
54 | val pkg = parser.parsePackage(packageFile, 0, false)
55 | @Suppress("DEPRECATION")
56 | if (flagsBits and GET_SIGNATURES != 0) {
57 | PackageParser.collectCertificates(pkg, false /* skipVerify */)
58 | }
59 | @Suppress("DEPRECATION")
60 | val info = PackageParser.generatePackageInfo(
61 | pkg, null, flagsBits, 0, 0, null,
62 | FrameworkPackageUserState.DEFAULT
63 | )
64 | CustomPackageInfo(info)
65 | } catch (e: Exception) {
66 | CustomPackageInfo(packageName)
67 | }
68 | }
69 | Build.VERSION.SDK_INT > Build.VERSION_CODES.R && Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU -> {
70 | val parser = ParseTypeImpl.forParsingWithoutPlatformCompat()
71 |
72 | val result = Class.forName("android.content.pm.parsing.ParsingPackageUtils")
73 | .getDeclaredMethod(
74 | "parseDefault",
75 | ParseInput::class.java,
76 | File::class.java,
77 | Int::class.java,
78 | List::class.java,
79 | Boolean::class.java
80 | )
81 | .invoke(
82 | null,
83 | parser,
84 | packageFile,
85 | flags,
86 | listOf(),
87 | true
88 | ).run {
89 | this::class.java.getDeclaredMethod("getResult")
90 | .invoke(this)
91 | }
92 |
93 | CustomPackageInfo(result!!)
94 | }
95 | Build.VERSION.SDK_INT > Build.VERSION_CODES.N_MR1 -> {
96 | @Suppress("DEPRECATION")
97 | CustomPackageInfo(PackageParser().parsePackage(packageFile, flags, useCaches))
98 | }
99 | else -> {
100 | @Suppress("DEPRECATION")
101 | CustomPackageInfo(PackageParser().parsePackage(packageFile, flags))
102 | }
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/util/BatchExportListener.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.util
2 |
3 | interface BatchExportListener {
4 | fun onProgressUpdate(current: Int, max: Int)
5 | fun onSubProgressUpdate(current: Int, max: Int)
6 | fun onBaseFilenameUpdate(filename: String)
7 | fun onCurrentFilenameUpdate(filename: String)
8 |
9 | fun onExportComplete()
10 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/util/Constants.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.util
2 |
3 | val extensionsToRasterize = arrayOf(
4 | "spr",
5 | "xml",
6 | "astc"
7 | )
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/util/ContextUtils.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.util
2 |
3 | import android.app.Application
4 | import android.app.ResourcesManager
5 | import android.content.Context
6 | import android.content.pm.PackageManager
7 | import android.content.res.Resources
8 | import android.util.TypedValue
9 | import kotlinx.coroutines.Dispatchers
10 | import kotlinx.coroutines.launch
11 | import tk.zwander.sprviewer.data.AppData
12 | import tk.zwander.sprviewer.ui.App
13 | import java.io.File
14 | import java.util.concurrent.ConcurrentLinkedDeque
15 | import kotlin.math.roundToInt
16 |
17 | suspend fun Context.getInstalledApps(listener: (data: AppData, size: Int, count: Int) -> Unit): Collection {
18 | val installedApps = packageManager.getInstalledApplicationsCompat(PackageManager.GET_META_DATA)
19 | val ret = ConcurrentLinkedDeque()
20 |
21 | var count = 0
22 |
23 | installedApps.forEachParallel(Dispatchers.IO) {
24 | count++
25 |
26 | val data = AppData(
27 | it.packageName,
28 | it.loadLabel(packageManager).toString(),
29 | it.loadIcon(packageManager)
30 | )
31 |
32 | ret.add(data)
33 | launch(Dispatchers.Main) {
34 | listener(data, installedApps.size, count)
35 | }
36 | }
37 |
38 | return ret
39 | }
40 |
41 | private val loadedResources = HashMap()
42 |
43 | fun Context.getAppRes(apk: File): Resources {
44 | val resMan = ResourcesManager.getInstance()
45 | val pkgInfo = (applicationContext as Application).mLoadedApk
46 |
47 | return loadedResources[apk.absolutePath] ?: resMan
48 | .getResourcesCompat(apk.absolutePath, pkgInfo)
49 | .apply {
50 | loadedResources[apk.absolutePath] = this
51 | }
52 | }
53 |
54 | fun destroyAppRes(apk: File) {
55 | val res = loadedResources[apk.absolutePath] ?: return
56 |
57 | res.assets.close()
58 |
59 | loadedResources.remove(apk.absolutePath)
60 | }
61 |
62 | /**
63 | * Convert a certain DP value to its equivalent in px
64 | * @param dpVal the chosen DP value
65 | * @return the DP value in terms of px
66 | */
67 | fun Context.dpAsPx(dpVal: Number) =
68 | TypedValue.applyDimension(
69 | TypedValue.COMPLEX_UNIT_DIP,
70 | dpVal.toFloat(),
71 | resources.displayMetrics
72 | ).roundToInt()
73 |
74 | val Context.app: App
75 | get() = applicationContext as App
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/util/IntentUtils.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.util
2 |
3 | import android.content.Intent
4 | import android.os.Build
5 | import android.os.Parcelable
6 |
7 | inline fun Intent.getParcelableExtraCompat(name: String): T {
8 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
9 | getParcelableExtra(name, T::class.java)
10 | } else {
11 | @Suppress("DEPRECATION")
12 | getParcelableExtra(name)
13 | }
14 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/util/ListUtils.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.util
2 |
3 | import kotlinx.coroutines.*
4 | import java.util.*
5 | import kotlin.coroutines.CoroutineContext
6 | import kotlin.coroutines.EmptyCoroutineContext
7 |
8 | suspend fun Collection.forEachParallel(
9 | context: CoroutineContext = EmptyCoroutineContext,
10 | block: suspend CoroutineScope.(T) -> Unit
11 | ) = coroutineScope {
12 | val jobs = ArrayList>(size)
13 | forEach {
14 | jobs.add(
15 | async(context) {
16 | block(it)
17 | }
18 | )
19 | }
20 | jobs.awaitAll()
21 | }
22 |
23 | fun Collection.get(index: Int): T {
24 | forEachIndexed { i, t ->
25 | if (i == index) {
26 | return t
27 | }
28 | }
29 |
30 | throw IndexOutOfBoundsException()
31 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/util/PackageUtils.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.util
2 |
3 | import android.content.pm.ApplicationInfo
4 | import android.content.pm.PackageInfo
5 | import android.content.pm.PackageManager
6 | import android.os.Build
7 |
8 | // Newer Android versions say these APIs are NonNull, but older versions can return null.
9 |
10 | fun PackageManager.getInstalledApplicationsCompat(flags: Int = 0): List {
11 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
12 | getInstalledApplications(PackageManager.ApplicationInfoFlags.of(flags.toLong()))
13 | } else {
14 | @Suppress("DEPRECATION")
15 | getInstalledApplications(flags)
16 | } ?: listOf()
17 | }
18 |
19 | fun PackageManager.getApplicationInfoCompat(pkg: String, flags: Int = 0): ApplicationInfo {
20 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
21 | getApplicationInfo(pkg, PackageManager.ApplicationInfoFlags.of(flags.toLong()))
22 | } else {
23 | @Suppress("DEPRECATION")
24 | getApplicationInfo(pkg, flags)
25 | }
26 | }
27 |
28 | fun PackageManager.getPackageInfoCompat(pkg: String, flags: Int = 0): PackageInfo {
29 | return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
30 | getPackageInfo(pkg, PackageManager.PackageInfoFlags.of(flags.toLong()))
31 | } else {
32 | @Suppress("DEPRECATION")
33 | getPackageInfo(pkg, flags)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/util/ResourceUtils.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.util
2 |
3 | import android.app.LoadedApk
4 | import android.app.ResourcesManager
5 | import android.content.res.CompatibilityInfo
6 | import android.content.res.Configuration
7 | import android.content.res.Resources
8 | import android.os.Build
9 | import android.os.IBinder
10 | import android.view.Display
11 | import net.dongliu.apk.parser.struct.resource.ResourcePackage
12 | import net.dongliu.apk.parser.struct.resource.ResourceTable
13 |
14 | @Suppress("UNCHECKED_CAST")
15 | val ResourceTable.packageMap: Map
16 | get() = ResourceTable::class.java
17 | .getDeclaredField("packageMap")
18 | .apply { isAccessible = true }
19 | .get(this) as Map
20 |
21 | fun ResourcesManager.getResourcesCompat(apkPath: String, pkgInfo: LoadedApk): Resources {
22 | return when {
23 | Build.VERSION.SDK_INT < Build.VERSION_CODES.N -> {
24 | ResourcesManager::class.java
25 | .getDeclaredMethod(
26 | "getTopLevelResources",
27 | String::class.java,
28 | Array::class.java,
29 | Array::class.java,
30 | Array::class.java,
31 | Int::class.java,
32 | Configuration::class.java,
33 | CompatibilityInfo::class.java
34 | )
35 | .apply { isAccessible = true }
36 | .invoke(
37 | this,
38 | apkPath,
39 | null,
40 | null,
41 | null,
42 | Display.DEFAULT_DISPLAY,
43 | null,
44 | pkgInfo.compatibilityInfo
45 | ) as Resources
46 | }
47 | Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q -> {
48 | ResourcesManager::class.java
49 | .getDeclaredMethod(
50 | "getResources",
51 | IBinder::class.java,
52 | String::class.java,
53 | Array::class.java,
54 | Array::class.java,
55 | Array::class.java,
56 | Int::class.java,
57 | Configuration::class.java,
58 | CompatibilityInfo::class.java,
59 | ClassLoader::class.java
60 | )
61 | .apply { isAccessible = true }
62 | .invoke(
63 | this,
64 | null,
65 | apkPath,
66 | null,
67 | null,
68 | null,
69 | Display.DEFAULT_DISPLAY,
70 | null,
71 | pkgInfo.compatibilityInfo,
72 | pkgInfo.classLoader
73 | ) as Resources
74 | }
75 | Build.VERSION.SDK_INT < 31 -> {
76 | ResourcesManager::class.java
77 | .getDeclaredMethod(
78 | "getResources",
79 | IBinder::class.java,
80 | String::class.java,
81 | Array::class.java,
82 | Array::class.java,
83 | Array::class.java,
84 | Int::class.java,
85 | Configuration::class.java,
86 | CompatibilityInfo::class.java,
87 | ClassLoader::class.java,
88 | List::class.java
89 | )
90 | .apply { isAccessible = true }
91 | .invoke(
92 | this,
93 | null,
94 | apkPath,
95 | null,
96 | null,
97 | null,
98 | Display.DEFAULT_DISPLAY,
99 | null,
100 | pkgInfo.compatibilityInfo,
101 | pkgInfo.classLoader,
102 | null
103 | ) as Resources
104 | }
105 | else -> {
106 | ResourcesManager::class.java
107 | .getDeclaredMethod(
108 | "getResources",
109 | IBinder::class.java,
110 | String::class.java,
111 | Array::class.java,
112 | Array::class.java,
113 | Array::class.java,
114 | Array::class.java,
115 | java.lang.Integer::class.java,
116 | Configuration::class.java,
117 | CompatibilityInfo::class.java,
118 | ClassLoader::class.java,
119 | List::class.java
120 | )
121 | .apply { isAccessible = true }
122 | .invoke(
123 | this,
124 | null,
125 | apkPath,
126 | null,
127 | null,
128 | null,
129 | null,
130 | Display.DEFAULT_DISPLAY,
131 | null,
132 | pkgInfo.compatibilityInfo,
133 | pkgInfo.classLoader,
134 | null
135 | ) as Resources
136 | }
137 | }
138 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/util/TextWatcherAdapter.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.util
2 |
3 | import android.text.Editable
4 | import android.text.TextWatcher
5 |
6 | abstract class TextWatcherAdapter : TextWatcher {
7 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
8 |
9 | }
10 |
11 | override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
12 |
13 | }
14 |
15 | override fun afterTextChanged(s: Editable?) {
16 |
17 | }
18 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/util/Utils.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.util
2 |
3 | import android.annotation.SuppressLint
4 | import android.app.Activity
5 | import android.content.res.Resources
6 | import android.view.View
7 | import android.widget.PopupWindow
8 | import androidx.collection.ArraySet
9 | import com.skydoves.balloon.createBalloon
10 | import kotlinx.coroutines.*
11 | import net.dongliu.apk.parser.ApkFile
12 | import net.dongliu.apk.parser.struct.resource.ResourceTable
13 | import tk.zwander.sprviewer.R
14 | import tk.zwander.sprviewer.data.*
15 | import java.util.*
16 | import java.util.concurrent.ConcurrentHashMap
17 | import kotlin.collections.ArrayList
18 | import kotlin.coroutines.CoroutineContext
19 |
20 | suspend fun getAppStringXmls(
21 | apk: ApkFile,
22 | stringXmlFound: (data: StringXmlData, size: Int, count: Int) -> Unit
23 | ): Collection = coroutineScope {
24 | val table = apk.resourceTable
25 | val map = ConcurrentHashMap()
26 |
27 | var count = 0
28 | var totalSize = table.packageMap.size
29 |
30 | table.packageMap.forEach { (k, v) ->
31 | val (pkgCode, resPkg) = k.toInt() to v
32 |
33 | val stringsIndex =
34 | resPkg.typeSpecMap.filter { it.value.name == "string" }.entries.elementAtOrNull(0)
35 |
36 | val stringsStart =
37 | if (stringsIndex != null) (stringsIndex.key.toInt() shl 16) or (pkgCode shl 24) else -1
38 |
39 | val stringsSize = stringsIndex?.value?.entryFlags?.size ?: 0
40 |
41 | val resInfos = LinkedList>()
42 |
43 | val loopRange: suspend CoroutineScope.(start: Int, end: Int) -> Unit = { start: Int, end: Int ->
44 | for (i in start until end) {
45 | try {
46 | val r = table.getResourcesById(i.toLong())
47 | if (r.isEmpty()) continue
48 |
49 | resInfos.add(r)
50 | totalSize += r.size
51 | } catch (ignored: Resources.NotFoundException) {}
52 | }
53 | }
54 |
55 | if (stringsStart != -1) {
56 | loopRange(stringsStart, stringsStart + stringsSize)
57 |
58 | resInfos.forEach {
59 | it.forEach { res ->
60 | val locale = res.type.locale
61 |
62 | if (map.containsKey(locale)) {
63 | map[locale]!!.apply {
64 | values.add(StringData(
65 | res.resourceEntry.key,
66 | res.resourceEntry.toStringValue(table, locale)
67 | ))
68 | }
69 | } else {
70 | map[locale] = StringXmlData(
71 | locale
72 | ).apply {
73 | values.add(StringData(
74 | res.resourceEntry.key,
75 | res.resourceEntry.toStringValue(table, locale)
76 | ))
77 | }
78 | }
79 |
80 | count++
81 | stringXmlFound(map[locale]!!, totalSize, count)
82 | }
83 | }
84 | }
85 | }
86 |
87 | return@coroutineScope map.values
88 | }
89 |
90 | suspend fun getAppValues(
91 | apk: ApkFile,
92 | packageInfo: CustomPackageInfo,
93 | valueFound: (data: UValueData, size: Int, count: Int) -> Unit
94 | ): Collection = coroutineScope {
95 | val table = apk.resourceTable
96 | var totalSize = table.packageMap.size
97 | var count = 0
98 |
99 | val list = ArraySet()
100 |
101 | table.packageMap.forEach { (k, v) ->
102 | val (pkgCode, resPkg) = k.toInt() to v
103 |
104 | val stringsIndex =
105 | resPkg.typeSpecMap.filter { it.value.name == "string" }.entries.elementAtOrNull(0)
106 |
107 | val stringsStart =
108 | if (stringsIndex != null) (stringsIndex.key.toInt() shl 16) or (pkgCode shl 24) else -1
109 |
110 | val stringsSize = stringsIndex?.value?.entryFlags?.size ?: 0
111 |
112 | totalSize += stringsSize
113 |
114 | val loopRange: suspend CoroutineScope.(start: Int, end: Int) -> Unit = { start: Int, end: Int ->
115 | for (i in start until end) {
116 | try {
117 | val r = table.getResourcesById(i.toLong())
118 | if (r.isEmpty()) continue
119 |
120 | val data = UValueData(
121 | "string",
122 | r[0].resourceEntry.key,
123 | apk.getFile().absolutePath,
124 | i,
125 | apk,
126 | packageInfo,
127 | r[0].resourceEntry.toStringValue(table, Locale.getDefault()),
128 | mutableListOf()
129 | )
130 |
131 | r.forEach {
132 | data.values.add(
133 | LocalizedValueData(
134 | it.type.locale,
135 | it.resourceEntry.toStringValue(table, it.type.locale)
136 | )
137 | )
138 |
139 | count++
140 | list.add(data)
141 | valueFound(data, totalSize, count)
142 | }
143 | } catch (ignored: Resources.NotFoundException) {}
144 | }
145 | }
146 |
147 | if (stringsStart != -1) {
148 | loopRange(stringsStart, stringsStart + stringsSize)
149 | }
150 | }
151 |
152 | return@coroutineScope list
153 | }
154 |
155 | suspend fun getAppDrawables(
156 | apk: ApkFile,
157 | packageInfo: CustomPackageInfo,
158 | drawableFound: (data: UDrawableData, size: Int, count: Int) -> Unit
159 | ): List = coroutineScope {
160 | val table = apk.resourceTable
161 | var totalSize = table.packageMap.size
162 | var count = 0
163 |
164 | val list = ArrayList()
165 |
166 | table.packageMap.forEach { (k, v) ->
167 | val (pkgCode, resPkg) = k.toInt() to v
168 |
169 | val drawableIndex =
170 | resPkg.typeSpecMap.filter { it.value.name == "drawable" }.entries.elementAtOrNull(0)
171 | val mipmapIndex =
172 | resPkg.typeSpecMap.filter { it.value.name == "mipmap" }.entries.elementAtOrNull(0)
173 | val rawIndex =
174 | resPkg.typeSpecMap.filter { it.value.name == "raw" }.entries.elementAtOrNull(0)
175 |
176 | val drawableStart =
177 | if (drawableIndex != null) (drawableIndex.key.toInt() shl 16) or (pkgCode shl 24) else -1
178 | val mipmapStart =
179 | if (mipmapIndex != null) (mipmapIndex.key.toInt() shl 16) or (pkgCode shl 24) else -1
180 | val rawStart =
181 | if (rawIndex != null) (rawIndex.key.toInt() shl 16) or (pkgCode shl 24) else -1
182 |
183 | val drawableSize = drawableIndex?.value?.entryFlags?.size ?: 0
184 | val mipmapSize = mipmapIndex?.value?.entryFlags?.size ?: 0
185 | val rawSize = rawIndex?.value?.entryFlags?.size ?: 0
186 | totalSize += drawableSize + mipmapSize + rawSize
187 |
188 | val loopRange: suspend CoroutineScope.(start: Int, end: Int) -> Unit = { start: Int, end: Int ->
189 | for (i in start until end) {
190 | try {
191 | val r = table.getResourcesById(i.toLong())
192 | if (r.isEmpty()) continue
193 |
194 | r.forEach { resource ->
195 | val entry = resource.resourceEntry
196 |
197 | val pathOrColor = entry.toStringValue(table, Locale.getDefault())
198 |
199 | val split = pathOrColor.split("/")
200 | val fullName = split.last().split(".")
201 |
202 | val typeMask = 0x00ff0000
203 | val typeSpec = resPkg.getTypeSpec(((typeMask and i) - 0xffff).toShort())
204 | val typeName = typeSpec?.name!!
205 | val name = entry.key
206 | val ext = if (fullName.size > 1) fullName.subList(1, fullName.size)
207 | .joinToString(".") else null
208 |
209 | val data = UDrawableData(
210 | typeName,
211 | name,
212 | ext,
213 | pathOrColor,
214 | i,
215 | apk,
216 | packageInfo,
217 | resource.type.config
218 | )
219 |
220 | count++
221 | list.add(data)
222 | drawableFound(data, totalSize, count)
223 | }
224 | } catch (ignored: Resources.NotFoundException) {}
225 | }
226 | }
227 |
228 | if (drawableStart != -1) {
229 | loopRange(drawableStart, drawableStart + drawableSize)
230 | }
231 | if (mipmapStart != -1) {
232 | loopRange(mipmapStart, mipmapStart + mipmapSize)
233 | }
234 | if (rawStart != -1) {
235 | loopRange(rawStart, rawStart + rawSize)
236 | }
237 | }
238 |
239 | return@coroutineScope list
240 | }
241 |
242 | fun CoroutineScope.lazyDeferred(
243 | context: CoroutineContext = Dispatchers.Default,
244 | block: suspend CoroutineScope.() -> T
245 | ): Lazy> {
246 | return lazy {
247 | async(context = context, start = CoroutineStart.LAZY) {
248 | block(this)
249 | }
250 | }
251 | }
252 |
253 | @ExperimentalCoroutinesApi
254 | suspend fun Deferred.getOrAwaitResult() = if (isCompleted) getCompleted() else await()
255 |
256 | @SuppressLint("Range")
257 | fun Activity.showTitleSnackBar(anchor: View) {
258 | createBalloon(this) {
259 | setPadding(8)
260 | setPaddingTop(16)
261 | setCornerRadius(12f)
262 | setBackgroundDrawableResource(R.drawable.snackbar_background)
263 | setTextSize(20f)
264 | setArrowSize(0)
265 | setMargin(0)
266 | setElevation(0)
267 | setBalloonAnimationStyle(R.style.SnackbarAnimStyle)
268 |
269 | text = title.toString()
270 | autoDismissDuration = -1L
271 | widthRatio = 1.0f
272 | isVisibleArrow = false
273 | }.apply {
274 | val popupWindow = this::class.java.getDeclaredField("bodyWindow")
275 | .apply { isAccessible = true }
276 | .get(this) as PopupWindow
277 |
278 | popupWindow.overlapAnchor = true
279 |
280 | showAsDropDown(anchor, 0,
281 | window.decorView
282 | .findViewById(R.id.action_bar)
283 | .height - dpAsPx(12))
284 |
285 | setOnBalloonOutsideTouchListener { _, _ ->
286 | dismiss()
287 | }
288 | }
289 | }
290 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/views/AnimatedImageView.kt:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2008 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | //from https://github.com/aosp-mirror/platform_frameworks_base/blob/6bebb8418ceecf44d2af40033870f3aabacfe36e/packages/SystemUI/src/com/android/systemui/statusbar/AnimatedImageView.java
18 |
19 | package tk.zwander.sprviewer.views
20 |
21 | import android.content.Context
22 | import android.graphics.drawable.Animatable
23 | import android.graphics.drawable.Animatable2
24 | import android.graphics.drawable.AnimationDrawable
25 | import android.graphics.drawable.Drawable
26 | import android.util.AttributeSet
27 | import android.view.View
28 | import android.widget.RemoteViews.RemoteView
29 | import com.github.chrisbanes.photoview.PhotoView
30 |
31 | @RemoteView
32 | class AnimatedImageView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : PhotoView(context, attrs) {
33 | var anim: Animatable? = null
34 | private var attached: Boolean = false
35 | private var allowAnimation = true
36 |
37 | // Tracks the last image that was set, so that we don't refresh the image if it is exactly
38 | // the same as the previous one. If this is a resid, we track that. If it's a drawable, we
39 | // track the hashcode of the drawable.
40 | private var drawableId: Int = 0
41 |
42 | fun setAllowAnimation(allowAnimation: Boolean) {
43 | if (this.allowAnimation != allowAnimation) {
44 | this.allowAnimation = allowAnimation
45 | updateAnim()
46 | if (!this.allowAnimation && anim != null) {
47 | // Reset drawable such that we show the first frame whenever we're not animating.
48 | drawable!!.setVisible(visibility == View.VISIBLE, true /* restart */)
49 | }
50 | }
51 | }
52 |
53 | private fun updateAnim() {
54 | val drawable = drawable
55 | if (attached && anim != null) {
56 | anim!!.stop()
57 | }
58 | if (drawable is Animatable && drawable::class.java.canonicalName?.contains("SemPathRenderingDrawable") == false) {
59 | anim = drawable
60 | if (isShown && allowAnimation) {
61 | anim!!.start()
62 | }
63 |
64 | if (drawable is Animatable2) {
65 | drawable.registerAnimationCallback(object : Animatable2.AnimationCallback() {
66 | override fun onAnimationEnd(drawable: Drawable?) {
67 | post {
68 | anim!!.start()
69 | }
70 | }
71 | })
72 | }
73 |
74 | if (drawable is AnimationDrawable) {
75 | drawable.isOneShot = false
76 | }
77 | } else {
78 | anim = null
79 | }
80 | }
81 |
82 | override fun setImageDrawable(drawable: Drawable?) {
83 | drawableId = if (drawable != null) {
84 | if (drawableId == drawable.hashCode()) return
85 |
86 | drawable.hashCode()
87 | } else {
88 | 0
89 | }
90 |
91 | super.setImageDrawable(drawable)
92 | updateAnim()
93 | }
94 |
95 | override fun setImageResource(resid: Int) {
96 | if (drawableId == resid) return
97 |
98 | drawableId = resid
99 | super.setImageResource(resid)
100 | updateAnim()
101 | }
102 |
103 | public override fun onAttachedToWindow() {
104 | super.onAttachedToWindow()
105 | attached = true
106 | updateAnim()
107 | }
108 |
109 | public override fun onDetachedFromWindow() {
110 | super.onDetachedFromWindow()
111 | if (anim != null) {
112 | anim!!.stop()
113 | }
114 | attached = false
115 | }
116 |
117 | override fun onVisibilityChanged(changedView: View, vis: Int) {
118 | super.onVisibilityChanged(changedView, vis)
119 | if (anim != null) {
120 | if (isShown && allowAnimation) {
121 | anim!!.start()
122 | } else {
123 | anim!!.stop()
124 | }
125 | }
126 | }
127 | }
128 |
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/views/BaseDimensionInputDialog.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.views
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.os.Parcelable
6 | import android.view.LayoutInflater
7 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
8 | import kotlinx.parcelize.Parcelize
9 | import tk.zwander.sprviewer.R
10 | import tk.zwander.sprviewer.databinding.PixelInputBinding
11 |
12 | @SuppressLint("InflateParams")
13 | class BaseDimensionInputDialog(
14 | context: Context,
15 | okListener: (info: ExportInfo) -> Unit
16 | ) : MaterialAlertDialogBuilder(context) {
17 | init {
18 | val binding = PixelInputBinding.inflate(LayoutInflater.from(context))
19 | setView(binding.root)
20 | setTitle(R.string.base_raster_width)
21 |
22 | setPositiveButton(android.R.string.ok) { _, _ ->
23 | okListener(
24 | ExportInfo(
25 | binding.pixelText.text?.toString().run {
26 | if (this.isNullOrBlank()) 512 else this.toInt()
27 | },
28 | binding.rasterizeXmls.isChecked,
29 | binding.rasterizeAstc.isChecked,
30 | binding.rasterizeSpr.isChecked,
31 | binding.exportRasters.isChecked,
32 | binding.exportXmls.isChecked,
33 | binding.exportAstcs.isChecked,
34 | binding.exportSprs.isChecked,
35 | binding.deobNames.isChecked
36 | )
37 | )
38 | }
39 |
40 | setNegativeButton(android.R.string.cancel, null)
41 | }
42 | }
43 |
44 | @Parcelize
45 | data class ExportInfo(
46 | val dimen: Int,
47 | val rasterizeXmls: Boolean,
48 | val rasterizeAstcs: Boolean,
49 | val rasterizeSprs: Boolean,
50 | val exportRasters: Boolean,
51 | val exportXmls: Boolean,
52 | val exportAstcs: Boolean,
53 | val exportSprs: Boolean,
54 | val deobNames: Boolean
55 | ) : Parcelable
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/views/CircularProgressDialog.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.views
2 |
3 | import android.annotation.SuppressLint
4 | import android.content.Context
5 | import android.view.LayoutInflater
6 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
7 | import tk.zwander.sprviewer.databinding.DeterminateProgressBinding
8 |
9 | class CircularProgressDialog(context: Context, private var maxProgress: Int = 0) : MaterialAlertDialogBuilder(context) {
10 | private val binding = DeterminateProgressBinding.inflate(LayoutInflater.from(context))
11 | private val progress = binding.progress
12 |
13 | var onCancelListener: (() -> Unit)? = null
14 |
15 | private var currentPercent = 0
16 | private var currentSubPercent = 0
17 |
18 | init {
19 | setView(binding.root)
20 |
21 | setNegativeButton(android.R.string.cancel) { _, _ ->
22 | onCancelListener?.invoke()
23 | }
24 | }
25 |
26 | fun setBaseFileName(name: String?) {
27 | binding.baseName.text = name
28 | }
29 |
30 | fun setCurrentFileName(name: String?) {
31 | binding.filename.text = name
32 | }
33 |
34 | @SuppressLint("SetTextI18n")
35 | fun updateProgress(progress: Int, maxProgress: Int = this.maxProgress) {
36 | val new = (progress.toFloat() / maxProgress.toFloat() * 100f).toInt()
37 |
38 | if (new != currentPercent) {
39 | currentPercent = new
40 | this.progress.progress = currentPercent
41 | }
42 |
43 | binding.fileFraction.text = "$progress/$maxProgress"
44 | }
45 |
46 | fun updateSubProgress(current: Int, max: Int = 100) {
47 | val new = (current.toFloat() / max.toFloat() * 100f).toInt()
48 |
49 | if (new != currentSubPercent) {
50 | currentSubPercent = new
51 | binding.subProgress.progress = currentSubPercent
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/views/DimensionInputDialog.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.views
2 |
3 | import android.content.Context
4 | import android.graphics.Color
5 | import android.graphics.drawable.Drawable
6 | import android.graphics.drawable.GradientDrawable
7 | import android.view.LayoutInflater
8 | import androidx.core.content.ContextCompat
9 | import androidx.fragment.app.FragmentActivity
10 | import com.google.android.material.dialog.MaterialAlertDialogBuilder
11 | import com.jaredrummler.android.colorpicker.ColorPickerDialog
12 | import com.jaredrummler.android.colorpicker.ColorPickerDialogListener
13 | import tk.zwander.sprviewer.R
14 | import tk.zwander.sprviewer.databinding.DimensionInputBinding
15 | import tk.zwander.sprviewer.util.TextWatcherAdapter
16 |
17 | class DimensionInputDialog(context: Context, drawable: Drawable) : MaterialAlertDialogBuilder(context) {
18 | private val defDimen = 1024
19 |
20 | private val intrinsicWidth = drawable.intrinsicWidth
21 | private val intrinsicHeight = drawable.intrinsicHeight
22 |
23 | private val hwRatio = intrinsicHeight.toFloat() / intrinsicWidth.toFloat()
24 | private val whRatio = intrinsicWidth.toFloat() / intrinsicHeight.toFloat()
25 |
26 | private val binding = DimensionInputBinding.inflate(LayoutInflater.from(context))
27 |
28 | private val tintDrawable = ContextCompat.getDrawable(context, R.drawable.outlined_circle)!!.mutate() as GradientDrawable
29 | private var tintColor = Color.TRANSPARENT
30 |
31 | private val widthListener: TextWatcherAdapter = object : TextWatcherAdapter() {
32 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
33 | binding.heightInput.apply {
34 | removeTextChangedListener(heightListener)
35 | setText(getScaledDimen(s, hwRatio, defDimen).toString())
36 | addTextChangedListener(heightListener)
37 | }
38 | }
39 | }
40 |
41 | private val heightListener: TextWatcherAdapter = object : TextWatcherAdapter() {
42 | override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
43 | binding.widthInput.apply {
44 | removeTextChangedListener(widthListener)
45 | setText(getScaledDimen(s, whRatio, defDimen).toString())
46 | addTextChangedListener(widthListener)
47 | }
48 | }
49 | }
50 |
51 | var saveListener: ((width: Int, height: Int, tint: Int) -> Unit)? = null
52 |
53 | init {
54 | binding.widthInput.addTextChangedListener(widthListener)
55 | binding.heightInput.addTextChangedListener(heightListener)
56 |
57 | binding.drawableTint
58 | .setCompoundDrawablesRelativeWithIntrinsicBounds(
59 | tintDrawable,
60 | null,
61 | null,
62 | null
63 | )
64 |
65 | binding.drawableTintCard.setOnClickListener {
66 | ColorPickerDialog.newBuilder()
67 | .setColor(tintColor)
68 | .setShowAlphaSlider(true)
69 | .setAllowCustom(true)
70 | .create().apply {
71 | setColorPickerDialogListener(object : ColorPickerDialogListener {
72 | override fun onColorSelected(dialogId: Int, color: Int) {
73 | tintColor = color
74 | tintDrawable.setColor(color)
75 | }
76 |
77 | override fun onDialogDismissed(dialogId: Int) {}
78 | })
79 |
80 | val activity = context as FragmentActivity
81 | show(activity.supportFragmentManager, "color-picker-dialog")
82 | }
83 | }
84 |
85 | setTitle(R.string.enter_dimensions)
86 | setView(binding.root)
87 |
88 | setPositiveButton(android.R.string.ok) { _, _ ->
89 | val widthInput = binding.widthInput.text?.toString()
90 | val heightInput = binding.heightInput.text?.toString()
91 |
92 | val (width, height) = if (widthInput.isNullOrBlank() && heightInput.isNullOrBlank()) {
93 | (if (intrinsicWidth > intrinsicHeight) defDimen else (defDimen * whRatio).toInt()) to
94 | (if (intrinsicWidth > intrinsicHeight) (defDimen * hwRatio).toInt() else defDimen)
95 | } else if (widthInput.isNullOrBlank()) {
96 | getScaledDimen(heightInput, whRatio, defDimen) to parseDimen(heightInput, defDimen)
97 | } else {
98 | parseDimen(widthInput, defDimen) to getScaledDimen(widthInput, hwRatio, defDimen)
99 | }
100 |
101 | saveListener?.invoke(width, height, tintColor)
102 | }
103 | setNegativeButton(android.R.string.cancel, null)
104 | }
105 |
106 | private fun getScaledDimen(input: CharSequence?, ratio: Float, @Suppress("SameParameterValue") def: Int): Int {
107 | return (parseDimen(input, def) * ratio).toInt()
108 | }
109 |
110 | private fun parseDimen(input: CharSequence?, def: Int): Int {
111 | if (input.isNullOrBlank()) return def
112 |
113 | return input.toString().toInt()
114 | }
115 | }
--------------------------------------------------------------------------------
/app/src/main/java/tk/zwander/sprviewer/views/ExtensionIndicator.kt:
--------------------------------------------------------------------------------
1 | package tk.zwander.sprviewer.views
2 |
3 | import android.content.Context
4 | import android.util.AttributeSet
5 | import android.util.TypedValue
6 | import android.view.Gravity
7 | import android.widget.LinearLayout
8 | import android.widget.TextView
9 | import androidx.appcompat.widget.AppCompatTextView
10 | import androidx.core.content.ContextCompat
11 | import androidx.core.widget.TextViewCompat
12 | import tk.zwander.sprviewer.R
13 |
14 | class ExtensionIndicator(context: Context, attrs: AttributeSet) : LinearLayout(context, attrs) {
15 | val text: TextView
16 | get() = findViewById(R.id.text)
17 |
18 | init {
19 | background = ContextCompat.getDrawable(context, R.drawable.solid_circle)
20 | gravity = Gravity.CENTER
21 | }
22 |
23 | fun setText(text: CharSequence?) {
24 | this.text.text = text
25 |
26 | if (!isInEditMode) {
27 | val backgroundColor = when (text) {
28 | "png" -> {
29 | R.color.png
30 | }
31 | "jpg" -> {
32 | R.color.jpg
33 | }
34 | "xml" -> {
35 | R.color.xml
36 | }
37 | "spr" -> {
38 | R.color.spr
39 | }
40 | "astc" -> {
41 | R.color.astc
42 | }
43 | "webp" -> {
44 | R.color.webp
45 | }
46 | "9.png" -> {
47 | R.color.ninepng
48 | }
49 | else -> {
50 | R.color.other
51 | }
52 | }
53 |
54 | background?.setTint(resources.getColor(backgroundColor, context.theme))
55 | }
56 | }
57 |
58 | class ExtensionIndicatorText(context: Context, attrs: AttributeSet) : AppCompatTextView(context, attrs) {
59 | init {
60 | setTextColor(resources.getColor(android.R.color.white, context.theme))
61 | gravity = Gravity.CENTER
62 | isSingleLine = true
63 | TextViewCompat.setAutoSizeTextTypeUniformWithConfiguration(
64 | this,
65 | 5,
66 | 18,
67 | 1,
68 | TypedValue.COMPLEX_UNIT_SP
69 | )
70 | isAllCaps = true
71 | }
72 | }
73 | }
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_in_down.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
10 |
18 |
--------------------------------------------------------------------------------
/app/src/main/res/anim/slide_out_up.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
9 |
17 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/action_bar_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/corner_left.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/dialog_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
12 |
13 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_add_black_24dp.xml:
--------------------------------------------------------------------------------
1 |
6 |
9 |
10 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_folder_open_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_image_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/ic_baseline_list_24.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/outlined_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
13 |
14 |
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/snackbar_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | -
6 |
7 |
10 |
11 |
15 |
16 |
17 |
18 | -
23 |
27 |
28 |
29 | -
33 |
37 |
38 |
--------------------------------------------------------------------------------
/app/src/main/res/drawable/solid_circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
8 |
9 |
13 |
14 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_drawable_view.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
18 |
19 |
28 |
29 |
35 |
36 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 |
86 |
87 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
17 |
18 |
27 |
28 |
34 |
35 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/app_info_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
14 |
15 |
20 |
21 |
24 |
25 |
33 |
34 |
40 |
41 |
42 |
43 |
54 |
55 |
63 |
64 |
70 |
71 |
72 |
73 |
74 |
75 |
83 |
84 |
90 |
91 |
100 |
101 |
102 |
103 |
109 |
110 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/batch_export_notification_content_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
18 |
19 |
26 |
27 |
35 |
36 |
43 |
44 |
51 |
52 |
59 |
60 |
68 |
69 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/determinate_progress.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
23 |
24 |
31 |
32 |
39 |
40 |
47 |
48 |
58 |
59 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/dimension_input.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
15 |
16 |
21 |
22 |
28 |
29 |
30 |
31 |
38 |
39 |
44 |
45 |
51 |
52 |
53 |
54 |
55 |
56 |
64 |
65 |
76 |
77 |
78 |
79 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/drawable_info_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
19 |
20 |
23 |
24 |
28 |
29 |
35 |
36 |
37 |
38 |
43 |
44 |
45 |
46 |
52 |
53 |
60 |
61 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/pixel_input.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
13 |
14 |
17 |
18 |
26 |
27 |
28 |
29 |
36 |
37 |
44 |
45 |
52 |
53 |
60 |
61 |
68 |
69 |
76 |
77 |
84 |
85 |
92 |
93 |
94 |
95 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/progress.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/layout/string_list_info_layout.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
19 |
20 |
23 |
24 |
28 |
29 |
35 |
36 |
37 |
38 |
43 |
44 |
45 |
46 |
52 |
53 |
60 |
61 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/batch.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/import_apk.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/save.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/menu/search.xml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-hdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-hdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-mdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-mdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
--------------------------------------------------------------------------------
/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
--------------------------------------------------------------------------------
/app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #D32F2F
4 | #673AB7
5 | #009688
6 | #FBC02D
7 | #4CAF50
8 | #C2185B
9 | #8BC34A
10 | #444444
11 |
12 | #6a1b9a
13 | #9c4dcc
14 | #38006b
15 | #3f51b5
16 | #757de8
17 | #002984
18 |
19 |
--------------------------------------------------------------------------------
/app/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 24dp
4 |
--------------------------------------------------------------------------------
/app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | SPRViewer
3 | Image
4 | XML
5 | Orig
6 | Save All
7 | Save All Complete
8 | Import APK
9 | View Images
10 | View Strings
11 |
12 | Width
13 | Height
14 | Open
15 | Tint
16 | Copied
17 | Exporting
18 |
19 | Base Raster Width
20 | Rasterize XMLs
21 | Export Rasters
22 | Export XMLs
23 | Rasterize ASTCs
24 | Rasterize SPRs
25 | Export ASTCs
26 | Export SPRs
27 | Deobfuscate Names
28 |
29 | Unable to load image
30 | Enter export dimensions
31 | Unable to launch file explorer
32 | Batch export complete!
33 | Choose Output Directory
34 | What do you want to do with this APK?
35 |
36 | Exporting: %1$d/%2$d
37 | Drawable: %1$d/%2$d
38 | Batch Export Progress
39 |
40 |
--------------------------------------------------------------------------------
/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
20 |
21 |
24 |
25 |
33 |
34 |
44 |
45 |
49 |
50 |
51 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | alias(libs.plugins.android.application) apply false
3 | alias(libs.plugins.bugsnag) apply false
4 | alias(libs.plugins.kotlin.android) apply false
5 | alias(libs.plugins.kotlin.parcelize) apply false
6 | }
7 |
8 | tasks.register("clean") {
9 | delete(rootProject.layout.buildDirectory)
10 | }
11 |
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx1536m
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app's APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 | android.nonTransitiveRClass=false
23 | android.nonFinalResIds=false
24 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | androidGradlePlugin = "8.7.0"
3 | apkParser = "7cd66d83c9"
4 | appcompat = "1.7.0"
5 | balloon = "1.6.7"
6 | bugsnagAndroid = "6.8.0"
7 | bugsnagGradlePlugin = "0.1.0"
8 | colorpicker = "1.1.0"
9 | constraintlayout = "2.1.4"
10 | coreKtx = "1.13.1"
11 | coroutines = "1.9.0"
12 | documentfile = "1.0.1"
13 | hiddenapibypass = "4.3"
14 | indicatorfastscroll = "1.4.0"
15 | kotlin = "2.0.20"
16 | numberprogressbar = "1.4"
17 | lifecycleViewmodelKtx = "2.8.6"
18 | material = "1.12.0"
19 | photoview = "2.3.0"
20 | picasso = "2.8"
21 | pngj = "2.1.2.1"
22 | progresscircula = "v0.2.1"
23 | recyclerview = "1.3.2"
24 |
25 | [libraries]
26 | apk-parser = { module = "com.github.zacharee:apk-parser", version.ref = "apkParser" }
27 | appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
28 | balloon = { module = "com.github.skydoves:balloon", version.ref = "balloon" }
29 | bugsnag-android = { module = "com.bugsnag:bugsnag-android", version.ref = "bugsnagAndroid" }
30 | colorpicker = { module = "com.jaredrummler:colorpicker", version.ref = "colorpicker" }
31 | constraintlayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintlayout" }
32 | core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
33 | documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" }
34 | hiddenapibypass = { module = "org.lsposed.hiddenapibypass:hiddenapibypass", version.ref = "hiddenapibypass" }
35 | indicatorfastscroll = { module = "com.github.reddit:IndicatorFastScroll", version.ref = "indicatorfastscroll" }
36 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" }
37 | kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
38 | kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
39 | numberprogressbar = { module = "com.daimajia.numberprogressbar:library", version.ref = "numberprogressbar" }
40 | lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }
41 | material = { module = "com.google.android.material:material", version.ref = "material" }
42 | photoview = { module = "com.github.chrisbanes:PhotoView", version.ref = "photoview" }
43 | picasso = { module = "com.squareup.picasso:picasso", version.ref = "picasso" }
44 | pngj = { module = "com.alexdupre:pngj", version.ref = "pngj" }
45 | progresscircula = { module = "com.github.2hamed:ProgressCircula", version.ref = "progresscircula" }
46 | recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
47 |
48 | [plugins]
49 | android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
50 | bugsnag = { id = "com.bugsnag.gradle", version.ref = "bugsnagGradlePlugin" }
51 | kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
52 | kotlin-parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" }
53 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zacharee/SPRViewer/94eac6c8da5a6c9c6666f4728f5ac99cc90b21b1/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
90 | ' "$PWD" ) || exit
91 |
92 | # Use the maximum available, or set MAX_FD != -1 to use that value.
93 | MAX_FD=maximum
94 |
95 | warn () {
96 | echo "$*"
97 | } >&2
98 |
99 | die () {
100 | echo
101 | echo "$*"
102 | echo
103 | exit 1
104 | } >&2
105 |
106 | # OS specific support (must be 'true' or 'false').
107 | cygwin=false
108 | msys=false
109 | darwin=false
110 | nonstop=false
111 | case "$( uname )" in #(
112 | CYGWIN* ) cygwin=true ;; #(
113 | Darwin* ) darwin=true ;; #(
114 | MSYS* | MINGW* ) msys=true ;; #(
115 | NONSTOP* ) nonstop=true ;;
116 | esac
117 |
118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
119 |
120 |
121 | # Determine the Java command to use to start the JVM.
122 | if [ -n "$JAVA_HOME" ] ; then
123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
124 | # IBM's JDK on AIX uses strange locations for the executables
125 | JAVACMD=$JAVA_HOME/jre/sh/java
126 | else
127 | JAVACMD=$JAVA_HOME/bin/java
128 | fi
129 | if [ ! -x "$JAVACMD" ] ; then
130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
131 |
132 | Please set the JAVA_HOME variable in your environment to match the
133 | location of your Java installation."
134 | fi
135 | else
136 | JAVACMD=java
137 | if ! command -v java >/dev/null 2>&1
138 | then
139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
140 |
141 | Please set the JAVA_HOME variable in your environment to match the
142 | location of your Java installation."
143 | fi
144 | fi
145 |
146 | # Increase the maximum file descriptors if we can.
147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
148 | case $MAX_FD in #(
149 | max*)
150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
151 | # shellcheck disable=SC2039,SC3045
152 | MAX_FD=$( ulimit -H -n ) ||
153 | warn "Could not query maximum file descriptor limit"
154 | esac
155 | case $MAX_FD in #(
156 | '' | soft) :;; #(
157 | *)
158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
159 | # shellcheck disable=SC2039,SC3045
160 | ulimit -n "$MAX_FD" ||
161 | warn "Could not set maximum file descriptor limit to $MAX_FD"
162 | esac
163 | fi
164 |
165 | # Collect all arguments for the java command, stacking in reverse order:
166 | # * args from the command line
167 | # * the main class name
168 | # * -classpath
169 | # * -D...appname settings
170 | # * --module-path (only if needed)
171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
172 |
173 | # For Cygwin or MSYS, switch paths to Windows format before running java
174 | if "$cygwin" || "$msys" ; then
175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
177 |
178 | JAVACMD=$( cygpath --unix "$JAVACMD" )
179 |
180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
181 | for arg do
182 | if
183 | case $arg in #(
184 | -*) false ;; # don't mess with options #(
185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
186 | [ -e "$t" ] ;; #(
187 | *) false ;;
188 | esac
189 | then
190 | arg=$( cygpath --path --ignore --mixed "$arg" )
191 | fi
192 | # Roll the args list around exactly as many times as the number of
193 | # args, so each arg winds up back in the position where it started, but
194 | # possibly modified.
195 | #
196 | # NB: a `for` loop captures its iteration list before it begins, so
197 | # changing the positional parameters here affects neither the number of
198 | # iterations, nor the values presented in `arg`.
199 | shift # remove old arg
200 | set -- "$@" "$arg" # push replacement arg
201 | done
202 | fi
203 |
204 |
205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
207 |
208 | # Collect all arguments for the java command:
209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
210 | # and any embedded shellness will be escaped.
211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
212 | # treated as '${Hostname}' itself on the command line.
213 |
214 | set -- \
215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
216 | -classpath "$CLASSPATH" \
217 | org.gradle.wrapper.GradleWrapperMain \
218 | "$@"
219 |
220 | # Stop when "xargs" is not available.
221 | if ! command -v xargs >/dev/null 2>&1
222 | then
223 | die "xargs is not available"
224 | fi
225 |
226 | # Use "xargs" to parse quoted args.
227 | #
228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
229 | #
230 | # In Bash we could simply go:
231 | #
232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
233 | # set -- "${ARGS[@]}" "$@"
234 | #
235 | # but POSIX shell has neither arrays nor command substitution, so instead we
236 | # post-process each arg (as a line of input to sed) to backslash-escape any
237 | # character that might be a shell metacharacter, then use eval to reverse
238 | # that process (while maintaining the separation between arguments), and wrap
239 | # the whole thing up as a single "set" statement.
240 | #
241 | # This will of course break if any of these variables contains a newline or
242 | # an unmatched quote.
243 | #
244 |
245 | eval "set -- $(
246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
247 | xargs -n1 |
248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
249 | tr '\n' ' '
250 | )" '"$@"'
251 |
252 | exec "$JAVACMD" "$@"
253 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 | @rem SPDX-License-Identifier: Apache-2.0
17 | @rem
18 |
19 | @if "%DEBUG%"=="" @echo off
20 | @rem ##########################################################################
21 | @rem
22 | @rem Gradle startup script for Windows
23 | @rem
24 | @rem ##########################################################################
25 |
26 | @rem Set local scope for the variables with windows NT shell
27 | if "%OS%"=="Windows_NT" setlocal
28 |
29 | set DIRNAME=%~dp0
30 | if "%DIRNAME%"=="" set DIRNAME=.
31 | @rem This is normally unused
32 | set APP_BASE_NAME=%~n0
33 | set APP_HOME=%DIRNAME%
34 |
35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
37 |
38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
40 |
41 | @rem Find java.exe
42 | if defined JAVA_HOME goto findJavaFromJavaHome
43 |
44 | set JAVA_EXE=java.exe
45 | %JAVA_EXE% -version >NUL 2>&1
46 | if %ERRORLEVEL% equ 0 goto execute
47 |
48 | echo. 1>&2
49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
50 | echo. 1>&2
51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
52 | echo location of your Java installation. 1>&2
53 |
54 | goto fail
55 |
56 | :findJavaFromJavaHome
57 | set JAVA_HOME=%JAVA_HOME:"=%
58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
59 |
60 | if exist "%JAVA_EXE%" goto execute
61 |
62 | echo. 1>&2
63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
64 | echo. 1>&2
65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2
66 | echo location of your Java installation. 1>&2
67 |
68 | goto fail
69 |
70 | :execute
71 | @rem Setup the command line
72 |
73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
78 |
79 | :end
80 | @rem End local scope for the variables with windows NT shell
81 | if %ERRORLEVEL% equ 0 goto mainEnd
82 |
83 | :fail
84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
85 | rem the _cmd.exe /c_ return code!
86 | set EXIT_CODE=%ERRORLEVEL%
87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1
88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
89 | exit /b %EXIT_CODE%
90 |
91 | :mainEnd
92 | if "%OS%"=="Windows_NT" endlocal
93 |
94 | :omega
95 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | pluginManagement {
4 | repositories {
5 | gradlePluginPortal()
6 | google()
7 | mavenCentral()
8 | }
9 | }
10 | dependencyResolutionManagement {
11 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
12 | repositories {
13 | google()
14 | mavenCentral()
15 | maven("https://jitpack.io")
16 | }
17 | }
18 |
19 | include(":app")
20 | rootProject.name = "SPRViewer"
21 |
--------------------------------------------------------------------------------