├── .gitignore ├── .gitmodules ├── .idea ├── .name ├── codeStyles │ └── Project.xml ├── gradle.xml ├── misc.xml ├── runConfigurations.xml └── vcs.xml ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── io │ │ └── mrarm │ │ └── mcversion │ │ └── ExampleInstrumentedTest.java │ ├── main │ ├── AndroidManifest.xml │ ├── cpp │ │ ├── CMakeLists.txt │ │ └── native-lib.cpp │ ├── ic_launcher-web.png │ ├── java │ │ └── io │ │ │ └── mrarm │ │ │ └── mcversion │ │ │ ├── FileDownloader.java │ │ │ ├── GoogleLoginActivity.java │ │ │ ├── IOUtil.java │ │ │ ├── MainActivity.java │ │ │ ├── PlayApi.java │ │ │ ├── PlayHelper.java │ │ │ ├── UiThreadHelper.java │ │ │ ├── UiVersion.java │ │ │ ├── VersionDownloader.java │ │ │ ├── VersionInstaller.java │ │ │ ├── VersionList.java │ │ │ └── VersionListAdapter.java │ └── res │ │ ├── drawable-v24 │ │ └── ic_launcher_foreground.xml │ │ ├── drawable │ │ ├── ic_launcher_background.xml │ │ └── ic_launcher_foreground.xml │ │ ├── layout │ │ ├── activity_main.xml │ │ └── version_entry.xml │ │ ├── mipmap-anydpi-v26 │ │ ├── ic_launcher.xml │ │ └── ic_launcher_round.xml │ │ ├── mipmap-hdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-mdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── mipmap-xxxhdpi │ │ ├── ic_launcher.png │ │ └── ic_launcher_round.png │ │ ├── raw │ │ ├── device_arm64.conf │ │ └── google_ca.pem │ │ └── values │ │ ├── colors.xml │ │ ├── dimens.xml │ │ ├── ic_launcher_background.xml │ │ ├── strings.xml │ │ └── styles.xml │ └── test │ └── java │ └── io │ └── mrarm │ └── mcversion │ └── ExampleUnitTest.java ├── build.gradle ├── deps ├── boringssl.cmake ├── curl.cmake ├── libuv.cmake ├── modules │ ├── FindCURL.cmake │ ├── FindOpenSSL.cmake │ └── FindProtobuf.cmake └── protobuf.cmake ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | /deps/protoc 16 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/google-play-api"] 2 | path = deps/google-play-api 3 | url = git@github.com:MCMrARM/Google-Play-API.git 4 | [submodule "deps/protobuf"] 5 | path = deps/protobuf 6 | url = git@github.com:protocolbuffers/protobuf.git 7 | -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | MinecraftVersion -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |
7 | 8 | 9 | 10 | xmlns:android 11 | 12 | ^$ 13 | 14 | 15 | 16 |
17 |
18 | 19 | 20 | 21 | xmlns:.* 22 | 23 | ^$ 24 | 25 | 26 | BY_NAME 27 | 28 |
29 |
30 | 31 | 32 | 33 | .*:id 34 | 35 | http://schemas.android.com/apk/res/android 36 | 37 | 38 | 39 |
40 |
41 | 42 | 43 | 44 | .*:name 45 | 46 | http://schemas.android.com/apk/res/android 47 | 48 | 49 | 50 |
51 |
52 | 53 | 54 | 55 | name 56 | 57 | ^$ 58 | 59 | 60 | 61 |
62 |
63 | 64 | 65 | 66 | style 67 | 68 | ^$ 69 | 70 | 71 | 72 |
73 |
74 | 75 | 76 | 77 | .* 78 | 79 | ^$ 80 | 81 | 82 | BY_NAME 83 | 84 |
85 |
86 | 87 | 88 | 89 | .* 90 | 91 | http://schemas.android.com/apk/res/android 92 | 93 | 94 | ANDROID_ATTRIBUTE_ORDER 95 | 96 |
97 |
98 | 99 | 100 | 101 | .* 102 | 103 | .* 104 | 105 | 106 | BY_NAME 107 | 108 |
109 |
110 |
111 |
112 |
113 |
-------------------------------------------------------------------------------- /.idea/gradle.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 14 | -------------------------------------------------------------------------------- /.idea/runConfigurations.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 28 5 | buildToolsVersion "29.0.3" 6 | defaultConfig { 7 | applicationId "io.mrarm.mcversion" 8 | minSdkVersion 17 9 | targetSdkVersion 28 10 | versionCode 1 11 | versionName "1.0" 12 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 13 | externalNativeBuild { 14 | cmake { 15 | cppFlags "" 16 | } 17 | } 18 | ndk { 19 | abiFilters "arm64-v8a" 20 | } 21 | } 22 | buildTypes { 23 | release { 24 | minifyEnabled false 25 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' 26 | } 27 | } 28 | dataBinding { 29 | enabled = true 30 | } 31 | externalNativeBuild { 32 | cmake { 33 | path "src/main/cpp/CMakeLists.txt" 34 | } 35 | } 36 | compileOptions { 37 | sourceCompatibility = 1.8 38 | targetCompatibility = 1.8 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation fileTree(dir: 'libs', include: ['*.jar']) 44 | implementation 'androidx.appcompat:appcompat:1.1.0' 45 | implementation 'androidx.constraintlayout:constraintlayout:1.1.3' 46 | implementation 'com.google.android.material:material:1.1.0' 47 | implementation 'com.google.code.gson:gson:2.8.6' 48 | implementation('com.github.mcmrarm:dataadapter:master-SNAPSHOT') {changing = true} 49 | implementation('com.github.mcmrarm:observabletransform:master-SNAPSHOT') {changing = true} 50 | testImplementation 'junit:junit:4.12' 51 | androidTestImplementation 'androidx.test.ext:junit:1.1.0' 52 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' 53 | } 54 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/mrarm/mcversion/ExampleInstrumentedTest.java: -------------------------------------------------------------------------------- 1 | package io.mrarm.mcversion; 2 | 3 | import android.content.Context; 4 | 5 | import androidx.test.platform.app.InstrumentationRegistry; 6 | import androidx.test.ext.junit.runners.AndroidJUnit4; 7 | 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | 11 | import static org.junit.Assert.*; 12 | 13 | /** 14 | * Instrumented test, which will execute on an Android device. 15 | * 16 | * @see Testing documentation 17 | */ 18 | @RunWith(AndroidJUnit4.class) 19 | public class ExampleInstrumentedTest { 20 | @Test 21 | public void useAppContext() { 22 | // Context of the app under test. 23 | Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); 24 | 25 | assertEquals("io.mrarm.mcversion", appContext.getPackageName()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /app/src/main/cpp/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.4.1) 2 | 3 | set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/../../../../deps/modules" ${CMAKE_MODULE_PATH}) 4 | 5 | set(CROSS_COMPILE_ARSG "-G${CMAKE_GENERATOR}") 6 | set(CROSS_COMPILE_CACHE_ARGS "-DCMAKE_TOOLCHAIN_FILE:FILEPATH=${CMAKE_TOOLCHAIN_FILE}" 7 | "-DANDROID_ABI:STRING=${ANDROID_ABI}" "-DANDROID_PLATFORM:STRING=${ANDROID_PLATFORM}" 8 | "-DANDROID_NDK:PATH=${ANDROID_NDK}" "-DCMAKE_MAKE_PROGRAM:FILEPATH=${CMAKE_MAKE_PROGRAM}") 9 | 10 | include(../../../../deps/boringssl.cmake) 11 | include(../../../../deps/curl.cmake) 12 | include(../../../../deps/libuv.cmake) 13 | include(../../../../deps/protobuf.cmake) 14 | add_subdirectory(../../../../deps/google-play-api google-play-api) 15 | 16 | add_library(native-lib SHARED native-lib.cpp ../../../../deps/google-play-api/src/config.cpp) 17 | 18 | target_link_libraries(native-lib gplayapi) -------------------------------------------------------------------------------- /app/src/main/cpp/native-lib.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include "../../../../deps/google-play-api/src/config.h" 11 | 12 | JavaVM *jvm; 13 | 14 | class JVMAttacher { 15 | private: 16 | JNIEnv *envPtr; 17 | bool needsDetach = false; 18 | 19 | public: 20 | JVMAttacher() { 21 | int ret = jvm->GetEnv((void **) &envPtr, JNI_VERSION_1_4); 22 | if (ret == JNI_EDETACHED) { 23 | needsDetach = true; 24 | if (jvm->AttachCurrentThread(&envPtr, NULL) != 0) 25 | throw std::runtime_error("AttachCurrentThread failed"); 26 | } else if (ret != JNI_OK) { 27 | throw std::runtime_error("GetEnv failed"); 28 | } 29 | } 30 | 31 | ~JVMAttacher() { 32 | if (needsDetach) { 33 | jvm->DetachCurrentThread(); 34 | } 35 | } 36 | 37 | JNIEnv *env() const { 38 | return envPtr; 39 | } 40 | }; 41 | 42 | extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* reserved) { 43 | jvm = vm; 44 | return JNI_VERSION_1_4; 45 | } 46 | 47 | 48 | struct NativePlayApi { 49 | playapi::device_info device; 50 | playapi::file_login_cache loginCache; 51 | playapi::login_api loginApi; 52 | playapi::api api; 53 | std::unique_ptr deviceConfig; 54 | 55 | NativePlayApi(std::string dataPath) : loginCache(dataPath + "login_cache.json"), loginApi(device, loginCache), api(device) { 56 | } 57 | }; 58 | 59 | static std::string stringFromJava(JNIEnv *env, jstring str) { 60 | const char *cstr = env->GetStringUTFChars(str, nullptr); 61 | std::string ret (cstr); 62 | env->ReleaseStringUTFChars(str, cstr); 63 | return ret; 64 | } 65 | 66 | 67 | extern "C" JNIEXPORT void JNICALL 68 | Java_io_mrarm_mcversion_PlayApi_initCurlSsl(JNIEnv* env, jclass, jstring jCaPath) { 69 | std::string caPath = stringFromJava(env, jCaPath); 70 | playapi::http_request::set_platform_curl_init_hook([caPath](CURL *curl) { 71 | curl_easy_setopt(curl, CURLOPT_CAINFO, caPath.c_str()); 72 | }); 73 | } 74 | 75 | extern "C" JNIEXPORT jlong JNICALL 76 | Java_io_mrarm_mcversion_PlayApi_init(JNIEnv *env, jclass, jstring jDataPath) { 77 | auto obj = new NativePlayApi(stringFromJava(env, jDataPath)); 78 | return (jlong) (uintptr_t) obj; 79 | } 80 | 81 | extern "C" JNIEXPORT void JNICALL 82 | Java_io_mrarm_mcversion_PlayApi_setDevice(JNIEnv *env, jclass, jlong jSelf, jstring jDeviceConfig, 83 | jstring jStatePath) { 84 | auto self = (NativePlayApi *) (size_t) jSelf; 85 | 86 | { 87 | std::stringstream deviceConfig(stringFromJava(env, jDeviceConfig)); 88 | playapi::config devInfoConfig; 89 | devInfoConfig.load(deviceConfig); 90 | self->device.load(devInfoConfig); 91 | } 92 | 93 | self->deviceConfig = std::make_unique(stringFromJava(env, jStatePath)); 94 | self->deviceConfig->load(); 95 | self->deviceConfig->load_device_info_data(self->device); 96 | self->device.generate_fields(); 97 | self->deviceConfig->set_device_info_data(self->device); 98 | self->deviceConfig->save(); 99 | self->loginApi.set_checkin_data(self->deviceConfig->checkin_data); 100 | } 101 | 102 | extern "C" JNIEXPORT void JNICALL 103 | Java_io_mrarm_mcversion_PlayApi_setLoginToken(JNIEnv *env, jclass, jlong jSelf, 104 | jstring jId, jstring jToken) { 105 | auto self = (NativePlayApi *) (size_t) jSelf; 106 | 107 | self->loginApi.set_token(stringFromJava(env, jId), stringFromJava(env, jToken)); 108 | } 109 | 110 | static std::function makeErrorCallback(jobject gCallback) { 111 | return [gCallback](std::exception_ptr err) { 112 | try { 113 | std::rethrow_exception(err); 114 | } catch (std::exception &e) { 115 | JVMAttacher attacher; 116 | auto mid = attacher.env()->GetMethodID(attacher.env()->GetObjectClass(gCallback), "onError", "(Ljava/lang/String;)V"); 117 | attacher.env()->CallVoidMethod(gCallback, mid, attacher.env()->NewStringUTF(e.what())); 118 | attacher.env()->DeleteGlobalRef(gCallback); 119 | } 120 | }; 121 | } 122 | 123 | extern "C" JNIEXPORT void JNICALL 124 | Java_io_mrarm_mcversion_PlayApi_loginWithAccessToken(JNIEnv *env, jclass, jlong jSelf, 125 | jstring jId, jstring jToken, jobject callback) { 126 | auto self = (NativePlayApi *) (size_t) jSelf; 127 | auto gCallback = env->NewGlobalRef(callback); 128 | 129 | self->loginApi.perform_with_access_token(stringFromJava(env, jToken), stringFromJava(env, jId), true)->call([self, gCallback]() { 130 | JVMAttacher attacher; 131 | auto mid = attacher.env()->GetMethodID(attacher.env()->GetObjectClass(gCallback), "onSuccess", "(Ljava/lang/String;)V"); 132 | attacher.env()->CallVoidMethod(gCallback, mid, attacher.env()->NewStringUTF(self->loginApi.get_token().c_str())); 133 | attacher.env()->DeleteGlobalRef(gCallback); 134 | }, makeErrorCallback(gCallback)); 135 | } 136 | 137 | static void doCheckIn(NativePlayApi *self, std::function successCb, std::function errCb) { 138 | if (self->deviceConfig->checkin_data.android_id == 0) { 139 | auto checkin = std::make_shared(self->device); 140 | checkin->add_auth(self->loginApi)->call([self, checkin, successCb, errCb]() { 141 | checkin->perform_checkin()->call([self, checkin, successCb](auto data) { 142 | self->deviceConfig->checkin_data = data; 143 | self->deviceConfig->save(); 144 | successCb(); 145 | }, errCb); 146 | }, errCb); 147 | } else { 148 | successCb(); 149 | } 150 | } 151 | 152 | static void doAuthToApi(NativePlayApi *self, std::function successCb, std::function errCb) { 153 | self->api.set_checkin_data(self->deviceConfig->checkin_data); 154 | self->deviceConfig->load_api_data(self->loginApi.get_email(), self->api); 155 | self->api.set_auth(self->loginApi)->call([self, successCb, errCb]() { 156 | if (self->api.device_config_token.length() == 0) { 157 | self->api.fetch_toc()->call([self, successCb, errCb](auto toc) { 158 | if (toc.payload().tocresponse().requiresuploaddeviceconfig()) { 159 | self->api.upload_device_config()->call([self, successCb](auto resp) { 160 | self->api.info_mutex.lock(); 161 | self->api.device_config_token = resp.payload().uploaddeviceconfigresponse().uploaddeviceconfigtoken(); 162 | self->deviceConfig->set_api_data(self->loginApi.get_email(), self->api); 163 | self->api.info_mutex.unlock(); 164 | 165 | self->deviceConfig->save(); 166 | successCb(); 167 | }, errCb); 168 | } else { 169 | successCb(); 170 | } 171 | }, errCb); 172 | } else { 173 | successCb(); 174 | } 175 | }, errCb); 176 | } 177 | 178 | extern "C" JNIEXPORT void JNICALL 179 | Java_io_mrarm_mcversion_PlayApi_authToApi(JNIEnv *env, jclass, jlong jSelf, jobject callback) { 180 | auto self = (NativePlayApi *) (size_t) jSelf; 181 | auto gCallback = env->NewGlobalRef(callback); 182 | 183 | auto errCb = makeErrorCallback(gCallback); 184 | 185 | doCheckIn(self, [self, errCb, gCallback]() { 186 | doAuthToApi(self, [gCallback]() { 187 | JVMAttacher attacher; 188 | auto mid = attacher.env()->GetMethodID(attacher.env()->GetObjectClass(gCallback), "onSuccess", "()V"); 189 | attacher.env()->CallVoidMethod(gCallback, mid); 190 | attacher.env()->DeleteGlobalRef(gCallback); 191 | }, errCb); 192 | }, errCb); 193 | } 194 | 195 | extern "C" JNIEXPORT void JNICALL 196 | Java_io_mrarm_mcversion_PlayApi_requestDelivery(JNIEnv *env, jclass, jlong jSelf, 197 | jstring jPkg, jint version, jobject callback) { 198 | auto self = (NativePlayApi *) (size_t) jSelf; 199 | auto gCallback = env->NewGlobalRef(callback); 200 | 201 | self->api.delivery(stringFromJava(env, jPkg), version, std::string())->call([self, gCallback](auto resp) { 202 | JVMAttacher attacher; 203 | auto env = attacher.env(); 204 | 205 | auto const &dd = resp.payload().deliveryresponse().appdeliverydata(); 206 | std::vector downloadLinks; 207 | std::vector downloadSizes; 208 | if (dd.has_downloadurl() || dd.has_gzippeddownloadurl()) { 209 | downloadLinks.push_back("main"); 210 | downloadLinks.push_back(dd.downloadurl()); 211 | downloadLinks.push_back(dd.gzippeddownloadurl()); 212 | downloadSizes.push_back(dd.downloadsize()); 213 | } 214 | for (auto const &d : dd.splitdeliverydata()) { 215 | downloadLinks.push_back(d.id()); 216 | downloadLinks.push_back(d.downloadurl()); 217 | downloadLinks.push_back(d.gzippeddownloadurl()); 218 | downloadSizes.push_back(d.downloadsize()); 219 | } 220 | 221 | auto arr = env->NewObjectArray((jsize) downloadLinks.size(), env->FindClass("java/lang/String"), nullptr); 222 | for (ssize_t i = downloadLinks.size() - 1; i >= 0; --i) { 223 | auto ref = env->NewStringUTF(downloadLinks[i].c_str()); 224 | env->SetObjectArrayElement(arr, (jsize) i, ref); 225 | env->DeleteLocalRef(ref); 226 | } 227 | auto arr2 = env->NewLongArray((jsize) downloadSizes.size()); 228 | env->SetLongArrayRegion(arr2, 0, (jsize) downloadSizes.size(), (const jlong *) downloadSizes.data()); 229 | 230 | auto mid = env->GetMethodID(env->GetObjectClass(gCallback), "onSuccess", "([Ljava/lang/String;[J)V"); 231 | env->CallVoidMethod(gCallback, mid, arr, arr2); 232 | env->DeleteGlobalRef(gCallback); 233 | }, makeErrorCallback(gCallback)); 234 | } 235 | -------------------------------------------------------------------------------- /app/src/main/ic_launcher-web.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCMrARM/mc-android-version-switcher/71ef27304830bd051e78aae454d79dc570178cf1/app/src/main/ic_launcher-web.png -------------------------------------------------------------------------------- /app/src/main/java/io/mrarm/mcversion/FileDownloader.java: -------------------------------------------------------------------------------- 1 | package io.mrarm.mcversion; 2 | 3 | import java.io.File; 4 | import java.io.FileOutputStream; 5 | import java.io.IOException; 6 | import java.io.InputStream; 7 | import java.io.OutputStream; 8 | import java.net.HttpURLConnection; 9 | import java.net.URL; 10 | 11 | public class FileDownloader { 12 | 13 | private HttpURLConnection conn; 14 | private CompletionCallback completionCallback; 15 | private ErrorCallback errorCallback; 16 | private ProgressCallback progressCallback; 17 | 18 | private synchronized void assertNotExecuted() { 19 | if (conn != null) 20 | throw new RuntimeException("Can only set conn before execute()"); 21 | } 22 | 23 | public void setCompletionCallback(CompletionCallback completionCallback) { 24 | assertNotExecuted(); 25 | this.completionCallback = completionCallback; 26 | } 27 | 28 | public void setErrorCallback(ErrorCallback errorCallback) { 29 | assertNotExecuted(); 30 | this.errorCallback = errorCallback; 31 | } 32 | 33 | public void setProgressCallback(ProgressCallback progressCallback) { 34 | assertNotExecuted(); 35 | this.progressCallback = progressCallback; 36 | } 37 | 38 | public void execute(String url, File outPath) { 39 | try { 40 | HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); 41 | synchronized (this) { 42 | this.conn = conn; 43 | } 44 | if (conn == null) 45 | throw new IOException(); 46 | long contentLength = conn.getContentLength(); 47 | try (InputStream inStream = conn.getInputStream(); 48 | OutputStream outStream = new FileOutputStream(outPath)) { 49 | byte[] b = new byte[128 * 1024]; 50 | int n; 51 | long downloaded = 0; 52 | while ((n = inStream.read(b)) > 0) { 53 | outStream.write(b, 0, n); 54 | downloaded += n; 55 | if (progressCallback != null) 56 | progressCallback.onProgress(downloaded, contentLength); 57 | } 58 | } 59 | completionCallback.onComplete(); 60 | } catch (IOException e) { 61 | errorCallback.onError(e); 62 | } finally { 63 | if (conn != null) 64 | conn.disconnect(); 65 | } 66 | } 67 | 68 | public synchronized void cancel() { 69 | if (conn != null) 70 | conn.disconnect(); 71 | } 72 | 73 | public interface CompletionCallback { 74 | void onComplete(); 75 | } 76 | 77 | public interface ErrorCallback { 78 | void onError(IOException exception); 79 | } 80 | 81 | public interface ProgressCallback { 82 | void onProgress(long downloaded, long total); 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /app/src/main/java/io/mrarm/mcversion/GoogleLoginActivity.java: -------------------------------------------------------------------------------- 1 | package io.mrarm.mcversion; 2 | 3 | import android.graphics.Bitmap; 4 | import android.os.Bundle; 5 | import android.util.Log; 6 | import android.webkit.CookieManager; 7 | import android.webkit.JavascriptInterface; 8 | import android.webkit.WebView; 9 | import android.webkit.WebViewClient; 10 | import android.widget.Toast; 11 | 12 | import androidx.appcompat.app.AppCompatActivity; 13 | 14 | public class GoogleLoginActivity extends AppCompatActivity { 15 | 16 | private String accountIdentifier; 17 | 18 | @Override 19 | protected void onCreate(Bundle savedInstanceState) { 20 | super.onCreate(savedInstanceState); 21 | 22 | CookieManager.getInstance().removeAllCookie(); 23 | 24 | WebView view = new WebView(this); 25 | view.addJavascriptInterface(new LoginInterface(), "mm"); 26 | view.setWebViewClient(new WebViewClient() { 27 | 28 | @Override 29 | public void doUpdateVisitedHistory(WebView view, String url, boolean isReload) { 30 | if (view.getUrl().endsWith("#close")) { 31 | completeLogin(); 32 | } 33 | super.doUpdateVisitedHistory(view, url, isReload); 34 | } 35 | }); 36 | view.getSettings().setJavaScriptEnabled(true); 37 | setContentView(view); 38 | view.loadUrl("https://accounts.google.com/embedded/setup/v2/android?source=com.android.settings&xoauth_display_name=Android%20Phone&canFrp=1&canSk=1&lang=en&langCountry=en_us&hl=en-US&cc=us"); 39 | } 40 | 41 | private void completeLogin() { 42 | String accountToken = null; 43 | 44 | String cookies = CookieManager.getInstance().getCookie("https://accounts.google.com/"); 45 | Log.d("GoogleLoginActivity", "Cookies: " + cookies); 46 | for (String c : cookies.split(";")) { 47 | String v = c.trim(); 48 | int iof = v.indexOf("="); 49 | if (iof == -1) 50 | continue; 51 | String k = v.substring(0, iof); 52 | v = v.substring(iof + 1); 53 | if (k.equals("oauth_token")) 54 | accountToken = v; 55 | } 56 | CookieManager.getInstance().removeAllCookie(); 57 | 58 | if (accountToken == null) 59 | return; 60 | 61 | Log.d("GoogleLoginActivity", "Oauth token: " + accountToken); 62 | 63 | PlayHelper.getInstance(this).getApi().loginWithAccessToken(accountIdentifier, accountToken, new PlayApi.AccessTokenCallback() { 64 | @Override 65 | public void onSuccess(String token) { 66 | Log.d("GoogleLoginActivity", "Login success! " + token); 67 | PlayHelper.getInstance(GoogleLoginActivity.this).saveAccount(accountIdentifier, token); 68 | PlayHelper.getInstance(GoogleLoginActivity.this).requestAuthToApi(); 69 | 70 | finish(); 71 | } 72 | 73 | @Override 74 | public void onError(String str) { 75 | Log.e("GoogleLoginActivity", "Login failed! " + str); 76 | runOnUiThread(() -> Toast.makeText(GoogleLoginActivity.this, str, Toast.LENGTH_LONG).show()); 77 | finish(); 78 | } 79 | }); 80 | } 81 | 82 | public class LoginInterface { 83 | 84 | @JavascriptInterface 85 | public void showView() { 86 | } 87 | 88 | @JavascriptInterface 89 | public void setAccountIdentifier(String identifier) { 90 | Log.d("GoogleLoginActivity", "Account id: " + identifier); 91 | accountIdentifier = identifier; 92 | } 93 | 94 | @JavascriptInterface 95 | public void log(String text) { 96 | Log.d("GoogleLoginActivity", "WWW: " + text); 97 | } 98 | 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /app/src/main/java/io/mrarm/mcversion/IOUtil.java: -------------------------------------------------------------------------------- 1 | package io.mrarm.mcversion; 2 | 3 | import android.content.Context; 4 | 5 | import java.io.File; 6 | import java.io.FileOutputStream; 7 | import java.io.IOException; 8 | import java.io.InputStream; 9 | import java.io.OutputStream; 10 | 11 | public final class IOUtil { 12 | 13 | private IOUtil() { 14 | } 15 | 16 | public static byte[] readRawResource(Context ctx, int resId) { 17 | try (InputStream stream = ctx.getResources().openRawResource(resId)) { 18 | byte[] b = new byte[stream.available()]; 19 | int o = 0; 20 | while (o < b.length) { 21 | int r = stream.read(b, o, b.length - o); 22 | if (r == -1) 23 | throw new IOException(); 24 | o += r; 25 | } 26 | return b; 27 | } catch (IOException e) { 28 | throw new RuntimeException(e); 29 | } 30 | } 31 | 32 | public static void copyRawResource(Context ctx, int resId, File outPath) { 33 | try (InputStream inStream = ctx.getResources().openRawResource(resId); 34 | OutputStream outStream = new FileOutputStream(outPath)) { 35 | byte[] b = new byte[16 * 1024]; 36 | int n; 37 | while ((n = inStream.read(b)) >= 0) 38 | outStream.write(b, 0, n); 39 | } catch (IOException e) { 40 | throw new RuntimeException(e); 41 | } 42 | } 43 | 44 | public static void deleteDirectory(File file) { 45 | File[] files = file.listFiles(); 46 | if (files != null) { 47 | for (File child : files) 48 | deleteDirectory(child); 49 | } 50 | file.delete(); 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/io/mrarm/mcversion/MainActivity.java: -------------------------------------------------------------------------------- 1 | package io.mrarm.mcversion; 2 | 3 | import androidx.appcompat.app.AppCompatActivity; 4 | import androidx.databinding.DataBindingUtil; 5 | import androidx.recyclerview.widget.LinearLayoutManager; 6 | 7 | import android.content.Intent; 8 | import android.os.AsyncTask; 9 | import android.os.Bundle; 10 | import android.util.Log; 11 | import android.widget.Toast; 12 | 13 | import java.io.File; 14 | import java.io.IOException; 15 | import java.util.ArrayList; 16 | 17 | import io.mrarm.mcversion.databinding.ActivityMainBinding; 18 | 19 | public class MainActivity extends AppCompatActivity { 20 | 21 | static { 22 | System.loadLibrary("native-lib"); 23 | } 24 | 25 | private VersionInstaller installer; 26 | 27 | @Override 28 | protected void onCreate(Bundle savedInstanceState) { 29 | super.onCreate(savedInstanceState); 30 | 31 | ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main); 32 | 33 | VersionDownloader downloader = new VersionDownloader(new File(getFilesDir(), "minecraft")); 34 | installer = new VersionInstaller(); 35 | installer.updateInstalledVersion(this); 36 | installer.deleteHangingSession(this); 37 | installer.registerIntentHandler(this); 38 | VersionListAdapter adapter = new VersionListAdapter(downloader, installer); 39 | 40 | VersionList versionList = new VersionList(); 41 | File versionListCacheFile = new File(getCacheDir(), "versions.json"); 42 | versionList.loadCached(versionListCacheFile); 43 | AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> { 44 | try { 45 | Log.d("MainActivity", "Loading version list from network"); 46 | versionList.loadNetwork("https://raw.githubusercontent.com/minecraft-linux/mcpelauncher-versiondb/master/versions.json"); 47 | versionList.saveToCache(versionListCacheFile); 48 | Log.d("MainActivity", "Loaded version list from network: " + versionList.versions.size() + " versions"); 49 | runOnUiThread(() -> adapter.setVersions(versionList.versions)); 50 | } catch (IOException e) { 51 | Log.e("MainActivity", "Loading version list from network failed"); 52 | runOnUiThread(() -> Toast.makeText(this, e.getLocalizedMessage(), Toast.LENGTH_SHORT).show()); 53 | e.printStackTrace(); 54 | } 55 | }); 56 | 57 | adapter.setVersions(versionList.versions != null ? versionList.versions : new ArrayList<>()); 58 | 59 | binding.content.setLayoutManager(new LinearLayoutManager(this)); 60 | binding.content.setAdapter(adapter); 61 | setContentView(binding.content); 62 | 63 | if (!PlayHelper.getInstance(this).hasSavedAccount()) 64 | openGoogleLogin(); 65 | } 66 | 67 | @Override 68 | protected void onDestroy() { 69 | super.onDestroy(); 70 | installer.unregisterIntentHandler(this); 71 | } 72 | 73 | private void openGoogleLogin() { 74 | Intent intent = new Intent(this, GoogleLoginActivity.class); 75 | startActivity(intent); 76 | } 77 | 78 | } 79 | -------------------------------------------------------------------------------- /app/src/main/java/io/mrarm/mcversion/PlayApi.java: -------------------------------------------------------------------------------- 1 | package io.mrarm.mcversion; 2 | 3 | public final class PlayApi { 4 | 5 | private long handle; 6 | 7 | public PlayApi(String dataPath) { 8 | handle = init(dataPath); 9 | } 10 | 11 | public void setDevice(String deviceConfig, String statePath) { 12 | setDevice(handle, deviceConfig, statePath); 13 | } 14 | 15 | public void setLoginToken(String id, String token) { 16 | setLoginToken(handle, id, token); 17 | } 18 | 19 | public void loginWithAccessToken(String id, String token, AccessTokenCallback cb) { 20 | loginWithAccessToken(handle, id, token, cb); 21 | } 22 | 23 | public void authToApi(Callback callback) { 24 | authToApi(handle, callback); 25 | } 26 | 27 | public void requestDelivery(String pkgName, int pkgVer, DeliveryCallback cb) { 28 | requestDelivery(handle, pkgName, pkgVer, new NativeDeliveryCallback() { 29 | @Override 30 | public void onSuccess(String[] strings, long[] ints) { 31 | DownloadInfo[] info = new DownloadInfo[strings.length / 3]; 32 | for (int i = strings.length / 3 - 1; i >= 0; --i) 33 | info[i] = new DownloadInfo(strings[3 * i], strings[3 * i + 1], strings[3 * i + 2], ints[i]); 34 | cb.onSuccess(info); 35 | } 36 | 37 | @Override 38 | public void onError(String str) { 39 | cb.onError(str); 40 | } 41 | }); 42 | } 43 | 44 | public static native void initCurlSsl(String caPath); 45 | 46 | private static native long init(String dataPath); 47 | 48 | private static native long setDevice(long self, String deviceConfig, String statePath); 49 | 50 | private static native long setLoginToken(long self, String id, String token); 51 | 52 | private static native long loginWithAccessToken(long self, String id, String token, AccessTokenCallback callback); 53 | 54 | private static native long authToApi(long self, Callback callback); 55 | 56 | private static native long requestDelivery(long self, String pkgName, int pkgVer, NativeDeliveryCallback callback); 57 | 58 | public interface Callback { 59 | 60 | void onSuccess(); 61 | 62 | void onError(String str); 63 | 64 | } 65 | 66 | public interface AccessTokenCallback { 67 | 68 | void onSuccess(String token); 69 | 70 | void onError(String str); 71 | 72 | } 73 | 74 | private interface NativeDeliveryCallback { 75 | 76 | void onSuccess(String[] strings, long[] ints); 77 | 78 | void onError(String str); 79 | 80 | } 81 | 82 | public interface DeliveryCallback { 83 | 84 | void onSuccess(DownloadInfo[] download); 85 | 86 | void onError(String str); 87 | 88 | } 89 | 90 | public static class DownloadInfo { 91 | 92 | private final String name, url, gzippedUrl; 93 | private final long dlSize; 94 | 95 | private DownloadInfo(String name, String url, String gzippedUrl, long dlSize) { 96 | this.name = name; 97 | this.url = url; 98 | this.gzippedUrl = gzippedUrl; 99 | this.dlSize = dlSize; 100 | } 101 | 102 | public String getName() { 103 | return name; 104 | } 105 | 106 | public String getUrl() { 107 | return url; 108 | } 109 | 110 | public String getGzippedUrl() { 111 | return gzippedUrl; 112 | } 113 | 114 | public long getSize() { 115 | return dlSize; 116 | } 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /app/src/main/java/io/mrarm/mcversion/PlayHelper.java: -------------------------------------------------------------------------------- 1 | package io.mrarm.mcversion; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | import android.preference.PreferenceManager; 6 | import android.util.Log; 7 | import android.widget.Toast; 8 | 9 | import java.io.File; 10 | import java.io.UnsupportedEncodingException; 11 | 12 | public class PlayHelper { 13 | 14 | private static final String PREF_ACCOUNT_ID = "play_account_id"; 15 | private static final String PREF_ACCOUNT_TOKEN = "play_account_token"; 16 | 17 | private static PlayHelper instance; 18 | 19 | public static PlayHelper getInstance(Context context) { 20 | if (instance == null) 21 | instance = new PlayHelper(context.getApplicationContext()); 22 | return instance; 23 | } 24 | 25 | private final Context context; 26 | private File dataDir; 27 | private PlayApi api; 28 | private boolean authToApiPending = false; 29 | private boolean authedToApi = false; 30 | 31 | public PlayHelper(Context context) { 32 | this.context = context; 33 | dataDir = new File(context.getFilesDir(), "playapi"); 34 | dataDir.mkdirs(); 35 | String dataDirS = dataDir.getAbsolutePath(); 36 | if (!dataDirS.endsWith("/")) 37 | dataDirS += "/"; 38 | initCurlSsl(context); 39 | api = new PlayApi(dataDirS); 40 | loadDevice(R.raw.device_arm64, "arm64"); 41 | if (hasSavedAccount()) { 42 | loadAccountFromPrefs(); 43 | requestAuthToApi(); 44 | } 45 | } 46 | 47 | private static void initCurlSsl(Context context) { 48 | File outCaFile = new File(context.getFilesDir(), "google_ca.pem"); 49 | IOUtil.copyRawResource(context, R.raw.google_ca, outCaFile); 50 | PlayApi.initCurlSsl(outCaFile.getAbsolutePath()); 51 | } 52 | 53 | private void loadDevice(int resId, String stateName) { 54 | try { 55 | String deviceConfig = new String(IOUtil.readRawResource(context, resId), "UTF-8"); 56 | api.setDevice(deviceConfig, new File(dataDir, "device-" + stateName + "-state.conf").getAbsolutePath()); 57 | } catch (UnsupportedEncodingException e) { 58 | throw new RuntimeException(e); 59 | } 60 | } 61 | 62 | private void loadAccountFromPrefs() { 63 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 64 | String accountId = prefs.getString(PREF_ACCOUNT_ID, null); 65 | String accountToken = prefs.getString(PREF_ACCOUNT_TOKEN, null); 66 | if (accountId != null && accountToken != null) 67 | api.setLoginToken(accountId, accountToken); 68 | } 69 | 70 | public boolean hasSavedAccount() { 71 | SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); 72 | return prefs.contains(PREF_ACCOUNT_ID) && prefs.contains(PREF_ACCOUNT_TOKEN); 73 | } 74 | 75 | public void saveAccount(String id, String token) { 76 | SharedPreferences.Editor prefs = PreferenceManager.getDefaultSharedPreferences(context).edit(); 77 | prefs.putString(PREF_ACCOUNT_ID, id); 78 | prefs.putString(PREF_ACCOUNT_TOKEN, token); 79 | prefs.apply(); 80 | } 81 | 82 | public PlayApi getApi() { 83 | return api; 84 | } 85 | 86 | public void requestAuthToApi() { 87 | synchronized (this) { 88 | if (authToApiPending) 89 | return; 90 | authToApiPending = true; 91 | } 92 | api.authToApi(new PlayApi.Callback() { 93 | @Override 94 | public void onSuccess() { 95 | Log.i("PlayHelper", "Authenticated to api"); 96 | synchronized (PlayHelper.this) { 97 | authToApiPending = false; 98 | authedToApi = true; 99 | } 100 | } 101 | 102 | @Override 103 | public void onError(String str) { 104 | synchronized (PlayHelper.this) { 105 | authToApiPending = false; 106 | } 107 | UiThreadHelper.runOnUiThread(() -> Toast.makeText(context, str, Toast.LENGTH_LONG).show()); 108 | } 109 | }); 110 | } 111 | 112 | public synchronized boolean isAuthedToApi() { 113 | return authedToApi; 114 | } 115 | 116 | } 117 | -------------------------------------------------------------------------------- /app/src/main/java/io/mrarm/mcversion/UiThreadHelper.java: -------------------------------------------------------------------------------- 1 | package io.mrarm.mcversion; 2 | 3 | import android.os.Handler; 4 | import android.os.Looper; 5 | 6 | public class UiThreadHelper { 7 | 8 | private static final Handler sUiHandler = new Handler(Looper.getMainLooper()); 9 | 10 | public static void runOnUiThread(Runnable r) { 11 | if (Looper.getMainLooper().getThread() == Thread.currentThread()) 12 | r.run(); 13 | else 14 | sUiHandler.post(r); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /app/src/main/java/io/mrarm/mcversion/UiVersion.java: -------------------------------------------------------------------------------- 1 | package io.mrarm.mcversion; 2 | 3 | import androidx.databinding.ObservableBoolean; 4 | import androidx.databinding.ObservableLong; 5 | 6 | public final class UiVersion { 7 | 8 | private final String name; 9 | private final int versionCode; 10 | private final ObservableBoolean downloaded = new ObservableBoolean(); 11 | private final ObservableBoolean downloading = new ObservableBoolean(); 12 | private final ObservableLong downloadTotal = new ObservableLong(); 13 | private final ObservableLong downloadComplete = new ObservableLong(); 14 | 15 | public UiVersion(String name, int versionCode) { 16 | this.name = name; 17 | this.versionCode = versionCode; 18 | } 19 | 20 | public String getName() { 21 | return name; 22 | } 23 | 24 | public int getVersionCode() { 25 | return versionCode; 26 | } 27 | 28 | public ObservableBoolean isDownloaded() { 29 | return downloaded; 30 | } 31 | 32 | public ObservableBoolean isDownloading() { 33 | return downloading; 34 | } 35 | 36 | public ObservableLong getDownloadTotal() { 37 | return downloadTotal; 38 | } 39 | 40 | public ObservableLong getDownloadComplete() { 41 | return downloadComplete; 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /app/src/main/java/io/mrarm/mcversion/VersionDownloader.java: -------------------------------------------------------------------------------- 1 | package io.mrarm.mcversion; 2 | 3 | import android.content.Context; 4 | import android.os.AsyncTask; 5 | import android.util.Log; 6 | import android.widget.Toast; 7 | 8 | import java.io.File; 9 | 10 | public class VersionDownloader { 11 | 12 | private File storageDir; 13 | 14 | public VersionDownloader(File storageDir) { 15 | this.storageDir = storageDir; 16 | } 17 | 18 | public File getVersionDir(UiVersion version) { 19 | return new File(storageDir, String.valueOf(version.getVersionCode())); 20 | } 21 | 22 | public void updateDownloadedStatus(UiVersion version) { 23 | version.isDownloaded().set(getVersionDir(version).exists()); 24 | } 25 | 26 | public void download(Context context, UiVersion version) { 27 | if (!PlayHelper.getInstance(context).isAuthedToApi()) { 28 | Toast.makeText(context, R.string.error_api_not_authenticated, Toast.LENGTH_LONG).show(); 29 | return; 30 | } 31 | if (version.isDownloading().get()) 32 | return; 33 | version.getDownloadComplete().set(0); 34 | version.getDownloadTotal().set(0); 35 | version.isDownloading().set(true); 36 | PlayHelper.getInstance(context).getApi().requestDelivery("com.mojang.minecraftpe", version.getVersionCode(), new PlayApi.DeliveryCallback() { 37 | @Override 38 | public void onSuccess(PlayApi.DownloadInfo[] download) { 39 | for (PlayApi.DownloadInfo i : download) 40 | Log.d("VersionDownloader", "Download: " + i.getName() + " " + i.getGzippedUrl()); 41 | 42 | UiThreadHelper.runOnUiThread(() -> { 43 | DownloadTracker tracker = new DownloadTracker(context, version, download, getVersionDir(version)); 44 | tracker.downloadNextFile(); 45 | }); 46 | } 47 | 48 | @Override 49 | public void onError(String str) { 50 | UiThreadHelper.runOnUiThread(() -> { 51 | Toast.makeText(context, str, Toast.LENGTH_LONG).show(); 52 | version.isDownloading().set(false); 53 | }); 54 | } 55 | }); 56 | } 57 | 58 | public void delete(UiVersion version) { 59 | IOUtil.deleteDirectory(getVersionDir(version)); 60 | version.isDownloaded().set(false); 61 | } 62 | 63 | private static class DownloadTracker { 64 | 65 | private final Context context; 66 | private final UiVersion version; 67 | private final PlayApi.DownloadInfo[] downloads; 68 | private final File downloadDir; 69 | private long totalSize, totalDownloaded; 70 | private int nextFileIndex = 0; 71 | 72 | public DownloadTracker(Context context, UiVersion version, PlayApi.DownloadInfo[] downloads, File downloadDir) { 73 | this.context = context; 74 | this.version = version; 75 | this.downloads = downloads; 76 | this.downloadDir = downloadDir; 77 | 78 | for (PlayApi.DownloadInfo i : downloads) 79 | totalSize += i.getSize(); 80 | version.getDownloadTotal().set(totalSize); 81 | } 82 | 83 | public void downloadNextFile() { 84 | if (nextFileIndex == downloads.length) { 85 | version.isDownloading().set(false); 86 | version.isDownloaded().set(true); 87 | return; 88 | } 89 | 90 | downloadDir.mkdirs(); 91 | 92 | PlayApi.DownloadInfo dlInfo = downloads[nextFileIndex++]; 93 | FileDownloader downloader = new FileDownloader(); 94 | downloader.setProgressCallback((c, t) -> UiThreadHelper.runOnUiThread(() -> { 95 | version.getDownloadComplete().set(totalDownloaded + c); 96 | })); 97 | downloader.setCompletionCallback(() -> UiThreadHelper.runOnUiThread(() -> { 98 | totalDownloaded += dlInfo.getSize(); 99 | version.getDownloadComplete().set(totalDownloaded); 100 | downloadNextFile(); 101 | })); 102 | downloader.setErrorCallback(e -> UiThreadHelper.runOnUiThread(() -> { 103 | Toast.makeText(context, e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); 104 | e.printStackTrace(); 105 | 106 | version.isDownloading().set(false); 107 | })); 108 | AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> downloader.execute(dlInfo.getUrl(), new File(downloadDir, dlInfo.getName() + ".apk"))); 109 | } 110 | 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /app/src/main/java/io/mrarm/mcversion/VersionInstaller.java: -------------------------------------------------------------------------------- 1 | package io.mrarm.mcversion; 2 | 3 | import android.app.PendingIntent; 4 | import android.content.BroadcastReceiver; 5 | import android.content.Context; 6 | import android.content.Intent; 7 | import android.content.IntentFilter; 8 | import android.content.pm.PackageInfo; 9 | import android.content.pm.PackageInstaller; 10 | import android.content.pm.PackageManager; 11 | import android.os.Build; 12 | import android.util.Log; 13 | 14 | import androidx.annotation.RequiresApi; 15 | import androidx.databinding.ObservableInt; 16 | 17 | import java.io.File; 18 | import java.io.FileInputStream; 19 | import java.io.IOException; 20 | import java.io.InputStream; 21 | import java.io.OutputStream; 22 | import java.util.HashMap; 23 | import java.util.Map; 24 | import java.util.UUID; 25 | 26 | public class VersionInstaller { 27 | 28 | private static final String ACTION_INSTALL_STATUS = "io.mrarm.mcversion.installstatus"; 29 | private static final String ACTION_UNINSTALL_STATUS = "io.mrarm.mcversion.uninstallstatus"; 30 | private static final String ARG_SESSION_ID = "session_id"; 31 | private static final String ARG_CALLBACK_ID = "callback_id"; 32 | 33 | private final ObservableInt installedVersion = new ObservableInt(); 34 | 35 | private final Map callbackMap = new HashMap<>(); 36 | 37 | private final BroadcastReceiver receiver = new BroadcastReceiver() { 38 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 39 | @Override 40 | public void onReceive(Context context, Intent intent) { 41 | final int statusCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, PackageInstaller.STATUS_FAILURE); 42 | if (statusCode == PackageInstaller.STATUS_PENDING_USER_ACTION) { 43 | Intent launchIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT); 44 | context.startActivity(launchIntent); 45 | return; 46 | } 47 | if (intent.getAction().equals(ACTION_INSTALL_STATUS)) { 48 | Log.d("VersionInstaller", "Install status: " + statusCode); 49 | 50 | PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); 51 | try { 52 | packageInstaller.abandonSession(intent.getIntExtra(ARG_SESSION_ID, -1)); 53 | } catch (SecurityException ignored) { 54 | } 55 | } else if (intent.getAction().equals(ACTION_UNINSTALL_STATUS)) { 56 | Log.d("VersionInstaller", "Uninstall status: " + statusCode); 57 | } 58 | 59 | updateInstalledVersion(context); 60 | 61 | if (intent.hasExtra(ARG_CALLBACK_ID)) { 62 | InstallCallback r = callbackMap.remove(intent.getStringExtra(ARG_CALLBACK_ID)); 63 | if (r == null) 64 | return; 65 | r.onComplete(statusCode); 66 | } 67 | } 68 | }; 69 | 70 | public void deleteHangingSession(Context context) { 71 | if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP) { 72 | PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); 73 | for (PackageInstaller.SessionInfo s : packageInstaller.getMySessions()) { 74 | packageInstaller.abandonSession(s.getSessionId()); 75 | } 76 | } 77 | } 78 | 79 | public void registerIntentHandler(Context context) { 80 | IntentFilter intentFilter = new IntentFilter(); 81 | intentFilter.addAction(ACTION_INSTALL_STATUS); 82 | intentFilter.addAction(ACTION_UNINSTALL_STATUS); 83 | context.registerReceiver(receiver, intentFilter, null, null); 84 | } 85 | 86 | public void unregisterIntentHandler(Context context) { 87 | context.unregisterReceiver(receiver); 88 | } 89 | 90 | private String registerCallback(InstallCallback callback) { 91 | String uuid = UUID.randomUUID().toString(); 92 | callbackMap.put(uuid, callback); 93 | return uuid; 94 | } 95 | 96 | public ObservableInt getInstalledVersion() { 97 | return installedVersion; 98 | } 99 | 100 | public void updateInstalledVersion(Context context) { 101 | try { 102 | PackageInfo info = context.getPackageManager().getPackageInfo("com.mojang.minecraftpe", 0); 103 | installedVersion.set(info.versionCode); 104 | } catch (PackageManager.NameNotFoundException e) { 105 | installedVersion.set(0); 106 | } 107 | } 108 | 109 | public boolean needsUninstall(Context context, int versionCode) { 110 | try { 111 | PackageInfo info = context.getPackageManager().getPackageInfo("com.mojang.minecraftpe", 0); 112 | return info.versionCode > versionCode; 113 | } catch (PackageManager.NameNotFoundException e) { 114 | return false; 115 | } 116 | } 117 | 118 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 119 | public void uninstall(Context context, InstallCallback callback) { 120 | PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); 121 | Intent intent = new Intent(ACTION_UNINSTALL_STATUS); 122 | intent.putExtra(ARG_CALLBACK_ID, registerCallback(callback)); 123 | PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 10000, intent, PendingIntent.FLAG_CANCEL_CURRENT); 124 | packageInstaller.uninstall("com.mojang.minecraftpe", pendingIntent.getIntentSender()); 125 | } 126 | 127 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 128 | private void install(Context context, File versionDir, InstallCallback callback) throws IOException { 129 | PackageInstaller packageInstaller = context.getPackageManager().getPackageInstaller(); 130 | PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( 131 | PackageInstaller.SessionParams.MODE_FULL_INSTALL); 132 | params.setAppPackageName("com.mojang.minecraftpe"); 133 | int sessionId = packageInstaller.createSession(params); 134 | PackageInstaller.Session session = packageInstaller.openSession(sessionId); 135 | for (File f : versionDir.listFiles()) { 136 | try (InputStream inStream = new FileInputStream(f); 137 | OutputStream outStream = session.openWrite(f.getName(), 0, -1)) { 138 | byte[] b = new byte[512 * 1024]; 139 | int n; 140 | while ((n = inStream.read(b)) >= 0) 141 | outStream.write(b, 0, n); 142 | session.fsync(outStream); 143 | } 144 | } 145 | 146 | Intent intent = new Intent(ACTION_INSTALL_STATUS); 147 | intent.putExtra(ARG_SESSION_ID, sessionId); 148 | intent.putExtra(ARG_CALLBACK_ID, registerCallback(callback)); 149 | PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 10001, intent, PendingIntent.FLAG_CANCEL_CURRENT); 150 | session.commit(pendingIntent.getIntentSender()); 151 | session.close(); 152 | } 153 | 154 | @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) 155 | public void install(Context context, VersionDownloader downloader, UiVersion version, InstallCallback callback) 156 | throws IOException { 157 | install(context, downloader.getVersionDir(version), callback); 158 | } 159 | 160 | public interface InstallCallback { 161 | void onComplete(int status); 162 | } 163 | 164 | } 165 | -------------------------------------------------------------------------------- /app/src/main/java/io/mrarm/mcversion/VersionList.java: -------------------------------------------------------------------------------- 1 | package io.mrarm.mcversion; 2 | 3 | import com.google.gson.Gson; 4 | import com.google.gson.annotations.SerializedName; 5 | import com.google.gson.reflect.TypeToken; 6 | 7 | import java.io.BufferedReader; 8 | import java.io.File; 9 | import java.io.FileReader; 10 | import java.io.FileWriter; 11 | import java.io.IOException; 12 | import java.io.InputStreamReader; 13 | import java.net.HttpURLConnection; 14 | import java.net.URL; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | public class VersionList { 19 | 20 | private static Gson gson = new Gson(); 21 | 22 | public List versions; 23 | 24 | public void loadCached(File file) { 25 | try (FileReader reader = new FileReader(file)) { 26 | versions = gson.fromJson(reader, new TypeToken>(){}.getType()); 27 | } catch (IOException ignored) { 28 | } 29 | } 30 | 31 | public void saveToCache(File file) { 32 | try (FileWriter writer = new FileWriter(file)) { 33 | gson.toJson(versions, writer); 34 | } catch (IOException ignored) { 35 | } 36 | } 37 | 38 | public void loadNetwork(String url) throws IOException { 39 | HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection(); 40 | if (conn == null) 41 | throw new IOException(); 42 | try (BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()))) { 43 | List versions = gson.fromJson(reader, new TypeToken>(){}.getType()); 44 | if (versions == null) 45 | throw new IOException("Version list empty"); 46 | this.versions = versions; 47 | } 48 | } 49 | 50 | 51 | public static class Version { 52 | @SerializedName("version_name") 53 | public String name; 54 | 55 | @SerializedName("codes") 56 | public Map codes; 57 | } 58 | 59 | 60 | } 61 | -------------------------------------------------------------------------------- /app/src/main/java/io/mrarm/mcversion/VersionListAdapter.java: -------------------------------------------------------------------------------- 1 | package io.mrarm.mcversion; 2 | 3 | import android.app.AlertDialog; 4 | import android.content.Context; 5 | import android.content.pm.PackageInstaller; 6 | import android.os.Build; 7 | import android.widget.Toast; 8 | 9 | import androidx.databinding.ObservableInt; 10 | 11 | import java.io.IOException; 12 | import java.util.ArrayList; 13 | import java.util.Collections; 14 | import java.util.HashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | 18 | import io.mrarm.dataadapter.DataAdapter; 19 | import io.mrarm.dataadapter.ListData; 20 | import io.mrarm.dataadapter.ViewHolderType; 21 | 22 | public class VersionListAdapter extends DataAdapter { 23 | 24 | private static final String[] ABIS = {"arm64-v8a", "armeabi-v7a"}; 25 | 26 | private final VersionDownloader downloader; 27 | private final VersionInstaller installer; 28 | private List currentVersions; 29 | 30 | public VersionListAdapter(VersionDownloader downloader, VersionInstaller installer) { 31 | this.downloader = downloader; 32 | this.installer = installer; 33 | } 34 | 35 | public ObservableInt getInstalledVersion() { 36 | return installer.getInstalledVersion(); 37 | } 38 | 39 | public void setVersions(List versions) { 40 | Map oldDownloads = new HashMap<>(); 41 | if (currentVersions != null) { 42 | for (UiVersion v : currentVersions) 43 | oldDownloads.put(v.getVersionCode(), v); 44 | } 45 | 46 | List uiVersions = new ArrayList<>(); 47 | for (VersionList.Version v : versions) { 48 | for (String abi : ABIS) { 49 | Integer versionCode = v.codes.get(abi); 50 | if (versionCode == null) 51 | continue; 52 | if (oldDownloads.containsKey(versionCode)) 53 | uiVersions.add(oldDownloads.get(versionCode)); 54 | else 55 | uiVersions.add(new UiVersion(v.name + " (" + abi + ")", versionCode)); 56 | } 57 | } 58 | for (UiVersion v : uiVersions) 59 | downloader.updateDownloadedStatus(v); 60 | Collections.reverse(uiVersions); 61 | ListData data = new ListData<>(uiVersions, ITEM); 62 | data.setContext(this); 63 | setSource(data); 64 | } 65 | 66 | private void doInstall(Context context, UiVersion version, Runnable cb) { 67 | try { 68 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 69 | installer.install(context, downloader, version, (s) -> cb.run()); 70 | } 71 | } catch (IOException e) { 72 | Toast.makeText(context, e.getLocalizedMessage(), Toast.LENGTH_LONG).show(); 73 | e.printStackTrace(); 74 | } 75 | } 76 | 77 | public void downloadOrInstall(Context context, UiVersion version) { 78 | if (version.isDownloaded().get()) { 79 | AlertDialog dialog = new AlertDialog.Builder(context) 80 | .setMessage(R.string.text_reinstall) 81 | .setCancelable(false) 82 | .show(); 83 | 84 | if (installer.needsUninstall(context, version.getVersionCode())) { 85 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 86 | installer.uninstall(context, (s) -> { 87 | if (s == PackageInstaller.STATUS_SUCCESS) { 88 | doInstall(context, version, dialog::dismiss); 89 | } else { 90 | dialog.dismiss(); 91 | } 92 | }); 93 | } 94 | } else { 95 | doInstall(context, version, dialog::dismiss); 96 | } 97 | } else { 98 | downloader.download(context, version); 99 | } 100 | } 101 | 102 | public boolean showLongPressMenu(Context context, UiVersion version) { 103 | if (!version.isDownloaded().get()) 104 | return false; 105 | new AlertDialog.Builder(context) 106 | .setTitle(version.getName()) 107 | .setItems(new CharSequence[] { context.getString(R.string.action_delete) }, (d, which) -> { 108 | if (which == 0) { 109 | downloader.delete(version); 110 | } 111 | }) 112 | .show(); 113 | return true; 114 | } 115 | 116 | 117 | private static final ViewHolderType ITEM = 118 | ViewHolderType.fromDataBinding(R.layout.version_entry) 119 | .setValueVarId(BR.version) 120 | .setContextVarId(BR.context) 121 | .build(); 122 | 123 | } 124 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 12 | 13 | 19 | 22 | 25 | 26 | 27 | 28 | 34 | 35 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 10 | 15 | 20 | 25 | 30 | 35 | 40 | 45 | 50 | 55 | 60 | 65 | 70 | 75 | 80 | 85 | 90 | 95 | 100 | 105 | 110 | 115 | 120 | 125 | 130 | 135 | 140 | 145 | 150 | 155 | 160 | 165 | 170 | 171 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/layout/version_entry.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | 22 | 23 | 29 | 30 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCMrARM/mc-android-version-switcher/71ef27304830bd051e78aae454d79dc570178cf1/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCMrARM/mc-android-version-switcher/71ef27304830bd051e78aae454d79dc570178cf1/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCMrARM/mc-android-version-switcher/71ef27304830bd051e78aae454d79dc570178cf1/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCMrARM/mc-android-version-switcher/71ef27304830bd051e78aae454d79dc570178cf1/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCMrARM/mc-android-version-switcher/71ef27304830bd051e78aae454d79dc570178cf1/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCMrARM/mc-android-version-switcher/71ef27304830bd051e78aae454d79dc570178cf1/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCMrARM/mc-android-version-switcher/71ef27304830bd051e78aae454d79dc570178cf1/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCMrARM/mc-android-version-switcher/71ef27304830bd051e78aae454d79dc570178cf1/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCMrARM/mc-android-version-switcher/71ef27304830bd051e78aae454d79dc570178cf1/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MCMrARM/mc-android-version-switcher/71ef27304830bd051e78aae454d79dc570178cf1/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/raw/device_arm64.conf: -------------------------------------------------------------------------------- 1 | ; data from my Zenphone 2 without a sim card running a quite old version of Cyanogenmod 2 | device_type = phone 3 | build.fingerprint = "asus/WW_Z00A/Z00A:5.0/LRX21V/2.20.40.165_20160118_6541_user:user/release-keys" 4 | build.id = "MOB3M" 5 | build.product = "mofd_v1" 6 | build.brand = "asus" 7 | build.radio = "1527_5.0.50.2_1225" 8 | build.bootloader = "unknown" 9 | build.client = "android-google" 10 | build.timestamp = 1466006494 11 | build.google_services = 10084470 12 | build.device = "Z00A" 13 | build.sdk_version = 23 14 | build.version_string = "6.0.1" 15 | build.model = "ASUS_Z00A" 16 | build.manufacturer = "asus" 17 | build.build_product = "WW_Z00A" 18 | build.ota_installed = true 19 | build.google_packages = [ 20 | ms-android-google:2 21 | android-google:1 22 | gmm-android-google:4 23 | mvapp-android-google:5 24 | am-android-google:5 25 | ms-android-google:9 26 | ] 27 | build.security_patch = "2016-06-01" 28 | 29 | # undefinied, notouch, stylus, finger 30 | config.touch_screen = finger 31 | # undefinied, nokeys, qwerty, twelvekey 32 | config.keyboard = nokeys 33 | # undefinied, nonav, dpad, trackball, wheel 34 | config.navigation = nonav 35 | # undefinied, small, normal, large, xlarge 36 | config.screen_layout = normal 37 | config.has_hard_keyboard = false 38 | config.has_five_way_navig = false 39 | config.screen_density = 480 40 | config.gl_es_version = 0x30000 41 | config.system_shared_libraries = [ 42 | android.test.runner, 43 | com.android.future.usb.accessory, 44 | com.android.location.provider, 45 | com.android.media.remotedisplay, 46 | com.android.mediadrm.signer, 47 | com.google.android.camera.experimental2015, 48 | com.google.android.dialer.support, 49 | com.google.android.gms, 50 | com.google.android.maps, 51 | com.google.android.media.effects, 52 | com.google.widevine.software.drm, 53 | javax.obex, 54 | org.apache.http.legacy, 55 | org.cyanogenmod.hardware, 56 | org.cyanogenmod.platform 57 | ] 58 | config.system_features = [ 59 | ; some features may have a minimal GLES version specified. Specify it like this: 60 | ; some_gl_related_feature:min_gles_version 61 | android.hardware.audio.output, 62 | android.hardware.bluetooth, 63 | android.hardware.bluetooth_le, 64 | android.hardware.camera, 65 | android.hardware.camera.any, 66 | android.hardware.camera.autofocus, 67 | android.hardware.camera.flash, 68 | android.hardware.camera.front, 69 | android.hardware.ethernet, 70 | android.hardware.faketouch, 71 | android.hardware.location, 72 | android.hardware.location.gps, 73 | android.hardware.location.network, 74 | android.hardware.microphone, 75 | android.hardware.nfc, 76 | android.hardware.nfc.hce, 77 | android.hardware.screen.landscape, 78 | android.hardware.screen.portrait, 79 | android.hardware.sensor.accelerometer, 80 | android.hardware.sensor.compass, 81 | android.hardware.sensor.gyroscope, 82 | android.hardware.sensor.light, 83 | android.hardware.sensor.proximity, 84 | android.hardware.sensor.stepcounter, 85 | android.hardware.sensor.stepdetector, 86 | android.hardware.telephony, 87 | android.hardware.telephony.gsm, 88 | android.hardware.touchscreen, 89 | android.hardware.touchscreen.multitouch, 90 | android.hardware.touchscreen.multitouch.distinct, 91 | android.hardware.touchscreen.multitouch.jazzhand, 92 | android.hardware.usb.accessory, 93 | android.hardware.usb.host, 94 | android.hardware.wifi, 95 | android.hardware.wifi.direct, 96 | android.software.app_widgets, 97 | android.software.backup, 98 | android.software.connectionservice, 99 | android.software.device_admin, 100 | android.software.home_screen, 101 | android.software.input_methods, 102 | android.software.live_wallpaper, 103 | android.software.managed_users, 104 | android.software.print, 105 | android.software.sip, 106 | android.software.sip.voip, 107 | android.software.voice_recognizers, 108 | android.software.webview, 109 | asus.software.zenui, 110 | com.cyanogenmod.android, 111 | com.cyanogenmod.nfc.enhanced, 112 | com.google.android.feature.EXCHANGE_6_2, 113 | com.google.android.feature.GOOGLE_BUILD, 114 | com.google.android.feature.GOOGLE_EXPERIENCE, 115 | org.cyanogenmod.appsuggest, 116 | org.cyanogenmod.audio, 117 | org.cyanogenmod.hardware, 118 | org.cyanogenmod.livedisplay, 119 | org.cyanogenmod.livelockscreen, 120 | org.cyanogenmod.partner, 121 | org.cyanogenmod.performance, 122 | org.cyanogenmod.profiles, 123 | org.cyanogenmod.statusbar, 124 | org.cyanogenmod.telephony, 125 | org.cyanogenmod.theme, 126 | org.cyanogenmod.theme.v1, 127 | org.cyanogenmod.weather 128 | ] 129 | config.native_platforms = [ 130 | arm64-v8a, 131 | armeabi-v7a, 132 | armeabi 133 | ] 134 | config.screen_width = 1080 135 | config.screen_height = 1920 136 | config.system_supported_locales = [ 137 | af, 138 | af-ZA, 139 | am, 140 | am-ET, 141 | ar, 142 | ar-EG, 143 | ar-XB, 144 | ast-ES, 145 | az, 146 | az-AZ, 147 | be, 148 | bg, 149 | bg-BG, 150 | bn, 151 | bn-BD, 152 | bs, 153 | ca, 154 | ca-ES, 155 | cs, 156 | cs-CZ, 157 | da, 158 | da-DK, 159 | de, 160 | de-AT, 161 | de-CH, 162 | de-DE, 163 | de-LI, 164 | el, 165 | el-GR, 166 | en, 167 | en-AU, 168 | en-CA, 169 | en-GB, 170 | en-IN, 171 | en-NZ, 172 | en-SG, 173 | en-US, 174 | en-XA, 175 | eo, 176 | es, 177 | es-ES, 178 | es-US, 179 | et, 180 | et-EE, 181 | eu, 182 | eu-ES, 183 | fa, 184 | fa-IR, 185 | fi, 186 | fi-FI, 187 | fil, 188 | fil-PH, 189 | fr, 190 | fr-BE, 191 | fr-CA, 192 | fr-CH, 193 | fr-FR, 194 | gl, 195 | gl-ES, 196 | gu, 197 | gu-IN, 198 | hi, 199 | hi-IN, 200 | hr, 201 | hr-HR, 202 | hu, 203 | hu-HU, 204 | hy, 205 | hy-AM, 206 | in, 207 | in-ID, 208 | is, 209 | is-IS, 210 | it, 211 | it-CH, 212 | it-IT, 213 | iw, 214 | iw-IL, 215 | ja, 216 | ja-JP, 217 | ka, 218 | ka-GE, 219 | kk, 220 | kk-KZ, 221 | km, 222 | km-KH, 223 | kn, 224 | kn-IN, 225 | ko, 226 | ko-KR, 227 | ku, 228 | ku-IQ, 229 | ky, 230 | ky-KG, 231 | lb, 232 | lb-LU, 233 | lo, 234 | lo-LA, 235 | lt, 236 | lt-LT, 237 | lv, 238 | lv-LV, 239 | mk, 240 | mk-MK, 241 | ml, 242 | ml-IN, 243 | mn, 244 | mn-MN, 245 | mr, 246 | mr-IN, 247 | ms, 248 | ms-MY, 249 | my, 250 | my-MM, 251 | nb, 252 | nb-NO, 253 | ne, 254 | ne-NP, 255 | nl, 256 | nl-BE, 257 | nl-NL, 258 | pa, 259 | pa-IN, 260 | pl, 261 | pl-PL, 262 | pt, 263 | pt-BR, 264 | pt-PT, 265 | rm, 266 | rm-CH, 267 | ro, 268 | ro-RO, 269 | ru, 270 | ru-RU, 271 | si, 272 | si-LK, 273 | sk, 274 | sk-SK, 275 | sl, 276 | sl-SI, 277 | sq, 278 | sq-AL, 279 | sr, 280 | sr-Latn, 281 | sr-RS, 282 | sv, 283 | sv-SE, 284 | sw, 285 | sw-TZ, 286 | ta, 287 | ta-IN, 288 | te, 289 | te-IN, 290 | th, 291 | th-TH, 292 | tr, 293 | tr-TR, 294 | uk, 295 | uk-UA, 296 | ur, 297 | ur-PK, 298 | uz, 299 | uz-UZ, 300 | vi, 301 | vi-VN, 302 | zh, 303 | zh-CN, 304 | zh-HK, 305 | zh-TW, 306 | zu, 307 | zu-ZA 308 | ] 309 | config.gl_extensions = [ 310 | GL_APPLE_texture_2D_limited_npot, 311 | GL_EXT_blend_minmax, 312 | GL_EXT_color_buffer_float, 313 | GL_EXT_debug_marker, 314 | GL_EXT_discard_framebuffer, 315 | GL_EXT_draw_buffers, 316 | GL_EXT_multi_draw_arrays, 317 | GL_EXT_multisampled_render_to_texture, 318 | GL_EXT_occlusion_query_boolean, 319 | GL_EXT_read_format_bgra, 320 | GL_EXT_robustness, 321 | GL_EXT_separate_shader_objects, 322 | GL_EXT_shader_framebuffer_fetch, 323 | GL_EXT_shader_texture_lod, 324 | GL_EXT_texture_filter_anisotropic, 325 | GL_EXT_texture_format_BGRA8888, 326 | GL_EXT_texture_rg, 327 | GL_EXT_texture_sRGB_decode, 328 | GL_IMG_multisampled_render_to_texture, 329 | GL_IMG_program_binary, 330 | GL_IMG_read_format, 331 | GL_IMG_shader_binary, 332 | GL_IMG_texture_compression_pvrtc, 333 | GL_IMG_texture_compression_pvrtc2, 334 | GL_IMG_texture_format_BGRA8888, 335 | GL_IMG_texture_npot, 336 | GL_IMG_vertex_array_object, 337 | GL_KHR_blend_equation_advanced, 338 | GL_KHR_blend_equation_advanced_coherent, 339 | GL_KHR_debug, 340 | GL_OES_EGL_image, 341 | GL_OES_EGL_image_external, 342 | GL_OES_EGL_sync, 343 | GL_OES_blend_equation_separate, 344 | GL_OES_blend_func_separate, 345 | GL_OES_blend_subtract, 346 | GL_OES_byte_coordinates, 347 | GL_OES_compressed_ETC1_RGB8_texture, 348 | GL_OES_compressed_paletted_texture, 349 | GL_OES_depth24, 350 | GL_OES_depth_texture, 351 | GL_OES_draw_texture, 352 | GL_OES_egl_sync, 353 | GL_OES_element_index_uint, 354 | GL_OES_extended_matrix_palette, 355 | GL_OES_fixed_point, 356 | GL_OES_fragment_precision_high, 357 | GL_OES_framebuffer_object, 358 | GL_OES_get_program_binary, 359 | GL_OES_mapbuffer, 360 | GL_OES_matrix_get, 361 | GL_OES_matrix_palette, 362 | GL_OES_packed_depth_stencil, 363 | GL_OES_point_size_array, 364 | GL_OES_point_sprite, 365 | GL_OES_query_matrix, 366 | GL_OES_read_format, 367 | GL_OES_required_internalformat, 368 | GL_OES_rgb8_rgba8, 369 | GL_OES_shader_image_atomic, 370 | GL_OES_single_precision, 371 | GL_OES_standard_derivatives, 372 | GL_OES_stencil8, 373 | GL_OES_stencil_wrap, 374 | GL_OES_surfaceless_context, 375 | GL_OES_texture_cube_map, 376 | GL_OES_texture_env_crossbar, 377 | GL_OES_texture_float, 378 | GL_OES_texture_half_float, 379 | GL_OES_texture_mirrored_repeat, 380 | GL_OES_texture_npot, 381 | GL_OES_texture_stencil8, 382 | GL_OES_texture_storage_multisample_2d_array, 383 | GL_OES_vertex_array_object, 384 | GL_OES_vertex_half_float 385 | ] 386 | config.smallest_screen_width_dp = 360 387 | config.low_ram = false 388 | config.total_mem = 4093796352 389 | config.cores = 4 390 | voice_capable = true 391 | wide_screen = false 392 | ota_certs = [ 393 | "71Q6Rn2DDZl1zPDVaaeEHItd+Yg=" 394 | ] 395 | 396 | ; 397 | ; OPTIONS THAT VARY BY DEVICE 398 | ; I have provided some example data here. You may customize them if you want. 399 | ; 400 | 401 | ; specify if the device is secured with a PIN, pattern of password 402 | config.keyguard_device_secure = true 403 | 404 | locale = "en_US" 405 | country = "us" 406 | time_zone = "America/New_York" 407 | 408 | roaming = "WIFI::" 409 | user_number = 0 410 | user_serial_number = 0 411 | 412 | mac_addr_type = wifi 413 | # let the program generate mac_addr&meid 414 | mac_addr = generate 415 | ; meid = generate - while this works it's rather better to omit this field than give a random value 416 | # generate a random 11 letter string as serial number 417 | serial_number = generate(12, "QWERTYUIOPASDFGHJKLZXCVBNM1234567890") 418 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #008577 4 | #00574B 5 | #D81B60 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 16dp 3 | 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #26A69A 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Minecraft Version 3 | Google Login 4 | 5 | Not downloaded 6 | Downloaded 7 | Installed 8 | 9 | Switching version… 10 | 11 | Delete 12 | 13 | Not authenticated to the API yet 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 15 | 16 |