Android fixes
This commit is contained in:
224
android/src/mindustry/android/AndroidLauncher.java
Normal file
224
android/src/mindustry/android/AndroidLauncher.java
Normal file
@@ -0,0 +1,224 @@
|
||||
package mindustry.android;
|
||||
|
||||
import android.*;
|
||||
import android.app.*;
|
||||
import android.content.*;
|
||||
import android.content.pm.*;
|
||||
import android.net.*;
|
||||
import android.os.Build.*;
|
||||
import android.os.*;
|
||||
import android.provider.Settings.*;
|
||||
import android.telephony.*;
|
||||
import arc.*;
|
||||
import arc.backend.android.*;
|
||||
import arc.files.*;
|
||||
import arc.func.*;
|
||||
import arc.scene.ui.layout.*;
|
||||
import arc.util.*;
|
||||
import arc.util.serialization.*;
|
||||
import mindustry.*;
|
||||
import mindustry.game.Saves.*;
|
||||
import mindustry.io.*;
|
||||
import mindustry.ui.dialogs.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.lang.System;
|
||||
import java.util.*;
|
||||
|
||||
import static mindustry.Vars.*;
|
||||
|
||||
|
||||
public class AndroidLauncher extends AndroidApplication{
|
||||
public static final int PERMISSION_REQUEST_CODE = 1;
|
||||
boolean doubleScaleTablets = true;
|
||||
FileChooser chooser;
|
||||
Runnable permCallback;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState){
|
||||
super.onCreate(savedInstanceState);
|
||||
if(doubleScaleTablets && isTablet(this.getContext())){
|
||||
Scl.setAddition(0.5f);
|
||||
}
|
||||
|
||||
initialize(new ClientLauncher(){
|
||||
|
||||
@Override
|
||||
public void hide(){
|
||||
moveTaskToBack(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getUUID(){
|
||||
try{
|
||||
String s = Secure.getString(getContext().getContentResolver(), Secure.ANDROID_ID);
|
||||
int len = s.length();
|
||||
byte[] data = new byte[len / 2];
|
||||
for(int i = 0; i < len; i += 2){
|
||||
data[i / 2] = (byte)((Character.digit(s.charAt(i), 16) << 4)
|
||||
+ Character.digit(s.charAt(i + 1), 16));
|
||||
}
|
||||
String result = new String(Base64Coder.encode(data));
|
||||
if(result.equals("AAAAAAAAAOA=")) throw new RuntimeException("Bad UUID.");
|
||||
return result;
|
||||
}catch(Exception e){
|
||||
return super.getUUID();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public org.mozilla.javascript.Context getScriptContext(){
|
||||
return AndroidRhinoContext.enter(getContext().getCacheDir());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shareFile(Fi file){
|
||||
}
|
||||
|
||||
@Override
|
||||
public void showFileChooser(boolean open, String extension, Cons<Fi> cons){
|
||||
if(VERSION.SDK_INT >= VERSION_CODES.Q){
|
||||
Intent intent = new Intent(open ? Intent.ACTION_OPEN_DOCUMENT : Intent.ACTION_CREATE_DOCUMENT);
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE);
|
||||
intent.setType(extension.equals("zip") ? "application/zip" : "*/*");
|
||||
addResultListener(i -> startActivityForResult(intent, i), (code, in) -> {
|
||||
if(code == Activity.RESULT_OK && in != null && in.getData() != null){
|
||||
Uri uri = in.getData();
|
||||
|
||||
if(uri.getPath().contains("(invalid)")) return;
|
||||
|
||||
Core.app.post(() -> Core.app.post(() -> cons.get(new Fi(uri.getPath()){
|
||||
@Override
|
||||
public InputStream read(){
|
||||
try{
|
||||
return getContentResolver().openInputStream(uri);
|
||||
}catch(IOException e){
|
||||
throw new ArcRuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream write(boolean append){
|
||||
try{
|
||||
return getContentResolver().openOutputStream(uri);
|
||||
}catch(IOException e){
|
||||
throw new ArcRuntimeException(e);
|
||||
}
|
||||
}
|
||||
})));
|
||||
}
|
||||
});
|
||||
}else if(VERSION.SDK_INT >= VERSION_CODES.M && !(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED &&
|
||||
checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED)){
|
||||
chooser = new FileChooser(open ? "$open" : "$save", file -> file.extension().equalsIgnoreCase(extension), open, file -> {
|
||||
if(!open){
|
||||
cons.get(file.parent().child(file.nameWithoutExtension() + "." + extension));
|
||||
}else{
|
||||
cons.get(file);
|
||||
}
|
||||
});
|
||||
|
||||
ArrayList<String> perms = new ArrayList<>();
|
||||
if(checkSelfPermission(Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
|
||||
perms.add(Manifest.permission.WRITE_EXTERNAL_STORAGE);
|
||||
}
|
||||
if(checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED){
|
||||
perms.add(Manifest.permission.READ_EXTERNAL_STORAGE);
|
||||
}
|
||||
requestPermissions(perms.toArray(new String[0]), PERMISSION_REQUEST_CODE);
|
||||
}else{
|
||||
super.showFileChooser(open, extension, cons);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void beginForceLandscape(){
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endForceLandscape(){
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER);
|
||||
}
|
||||
|
||||
}, new AndroidApplicationConfiguration(){{
|
||||
useImmersiveMode = true;
|
||||
depth = 0;
|
||||
hideStatusBar = true;
|
||||
//errorHandler = ModCrashHandler::handle;
|
||||
}});
|
||||
checkFiles(getIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults){
|
||||
if(requestCode == PERMISSION_REQUEST_CODE){
|
||||
for(int i : grantResults){
|
||||
if(i != PackageManager.PERMISSION_GRANTED) return;
|
||||
}
|
||||
if(chooser != null){
|
||||
Core.app.post(chooser::show);
|
||||
}
|
||||
if(permCallback != null){
|
||||
Core.app.post(permCallback);
|
||||
permCallback = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkFiles(Intent intent){
|
||||
try{
|
||||
Uri uri = intent.getData();
|
||||
if(uri != null){
|
||||
File myFile = null;
|
||||
String scheme = uri.getScheme();
|
||||
if(scheme.equals("file")){
|
||||
String fileName = uri.getEncodedPath();
|
||||
myFile = new File(fileName);
|
||||
}else if(!scheme.equals("content")){
|
||||
//error
|
||||
return;
|
||||
}
|
||||
boolean save = uri.getPath().endsWith(saveExtension);
|
||||
boolean map = uri.getPath().endsWith(mapExtension);
|
||||
InputStream inStream;
|
||||
if(myFile != null) inStream = new FileInputStream(myFile);
|
||||
else inStream = getContentResolver().openInputStream(uri);
|
||||
Core.app.post(() -> Core.app.post(() -> {
|
||||
if(save){ //open save
|
||||
System.out.println("Opening save.");
|
||||
Fi file = Core.files.local("temp-save." + saveExtension);
|
||||
file.write(inStream, false);
|
||||
if(SaveIO.isSaveValid(file)){
|
||||
try{
|
||||
SaveSlot slot = control.saves.importSave(file);
|
||||
ui.load.runLoadSave(slot);
|
||||
}catch(IOException e){
|
||||
ui.showException("$save.import.fail", e);
|
||||
}
|
||||
}else{
|
||||
ui.showErrorMessage("$save.import.invalid");
|
||||
}
|
||||
}else if(map){ //open map
|
||||
Fi file = Core.files.local("temp-map." + mapExtension);
|
||||
file.write(inStream, false);
|
||||
Core.app.post(() -> {
|
||||
System.out.println("Opening map.");
|
||||
if(!ui.editor.isShown()){
|
||||
ui.editor.show();
|
||||
}
|
||||
ui.editor.beginEditMap(file);
|
||||
});
|
||||
}
|
||||
}));
|
||||
}
|
||||
}catch(IOException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isTablet(Context context){
|
||||
TelephonyManager manager = (TelephonyManager)context.getSystemService(Context.TELEPHONY_SERVICE);
|
||||
return manager != null && manager.getPhoneType() == TelephonyManager.PHONE_TYPE_NONE;
|
||||
}
|
||||
}
|
||||
227
android/src/mindustry/android/AndroidRhinoContext.java
Normal file
227
android/src/mindustry/android/AndroidRhinoContext.java
Normal file
@@ -0,0 +1,227 @@
|
||||
package mindustry.android;
|
||||
|
||||
import android.annotation.*;
|
||||
import android.os.*;
|
||||
import android.os.Build.*;
|
||||
import arc.*;
|
||||
import arc.backend.android.*;
|
||||
import com.android.dex.*;
|
||||
import com.android.dx.cf.direct.*;
|
||||
import com.android.dx.command.dexer.*;
|
||||
import com.android.dx.dex.*;
|
||||
import com.android.dx.dex.cf.*;
|
||||
import com.android.dx.dex.file.DexFile;
|
||||
import com.android.dx.merge.*;
|
||||
import dalvik.system.*;
|
||||
import org.mozilla.javascript.*;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.*;
|
||||
|
||||
/**
|
||||
* Helps to prepare a Rhino Context for usage on android.
|
||||
* @author F43nd1r
|
||||
* @since 11.01.2016
|
||||
*/
|
||||
public class AndroidRhinoContext{
|
||||
|
||||
/**
|
||||
* call this instead of {@link Context#enter()}
|
||||
* @return a context prepared for android
|
||||
*/
|
||||
public static Context enter(File cacheDirectory){
|
||||
if(!SecurityController.hasGlobal())
|
||||
SecurityController.initGlobal(new SecurityController(){
|
||||
@Override
|
||||
public GeneratedClassLoader createClassLoader(ClassLoader classLoader, Object o){
|
||||
return Context.getCurrentContext().createClassLoader(classLoader);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getDynamicSecurityDomain(Object o){
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
AndroidContextFactory factory;
|
||||
if(!ContextFactory.hasExplicitGlobal()){
|
||||
factory = new AndroidContextFactory(cacheDirectory);
|
||||
ContextFactory.getGlobalSetter().setContextFactoryGlobal(factory);
|
||||
}else if(!(ContextFactory.getGlobal() instanceof AndroidContextFactory)){
|
||||
throw new IllegalStateException("Cannot initialize factory for Android Rhino: There is already another factory");
|
||||
}else{
|
||||
factory = (AndroidContextFactory)ContextFactory.getGlobal();
|
||||
}
|
||||
|
||||
return factory.enterContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the classLoader used is correct
|
||||
* @author F43nd1r
|
||||
* @since 11.01.2016
|
||||
*/
|
||||
public static class AndroidContextFactory extends ContextFactory{
|
||||
private final File cacheDirectory;
|
||||
|
||||
/**
|
||||
* Create a new factory. It will cache generated code in the given directory
|
||||
* @param cacheDirectory the cache directory
|
||||
*/
|
||||
public AndroidContextFactory(File cacheDirectory){
|
||||
this.cacheDirectory = cacheDirectory;
|
||||
initApplicationClassLoader(createClassLoader(AndroidContextFactory.class.getClassLoader()));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ClassLoader which is able to deal with bytecode
|
||||
* @param parent the parent of the create classloader
|
||||
* @return a new ClassLoader
|
||||
*/
|
||||
@Override
|
||||
public BaseAndroidClassLoader createClassLoader(ClassLoader parent){
|
||||
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
|
||||
return new InMemoryAndroidClassLoader(parent);
|
||||
}
|
||||
return new FileAndroidClassLoader(parent, cacheDirectory);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onContextReleased(final Context cx){
|
||||
super.onContextReleased(cx);
|
||||
((BaseAndroidClassLoader)cx.getApplicationClassLoader()).reset();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiles java bytecode to dex bytecode and loads it
|
||||
* @author F43nd1r
|
||||
* @since 11.01.2016
|
||||
*/
|
||||
abstract static class BaseAndroidClassLoader extends ClassLoader implements GeneratedClassLoader{
|
||||
|
||||
public BaseAndroidClassLoader(ClassLoader parent){
|
||||
super(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> defineClass(String name, byte[] data){
|
||||
try{
|
||||
DexOptions dexOptions = new DexOptions();
|
||||
DexFile dexFile = new DexFile(dexOptions);
|
||||
DirectClassFile classFile = new DirectClassFile(data, name.replace('.', '/') + ".class", true);
|
||||
classFile.setAttributeFactory(StdAttributeFactory.THE_ONE);
|
||||
classFile.getMagic();
|
||||
DxContext context = new DxContext();
|
||||
dexFile.add(CfTranslator.translate(context, classFile, null, new CfOptions(), dexOptions, dexFile));
|
||||
Dex dex = new Dex(dexFile.toDex(null, false));
|
||||
Dex oldDex = getLastDex();
|
||||
if(oldDex != null){
|
||||
dex = new DexMerger(new Dex[]{dex, oldDex}, CollisionPolicy.KEEP_FIRST, context).merge();
|
||||
}
|
||||
return loadClass(dex, name);
|
||||
}catch(IOException | ClassNotFoundException e){
|
||||
throw new FatalLoadingException(e);
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract Class<?> loadClass(Dex dex, String name) throws ClassNotFoundException;
|
||||
|
||||
protected abstract Dex getLastDex();
|
||||
|
||||
protected abstract void reset();
|
||||
|
||||
@Override
|
||||
public void linkClass(Class<?> aClass){}
|
||||
|
||||
@Override
|
||||
public Class<?> loadClass(String name, boolean resolve)
|
||||
throws ClassNotFoundException{
|
||||
Class<?> loadedClass = findLoadedClass(name);
|
||||
if(loadedClass == null){
|
||||
Dex dex = getLastDex();
|
||||
if(dex != null){
|
||||
loadedClass = loadClass(dex, name);
|
||||
}
|
||||
if(loadedClass == null){
|
||||
loadedClass = getParent().loadClass(name);
|
||||
}
|
||||
}
|
||||
return loadedClass;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** Might be thrown in any Rhino method that loads bytecode if the loading failed. */
|
||||
public static class FatalLoadingException extends RuntimeException{
|
||||
FatalLoadingException(Throwable t){
|
||||
super("Failed to define class", t);
|
||||
}
|
||||
}
|
||||
|
||||
static class FileAndroidClassLoader extends BaseAndroidClassLoader{
|
||||
private static int instanceCounter = 0;
|
||||
private final File dexFile;
|
||||
|
||||
public FileAndroidClassLoader(ClassLoader parent, File cacheDir){
|
||||
super(parent);
|
||||
int id = instanceCounter++;
|
||||
dexFile = new File(cacheDir, id + ".dex");
|
||||
cacheDir.mkdirs();
|
||||
reset();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(Dex dex, String name) throws ClassNotFoundException{
|
||||
try{
|
||||
dex.writeTo(dexFile);
|
||||
}catch(IOException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
android.content.Context context = ((AndroidApplication) Core.app).getContext();
|
||||
return new DexClassLoader(dexFile.getPath(), VERSION.SDK_INT >= 21 ? context.getCodeCacheDir().getPath() : context.getCacheDir().getAbsolutePath(), null, getParent()).loadClass(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dex getLastDex(){
|
||||
if(dexFile.exists()){
|
||||
try{
|
||||
return new Dex(dexFile);
|
||||
}catch(IOException e){
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reset(){
|
||||
dexFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
static class InMemoryAndroidClassLoader extends BaseAndroidClassLoader{
|
||||
private Dex last;
|
||||
|
||||
public InMemoryAndroidClassLoader(ClassLoader parent){
|
||||
super(parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Class<?> loadClass(Dex dex, String name) throws ClassNotFoundException{
|
||||
last = dex;
|
||||
return new InMemoryDexClassLoader(ByteBuffer.wrap(dex.getBytes()), getParent()).loadClass(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Dex getLastDex(){
|
||||
return last;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reset(){
|
||||
last = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user