2009Scape-mobile/app_pojavlauncher/src/main/java/net/kdt/pojavlaunch/Tools.java

1037 lines
45 KiB
Java

package net.kdt.pojavlaunch;
import static android.os.Build.VERSION.SDK_INT;
import static android.os.Build.VERSION_CODES.P;
import static net.kdt.pojavlaunch.PojavApplication.sExecutorService;
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_IGNORE_NOTCH;
import static net.kdt.pojavlaunch.prefs.LauncherPreferences.PREF_NOTCH_SIZE;
import android.app.Activity;
import android.app.ActivityManager;
import android.app.AlertDialog;
import android.app.NotificationChannel;
import android.app.NotificationManager;
import android.app.ProgressDialog;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.provider.DocumentsContract;
import android.provider.OpenableColumns;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.View;
import android.view.WindowManager;
import android.webkit.MimeTypeMap;
import android.widget.EditText;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.NotificationManagerCompat;
import androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentTransaction;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import net.kdt.pojavlaunch.extra.ExtraConstants;
import net.kdt.pojavlaunch.extra.ExtraCore;
import net.kdt.pojavlaunch.multirt.MultiRTUtils;
import net.kdt.pojavlaunch.multirt.Runtime;
import net.kdt.pojavlaunch.plugins.FFmpegPlugin;
import net.kdt.pojavlaunch.prefs.LauncherPreferences;
import net.kdt.pojavlaunch.utils.DownloadUtils;
import net.kdt.pojavlaunch.utils.JREUtils;
import net.kdt.pojavlaunch.utils.JSONUtils;
import net.kdt.pojavlaunch.utils.OldVersionsUtils;
import net.kdt.pojavlaunch.value.DependentLibrary;
import net.kdt.pojavlaunch.value.MinecraftAccount;
import net.kdt.pojavlaunch.value.launcherprofiles.LauncherProfiles;
import net.kdt.pojavlaunch.value.launcherprofiles.MinecraftProfile;
import org.apache.commons.codec.binary.Hex;
import org.apache.commons.io.IOUtils;
import org.lwjgl.glfw.CallbackBridge;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
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.OutputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;
@SuppressWarnings("IOStreamConstructor")
public final class Tools {
public static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
public static String APP_NAME = "null";
public static final String RT4_MAIN_CLASS = "rt4.client";
public static final Gson GLOBAL_GSON = new GsonBuilder().setPrettyPrinting().create();
public static final String URL_HOME = "https://pojavlauncherteam.github.io";
public static String NATIVE_LIB_DIR;
public static String DIR_DATA; //Initialized later to get context
public static File DIR_CACHE;
public static String MULTIRT_HOME;
public static String LOCAL_RENDERER = null;
public static int DEVICE_ARCHITECTURE;
public static final String LAUNCHERPROFILES_RTPREFIX = "pojav://";
// New since 3.3.1
public static String DIR_ACCOUNT_NEW;
public static String DIR_GAME_HOME = Environment.getExternalStorageDirectory().getAbsolutePath() + "/games/PojavLauncher";
public static String DIR_GAME_NEW;
// New since 3.0.0
public static String DIRNAME_HOME_JRE = "lib";
// New since 2.4.2
public static String DIR_HOME_VERSION;
public static String DIR_HOME_LIBRARY;
public static String DIR_HOME_CRASH;
public static String ASSETS_PATH;
public static String OBSOLETE_RESOURCES_PATH;
public static String CTRLMAP_PATH;
public static String CTRLDEF_FILE;
public static final int RUN_MOD_INSTALLER = 2050;
private static File getPojavStorageRoot(Context ctx) {
if(SDK_INT >= 29) {
return ctx.getExternalFilesDir(null);
}else{
return new File(Environment.getExternalStorageDirectory(),"games/PojavLauncher");
}
}
/**
* Checks if the Pojav's storage root is accessible and read-writable
* @param context context to get the storage root if it's not set yet
* @return true if storage is fine, false if storage is not accessible
*/
public static boolean checkStorageRoot(Context context) {
File externalFilesDir = DIR_GAME_HOME == null ? Tools.getPojavStorageRoot(context) : new File(DIR_GAME_HOME);
//externalFilesDir == null when the storage is not mounted if it was obtained with the context call
return externalFilesDir != null && Environment.getExternalStorageState(externalFilesDir).equals(Environment.MEDIA_MOUNTED);
}
/**
* Since some constant requires the use of the Context object
* You can call this function to initialize them.
* Any value (in)directly dependant on DIR_DATA should be set only here.
*/
public static void initContextConstants(Context ctx){
DIR_CACHE = ctx.getCacheDir();
DIR_DATA = ctx.getFilesDir().getParent();
MULTIRT_HOME = DIR_DATA+"/runtimes";
DIR_GAME_HOME = getPojavStorageRoot(ctx).getAbsolutePath();
DIR_GAME_NEW = DIR_GAME_HOME + "/.minecraft";
DIR_HOME_VERSION = DIR_GAME_NEW + "/versions";
DIR_HOME_LIBRARY = DIR_GAME_NEW + "/libraries";
DIR_HOME_CRASH = DIR_GAME_NEW + "/crash-reports";
ASSETS_PATH = DIR_GAME_NEW + "/assets";
OBSOLETE_RESOURCES_PATH= DIR_GAME_NEW + "/resources";
CTRLMAP_PATH = DIR_GAME_HOME + "/controlmap";
CTRLDEF_FILE = DIR_GAME_HOME + "/controlmap/default.json";
NATIVE_LIB_DIR = ctx.getApplicationInfo().nativeLibraryDir;
}
public static void launchGLJRE(final Activity activity) throws Throwable {
Runtime runtime = MultiRTUtils.forceReread("Internal");
File gamedir = new File(Tools.DIR_DATA);
ExtraCore.setValue(ExtraConstants.OPEN_GL_VERSION, "2");
List<String> javaArgList = new ArrayList<>();
javaArgList.add("-Dorg.lwjgl.util.NoChecks=true");
getCacioJavaArgs(javaArgList, runtime.javaVersion == 8);
javaArgList.add("-DpluginDir="+ Tools.DIR_DATA + "/plugins/");
javaArgList.add("-DconfigFile="+Tools.DIR_DATA + "/config.json");
javaArgList.add("-cp");
javaArgList.add(getLWJGL3ClassPath()+":"+Tools.DIR_DATA+"/rt4.jar");
javaArgList.add(RT4_MAIN_CLASS);
String args = LauncherPreferences.PREF_CUSTOM_JAVA_ARGS;
JREUtils.launchJavaVM(activity, runtime, gamedir, javaArgList, args);
}
public static File getGameDirPath(@NonNull MinecraftProfile minecraftProfile){
if(minecraftProfile.gameDir != null){
if(minecraftProfile.gameDir.startsWith(Tools.LAUNCHERPROFILES_RTPREFIX))
return new File(minecraftProfile.gameDir.replace(Tools.LAUNCHERPROFILES_RTPREFIX,Tools.DIR_GAME_HOME+"/"));
else
return new File(Tools.DIR_GAME_HOME,minecraftProfile.gameDir);
}
return new File(Tools.DIR_GAME_NEW);
}
public static void buildNotificationChannel(Context context){
if(Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return;
NotificationChannel channel = new NotificationChannel(
context.getString(R.string.notif_channel_id),
context.getString(R.string.notif_channel_name), NotificationManager.IMPORTANCE_DEFAULT);
NotificationManagerCompat manager = NotificationManagerCompat.from(context);
manager.createNotificationChannel(channel);
}
public static void disableSplash(File dir) {
File configDir = new File(dir, "config");
if(configDir.exists() || configDir.mkdirs()) {
File forgeSplashFile = new File(dir, "config/splash.properties");
String forgeSplashContent = "enabled=true";
try {
if (forgeSplashFile.exists()) {
forgeSplashContent = Tools.read(forgeSplashFile.getAbsolutePath());
}
if (forgeSplashContent.contains("enabled=true")) {
Tools.write(forgeSplashFile.getAbsolutePath(),
forgeSplashContent.replace("enabled=true", "enabled=false"));
}
} catch (IOException e) {
Log.w(Tools.APP_NAME, "Could not disable Forge 1.12.2 and below splash screen!", e);
}
} else {
Log.w(Tools.APP_NAME, "Failed to create the configuration directory");
}
}
public static void getCacioJavaArgs(List<String> javaArgList, boolean isJava8) {
// Caciocavallo config AWT-enabled version
javaArgList.add("-Djava.awt.headless=false");
javaArgList.add("-Dcacio.managed.screensize=" + AWTCanvasView.AWT_CANVAS_WIDTH + "x" + AWTCanvasView.AWT_CANVAS_HEIGHT);
javaArgList.add("-Dcacio.font.fontmanager=sun.awt.X11FontManager");
javaArgList.add("-Dcacio.font.fontscaler=sun.font.FreetypeFontScaler");
javaArgList.add("-Dswing.defaultlaf=javax.swing.plaf.metal.MetalLookAndFeel");
if (isJava8) {
javaArgList.add("-Dawt.toolkit=net.java.openjdk.cacio.ctc.CTCToolkit");
javaArgList.add("-Djava.awt.graphicsenv=net.java.openjdk.cacio.ctc.CTCGraphicsEnvironment");
} else {
javaArgList.add("-Dawt.toolkit=com.github.caciocavallosilano.cacio.ctc.CTCToolkit");
javaArgList.add("-Djava.awt.graphicsenv=com.github.caciocavallosilano.cacio.ctc.CTCGraphicsEnvironment");
javaArgList.add("-Djava.system.class.loader=com.github.caciocavallosilano.cacio.ctc.CTCPreloadClassLoader");
javaArgList.add("--add-exports=java.desktop/java.awt=ALL-UNNAMED");
javaArgList.add("--add-exports=java.desktop/java.awt.peer=ALL-UNNAMED");
javaArgList.add("--add-exports=java.desktop/sun.awt.image=ALL-UNNAMED");
javaArgList.add("--add-exports=java.desktop/sun.java2d=ALL-UNNAMED");
javaArgList.add("--add-exports=java.desktop/java.awt.dnd.peer=ALL-UNNAMED");
javaArgList.add("--add-exports=java.desktop/sun.awt=ALL-UNNAMED");
javaArgList.add("--add-exports=java.desktop/sun.awt.event=ALL-UNNAMED");
javaArgList.add("--add-exports=java.desktop/sun.awt.datatransfer=ALL-UNNAMED");
javaArgList.add("--add-exports=java.desktop/sun.font=ALL-UNNAMED");
javaArgList.add("--add-exports=java.base/sun.security.action=ALL-UNNAMED");
javaArgList.add("--add-opens=java.base/java.util=ALL-UNNAMED");
javaArgList.add("--add-opens=java.desktop/java.awt=ALL-UNNAMED");
javaArgList.add("--add-opens=java.desktop/sun.font=ALL-UNNAMED");
javaArgList.add("--add-opens=java.desktop/sun.java2d=ALL-UNNAMED");
javaArgList.add("--add-opens=java.base/java.lang.reflect=ALL-UNNAMED");
// Opens the java.net package to Arc DNS injector on Java 9+
javaArgList.add("--add-opens=java.base/java.net=ALL-UNNAMED");
}
StringBuilder cacioClasspath = new StringBuilder();
cacioClasspath.append("-Xbootclasspath/").append(isJava8 ? "p" : "a");
File cacioDir = new File(DIR_GAME_HOME + "/caciocavallo" + (isJava8 ? "" : "17"));
File[] cacioFiles = cacioDir.listFiles();
if (cacioFiles != null) {
for (File file : cacioFiles) {
if (file.getName().endsWith(".jar")) {
cacioClasspath.append(":").append(file.getAbsolutePath());
}
}
}
javaArgList.add(cacioClasspath.toString());
}
public static String[] getMinecraftJVMArgs(String versionName, File gameDir) {
JMinecraftVersionList.Version versionInfo = Tools.getVersionInfo(versionName, true);
// Parse Forge 1.17+ additional JVM Arguments
if (versionInfo.inheritsFrom == null || versionInfo.arguments == null || versionInfo.arguments.jvm == null) {
return new String[0];
}
Map<String, String> varArgMap = new ArrayMap<>();
varArgMap.put("classpath_separator", ":");
varArgMap.put("library_directory", DIR_HOME_LIBRARY);
varArgMap.put("version_name", versionInfo.id);
varArgMap.put("natives_directory", Tools.NATIVE_LIB_DIR);
List<String> minecraftArgs = new ArrayList<>();
if (versionInfo.arguments != null) {
for (Object arg : versionInfo.arguments.jvm) {
if (arg instanceof String) {
minecraftArgs.add((String) arg);
} //TODO: implement (?maybe?)
}
}
return JSONUtils.insertJSONValueList(minecraftArgs.toArray(new String[0]), varArgMap);
}
public static String[] getMinecraftClientArgs(MinecraftAccount profile, JMinecraftVersionList.Version versionInfo, File gameDir) {
String username = profile.username;
String versionName = versionInfo.id;
if (versionInfo.inheritsFrom != null) {
versionName = versionInfo.inheritsFrom;
}
String userType = "mojang";
Map<String, String> varArgMap = new ArrayMap<>();
varArgMap.put("auth_session", profile.accessToken); // For legacy versions of MC
varArgMap.put("auth_access_token", profile.accessToken);
varArgMap.put("auth_player_name", username);
varArgMap.put("auth_uuid", profile.profileId.replace("-", ""));
varArgMap.put("auth_xuid", profile.xuid);
varArgMap.put("assets_root", Tools.ASSETS_PATH);
varArgMap.put("assets_index_name", versionInfo.assets);
varArgMap.put("game_assets", Tools.ASSETS_PATH);
varArgMap.put("game_directory", gameDir.getAbsolutePath());
varArgMap.put("user_properties", "{}");
varArgMap.put("user_type", userType);
varArgMap.put("version_name", versionName);
varArgMap.put("version_type", versionInfo.type);
List<String> minecraftArgs = new ArrayList<>();
if (versionInfo.arguments != null) {
// Support Minecraft 1.13+
for (Object arg : versionInfo.arguments.game) {
if (arg instanceof String) {
minecraftArgs.add((String) arg);
} //TODO: implement else clause
}
}
return JSONUtils.insertJSONValueList(
splitAndFilterEmpty(
versionInfo.minecraftArguments == null ?
fromStringArray(minecraftArgs.toArray(new String[0])):
versionInfo.minecraftArguments
), varArgMap
);
}
public static String fromStringArray(String[] strArr) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < strArr.length; i++) {
if (i > 0) builder.append(" ");
builder.append(strArr[i]);
}
return builder.toString();
}
private static String[] splitAndFilterEmpty(String argStr) {
List<String> strList = new ArrayList<>();
for (String arg : argStr.split(" ")) {
if (!arg.isEmpty()) {
strList.add(arg);
}
}
//strList.add("--fullscreen");
return strList.toArray(new String[0]);
}
public static String artifactToPath(DependentLibrary library) {
if (library.downloads != null &&
library.downloads.artifact != null &&
library.downloads.artifact.path != null)
return library.downloads.artifact.path;
String[] libInfos = library.name.split(":");
return libInfos[0].replaceAll("\\.", "/") + "/" + libInfos[1] + "/" + libInfos[2] + "/" + libInfos[1] + "-" + libInfos[2] + ".jar";
}
public static String getPatchedFile(String version) {
return DIR_HOME_VERSION + "/" + version + "/" + version + ".jar";
}
private static String getLWJGL3ClassPath() {
StringBuilder libStr = new StringBuilder();
File lwjgl3Folder = new File(Tools.DIR_GAME_HOME, "lwjgl3");
File[] lwjgl3Files = lwjgl3Folder.listFiles();
if (lwjgl3Files != null) {
for (File file: lwjgl3Files) {
if (file.getName().endsWith(".jar")) {
libStr.append(file.getAbsolutePath()).append(":");
}
}
}
// Remove the ':' at the end
libStr.setLength(libStr.length() - 1);
return libStr.toString();
}
private final static boolean isClientFirst = false;
public static String generateLaunchClassPath(JMinecraftVersionList.Version info,String actualname) {
StringBuilder libStr = new StringBuilder(); //versnDir + "/" + version + "/" + version + ".jar:";
String[] classpath = generateLibClasspath(info);
if (isClientFirst) {
libStr.append(getPatchedFile(actualname));
}
for (String perJar : classpath) {
if (!new File(perJar).exists()) {
Log.d(APP_NAME, "Ignored non-exists file: " + perJar);
continue;
}
libStr.append((isClientFirst ? ":" : "")).append(perJar).append(!isClientFirst ? ":" : "");
}
if (!isClientFirst) {
libStr.append(getPatchedFile(actualname));
}
return libStr.toString();
}
public static DisplayMetrics getDisplayMetrics(Activity activity) {
DisplayMetrics displayMetrics = new DisplayMetrics();
if(SDK_INT >= Build.VERSION_CODES.N && (activity.isInMultiWindowMode() || activity.isInPictureInPictureMode())){
//For devices with free form/split screen, we need window size, not screen size.
displayMetrics = activity.getResources().getDisplayMetrics();
}else{
if (SDK_INT >= Build.VERSION_CODES.R) {
activity.getDisplay().getRealMetrics(displayMetrics);
} else { // Removed the clause for devices with unofficial notch support, since it also ruins all devices with virtual nav bars before P
activity.getWindowManager().getDefaultDisplay().getRealMetrics(displayMetrics);
}
if(!PREF_IGNORE_NOTCH){
//Remove notch width when it isn't ignored.
if(activity.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT)
displayMetrics.heightPixels -= PREF_NOTCH_SIZE;
else
displayMetrics.widthPixels -= PREF_NOTCH_SIZE;
}
}
currentDisplayMetrics = displayMetrics;
return displayMetrics;
}
public static void setFullscreen(Activity activity, boolean fullscreen) {
final View decorView = activity.getWindow().getDecorView();
View.OnSystemUiVisibilityChangeListener visibilityChangeListener = visibility -> {
if(fullscreen){
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE
| View.SYSTEM_UI_FLAG_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
}else{
decorView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
}
};
decorView.setOnSystemUiVisibilityChangeListener(visibilityChangeListener);
visibilityChangeListener.onSystemUiVisibilityChange(decorView.getSystemUiVisibility()); //call it once since the UI state may not change after the call, so the activity wont become fullscreen
}
public static DisplayMetrics currentDisplayMetrics;
public static void updateWindowSize(Activity activity) {
currentDisplayMetrics = getDisplayMetrics(activity);
CallbackBridge.physicalWidth = currentDisplayMetrics.widthPixels;
CallbackBridge.physicalHeight = currentDisplayMetrics.heightPixels;
}
public static float dpToPx(float dp) {
//Better hope for the currentDisplayMetrics to be good
return dp * currentDisplayMetrics.density;
}
public static float pxToDp(float px){
//Better hope for the currentDisplayMetrics to be good
return px / currentDisplayMetrics.density;
}
public static void copyAssetFile(Context ctx, String fileName, String output, boolean overwrite) throws IOException {
copyAssetFile(ctx, fileName, output, new File(fileName).getName(), overwrite);
}
public static void copyAssetFile(Context ctx, String fileName, String output, String outputName, boolean overwrite) throws IOException {
File parentFolder = new File(output);
if(!parentFolder.exists() && !parentFolder.mkdirs()) {
throw new IOException("Failed to create parent directory");
}
File destinationFile = new File(output, outputName);
if(!destinationFile.exists() || overwrite){
try(InputStream inputStream = ctx.getAssets().open(fileName)) {
try (OutputStream outputStream = new FileOutputStream(destinationFile)){
IOUtils.copy(inputStream, outputStream);
}
}
}
}
public static String printToString(Throwable throwable) {
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
throwable.printStackTrace(printWriter);
printWriter.close();
return stringWriter.toString();
}
public static void showError(Context ctx, Throwable e) {
showError(ctx, e, false);
}
public static void showError(final Context ctx, final Throwable e, final boolean exitIfOk) {
showError(ctx, R.string.global_error, null ,e, exitIfOk, false);
}
public static void showError(final Context ctx, final int rolledMessage, final Throwable e) {
showError(ctx, R.string.global_error, ctx.getString(rolledMessage), e, false, false);
}
public static void showError(final Context ctx, final String rolledMessage, final Throwable e) {
showError(ctx, R.string.global_error, rolledMessage, e, false, false);
}
public static void showError(final Context ctx, final int titleId, final Throwable e, final boolean exitIfOk) {
showError(ctx, titleId, null, e, exitIfOk, false);
}
private static void showError(final Context ctx, final int titleId, final String rolledMessage, final Throwable e, final boolean exitIfOk, final boolean showMore) {
e.printStackTrace();
Runnable runnable = () -> {
final String errMsg = showMore ? printToString(e) : rolledMessage != null ? rolledMessage : e.getMessage();
AlertDialog.Builder builder = new AlertDialog.Builder(ctx)
.setTitle(titleId)
.setMessage(errMsg)
.setPositiveButton(android.R.string.ok, (p1, p2) -> {
if(exitIfOk) {
if (ctx instanceof MainActivity) {
MainActivity.fullyExit();
} else if (ctx instanceof Activity) {
((Activity) ctx).finish();
}
}
})
.setNegativeButton(showMore ? R.string.error_show_less : R.string.error_show_more, (p1, p2) -> showError(ctx, titleId, rolledMessage, e, exitIfOk, !showMore))
.setNeutralButton(android.R.string.copy, (p1, p2) -> {
ClipboardManager mgr = (ClipboardManager) ctx.getSystemService(Context.CLIPBOARD_SERVICE);
mgr.setPrimaryClip(ClipData.newPlainText("error", Log.getStackTraceString(e)));
if(exitIfOk) {
if (ctx instanceof MainActivity) {
MainActivity.fullyExit();
} else {
((Activity) ctx).finish();
}
}
})
.setCancelable(!exitIfOk);
try {
builder.show();
} catch (Throwable th) {
th.printStackTrace();
}
};
if (ctx instanceof Activity) {
((Activity) ctx).runOnUiThread(runnable);
} else {
runnable.run();
}
}
public static void dialogOnUiThread(final Activity activity, final CharSequence title, final CharSequence message) {
activity.runOnUiThread(()->dialog(activity, title, message));
}
public static void dialog(final Context context, final CharSequence title, final CharSequence message) {
new AlertDialog.Builder(context)
.setTitle(title)
.setMessage(message)
.setPositiveButton(android.R.string.ok, null)
.show();
}
public static void openURL(Activity act, String url) {
Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
act.startActivity(browserIntent);
}
private static boolean checkRules(JMinecraftVersionList.Arguments.ArgValue.ArgRules[] rules) {
if(rules == null) return true; // always allow
for (JMinecraftVersionList.Arguments.ArgValue.ArgRules rule : rules) {
if (rule.action.equals("allow") && rule.os != null && rule.os.name.equals("osx")) {
return false; //disallow
}
}
return true; // allow if none match
}
public static String[] generateLibClasspath(JMinecraftVersionList.Version info) {
List<String> libDir = new ArrayList<>();
for (DependentLibrary libItem: info.libraries) {
if(!checkRules(libItem.rules)) continue;
libDir.add(Tools.DIR_HOME_LIBRARY + "/" + artifactToPath(libItem));
}
return libDir.toArray(new String[0]);
}
public static JMinecraftVersionList.Version getVersionInfo(String versionName) {
return getVersionInfo(versionName, false);
}
@SuppressWarnings({"unchecked", "rawtypes"})
public static JMinecraftVersionList.Version getVersionInfo(String versionName, boolean skipInheriting) {
try {
JMinecraftVersionList.Version customVer = Tools.GLOBAL_GSON.fromJson(read(DIR_HOME_VERSION + "/" + versionName + "/" + versionName + ".json"), JMinecraftVersionList.Version.class);
if (skipInheriting || customVer.inheritsFrom == null || customVer.inheritsFrom.equals(customVer.id)) {
return customVer;
} else {
JMinecraftVersionList.Version inheritsVer;
//If it won't download, just search for it
try{
inheritsVer = Tools.GLOBAL_GSON.fromJson(read(DIR_HOME_VERSION + "/" + customVer.inheritsFrom + "/" + customVer.inheritsFrom + ".json"), JMinecraftVersionList.Version.class);
}catch(IOException e) {
throw new RuntimeException("Can't find the source version for "+ versionName +" (req version="+customVer.inheritsFrom+")");
}
//inheritsVer.inheritsFrom = inheritsVer.id;
insertSafety(inheritsVer, customVer,
"assetIndex", "assets", "id",
"mainClass", "minecraftArguments",
"releaseTime", "time", "type"
);
List<DependentLibrary> libList = new ArrayList<>(Arrays.asList(inheritsVer.libraries));
try {
loop_1:
for (DependentLibrary lib : customVer.libraries) {
String libName = lib.name.substring(0, lib.name.lastIndexOf(":"));
for (int i = 0; i < libList.size(); i++) {
DependentLibrary libAdded = libList.get(i);
String libAddedName = libAdded.name.substring(0, libAdded.name.lastIndexOf(":"));
if (libAddedName.equals(libName)) {
Log.d(APP_NAME, "Library " + libName + ": Replaced version " +
libName.substring(libName.lastIndexOf(":") + 1) + " with " +
libAddedName.substring(libAddedName.lastIndexOf(":") + 1));
libList.set(i, lib);
continue loop_1;
}
}
libList.add(0, lib);
}
} finally {
inheritsVer.libraries = libList.toArray(new DependentLibrary[0]);
}
// Inheriting Minecraft 1.13+ with append custom args
if (inheritsVer.arguments != null && customVer.arguments != null) {
List totalArgList = new ArrayList(Arrays.asList(inheritsVer.arguments.game));
int nskip = 0;
for (int i = 0; i < customVer.arguments.game.length; i++) {
if (nskip > 0) {
nskip--;
continue;
}
Object perCustomArg = customVer.arguments.game[i];
if (perCustomArg instanceof String) {
String perCustomArgStr = (String) perCustomArg;
// Check if there is a duplicate argument on combine
if (perCustomArgStr.startsWith("--") && totalArgList.contains(perCustomArgStr)) {
perCustomArg = customVer.arguments.game[i + 1];
if (perCustomArg instanceof String) {
perCustomArgStr = (String) perCustomArg;
// If the next is argument value, skip it
if (!perCustomArgStr.startsWith("--")) {
nskip++;
}
}
} else {
totalArgList.add(perCustomArgStr);
}
} else if (!totalArgList.contains(perCustomArg)) {
totalArgList.add(perCustomArg);
}
}
inheritsVer.arguments.game = totalArgList.toArray(new Object[0]);
}
return inheritsVer;
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
// Prevent NullPointerException
private static void insertSafety(JMinecraftVersionList.Version targetVer, JMinecraftVersionList.Version fromVer, String... keyArr) {
for (String key : keyArr) {
Object value = null;
try {
Field fieldA = fromVer.getClass().getDeclaredField(key);
value = fieldA.get(fromVer);
if (((value instanceof String) && !((String) value).isEmpty()) || value != null) {
Field fieldB = targetVer.getClass().getDeclaredField(key);
fieldB.set(targetVer, value);
}
} catch (Throwable th) {
Log.w(Tools.APP_NAME, "Unable to insert " + key + "=" + value, th);
}
}
}
public static String read(InputStream is) throws IOException {
String readResult = IOUtils.toString(is, StandardCharsets.UTF_8);
is.close();
return readResult;
}
public static String read(String path) throws IOException {
return read(new FileInputStream(path));
}
public static void write(String path, String content) throws IOException {
File file = new File(path);
File parent = file.getParentFile();
if(parent != null && !parent.exists()) {
if(!parent.mkdirs()) throw new IOException("Failed to create parent directory");
}
try(FileOutputStream outStream = new FileOutputStream(file)) {
IOUtils.write(content, outStream);
}
}
public static void downloadFile(String urlInput, String nameOutput) throws IOException {
File file = new File(nameOutput);
DownloadUtils.downloadFile(urlInput, file);
}
public interface DownloaderFeedback {
void updateProgress(int curr, int max);
}
public static class ZipTool
{
private ZipTool(){}
public static void zip(List<File> files, File zipFile) throws IOException {
final int BUFFER_SIZE = 2048;
BufferedInputStream origin = null;
ZipOutputStream out = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(zipFile)));
try {
byte data[] = new byte[BUFFER_SIZE];
for (File file : files) {
FileInputStream fileInputStream = new FileInputStream( file );
origin = new BufferedInputStream(fileInputStream, BUFFER_SIZE);
try {
ZipEntry entry = new ZipEntry(file.getName());
out.putNextEntry(entry);
int count;
while ((count = origin.read(data, 0, BUFFER_SIZE)) != -1) {
out.write(data, 0, count);
}
}
finally {
origin.close();
}
}
} finally {
out.close();
}
}
public static void unzip(File zipFile, File targetDirectory) throws IOException {
final int BUFFER_SIZE = 1024;
ZipInputStream zis = new ZipInputStream(
new BufferedInputStream(new FileInputStream(zipFile)));
try {
ZipEntry ze;
int count;
byte[] buffer = new byte[BUFFER_SIZE];
while ((ze = zis.getNextEntry()) != null) {
File file = new File(targetDirectory, ze.getName());
File dir = ze.isDirectory() ? file : file.getParentFile();
if (!dir.isDirectory() && !dir.mkdirs())
throw new FileNotFoundException("Failed to ensure directory: " +
dir.getAbsolutePath());
if (ze.isDirectory())
continue;
FileOutputStream fout = new FileOutputStream(file);
try {
while ((count = zis.read(buffer)) != -1)
fout.write(buffer, 0, count);
} finally {
fout.close();
}
}
} finally {
zis.close();
}
}
}
public static boolean compareSHA1(File f, String sourceSHA) {
try {
String sha1_dst;
try (InputStream is = new FileInputStream(f)) {
sha1_dst = new String(Hex.encodeHex(org.apache.commons.codec.digest.DigestUtils.sha1(is)));
}
if(sourceSHA != null) {
return sha1_dst.equalsIgnoreCase(sourceSHA);
} else{
return true; // fake match
}
}catch (IOException e) {
Log.i("SHA1","Fake-matching a hash due to a read error",e);
return true;
}
}
public static void ignoreNotch(boolean shouldIgnore, Activity ctx){
if (SDK_INT >= P) {
if (shouldIgnore) {
ctx.getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
} else {
ctx.getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER;
}
ctx.getWindow().setFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
Tools.updateWindowSize(ctx);
}
}
public static int getTotalDeviceMemory(Context ctx){
ActivityManager actManager = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
actManager.getMemoryInfo(memInfo);
return (int) (memInfo.totalMem / 1048576L);
}
public static int getFreeDeviceMemory(Context ctx){
ActivityManager actManager = (ActivityManager) ctx.getSystemService(Context.ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memInfo = new ActivityManager.MemoryInfo();
actManager.getMemoryInfo(memInfo);
return (int) (memInfo.availMem / 1048576L);
}
public static int getDisplayFriendlyRes(int displaySideRes, float scaling){
displaySideRes *= scaling;
if(displaySideRes % 2 != 0) displaySideRes ++;
return displaySideRes;
}
public static String getFileName(Context ctx, Uri uri) {
Cursor c = ctx.getContentResolver().query(uri, null, null, null, null);
if(c == null) return uri.getLastPathSegment(); // idk myself but it happens on asus file manager
c.moveToFirst();
int columnIndex = c.getColumnIndex(OpenableColumns.DISPLAY_NAME);
if(columnIndex == -1) return uri.getLastPathSegment();
String fileName = c.getString(columnIndex);
c.close();
return fileName;
}
/** Swap the main fragment with another */
public static void swapFragment(FragmentActivity fragmentActivity , Class<? extends Fragment> fragmentClass,
@Nullable String fragmentTag, boolean addCurrentToBackstack, @Nullable Bundle bundle) {
// When people tab out, it might happen
//TODO handle custom animations
FragmentTransaction transaction = fragmentActivity.getSupportFragmentManager().beginTransaction()
.setReorderingAllowed(true)
.replace(R.id.container_fragment, fragmentClass, bundle, fragmentTag);
if(addCurrentToBackstack) transaction.addToBackStack(null);
transaction.commit();
}
/** Remove the current fragment */
public static void removeCurrentFragment(FragmentActivity fragmentActivity){
fragmentActivity.getSupportFragmentManager().popBackStackImmediate();
}
public static void installMod(Activity activity, boolean customJavaArgs) {
if (MultiRTUtils.getExactJreName(8) == null) {
Toast.makeText(activity, R.string.multirt_nojava8rt, Toast.LENGTH_LONG).show();
return;
}
if(!customJavaArgs){ // Launch the intent to get the jar file
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
intent.addCategory(Intent.CATEGORY_OPENABLE);
String mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension("jar");
if(mimeType == null) mimeType = "*/*";
intent.setType(mimeType);
activity.startActivityForResult(intent, RUN_MOD_INSTALLER);
return;
}
// install mods with custom arguments
final EditText editText = new EditText(activity);
editText.setSingleLine();
editText.setHint("-jar/-cp /path/to/file.jar ...");
AlertDialog.Builder builder = new AlertDialog.Builder(activity)
.setTitle(R.string.alerttitle_installmod)
.setNegativeButton(android.R.string.cancel, null)
.setView(editText)
.setPositiveButton(android.R.string.ok, (di, i) -> {
Intent intent = new Intent(activity, JavaGUILauncherActivity.class);
intent.putExtra("skipDetectMod", true);
intent.putExtra("javaArgs", editText.getText().toString());
activity.startActivity(intent);
});
builder.show();
}
/** Display and return a progress dialog, instructing to wait */
private static ProgressDialog getWaitingDialog(Context ctx){
final ProgressDialog barrier = new ProgressDialog(ctx);
barrier.setMessage(ctx.getString(R.string.global_waiting));
barrier.setProgressStyle(ProgressDialog.STYLE_SPINNER);
barrier.setCancelable(false);
barrier.show();
return barrier;
}
/** Copy the mod file, and launch the mod installer activity */
public static void launchModInstaller(Activity activity, @NonNull Intent data){
final ProgressDialog alertDialog = getWaitingDialog(activity);
final Uri uri = data.getData();
alertDialog.setMessage(activity.getString(R.string.multirt_progress_caching));
sExecutorService.execute(() -> {
try {
final String name = getFileName(activity, uri);
final File modInstallerFile = new File(Tools.DIR_CACHE, name);
FileOutputStream fos = new FileOutputStream(modInstallerFile);
InputStream input = activity.getContentResolver().openInputStream(uri);
IOUtils.copy(input, fos);
input.close();
fos.close();
activity.runOnUiThread(() -> {
alertDialog.dismiss();
Intent intent = new Intent(activity, JavaGUILauncherActivity.class);
intent.putExtra("modFile", modInstallerFile);
activity.startActivity(intent);
});
}catch(IOException e) {
Tools.showError(activity, e);
}
});
}
public static void installRuntimeFromUri(Activity activity, Uri uri){
sExecutorService.execute(() -> {
try {
String name = getFileName(activity, uri);
MultiRTUtils.installRuntimeNamed(
NATIVE_LIB_DIR,
activity.getContentResolver().openInputStream(uri),
name);
MultiRTUtils.postPrepare(name);
} catch (IOException e) {
Tools.showError(activity, e);
}
});
}
public static String extractUntilCharacter(String input, String whatFor, char terminator) {
int whatForStart = input.indexOf(whatFor);
if(whatForStart == -1) return null;
whatForStart += whatFor.length();
int terminatorIndex = input.indexOf(terminator, whatForStart);
if(terminatorIndex == -1) return null;
return input.substring(whatForStart, terminatorIndex);
}
public static boolean isValidString(String string) {
return string != null && !string.isEmpty();
}
public static String getRuntimeName(String prefixedName) {
if(prefixedName == null) return prefixedName;
if(!prefixedName.startsWith(Tools.LAUNCHERPROFILES_RTPREFIX)) return null;
return prefixedName.substring(Tools.LAUNCHERPROFILES_RTPREFIX.length());
}
public static String getSelectedRuntime(MinecraftProfile minecraftProfile) {
String runtime = LauncherPreferences.PREF_DEFAULT_RUNTIME;
String profileRuntime = getRuntimeName(minecraftProfile.javaDir);
if(profileRuntime != null) {
if(MultiRTUtils.forceReread(profileRuntime).versionString != null) {
runtime = profileRuntime;
}
}
return runtime;
}
public static void runOnUiThread(Runnable runnable) {
MAIN_HANDLER.post(runnable);
}
public static @NonNull String pickRuntime(MinecraftProfile minecraftProfile, int targetJavaVersion) {
String runtime = getSelectedRuntime(minecraftProfile);
String profileRuntime = getRuntimeName(minecraftProfile.javaDir);
Runtime pickedRuntime = MultiRTUtils.read(runtime);
if(runtime == null || pickedRuntime.javaVersion == 0 || pickedRuntime.javaVersion < targetJavaVersion) {
String preferredRuntime = MultiRTUtils.getNearestJreName(targetJavaVersion);
if(preferredRuntime == null) throw new RuntimeException("Failed to autopick runtime!");
if(profileRuntime != null) minecraftProfile.javaDir = Tools.LAUNCHERPROFILES_RTPREFIX+preferredRuntime;
runtime = preferredRuntime;
}
return runtime;
}
/** Triggers the share intent chooser, with the latestlog file attached to it */
public static void shareLog(Context context){
Uri contentUri = DocumentsContract.buildDocumentUri(context.getString(R.string.storageProviderAuthorities), Tools.DIR_GAME_HOME + "/latestlog.txt");
Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
shareIntent.setType("text/plain");
Intent sendIntent = Intent.createChooser(shareIntent, "latestlog.txt");
context.startActivity(sendIntent);
}
}