Initial commit
This commit is contained in:
commit
d0bd6ee352
|
@ -0,0 +1,15 @@
|
|||
*.iml
|
||||
.gradle
|
||||
/local.properties
|
||||
/.idea/caches
|
||||
/.idea/libraries
|
||||
/.idea/modules.xml
|
||||
/.idea/workspace.xml
|
||||
/.idea/navEditor.xml
|
||||
/.idea/assetWizardSettings.xml
|
||||
.DS_Store
|
||||
/build
|
||||
/captures
|
||||
/app/release
|
||||
.externalNativeBuild
|
||||
.cxx
|
|
@ -0,0 +1 @@
|
|||
SMAPI Installer
|
|
@ -0,0 +1,116 @@
|
|||
<component name="ProjectCodeStyleConfiguration">
|
||||
<code_scheme name="Project" version="173">
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
<arrangement>
|
||||
<rules>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:android</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>xmlns:.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:id</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*:name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>name</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>style</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||
</rule>
|
||||
</section>
|
||||
<section>
|
||||
<rule>
|
||||
<match>
|
||||
<AND>
|
||||
<NAME>.*</NAME>
|
||||
<XML_ATTRIBUTE />
|
||||
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||
</AND>
|
||||
</match>
|
||||
<order>BY_NAME</order>
|
||||
</rule>
|
||||
</section>
|
||||
</rules>
|
||||
</arrangement>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||
<component name="GradleSettings">
|
||||
<option name="linkedExternalProjectsSettings">
|
||||
<GradleProjectSettings>
|
||||
<option name="testRunner" value="PLATFORM" />
|
||||
<option name="distributionType" value="DEFAULT_WRAPPED" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="modules">
|
||||
<set>
|
||||
<option value="$PROJECT_DIR$" />
|
||||
<option value="$PROJECT_DIR$/app" />
|
||||
</set>
|
||||
</option>
|
||||
<option name="resolveModulePerSourceSet" value="false" />
|
||||
</GradleProjectSettings>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectPlainTextFileTypeManager">
|
||||
<file url="file://$PROJECT_DIR$/app/src/main/assets/AndroidManifest.xml" />
|
||||
<file url="file://$PROJECT_DIR$/app/src/main/assets/apk/AndroidManifest.xml" />
|
||||
<file url="file://$PROJECT_DIR$/app/src/main/assets/apk/AndroidManifest.xml" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_1_8" project-jdk-name="1.8" project-jdk-type="JavaSDK">
|
||||
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||
</component>
|
||||
<component name="ProjectType">
|
||||
<option name="id" value="Android" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="RunConfigurationProducerService">
|
||||
<option name="ignoredProducers">
|
||||
<set>
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
|
||||
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
|
||||
</set>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
|
@ -0,0 +1 @@
|
|||
/build
|
|
@ -0,0 +1,62 @@
|
|||
apply plugin: 'com.android.application'
|
||||
apply plugin: "androidx.navigation.safeargs"
|
||||
|
||||
android {
|
||||
compileSdkVersion 28
|
||||
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.zane.smapiinstaller"
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
versionCode 1
|
||||
versionName "1.0.1"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
multiDexEnabled true
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
release {
|
||||
minifyEnabled true
|
||||
shrinkResources true
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility JavaVersion.VERSION_1_8
|
||||
targetCompatibility JavaVersion.VERSION_1_8
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.1.0'
|
||||
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||
implementation 'com.google.android.material:material:1.1.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
|
||||
implementation 'androidx.navigation:navigation-fragment:2.3.0-alpha02'
|
||||
implementation 'androidx.navigation:navigation-ui:2.3.0-alpha02'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||
implementation 'com.madgag.spongycastle:core:1.54.0.0'
|
||||
implementation 'com.madgag.spongycastle:prov:1.54.0.0'
|
||||
implementation 'com.madgag.spongycastle:pkix:1.54.0.0'
|
||||
implementation 'com.madgag.spongycastle:pg:1.54.0.0'
|
||||
implementation 'com.afollestad.material-dialogs:core:0.9.6.0'
|
||||
// https://mvnrepository.com/artifact/com.jakewharton/butterknife
|
||||
implementation 'com.jakewharton:butterknife:10.2.1'
|
||||
annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.1'
|
||||
// https://mvnrepository.com/artifact/com.google.guava/guava
|
||||
implementation group: 'com.google.guava', name: 'guava', version: '28.2-android'
|
||||
// https://mvnrepository.com/artifact/org.zeroturnaround/zt-zip
|
||||
implementation group: 'org.zeroturnaround', name: 'zt-zip', version: '1.14'
|
||||
// https://mvnrepository.com/artifact/com.google.code.gson/gson
|
||||
implementation group: 'com.google.code.gson', name: 'gson', version: '2.8.6'
|
||||
|
||||
testImplementation 'junit:junit:4.12'
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
|
||||
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
|
||||
implementation 'com.android.support:multidex:1.0.3'
|
||||
}
|
|
@ -0,0 +1,156 @@
|
|||
# Add project specific ProGuard rules here.
|
||||
# You can control the set of applied configuration files using the
|
||||
# proguardFiles setting in build.gradle.
|
||||
#
|
||||
# For more details, see
|
||||
# http://developer.android.com/guide/developing/tools/proguard.html
|
||||
|
||||
# If your project uses WebView with JS, uncomment the following
|
||||
# and specify the fully qualified class name to the JavaScript interface
|
||||
# class:
|
||||
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
|
||||
# public *;
|
||||
#}
|
||||
|
||||
# Uncomment this to preserve the line number information for
|
||||
# debugging stack traces.
|
||||
#-keepattributes SourceFile,LineNumberTable
|
||||
|
||||
# If you keep the line number information, uncomment this to
|
||||
# hide the original source file name.
|
||||
#-renamesourcefileattribute SourceFile
|
||||
-keepnames class com.path.to.your.ParcelableArg
|
||||
-keepnames class com.path.to.your.SerializableArg
|
||||
-keepnames class com.path.to.your.EnumArg
|
||||
|
||||
-dontwarn javax.lang.model.element.Modifier
|
||||
|
||||
# Note: We intentionally don't add the flags we'd need to make Enums work.
|
||||
# That's because the Proguard configuration required to make it work on
|
||||
# optimized code would preclude lots of optimization, like converting enums
|
||||
# into ints.
|
||||
|
||||
# Throwables uses internal APIs for lazy stack trace resolution
|
||||
-dontnote sun.misc.SharedSecrets
|
||||
-keep class sun.misc.SharedSecrets {
|
||||
*** getJavaLangAccess(...);
|
||||
}
|
||||
-dontnote sun.misc.JavaLangAccess
|
||||
-keep class sun.misc.JavaLangAccess {
|
||||
*** getStackTraceElement(...);
|
||||
*** getStackTraceDepth(...);
|
||||
}
|
||||
|
||||
# FinalizableReferenceQueue calls this reflectively
|
||||
# Proguard is intelligent enough to spot the use of reflection onto this, so we
|
||||
# only need to keep the names, and allow it to be stripped out if
|
||||
# FinalizableReferenceQueue is unused.
|
||||
-keepnames class com.google.common.base.internal.Finalizer {
|
||||
*** startFinalizer(...);
|
||||
}
|
||||
# However, it cannot "spot" that this method needs to be kept IF the class is.
|
||||
-keepclassmembers class com.google.common.base.internal.Finalizer {
|
||||
*** startFinalizer(...);
|
||||
}
|
||||
-keepnames class com.google.common.base.FinalizableReference {
|
||||
void finalizeReferent();
|
||||
}
|
||||
-keepclassmembers class com.google.common.base.FinalizableReference {
|
||||
void finalizeReferent();
|
||||
}
|
||||
|
||||
# Striped64, LittleEndianByteArray, UnsignedBytes, AbstractFuture
|
||||
-dontwarn sun.misc.Unsafe
|
||||
|
||||
# Striped64 appears to make some assumptions about object layout that
|
||||
# really might not be safe. This should be investigated.
|
||||
-keepclassmembers class com.google.common.cache.Striped64 {
|
||||
*** base;
|
||||
*** busy;
|
||||
}
|
||||
-keepclassmembers class com.google.common.cache.Striped64$Cell {
|
||||
<fields>;
|
||||
}
|
||||
|
||||
-dontwarn java.lang.SafeVarargs
|
||||
|
||||
-keep class java.lang.Throwable {
|
||||
*** addSuppressed(...);
|
||||
}
|
||||
|
||||
# Futures.getChecked, in both of its variants, is incompatible with proguard.
|
||||
|
||||
# Used by AtomicReferenceFieldUpdater and sun.misc.Unsafe
|
||||
-keepclassmembers class com.google.common.util.concurrent.AbstractFuture** {
|
||||
*** waiters;
|
||||
*** value;
|
||||
*** listeners;
|
||||
*** thread;
|
||||
*** next;
|
||||
}
|
||||
-keepclassmembers class com.google.common.util.concurrent.AtomicDouble {
|
||||
*** value;
|
||||
}
|
||||
-keepclassmembers class com.google.common.util.concurrent.AggregateFutureState {
|
||||
*** remaining;
|
||||
*** seenExceptions;
|
||||
}
|
||||
|
||||
# Since Unsafe is using the field offsets of these inner classes, we don't want
|
||||
# to have class merging or similar tricks applied to these classes and their
|
||||
# fields. It's safe to allow obfuscation, since the by-name references are
|
||||
# already preserved in the -keep statement above.
|
||||
-keep,allowshrinking,allowobfuscation class com.google.common.util.concurrent.AbstractFuture** {
|
||||
<fields>;
|
||||
}
|
||||
|
||||
# Futures.getChecked (which often won't work with Proguard anyway) uses this. It
|
||||
# has a fallback, but again, don't use Futures.getChecked on Android regardless.
|
||||
-dontwarn java.lang.ClassValue
|
||||
|
||||
# MoreExecutors references AppEngine
|
||||
-dontnote com.google.appengine.api.ThreadManager
|
||||
-keep class com.google.appengine.api.ThreadManager {
|
||||
static *** currentRequestThreadFactory(...);
|
||||
}
|
||||
-dontnote com.google.apphosting.api.ApiProxy
|
||||
-keep class com.google.apphosting.api.ApiProxy {
|
||||
static *** getCurrentEnvironment (...);
|
||||
}
|
||||
|
||||
##---------------Begin: proguard configuration for Gson ----------
|
||||
# Gson uses generic type information stored in a class file when working with fields. Proguard
|
||||
# removes such information by default, so configure it to keep all of it.
|
||||
-keepattributes Signature
|
||||
|
||||
# For using GSON @Expose annotation
|
||||
-keepattributes *Annotation*
|
||||
|
||||
# Gson specific classes
|
||||
-dontwarn sun.misc.**
|
||||
#-keep class com.google.gson.stream.** { *; }
|
||||
|
||||
# Application classes that will be serialized/deserialized over Gson
|
||||
-keep class com.google.gson.examples.android.model.** { <fields>; }
|
||||
|
||||
# Prevent proguard from stripping interface information from TypeAdapter, TypeAdapterFactory,
|
||||
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
|
||||
-keep class * implements com.google.gson.TypeAdapter
|
||||
-keep class * implements com.google.gson.TypeAdapterFactory
|
||||
-keep class * implements com.google.gson.JsonSerializer
|
||||
-keep class * implements com.google.gson.JsonDeserializer
|
||||
|
||||
# Prevent R8 from leaving Data object members always null
|
||||
-keepclassmembers,allowobfuscation class * {
|
||||
@com.google.gson.annotations.SerializedName <fields>;
|
||||
}
|
||||
|
||||
##---------------End: proguard configuration for Gson ----------
|
||||
|
||||
-keep class com.zane.** { *; }
|
||||
-keep class pxb.android.** { *; }
|
||||
-keep class net.fornwall.apksigner.** { *; }
|
||||
-keep class org.spongycastle.**
|
||||
-dontwarn org.spongycastle.jce.provider.X509LDAPCertStoreSpi
|
||||
-dontwarn org.spongycastle.x509.util.LDAPStoreHelper
|
||||
-keep class org.slf4j.**
|
|
@ -0,0 +1,26 @@
|
|||
package com.zane.smapiinstaller;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.test.platform.app.InstrumentationRegistry;
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4;
|
||||
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
|
||||
*/
|
||||
@RunWith(AndroidJUnit4.class)
|
||||
public class ExampleInstrumentedTest {
|
||||
@Test
|
||||
public void useAppContext() {
|
||||
// Context of the app under test.
|
||||
Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
|
||||
|
||||
assertEquals("com.zane.smapiinstaller", appContext.getPackageName());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.zane.smapiinstaller">
|
||||
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES"/>
|
||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
|
||||
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/AppTheme">
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="${applicationId}.provider"
|
||||
android:exported="false"
|
||||
android:grantUriPermissions="true">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/provider_paths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 5.9 KiB |
|
@ -0,0 +1,62 @@
|
|||
[
|
||||
{
|
||||
"targetPath": "classes.dex",
|
||||
"assetPath": "apk/classes.dex",
|
||||
"compression": 8
|
||||
},
|
||||
{
|
||||
"targetPath": "res/mipmap-mdpi-v4/ic_launcher_foreground.png",
|
||||
"assetPath": "apk/ic_launcher_foreground.png",
|
||||
"compression": 8
|
||||
},
|
||||
{
|
||||
"targetPath": "res/mipmap-hdpi-v4/ic_launcher_foreground.png",
|
||||
"assetPath": "apk/ic_launcher_foreground.png",
|
||||
"compression": 8
|
||||
},
|
||||
{
|
||||
"targetPath": "res/mipmap-xhdpi-v4/ic_launcher_foreground.png",
|
||||
"assetPath": "apk/ic_launcher_foreground.png",
|
||||
"compression": 8
|
||||
},
|
||||
{
|
||||
"targetPath": "res/mipmap-xxhdpi-v4/ic_launcher_foreground.png",
|
||||
"assetPath": "apk/ic_launcher_foreground.png",
|
||||
"compression": 8
|
||||
},
|
||||
{
|
||||
"targetPath": "res/mipmap-xxxhdpi-v4/ic_launcher_foreground.png",
|
||||
"assetPath": "apk/ic_launcher_foreground.png",
|
||||
"compression": 8
|
||||
},
|
||||
{
|
||||
"targetPath": "assemblies/Newtonsoft.Json.dll",
|
||||
"assetPath": "apk/Newtonsoft.Json.dll",
|
||||
"compression": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "assemblies/SMAPI.Toolkit.CoreInterfaces.dll",
|
||||
"assetPath": "apk/SMAPI.Toolkit.CoreInterfaces.dll",
|
||||
"compression": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "assemblies/SMAPI.Toolkit.dll",
|
||||
"assetPath": "apk/SMAPI.Toolkit.dll",
|
||||
"compression": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "assemblies/StardewModdingAPI.dll",
|
||||
"assetPath": "apk/StardewModdingAPI.dll",
|
||||
"compression": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "assemblies/System.Data.dll",
|
||||
"assetPath": "apk/System.Data.dll",
|
||||
"compression": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "assemblies/System.Numerics.dll",
|
||||
"assetPath": "apk/System.Numerics.dll",
|
||||
"compression": 0
|
||||
}
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,22 @@
|
|||
[
|
||||
{
|
||||
"assetPath":"mods/virtual-keyboard.zip",
|
||||
"Name": "VirtualKeyboard",
|
||||
"UniqueID": "SMAPI.VirtualKeyboard"
|
||||
},
|
||||
{
|
||||
"assetPath":"mods/custom-localization.zip",
|
||||
"Name": "CustomLocalization",
|
||||
"UniqueID": "ZaneYork.CustomLocalization"
|
||||
},
|
||||
{
|
||||
"assetPath":"mods/console-commands.zip",
|
||||
"Name": "Console Commands",
|
||||
"UniqueID": "SMAPI.ConsoleCommands"
|
||||
},
|
||||
{
|
||||
"assetPath":"mods/save-backup.zip",
|
||||
"Name": "SaveBackup",
|
||||
"UniqueID": "SMAPI.SaveBackup"
|
||||
}
|
||||
]
|
|
@ -0,0 +1,6 @@
|
|||
[
|
||||
"com.chucklefish.stardewvalley",
|
||||
"com.chucklefish.stardewvalleysamsung",
|
||||
"com.zane.stardewvalley",
|
||||
"com.martyrpher.stardewvalley"
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
|
||||
|
||||
|
||||
This file contains advanced configuration for SMAPI. You generally shouldn't change this file.
|
||||
The default values are mirrored in StardewModdingAPI.Framework.Models.SConfig to log custom changes.
|
||||
|
||||
|
||||
|
||||
*/
|
||||
{
|
||||
/**
|
||||
* Whether SMAPI should log more information about the game context.
|
||||
*/
|
||||
"VerboseLogging": false,
|
||||
|
||||
/**
|
||||
* Whether SMAPI should check for newer versions of SMAPI and mods when you load the game. If new
|
||||
* versions are available, an alert will be shown in the console. This doesn't affect the load
|
||||
* time even if your connection is offline or slow, because it happens in the background.
|
||||
*/
|
||||
"CheckForUpdates": true,
|
||||
|
||||
/**
|
||||
* Whether to enable features intended for mod developers. Currently this only makes TRACE-level
|
||||
* messages appear in the console.
|
||||
*/
|
||||
"DeveloperMode": false,
|
||||
|
||||
/**
|
||||
* Whether to add a section to the 'mod issues' list for mods which directly use potentially
|
||||
* sensitive .NET APIs like file or shell access. Note that many mods do this legitimately as
|
||||
* part of their normal functionality, so these warnings are meaningless without further
|
||||
* investigation. When this is commented out, it'll be true for local debug builds and false
|
||||
* otherwise.
|
||||
*/
|
||||
//"ParanoidWarnings": true,
|
||||
|
||||
/**
|
||||
* Whether SMAPI should show newer beta versions as an available update. When this is commented
|
||||
* out, it'll be true if the current SMAPI version is beta, and false otherwise.
|
||||
*/
|
||||
//"UseBetaChannel": true,
|
||||
|
||||
/**
|
||||
* SMAPI's GitHub project name, used to perform update checks.
|
||||
*/
|
||||
"GitHubProjectName": "MartyrPher/SMAPI-Android-Installer",
|
||||
|
||||
/**
|
||||
* The base URL for SMAPI's web API, used to perform update checks.
|
||||
* Note: the protocol will be changed to http:// on Linux/Mac due to OpenSSL issues with the
|
||||
* game's bundled Mono.
|
||||
*/
|
||||
"WebApiBaseUrl": "https://smapi.io/api/",
|
||||
|
||||
/**
|
||||
* Whether SMAPI should log network traffic (may be very verbose). Best combined with VerboseLogging, which includes network metadata.
|
||||
*/
|
||||
"LogNetworkTraffic": false,
|
||||
|
||||
/**
|
||||
* Whether to generate a 'SMAPI-latest.metadata-dump.json' file in the logs folder with the full mod
|
||||
* metadata for detected mods. This is only needed when troubleshooting some cases.
|
||||
*/
|
||||
"DumpMetadata": false,
|
||||
|
||||
/**
|
||||
* The colors to use for text written to the SMAPI console.
|
||||
*
|
||||
* The possible values for 'UseScheme' are:
|
||||
* - AutoDetect: SMAPI will assume a light background on Mac, and detect the background color
|
||||
* automatically on Linux or Windows.
|
||||
* - LightBackground: use darker text colors that look better on a white or light background.
|
||||
* - DarkBackground: use lighter text colors that look better on a black or dark background.
|
||||
*
|
||||
* For available color codes, see https://docs.microsoft.com/en-us/dotnet/api/system.consolecolor.
|
||||
*
|
||||
* (These values are synched with ColorfulConsoleWriter.GetDefaultColorSchemeConfig in the
|
||||
* SMAPI code.)
|
||||
*/
|
||||
"ConsoleColors": {
|
||||
"UseScheme": "AutoDetect",
|
||||
|
||||
"Schemes": {
|
||||
"DarkBackground": {
|
||||
"Trace": "DarkGray",
|
||||
"Debug": "DarkGray",
|
||||
"Info": "White",
|
||||
"Warn": "Yellow",
|
||||
"Error": "Red",
|
||||
"Alert": "Magenta",
|
||||
"Success": "DarkGreen"
|
||||
},
|
||||
"LightBackground": {
|
||||
"Trace": "DarkGray",
|
||||
"Debug": "DarkGray",
|
||||
"Info": "Black",
|
||||
"Warn": "DarkYellow",
|
||||
"Error": "Red",
|
||||
"Alert": "DarkMagenta",
|
||||
"Success": "DarkGreen"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* The mod IDs SMAPI should ignore when performing update checks or validating update keys.
|
||||
*/
|
||||
"SuppressUpdateChecks": [
|
||||
"SMAPI.ConsoleCommands",
|
||||
"SMAPI.SaveBackup"
|
||||
]
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"warn.invalid-content-removed": "Ungültiger Inhalt wurde entfernt, um einen Absturz zu verhindern (siehe SMAPI Konsole für weitere Informationen)."
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"warn.invalid-content-removed": "Invalid content was removed to prevent a crash (see the SMAPI console for info)."
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"warn.invalid-content-removed": "Se ha quitado contenido inválido para evitar un cierre forzoso (revisa la consola de SMAPI para más información)."
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"warn.invalid-content-removed": "Le contenu non valide a été supprimé afin d'éviter un plantage (voir la console de SMAPI pour plus d'informations)."
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"warn.invalid-content-removed": "クラッシュを防ぐために無効なコンテンツを取り除きました (詳細はSMAPIコンソールを参照)"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"warn.invalid-content-removed": "Conteúdo inválido foi removido para prevenir uma falha (veja o console do SMAPI para mais informações)."
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"warn.invalid-content-removed": "Недопустимое содержимое было удалено, чтобы предотвратить сбой (см. информацию в консоли SMAPI)"
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"warn.invalid-content-removed": "Yanlış paketlenmiş bir içerik, oyunun çökmemesi için yüklenmedi (SMAPI konsol penceresinde detaylı bilgi mevcut)."
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"warn.invalid-content-removed": "非法内容已移除以防游戏闪退(查看SMAPI控制台获得更多信息)"
|
||||
}
|
|
@ -0,0 +1,471 @@
|
|||
{
|
||||
/**
|
||||
* Metadata about some SMAPI mods used in compatibility, update, and dependency checks. This
|
||||
* field shouldn't be edited by players in most cases.
|
||||
*
|
||||
* Standard fields
|
||||
* ===============
|
||||
* The predefined fields are documented below (only 'ID' is required). Each entry's key is the
|
||||
* default display name for the mod if one isn't available (e.g. in dependency checks).
|
||||
*
|
||||
* - ID: the mod's latest unique ID (if any).
|
||||
*
|
||||
* - FormerIDs: uniquely identifies the mod across multiple versions, and supports matching
|
||||
* other fields if no ID was specified. This doesn't include the latest ID, if any. Multiple
|
||||
* variants can be separated with '|'.
|
||||
*
|
||||
* Versioned metadata
|
||||
* ==================
|
||||
* Each record can also specify extra metadata using the field keys below.
|
||||
*
|
||||
* Each key consists of a field name prefixed with any combination of version range and 'Default',
|
||||
* separated by pipes (whitespace trimmed). For example, 'UpdateKey' will always override,
|
||||
* 'Default | UpdateKey' will only override if the mod has no update keys, and
|
||||
* '~1.1 | Default | Name' will do the same up to version 1.1.
|
||||
*
|
||||
* The version format is 'min~max' (where either side can be blank for unbounded), or a single
|
||||
* version number.
|
||||
*
|
||||
* These are the valid field names:
|
||||
*
|
||||
* - UpdateKey: the update key to set in the mod's manifest. This is used to enable update
|
||||
* checks for older mods that haven't been updated to use it yet.
|
||||
*
|
||||
* - Status: overrides compatibility checks. The possible values are Obsolete (SMAPI won't load
|
||||
* it because the mod should no longer be used), AssumeBroken (SMAPI won't load it because
|
||||
* the specified version isn't compatible), or AssumeCompatible (SMAPI will try to load it
|
||||
* even if it detects incompatible code).
|
||||
*
|
||||
* Note that this shouldn't be set to 'AssumeBroken' if SMAPI can detect the incompatibility
|
||||
* automatically, since that hides the details from trace logs.
|
||||
*
|
||||
* - StatusReasonPhrase: a message to show to the player explaining why the mod can't be loaded
|
||||
* (if applicable). If blank, will default to a generic not-compatible message.
|
||||
*
|
||||
* - AlternativeUrl: a URL where the player can find an unofficial update or alternative if the
|
||||
* mod is no longer compatible.
|
||||
*/
|
||||
"ModData": {
|
||||
/*********
|
||||
** Common dependencies for friendly errors
|
||||
*********/
|
||||
"Advanced Location Loader": {
|
||||
"ID": "Entoarox.AdvancedLocationLoader",
|
||||
"Default | UpdateKey": "Nexus:2270"
|
||||
},
|
||||
|
||||
//"Content Patcher": {
|
||||
// "ID": "Pathoschild.ContentPatcher",
|
||||
// "Default | UpdateKey": "Nexus:1915"
|
||||
//},
|
||||
|
||||
//"Custom Farming Redux": {
|
||||
// "ID": "Platonymous.CustomFarming",
|
||||
// "Default | UpdateKey": "Nexus:991"
|
||||
//},
|
||||
|
||||
"Custom Shirts": {
|
||||
"ID": "Platonymous.CustomShirts",
|
||||
"Default | UpdateKey": "Nexus:2416"
|
||||
},
|
||||
|
||||
"Entoarox Framework": {
|
||||
"ID": "Entoarox.EntoaroxFramework",
|
||||
"Default | UpdateKey": "Nexus:2269"
|
||||
},
|
||||
|
||||
"JSON Assets": {
|
||||
"ID": "spacechase0.JsonAssets",
|
||||
"Default | UpdateKey": "Nexus:1720",
|
||||
"1.3.1 | Status": "AssumeBroken" // causes runtime crashes
|
||||
},
|
||||
|
||||
"Mail Framework": {
|
||||
"ID": "DIGUS.MailFrameworkMod",
|
||||
"Default | UpdateKey": "Nexus:1536"
|
||||
},
|
||||
|
||||
"MTN": {
|
||||
"ID": "SgtPickles.MTN",
|
||||
"Default | UpdateKey": "Nexus:2256",
|
||||
"~1.2.6 | Status": "AssumeBroken" // replaces Game1.multiplayer, which breaks SMAPI's multiplayer API.
|
||||
},
|
||||
|
||||
"PyTK": {
|
||||
"ID": "Platonymous.Toolkit",
|
||||
"Default | UpdateKey": "Nexus:1726"
|
||||
},
|
||||
|
||||
"Rubydew": {
|
||||
"ID": "bwdy.rubydew",
|
||||
"SuppressWarnings": "UsesDynamic", // mod explicitly loads DLLs for Linux/Mac compatibility
|
||||
"Default | UpdateKey": "Nexus:3656"
|
||||
},
|
||||
|
||||
"SpaceCore": {
|
||||
"ID": "spacechase0.SpaceCore",
|
||||
"Default | UpdateKey": "Nexus:1348"
|
||||
},
|
||||
|
||||
"Stardust Core": {
|
||||
"ID": "Omegasis.StardustCore",
|
||||
"Default | UpdateKey": "Nexus:2341"
|
||||
},
|
||||
|
||||
"TMX Loader": {
|
||||
"ID": "Platonymous.TMXLoader",
|
||||
"Default | UpdateKey": "Nexus:1820"
|
||||
},
|
||||
|
||||
/*********
|
||||
** Obsolete
|
||||
*********/
|
||||
"Animal Mood Fix": {
|
||||
"ID": "GPeters-AnimalMoodFix",
|
||||
"~ | Status": "Obsolete",
|
||||
"~ | StatusReasonPhrase": "the animal mood bugs were fixed in Stardew Valley 1.2."
|
||||
},
|
||||
|
||||
"Bee House Flower Range Fix": {
|
||||
"ID": "kirbylink.beehousefix",
|
||||
"~ | Status": "Obsolete",
|
||||
"~ | StatusReasonPhrase": "the bee house flower range was fixed in Stardew Valley 1.4."
|
||||
},
|
||||
|
||||
"Colored Chests": {
|
||||
"ID": "4befde5c-731c-4853-8e4b-c5cdf946805f",
|
||||
"~ | Status": "Obsolete",
|
||||
"~ | StatusReasonPhrase": "colored chests were added in Stardew Valley 1.1."
|
||||
},
|
||||
|
||||
"Modder Serialization Utility": {
|
||||
"ID": "SerializerUtils-0-1",
|
||||
"~ | Status": "Obsolete",
|
||||
"~ | StatusReasonPhrase": "it's no longer maintained or used."
|
||||
},
|
||||
|
||||
"No Debug Mode": {
|
||||
"ID": "NoDebugMode",
|
||||
"~ | Status": "Obsolete",
|
||||
"~ | StatusReasonPhrase": "debug mode was removed in SMAPI 1.0."
|
||||
},
|
||||
|
||||
/*********
|
||||
** Broke in SDV 1.4
|
||||
*********/
|
||||
"Fix Dice": {
|
||||
"ID": "ashley.fixdice",
|
||||
"~1.1.2 | Status": "AssumeBroken" // crashes game on startup
|
||||
},
|
||||
|
||||
"Fix Dice": {
|
||||
"ID": "ashley.fixdice",
|
||||
"~1.1.2 | Status": "AssumeBroken" // crashes game on startup
|
||||
},
|
||||
|
||||
"Grass Growth": {
|
||||
"ID": "bcmpinc.GrassGrowth",
|
||||
"~1.0 | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Invite Code Mod": {
|
||||
"ID": "KOREJJamJar.InviteCodeMod",
|
||||
"~1.0.1 | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Loved Labels": {
|
||||
"ID": "Advize.LovedLabels",
|
||||
"~2.2.1-unofficial.2-pathoschild | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Neat Additions": {
|
||||
"ID": "ilyaki.neatadditions",
|
||||
"~1.0.3 | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Remote Fridge Storage": {
|
||||
"ID": "EternalSoap.RemoteFridgeStorage",
|
||||
"~1.5 | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Stack Everything": {
|
||||
"ID": "cat.stackeverything",
|
||||
"~2.15 | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Yet Another Harvest With Scythe Mod": {
|
||||
"ID": "bcmpinc.HarvestWithScythe",
|
||||
"~1.1 | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
/*********
|
||||
** Broke in SMAPI 3.0 (runtime errors due to lifecycle changes)
|
||||
*********/
|
||||
"Advancing Sprinklers": {
|
||||
"ID": "warix3.advancingsprinklers",
|
||||
"~1.0.0 | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Arcade 2048": {
|
||||
"ID": "Platonymous.2048",
|
||||
"~1.0.6 | Status": "AssumeBroken" // possibly due to PyTK
|
||||
},
|
||||
|
||||
"Arcade Snake": {
|
||||
"ID": "Platonymous.Snake",
|
||||
"~1.1.0 | Status": "AssumeBroken" // possibly due to PyTK
|
||||
},
|
||||
|
||||
"Better Sprinklers": {
|
||||
"ID": "Speeder.BetterSprinklers",
|
||||
"~2.3.1-unofficial.7-pathoschild | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Content Patcher": {
|
||||
"ID": "Pathoschild.ContentPatcher",
|
||||
"Default | UpdateKey": "Nexus:1915",
|
||||
"~1.6.4 | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Current Location (Vrakyas)": {
|
||||
"ID": "Vrakyas.CurrentLocation",
|
||||
"~1.5.4 | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Custom Adventure Guild Challenges": {
|
||||
"ID": "DefenTheNation.CustomGuildChallenges",
|
||||
"~1.8 | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Custom Farming Redux": {
|
||||
"ID": "Platonymous.CustomFarming",
|
||||
"Default | UpdateKey": "Nexus:991",
|
||||
"~2.10.10 | Status": "AssumeBroken" // possibly due to PyTK
|
||||
},
|
||||
|
||||
"Decrafting Mod": {
|
||||
"ID": "MSCFC.DecraftingMod",
|
||||
"~1.0 | Status": "AssumeBroken" // NRE in ModEntry
|
||||
},
|
||||
|
||||
"JoJaBan - Arcade Sokoban": {
|
||||
"ID": "Platonymous.JoJaBan",
|
||||
"~0.4.3 | Status": "AssumeBroken" // possibly due to PyTK
|
||||
},
|
||||
|
||||
"Level Extender": {
|
||||
"ID": "DevinLematty.LevelExtender",
|
||||
"~3.1 | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Mod Update Menu": {
|
||||
"ID": "cat.modupdatemenu",
|
||||
"~1.4 | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Quick Start": {
|
||||
"ID": "WuestMan.QuickStart",
|
||||
"~1.5 | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Seed Bag": {
|
||||
"ID": "Platonymous.SeedBag",
|
||||
"~1.2.7 | Status": "AssumeBroken" // possibly due to PyTK
|
||||
},
|
||||
|
||||
"Stardew Valley ESP": {
|
||||
"ID": "reimu.sdv-helper",
|
||||
"~1.1 | Status": "AssumeBroken"
|
||||
},
|
||||
|
||||
"Underdark Krobus": {
|
||||
"ID": "melnoelle.underdarkkrobus",
|
||||
"~1.0.0 | Status": "AssumeBroken" // NRE in ModEntry
|
||||
},
|
||||
|
||||
"Underdark Sewer": {
|
||||
"ID": "melnoelle.underdarksewer",
|
||||
"~1.1.0 | Status": "AssumeBroken" // NRE in ModEntry
|
||||
},
|
||||
|
||||
/*********
|
||||
** Broke in SDV 1.3.36
|
||||
*********/
|
||||
"2cute FarmCave": {
|
||||
"ID": "taintedwheat.2CuteFarmCave",
|
||||
"Default | UpdateKey": "Nexus:843",
|
||||
"~2.0 | Status": "AssumeBroken" // references deleted Content/Mine.xnb
|
||||
},
|
||||
|
||||
"Ace's Expanded Caves - Default Cave": {
|
||||
"ID": "Acerbicon.AECdefault",
|
||||
"Default | UpdateKey": "Nexus:2131",
|
||||
"~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb
|
||||
},
|
||||
|
||||
"Ace's Expanded Caves - Desert Cave": {
|
||||
"ID": "Acerbicon.AECdesert",
|
||||
"Default | UpdateKey": "Nexus:2131",
|
||||
"~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb
|
||||
},
|
||||
|
||||
"Ace's Expanded Caves - Ice Cave": {
|
||||
"ID": "Acerbicon.AECice",
|
||||
"Default | UpdateKey": "Nexus:2131",
|
||||
"~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb
|
||||
},
|
||||
|
||||
"Ace's Expanded Caves - Lava Cave": {
|
||||
"ID": "Acerbicon.AEClava",
|
||||
"Default | UpdateKey": "Nexus:2131",
|
||||
"~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb
|
||||
},
|
||||
|
||||
"Ace's Expanded Caves - Slime Cave": {
|
||||
"ID": "Acerbicon.AECslime",
|
||||
"Default | UpdateKey": "Nexus:2131",
|
||||
"~1.2.2 | Status": "AssumeBroken" // references deleted Content/Mine.xnb
|
||||
},
|
||||
|
||||
"Green Pastures Farm": {
|
||||
"ID": "bugbuddy.GreenPasturesFarm",
|
||||
"Default | UpdateKey": "Nexus:2326",
|
||||
"~1.0 | Status": "AssumeBroken" // references deleted Content/weapons.xnb
|
||||
},
|
||||
|
||||
"Immersive Farm 2": {
|
||||
"ID": "zander.immersivefarm2",
|
||||
"~2.0.1 | Status": "AssumeBroken" // references deleted Content/Mine.xnb
|
||||
},
|
||||
|
||||
"Karmylla's Immersive Map Edits": {
|
||||
"ID": "Karmylla.ImmersiveMapEdits",
|
||||
"Default | UpdateKey": "Nexus:1149",
|
||||
"~2.4 | Status": "AssumeBroken" // references deleted Content/weapons.xnb
|
||||
},
|
||||
|
||||
"Secret Gardens Greenhouse": {
|
||||
"ID": "jessebot.secretgardens",
|
||||
"Default | UpdateKey": "Nexus:3067",
|
||||
"~2.0.1 | Status": "AssumeBroken" // references deleted Content/Mine.xnb
|
||||
},
|
||||
|
||||
/*********
|
||||
** Broke circa SDV 1.3
|
||||
*********/
|
||||
"Canon-Friendly Dialogue Expansion": {
|
||||
"ID": "gizzymo.canonfriendlyexpansion",
|
||||
"~1.1.1 | Status": "AssumeBroken" // causes a save crash on certain dates
|
||||
},
|
||||
|
||||
"Everytime Submarine": {
|
||||
"ID": "MustafaDemirel.EverytimeSubmarine",
|
||||
"~1.0.0 | Status": "AssumeBroken" // breaks player saves if their beach bridge is fixed
|
||||
},
|
||||
|
||||
"Always Scroll Map": {
|
||||
"ID": "bcmpinc.AlwaysScrollMap",
|
||||
"~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
|
||||
},
|
||||
|
||||
"Arcade Pong": {
|
||||
"ID": "Platonymous.ArcadePong",
|
||||
"~1.0.2 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals
|
||||
},
|
||||
|
||||
"BJS Night Sounds": {
|
||||
"ID": "BunnyJumps.BJSNightSounds",
|
||||
"~1.0.0 | Status": "AssumeBroken" // runtime errors with Harmony 1.2.0.1 in SMAPI 2.8+
|
||||
},
|
||||
|
||||
"Craft Counter": {
|
||||
"ID": "bcmpinc.CraftCounter",
|
||||
"~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
|
||||
},
|
||||
|
||||
"Fishing Adjust": {
|
||||
"ID": "shuaiz.FishingAdjustMod",
|
||||
"~2.0.1 | Status": "AssumeBroken" // Method not found: 'Void Harmony.HarmonyInstance.Patch(System.Reflection.MethodBase, Harmony.HarmonyMethod, Harmony.HarmonyMethod, Harmony.HarmonyMethod)'
|
||||
},
|
||||
|
||||
"Fishing Automaton": {
|
||||
"ID": "Drynwynn.FishingAutomaton",
|
||||
"~1.1 | Status": "AssumeBroken" // runtime errors with Harmony 1.2.0.1 in SMAPI 2.8+
|
||||
},
|
||||
|
||||
"Fix Animal Tools": {
|
||||
"ID": "bcmpinc.FixAnimalTools",
|
||||
"~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
|
||||
},
|
||||
|
||||
"Fix Scythe Exp": {
|
||||
"ID": "bcmpinc.FixScytheExp",
|
||||
"~0.3 | Status": "AssumeBroken" // broke in 1.3: Exception from HarmonyInstance "bcmpinc.FixScytheExp" [...] Bad label content in ILGenerator.
|
||||
},
|
||||
|
||||
"More Silo Storage": {
|
||||
"ID": "OrneryWalrus.MoreSiloStorage",
|
||||
"~1.0.1 | Status": "AssumeBroken" // broke in SDV 1.3
|
||||
},
|
||||
|
||||
"Movement Speed": {
|
||||
"ID": "bcmpinc.MovementSpeed",
|
||||
"~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
|
||||
},
|
||||
|
||||
"No Added Flying Mine Monsters": {
|
||||
"ID": "Drynwynn.NoAddedFlyingMineMonsters",
|
||||
"~1.1 | Status": "AssumeBroken" // runtime errors with Harmony 1.2.0.1 in SMAPI 2.8+
|
||||
},
|
||||
|
||||
"Server Bookmarker": {
|
||||
"ID": "Ilyaki.ServerBookmarker",
|
||||
"~1.0.0 | Status": "AssumeBroken" // broke in Stardew Valley 1.3.29 (runtime errors)
|
||||
},
|
||||
|
||||
"Skull Cave Saver": {
|
||||
"ID": "cantorsdust.SkullCaveSaver",
|
||||
"FormerIDs": "8ac06349-26f7-4394-806c-95d48fd35774 | community.SkullCaveSaver", // changed in 1.1 and 1.2.2
|
||||
"1.3-beta | Status": "AssumeBroken" // doesn't work in multiplayer, no longer maintained
|
||||
},
|
||||
|
||||
"Split Screen": {
|
||||
"ID": "Ilyaki.SplitScreen",
|
||||
"~3.0.1 | Status": "AssumeBroken" // broke in SMAPI 2.6-beta.16 due to reflection into SMAPI internals
|
||||
},
|
||||
|
||||
"Stardew Hack": {
|
||||
"ID": "bcmpinc.StardewHack",
|
||||
"~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
|
||||
},
|
||||
|
||||
"Stephan's Lots of Crops": {
|
||||
"ID": "stephansstardewcrops",
|
||||
"~1.1 | Status": "AssumeBroken" // broke in SDV 1.3 (overwrites vanilla items)
|
||||
},
|
||||
|
||||
"Summit Reborn": {
|
||||
"ID": "KoihimeNakamura.summitreborn",
|
||||
"FormerIDs": "emissaryofinfinity.summitreborn", // changed in 1.0.2
|
||||
"~1.0.2 | Status": "AssumeBroken" // broke in SDV 1.3 (runtime errors)
|
||||
},
|
||||
|
||||
"Tilled Soil Decay": {
|
||||
"ID": "bcmpinc.TilledSoilDecay",
|
||||
"~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
|
||||
},
|
||||
|
||||
"Tree Spread": {
|
||||
"ID": "bcmpinc.TreeSpread",
|
||||
"~0.6 | Status": "AssumeBroken" // breaks newer versions of bcmpinc mods (per bcmpinc's request)
|
||||
},
|
||||
|
||||
/*********
|
||||
** Broke circa SDV 1.2
|
||||
*********/
|
||||
"Move Faster": {
|
||||
"ID": "shuaiz.MoveFasterMod",
|
||||
"~1.0.1 | Status": "AssumeBroken" // doesn't do anything as of SDV 1.2.33 (bad Harmony patch?)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
[
|
||||
{
|
||||
"targetPath": "smapi-internal/0Harmony.dll",
|
||||
"assetPath": "smapi/0Harmony.dll",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/config.json",
|
||||
"assetPath": "smapi/config.json",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/metadata.json",
|
||||
"assetPath": "smapi/metadata.json",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/Mono.Cecil.dll",
|
||||
"assetPath": "smapi/Mono.Cecil.dll",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/MonoGame.Framework.dll",
|
||||
"assetPath": "assemblies/MonoGame.Framework.dll",
|
||||
"origin": 1
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/MonoMod.RuntimeDetour.dll",
|
||||
"assetPath": "smapi/MonoMod.RuntimeDetour.dll",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/MonoMod.Utils.dll",
|
||||
"assetPath": "smapi/MonoMod.Utils.dll",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/mscorlib.dll",
|
||||
"assetPath": "assemblies/mscorlib.dll",
|
||||
"origin": 1
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/Newtonsoft.Json.dll",
|
||||
"assetPath": "apk/Newtonsoft.Json.dll",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/SMAPI.Toolkit.CoreInterfaces.dll",
|
||||
"assetPath": "apk/SMAPI.Toolkit.CoreInterfaces.dll",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/SMAPI.Toolkit.dll",
|
||||
"assetPath": "apk/SMAPI.Toolkit.dll",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/StardewModdingAPI.dll",
|
||||
"assetPath": "apk/StardewModdingAPI.dll",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/StardewValley.dll",
|
||||
"assetPath": "assemblies/StardewValley.dll",
|
||||
"origin": 1
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/StardewValley.GameData.dll",
|
||||
"assetPath": "assemblies/StardewValley.GameData.dll",
|
||||
"origin": 1
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/System.Core.dll",
|
||||
"assetPath": "assemblies/System.Core.dll",
|
||||
"origin": 1
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/System.Data.dll",
|
||||
"assetPath": "apk/System.Data.dll",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/System.dll",
|
||||
"assetPath": "assemblies/System.dll",
|
||||
"origin": 1
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/System.Net.Http.dll",
|
||||
"assetPath": "assemblies/System.Net.Http.dll",
|
||||
"origin": 1
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/System.Numerics.dll",
|
||||
"assetPath": "apk/System.Numerics.dll",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/System.Runtime.Serialization.dll",
|
||||
"assetPath": "assemblies/System.Runtime.Serialization.dll",
|
||||
"origin": 1
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/System.ServiceModel.Internals.dll",
|
||||
"assetPath": "assemblies/System.ServiceModel.Internals.dll",
|
||||
"origin": 1
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/System.Xml.dll",
|
||||
"assetPath": "assemblies/System.Xml.dll",
|
||||
"origin": 1
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/System.Xml.Linq.dll",
|
||||
"assetPath": "smapi/System.Xml.Linq.dll",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/TMXTile.dll",
|
||||
"assetPath": "smapi/TMXTile.dll",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/xTile.dll",
|
||||
"assetPath": "assemblies/xTile.dll",
|
||||
"origin": 1
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/i18n/de.json",
|
||||
"assetPath": "smapi/i18n/de.json",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/i18n/default.json",
|
||||
"assetPath": "smapi/i18n/default.json",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/i18n/es.json",
|
||||
"assetPath": "smapi/i18n/es.json",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/i18n/fr.json",
|
||||
"assetPath": "smapi/i18n/fr.json",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/i18n/ja.json",
|
||||
"assetPath": "smapi/i18n/ja.json",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/i18n/pt.json",
|
||||
"assetPath": "smapi/i18n/pt.json",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/i18n/ru.json",
|
||||
"assetPath": "smapi/i18n/ru.json",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/i18n/tr.json",
|
||||
"assetPath": "smapi/i18n/tr.json",
|
||||
"origin": 0
|
||||
},
|
||||
{
|
||||
"targetPath": "smapi-internal/i18n/zh.json",
|
||||
"assetPath": "smapi/i18n/zh.json",
|
||||
"origin": 0
|
||||
}
|
||||
]
|
|
@ -0,0 +1,100 @@
|
|||
package com.zane.smapiinstaller;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
import com.zane.smapiinstaller.logic.GameLauncher;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.navigation.ui.AppBarConfiguration;
|
||||
import androidx.navigation.ui.NavigationUI;
|
||||
|
||||
import androidx.drawerlayout.widget.DrawerLayout;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.Toolbar;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private AppBarConfiguration mAppBarConfiguration;
|
||||
|
||||
@BindView(R.id.toolbar)
|
||||
Toolbar toolbar;
|
||||
|
||||
@BindView(R.id.drawer_layout)
|
||||
DrawerLayout drawer;
|
||||
|
||||
@BindView(R.id.nav_view)
|
||||
NavigationView navigationView;
|
||||
|
||||
public void requestPermissions()
|
||||
{
|
||||
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
!= PackageManager.PERMISSION_GRANTED) {
|
||||
ActivityCompat.requestPermissions(MainActivity.this,
|
||||
new String[]{Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
initView();
|
||||
}
|
||||
}
|
||||
|
||||
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED)
|
||||
{
|
||||
initView();
|
||||
}
|
||||
else {
|
||||
requestPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
ButterKnife.bind(this);
|
||||
requestPermissions();
|
||||
}
|
||||
|
||||
private void initView() {
|
||||
setSupportActionBar(toolbar);
|
||||
// Passing each menu ID as a set of Ids because each
|
||||
// menu should be considered as top level destinations.
|
||||
mAppBarConfiguration = new AppBarConfiguration.Builder(
|
||||
R.id.nav_install, R.id.nav_config, R.id.nav_help)
|
||||
.setDrawerLayout(drawer)
|
||||
.build();
|
||||
final NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
|
||||
NavigationUI.setupActionBarWithNavController(this, navController, mAppBarConfiguration);
|
||||
NavigationUI.setupWithNavController(navigationView, navController);
|
||||
}
|
||||
|
||||
@OnClick(R.id.launch) void launchButtonClick() {
|
||||
new GameLauncher(navigationView).launch();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
// Inflate the menu; this adds items to the action bar if it is present.
|
||||
getMenuInflater().inflate(R.menu.main, menu);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSupportNavigateUp() {
|
||||
NavController navController = Navigation.findNavController(this, R.id.nav_host_fragment);
|
||||
return NavigationUI.navigateUp(navController, mAppBarConfiguration)
|
||||
|| super.onSupportNavigateUp();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.zane.smapiinstaller.constant;
|
||||
|
||||
public class Constants {
|
||||
public static String MOD_PATH = "StardewValley/Mods";
|
||||
public static String LOG_PATH = "StardewValley/ErrorLogs/SMAPI-latest.txt";
|
||||
|
||||
public static String TARGET_PACKAGE_NAME = "com.zane.stardewvalley";
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
package com.zane.smapiinstaller.entity;
|
||||
|
||||
public class ManifestEntry {
|
||||
private String targetPath;
|
||||
private String assetPath;
|
||||
private int compression;
|
||||
private int origin;
|
||||
|
||||
public String getTargetPath() {
|
||||
return targetPath;
|
||||
}
|
||||
|
||||
public void setTargetPath(String targetPath) {
|
||||
this.targetPath = targetPath;
|
||||
}
|
||||
|
||||
public String getAssetPath() {
|
||||
return assetPath;
|
||||
}
|
||||
|
||||
public void setAssetPath(String assetPath) {
|
||||
this.assetPath = assetPath;
|
||||
}
|
||||
|
||||
public int getCompression() {
|
||||
return compression;
|
||||
}
|
||||
|
||||
public void setCompression(int compression) {
|
||||
this.compression = compression;
|
||||
}
|
||||
|
||||
public int getOrigin() {
|
||||
return origin;
|
||||
}
|
||||
|
||||
public void setOrigin(int origin) {
|
||||
this.origin = origin;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package com.zane.smapiinstaller.entity;
|
||||
|
||||
public class ModManifestEntry {
|
||||
private String assetPath;
|
||||
private String Name;
|
||||
private String UniqueID;
|
||||
private String Description;
|
||||
private ModManifestEntry ContentPackFor;
|
||||
|
||||
public String getAssetPath() {
|
||||
return assetPath;
|
||||
}
|
||||
|
||||
public void setAssetPath(String assetPath) {
|
||||
this.assetPath = assetPath;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return Name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
Name = name;
|
||||
}
|
||||
|
||||
public String getUniqueID() {
|
||||
return UniqueID;
|
||||
}
|
||||
|
||||
public void setUniqueID(String uniqueID) {
|
||||
UniqueID = uniqueID;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return Description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
Description = description;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,199 @@
|
|||
package com.zane.smapiinstaller.logic;
|
||||
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.io.Files;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.zane.smapiinstaller.BuildConfig;
|
||||
import com.zane.smapiinstaller.R;
|
||||
import com.zane.smapiinstaller.constant.Constants;
|
||||
import com.zane.smapiinstaller.entity.ManifestEntry;
|
||||
|
||||
import net.fornwall.apksigner.KeyStoreFileManager;
|
||||
import net.fornwall.apksigner.ZipSigner;
|
||||
|
||||
import org.zeroturnaround.zip.ByteSource;
|
||||
import org.zeroturnaround.zip.ZipEntrySource;
|
||||
import org.zeroturnaround.zip.ZipUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.security.KeyStore;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.zip.Deflater;
|
||||
|
||||
import androidx.core.content.FileProvider;
|
||||
import pxb.android.axml.NodeVisitor;
|
||||
|
||||
public class ApkPatcher {
|
||||
|
||||
private static final String PASSWORD = "android";
|
||||
|
||||
private Context context;
|
||||
|
||||
private static String TAG = "PATCHER";
|
||||
|
||||
public ApkPatcher(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
public String extract() {
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
List<String> packageNames = CommonLogic.getAssetJson(context, "package_names.json", new TypeToken<List<String>>() {
|
||||
}.getType());
|
||||
if (packageNames == null)
|
||||
return null;
|
||||
for (String packageName : packageNames) {
|
||||
try {
|
||||
PackageInfo packageInfo = packageManager.getPackageInfo(packageName, 0);
|
||||
String sourceDir = packageInfo.applicationInfo.publicSourceDir;
|
||||
File apkFile = new File(sourceDir);
|
||||
|
||||
File externalFilesDir = Environment.getExternalStorageDirectory();
|
||||
if (externalFilesDir != null) {
|
||||
File dest = new File(externalFilesDir.getAbsolutePath() + "/SMAPI Installer/");
|
||||
if (!dest.exists()) {
|
||||
if (!dest.mkdir()) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
File distFile = new File(dest, apkFile.getName());
|
||||
Files.copy(apkFile, distFile);
|
||||
return distFile.getAbsolutePath();
|
||||
}
|
||||
} catch (PackageManager.NameNotFoundException | IOException e) {
|
||||
Log.e(TAG, "Extract error", e);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public boolean patch(String apkPath) {
|
||||
if (apkPath == null)
|
||||
return false;
|
||||
File file = new File(apkPath);
|
||||
if (!file.exists())
|
||||
return false;
|
||||
try {
|
||||
List<ManifestEntry> manifestEntries = CommonLogic.getAssetJson(context, "apk_files_manifest.json", new TypeToken<List<ManifestEntry>>() {
|
||||
}.getType());
|
||||
if (manifestEntries == null)
|
||||
return false;
|
||||
List<ZipEntrySource> zipEntrySourceList = new ArrayList<>();
|
||||
for (ManifestEntry entry : manifestEntries) {
|
||||
zipEntrySourceList.add(new ByteSource(entry.getTargetPath(), CommonLogic.getAssetBytes(context, entry.getAssetPath()), entry.getCompression()));
|
||||
}
|
||||
byte[] manifest = ZipUtil.unpackEntry(file, "AndroidManifest.xml");
|
||||
byte[] modifiedManifest = modifyManifest(manifest);
|
||||
if(modifiedManifest == null) {
|
||||
return false;
|
||||
}
|
||||
zipEntrySourceList.add(new ByteSource("AndroidManifest.xml", modifiedManifest, Deflater.DEFLATED));
|
||||
ZipUtil.addOrReplaceEntries(file, zipEntrySourceList.toArray(new ZipEntrySource[0]));
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Patch error", e);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private byte[] modifyManifest(byte[] bytes) {
|
||||
AtomicReference<String> packageName = new AtomicReference<>();
|
||||
Predicate<ManifestTagVisitor.AttrArgs> processLogic = (attr) -> {
|
||||
if (attr.type == NodeVisitor.TYPE_STRING) {
|
||||
String strObj = (String) attr.obj;
|
||||
switch (attr.name) {
|
||||
case "package":
|
||||
if (packageName.get() == null) {
|
||||
packageName.set(strObj);
|
||||
attr.obj = Constants.TARGET_PACKAGE_NAME;
|
||||
}
|
||||
break;
|
||||
case "label":
|
||||
if (strObj.contains("Stardew Valley")) {
|
||||
attr.obj = context.getString(R.string.smapi_game_name);
|
||||
}
|
||||
break;
|
||||
case "authorities":
|
||||
if (strObj.contains(packageName.get())) {
|
||||
attr.obj = strObj.replace(packageName.get(), Constants.TARGET_PACKAGE_NAME);
|
||||
}
|
||||
case "name":
|
||||
if (strObj.contains(".MainActivity")) {
|
||||
attr.obj = strObj.replaceFirst("\\w+\\.MainActivity", "md5723872fa9a204f7f942686e9ed9d0b7d.SMainActivity");
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
return CommonLogic.modifyManifest(bytes, processLogic);
|
||||
}
|
||||
|
||||
public String sign(String apkPath) {
|
||||
try {
|
||||
File externalFilesDir = Environment.getExternalStorageDirectory();
|
||||
if (externalFilesDir != null) {
|
||||
String signApkPath = externalFilesDir.getAbsolutePath() + "/SMAPI Installer/base_signed.apk";
|
||||
KeyStore ks = new KeyStoreFileManager.JksKeyStore();
|
||||
try (InputStream fis = context.getAssets().open("debug.keystore")) {
|
||||
ks.load(fis, PASSWORD.toCharArray());
|
||||
}
|
||||
String alias = ks.aliases().nextElement();
|
||||
X509Certificate publicKey = (X509Certificate) ks.getCertificate(alias);
|
||||
try {
|
||||
PrivateKey privateKey = (PrivateKey) ks.getKey(alias, "android".toCharArray());
|
||||
ZipSigner.signZip(publicKey, privateKey, "SHA1withRSA", apkPath, signApkPath);
|
||||
new File(apkPath).delete();
|
||||
return signApkPath;
|
||||
} catch (NoSuchAlgorithmException ignored) {
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "Sign error", e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public void install(String apkPath) {
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(fromFile(new File(apkPath)), "application/vnd.android.package-archive");
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
|
||||
try {
|
||||
context.startActivity(intent);
|
||||
} catch (ActivityNotFoundException e) {
|
||||
Log.e(TAG, "Install error", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URI from a file
|
||||
*
|
||||
* @param file = The file to try and get the URI from
|
||||
* @return The URI for the file
|
||||
*/
|
||||
private Uri fromFile(File file) {
|
||||
//Android versions greater than Nougat use FileProvider, others use the URI.fromFile.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
|
||||
return FileProvider.getUriForFile(context, BuildConfig.APPLICATION_ID + ".provider", file);
|
||||
else
|
||||
return Uri.fromFile(file);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,210 @@
|
|||
package com.zane.smapiinstaller.logic;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.io.ByteStreams;
|
||||
import com.google.common.io.CharStreams;
|
||||
import com.google.common.io.Files;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.zane.smapiinstaller.R;
|
||||
import com.zane.smapiinstaller.entity.ManifestEntry;
|
||||
|
||||
import org.zeroturnaround.zip.ZipUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.List;
|
||||
|
||||
import pxb.android.axml.AxmlReader;
|
||||
import pxb.android.axml.AxmlVisitor;
|
||||
import pxb.android.axml.AxmlWriter;
|
||||
import pxb.android.axml.NodeVisitor;
|
||||
|
||||
public class CommonLogic {
|
||||
|
||||
public static String getFileText(File file) {
|
||||
try {
|
||||
InputStream inputStream = new FileInputStream(file);
|
||||
try (InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
|
||||
return CharStreams.toString(reader);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static <T> T getFileJson(File file, Type type) {
|
||||
try {
|
||||
InputStream inputStream = new FileInputStream(file);
|
||||
try (InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
|
||||
return new Gson().fromJson(CharStreams.toString(reader), type);
|
||||
}
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static <T> T getAssetJson(Context context, String filename, Type type) {
|
||||
try {
|
||||
InputStream inputStream = context.getAssets().open(filename);
|
||||
try (InputStreamReader reader = new InputStreamReader(inputStream, StandardCharsets.UTF_8)) {
|
||||
return new Gson().fromJson(CharStreams.toString(reader), type);
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static byte[] getAssetBytes(Context context, String filename) {
|
||||
try {
|
||||
try (InputStream inputStream = context.getAssets().open(filename)) {
|
||||
return ByteStreams.toByteArray(inputStream);
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
public static void setProgressDialogState(View view, MaterialDialog dialog, int message, int progress) {
|
||||
Activity activity = getActivityFromView(view);
|
||||
if(activity != null && !activity.isFinishing() && !dialog.isCancelled()) {
|
||||
activity.runOnUiThread(()->{
|
||||
dialog.incrementProgress(progress - dialog.getCurrentProgress());
|
||||
dialog.setContent(message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static Activity getActivityFromView(View view) {
|
||||
if (null != view) {
|
||||
Context context = view.getContext();
|
||||
while (context instanceof ContextWrapper) {
|
||||
if (context instanceof Activity) {
|
||||
return (Activity) context;
|
||||
}
|
||||
context = ((ContextWrapper) context).getBaseContext();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void showAlertDialog(View view, int title, String message) {
|
||||
Activity activity = getActivityFromView(view);
|
||||
if(activity != null && !activity.isFinishing()) {
|
||||
activity.runOnUiThread(()->{
|
||||
new MaterialDialog.Builder(activity).title(title).content(message).positiveText(R.string.ok).show();
|
||||
});
|
||||
}
|
||||
}
|
||||
public static void showAlertDialog(View view, int title, int message) {
|
||||
Activity activity = getActivityFromView(view);
|
||||
if(activity != null && !activity.isFinishing()) {
|
||||
activity.runOnUiThread(()->{
|
||||
new MaterialDialog.Builder(activity).title(title).content(message).positiveText(R.string.ok).show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static void showConfirmDialog(View view, int title, int message, MaterialDialog.SingleButtonCallback callback) {
|
||||
Activity activity = getActivityFromView(view);
|
||||
if(activity != null && !activity.isFinishing()) {
|
||||
activity.runOnUiThread(()->{
|
||||
new MaterialDialog.Builder(activity).title(title).content(message).positiveText(R.string.confirm).negativeText(R.string.cancel).onAny(callback).show();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean unpackSmapiFiles(Context context, String apkPath, boolean checkMod) {
|
||||
List<ManifestEntry> manifestEntries = CommonLogic.getAssetJson(context, "smapi_files_manifest.json", new TypeToken<List<ManifestEntry>>() {
|
||||
}.getType());
|
||||
if(manifestEntries == null)
|
||||
return false;
|
||||
File basePath = new File(Environment.getExternalStorageDirectory() + "/StardewValley/");
|
||||
if(!basePath.exists()) {
|
||||
if(!basePath.mkdir()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
File noMedia = new File(basePath,".nomedia");
|
||||
if (!noMedia.exists()) {
|
||||
try {
|
||||
noMedia.createNewFile();
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
}
|
||||
for (ManifestEntry entry : manifestEntries) {
|
||||
File targetFile = new File(basePath, entry.getTargetPath());
|
||||
switch (entry.getOrigin()) {
|
||||
case 0:
|
||||
if(!checkMod || !targetFile.exists()) {
|
||||
try (InputStream inputStream = context.getAssets().open(entry.getAssetPath())) {
|
||||
if (!targetFile.getParentFile().exists()) {
|
||||
if (!targetFile.getParentFile().mkdirs()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
try (FileOutputStream outputStream = new FileOutputStream(targetFile)) {
|
||||
ByteStreams.copy(inputStream, outputStream);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
Log.e("COMMON", "Copy Error", e);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
if(!checkMod || !targetFile.exists()) {
|
||||
ZipUtil.unpackEntry(new File(apkPath), entry.getAssetPath(), targetFile);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void openUrl(Context context, String url) {
|
||||
Intent intent = new Intent();
|
||||
intent.setData(Uri.parse(url));
|
||||
intent.setAction(Intent.ACTION_VIEW);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
public static byte[] modifyManifest(byte[] bytes, Predicate<ManifestTagVisitor.AttrArgs> processLogic) {
|
||||
AxmlReader reader = new AxmlReader(bytes);
|
||||
AxmlWriter writer = new AxmlWriter();
|
||||
try {
|
||||
reader.accept(new AxmlVisitor(writer) {
|
||||
@Override
|
||||
public NodeVisitor child(String ns, String name) {
|
||||
NodeVisitor child = super.child(ns, name);
|
||||
return new ManifestTagVisitor(child, processLogic);
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
try {
|
||||
return writer.toByteArray();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package com.zane.smapiinstaller.logic;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageInfo;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.view.View;
|
||||
|
||||
import com.zane.smapiinstaller.R;
|
||||
import com.zane.smapiinstaller.constant.Constants;
|
||||
|
||||
public class GameLauncher {
|
||||
|
||||
private View root;
|
||||
|
||||
public GameLauncher(View root) {
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
public void launch() {
|
||||
Activity context = CommonLogic.getActivityFromView(root);
|
||||
PackageManager packageManager = context.getPackageManager();
|
||||
try {
|
||||
PackageInfo packageInfo = packageManager.getPackageInfo(Constants.TARGET_PACKAGE_NAME, 0);
|
||||
if(!CommonLogic.unpackSmapiFiles(context, packageInfo.applicationInfo.publicSourceDir, true)) {
|
||||
CommonLogic.showAlertDialog(root, R.string.error, R.string.error_failed_to_repair);
|
||||
return;
|
||||
}
|
||||
Intent intent = packageManager.getLaunchIntentForPackage(Constants.TARGET_PACKAGE_NAME);
|
||||
context.startActivity(intent);
|
||||
} catch (PackageManager.NameNotFoundException ignored) {
|
||||
CommonLogic.showAlertDialog(root, R.string.error, R.string.error_smapi_not_installed);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.zane.smapiinstaller.logic;
|
||||
|
||||
import com.google.common.base.Function;
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import pxb.android.axml.NodeVisitor;
|
||||
|
||||
public class ManifestTagVisitor extends NodeVisitor {
|
||||
|
||||
private Predicate<AttrArgs> attrProcessLogic;
|
||||
|
||||
public ManifestTagVisitor(NodeVisitor nv, Predicate<AttrArgs> attrProcessLogic) {
|
||||
super(nv);
|
||||
this.attrProcessLogic = attrProcessLogic;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attr(String ns, String name, int resourceId, int type, Object obj) {
|
||||
AttrArgs attrArgs = new AttrArgs(ns, name, resourceId, type, obj);
|
||||
attrProcessLogic.apply(attrArgs);
|
||||
super.attr(attrArgs.ns, attrArgs.name, attrArgs.resourceId, attrArgs.type, attrArgs.obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeVisitor child(String ns, String name) {
|
||||
return new ManifestTagVisitor(super.child(ns, name), attrProcessLogic);
|
||||
}
|
||||
|
||||
public static class AttrArgs {
|
||||
String ns;
|
||||
String name;
|
||||
int resourceId;
|
||||
int type;
|
||||
Object obj;
|
||||
|
||||
public AttrArgs(String ns, String name, int resourceId, int type, Object obj) {
|
||||
this.ns = ns;
|
||||
this.name = name;
|
||||
this.resourceId = resourceId;
|
||||
this.type = type;
|
||||
this.obj = obj;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
package com.zane.smapiinstaller.logic;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.os.Environment;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
|
||||
import com.google.common.base.Joiner;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableListMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Multimaps;
|
||||
import com.google.common.collect.Queues;
|
||||
import com.google.gson.reflect.TypeToken;
|
||||
import com.zane.smapiinstaller.R;
|
||||
import com.zane.smapiinstaller.constant.Constants;
|
||||
import com.zane.smapiinstaller.entity.ModManifestEntry;
|
||||
|
||||
import org.zeroturnaround.zip.ZipUtil;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
public class ModAssetsManager {
|
||||
|
||||
private View root;
|
||||
|
||||
private static String TAG = "MANAGER";
|
||||
|
||||
public ModAssetsManager(View root) {
|
||||
this.root = root;
|
||||
}
|
||||
|
||||
public List<ModManifestEntry> findAllInstalledMods() {
|
||||
ConcurrentLinkedQueue<File> files = Queues.newConcurrentLinkedQueue();
|
||||
files.add(new File(Environment.getExternalStorageDirectory(), Constants.MOD_PATH));
|
||||
List<ModManifestEntry> mods = new ArrayList<>(30);
|
||||
do {
|
||||
File currentFile = files.poll();
|
||||
if(currentFile != null && currentFile.exists()) {
|
||||
boolean foundManifest = false;
|
||||
for(File file : currentFile.listFiles(File::isFile)) {
|
||||
if(file.getName().equalsIgnoreCase("manifest.json")) {
|
||||
ModManifestEntry manifest = CommonLogic.getFileJson(file, new TypeToken<ModManifestEntry>(){}.getType());
|
||||
foundManifest = true;
|
||||
if(manifest != null) {
|
||||
manifest.setAssetPath(file.getParentFile().getAbsolutePath());
|
||||
mods.add(manifest);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!foundManifest) {
|
||||
files.addAll(Lists.newArrayList(currentFile.listFiles(File::isDirectory)));
|
||||
}
|
||||
}
|
||||
} while (!files.isEmpty());
|
||||
return mods;
|
||||
}
|
||||
|
||||
public boolean installDefaultMods() {
|
||||
Activity context = CommonLogic.getActivityFromView(root);
|
||||
List<ModManifestEntry> modManifestEntries = CommonLogic.getAssetJson(context, "mods_manifest.json", new TypeToken<List<ModManifestEntry>>() {
|
||||
}.getType());
|
||||
if(modManifestEntries == null)
|
||||
return false;
|
||||
File modFolder = new File(Environment.getExternalStorageDirectory(), Constants.MOD_PATH);
|
||||
ImmutableListMultimap<String, ModManifestEntry> installedModMap = Multimaps.index(findAllInstalledMods(), ModManifestEntry::getUniqueID);
|
||||
for (ModManifestEntry mod : modManifestEntries) {
|
||||
if(installedModMap.containsKey(mod.getUniqueID())) {
|
||||
ImmutableList<ModManifestEntry> installedMods = installedModMap.get(mod.getUniqueID());
|
||||
if(installedMods.size() > 1) {
|
||||
CommonLogic.showAlertDialog(root, R.string.error,
|
||||
String.format(context.getString(R.string.duplicate_mod_found),
|
||||
Joiner.on(",").join(Lists.transform(installedMods, ModManifestEntry::getAssetPath))));
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
ZipUtil.unpackEntry(context.getAssets().open(mod.getAssetPath()), mod.getName(), new File(installedMods.get(0).getAssetPath()));
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Install Mod Error", e);
|
||||
}
|
||||
}
|
||||
else {
|
||||
try {
|
||||
ZipUtil.unpack(context.getAssets().open(mod.getAssetPath()), modFolder);
|
||||
} catch (IOException e) {
|
||||
Log.e(TAG, "Install Mod Error", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
package com.zane.smapiinstaller.ui.config;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.zane.smapiinstaller.R;
|
||||
import com.zane.smapiinstaller.logic.CommonLogic;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.navigation.Navigation;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
|
||||
public class ConfigEditFragment extends Fragment {
|
||||
@BindView(R.id.edit_text_config_edit)
|
||||
EditText editText;
|
||||
private Boolean editable;
|
||||
private String configPath;
|
||||
@BindView(R.id.button_config_save)
|
||||
Button buttonConfigSave;
|
||||
@BindView(R.id.button_config_cancel)
|
||||
Button buttonConfigCancel;
|
||||
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
ViewGroup container, Bundle savedInstanceState) {
|
||||
View root = inflater.inflate(R.layout.fragment_config_edit, container, false);
|
||||
ButterKnife.bind(this, root);
|
||||
editable = this.getArguments().getBoolean("editable");
|
||||
if(!editable) {
|
||||
editText.setKeyListener(null);
|
||||
buttonConfigSave.setVisibility(View.INVISIBLE);
|
||||
buttonConfigCancel.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
configPath = this.getArguments().getString("configPath");
|
||||
if(configPath != null) {
|
||||
String fileText = CommonLogic.getFileText(new File(configPath));
|
||||
if(fileText != null) {
|
||||
editText.setText(fileText);
|
||||
}
|
||||
}
|
||||
return root;
|
||||
}
|
||||
@OnClick(R.id.button_config_save) void onConfigSave() {
|
||||
try {
|
||||
new Gson().fromJson(editText.getText().toString(), Object.class);
|
||||
FileOutputStream outputStream = new FileOutputStream(configPath);
|
||||
try(OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream)){
|
||||
outputStreamWriter.write(editText.getText().toString());
|
||||
outputStreamWriter.flush();
|
||||
}
|
||||
}
|
||||
catch (Exception e) {
|
||||
CommonLogic.showAlertDialog(getView(), R.string.error, e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.button_config_cancel) void onConfigCancel() {
|
||||
Navigation.findNavController(getView()).popBackStack();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.zane.smapiinstaller.ui.config;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
|
||||
import com.zane.smapiinstaller.R;
|
||||
import com.zane.smapiinstaller.entity.ModManifestEntry;
|
||||
import com.zane.smapiinstaller.logic.ModAssetsManager;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ConfigFragment extends Fragment {
|
||||
|
||||
private ConfigViewModel configViewModel;
|
||||
|
||||
@BindView(R.id.view_mod_list)
|
||||
RecyclerView recyclerView;
|
||||
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
ViewGroup container, Bundle savedInstanceState) {
|
||||
View root = inflater.inflate(R.layout.fragment_config, container, false);
|
||||
ButterKnife.bind(this, root);
|
||||
configViewModel = new ConfigViewModel(root);
|
||||
recyclerView.setLayoutManager(new LinearLayoutManager(this.getContext()));
|
||||
recyclerView.setAdapter(new ModManifestAdapter(configViewModel.getModList().getValue()));
|
||||
recyclerView.addItemDecoration(new DividerItemDecoration(this.getContext(), DividerItemDecoration.VERTICAL));
|
||||
configViewModel.getModList().observe(getViewLifecycleOwner(), modList -> {
|
||||
recyclerView.getAdapter().notifyDataSetChanged();
|
||||
});
|
||||
return root;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package com.zane.smapiinstaller.ui.config;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
|
||||
import com.zane.smapiinstaller.entity.ModManifestEntry;
|
||||
import com.zane.smapiinstaller.logic.ModAssetsManager;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
public class ConfigViewModel extends ViewModel {
|
||||
|
||||
private MutableLiveData<List<ModManifestEntry>> modList;
|
||||
|
||||
public ConfigViewModel(View root) {
|
||||
ModAssetsManager manager = new ModAssetsManager(root);
|
||||
this.modList = new MutableLiveData<>();
|
||||
List<ModManifestEntry> entryList = manager.findAllInstalledMods();
|
||||
Collections.sort(entryList, (a, b)-> a.getName().compareTo(b.getName()));
|
||||
this.modList.setValue(entryList);
|
||||
}
|
||||
|
||||
public MutableLiveData<List<ModManifestEntry>> getModList() {
|
||||
return modList;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package com.zane.smapiinstaller.ui.config;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.zane.smapiinstaller.R;
|
||||
import com.zane.smapiinstaller.entity.ModManifestEntry;
|
||||
import com.zane.smapiinstaller.logic.CommonLogic;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import butterknife.BindView;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
|
||||
public class ModManifestAdapter extends RecyclerView.Adapter<ModManifestAdapter.ViewHolder> {
|
||||
private List<ModManifestEntry> modList;
|
||||
|
||||
public ModManifestAdapter(List<ModManifestEntry> modList){
|
||||
this.modList=modList;
|
||||
}
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view= LayoutInflater.from(parent.getContext()).inflate(R.layout.mod_list_item,null);
|
||||
return new ViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
ModManifestEntry mod = modList.get(position);
|
||||
holder.modName.setText(mod.getName());
|
||||
holder.modDescription.setText(mod.getDescription());
|
||||
holder.setModPath(mod.getAssetPath());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return modList.size();
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder{
|
||||
private String modPath;
|
||||
public void setModPath(String modPath) {
|
||||
this.modPath = modPath;
|
||||
File file = new File(modPath, "config.json");
|
||||
if(!file.exists()) {
|
||||
configModButton.setVisibility(View.INVISIBLE);
|
||||
}
|
||||
else {
|
||||
configModButton.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
@BindView(R.id.button_config_mod)
|
||||
Button configModButton;
|
||||
@BindView(R.id.text_view_mod_name)
|
||||
TextView modName;
|
||||
@BindView(R.id.text_view_mod_description)
|
||||
TextView modDescription;
|
||||
ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
ButterKnife.bind(this, itemView);
|
||||
}
|
||||
@OnClick(R.id.button_remove_mod) void removeMod() {
|
||||
CommonLogic.showConfirmDialog(itemView, R.string.confirm, R.string.confirm_delete_mod, (dialog, which)->{
|
||||
switch (which){
|
||||
case POSITIVE:
|
||||
File file = new File(modPath);
|
||||
if(file.exists()) {
|
||||
file.delete();
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
@OnClick(R.id.button_config_mod) void configMod() {
|
||||
File file = new File(modPath, "config.json");
|
||||
if(file.exists()) {
|
||||
NavController controller = Navigation.findNavController(itemView);
|
||||
ConfigFragmentDirections.ActionNavConfigToConfigEditFragment action = ConfigFragmentDirections.actionNavConfigToConfigEditFragment(file.getAbsolutePath());
|
||||
controller.navigate(action);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package com.zane.smapiinstaller.ui.help;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.Navigation;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
|
||||
import com.zane.smapiinstaller.R;
|
||||
import com.zane.smapiinstaller.constant.Constants;
|
||||
import com.zane.smapiinstaller.logic.CommonLogic;
|
||||
import com.zane.smapiinstaller.ui.config.ConfigFragmentDirections;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class HelpFragment extends Fragment {
|
||||
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
ViewGroup container, Bundle savedInstanceState) {
|
||||
View root = inflater.inflate(R.layout.fragment_help, container, false);
|
||||
ButterKnife.bind(this, root);
|
||||
return root;
|
||||
}
|
||||
@OnClick(R.id.button_compat) void compat() {
|
||||
CommonLogic.openUrl(this.getContext(), "https://smapi.io/mods");
|
||||
}
|
||||
@OnClick(R.id.button_nexus) void nexus() {
|
||||
CommonLogic.openUrl(this.getContext(), "https://www.nexusmods.com/stardewvalley/mods/");
|
||||
}
|
||||
@OnClick(R.id.button_release) void release() {
|
||||
CommonLogic.showConfirmDialog(this.getView(), R.string.confirm, R.string.test_message, (dialog, which)-> {
|
||||
switch (which) {
|
||||
case POSITIVE:
|
||||
if(this.getString(R.string.test_message).contains("860453392")) {
|
||||
CommonLogic.openUrl(this.getContext(), "mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26k%3D" + "AAflCLHiWw1haM1obu_f-CpGsETxXc6b");
|
||||
}
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
@OnClick({R.id.button_logs}) void showLog() {
|
||||
NavController controller = Navigation.findNavController(this.getView());
|
||||
File logFile = new File(Environment.getExternalStorageDirectory(), Constants.LOG_PATH);
|
||||
if(logFile.exists()) {
|
||||
HelpFragmentDirections.ActionNavHelpToConfigEditFragment action = HelpFragmentDirections.actionNavHelpToConfigEditFragment(logFile.getAbsolutePath());
|
||||
action.setEditable(false);
|
||||
controller.navigate(action);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package com.zane.smapiinstaller.ui.help;
|
||||
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
public class HelpViewModel extends ViewModel {
|
||||
|
||||
private MutableLiveData<String> mText;
|
||||
|
||||
public HelpViewModel() {
|
||||
mText = new MutableLiveData<>();
|
||||
mText.setValue("This is help fragment");
|
||||
}
|
||||
|
||||
public LiveData<String> getText() {
|
||||
return mText;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package com.zane.smapiinstaller.ui.install;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.afollestad.materialdialogs.GravityEnum;
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
import com.zane.smapiinstaller.R;
|
||||
import com.zane.smapiinstaller.logic.ApkPatcher;
|
||||
import com.zane.smapiinstaller.logic.CommonLogic;
|
||||
import com.zane.smapiinstaller.logic.ModAssetsManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
|
||||
public class InstallFragment extends Fragment {
|
||||
|
||||
private Context context;
|
||||
|
||||
private Thread task;
|
||||
|
||||
private View root;
|
||||
|
||||
public View onCreateView(@NonNull LayoutInflater inflater,
|
||||
ViewGroup container, Bundle savedInstanceState) {
|
||||
root = inflater.inflate(R.layout.fragment_install, container, false);
|
||||
ButterKnife.bind(this, root);
|
||||
context = this.getActivity();
|
||||
return root;
|
||||
}
|
||||
|
||||
@OnClick(R.id.button_install)
|
||||
void Install() {
|
||||
new MaterialDialog.Builder(context).title(R.string.install_progress_title).content(R.string.extracting_package).contentGravity(GravityEnum.CENTER)
|
||||
.progress(false, 100, true).cancelable(false).cancelListener(dialog -> {
|
||||
if (task != null) {
|
||||
task.interrupt();
|
||||
}
|
||||
}).showListener(dialogInterface -> {
|
||||
final MaterialDialog dialog = (MaterialDialog) dialogInterface;
|
||||
if (task != null) {
|
||||
task.interrupt();
|
||||
}
|
||||
task = new Thread(() -> {
|
||||
try {
|
||||
ApkPatcher patcher = new ApkPatcher(context);
|
||||
CommonLogic.setProgressDialogState(root, dialog, R.string.extracting_package, 0);
|
||||
String path = patcher.extract();
|
||||
if (path == null) {
|
||||
CommonLogic.showAlertDialog(root, R.string.error, R.string.error_game_not_found);
|
||||
return;
|
||||
}
|
||||
CommonLogic.setProgressDialogState(root, dialog, R.string.unpacking_smapi_files, 10);
|
||||
if (!CommonLogic.unpackSmapiFiles(context, path, false)) {
|
||||
CommonLogic.showAlertDialog(root, R.string.error, R.string.failed_to_unpack_smapi_files);
|
||||
return;
|
||||
}
|
||||
ModAssetsManager modAssetsManager = new ModAssetsManager(root);
|
||||
CommonLogic.setProgressDialogState(root, dialog, R.string.unpacking_smapi_files, 15);
|
||||
modAssetsManager.installDefaultMods();
|
||||
CommonLogic.setProgressDialogState(root, dialog, R.string.patching_package, 25);
|
||||
if (!patcher.patch(path)) {
|
||||
CommonLogic.showAlertDialog(root, R.string.error, R.string.failed_to_patch_game);
|
||||
return;
|
||||
}
|
||||
CommonLogic.setProgressDialogState(root, dialog, R.string.signing_package, 55);
|
||||
String signPath = patcher.sign(path);
|
||||
if (signPath == null) {
|
||||
CommonLogic.showAlertDialog(root, R.string.error, R.string.failed_to_sign_game);
|
||||
return;
|
||||
}
|
||||
CommonLogic.setProgressDialogState(root, dialog, R.string.installing_package, 99);
|
||||
patcher.install(signPath);
|
||||
dialog.incrementProgress(1);
|
||||
|
||||
} finally {
|
||||
if (!dialog.isCancelled()) {
|
||||
dialog.dismiss();
|
||||
}
|
||||
}
|
||||
});
|
||||
task.start();
|
||||
}).show();
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package net.fornwall.apksigner;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
import org.spongycastle.util.encoders.Base64Encoder;
|
||||
|
||||
/** Base64 encoding handling in a portable way across Android and JSE. */
|
||||
public class Base64 {
|
||||
|
||||
public static String encode(byte[] data) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
try {
|
||||
new Base64Encoder().encode(data, 0, data.length, baos);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
return new String(baos.toByteArray());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
package net.fornwall.apksigner;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.KeyStore;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Vector;
|
||||
|
||||
import org.spongycastle.asn1.ASN1ObjectIdentifier;
|
||||
import org.spongycastle.asn1.x500.style.BCStyle;
|
||||
import org.spongycastle.jce.X509Principal;
|
||||
import org.spongycastle.x509.X509V3CertificateGenerator;
|
||||
|
||||
/** All methods create self-signed certificates. */
|
||||
public class CertCreator {
|
||||
|
||||
/** Helper class for dealing with the distinguished name RDNs. */
|
||||
@SuppressWarnings("serial")
|
||||
public static class DistinguishedNameValues extends LinkedHashMap<ASN1ObjectIdentifier, String> {
|
||||
|
||||
public DistinguishedNameValues() {
|
||||
put(BCStyle.C, null);
|
||||
put(BCStyle.ST, null);
|
||||
put(BCStyle.L, null);
|
||||
put(BCStyle.STREET, null);
|
||||
put(BCStyle.O, null);
|
||||
put(BCStyle.OU, null);
|
||||
put(BCStyle.CN, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String put(ASN1ObjectIdentifier oid, String value) {
|
||||
if (value != null && value.equals(""))
|
||||
value = null;
|
||||
if (containsKey(oid))
|
||||
super.put(oid, value); // preserve original ordering
|
||||
else {
|
||||
super.put(oid, value);
|
||||
// String cn = remove(BCStyle.CN); // CN will always be last.
|
||||
// put(BCStyle.CN,cn);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
public void setCountry(String country) {
|
||||
put(BCStyle.C, country);
|
||||
}
|
||||
|
||||
public void setState(String state) {
|
||||
put(BCStyle.ST, state);
|
||||
}
|
||||
|
||||
public void setLocality(String locality) {
|
||||
put(BCStyle.L, locality);
|
||||
}
|
||||
|
||||
public void setStreet(String street) {
|
||||
put(BCStyle.STREET, street);
|
||||
}
|
||||
|
||||
public void setOrganization(String organization) {
|
||||
put(BCStyle.O, organization);
|
||||
}
|
||||
|
||||
public void setOrganizationalUnit(String organizationalUnit) {
|
||||
put(BCStyle.OU, organizationalUnit);
|
||||
}
|
||||
|
||||
public void setCommonName(String commonName) {
|
||||
put(BCStyle.CN, commonName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int size() {
|
||||
int result = 0;
|
||||
for (String value : values()) {
|
||||
if (value != null)
|
||||
result += 1;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
public X509Principal getPrincipal() {
|
||||
Vector<ASN1ObjectIdentifier> oids = new Vector<>();
|
||||
Vector<String> values = new Vector<>();
|
||||
for (Map.Entry<ASN1ObjectIdentifier, String> entry : entrySet()) {
|
||||
if (entry.getValue() != null && !entry.getValue().equals("")) {
|
||||
oids.add(entry.getKey());
|
||||
values.add(entry.getValue());
|
||||
}
|
||||
}
|
||||
return new X509Principal(oids, values);
|
||||
}
|
||||
}
|
||||
|
||||
public static KeySet createKeystoreAndKey(String storePath, char[] storePass, String keyAlgorithm, int keySize,
|
||||
String keyName, char[] keyPass, String certSignatureAlgorithm, int certValidityYears,
|
||||
DistinguishedNameValues distinguishedNameValues) {
|
||||
try {
|
||||
KeySet keySet = createKey(keyAlgorithm, keySize, certSignatureAlgorithm, certValidityYears,
|
||||
distinguishedNameValues);
|
||||
|
||||
KeyStore privateKS = KeyStoreFileManager.createKeyStore(storePass);
|
||||
privateKS.setKeyEntry(keyName, keySet.privateKey, keyPass,
|
||||
new java.security.cert.Certificate[] { keySet.publicKey });
|
||||
|
||||
File sfile = new File(storePath);
|
||||
if (sfile.exists()) {
|
||||
throw new IOException("File already exists: " + storePath);
|
||||
}
|
||||
KeyStoreFileManager.writeKeyStore(privateKS, storePath, storePass);
|
||||
|
||||
return keySet;
|
||||
} catch (RuntimeException x) {
|
||||
throw x;
|
||||
} catch (Exception x) {
|
||||
throw new RuntimeException(x.getMessage(), x);
|
||||
}
|
||||
}
|
||||
|
||||
private static KeySet createKey(String keyAlgorithm, int keySize, String certSignatureAlgorithm,
|
||||
int certValidityYears, DistinguishedNameValues distinguishedNameValues) {
|
||||
try {
|
||||
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(keyAlgorithm);
|
||||
keyPairGenerator.initialize(keySize);
|
||||
KeyPair KPair = keyPairGenerator.generateKeyPair();
|
||||
|
||||
X509V3CertificateGenerator v3CertGen = new X509V3CertificateGenerator();
|
||||
X509Principal principal = distinguishedNameValues.getPrincipal();
|
||||
|
||||
// generate a positive serial number
|
||||
BigInteger serialNumber = BigInteger.valueOf(new SecureRandom().nextInt());
|
||||
while (serialNumber.compareTo(BigInteger.ZERO) < 0)
|
||||
serialNumber = BigInteger.valueOf(new SecureRandom().nextInt());
|
||||
v3CertGen.setSerialNumber(serialNumber);
|
||||
v3CertGen.setIssuerDN(principal);
|
||||
v3CertGen.setNotBefore(new Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L));
|
||||
v3CertGen.setNotAfter(
|
||||
new Date(System.currentTimeMillis() + (1000L * 60L * 60L * 24L * 366L * certValidityYears)));
|
||||
v3CertGen.setSubjectDN(principal);
|
||||
v3CertGen.setPublicKey(KPair.getPublic());
|
||||
v3CertGen.setSignatureAlgorithm(certSignatureAlgorithm);
|
||||
|
||||
X509Certificate PKCertificate = v3CertGen.generate(KPair.getPrivate(),
|
||||
KeyStoreFileManager.SECURITY_PROVIDER.getName());
|
||||
return new KeySet(PKCertificate, KPair.getPrivate(), null);
|
||||
} catch (Exception x) {
|
||||
throw new RuntimeException(x.getMessage(), x);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,478 @@
|
|||
/* JKS.java -- implementation of the "JKS" key store.
|
||||
Copyright (C) 2003 Casey Marshall <rsdio@metastatic.org>
|
||||
|
||||
Permission to use, copy, modify, distribute, and sell this software and
|
||||
its documentation for any purpose is hereby granted without fee,
|
||||
provided that the above copyright notice appear in all copies and that
|
||||
both that copyright notice and this permission notice appear in
|
||||
supporting documentation. No representations are made about the
|
||||
suitability of this software for any purpose. It is provided "as is"
|
||||
without express or implied warranty.
|
||||
|
||||
This program was derived by reverse-engineering Sun's own
|
||||
implementation, using only the public API that is available in the 1.4.1
|
||||
JDK. Hence nothing in this program is, or is derived from, anything
|
||||
copyrighted by Sun Microsystems. While the "Binary Evaluation License
|
||||
Agreement" that the JDK is licensed under contains blanket statements
|
||||
that forbid reverse-engineering (among other things), it is my position
|
||||
that US copyright law does not and cannot forbid reverse-engineering of
|
||||
software to produce a compatible implementation. There are, in fact,
|
||||
numerous clauses in copyright law that specifically allow
|
||||
reverse-engineering, and therefore I believe it is outside of Sun's
|
||||
power to enforce restrictions on reverse-engineering of their software,
|
||||
and it is irresponsible for them to claim they can. */
|
||||
|
||||
package net.fornwall.apksigner;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.DigestInputStream;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.Key;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.KeyStoreException;
|
||||
import java.security.KeyStoreSpi;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.UnrecoverableKeyException;
|
||||
import java.security.cert.Certificate;
|
||||
import java.security.cert.CertificateException;
|
||||
import java.security.cert.CertificateFactory;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.util.Date;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Vector;
|
||||
|
||||
import javax.crypto.EncryptedPrivateKeyInfo;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
/**
|
||||
* This is an implementation of Sun's proprietary key store algorithm, called "JKS" for "Java Key Store". This
|
||||
* implementation was created entirely through reverse-engineering.
|
||||
*
|
||||
* <p>
|
||||
* The format of JKS files is, from the start of the file:
|
||||
*
|
||||
* <ol>
|
||||
* <li>Magic bytes. This is a four-byte integer, in big-endian byte order, equal to <code>0xFEEDFEED</code>.</li>
|
||||
* <li>The version number (probably), as a four-byte integer (all multibyte integral types are in big-endian byte
|
||||
* order). The current version number (in modern distributions of the JDK) is 2.</li>
|
||||
* <li>The number of entries in this keystore, as a four-byte integer. Call this value <i>n</i></li>
|
||||
* <li>Then, <i>n</i> times:
|
||||
* <ol>
|
||||
* <li>The entry type, a four-byte int. The value 1 denotes a private key entry, and 2 denotes a trusted certificate.</li>
|
||||
* <li>The entry's alias, formatted as strings such as those written by <a
|
||||
* href="http://java.sun.com/j2se/1.4.1/docs/api/java/io/DataOutput.html#writeUTF(java.lang.String)"
|
||||
* >DataOutput.writeUTF(String)</a>.</li>
|
||||
* <li>An eight-byte integer, representing the entry's creation date, in milliseconds since the epoch.
|
||||
*
|
||||
* <p>
|
||||
* Then, if the entry is a private key entry:
|
||||
* <ol>
|
||||
* <li>The size of the encoded key as a four-byte int, then that number of bytes. The encoded key is the DER encoded
|
||||
* bytes of the <a
|
||||
* href="http://java.sun.com/j2se/1.4.1/docs/api/javax/crypto/EncryptedPrivateKeyInfo.html">EncryptedPrivateKeyInfo</a>
|
||||
* structure (the encryption algorithm is discussed later).</li>
|
||||
* <li>A four-byte integer, followed by that many encoded certificates, encoded as described in the trusted certificates
|
||||
* section.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>
|
||||
* Otherwise, the entry is a trusted certificate, which is encoded as the name of the encoding algorithm (e.g. X.509),
|
||||
* encoded the same way as alias names. Then, a four-byte integer representing the size of the encoded certificate, then
|
||||
* that many bytes representing the encoded certificate (e.g. the DER bytes in the case of X.509).</li>
|
||||
* </ol>
|
||||
* </li>
|
||||
* <li>Then, the signature.</li>
|
||||
* </ol>
|
||||
* </ol> </li> </ol>
|
||||
*
|
||||
* <p>
|
||||
* (See <a href="http://metastatic.org/source/genkey.java">this file</a> for some idea of how I was able to figure out these algorithms)
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Decrypting the key works as follows:
|
||||
*
|
||||
* <ol>
|
||||
* <li>The key length is the length of the ciphertext minus 40. The encrypted key, <code>ekey</code>, is the middle
|
||||
* bytes of the ciphertext.</li>
|
||||
* <li>Take the first 20 bytes of the encrypted key as a seed value, <code>K[0]</code>.</li>
|
||||
* <li>Compute <code>K[1] ... K[n]</code>, where <code>|K[i]| = 20</code>, <code>n = ceil(|ekey| / 20)</code>, and
|
||||
* <code>K[i] = SHA-1(UTF-16BE(password) + K[i-1])</code>.</li>
|
||||
* <li><code>key = ekey ^ (K[1] + ... + K[n])</code>.</li>
|
||||
* <li>The last 20 bytes are the checksum, computed as <code>H =
|
||||
* SHA-1(UTF-16BE(password) + key)</code>. If this value does not match the last 20 bytes of the ciphertext, output
|
||||
* <code>FAIL</code>. Otherwise, output <code>key</code>.</li>
|
||||
* </ol>
|
||||
*
|
||||
* <p>
|
||||
* The signature is defined as <code>SHA-1(UTF-16BE(password) +
|
||||
* US_ASCII("Mighty Aphrodite") + encoded_keystore)</code> (yup, Sun engineers are just that clever).
|
||||
*
|
||||
* <p>
|
||||
* (Above, SHA-1 denotes the secure hash algorithm, UTF-16BE the big-endian byte representation of a UTF-16 string, and
|
||||
* US_ASCII the ASCII byte representation of the string.)
|
||||
*
|
||||
* <p>
|
||||
* The original source code by Casey Marshall of this class should be available in the file <a
|
||||
* href="http://metastatic.org/source/JKS.java">http://metastatic.org/source/JKS.java</a>.
|
||||
*
|
||||
* <p>
|
||||
* Changes by Ken Ellinwood:
|
||||
* <ul>
|
||||
* <li>Fixed a NullPointerException in engineLoad(). This method must return gracefully if the keystore input stream is
|
||||
* null.</li>
|
||||
* <li>engineGetCertificateEntry() was updated to return the first cert in the chain for private key entries.</li>
|
||||
* <li>Lowercase the alias names, otherwise keytool chokes on the file created by this code.</li>
|
||||
* <li>Fixed the integrity check in engineLoad(), previously the exception was never thrown regardless of password
|
||||
* value.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @author Casey Marshall (rsdio@metastatic.org)
|
||||
* @author Ken Ellinwood
|
||||
*/
|
||||
public class JKS extends KeyStoreSpi {
|
||||
|
||||
/** Ah, Sun. So goddamned clever with those magic bytes. */
|
||||
private static final int MAGIC = 0xFEEDFEED;
|
||||
|
||||
private static final int PRIVATE_KEY = 1;
|
||||
private static final int TRUSTED_CERT = 2;
|
||||
|
||||
private final Vector<String> aliases = new Vector<>();
|
||||
private final HashMap<String, Certificate> trustedCerts = new HashMap<>();
|
||||
private final HashMap<String, byte[]> privateKeys = new HashMap<>();
|
||||
private final HashMap<String, Certificate[]> certChains = new HashMap<>();
|
||||
private final HashMap<String, Date> dates = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Key engineGetKey(String alias, char[] password) throws NoSuchAlgorithmException, UnrecoverableKeyException {
|
||||
alias = alias.toLowerCase();
|
||||
|
||||
if (!privateKeys.containsKey(alias))
|
||||
return null;
|
||||
byte[] key = decryptKey(privateKeys.get(alias), charsToBytes(password));
|
||||
Certificate[] chain = engineGetCertificateChain(alias);
|
||||
if (chain.length > 0) {
|
||||
try {
|
||||
// Private and public keys MUST have the same algorithm.
|
||||
KeyFactory fact = KeyFactory.getInstance(chain[0].getPublicKey().getAlgorithm());
|
||||
return fact.generatePrivate(new PKCS8EncodedKeySpec(key));
|
||||
} catch (InvalidKeySpecException x) {
|
||||
throw new UnrecoverableKeyException(x.getMessage());
|
||||
}
|
||||
} else
|
||||
return new SecretKeySpec(key, alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Certificate[] engineGetCertificateChain(String alias) {
|
||||
return certChains.get(alias.toLowerCase());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Certificate engineGetCertificate(String alias) {
|
||||
alias = alias.toLowerCase();
|
||||
if (engineIsKeyEntry(alias)) {
|
||||
Certificate[] certChain = certChains.get(alias);
|
||||
if (certChain != null && certChain.length > 0)
|
||||
return certChain[0];
|
||||
}
|
||||
return trustedCerts.get(alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date engineGetCreationDate(String alias) {
|
||||
alias = alias.toLowerCase();
|
||||
return dates.get(alias);
|
||||
}
|
||||
|
||||
// XXX implement writing methods.
|
||||
@Override
|
||||
public void engineSetKeyEntry(String alias, Key key, char[] passwd, Certificate[] certChain)
|
||||
throws KeyStoreException {
|
||||
alias = alias.toLowerCase();
|
||||
if (trustedCerts.containsKey(alias))
|
||||
throw new KeyStoreException("\"" + alias + " is a trusted certificate entry");
|
||||
privateKeys.put(alias, encryptKey(key, charsToBytes(passwd)));
|
||||
if (certChain != null)
|
||||
certChains.put(alias, certChain);
|
||||
else
|
||||
certChains.put(alias, new Certificate[0]);
|
||||
if (!aliases.contains(alias)) {
|
||||
dates.put(alias, new Date());
|
||||
aliases.add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@Override
|
||||
public void engineSetKeyEntry(String alias, byte[] encodedKey, Certificate[] certChain) throws KeyStoreException {
|
||||
alias = alias.toLowerCase();
|
||||
if (trustedCerts.containsKey(alias))
|
||||
throw new KeyStoreException("\"" + alias + "\" is a trusted certificate entry");
|
||||
try {
|
||||
new EncryptedPrivateKeyInfo(encodedKey);
|
||||
} catch (IOException ioe) {
|
||||
throw new KeyStoreException("encoded key is not an EncryptedPrivateKeyInfo");
|
||||
}
|
||||
privateKeys.put(alias, encodedKey);
|
||||
if (certChain != null)
|
||||
certChains.put(alias, certChain);
|
||||
else
|
||||
certChains.put(alias, new Certificate[0]);
|
||||
if (!aliases.contains(alias)) {
|
||||
dates.put(alias, new Date());
|
||||
aliases.add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void engineSetCertificateEntry(String alias, Certificate cert) throws KeyStoreException {
|
||||
alias = alias.toLowerCase();
|
||||
if (privateKeys.containsKey(alias))
|
||||
throw new KeyStoreException("\"" + alias + "\" is a private key entry");
|
||||
if (cert == null)
|
||||
throw new NullPointerException();
|
||||
trustedCerts.put(alias, cert);
|
||||
if (!aliases.contains(alias)) {
|
||||
dates.put(alias, new Date());
|
||||
aliases.add(alias);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void engineDeleteEntry(String alias) throws KeyStoreException {
|
||||
alias = alias.toLowerCase();
|
||||
aliases.remove(alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Enumeration<String> engineAliases() {
|
||||
return aliases.elements();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean engineContainsAlias(String alias) {
|
||||
alias = alias.toLowerCase();
|
||||
return aliases.contains(alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int engineSize() {
|
||||
return aliases.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean engineIsKeyEntry(String alias) {
|
||||
alias = alias.toLowerCase();
|
||||
return privateKeys.containsKey(alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean engineIsCertificateEntry(String alias) {
|
||||
alias = alias.toLowerCase();
|
||||
return trustedCerts.containsKey(alias);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String engineGetCertificateAlias(Certificate cert) {
|
||||
for (String alias : trustedCerts.keySet())
|
||||
if (cert.equals(trustedCerts.get(alias)))
|
||||
return alias;
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void engineStore(OutputStream out, char[] passwd) throws IOException, NoSuchAlgorithmException,
|
||||
CertificateException {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
md.update(charsToBytes(passwd));
|
||||
md.update("Mighty Aphrodite".getBytes(StandardCharsets.UTF_8));
|
||||
DataOutputStream dout = new DataOutputStream(new DigestOutputStream(out, md));
|
||||
dout.writeInt(MAGIC);
|
||||
dout.writeInt(2);
|
||||
dout.writeInt(aliases.size());
|
||||
for (Enumeration<String> e = aliases.elements(); e.hasMoreElements();) {
|
||||
String alias = e.nextElement();
|
||||
if (trustedCerts.containsKey(alias)) {
|
||||
dout.writeInt(TRUSTED_CERT);
|
||||
dout.writeUTF(alias);
|
||||
dout.writeLong(dates.get(alias).getTime());
|
||||
writeCert(dout, trustedCerts.get(alias));
|
||||
} else {
|
||||
dout.writeInt(PRIVATE_KEY);
|
||||
dout.writeUTF(alias);
|
||||
dout.writeLong(dates.get(alias).getTime());
|
||||
byte[] key = privateKeys.get(alias);
|
||||
dout.writeInt(key.length);
|
||||
dout.write(key);
|
||||
Certificate[] chain = certChains.get(alias);
|
||||
dout.writeInt(chain.length);
|
||||
for (int i = 0; i < chain.length; i++)
|
||||
writeCert(dout, chain[i]);
|
||||
}
|
||||
}
|
||||
byte[] digest = md.digest();
|
||||
dout.write(digest);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void engineLoad(InputStream in, char[] passwd) throws IOException, NoSuchAlgorithmException,
|
||||
CertificateException {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA");
|
||||
if (passwd != null)
|
||||
md.update(charsToBytes(passwd));
|
||||
md.update("Mighty Aphrodite".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
aliases.clear();
|
||||
trustedCerts.clear();
|
||||
privateKeys.clear();
|
||||
certChains.clear();
|
||||
dates.clear();
|
||||
if (in == null)
|
||||
return;
|
||||
DataInputStream din = new DataInputStream(new DigestInputStream(in, md));
|
||||
if (din.readInt() != MAGIC)
|
||||
throw new IOException("not a JavaKeyStore");
|
||||
din.readInt(); // version no.
|
||||
final int n = din.readInt();
|
||||
aliases.ensureCapacity(n);
|
||||
if (n < 0)
|
||||
throw new LoadKeystoreException("Malformed key store");
|
||||
for (int i = 0; i < n; i++) {
|
||||
int type = din.readInt();
|
||||
String alias = din.readUTF();
|
||||
aliases.add(alias);
|
||||
dates.put(alias, new Date(din.readLong()));
|
||||
switch (type) {
|
||||
case PRIVATE_KEY:
|
||||
int len = din.readInt();
|
||||
byte[] encoded = new byte[len];
|
||||
din.read(encoded);
|
||||
privateKeys.put(alias, encoded);
|
||||
int count = din.readInt();
|
||||
Certificate[] chain = new Certificate[count];
|
||||
for (int j = 0; j < count; j++)
|
||||
chain[j] = readCert(din);
|
||||
certChains.put(alias, chain);
|
||||
break;
|
||||
|
||||
case TRUSTED_CERT:
|
||||
trustedCerts.put(alias, readCert(din));
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new LoadKeystoreException("Malformed key store");
|
||||
}
|
||||
}
|
||||
|
||||
if (passwd != null) {
|
||||
byte[] computedHash = md.digest();
|
||||
byte[] storedHash = new byte[20];
|
||||
din.read(storedHash);
|
||||
if (!MessageDigest.isEqual(storedHash, computedHash)) {
|
||||
throw new LoadKeystoreException("Incorrect password, or integrity check failed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Own methods.
|
||||
// ------------------------------------------------------------------------
|
||||
|
||||
private static Certificate readCert(DataInputStream in) throws IOException, CertificateException {
|
||||
String type = in.readUTF();
|
||||
int len = in.readInt();
|
||||
byte[] encoded = new byte[len];
|
||||
in.read(encoded);
|
||||
CertificateFactory factory = CertificateFactory.getInstance(type);
|
||||
return factory.generateCertificate(new ByteArrayInputStream(encoded));
|
||||
}
|
||||
|
||||
private static void writeCert(DataOutputStream dout, Certificate cert) throws IOException, CertificateException {
|
||||
dout.writeUTF(cert.getType());
|
||||
byte[] b = cert.getEncoded();
|
||||
dout.writeInt(b.length);
|
||||
dout.write(b);
|
||||
}
|
||||
|
||||
private static byte[] decryptKey(byte[] encryptedPKI, byte[] passwd) throws UnrecoverableKeyException {
|
||||
try {
|
||||
EncryptedPrivateKeyInfo epki = new EncryptedPrivateKeyInfo(encryptedPKI);
|
||||
byte[] encr = epki.getEncryptedData();
|
||||
byte[] keystream = new byte[20];
|
||||
System.arraycopy(encr, 0, keystream, 0, 20);
|
||||
byte[] check = new byte[20];
|
||||
System.arraycopy(encr, encr.length - 20, check, 0, 20);
|
||||
byte[] key = new byte[encr.length - 40];
|
||||
MessageDigest sha = MessageDigest.getInstance("SHA1");
|
||||
int count = 0;
|
||||
while (count < key.length) {
|
||||
sha.reset();
|
||||
sha.update(passwd);
|
||||
sha.update(keystream);
|
||||
sha.digest(keystream, 0, keystream.length);
|
||||
for (int i = 0; i < keystream.length && count < key.length; i++) {
|
||||
key[count] = (byte) (keystream[i] ^ encr[count + 20]);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
sha.reset();
|
||||
sha.update(passwd);
|
||||
sha.update(key);
|
||||
if (!MessageDigest.isEqual(check, sha.digest()))
|
||||
throw new UnrecoverableKeyException("checksum mismatch");
|
||||
return key;
|
||||
} catch (Exception x) {
|
||||
throw new UnrecoverableKeyException(x.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] encryptKey(Key key, byte[] passwd) throws KeyStoreException {
|
||||
try {
|
||||
MessageDigest sha = MessageDigest.getInstance("SHA1");
|
||||
// SecureRandom rand = SecureRandom.getInstance("SHA1PRNG");
|
||||
byte[] k = key.getEncoded();
|
||||
byte[] encrypted = new byte[k.length + 40];
|
||||
byte[] keystream = SecureRandom.getSeed(20);
|
||||
System.arraycopy(keystream, 0, encrypted, 0, 20);
|
||||
int count = 0;
|
||||
while (count < k.length) {
|
||||
sha.reset();
|
||||
sha.update(passwd);
|
||||
sha.update(keystream);
|
||||
sha.digest(keystream, 0, keystream.length);
|
||||
for (int i = 0; i < keystream.length && count < k.length; i++) {
|
||||
encrypted[count + 20] = (byte) (keystream[i] ^ k[count]);
|
||||
count++;
|
||||
}
|
||||
}
|
||||
sha.reset();
|
||||
sha.update(passwd);
|
||||
sha.update(k);
|
||||
sha.digest(encrypted, encrypted.length - 20, 20);
|
||||
// 1.3.6.1.4.1.42.2.17.1.1 is Sun's private OID for this encryption algorithm.
|
||||
return new EncryptedPrivateKeyInfo("1.3.6.1.4.1.42.2.17.1.1", encrypted).getEncoded();
|
||||
} catch (Exception x) {
|
||||
throw new KeyStoreException(x.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] charsToBytes(char[] passwd) {
|
||||
byte[] buf = new byte[passwd.length * 2];
|
||||
for (int i = 0, j = 0; i < passwd.length; i++) {
|
||||
buf[j++] = (byte) (passwd[i] >>> 8);
|
||||
buf[j++] = (byte) passwd[i];
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package net.fornwall.apksigner;
|
||||
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
|
||||
public class KeySet {
|
||||
|
||||
/** Certificate. */
|
||||
public final X509Certificate publicKey;
|
||||
/** Private key. */
|
||||
public final PrivateKey privateKey;
|
||||
public final String signatureAlgorithm;
|
||||
|
||||
public KeySet(X509Certificate publicKey, PrivateKey privateKey, String signatureAlgorithm) {
|
||||
this.publicKey = publicKey;
|
||||
this.privateKey = privateKey;
|
||||
this.signatureAlgorithm = (signatureAlgorithm != null) ? signatureAlgorithm : "SHA1withRSA";
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
package net.fornwall.apksigner;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.security.KeyStore;
|
||||
import java.security.Provider;
|
||||
import java.security.Security;
|
||||
|
||||
import org.spongycastle.jce.provider.BouncyCastleProvider;
|
||||
|
||||
public class KeyStoreFileManager {
|
||||
|
||||
public static final Provider SECURITY_PROVIDER = new BouncyCastleProvider();
|
||||
|
||||
static {
|
||||
// Add the spongycastle version of the BC provider so that the implementation classes returned from the keystore
|
||||
// are all from the spongycastle libs.
|
||||
Security.addProvider(SECURITY_PROVIDER);
|
||||
}
|
||||
|
||||
public static class JksKeyStore extends KeyStore {
|
||||
public JksKeyStore() {
|
||||
super(new JKS(), SECURITY_PROVIDER, "jks");
|
||||
}
|
||||
}
|
||||
|
||||
public static KeyStore createKeyStore(char[] password) throws Exception {
|
||||
KeyStore ks = new JksKeyStore();
|
||||
ks.load(null, password);
|
||||
return ks;
|
||||
}
|
||||
|
||||
public static KeyStore loadKeyStore(String keystorePath, char[] password) throws Exception {
|
||||
KeyStore ks = new JksKeyStore();
|
||||
try (FileInputStream fis = new FileInputStream(keystorePath)) {
|
||||
ks.load(fis, password);
|
||||
}
|
||||
return ks;
|
||||
}
|
||||
|
||||
public static void writeKeyStore(KeyStore ks, String keystorePath, char[] password) throws Exception {
|
||||
File keystoreFile = new File(keystorePath);
|
||||
try {
|
||||
if (keystoreFile.exists()) {
|
||||
// I've had some trouble saving new versions of the key store file in which the file becomes
|
||||
// empty/corrupt. Saving the new version to a new file and creating a backup of the old version.
|
||||
File tmpFile = File.createTempFile(keystoreFile.getName(), null, keystoreFile.getParentFile());
|
||||
try (FileOutputStream fos = new FileOutputStream(tmpFile)) {
|
||||
ks.store(fos, password);
|
||||
}
|
||||
/*
|
||||
* create a backup of the previous version int i = 1; File backup = new File( keystorePath + "." + i +
|
||||
* ".bak"); while (backup.exists()) { i += 1; backup = new File( keystorePath + "." + i + ".bak"); }
|
||||
* renameTo(keystoreFile, backup);
|
||||
*/
|
||||
renameTo(tmpFile, keystoreFile);
|
||||
} else {
|
||||
try (FileOutputStream fos = new FileOutputStream(keystorePath)) {
|
||||
ks.store(fos, password);
|
||||
}
|
||||
}
|
||||
} catch (Exception x) {
|
||||
try {
|
||||
File logfile = File.createTempFile("zipsigner-error", ".log", keystoreFile.getParentFile());
|
||||
try (PrintWriter pw = new PrintWriter(new FileWriter(logfile))) {
|
||||
x.printStackTrace(pw);
|
||||
}
|
||||
} catch (Exception y) {
|
||||
}
|
||||
throw x;
|
||||
}
|
||||
}
|
||||
|
||||
static void copyFile(File srcFile, File destFile, boolean preserveFileDate) throws IOException {
|
||||
if (destFile.exists() && destFile.isDirectory())
|
||||
throw new IOException("Destination '" + destFile + "' exists but is a directory");
|
||||
try (FileInputStream input = new FileInputStream(srcFile)) {
|
||||
try (FileOutputStream output = new FileOutputStream(destFile)) {
|
||||
byte[] buffer = new byte[4096];
|
||||
int n = 0;
|
||||
while (-1 != (n = input.read(buffer))) {
|
||||
output.write(buffer, 0, n);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (srcFile.length() != destFile.length()) {
|
||||
throw new IOException("Failed to copy full contents from '" + srcFile + "' to '" + destFile + "'");
|
||||
}
|
||||
if (preserveFileDate)
|
||||
destFile.setLastModified(srcFile.lastModified());
|
||||
}
|
||||
|
||||
public static void renameTo(File fromFile, File toFile) throws IOException {
|
||||
copyFile(fromFile, toFile, true);
|
||||
if (!fromFile.delete())
|
||||
throw new IOException("Failed to delete " + fromFile);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package net.fornwall.apksigner;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/** Thrown by JKS.engineLoad() for errors that occur after determining the keystore is actually a JKS keystore. */
|
||||
@SuppressWarnings("serial")
|
||||
public class LoadKeystoreException extends IOException {
|
||||
|
||||
public LoadKeystoreException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package net.fornwall.apksigner;
|
||||
|
||||
import org.spongycastle.cert.jcajce.JcaCertStore;
|
||||
import org.spongycastle.cms.*;
|
||||
import org.spongycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder;
|
||||
import org.spongycastle.operator.ContentSigner;
|
||||
import org.spongycastle.operator.DigestCalculatorProvider;
|
||||
import org.spongycastle.operator.jcajce.JcaContentSignerBuilder;
|
||||
import org.spongycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder;
|
||||
import org.spongycastle.util.Store;
|
||||
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class SignatureBlockGenerator {
|
||||
|
||||
/**
|
||||
* Sign the given content using the private and public keys from the keySet and return the encoded CMS (PKCS#7)
|
||||
* data. Use of direct signature and DER encoding produces a block that is verifiable by Android recovery programs.
|
||||
*/
|
||||
public static byte[] generate(KeySet keySet, byte[] content) {
|
||||
try {
|
||||
List<X509Certificate> certList = new ArrayList<>();
|
||||
CMSTypedData msg = new CMSProcessableByteArray(content);
|
||||
|
||||
certList.add(keySet.publicKey);
|
||||
|
||||
Store certs = new JcaCertStore(certList);
|
||||
|
||||
CMSSignedDataGenerator gen = new CMSSignedDataGenerator();
|
||||
|
||||
JcaContentSignerBuilder jcaContentSignerBuilder = new JcaContentSignerBuilder(keySet.signatureAlgorithm)
|
||||
.setProvider("SC");
|
||||
ContentSigner sha1Signer = jcaContentSignerBuilder.build(keySet.privateKey);
|
||||
|
||||
JcaDigestCalculatorProviderBuilder jcaDigestCalculatorProviderBuilder = new JcaDigestCalculatorProviderBuilder()
|
||||
.setProvider("SC");
|
||||
DigestCalculatorProvider digestCalculatorProvider = jcaDigestCalculatorProviderBuilder.build();
|
||||
|
||||
JcaSignerInfoGeneratorBuilder jcaSignerInfoGeneratorBuilder = new JcaSignerInfoGeneratorBuilder(
|
||||
digestCalculatorProvider);
|
||||
jcaSignerInfoGeneratorBuilder.setDirectSignature(true);
|
||||
SignerInfoGenerator signerInfoGenerator = jcaSignerInfoGeneratorBuilder.build(sha1Signer, keySet.publicKey);
|
||||
|
||||
gen.addSignerInfoGenerator(signerInfoGenerator);
|
||||
gen.addCertificates(certs);
|
||||
|
||||
CMSSignedData sigData = gen.generate(msg, false);
|
||||
return sigData.toASN1Structure().getEncoded("DER");
|
||||
} catch (Exception x) {
|
||||
throw new RuntimeException(x.getMessage(), x);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,186 @@
|
|||
package net.fornwall.apksigner;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.PrintStream;
|
||||
import java.security.DigestOutputStream;
|
||||
import java.security.GeneralSecurityException;
|
||||
import java.security.MessageDigest;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.cert.X509Certificate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.jar.Attributes;
|
||||
import java.util.jar.JarFile;
|
||||
import java.util.jar.Manifest;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import net.fornwall.apksigner.zipio.ZioEntry;
|
||||
import net.fornwall.apksigner.zipio.ZipInput;
|
||||
import net.fornwall.apksigner.zipio.ZipOutput;
|
||||
|
||||
/**
|
||||
* This is a modified copy of com.android.signapk.SignApk.java. It provides an API to sign JAR files (including APKs and
|
||||
* Zip/OTA updates) in a way compatible with the mincrypt verifier, using SHA1 and RSA keys.
|
||||
*/
|
||||
public class ZipSigner {
|
||||
|
||||
static {
|
||||
if (!KeyStoreFileManager.SECURITY_PROVIDER.getName().equals("SC")) {
|
||||
throw new RuntimeException("Invalid security provider");
|
||||
}
|
||||
}
|
||||
|
||||
private static final String CERT_SF_NAME = "META-INF/CERT.SF";
|
||||
private static final String CERT_RSA_NAME = "META-INF/CERT.RSA";
|
||||
|
||||
// Files matching this pattern are not copied to the output.
|
||||
private static final Pattern stripPattern = Pattern.compile("^META-INF/(.*)[.](SF|RSA|DSA)$");
|
||||
|
||||
/** Add the SHA1 of every file to the manifest, creating it if necessary. */
|
||||
private static Manifest addDigestsToManifest(Map<String, ZioEntry> entries)
|
||||
throws IOException, GeneralSecurityException {
|
||||
Manifest input = null;
|
||||
ZioEntry manifestEntry = entries.get(JarFile.MANIFEST_NAME);
|
||||
if (manifestEntry != null) {
|
||||
input = new Manifest();
|
||||
input.read(manifestEntry.getInputStream());
|
||||
}
|
||||
Manifest output = new Manifest();
|
||||
Attributes main = output.getMainAttributes();
|
||||
if (input != null) {
|
||||
main.putAll(input.getMainAttributes());
|
||||
} else {
|
||||
main.putValue("Manifest-Version", "1.0");
|
||||
main.putValue("Created-By", "1.0 (Android SignApk)");
|
||||
}
|
||||
|
||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
byte[] buffer = new byte[512];
|
||||
int num;
|
||||
|
||||
// We sort the input entries by name, and add them to the output manifest in sorted order. We expect that the
|
||||
// output map will be deterministic.
|
||||
TreeMap<String, ZioEntry> byName = new TreeMap<>();
|
||||
byName.putAll(entries);
|
||||
|
||||
// if (debug) getLogger().debug("Manifest entries:");
|
||||
for (ZioEntry entry : byName.values()) {
|
||||
String name = entry.getName();
|
||||
// if (debug) getLogger().debug(name);
|
||||
if (!entry.isDirectory() && !name.equals(JarFile.MANIFEST_NAME) && !name.equals(CERT_SF_NAME)
|
||||
&& !name.equals(CERT_RSA_NAME) && (stripPattern == null || !stripPattern.matcher(name).matches())) {
|
||||
|
||||
InputStream data = entry.getInputStream();
|
||||
while ((num = data.read(buffer)) > 0) {
|
||||
md.update(buffer, 0, num);
|
||||
}
|
||||
|
||||
Attributes attr = null;
|
||||
if (input != null) {
|
||||
java.util.jar.Attributes inAttr = input.getAttributes(name);
|
||||
if (inAttr != null)
|
||||
attr = new Attributes(inAttr);
|
||||
}
|
||||
if (attr == null)
|
||||
attr = new Attributes();
|
||||
attr.putValue("SHA1-Digest", Base64.encode(md.digest()));
|
||||
output.getEntries().put(name, attr);
|
||||
}
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/** Write the signature file to the given output stream. */
|
||||
private static byte[] generateSignatureFile(Manifest manifest) throws IOException, GeneralSecurityException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
out.write(("Signature-Version: 1.0\r\n").getBytes());
|
||||
out.write(("Created-By: 1.0 (Android SignApk)\r\n").getBytes());
|
||||
|
||||
MessageDigest md = MessageDigest.getInstance("SHA1");
|
||||
PrintStream print = new PrintStream(new DigestOutputStream(new ByteArrayOutputStream(), md), true, "UTF-8");
|
||||
|
||||
// Digest of the entire manifest
|
||||
manifest.write(print);
|
||||
print.flush();
|
||||
|
||||
out.write(("SHA1-Digest-Manifest: " + Base64.encode(md.digest()) + "\r\n\r\n").getBytes());
|
||||
|
||||
Map<String, Attributes> entries = manifest.getEntries();
|
||||
for (Map.Entry<String, Attributes> entry : entries.entrySet()) {
|
||||
// Digest of the manifest stanza for this entry.
|
||||
String nameEntry = "Name: " + entry.getKey() + "\r\n";
|
||||
print.print(nameEntry);
|
||||
for (Map.Entry<Object, Object> att : entry.getValue().entrySet()) {
|
||||
print.print(att.getKey() + ": " + att.getValue() + "\r\n");
|
||||
}
|
||||
print.print("\r\n");
|
||||
print.flush();
|
||||
|
||||
out.write(nameEntry.getBytes());
|
||||
out.write(("SHA1-Digest: " + Base64.encode(md.digest()) + "\r\n\r\n").getBytes());
|
||||
}
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sign the file using the given public key cert, private key, and signature block template. The signature block
|
||||
* template parameter may be null, but if so android-sun-jarsign-support.jar must be in the classpath.
|
||||
*/
|
||||
public static void signZip(X509Certificate publicKey, PrivateKey privateKey, String signatureAlgorithm,
|
||||
String inputZipFilename, String outputZipFilename) throws IOException, GeneralSecurityException {
|
||||
KeySet keySet = new KeySet(publicKey, privateKey, signatureAlgorithm);
|
||||
|
||||
File inFile = new File(inputZipFilename).getCanonicalFile();
|
||||
File outFile = new File(outputZipFilename).getCanonicalFile();
|
||||
if (inFile.equals(outFile))
|
||||
throw new IllegalArgumentException("Input and output files are the same");
|
||||
|
||||
try (ZipInput input = new ZipInput(inputZipFilename)) {
|
||||
try (ZipOutput zipOutput = new ZipOutput(new FileOutputStream(outputZipFilename))) {
|
||||
// Assume the certificate is valid for at least an hour.
|
||||
long timestamp = publicKey.getNotBefore().getTime() + 3600L * 1000;
|
||||
|
||||
// MANIFEST.MF
|
||||
Manifest manifest = addDigestsToManifest(input.entries);
|
||||
ZioEntry ze = new ZioEntry(JarFile.MANIFEST_NAME);
|
||||
ze.setTime(timestamp);
|
||||
manifest.write(ze.getOutputStream());
|
||||
zipOutput.write(ze);
|
||||
|
||||
byte[] certSfBytes = generateSignatureFile(manifest);
|
||||
|
||||
// CERT.SF
|
||||
ze = new ZioEntry(CERT_SF_NAME);
|
||||
ze.setTime(timestamp);
|
||||
ze.getOutputStream().write(certSfBytes);
|
||||
zipOutput.write(ze);
|
||||
|
||||
// CERT.RSA
|
||||
ze = new ZioEntry(CERT_RSA_NAME);
|
||||
ze.setTime(timestamp);
|
||||
ze.getOutputStream().write(SignatureBlockGenerator.generate(keySet, certSfBytes));
|
||||
zipOutput.write(ze);
|
||||
|
||||
// Copy all the files in a manifest from input to output. We set the modification times in the output to
|
||||
// a fixed time, so as to reduce variation in the output file and make incremental OTAs more efficient.
|
||||
Map<String, Attributes> entries = manifest.getEntries();
|
||||
List<String> names = new ArrayList<>(entries.keySet());
|
||||
Collections.sort(names);
|
||||
for (String name : names) {
|
||||
ZioEntry inEntry = input.entries.get(name);
|
||||
inEntry.setTime(timestamp);
|
||||
zipOutput.write(inEntry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package net.fornwall.apksigner.zipio;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
class CentralEnd {
|
||||
|
||||
public int signature = 0x06054b50; // end of central dir signature 4 bytes
|
||||
public short numberThisDisk = 0; // number of this disk 2 bytes
|
||||
public short centralStartDisk = 0; // number of the disk with the start of
|
||||
// the central directory 2 bytes
|
||||
public short numCentralEntries; // total number of entries in the central
|
||||
// directory on this disk 2 bytes
|
||||
public short totalCentralEntries; // total number of entries in the central
|
||||
// directory 2 bytes
|
||||
|
||||
public int centralDirectorySize; // size of the central directory 4 bytes
|
||||
public int centralStartOffset; // offset of start of central directory with
|
||||
// respect to the starting disk number 4
|
||||
// bytes
|
||||
public String fileComment; // .ZIP file comment (variable size)
|
||||
|
||||
public static CentralEnd read(ZipInput input) throws IOException {
|
||||
int signature = input.readInt();
|
||||
if (signature != 0x06054b50) {
|
||||
// Back up to the signature.
|
||||
input.seek(input.getFilePointer() - 4);
|
||||
return null;
|
||||
}
|
||||
|
||||
CentralEnd entry = new CentralEnd();
|
||||
entry.numberThisDisk = input.readShort();
|
||||
entry.centralStartDisk = input.readShort();
|
||||
entry.numCentralEntries = input.readShort();
|
||||
entry.totalCentralEntries = input.readShort();
|
||||
entry.centralDirectorySize = input.readInt();
|
||||
entry.centralStartOffset = input.readInt();
|
||||
short zipFileCommentLen = input.readShort();
|
||||
entry.fileComment = input.readString(zipFileCommentLen);
|
||||
return entry;
|
||||
}
|
||||
|
||||
public void write(ZipOutput output) throws IOException {
|
||||
output.writeInt(signature);
|
||||
output.writeShort(numberThisDisk);
|
||||
output.writeShort(centralStartDisk);
|
||||
output.writeShort(numCentralEntries);
|
||||
output.writeShort(totalCentralEntries);
|
||||
output.writeInt(centralDirectorySize);
|
||||
output.writeInt(centralStartOffset);
|
||||
output.writeShort((short) fileComment.length());
|
||||
output.writeString(fileComment);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,474 @@
|
|||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package net.fornwall.apksigner.zipio;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.SequenceInputStream;
|
||||
import java.util.Date;
|
||||
import java.util.zip.Inflater;
|
||||
import java.util.zip.InflaterInputStream;
|
||||
|
||||
public final class ZioEntry implements Cloneable {
|
||||
|
||||
private ZipInput zipInput;
|
||||
|
||||
// public int signature = 0x02014b50;
|
||||
private short versionMadeBy;
|
||||
private short versionRequired;
|
||||
private short generalPurposeBits;
|
||||
private short compression;
|
||||
private short modificationTime;
|
||||
private short modificationDate;
|
||||
private int crc32;
|
||||
private int compressedSize;
|
||||
private int size;
|
||||
private String filename;
|
||||
private byte[] extraData;
|
||||
private short numAlignBytes = 0;
|
||||
private String fileComment;
|
||||
private short diskNumberStart;
|
||||
private short internalAttributes;
|
||||
private int externalAttributes;
|
||||
|
||||
private int localHeaderOffset;
|
||||
private long dataPosition = -1;
|
||||
private byte[] data = null;
|
||||
private ZioEntryOutputStream entryOut = null;
|
||||
|
||||
private static byte[] alignBytes = new byte[4];
|
||||
|
||||
public ZioEntry(ZipInput input) throws IOException {
|
||||
this.zipInput = input;
|
||||
|
||||
// 0 4 Central directory header signature = 0x02014b50
|
||||
int signature = input.readInt();
|
||||
if (signature != 0x02014b50) {
|
||||
// back up to the signature
|
||||
input.seek(input.getFilePointer() - 4);
|
||||
throw new IOException("Central directory header signature not found");
|
||||
}
|
||||
|
||||
// 4 2 Version needed to extract (minimum)
|
||||
versionMadeBy = input.readShort();
|
||||
|
||||
// 4 2 Version required
|
||||
versionRequired = input.readShort();
|
||||
|
||||
// 6 2 General purpose bit flag
|
||||
generalPurposeBits = input.readShort();
|
||||
// Bits 1, 2, 3, and 11 are allowed to be set (first bit is bit zero). Any others are a problem.
|
||||
if ((generalPurposeBits & 0xF7F1) != 0x0000) {
|
||||
throw new IllegalStateException("Can't handle general purpose bits == "
|
||||
+ String.format("0x%04x", generalPurposeBits));
|
||||
}
|
||||
|
||||
// 8 2 Compression method
|
||||
compression = input.readShort();
|
||||
|
||||
// 10 2 File last modification time
|
||||
modificationTime = input.readShort();
|
||||
|
||||
// 12 2 File last modification date
|
||||
modificationDate = input.readShort();
|
||||
|
||||
// 14 4 CRC-32
|
||||
crc32 = input.readInt();
|
||||
|
||||
// 18 4 Compressed size
|
||||
compressedSize = input.readInt();
|
||||
|
||||
// 22 4 Uncompressed size
|
||||
size = input.readInt();
|
||||
|
||||
// 26 2 File name length (n)
|
||||
short fileNameLen = input.readShort();
|
||||
// log.debug(String.format("File name length: 0x%04x", fileNameLen));
|
||||
|
||||
// 28 2 Extra field length (m)
|
||||
short extraLen = input.readShort();
|
||||
// log.debug(String.format("Extra length: 0x%04x", extraLen));
|
||||
|
||||
short fileCommentLen = input.readShort();
|
||||
diskNumberStart = input.readShort();
|
||||
internalAttributes = input.readShort();
|
||||
externalAttributes = input.readInt();
|
||||
localHeaderOffset = input.readInt();
|
||||
|
||||
// 30 n File name
|
||||
filename = input.readString(fileNameLen);
|
||||
extraData = input.readBytes(extraLen);
|
||||
fileComment = input.readString(fileCommentLen);
|
||||
|
||||
generalPurposeBits = (short) (generalPurposeBits & 0x0800); // Don't
|
||||
// write a
|
||||
// data
|
||||
// descriptor,
|
||||
// preserve
|
||||
// UTF-8
|
||||
// encoded
|
||||
// filename
|
||||
// bit
|
||||
|
||||
// Don't write zero-length entries with compression.
|
||||
if (size == 0) {
|
||||
compressedSize = 0;
|
||||
compression = 0;
|
||||
crc32 = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public ZioEntry(String name) {
|
||||
filename = name;
|
||||
fileComment = "";
|
||||
compression = 8;
|
||||
extraData = new byte[0];
|
||||
setTime(System.currentTimeMillis());
|
||||
}
|
||||
|
||||
public void readLocalHeader() throws IOException {
|
||||
ZipInput input = zipInput;
|
||||
input.seek(localHeaderOffset);
|
||||
|
||||
// 0 4 Local file header signature = 0x04034b50
|
||||
int signature = input.readInt();
|
||||
if (signature != 0x04034b50) {
|
||||
throw new IllegalStateException(String.format("Local header not found at pos=0x%08x, file=%s",
|
||||
input.getFilePointer(), filename));
|
||||
}
|
||||
|
||||
// This method is usually called just before the data read, so
|
||||
// its only purpose currently is to position the file pointer
|
||||
// for the data read. The entry's attributes might also have
|
||||
// been changed since the central dir entry was read (e.g.,
|
||||
// filename), so throw away the values here.
|
||||
|
||||
// 4 2 Version needed to extract (minimum)
|
||||
/* versionRequired */input.readShort();
|
||||
|
||||
// 6 2 General purpose bit flag
|
||||
/* generalPurposeBits */input.readShort();
|
||||
|
||||
// 8 2 Compression method
|
||||
/* compression */input.readShort();
|
||||
|
||||
// 10 2 File last modification time
|
||||
/* modificationTime */input.readShort();
|
||||
|
||||
// 12 2 File last modification date
|
||||
/* modificationDate */input.readShort();
|
||||
|
||||
// 14 4 CRC-32
|
||||
/* crc32 */input.readInt();
|
||||
|
||||
// 18 4 Compressed size
|
||||
/* compressedSize */input.readInt();
|
||||
|
||||
// 22 4 Uncompressed size
|
||||
/* size */input.readInt();
|
||||
|
||||
// 26 2 File name length (n)
|
||||
short fileNameLen = input.readShort();
|
||||
|
||||
// 28 2 Extra field length (m)
|
||||
short extraLen = input.readShort();
|
||||
|
||||
// 30 n File name
|
||||
/* String localFilename = */input.readString(fileNameLen);
|
||||
|
||||
// Extra data. FIXME: Avoid useless memory allocation.
|
||||
/* byte[] extra = */input.readBytes(extraLen);
|
||||
|
||||
// Record the file position of this entry's data.
|
||||
dataPosition = input.getFilePointer();
|
||||
}
|
||||
|
||||
public void writeLocalEntry(ZipOutput output) throws IOException {
|
||||
if (data == null && dataPosition < 0 && zipInput != null)
|
||||
readLocalHeader();
|
||||
localHeaderOffset = output.getFilePointer();
|
||||
|
||||
if (entryOut != null) {
|
||||
entryOut.close();
|
||||
size = entryOut.getSize();
|
||||
data = ((ByteArrayOutputStream) entryOut.wrapped).toByteArray();
|
||||
compressedSize = data.length;
|
||||
crc32 = entryOut.getCRC();
|
||||
}
|
||||
|
||||
output.writeInt(0x04034b50);
|
||||
output.writeShort(versionRequired);
|
||||
output.writeShort(generalPurposeBits);
|
||||
output.writeShort(compression);
|
||||
output.writeShort(modificationTime);
|
||||
output.writeShort(modificationDate);
|
||||
output.writeInt(crc32);
|
||||
output.writeInt(compressedSize);
|
||||
output.writeInt(size);
|
||||
output.writeShort((short) filename.length());
|
||||
|
||||
numAlignBytes = 0;
|
||||
|
||||
// Zipalign if the file is uncompressed, i.e., "Stored", and file size is not zero.
|
||||
if (compression == 0) {
|
||||
long dataPos = output.getFilePointer() + // current position
|
||||
2 + // plus size of extra data length
|
||||
filename.length() + // plus filename
|
||||
extraData.length; // plus extra data
|
||||
short dataPosMod4 = (short) (dataPos % 4);
|
||||
if (dataPosMod4 > 0) {
|
||||
numAlignBytes = (short) (4 - dataPosMod4);
|
||||
}
|
||||
}
|
||||
|
||||
// 28 2 Extra field length (m)
|
||||
output.writeShort((short) (extraData.length + numAlignBytes));
|
||||
|
||||
// 30 n File name
|
||||
output.writeString(filename);
|
||||
|
||||
// Extra data
|
||||
output.writeBytes(extraData);
|
||||
|
||||
// Zipalign bytes
|
||||
if (numAlignBytes > 0)
|
||||
output.writeBytes(alignBytes, 0, numAlignBytes);
|
||||
|
||||
if (data != null) {
|
||||
output.writeBytes(data);
|
||||
} else {
|
||||
zipInput.seek(dataPosition);
|
||||
|
||||
int bufferSize = Math.min(compressedSize, 8096);
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
long totalCount = 0;
|
||||
|
||||
while (totalCount != compressedSize) {
|
||||
int numRead = zipInput.in.read(buffer, 0, (int) Math.min(compressedSize - totalCount, bufferSize));
|
||||
if (numRead > 0) {
|
||||
output.writeBytes(buffer, 0, numRead);
|
||||
// if (debug)
|
||||
// getLogger().debug(
|
||||
// String.format("Wrote %d bytes", numRead));
|
||||
totalCount += numRead;
|
||||
} else
|
||||
throw new IllegalStateException(String.format(
|
||||
"EOF reached while copying %s with %d bytes left to go", filename, compressedSize
|
||||
- totalCount));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Returns the entry's data. */
|
||||
public byte[] getData() throws IOException {
|
||||
if (data != null)
|
||||
return data;
|
||||
|
||||
byte[] tmpdata = new byte[size];
|
||||
|
||||
InputStream din = getInputStream();
|
||||
int count = 0;
|
||||
|
||||
while (count != size) {
|
||||
int numRead = din.read(tmpdata, count, size - count);
|
||||
if (numRead < 0)
|
||||
throw new IllegalStateException(String.format("Read failed, expecting %d bytes, got %d instead", size,
|
||||
count));
|
||||
count += numRead;
|
||||
}
|
||||
return tmpdata;
|
||||
}
|
||||
|
||||
// Returns an input stream for reading the entry's data.
|
||||
public InputStream getInputStream() throws IOException {
|
||||
if (entryOut != null) {
|
||||
entryOut.close();
|
||||
size = entryOut.getSize();
|
||||
data = ((ByteArrayOutputStream) entryOut.wrapped).toByteArray();
|
||||
compressedSize = data.length;
|
||||
crc32 = entryOut.getCRC();
|
||||
entryOut = null;
|
||||
InputStream rawis = new ByteArrayInputStream(data);
|
||||
if (compression == 0)
|
||||
return rawis;
|
||||
else {
|
||||
// Hacky, inflate using a sequence of input streams that returns
|
||||
// 1 byte more than the actual length of the data.
|
||||
// This extra dummy byte is required by InflaterInputStream when
|
||||
// the data doesn't have the header and crc fields (as it is in
|
||||
// zip files).
|
||||
return new InflaterInputStream(new SequenceInputStream(rawis, new ByteArrayInputStream(new byte[1])),
|
||||
new Inflater(true));
|
||||
}
|
||||
}
|
||||
|
||||
ZioEntryInputStream dataStream;
|
||||
dataStream = new ZioEntryInputStream(this);
|
||||
if (compression != 0) {
|
||||
// Note: When using nowrap=true with Inflater it is also necessary to provide
|
||||
// an extra "dummy" byte as input. This is required by the ZLIB native library
|
||||
// in order to support certain optimizations.
|
||||
dataStream.setReturnDummyByte(true);
|
||||
return new InflaterInputStream(dataStream, new Inflater(true));
|
||||
} else
|
||||
return dataStream;
|
||||
}
|
||||
|
||||
// Returns an output stream for writing an entry's data.
|
||||
public OutputStream getOutputStream() {
|
||||
entryOut = new ZioEntryOutputStream(compression, new ByteArrayOutputStream());
|
||||
return entryOut;
|
||||
}
|
||||
|
||||
public void write(ZipOutput output) throws IOException {
|
||||
output.writeInt(0x02014b50);
|
||||
output.writeShort(versionMadeBy);
|
||||
output.writeShort(versionRequired);
|
||||
output.writeShort(generalPurposeBits);
|
||||
output.writeShort(compression);
|
||||
output.writeShort(modificationTime);
|
||||
output.writeShort(modificationDate);
|
||||
output.writeInt(crc32);
|
||||
output.writeInt(compressedSize);
|
||||
output.writeInt(size);
|
||||
output.writeShort((short) filename.length());
|
||||
output.writeShort((short) (extraData.length + numAlignBytes));
|
||||
output.writeShort((short) fileComment.length());
|
||||
output.writeShort(diskNumberStart);
|
||||
output.writeShort(internalAttributes);
|
||||
output.writeInt(externalAttributes);
|
||||
output.writeInt(localHeaderOffset);
|
||||
|
||||
output.writeString(filename);
|
||||
output.writeBytes(extraData);
|
||||
if (numAlignBytes > 0)
|
||||
output.writeBytes(alignBytes, 0, numAlignBytes);
|
||||
output.writeString(fileComment);
|
||||
}
|
||||
|
||||
/** Returns time in Java format. */
|
||||
public long getTime() {
|
||||
int year = ((modificationDate >> 9) & 0x007f) + 80;
|
||||
int month = ((modificationDate >> 5) & 0x000f) - 1;
|
||||
int day = modificationDate & 0x001f;
|
||||
int hour = (modificationTime >> 11) & 0x001f;
|
||||
int minute = (modificationTime >> 5) & 0x003f;
|
||||
int seconds = (modificationTime << 1) & 0x003e;
|
||||
Date d = new Date(year, month, day, hour, minute, seconds);
|
||||
return d.getTime();
|
||||
}
|
||||
|
||||
/** Set the file timestamp (using a Java time value). */
|
||||
public void setTime(long time) {
|
||||
Date d = new Date(time);
|
||||
long dtime;
|
||||
int year = d.getYear() + 1900;
|
||||
if (year < 1980) {
|
||||
dtime = (1 << 21) | (1 << 16);
|
||||
} else {
|
||||
dtime = (year - 1980) << 25 | (d.getMonth() + 1) << 21 | d.getDate() << 16 | d.getHours() << 11
|
||||
| d.getMinutes() << 5 | d.getSeconds() >> 1;
|
||||
}
|
||||
|
||||
modificationDate = (short) (dtime >> 16);
|
||||
modificationTime = (short) (dtime & 0xFFFF);
|
||||
}
|
||||
|
||||
public boolean isDirectory() {
|
||||
return filename.endsWith("/");
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return filename;
|
||||
}
|
||||
|
||||
public void setName(String filename) {
|
||||
this.filename = filename;
|
||||
}
|
||||
|
||||
/** Use 0 (STORED), or 8 (DEFLATE). */
|
||||
public void setCompression(int compression) {
|
||||
this.compression = (short) compression;
|
||||
}
|
||||
|
||||
public short getVersionMadeBy() {
|
||||
return versionMadeBy;
|
||||
}
|
||||
|
||||
public short getVersionRequired() {
|
||||
return versionRequired;
|
||||
}
|
||||
|
||||
public short getGeneralPurposeBits() {
|
||||
return generalPurposeBits;
|
||||
}
|
||||
|
||||
public short getCompression() {
|
||||
return compression;
|
||||
}
|
||||
|
||||
public int getCrc32() {
|
||||
return crc32;
|
||||
}
|
||||
|
||||
public int getCompressedSize() {
|
||||
return compressedSize;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public byte[] getExtraData() {
|
||||
return extraData;
|
||||
}
|
||||
|
||||
public String getFileComment() {
|
||||
return fileComment;
|
||||
}
|
||||
|
||||
public short getDiskNumberStart() {
|
||||
return diskNumberStart;
|
||||
}
|
||||
|
||||
public short getInternalAttributes() {
|
||||
return internalAttributes;
|
||||
}
|
||||
|
||||
public int getExternalAttributes() {
|
||||
return externalAttributes;
|
||||
}
|
||||
|
||||
public int getLocalHeaderOffset() {
|
||||
return localHeaderOffset;
|
||||
}
|
||||
|
||||
public long getDataPosition() {
|
||||
return dataPosition;
|
||||
}
|
||||
|
||||
public ZioEntryOutputStream getEntryOut() {
|
||||
return entryOut;
|
||||
}
|
||||
|
||||
public ZipInput getZipInput() {
|
||||
return zipInput;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package net.fornwall.apksigner.zipio;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.RandomAccessFile;
|
||||
|
||||
/** Input stream used to read just the data from a zip file entry. */
|
||||
final class ZioEntryInputStream extends InputStream {
|
||||
|
||||
RandomAccessFile raf;
|
||||
int size;
|
||||
int offset;
|
||||
boolean returnDummyByte = false;
|
||||
|
||||
public ZioEntryInputStream(ZioEntry entry) throws IOException {
|
||||
offset = 0;
|
||||
size = entry.getCompressedSize();
|
||||
raf = entry.getZipInput().in;
|
||||
long dpos = entry.getDataPosition();
|
||||
if (dpos >= 0) {
|
||||
raf.seek(entry.getDataPosition());
|
||||
} else {
|
||||
// seeks to, then reads, the local header, causing the
|
||||
// file pointer to be positioned at the start of the data.
|
||||
entry.readLocalHeader();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void setReturnDummyByte(boolean returnExtraByte) {
|
||||
returnDummyByte = returnExtraByte;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean markSupported() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int available() throws IOException {
|
||||
int available = size - offset;
|
||||
// log.debug(String.format("Available = %d", available));
|
||||
if (available == 0 && returnDummyByte)
|
||||
return 1;
|
||||
else
|
||||
return available;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
if ((size - offset) == 0) {
|
||||
if (returnDummyByte) {
|
||||
returnDummyByte = false;
|
||||
return 0;
|
||||
} else
|
||||
return -1;
|
||||
}
|
||||
int b = raf.read();
|
||||
if (b >= 0) {
|
||||
offset += 1;
|
||||
}
|
||||
return b;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
return readBytes(b, off, len);
|
||||
}
|
||||
|
||||
private int readBytes(byte[] b, int off, int len) throws IOException {
|
||||
if ((size - offset) == 0) {
|
||||
if (returnDummyByte) {
|
||||
returnDummyByte = false;
|
||||
b[off] = 0;
|
||||
return 1;
|
||||
} else
|
||||
return -1;
|
||||
}
|
||||
int numToRead = Math.min(len, available());
|
||||
int numRead = raf.read(b, off, numToRead);
|
||||
if (numRead > 0) {
|
||||
offset += numRead;
|
||||
}
|
||||
return numRead;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b) throws IOException {
|
||||
return readBytes(b, 0, b.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long skip(long n) throws IOException {
|
||||
long numToSkip = Math.min(n, available());
|
||||
raf.seek(raf.getFilePointer() + numToSkip);
|
||||
return numToSkip;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package net.fornwall.apksigner.zipio;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.zip.CRC32;
|
||||
import java.util.zip.Deflater;
|
||||
import java.util.zip.DeflaterOutputStream;
|
||||
|
||||
final class ZioEntryOutputStream extends OutputStream {
|
||||
|
||||
int size = 0; // tracks uncompressed size of data
|
||||
final CRC32 crc = new CRC32();
|
||||
int crcValue = 0;
|
||||
final OutputStream wrapped;
|
||||
final OutputStream downstream;
|
||||
|
||||
public ZioEntryOutputStream(int compression, OutputStream wrapped) {
|
||||
this.wrapped = wrapped;
|
||||
downstream = (compression == 0) ? wrapped : new DeflaterOutputStream(wrapped, new Deflater(
|
||||
Deflater.BEST_COMPRESSION, true));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
downstream.close();
|
||||
crcValue = (int) crc.getValue();
|
||||
}
|
||||
|
||||
public int getCRC() {
|
||||
return crcValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
downstream.flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b) throws IOException {
|
||||
downstream.write(b);
|
||||
crc.update(b);
|
||||
size += b.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(byte[] b, int off, int len) throws IOException {
|
||||
downstream.write(b, off, len);
|
||||
crc.update(b, off, len);
|
||||
size += len;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(int b) throws IOException {
|
||||
downstream.write(b);
|
||||
crc.update(b);
|
||||
size += 1;
|
||||
}
|
||||
|
||||
public int getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,113 @@
|
|||
package net.fornwall.apksigner.zipio;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.RandomAccessFile;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.jar.Manifest;
|
||||
|
||||
public final class ZipInput implements AutoCloseable {
|
||||
|
||||
final RandomAccessFile in;
|
||||
final long fileLength;
|
||||
int scanIterations = 0;
|
||||
|
||||
public final Map<String, ZioEntry> entries = new LinkedHashMap<>();
|
||||
final CentralEnd centralEnd;
|
||||
Manifest manifest;
|
||||
|
||||
public ZipInput(String filename) throws IOException {
|
||||
in = new RandomAccessFile(filename, "r");
|
||||
fileLength = in.length();
|
||||
|
||||
long posEOCDR = scanForEOCDR((int) Math.min(fileLength, 256));
|
||||
in.seek(posEOCDR);
|
||||
centralEnd = CentralEnd.read(this);
|
||||
in.seek(centralEnd.centralStartOffset);
|
||||
|
||||
for (int i = 0; i < centralEnd.totalCentralEntries; i++) {
|
||||
ZioEntry entry = new ZioEntry(this);
|
||||
entries.put(entry.getName(), entry);
|
||||
}
|
||||
}
|
||||
|
||||
public Manifest getManifest() throws IOException {
|
||||
if (manifest == null) {
|
||||
ZioEntry e = entries.get("META-INF/MANIFEST.MF");
|
||||
if (e != null)
|
||||
manifest = new Manifest(e.getInputStream());
|
||||
}
|
||||
return manifest;
|
||||
}
|
||||
|
||||
/**
|
||||
* Scan the end of the file for the end of central directory record (EOCDR). Returns the file offset of the EOCD
|
||||
* signature. The size parameter is an initial buffer size (e.g., 256).
|
||||
*/
|
||||
public long scanForEOCDR(int size) throws IOException {
|
||||
if (size > fileLength || size > 65536)
|
||||
throw new IllegalStateException("End of central directory not found");
|
||||
|
||||
int scanSize = (int) Math.min(fileLength, size);
|
||||
|
||||
byte[] scanBuf = new byte[scanSize];
|
||||
in.seek(fileLength - scanSize);
|
||||
in.readFully(scanBuf);
|
||||
|
||||
for (int i = scanSize - 22; i >= 0; i--) {
|
||||
scanIterations += 1;
|
||||
if (scanBuf[i] == 0x50 && scanBuf[i + 1] == 0x4b && scanBuf[i + 2] == 0x05 && scanBuf[i + 3] == 0x06) {
|
||||
return fileLength - scanSize + i;
|
||||
}
|
||||
}
|
||||
|
||||
return scanForEOCDR(size * 2);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
if (in != null)
|
||||
try {
|
||||
in.close();
|
||||
} catch (Throwable t) {
|
||||
}
|
||||
}
|
||||
|
||||
public long getFilePointer() throws IOException {
|
||||
return in.getFilePointer();
|
||||
}
|
||||
|
||||
public void seek(long position) throws IOException {
|
||||
in.seek(position);
|
||||
}
|
||||
|
||||
public int readInt() throws IOException {
|
||||
int result = 0;
|
||||
for (int i = 0; i < 4; i++)
|
||||
result |= (in.readUnsignedByte() << (8 * i));
|
||||
return result;
|
||||
}
|
||||
|
||||
public short readShort() throws IOException {
|
||||
short result = 0;
|
||||
for (int i = 0; i < 2; i++)
|
||||
result |= (in.readUnsignedByte() << (8 * i));
|
||||
return result;
|
||||
}
|
||||
|
||||
public String readString(int length) throws IOException {
|
||||
byte[] buffer = new byte[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
buffer[i] = in.readByte();
|
||||
return new String(buffer);
|
||||
}
|
||||
|
||||
public byte[] readBytes(int length) throws IOException {
|
||||
byte[] buffer = new byte[length];
|
||||
for (int i = 0; i < length; i++) {
|
||||
buffer[i] = in.readByte();
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* Copyright (C) 2010 Ken Ellinwood
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package net.fornwall.apksigner.zipio;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
public final class ZipOutput implements AutoCloseable {
|
||||
|
||||
final OutputStream out;
|
||||
int filePointer = 0;
|
||||
final List<ZioEntry> entriesWritten = new LinkedList<>();
|
||||
final Set<String> namesWritten = new HashSet<>();
|
||||
|
||||
public ZipOutput(OutputStream out) {
|
||||
this.out = out;
|
||||
}
|
||||
|
||||
public void write(ZioEntry entry) throws IOException {
|
||||
String entryName = entry.getName();
|
||||
if (namesWritten.contains(entryName)) {
|
||||
System.err.println("Skipping duplicate file in output: " + entryName);
|
||||
return;
|
||||
}
|
||||
entry.writeLocalEntry(this);
|
||||
entriesWritten.add(entry);
|
||||
namesWritten.add(entryName);
|
||||
}
|
||||
|
||||
public int getFilePointer() {
|
||||
return filePointer;
|
||||
}
|
||||
|
||||
public void writeInt(int value) throws IOException {
|
||||
byte[] data = new byte[4];
|
||||
for (int i = 0; i < 4; i++) {
|
||||
data[i] = (byte) (value & 0xFF);
|
||||
value = value >> 8;
|
||||
}
|
||||
out.write(data);
|
||||
filePointer += 4;
|
||||
}
|
||||
|
||||
public void writeShort(short value) throws IOException {
|
||||
byte[] data = new byte[2];
|
||||
for (int i = 0; i < 2; i++) {
|
||||
data[i] = (byte) (value & 0xFF);
|
||||
value = (short) (value >> 8);
|
||||
}
|
||||
out.write(data);
|
||||
filePointer += 2;
|
||||
}
|
||||
|
||||
public void writeString(String value) throws IOException {
|
||||
byte[] data = value.getBytes();
|
||||
out.write(data);
|
||||
filePointer += data.length;
|
||||
}
|
||||
|
||||
public void writeBytes(byte[] value) throws IOException {
|
||||
out.write(value);
|
||||
filePointer += value.length;
|
||||
}
|
||||
|
||||
public void writeBytes(byte[] value, int offset, int length) throws IOException {
|
||||
out.write(value, offset, length);
|
||||
filePointer += length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
CentralEnd centralEnd = new CentralEnd();
|
||||
centralEnd.centralStartOffset = getFilePointer();
|
||||
centralEnd.numCentralEntries = centralEnd.totalCentralEntries = (short) entriesWritten.size();
|
||||
|
||||
for (ZioEntry entry : entriesWritten)
|
||||
entry.write(this);
|
||||
|
||||
centralEnd.centralDirectorySize = (getFilePointer() - centralEnd.centralStartOffset);
|
||||
centralEnd.fileComment = "";
|
||||
|
||||
centralEnd.write(this);
|
||||
|
||||
if (out != null)
|
||||
try {
|
||||
out.close();
|
||||
} catch (Throwable t) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (c) 2009-2013 Panxiaobo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package pxb.android;
|
||||
|
||||
public interface ResConst {
|
||||
int RES_STRING_POOL_TYPE = 0x0001;
|
||||
int RES_TABLE_TYPE = 0x0002;
|
||||
int RES_TABLE_PACKAGE_TYPE = 0x0200;
|
||||
int RES_TABLE_TYPE_SPEC_TYPE = 0x0202;
|
||||
int RES_TABLE_TYPE_TYPE = 0x0201;
|
||||
|
||||
int RES_XML_TYPE = 0x0003;
|
||||
int RES_XML_RESOURCE_MAP_TYPE = 0x0180;
|
||||
int RES_XML_END_NAMESPACE_TYPE = 0x0101;
|
||||
int RES_XML_END_ELEMENT_TYPE = 0x0103;
|
||||
int RES_XML_START_NAMESPACE_TYPE = 0x0100;
|
||||
int RES_XML_START_ELEMENT_TYPE = 0x0102;
|
||||
int RES_XML_CDATA_TYPE = 0x0104;
|
||||
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright (c) 2009-2013 Panxiaobo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package pxb.android;
|
||||
|
||||
public class StringItem {
|
||||
public String data;
|
||||
public int dataOffset;
|
||||
public int index;
|
||||
|
||||
public StringItem() {
|
||||
super();
|
||||
}
|
||||
|
||||
public StringItem(String data) {
|
||||
super();
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
if (getClass() != obj.getClass())
|
||||
return false;
|
||||
StringItem other = (StringItem) obj;
|
||||
if (data == null) {
|
||||
if (other.data != null)
|
||||
return false;
|
||||
} else if (!data.equals(other.data))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
final int prime = 31;
|
||||
int result = 1;
|
||||
result = prime * result + ((data == null) ? 0 : data.hashCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
public String toString() {
|
||||
return String.format("S%04d %s", index, data);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,163 @@
|
|||
/*
|
||||
* Copyright (c) 2009-2013 Panxiaobo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package pxb.android;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@SuppressWarnings("serial")
|
||||
public class StringItems extends ArrayList<StringItem> {
|
||||
private static final int UTF8_FLAG = 0x00000100;
|
||||
|
||||
|
||||
public static String[] read(ByteBuffer in) throws IOException {
|
||||
int trunkOffset = in.position() - 8;
|
||||
int stringCount = in.getInt();
|
||||
int styleOffsetCount = in.getInt();
|
||||
int flags = in.getInt();
|
||||
int stringDataOffset = in.getInt();
|
||||
int stylesOffset = in.getInt();
|
||||
int offsets[] = new int[stringCount];
|
||||
String strings[] = new String[stringCount];
|
||||
for (int i = 0; i < stringCount; i++) {
|
||||
offsets[i] = in.getInt();
|
||||
}
|
||||
|
||||
int base = trunkOffset + stringDataOffset;
|
||||
for (int i = 0; i < offsets.length; i++) {
|
||||
in.position(base + offsets[i]);
|
||||
String s;
|
||||
|
||||
if (0 != (flags & UTF8_FLAG)) {
|
||||
u8length(in); // ignored
|
||||
int u8len = u8length(in);
|
||||
int start = in.position();
|
||||
int blength = u8len;
|
||||
while (in.get(start + blength) != 0) {
|
||||
blength++;
|
||||
}
|
||||
s = new String(in.array(), start, blength, "UTF-8");
|
||||
} else {
|
||||
int length = u16length(in);
|
||||
s = new String(in.array(), in.position(), length * 2, "UTF-16LE");
|
||||
}
|
||||
strings[i] = s;
|
||||
}
|
||||
return strings;
|
||||
}
|
||||
|
||||
static int u16length(ByteBuffer in) {
|
||||
int length = in.getShort() & 0xFFFF;
|
||||
if (length > 0x7FFF) {
|
||||
length = ((length & 0x7FFF) << 8) | (in.getShort() & 0xFFFF);
|
||||
}
|
||||
return length;
|
||||
}
|
||||
|
||||
static int u8length(ByteBuffer in) {
|
||||
int len = in.get() & 0xFF;
|
||||
if ((len & 0x80) != 0) {
|
||||
len = ((len & 0x7F) << 8) | (in.get() & 0xFF);
|
||||
}
|
||||
return len;
|
||||
}
|
||||
|
||||
byte[] stringData;
|
||||
|
||||
public int getSize() {
|
||||
return 5 * 4 + this.size() * 4 + stringData.length + 0;// TODO
|
||||
}
|
||||
|
||||
public void prepare() throws IOException {
|
||||
for (StringItem s : this) {
|
||||
if (s.data.length() > 0x7FFF) {
|
||||
useUTF8 = false;
|
||||
}
|
||||
}
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
int i = 0;
|
||||
int offset = 0;
|
||||
baos.reset();
|
||||
Map<String, Integer> map = new HashMap<String, Integer>();
|
||||
for (StringItem item : this) {
|
||||
item.index = i++;
|
||||
String stringData = item.data;
|
||||
Integer of = map.get(stringData);
|
||||
if (of != null) {
|
||||
item.dataOffset = of;
|
||||
} else {
|
||||
item.dataOffset = offset;
|
||||
map.put(stringData, offset);
|
||||
if (useUTF8) {
|
||||
int length = stringData.length();
|
||||
byte[] data = stringData.getBytes("UTF-8");
|
||||
int u8lenght = data.length;
|
||||
|
||||
if (length > 0x7F) {
|
||||
offset++;
|
||||
baos.write((length >> 8) | 0x80);
|
||||
}
|
||||
baos.write(length);
|
||||
|
||||
if (u8lenght > 0x7F) {
|
||||
offset++;
|
||||
baos.write((u8lenght >> 8) | 0x80);
|
||||
}
|
||||
baos.write(u8lenght);
|
||||
baos.write(data);
|
||||
baos.write(0);
|
||||
offset += 3 + u8lenght;
|
||||
} else {
|
||||
int length = stringData.length();
|
||||
byte[] data = stringData.getBytes("UTF-16LE");
|
||||
if (length > 0x7FFF) {
|
||||
int x = (length >> 16) | 0x8000;
|
||||
baos.write(x);
|
||||
baos.write(x >> 8);
|
||||
offset += 2;
|
||||
}
|
||||
baos.write(length);
|
||||
baos.write(length >> 8);
|
||||
baos.write(data);
|
||||
baos.write(0);
|
||||
baos.write(0);
|
||||
offset += 4 + data.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO
|
||||
stringData = baos.toByteArray();
|
||||
}
|
||||
|
||||
private boolean useUTF8 = true;
|
||||
|
||||
public void write(ByteBuffer out) throws IOException {
|
||||
out.putInt(this.size());
|
||||
out.putInt(0);// TODO style count
|
||||
out.putInt(useUTF8 ? UTF8_FLAG : 0);
|
||||
out.putInt(7 * 4 + this.size() * 4);
|
||||
out.putInt(0);
|
||||
for (StringItem item : this) {
|
||||
out.putInt(item.dataOffset);
|
||||
}
|
||||
out.put(stringData);
|
||||
// TODO
|
||||
}
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
/*
|
||||
* Copyright (c) 2009-2013 Panxiaobo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package pxb.android.axml;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class Axml extends AxmlVisitor {
|
||||
|
||||
public static class Node extends NodeVisitor {
|
||||
public static class Attr {
|
||||
public String ns, name;
|
||||
public int resourceId, type;
|
||||
public Object value;
|
||||
|
||||
public void accept(NodeVisitor nodeVisitor) {
|
||||
nodeVisitor.attr(ns, name, resourceId, type, value);
|
||||
}
|
||||
}
|
||||
|
||||
public static class Text {
|
||||
public int ln;
|
||||
public String text;
|
||||
|
||||
public void accept(NodeVisitor nodeVisitor) {
|
||||
nodeVisitor.text(ln, text);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Attr> attrs = new ArrayList<Attr>();
|
||||
public List<Node> children = new ArrayList<Node>();
|
||||
public Integer ln;
|
||||
public String ns, name;
|
||||
public Text text;
|
||||
|
||||
public void accept(NodeVisitor nodeVisitor) {
|
||||
NodeVisitor nodeVisitor2 = nodeVisitor.child(ns, name);
|
||||
acceptB(nodeVisitor2);
|
||||
nodeVisitor2.end();
|
||||
}
|
||||
|
||||
public void acceptB(NodeVisitor nodeVisitor) {
|
||||
if (text != null) {
|
||||
text.accept(nodeVisitor);
|
||||
}
|
||||
for (Attr a : attrs) {
|
||||
a.accept(nodeVisitor);
|
||||
}
|
||||
if (ln != null) {
|
||||
nodeVisitor.line(ln);
|
||||
}
|
||||
for (Node c : children) {
|
||||
c.accept(nodeVisitor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attr(String ns, String name, int resourceId, int type, Object obj) {
|
||||
Attr attr = new Attr();
|
||||
attr.name = name;
|
||||
attr.ns = ns;
|
||||
attr.resourceId = resourceId;
|
||||
attr.type = type;
|
||||
attr.value = obj;
|
||||
attrs.add(attr);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeVisitor child(String ns, String name) {
|
||||
Node node = new Node();
|
||||
node.name = name;
|
||||
node.ns = ns;
|
||||
children.add(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void line(int ln) {
|
||||
this.ln = ln;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void text(int lineNumber, String value) {
|
||||
Text text = new Text();
|
||||
text.ln = lineNumber;
|
||||
text.text = value;
|
||||
this.text = text;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Ns {
|
||||
public int ln;
|
||||
public String prefix, uri;
|
||||
|
||||
public void accept(AxmlVisitor visitor) {
|
||||
visitor.ns(prefix, uri, ln);
|
||||
}
|
||||
}
|
||||
|
||||
public List<Node> firsts = new ArrayList<Node>();
|
||||
public List<Ns> nses = new ArrayList<Ns>();
|
||||
|
||||
public void accept(final AxmlVisitor visitor) {
|
||||
for (Ns ns : nses) {
|
||||
ns.accept(visitor);
|
||||
}
|
||||
for (Node first : firsts) {
|
||||
first.accept(visitor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeVisitor child(String ns, String name) {
|
||||
Node node = new Node();
|
||||
node.name = name;
|
||||
node.ns = ns;
|
||||
firsts.add(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ns(String prefix, String uri, int ln) {
|
||||
Ns ns = new Ns();
|
||||
ns.prefix = prefix;
|
||||
ns.uri = uri;
|
||||
ns.ln = ln;
|
||||
nses.add(ns);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,274 @@
|
|||
/*
|
||||
* Copyright (c) 2009-2013 Panxiaobo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package pxb.android.axml;
|
||||
|
||||
import static pxb.android.axml.NodeVisitor.TYPE_INT_BOOLEAN;
|
||||
import static pxb.android.axml.NodeVisitor.TYPE_STRING;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.IntBuffer;
|
||||
|
||||
import pxb.android.ResConst;
|
||||
import pxb.android.StringItems;
|
||||
|
||||
/**
|
||||
* a class to read android axml
|
||||
*
|
||||
* @author <a href="mailto:pxb1988@gmail.com">Panxiaobo</a>
|
||||
*/
|
||||
public class AxmlParser implements ResConst {
|
||||
|
||||
public static final int END_FILE = 7;
|
||||
public static final int END_NS = 5;
|
||||
public static final int END_TAG = 3;
|
||||
public static final int START_FILE = 1;
|
||||
public static final int START_NS = 4;
|
||||
public static final int START_TAG = 2;
|
||||
public static final int TEXT = 6;
|
||||
// private int attrName[];
|
||||
// private int attrNs[];
|
||||
// private int attrResId[];
|
||||
// private int attrType[];
|
||||
// private Object attrValue[];
|
||||
|
||||
private int attributeCount;
|
||||
|
||||
private IntBuffer attrs;
|
||||
|
||||
private int classAttribute;
|
||||
private int fileSize = -1;
|
||||
private int idAttribute;
|
||||
private ByteBuffer in;
|
||||
private int lineNumber;
|
||||
private int nameIdx;
|
||||
private int nsIdx;
|
||||
|
||||
private int prefixIdx;
|
||||
|
||||
private int[] resourceIds;
|
||||
|
||||
private String[] strings;
|
||||
|
||||
private int styleAttribute;
|
||||
|
||||
private int textIdx;
|
||||
|
||||
public AxmlParser(byte[] data) {
|
||||
this(ByteBuffer.wrap(data));
|
||||
}
|
||||
|
||||
public AxmlParser(ByteBuffer in) {
|
||||
super();
|
||||
this.in = in.order(ByteOrder.LITTLE_ENDIAN);
|
||||
}
|
||||
|
||||
public int getAttrCount() {
|
||||
return attributeCount;
|
||||
}
|
||||
|
||||
public int getAttributeCount() {
|
||||
return attributeCount;
|
||||
}
|
||||
|
||||
public String getAttrName(int i) {
|
||||
int idx = attrs.get(i * 5 + 1);
|
||||
return strings[idx];
|
||||
|
||||
}
|
||||
|
||||
public String getAttrNs(int i) {
|
||||
int idx = attrs.get(i * 5 + 0);
|
||||
return idx >= 0 ? strings[idx] : null;
|
||||
}
|
||||
|
||||
String getAttrRawString(int i) {
|
||||
int idx = attrs.get(i * 5 + 2);
|
||||
if (idx >= 0) {
|
||||
return strings[idx];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public int getAttrResId(int i) {
|
||||
if (resourceIds != null) {
|
||||
int idx = attrs.get(i * 5 + 1);
|
||||
if (idx >= 0 && idx < resourceIds.length) {
|
||||
return resourceIds[idx];
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
public int getAttrType(int i) {
|
||||
return attrs.get(i * 5 + 3) >> 24;
|
||||
}
|
||||
|
||||
public Object getAttrValue(int i) {
|
||||
int v = attrs.get(i * 5 + 4);
|
||||
|
||||
if (i == idAttribute) {
|
||||
return ValueWrapper.wrapId(v, getAttrRawString(i));
|
||||
} else if (i == styleAttribute) {
|
||||
return ValueWrapper.wrapStyle(v, getAttrRawString(i));
|
||||
} else if (i == classAttribute) {
|
||||
return ValueWrapper.wrapClass(v, getAttrRawString(i));
|
||||
}
|
||||
|
||||
switch (getAttrType(i)) {
|
||||
case TYPE_STRING:
|
||||
return strings[v];
|
||||
case TYPE_INT_BOOLEAN:
|
||||
return v != 0;
|
||||
default:
|
||||
return v;
|
||||
}
|
||||
}
|
||||
|
||||
public int getLineNumber() {
|
||||
return lineNumber;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return strings[nameIdx];
|
||||
}
|
||||
|
||||
public String getNamespacePrefix() {
|
||||
return strings[prefixIdx];
|
||||
}
|
||||
|
||||
public String getNamespaceUri() {
|
||||
return nsIdx >= 0 ? strings[nsIdx] : null;
|
||||
}
|
||||
|
||||
public String getText() {
|
||||
return strings[textIdx];
|
||||
}
|
||||
|
||||
public int next() throws IOException {
|
||||
if (fileSize < 0) {
|
||||
int type = in.getInt() & 0xFFFF;
|
||||
if (type != RES_XML_TYPE) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
fileSize = in.getInt();
|
||||
return START_FILE;
|
||||
}
|
||||
int event = -1;
|
||||
for (int p = in.position(); p < fileSize; p = in.position()) {
|
||||
int type = in.getInt() & 0xFFFF;
|
||||
int size = in.getInt();
|
||||
switch (type) {
|
||||
case RES_XML_START_ELEMENT_TYPE: {
|
||||
{
|
||||
lineNumber = in.getInt();
|
||||
in.getInt();/* skip, 0xFFFFFFFF */
|
||||
nsIdx = in.getInt();
|
||||
nameIdx = in.getInt();
|
||||
int flag = in.getInt();// 0x00140014 ?
|
||||
if (flag != 0x00140014) {
|
||||
throw new RuntimeException();
|
||||
}
|
||||
}
|
||||
|
||||
attributeCount = in.getShort() & 0xFFFF;
|
||||
idAttribute = (in.getShort() & 0xFFFF) - 1;
|
||||
classAttribute = (in.getShort() & 0xFFFF) - 1;
|
||||
styleAttribute = (in.getShort() & 0xFFFF) - 1;
|
||||
|
||||
attrs = in.asIntBuffer();
|
||||
|
||||
// attrResId = new int[attributeCount];
|
||||
// attrName = new int[attributeCount];
|
||||
// attrNs = new int[attributeCount];
|
||||
// attrType = new int[attributeCount];
|
||||
// attrValue = new Object[attributeCount];
|
||||
// for (int i = 0; i < attributeCount; i++) {
|
||||
// int attrNsIdx = in.getInt();
|
||||
// int attrNameIdx = in.getInt();
|
||||
// int raw = in.getInt();
|
||||
// int aValueType = in.getInt() >>> 24;
|
||||
// int aValue = in.getInt();
|
||||
// Object value = null;
|
||||
// switch (aValueType) {
|
||||
// case TYPE_STRING:
|
||||
// value = strings[aValue];
|
||||
// break;
|
||||
// case TYPE_INT_BOOLEAN:
|
||||
// value = aValue != 0;
|
||||
// break;
|
||||
// default:
|
||||
// value = aValue;
|
||||
// }
|
||||
// int resourceId = attrNameIdx < this.resourceIds.length ?
|
||||
// resourceIds[attrNameIdx] : -1;
|
||||
// attrNs[i] = attrNsIdx;
|
||||
// attrName[i] = attrNameIdx;
|
||||
// attrType[i] = aValueType;
|
||||
// attrResId[i] = resourceId;
|
||||
// attrValue[i] = value;
|
||||
// }
|
||||
event = START_TAG;
|
||||
}
|
||||
break;
|
||||
case RES_XML_END_ELEMENT_TYPE: {
|
||||
in.position(p + size);
|
||||
event = END_TAG;
|
||||
}
|
||||
break;
|
||||
case RES_XML_START_NAMESPACE_TYPE:
|
||||
lineNumber = in.getInt();
|
||||
in.getInt();/* 0xFFFFFFFF */
|
||||
prefixIdx = in.getInt();
|
||||
nsIdx = in.getInt();
|
||||
event = START_NS;
|
||||
break;
|
||||
case RES_XML_END_NAMESPACE_TYPE:
|
||||
in.position(p + size);
|
||||
event = END_NS;
|
||||
break;
|
||||
case RES_STRING_POOL_TYPE:
|
||||
strings = StringItems.read(in);
|
||||
in.position(p + size);
|
||||
continue;
|
||||
case RES_XML_RESOURCE_MAP_TYPE:
|
||||
int count = size / 4 - 2;
|
||||
resourceIds = new int[count];
|
||||
for (int i = 0; i < count; i++) {
|
||||
resourceIds[i] = in.getInt();
|
||||
}
|
||||
in.position(p + size);
|
||||
continue;
|
||||
case RES_XML_CDATA_TYPE:
|
||||
lineNumber = in.getInt();
|
||||
in.getInt();/* 0xFFFFFFFF */
|
||||
textIdx = in.getInt();
|
||||
|
||||
in.getInt();/* 00000008 00000000 */
|
||||
in.getInt();
|
||||
|
||||
event = TEXT;
|
||||
break;
|
||||
default:
|
||||
throw new RuntimeException();
|
||||
}
|
||||
in.position(p + size);
|
||||
return event;
|
||||
}
|
||||
return END_FILE;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
* Copyright (c) 2009-2013 Panxiaobo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package pxb.android.axml;
|
||||
|
||||
import static pxb.android.axml.AxmlParser.END_FILE;
|
||||
import static pxb.android.axml.AxmlParser.END_NS;
|
||||
import static pxb.android.axml.AxmlParser.END_TAG;
|
||||
import static pxb.android.axml.AxmlParser.START_FILE;
|
||||
import static pxb.android.axml.AxmlParser.START_NS;
|
||||
import static pxb.android.axml.AxmlParser.START_TAG;
|
||||
import static pxb.android.axml.AxmlParser.TEXT;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Stack;
|
||||
|
||||
/**
|
||||
* a class to read android axml
|
||||
*
|
||||
* @author <a href="mailto:pxb1988@gmail.com">Panxiaobo</a>
|
||||
*/
|
||||
public class AxmlReader {
|
||||
public static final NodeVisitor EMPTY_VISITOR = new NodeVisitor() {
|
||||
|
||||
@Override
|
||||
public NodeVisitor child(String ns, String name) {
|
||||
return this;
|
||||
}
|
||||
|
||||
};
|
||||
final AxmlParser parser;
|
||||
|
||||
public AxmlReader(byte[] data) {
|
||||
super();
|
||||
this.parser = new AxmlParser(data);
|
||||
}
|
||||
|
||||
public void accept(final AxmlVisitor av) throws IOException {
|
||||
Stack<NodeVisitor> nvs = new Stack<NodeVisitor>();
|
||||
NodeVisitor tos = av;
|
||||
while (true) {
|
||||
int type = parser.next();
|
||||
switch (type) {
|
||||
case START_FILE:
|
||||
break;
|
||||
case START_TAG:
|
||||
nvs.push(tos);
|
||||
tos = tos.child(parser.getNamespaceUri(), parser.getName());
|
||||
if (tos != null) {
|
||||
if (tos != EMPTY_VISITOR) {
|
||||
tos.line(parser.getLineNumber());
|
||||
for (int i = 0; i < parser.getAttrCount(); i++) {
|
||||
tos.attr(parser.getAttrNs(i), parser.getAttrName(i), parser.getAttrResId(i),
|
||||
parser.getAttrType(i), parser.getAttrValue(i));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tos = EMPTY_VISITOR;
|
||||
}
|
||||
break;
|
||||
case END_TAG:
|
||||
tos.end();
|
||||
tos = nvs.pop();
|
||||
break;
|
||||
case START_NS:
|
||||
av.ns(parser.getNamespacePrefix(), parser.getNamespaceUri(), parser.getLineNumber());
|
||||
break;
|
||||
case END_NS:
|
||||
break;
|
||||
case TEXT:
|
||||
tos.text(parser.getLineNumber(), parser.getText());
|
||||
break;
|
||||
case END_FILE:
|
||||
return;
|
||||
default:
|
||||
System.err.println("AxmlReader: Unsupported tag: " + type);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (c) 2009-2013 Panxiaobo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package pxb.android.axml;
|
||||
|
||||
/**
|
||||
* visitor to visit an axml
|
||||
*
|
||||
* @author <a href="mailto:pxb1988@gmail.com">Panxiaobo</a>
|
||||
*/
|
||||
public class AxmlVisitor extends NodeVisitor {
|
||||
|
||||
public AxmlVisitor() {
|
||||
super();
|
||||
|
||||
}
|
||||
|
||||
public AxmlVisitor(NodeVisitor av) {
|
||||
super(av);
|
||||
}
|
||||
|
||||
/**
|
||||
* create a ns
|
||||
*
|
||||
* @param prefix
|
||||
* @param uri
|
||||
* @param ln
|
||||
*/
|
||||
public void ns(String prefix, String uri, int ln) {
|
||||
if (nv != null && nv instanceof AxmlVisitor) {
|
||||
((AxmlVisitor) nv).ns(prefix, uri, ln);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,454 @@
|
|||
/*
|
||||
* Copyright (c) 2009-2013 Panxiaobo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package pxb.android.axml;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.Stack;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import pxb.android.StringItem;
|
||||
import pxb.android.StringItems;
|
||||
|
||||
import static pxb.android.ResConst.RES_STRING_POOL_TYPE;
|
||||
import static pxb.android.ResConst.RES_XML_CDATA_TYPE;
|
||||
import static pxb.android.ResConst.RES_XML_END_ELEMENT_TYPE;
|
||||
import static pxb.android.ResConst.RES_XML_END_NAMESPACE_TYPE;
|
||||
import static pxb.android.ResConst.RES_XML_RESOURCE_MAP_TYPE;
|
||||
import static pxb.android.ResConst.RES_XML_START_ELEMENT_TYPE;
|
||||
import static pxb.android.ResConst.RES_XML_START_NAMESPACE_TYPE;
|
||||
import static pxb.android.ResConst.RES_XML_TYPE;
|
||||
|
||||
/**
|
||||
* a class to write android axml
|
||||
*
|
||||
* @author <a href="mailto:pxb1988@gmail.com">Panxiaobo</a>
|
||||
*/
|
||||
public class AxmlWriter extends AxmlVisitor {
|
||||
static final Comparator<Attr> ATTR_CMP = new Comparator<Attr>() {
|
||||
|
||||
@Override
|
||||
public int compare(Attr a, Attr b) {
|
||||
int x = a.resourceId - b.resourceId;
|
||||
if (x == 0) {
|
||||
x = a.name.data.compareTo(b.name.data);
|
||||
if (x == 0) {
|
||||
boolean aNsIsnull = a.ns == null;
|
||||
boolean bNsIsnull = b.ns == null;
|
||||
if (aNsIsnull) {
|
||||
if (bNsIsnull) {
|
||||
x = 0;
|
||||
} else {
|
||||
x = -1;
|
||||
}
|
||||
} else {
|
||||
if (bNsIsnull) {
|
||||
x = 1;
|
||||
} else {
|
||||
x = a.ns.data.compareTo(b.ns.data);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
return x;
|
||||
}
|
||||
};
|
||||
|
||||
static class Attr {
|
||||
|
||||
public int index;
|
||||
public StringItem name;
|
||||
public StringItem ns;
|
||||
public int resourceId;
|
||||
public int type;
|
||||
public Object value;
|
||||
public StringItem raw;
|
||||
|
||||
public Attr(StringItem ns, StringItem name, int resourceId) {
|
||||
super();
|
||||
this.ns = ns;
|
||||
this.name = name;
|
||||
this.resourceId = resourceId;
|
||||
}
|
||||
|
||||
public void prepare(AxmlWriter axmlWriter) {
|
||||
ns = axmlWriter.updateNs(ns);
|
||||
if (this.name != null) {
|
||||
if (resourceId != -1) {
|
||||
this.name = axmlWriter.updateWithResourceId(this.name, this.resourceId);
|
||||
} else {
|
||||
this.name = axmlWriter.update(this.name);
|
||||
}
|
||||
}
|
||||
if (value instanceof StringItem) {
|
||||
value = axmlWriter.update((StringItem) value);
|
||||
}
|
||||
if (raw != null) {
|
||||
raw = axmlWriter.update(raw);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class NodeImpl extends NodeVisitor {
|
||||
private Set<Attr> attrs = new TreeSet<Attr>(ATTR_CMP);
|
||||
private List<NodeImpl> children = new ArrayList<NodeImpl>();
|
||||
private int line;
|
||||
private StringItem name;
|
||||
private StringItem ns;
|
||||
private StringItem text;
|
||||
private int textLineNumber;
|
||||
Attr id;
|
||||
Attr style;
|
||||
Attr clz;
|
||||
|
||||
public NodeImpl(String ns, String name) {
|
||||
super(null);
|
||||
this.ns = ns == null ? null : new StringItem(ns);
|
||||
this.name = name == null ? null : new StringItem(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attr(String ns, String name, int resourceId, int type, Object value) {
|
||||
if (name == null) {
|
||||
throw new RuntimeException("name can't be null");
|
||||
}
|
||||
Attr a = new Attr(ns == null ? null : new StringItem(ns), new StringItem(name), resourceId);
|
||||
a.type = type;
|
||||
|
||||
if (value instanceof ValueWrapper) {
|
||||
ValueWrapper valueWrapper = (ValueWrapper) value;
|
||||
if (valueWrapper.raw != null) {
|
||||
a.raw = new StringItem(valueWrapper.raw);
|
||||
}
|
||||
a.value = valueWrapper.ref;
|
||||
switch (valueWrapper.type) {
|
||||
case ValueWrapper.CLASS:
|
||||
clz = a;
|
||||
break;
|
||||
case ValueWrapper.ID:
|
||||
id = a;
|
||||
break;
|
||||
case ValueWrapper.STYLE:
|
||||
style = a;
|
||||
break;
|
||||
}
|
||||
} else if (type == TYPE_STRING) {
|
||||
StringItem raw = new StringItem((String) value);
|
||||
a.raw = raw;
|
||||
a.value = raw;
|
||||
|
||||
} else {
|
||||
a.raw = null;
|
||||
a.value = value;
|
||||
}
|
||||
|
||||
attrs.add(a);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeVisitor child(String ns, String name) {
|
||||
NodeImpl child = new NodeImpl(ns, name);
|
||||
this.children.add(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void line(int ln) {
|
||||
this.line = ln;
|
||||
}
|
||||
|
||||
public int prepare(AxmlWriter axmlWriter) {
|
||||
ns = axmlWriter.updateNs(ns);
|
||||
name = axmlWriter.update(name);
|
||||
|
||||
int attrIndex = 0;
|
||||
for (Attr attr : attrs) {
|
||||
attr.index = attrIndex++;
|
||||
attr.prepare(axmlWriter);
|
||||
}
|
||||
|
||||
text = axmlWriter.update(text);
|
||||
int size = 24 + 36 + attrs.size() * 20;// 24 for end tag,36+x*20 for
|
||||
// start tag
|
||||
for (NodeImpl child : children) {
|
||||
size += child.prepare(axmlWriter);
|
||||
}
|
||||
if (text != null) {
|
||||
size += 28;
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void text(int ln, String value) {
|
||||
this.text = new StringItem(value);
|
||||
this.textLineNumber = ln;
|
||||
}
|
||||
|
||||
void write(ByteBuffer out) throws IOException {
|
||||
// start tag
|
||||
out.putInt(RES_XML_START_ELEMENT_TYPE | (0x0010 << 16));
|
||||
out.putInt(36 + attrs.size() * 20);
|
||||
out.putInt(line);
|
||||
out.putInt(0xFFFFFFFF);
|
||||
out.putInt(ns != null ? this.ns.index : -1);
|
||||
out.putInt(name.index);
|
||||
out.putInt(0x00140014);// TODO
|
||||
out.putShort((short) this.attrs.size());
|
||||
out.putShort((short) (id == null ? 0 : id.index + 1));
|
||||
out.putShort((short) (clz == null ? 0 : clz.index + 1));
|
||||
out.putShort((short) (style == null ? 0 : style.index + 1));
|
||||
for (Attr attr : attrs) {
|
||||
out.putInt(attr.ns == null ? -1 : attr.ns.index);
|
||||
out.putInt(attr.name.index);
|
||||
out.putInt(attr.raw != null ? attr.raw.index : -1);
|
||||
out.putInt((attr.type << 24) | 0x000008);
|
||||
Object v = attr.value;
|
||||
if (v instanceof StringItem) {
|
||||
out.putInt(((StringItem) attr.value).index);
|
||||
} else if (v instanceof Boolean) {
|
||||
out.putInt(Boolean.TRUE.equals(v) ? -1 : 0);
|
||||
} else {
|
||||
if (attr.value instanceof Integer) {
|
||||
out.putInt((Integer) attr.value);
|
||||
} else if (attr.value instanceof String) {
|
||||
if ("true".equalsIgnoreCase((String) attr.value)) {
|
||||
out.putInt(-1);
|
||||
} else if ("false".equalsIgnoreCase((String) attr.value)) {
|
||||
out.putInt(0);
|
||||
} else {
|
||||
try {
|
||||
out.putInt(Integer.valueOf((String) attr.value));
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.text != null) {
|
||||
out.putInt(RES_XML_CDATA_TYPE | (0x0010 << 16));
|
||||
out.putInt(28);
|
||||
out.putInt(textLineNumber);
|
||||
out.putInt(0xFFFFFFFF);
|
||||
out.putInt(text.index);
|
||||
out.putInt(0x00000008);
|
||||
out.putInt(0x00000000);
|
||||
}
|
||||
|
||||
// children
|
||||
for (NodeImpl child : children) {
|
||||
child.write(out);
|
||||
}
|
||||
|
||||
// end tag
|
||||
out.putInt(RES_XML_END_ELEMENT_TYPE | (0x0010 << 16));
|
||||
out.putInt(24);
|
||||
out.putInt(-1);
|
||||
out.putInt(0xFFFFFFFF);
|
||||
out.putInt(ns != null ? this.ns.index : -1);
|
||||
out.putInt(name.index);
|
||||
}
|
||||
}
|
||||
|
||||
static class Ns {
|
||||
int ln;
|
||||
StringItem prefix;
|
||||
StringItem uri;
|
||||
|
||||
public Ns(StringItem prefix, StringItem uri, int ln) {
|
||||
super();
|
||||
this.prefix = prefix;
|
||||
this.uri = uri;
|
||||
this.ln = ln;
|
||||
}
|
||||
}
|
||||
|
||||
private List<NodeImpl> firsts = new ArrayList<NodeImpl>(3);
|
||||
|
||||
private Map<String, Ns> nses = new HashMap<String, Ns>();
|
||||
|
||||
private List<StringItem> otherString = new ArrayList<StringItem>();
|
||||
|
||||
private Map<String, StringItem> resourceId2Str = new HashMap<String, StringItem>();
|
||||
|
||||
private List<Integer> resourceIds = new ArrayList<Integer>();
|
||||
|
||||
private List<StringItem> resourceString = new ArrayList<StringItem>();
|
||||
|
||||
private StringItems stringItems = new StringItems();
|
||||
|
||||
// TODO add style support
|
||||
// private List<StringItem> styleItems = new ArrayList();
|
||||
|
||||
@Override
|
||||
public NodeVisitor child(String ns, String name) {
|
||||
NodeImpl first = new NodeImpl(ns, name);
|
||||
this.firsts.add(first);
|
||||
return first;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void end() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ns(String prefix, String uri, int ln) {
|
||||
nses.put(uri, new Ns(prefix == null ? null : new StringItem(prefix), new StringItem(uri), ln));
|
||||
}
|
||||
|
||||
private int prepare() throws IOException {
|
||||
int size = 0;
|
||||
|
||||
for (NodeImpl first : firsts) {
|
||||
size += first.prepare(this);
|
||||
}
|
||||
{
|
||||
int a = 0;
|
||||
for (Map.Entry<String, Ns> e : nses.entrySet()) {
|
||||
Ns ns = e.getValue();
|
||||
if (ns == null) {
|
||||
ns = new Ns(null, new StringItem(e.getKey()), 0);
|
||||
e.setValue(ns);
|
||||
}
|
||||
if (ns.prefix == null) {
|
||||
ns.prefix = new StringItem(String.format("axml_auto_%02d", a++));
|
||||
}
|
||||
ns.prefix = update(ns.prefix);
|
||||
ns.uri = update(ns.uri);
|
||||
}
|
||||
}
|
||||
|
||||
size += nses.size() * 24 * 2;
|
||||
|
||||
this.stringItems.addAll(resourceString);
|
||||
resourceString = null;
|
||||
this.stringItems.addAll(otherString);
|
||||
otherString = null;
|
||||
this.stringItems.prepare();
|
||||
int stringSize = this.stringItems.getSize();
|
||||
if (stringSize % 4 != 0) {
|
||||
stringSize += 4 - stringSize % 4;
|
||||
}
|
||||
size += 8 + stringSize;
|
||||
size += 8 + resourceIds.size() * 4;
|
||||
return size;
|
||||
}
|
||||
|
||||
public byte[] toByteArray() throws IOException {
|
||||
|
||||
int size = 8 + prepare();
|
||||
ByteBuffer out = ByteBuffer.allocate(size).order(ByteOrder.LITTLE_ENDIAN);
|
||||
|
||||
out.putInt(RES_XML_TYPE | (0x0008 << 16));
|
||||
out.putInt(size);
|
||||
|
||||
int stringSize = this.stringItems.getSize();
|
||||
int padding = 0;
|
||||
if (stringSize % 4 != 0) {
|
||||
padding = 4 - stringSize % 4;
|
||||
}
|
||||
out.putInt(RES_STRING_POOL_TYPE | (0x001C << 16));
|
||||
out.putInt(stringSize + padding + 8);
|
||||
this.stringItems.write(out);
|
||||
out.put(new byte[padding]);
|
||||
|
||||
out.putInt(RES_XML_RESOURCE_MAP_TYPE | (0x0008 << 16));
|
||||
out.putInt(8 + this.resourceIds.size() * 4);
|
||||
for (Integer i : resourceIds) {
|
||||
out.putInt(i);
|
||||
}
|
||||
|
||||
Stack<Ns> stack = new Stack<Ns>();
|
||||
for (Map.Entry<String, Ns> e : this.nses.entrySet()) {
|
||||
Ns ns = e.getValue();
|
||||
stack.push(ns);
|
||||
out.putInt(RES_XML_START_NAMESPACE_TYPE | (0x0010 << 16));
|
||||
out.putInt(24);
|
||||
out.putInt(-1);
|
||||
out.putInt(0xFFFFFFFF);
|
||||
out.putInt(ns.prefix.index);
|
||||
out.putInt(ns.uri.index);
|
||||
}
|
||||
|
||||
for (NodeImpl first : firsts) {
|
||||
first.write(out);
|
||||
}
|
||||
|
||||
while (stack.size() > 0) {
|
||||
Ns ns = stack.pop();
|
||||
out.putInt(RES_XML_END_NAMESPACE_TYPE | (0x0010 << 16));
|
||||
out.putInt(24);
|
||||
out.putInt(ns.ln);
|
||||
out.putInt(0xFFFFFFFF);
|
||||
out.putInt(ns.prefix.index);
|
||||
out.putInt(ns.uri.index);
|
||||
}
|
||||
return out.array();
|
||||
}
|
||||
|
||||
StringItem update(StringItem item) {
|
||||
if (item == null)
|
||||
return null;
|
||||
int i = this.otherString.indexOf(item);
|
||||
if (i < 0) {
|
||||
StringItem copy = new StringItem(item.data);
|
||||
this.otherString.add(copy);
|
||||
return copy;
|
||||
} else {
|
||||
return this.otherString.get(i);
|
||||
}
|
||||
}
|
||||
|
||||
StringItem updateNs(StringItem item) {
|
||||
if (item == null) {
|
||||
return null;
|
||||
}
|
||||
String ns = item.data;
|
||||
if (!this.nses.containsKey(ns)) {
|
||||
this.nses.put(ns, null);
|
||||
}
|
||||
return update(item);
|
||||
}
|
||||
|
||||
StringItem updateWithResourceId(StringItem name, int resourceId) {
|
||||
String key = name.data + resourceId;
|
||||
StringItem item = this.resourceId2Str.get(key);
|
||||
if (item != null) {
|
||||
return item;
|
||||
} else {
|
||||
StringItem copy = new StringItem(name.data);
|
||||
resourceIds.add(resourceId);
|
||||
resourceString.add(copy);
|
||||
resourceId2Str.put(key, copy);
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,116 @@
|
|||
/*
|
||||
* Copyright (c) 2009-2013 Panxiaobo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package pxb.android.axml;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* dump axml to stdout
|
||||
*
|
||||
* @author <a href="mailto:pxb1988@gmail.com">Panxiaobo</a>
|
||||
*/
|
||||
public class DumpAdapter extends AxmlVisitor {
|
||||
protected int deep;
|
||||
protected Map<String, String> nses;
|
||||
|
||||
public DumpAdapter() {
|
||||
this(null);
|
||||
}
|
||||
|
||||
public DumpAdapter(NodeVisitor nv) {
|
||||
this(nv, 0, new HashMap<String, String>());
|
||||
}
|
||||
|
||||
public DumpAdapter(NodeVisitor nv, int x, Map<String, String> nses) {
|
||||
super(nv);
|
||||
this.deep = x;
|
||||
this.nses = nses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void attr(String ns, String name, int resourceId, int type, Object obj) {
|
||||
for (int i = 0; i < deep; i++) {
|
||||
System.out.print(" ");
|
||||
}
|
||||
if (ns != null) {
|
||||
System.out.print(String.format("%s:", getPrefix(ns)));
|
||||
}
|
||||
System.out.print(name);
|
||||
if (resourceId != -1) {
|
||||
System.out.print(String.format("(%08x)", resourceId));
|
||||
}
|
||||
if (obj instanceof String) {
|
||||
System.out.print(String.format("=[%08x]\"%s\"", type, obj));
|
||||
} else if (obj instanceof Boolean) {
|
||||
System.out.print(String.format("=[%08x]\"%b\"", type, obj));
|
||||
} else if (obj instanceof ValueWrapper) {
|
||||
ValueWrapper w = (ValueWrapper) obj;
|
||||
System.out.print(String.format("=[%08x]@%08x, raw: \"%s\"", type, w.ref, w.raw));
|
||||
} else if (type == TYPE_REFERENCE) {
|
||||
System.out.print(String.format("=[%08x]@%08x", type, obj));
|
||||
} else {
|
||||
System.out.print(String.format("=[%08x]%08x", type, obj));
|
||||
}
|
||||
System.out.println();
|
||||
super.attr(ns, name, resourceId, type, obj);
|
||||
}
|
||||
|
||||
@Override
|
||||
public NodeVisitor child(String ns, String name) {
|
||||
for (int i = 0; i < deep; i++) {
|
||||
System.out.print(" ");
|
||||
}
|
||||
System.out.print("<");
|
||||
if (ns != null) {
|
||||
System.out.print(getPrefix(ns) + ":");
|
||||
}
|
||||
System.out.println(name);
|
||||
NodeVisitor nv = super.child(ns, name);
|
||||
if (nv != null) {
|
||||
return new DumpAdapter(nv, deep + 1, nses);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected String getPrefix(String uri) {
|
||||
if (nses != null) {
|
||||
String prefix = nses.get(uri);
|
||||
if (prefix != null) {
|
||||
return prefix;
|
||||
}
|
||||
}
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void ns(String prefix, String uri, int ln) {
|
||||
System.out.println(prefix + "=" + uri);
|
||||
this.nses.put(uri, prefix);
|
||||
super.ns(prefix, uri, ln);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void text(int ln, String value) {
|
||||
for (int i = 0; i < deep + 1; i++) {
|
||||
System.out.print(" ");
|
||||
}
|
||||
System.out.print("T: ");
|
||||
System.out.println(value);
|
||||
super.text(ln, value);
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,97 @@
|
|||
/*
|
||||
* Copyright (c) 2009-2013 Panxiaobo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package pxb.android.axml;
|
||||
|
||||
public abstract class NodeVisitor {
|
||||
|
||||
public static final int TYPE_FIRST_INT = 0x10;
|
||||
public static final int TYPE_INT_BOOLEAN = 0x12;
|
||||
public static final int TYPE_INT_HEX = 0x11;
|
||||
public static final int TYPE_REFERENCE = 0x01;
|
||||
public static final int TYPE_STRING = 0x03;
|
||||
protected NodeVisitor nv;
|
||||
|
||||
public NodeVisitor() {
|
||||
super();
|
||||
}
|
||||
|
||||
public NodeVisitor(NodeVisitor nv) {
|
||||
super();
|
||||
this.nv = nv;
|
||||
}
|
||||
|
||||
/**
|
||||
* add attribute to the node
|
||||
*
|
||||
* @param ns
|
||||
* @param name
|
||||
* @param resourceId
|
||||
* @param type
|
||||
* {@link #TYPE_STRING} or others
|
||||
* @param obj
|
||||
* a string for {@link #TYPE_STRING} ,and Integer for others
|
||||
*/
|
||||
public void attr(String ns, String name, int resourceId, int type, Object obj) {
|
||||
if (nv != null) {
|
||||
nv.attr(ns, name, resourceId, type, obj);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* create a child node
|
||||
*
|
||||
* @param ns
|
||||
* @param name
|
||||
* @return
|
||||
*/
|
||||
public NodeVisitor child(String ns, String name) {
|
||||
if (nv != null) {
|
||||
return nv.child(ns, name);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* end the visit
|
||||
*/
|
||||
public void end() {
|
||||
if (nv != null) {
|
||||
nv.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* line number in the .xml
|
||||
*
|
||||
* @param ln
|
||||
*/
|
||||
public void line(int ln) {
|
||||
if (nv != null) {
|
||||
nv.line(ln);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* the node text
|
||||
*
|
||||
* @param value
|
||||
*/
|
||||
public void text(int lineNumber, String value) {
|
||||
if (nv != null) {
|
||||
nv.text(lineNumber, value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (c) 2009-2013 Panxiaobo
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package pxb.android.axml;
|
||||
|
||||
import java.io.BufferedReader;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.InputStreamReader;
|
||||
import java.io.OutputStream;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Util {
|
||||
public static byte[] readFile(File in) throws IOException {
|
||||
InputStream is = new FileInputStream(in);
|
||||
byte[] xml = new byte[is.available()];
|
||||
is.read(xml);
|
||||
is.close();
|
||||
return xml;
|
||||
}
|
||||
|
||||
public static byte[] readIs(InputStream is) throws IOException {
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
copy(is, os);
|
||||
return os.toByteArray();
|
||||
}
|
||||
|
||||
public static void writeFile(byte[] data, File out) throws IOException {
|
||||
FileOutputStream fos = new FileOutputStream(out);
|
||||
fos.write(data);
|
||||
fos.close();
|
||||
}
|
||||
|
||||
public static Map<String, String> readProguardConfig(File config) throws IOException {
|
||||
Map<String, String> clzMap = new HashMap<String, String>();
|
||||
BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(config), "utf8"));
|
||||
try {
|
||||
for (String ln = r.readLine(); ln != null; ln = r.readLine()) {
|
||||
if (ln.startsWith("#") || ln.startsWith(" ")) {
|
||||
continue;
|
||||
}
|
||||
// format a.pt.Main -> a.a.a:
|
||||
int i = ln.indexOf("->");
|
||||
if (i > 0) {
|
||||
clzMap.put(ln.substring(0, i).trim(), ln.substring(i + 2, ln.length() - 1).trim());
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
r.close();
|
||||
}
|
||||
return clzMap;
|
||||
}
|
||||
|
||||
public static void copy(InputStream is, OutputStream os) throws IOException {
|
||||
byte[] xml = new byte[10 * 1024];
|
||||
for (int c = is.read(xml); c > 0; c = is.read(xml)) {
|
||||
os.write(xml, 0, c);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
package pxb.android.axml;
|
||||
|
||||
public class ValueWrapper {
|
||||
|
||||
public static final int ID = 1;
|
||||
public static final int STYLE = 2;
|
||||
public static final int CLASS = 3;
|
||||
public final int type;
|
||||
public final String raw;
|
||||
public final int ref;
|
||||
|
||||
private ValueWrapper(int type, int ref, String raw) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.raw = raw;
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
public ValueWrapper replaceRaw(String raw) {
|
||||
return new ValueWrapper(type, ref, raw);
|
||||
}
|
||||
|
||||
public static ValueWrapper wrapId(int ref, String raw) {
|
||||
return new ValueWrapper(ID, ref, raw);
|
||||
}
|
||||
|
||||
public static ValueWrapper wrapStyle(int ref, String raw) {
|
||||
return new ValueWrapper(STYLE, ref, raw);
|
||||
}
|
||||
|
||||
public static ValueWrapper wrapClass(int ref, String raw) {
|
||||
return new ValueWrapper(CLASS, ref, raw);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="#eee"/>
|
||||
<size android:height="1dp"/>
|
||||
</shape>
|
|
@ -0,0 +1,9 @@
|
|||
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:shape="rectangle">
|
||||
<gradient
|
||||
android:angle="135"
|
||||
android:centerColor="#009688"
|
||||
android:endColor="#00695C"
|
||||
android:startColor="#4DB6AC"
|
||||
android:type="linear" />
|
||||
</shape>
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.drawerlayout.widget.DrawerLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/drawer_layout"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true"
|
||||
tools:openDrawer="start">
|
||||
|
||||
<include
|
||||
layout="@layout/app_bar_main"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.google.android.material.navigation.NavigationView
|
||||
android:id="@+id/nav_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="start"
|
||||
android:fitsSystemWindows="true"
|
||||
app:headerLayout="@layout/nav_header_main"
|
||||
app:menu="@menu/activity_main_drawer" />
|
||||
</androidx.drawerlayout.widget.DrawerLayout>
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:theme="@style/AppTheme.AppBarOverlay">
|
||||
|
||||
<androidx.appcompat.widget.Toolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?attr/actionBarSize"
|
||||
android:background="?attr/colorPrimary"
|
||||
app:popupTheme="@style/AppTheme.PopupOverlay" />
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.FloatingActionButton
|
||||
android:id="@+id/launch"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="bottom|center"
|
||||
android:layout_margin="@dimen/fab_margin"
|
||||
app:srcCompat="@android:drawable/ic_menu_send" />
|
||||
|
||||
<include layout="@layout/content_main" />
|
||||
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:layout_behavior="@string/appbar_scrolling_view_behavior"
|
||||
tools:showIn="@layout/app_bar_main">
|
||||
|
||||
<fragment
|
||||
android:id="@+id/nav_host_fragment"
|
||||
android:name="androidx.navigation.fragment.NavHostFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
app:defaultNavHost="true"
|
||||
app:layout_constraintLeft_toLeftOf="parent"
|
||||
app:layout_constraintRight_toRightOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:navGraph="@navigation/mobile_navigation" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,14 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.config.ConfigFragment">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/view_mod_list"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,66 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_marginBottom="60dp"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline_v1"
|
||||
app:layout_constraintEnd_toStartOf="@id/guideline_v2"
|
||||
app:layout_constraintTop_toBottomOf="@id/guideline_h1"
|
||||
app:layout_constraintBottom_toTopOf="@id/guideline_h2">
|
||||
<EditText
|
||||
android:id="@+id/edit_text_config_edit"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textMultiLine"
|
||||
android:gravity="top|start"/>
|
||||
</ScrollView>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_config_save"
|
||||
android:text="@string/save"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline_v1"
|
||||
app:layout_constraintTop_toBottomOf="@id/guideline_h2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_config_cancel"
|
||||
android:text="@string/cancel"
|
||||
app:layout_constraintEnd_toStartOf="@id/guideline_v2"
|
||||
app:layout_constraintTop_toBottomOf="@id/guideline_h2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_h1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="20dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_h2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_end="60dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_v1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_begin="20dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_v2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_end="20dp" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,54 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.help.HelpFragment">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_compat"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_compat"
|
||||
app:layout_constraintTop_toBottomOf="@id/guideline_h1"
|
||||
app:layout_constraintStart_toEndOf="@id/guideline_v1"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_nexus"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_nexus"
|
||||
app:layout_constraintTop_toBottomOf="@id/guideline_h1"
|
||||
app:layout_constraintStart_toEndOf="@id/button_compat" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_logs"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_logs"
|
||||
app:layout_constraintTop_toBottomOf="@id/guideline_h1"
|
||||
app:layout_constraintStart_toEndOf="@id/button_nexus" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_release"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_release"
|
||||
app:layout_constraintTop_toBottomOf="@id/guideline_h1"
|
||||
app:layout_constraintStart_toEndOf="@id/button_logs" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_h1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_begin="20dp" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_v1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintGuide_begin="20dp" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,48 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".ui.install.InstallFragment">
|
||||
|
||||
<Button
|
||||
android:id="@+id/button_install"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/button_install"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/guideline_h1"
|
||||
android:text="@string/text_install_tip1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<TextView
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/guideline_h2"
|
||||
android:text="@string/text_install_tip2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_h1"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.2" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/guideline_h2"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintGuide_percent="0.3" />
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginEnd="80dp"
|
||||
android:divider="@drawable/divider"
|
||||
android:showDividers="middle"
|
||||
android:orientation="vertical">
|
||||
<TextView
|
||||
android:id="@+id/text_view_mod_name"
|
||||
android:gravity="start"
|
||||
android:textSize="20sp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/text_view_mod_description"
|
||||
android:gravity="start"
|
||||
android:textSize="16sp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true">
|
||||
<Button
|
||||
android:id="@+id/button_remove_mod"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:background="@android:drawable/ic_menu_delete" />
|
||||
<Button
|
||||
android:id="@+id/button_config_mod"
|
||||
android:layout_width="32dp"
|
||||
android:layout_height="32dp"
|
||||
android:background="@android:drawable/ic_menu_edit" />
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
|
@ -0,0 +1,36 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/nav_header_height"
|
||||
android:background="@drawable/side_nav_bar"
|
||||
android:gravity="bottom"
|
||||
android:orientation="vertical"
|
||||
android:paddingBottom="@dimen/activity_vertical_margin"
|
||||
android:paddingLeft="@dimen/activity_horizontal_margin"
|
||||
android:paddingRight="@dimen/activity_horizontal_margin"
|
||||
android:paddingTop="@dimen/activity_vertical_margin"
|
||||
android:theme="@style/ThemeOverlay.AppCompat.Dark">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/imageView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@string/nav_header_desc"
|
||||
android:paddingTop="@dimen/nav_header_vertical_spacing"
|
||||
app:srcCompat="@mipmap/ic_launcher_round" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="@dimen/nav_header_vertical_spacing"
|
||||
android:text="@string/nav_header_title"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/nav_header_subtitle" />
|
||||
|
||||
</LinearLayout>
|
|
@ -0,0 +1,20 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:showIn="navigation_view">
|
||||
|
||||
<group android:checkableBehavior="single">
|
||||
<item
|
||||
android:id="@+id/nav_install"
|
||||
android:icon="@android:drawable/ic_menu_add"
|
||||
android:title="@string/menu_install" />
|
||||
<item
|
||||
android:id="@+id/nav_config"
|
||||
android:icon="@android:drawable/ic_menu_edit"
|
||||
android:title="@string/menu_config" />
|
||||
<item
|
||||
android:id="@+id/nav_help"
|
||||
android:icon="@android:drawable/ic_menu_help"
|
||||
android:title="@string/menu_help" />
|
||||
</group>
|
||||
</menu>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue