Move :stub to :app:stub

This commit is contained in:
topjohnwu
2024-07-04 00:21:34 -07:00
parent 3e38b8fed1
commit b168163ef0
68 changed files with 15 additions and 9 deletions
@@ -0,0 +1,137 @@
package com.topjohnwu.magisk;
import android.app.job.JobService;
import android.content.pm.ActivityInfo;
import android.content.pm.PackageInfo;
import android.content.pm.ServiceInfo;
import com.topjohnwu.magisk.dummy.DummyProvider;
import com.topjohnwu.magisk.dummy.DummyReceiver;
import com.topjohnwu.magisk.dummy.DummyService;
import com.topjohnwu.magisk.utils.DynamicClassLoader;
import java.io.File;
import java.util.HashMap;
import java.util.Map;
// Wrap the actual classloader as we only want to resolve classname
// mapping when loading from platform (via LoadedApk.mClassLoader)
class AppClassLoader extends ClassLoader {
final Map<String, String> mapping = new HashMap<>();
AppClassLoader(File apk) {
super(new DynamicClassLoader(apk));
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
String clz = mapping.get(name);
name = clz != null ? clz : name;
return super.loadClass(name, resolve);
}
void updateComponentMap(PackageInfo stub, PackageInfo app) {
{
var src = stub.activities;
var dest = app.activities;
final ActivityInfo sa;
final ActivityInfo da;
final ActivityInfo sb;
final ActivityInfo db;
if (src[0].exported) {
sa = src[0];
sb = src[1];
} else {
sa = src[1];
sb = src[0];
}
if (dest[0].exported) {
da = dest[0];
db = dest[1];
} else {
da = dest[1];
db = dest[0];
}
mapping.put(sa.name, da.name);
mapping.put(sb.name, db.name);
}
{
var src = stub.services;
var dest = app.services;
final ServiceInfo sa;
final ServiceInfo da;
final ServiceInfo sb;
final ServiceInfo db;
if (JobService.PERMISSION_BIND.equals(src[0].permission)) {
sa = src[0];
sb = src[1];
} else {
sa = src[1];
sb = src[0];
}
if (JobService.PERMISSION_BIND.equals(dest[0].permission)) {
da = dest[0];
db = dest[1];
} else {
da = dest[1];
db = dest[0];
}
mapping.put(sa.name, da.name);
mapping.put(sb.name, db.name);
}
{
var src = stub.receivers;
var dest = app.receivers;
mapping.put(src[0].name, dest[0].name);
}
{
var src = stub.providers;
var dest = app.providers;
mapping.put(src[0].name, dest[0].name);
}
}
}
class StubClassLoader extends ClassLoader {
private final Map<String, Class<?>> mapping = new HashMap<>();
StubClassLoader(PackageInfo info) {
super(StubClassLoader.class.getClassLoader());
for (var c : info.activities) {
mapping.put(c.name, DownloadActivity.class);
}
for (var c : info.services) {
mapping.put(c.name, DummyService.class);
}
for (var c : info.providers) {
mapping.put(c.name, DummyProvider.class);
}
for (var c : info.receivers) {
mapping.put(c.name, DummyReceiver.class);
}
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> clz = mapping.get(name);
return clz == null ? super.loadClass(name, resolve) : clz;
}
}
class DelegateClassLoader extends ClassLoader {
DelegateClassLoader() {
super();
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
return DynLoad.activeClassLoader.loadClass(name);
}
}
@@ -0,0 +1,30 @@
package com.topjohnwu.magisk;
import android.app.Application;
import android.content.Context;
import android.content.res.Configuration;
public class DelegateApplication extends Application {
private Application receiver;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
receiver = DynLoad.createAndSetupApp(this);
}
@Override
public void onCreate() {
super.onCreate();
if (receiver != null)
receiver.onCreate();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (receiver != null)
receiver.onConfigurationChanged(newConfig);
}
}
@@ -0,0 +1,78 @@
package com.topjohnwu.magisk;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AppComponentFactory;
import android.app.Application;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.ContentProvider;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import com.topjohnwu.magisk.dummy.DummyProvider;
import com.topjohnwu.magisk.dummy.DummyReceiver;
import com.topjohnwu.magisk.dummy.DummyService;
@SuppressLint("NewApi")
public class DelegateComponentFactory extends AppComponentFactory {
AppComponentFactory receiver;
public DelegateComponentFactory() {
DynLoad.componentFactory = this;
}
@Override
public ClassLoader instantiateClassLoader(ClassLoader cl, ApplicationInfo info) {
return new DelegateClassLoader();
}
@Override
public Application instantiateApplication(ClassLoader cl, String className) {
return new DelegateApplication();
}
@Override
public Activity instantiateActivity(ClassLoader cl, String className, Intent intent)
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null)
return receiver.instantiateActivity(DynLoad.activeClassLoader, className, intent);
return create(className, DownloadActivity.class);
}
@Override
public BroadcastReceiver instantiateReceiver(ClassLoader cl, String className, Intent intent)
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null)
return receiver.instantiateReceiver(DynLoad.activeClassLoader, className, intent);
return create(className, DummyReceiver.class);
}
@Override
public Service instantiateService(ClassLoader cl, String className, Intent intent)
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null)
return receiver.instantiateService(DynLoad.activeClassLoader, className, intent);
return create(className, DummyService.class);
}
@Override
public ContentProvider instantiateProvider(ClassLoader cl, String className)
throws ClassNotFoundException, IllegalAccessException, InstantiationException {
if (receiver != null)
return receiver.instantiateProvider(DynLoad.activeClassLoader, className);
return create(className, DummyProvider.class);
}
private <T> T create(String name, Class<T> fallback)
throws IllegalAccessException, InstantiationException {
try {
// noinspection unchecked
return (T) DynLoad.activeClassLoader.loadClass(name).newInstance();
} catch (ClassNotFoundException e) {
return fallback.newInstance();
}
}
}
@@ -0,0 +1,43 @@
package com.topjohnwu.magisk;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.util.Log;
import java.io.File;
import java.lang.reflect.Constructor;
public class DelegateRootService extends ContextWrapper {
public DelegateRootService() {
super(null);
}
@Override
protected void attachBaseContext(Context base) {
ClassLoader loader = DynLoad.loadApk(base);
if (loader == null)
return;
try {
// Create application to get the real root service class
var data = DynLoad.createApkData();
File apk = StubApk.current(base);
PackageManager pm = base.getPackageManager();
PackageInfo pkgInfo = pm.getPackageArchiveInfo(apk.getPath(), 0);
loader.loadClass(pkgInfo.applicationInfo.className)
.getConstructor(Object.class)
.newInstance(data.getObject());
// Create the actual RootService and call its attachBaseContext
Constructor<?> ctor = data.getRootService().getConstructor(Object.class);
ctor.setAccessible(true);
Object service = ctor.newInstance(this);
DynLoad.attachContext(service, base);
} catch (Exception e) {
Log.e(DelegateRootService.class.getSimpleName(), "", e);
}
}
}
@@ -0,0 +1,201 @@
package com.topjohnwu.magisk;
import static android.R.string.no;
import static android.R.string.ok;
import static android.R.string.yes;
import static com.topjohnwu.magisk.R.string.dling;
import static com.topjohnwu.magisk.R.string.no_internet_msg;
import static com.topjohnwu.magisk.R.string.upgrade_msg;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.Context;
import android.content.Intent;
import android.content.res.loader.ResourcesLoader;
import android.content.res.loader.ResourcesProvider;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.system.Os;
import android.system.OsConstants;
import android.util.Log;
import android.view.ContextThemeWrapper;
import com.topjohnwu.magisk.net.Networking;
import com.topjohnwu.magisk.net.Request;
import com.topjohnwu.magisk.utils.APKInstall;
import org.json.JSONException;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.zip.InflaterInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class DownloadActivity extends Activity {
private static final String APP_NAME = "Magisk";
private static final String JSON_URL = BuildConfig.DEBUG ?
"https://topjohnwu.github.io/magisk-files/debug.json" :
"https://topjohnwu.github.io/magisk-files/canary.json";
private String apkLink = BuildConfig.APK_URL;
private Context themed;
private ProgressDialog dialog;
private boolean dynLoad;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
if (DynLoad.activeClassLoader instanceof AppClassLoader) {
// For some reason activity is created before Application.attach(),
// relaunch the activity using the same intent
finishAffinity();
startActivity(getIntent());
return;
}
themed = new ContextThemeWrapper(this, android.R.style.Theme_DeviceDefault);
// Only download and dynamic load full APK if hidden
dynLoad = !getPackageName().equals(BuildConfig.APPLICATION_ID);
// Inject resources
try {
loadResources();
} catch (Exception e) {
error(e);
}
ProviderInstaller.install(this);
if (Networking.checkNetworkStatus(this)) {
if (BuildConfig.APK_URL == null) {
fetchCanary();
} else {
showDialog();
}
} else {
new AlertDialog.Builder(themed)
.setCancelable(false)
.setTitle(APP_NAME)
.setMessage(getString(no_internet_msg))
.setNegativeButton(ok, (d, w) -> finish())
.show();
}
}
@Override
public void finish() {
super.finish();
Runtime.getRuntime().exit(0);
}
private void error(Throwable e) {
Log.e(getClass().getSimpleName(), Log.getStackTraceString(e));
finish();
}
private Request request(String url) {
return Networking.get(url).setErrorHandler((conn, e) -> error(e));
}
private void showDialog() {
new AlertDialog.Builder(themed)
.setCancelable(false)
.setTitle(APP_NAME)
.setMessage(getString(upgrade_msg))
.setPositiveButton(yes, (d, w) -> dlAPK())
.setNegativeButton(no, (d, w) -> finish())
.show();
}
private void fetchCanary() {
dialog = ProgressDialog.show(themed, "", "", true);
request(JSON_URL).getAsJSONObject(json -> {
dialog.dismiss();
try {
apkLink = json.getJSONObject("magisk").getString("link");
showDialog();
} catch (JSONException e) {
error(e);
}
});
}
private void dlAPK() {
dialog = ProgressDialog.show(themed, getString(dling), getString(dling) + " " + APP_NAME, true);
// Download and upgrade the app
var request = request(apkLink).setExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
if (dynLoad) {
request.getAsFile(StubApk.current(this), file -> StubApk.restartProcess(this));
} else {
request.getAsInputStream(input -> {
var session = APKInstall.startSession(this);
try (input; var out = session.openStream(this)) {
if (out != null)
APKInstall.transfer(input, out);
} catch (IOException e) {
error(e);
}
Intent intent = session.waitIntent();
if (intent != null)
startActivity(intent);
});
}
}
private void decryptResources(OutputStream out) throws Exception {
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
SecretKey key = new SecretKeySpec(Bytes.key(), "AES");
IvParameterSpec iv = new IvParameterSpec(Bytes.iv());
cipher.init(Cipher.DECRYPT_MODE, key, iv);
var is = new InflaterInputStream(new CipherInputStream(
new ByteArrayInputStream(Bytes.res()), cipher));
try (is; out) {
APKInstall.transfer(is, out);
}
}
private void loadResources() throws Exception {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
var fd = Os.memfd_create("res", 0);
try {
decryptResources(new FileOutputStream(fd));
Os.lseek(fd, 0, OsConstants.SEEK_SET);
var loader = new ResourcesLoader();
try (var pfd = ParcelFileDescriptor.dup(fd)) {
loader.addProvider(ResourcesProvider.loadFromTable(pfd, null));
getResources().addLoaders(loader);
}
} finally {
Os.close(fd);
}
} else {
File res = new File(getCodeCacheDir(), "res.apk");
try (var out = new ZipOutputStream(new FileOutputStream(res))) {
// AndroidManifest.xml is reuqired on Android 6-, and directory support is broken on Android 9-10
out.putNextEntry(new ZipEntry("AndroidManifest.xml"));
try (var stubApk = new ZipFile(getPackageCodePath())) {
APKInstall.transfer(stubApk.getInputStream(stubApk.getEntry("AndroidManifest.xml")), out);
}
out.putNextEntry(new ZipEntry("resources.arsc"));
decryptResources(out);
}
StubApk.addAssetPath(getResources(), res.getPath());
}
}
}
@@ -0,0 +1,201 @@
package com.topjohnwu.magisk;
import static com.topjohnwu.magisk.BuildConfig.APPLICATION_ID;
import android.app.AppComponentFactory;
import android.app.Application;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import android.util.Log;
import com.topjohnwu.magisk.utils.APKInstall;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.HashMap;
@SuppressWarnings("ResultOfMethodCallIgnored")
public class DynLoad {
static Object componentFactory;
static ClassLoader activeClassLoader = DynLoad.class.getClassLoader();
static StubApk.Data createApkData() {
var data = new StubApk.Data();
data.setVersion(BuildConfig.STUB_VERSION);
data.setClassToComponent(new HashMap<>());
data.setRootService(DelegateRootService.class);
return data;
}
static void attachContext(Object o, Context context) {
if (!(o instanceof ContextWrapper))
return;
try {
Method m = ContextWrapper.class.getDeclaredMethod("attachBaseContext", Context.class);
m.setAccessible(true);
m.invoke(o, context);
} catch (Exception ignored) { /* Impossible */ }
}
// Dynamically load APK from internal, external storage, or previous app
static AppClassLoader loadApk(Context context) {
File apk = StubApk.current(context);
File update = StubApk.update(context);
if (update.exists()) {
// Rename from update
update.renameTo(apk);
}
// Copy from external for easier development
if (BuildConfig.DEBUG) {
try {
File external = new File(context.getExternalFilesDir(null), "magisk.apk");
if (external.exists()) {
apk.delete();
try {
var in = new FileInputStream(external);
var out = new FileOutputStream(apk);
apk.setReadOnly();
try (in; out) {
APKInstall.transfer(in, out);
}
} catch (IOException e) {
Log.e(DynLoad.class.getSimpleName(), "", e);
apk.delete();
} finally {
external.delete();
}
}
} catch (SecurityException e) {
// Do not crash in root service
}
}
if (apk.exists()) {
apk.setReadOnly();
return new AppClassLoader(apk);
}
// If no APK is loaded, attempt to copy from previous app
if (!context.getPackageName().equals(APPLICATION_ID)) {
try {
var info = context.getPackageManager().getApplicationInfo(APPLICATION_ID, 0);
apk.delete();
var src = new FileInputStream(info.sourceDir);
var out = new FileOutputStream(apk);
apk.setReadOnly();
try (src; out) {
APKInstall.transfer(src, out);
}
return new AppClassLoader(apk);
} catch (PackageManager.NameNotFoundException ignored) {
} catch (IOException e) {
Log.e(DynLoad.class.getSimpleName(), "", e);
apk.delete();
}
}
return null;
}
// Dynamically load APK and create the Application instance from the loaded APK
static Application createAndSetupApp(Application context) {
// On API >= 29, AppComponentFactory will replace the ClassLoader for us
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
replaceClassLoader(context);
// noinspection InlinedApi
int flags = PackageManager.GET_ACTIVITIES | PackageManager.GET_SERVICES
| PackageManager.GET_PROVIDERS | PackageManager.GET_RECEIVERS
| PackageManager.MATCH_DIRECT_BOOT_AWARE | PackageManager.MATCH_DISABLED_COMPONENTS
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
var pm = context.getPackageManager();
final PackageInfo info;
try {
// noinspection WrongConstant
info = pm.getPackageInfo(context.getPackageName(), flags);
} catch (PackageManager.NameNotFoundException e) {
// Impossible
throw new RuntimeException(e);
}
File apk = StubApk.current(context);
final var cl = loadApk(context);
if (cl != null) try {
// noinspection WrongConstant
var pkgInfo = pm.getPackageArchiveInfo(apk.getPath(), flags);
cl.updateComponentMap(info, pkgInfo);
var appInfo = pkgInfo.applicationInfo;
var data = createApkData();
var map = data.getClassToComponent();
// Create the inverse mapping (class to component name)
for (var e : cl.mapping.entrySet()) {
map.put(e.getValue(), e.getKey());
}
// Create the receiver Application
var app = (Application) cl.loadClass(appInfo.className)
.getConstructor(Object.class)
.newInstance(data.getObject());
// Create the receiver component factory
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && componentFactory != null) {
var delegate = (DelegateComponentFactory) componentFactory;
if (appInfo.appComponentFactory == null) {
delegate.receiver = new AppComponentFactory();
} else {
Object factory = cl.loadClass(appInfo.appComponentFactory).newInstance();
delegate.receiver = (AppComponentFactory) factory;
}
}
activeClassLoader = cl;
// Send real application to attachBaseContext
attachContext(app, context);
return app;
} catch (Exception e) {
Log.e(DynLoad.class.getSimpleName(), "", e);
apk.delete();
}
activeClassLoader = new StubClassLoader(info);
return null;
}
// Replace LoadedApk mClassLoader
private static void replaceClassLoader(Context context) {
// Get ContextImpl
while (context instanceof ContextWrapper) {
context = ((ContextWrapper) context).getBaseContext();
}
try {
Field mInfo = context.getClass().getDeclaredField("mPackageInfo");
mInfo.setAccessible(true);
Object loadedApk = mInfo.get(context);
assert loadedApk != null;
Field mcl = loadedApk.getClass().getDeclaredField("mClassLoader");
mcl.setAccessible(true);
mcl.set(loadedApk, new DelegateClassLoader());
} catch (Exception e) {
// Actually impossible as this method is only called on API < 29,
// and API 23 - 28 do not restrict access to these fields.
Log.e(DynLoad.class.getSimpleName(), "", e);
}
}
}
@@ -0,0 +1,38 @@
package com.topjohnwu.magisk.dummy;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
public class DummyProvider extends ContentProvider {
@Override
public boolean onCreate() {
return false;
}
@Override
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
return null;
}
@Override
public String getType(Uri uri) {
return null;
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
}
@@ -0,0 +1,10 @@
package com.topjohnwu.magisk.dummy;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
public class DummyReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {}
}
@@ -0,0 +1,12 @@
package com.topjohnwu.magisk.dummy;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
public class DummyService extends Service {
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
@@ -0,0 +1,50 @@
package com.topjohnwu.magisk.net;
import org.json.JSONArray;
import org.json.JSONObject;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
class BadRequest extends Request {
private final IOException ex;
BadRequest(IOException e) { super(null); ex = e; }
@Override
public Request addHeaders(String key, String value) { return this; }
@Override
public Result<InputStream> execForInputStream() { fail(); return new Result<>(); }
@Override
public void getAsFile(File out, ResponseListener<File> rs) { fail(); }
@Override
public void execForFile(File out) { fail(); }
@Override
public void getAsString(ResponseListener<String> rs) { fail(); }
@Override
public Result<String> execForString() { fail(); return new Result<>(); }
@Override
public void getAsJSONObject(ResponseListener<JSONObject> rs) { fail(); }
@Override
public Result<JSONObject> execForJSONObject() { fail(); return new Result<>(); }
@Override
public void getAsJSONArray(ResponseListener<JSONArray> rs) { fail(); }
@Override
public Result<JSONArray> execForJSONArray() { fail(); return new Result<>(); }
private void fail() {
if (err != null)
err.onError(null, ex);
}
}
@@ -0,0 +1,7 @@
package com.topjohnwu.magisk.net;
import java.net.HttpURLConnection;
public interface ErrorHandler {
void onError(HttpURLConnection conn, Exception e);
}
@@ -0,0 +1,41 @@
package com.topjohnwu.magisk.net;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Handler;
import android.os.Looper;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
public class Networking {
private static final int READ_TIMEOUT = 15000;
private static final int CONNECT_TIMEOUT = 15000;
static Handler mainHandler = new Handler(Looper.getMainLooper());
private static Request request(String url, String method) {
try {
HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
conn.setRequestMethod(method);
conn.setReadTimeout(READ_TIMEOUT);
conn.setConnectTimeout(CONNECT_TIMEOUT);
return new Request(conn);
} catch (IOException e) {
return new BadRequest(e);
}
}
public static Request get(String url) {
return request(url, "GET");
}
public static boolean checkNetworkStatus(Context context) {
ConnectivityManager manager = (ConnectivityManager)
context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo networkInfo = manager.getActiveNetworkInfo();
return networkInfo != null && networkInfo.isConnected();
}
}
@@ -0,0 +1,209 @@
package com.topjohnwu.magisk.net;
import android.os.AsyncTask;
import com.topjohnwu.magisk.utils.APKInstall;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.HttpURLConnection;
import java.util.Scanner;
import java.util.concurrent.Executor;
public class Request {
private final HttpURLConnection conn;
private Executor executor = null;
private int code = -1;
ErrorHandler err = null;
private interface Requestor<T> {
T request() throws Exception;
}
public class Result<T> {
T result;
public T getResult() {
return result;
}
public int getCode() {
return code;
}
public boolean isSuccess() {
return code >= 200 && code <= 299;
}
public HttpURLConnection getConnection() {
return conn;
}
}
Request(HttpURLConnection c) {
conn = c;
}
public Request addHeaders(String key, String value) {
conn.setRequestProperty(key, value);
return this;
}
public Request setErrorHandler(ErrorHandler handler) {
err = handler;
return this;
}
public Request setExecutor(Executor e) {
executor = e;
return this;
}
public Result<Void> connect() {
try {
connect0();
} catch (IOException e) {
if (err != null)
err.onError(conn, e);
}
return new Result<>();
}
public Result<InputStream> execForInputStream() {
return exec(this::getInputStream);
}
public void getAsInputStream(ResponseListener<InputStream> rs) {
submit(this::getInputStream, rs);
}
public void getAsFile(File out, ResponseListener<File> rs) {
submit(() -> dlFile(out), rs);
}
public void execForFile(File out) {
exec(() -> dlFile(out));
}
public void getAsBytes(ResponseListener<byte[]> rs) {
submit(this::dlBytes, rs);
}
public Result<byte[]> execForBytes() {
return exec(this::dlBytes);
}
public void getAsString(ResponseListener<String> rs) {
submit(this::dlString, rs);
}
public Result<String> execForString() {
return exec(this::dlString);
}
public void getAsJSONObject(ResponseListener<JSONObject> rs) {
submit(this::dlJSONObject, rs);
}
public Result<JSONObject> execForJSONObject() {
return exec(this::dlJSONObject);
}
public void getAsJSONArray(ResponseListener<JSONArray> rs) {
submit(this::dlJSONArray, rs);
}
public Result<JSONArray> execForJSONArray() {
return exec(this::dlJSONArray);
}
private void connect0() throws IOException {
conn.connect();
code = conn.getResponseCode();
}
private <T> Result<T> exec(Requestor<T> req) {
Result<T> res = new Result<>();
try {
res.result = req.request();
} catch (Exception e) {
if (err != null)
err.onError(conn, e);
}
return res;
}
private <T> void submit(Requestor<T> req, ResponseListener<T> rs) {
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
try {
T t = req.request();
Runnable cb = () -> rs.onResponse(t);
if (executor == null)
Networking.mainHandler.post(cb);
else
executor.execute(cb);
} catch (Exception e) {
if (err != null)
err.onError(conn, e);
}
});
}
private BufferedInputStream getInputStream() throws IOException {
connect0();
InputStream in = new FilterInputStream(conn.getInputStream()) {
@Override
public void close() throws IOException {
super.close();
conn.disconnect();
}
};
return new BufferedInputStream(in);
}
private String dlString() throws IOException {
try (Scanner s = new Scanner(getInputStream(), "UTF-8")) {
s.useDelimiter("\\A");
return s.next();
}
}
private JSONObject dlJSONObject() throws IOException, JSONException {
return new JSONObject(dlString());
}
private JSONArray dlJSONArray() throws IOException, JSONException {
return new JSONArray(dlString());
}
private File dlFile(File f) throws IOException {
try (InputStream in = getInputStream();
OutputStream out = new BufferedOutputStream(new FileOutputStream(f))) {
APKInstall.transfer(in, out);
}
return f;
}
private byte[] dlBytes() throws IOException {
int len = conn.getContentLength();
len = len > 0 ? len : 32;
ByteArrayOutputStream out = new ByteArrayOutputStream(len);
try (InputStream in = getInputStream()) {
APKInstall.transfer(in, out);
}
return out.toByteArray();
}
}
@@ -0,0 +1,5 @@
package com.topjohnwu.magisk.net;
public interface ResponseListener<T> {
void onResponse(T response);
}