From c193b8f2f8cb7c60dbad94af653194f7ac5676cb Mon Sep 17 00:00:00 2001
From: akwizgran <michael@briarproject.org>
Date: Tue, 28 Mar 2023 15:25:18 +0100
Subject: [PATCH] Add missing Android wrapper.

---
 .../onionwrapper/AndroidTorWrapper.java       | 253 ++++++++++++++++++
 1 file changed, 253 insertions(+)
 create mode 100644 onionwrapper-android/src/main/java/org/briarproject/onionwrapper/AndroidTorWrapper.java

diff --git a/onionwrapper-android/src/main/java/org/briarproject/onionwrapper/AndroidTorWrapper.java b/onionwrapper-android/src/main/java/org/briarproject/onionwrapper/AndroidTorWrapper.java
new file mode 100644
index 0000000..137d9ea
--- /dev/null
+++ b/onionwrapper-android/src/main/java/org/briarproject/onionwrapper/AndroidTorWrapper.java
@@ -0,0 +1,253 @@
+package org.briarproject.onionwrapper;
+
+import android.app.Application;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.os.Build;
+
+import org.briarproject.android.dontkillmelib.wakelock.AndroidWakeLock;
+import org.briarproject.android.dontkillmelib.wakelock.AndroidWakeLockManager;
+import org.briarproject.nullsafety.NotNullByDefault;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Executor;
+import java.util.logging.Logger;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import static android.os.Build.VERSION.SDK_INT;
+import static java.util.Arrays.asList;
+import static java.util.logging.Level.INFO;
+import static java.util.logging.Logger.getLogger;
+
+/**
+ * A Tor wrapper for the Android operating system.
+ */
+@NotNullByDefault
+public class AndroidTorWrapper extends AbstractTorWrapper {
+
+	private static final List<String> LIBRARY_ARCHITECTURES =
+			asList("armeabi-v7a", "arm64-v8a", "x86", "x86_64");
+
+	private static final String TOR_LIB_NAME = "libtor.so";
+	private static final String OBFS4_LIB_NAME = "libobfs4proxy.so";
+	private static final String SNOWFLAKE_LIB_NAME = "libsnowflake.so";
+
+	private static final Logger LOG =
+			getLogger(AndroidTorWrapper.class.getName());
+
+	private final Application app;
+	private final AndroidWakeLock wakeLock;
+	private final File torLib, obfs4Lib, snowflakeLib;
+
+	/**
+	 * @param app The application instance.
+	 * @param wakeLockManager The interface for managing a shared wake lock.
+	 * @param ioExecutor The wrapper will use this executor to run IO tasks,
+	 * 		some of which may run for the lifetime of the wrapper, so the executor
+	 * 		should have an unlimited thread pool.
+	 * @param eventExecutor The wrapper will use this executor to call the
+	 *        {@link Observer observer} (if any). To ensure that events are observed
+	 * 		in the order they occur, this executor should have a single thread (eg
+	 * 		the app's main thread).
+	 * @param architecture The processor architecture of the Tor and pluggable
+	 * 		transport binaries.
+	 * @param torDirectory The directory where the Tor process should keep its
+	 * 		state.
+	 * @param torSocksPort The port number to use for Tor's SOCKS port.
+	 * @param torControlPort The port number to use for Tor's control port.
+	 */
+	public AndroidTorWrapper(Application app,
+			AndroidWakeLockManager wakeLockManager,
+			Executor ioExecutor,
+			Executor eventExecutor,
+			String architecture,
+			File torDirectory,
+			int torSocksPort,
+			int torControlPort) {
+		super(ioExecutor, eventExecutor, architecture, torDirectory,
+				torSocksPort, torControlPort);
+		this.app = app;
+		wakeLock = wakeLockManager.createWakeLock("TorPlugin");
+		String nativeLibDir = app.getApplicationInfo().nativeLibraryDir;
+		torLib = new File(nativeLibDir, TOR_LIB_NAME);
+		obfs4Lib = new File(nativeLibDir, OBFS4_LIB_NAME);
+		snowflakeLib = new File(nativeLibDir, SNOWFLAKE_LIB_NAME);
+	}
+
+	@Override
+	protected int getProcessId() {
+		return android.os.Process.myPid();
+	}
+
+	@Override
+	protected long getLastUpdateTime() {
+		try {
+			PackageManager pm = app.getPackageManager();
+			PackageInfo pi = pm.getPackageInfo(app.getPackageName(), 0);
+			return pi.lastUpdateTime;
+		} catch (NameNotFoundException e) {
+			throw new AssertionError(e);
+		}
+	}
+
+	@Override
+	public InputStream getResourceInputStream(String name, String extension) {
+		Resources res = app.getResources();
+		// Extension is ignored on Android, resources are retrieved without it
+		int resId = res.getIdentifier(name, "raw", app.getPackageName());
+		return res.openRawResource(resId);
+	}
+
+	@Override
+	public void enableNetwork(boolean enable) throws IOException {
+		if (enable) wakeLock.acquire();
+		try {
+			super.enableNetwork(enable);
+		} finally {
+			if (!enable) wakeLock.release();
+		}
+	}
+
+	@Override
+	public void stop() throws IOException {
+		try {
+			super.stop();
+		} finally {
+			wakeLock.release();
+		}
+	}
+
+	@Override
+	protected File getTorExecutableFile() {
+		return torLib.exists() ? torLib : super.getTorExecutableFile();
+	}
+
+	@Override
+	protected File getObfs4ExecutableFile() {
+		return obfs4Lib.exists() ? obfs4Lib : super.getObfs4ExecutableFile();
+	}
+
+	@Override
+	protected File getSnowflakeExecutableFile() {
+		return snowflakeLib.exists() ? snowflakeLib
+				: super.getSnowflakeExecutableFile();
+	}
+
+	@Override
+	protected void installTorExecutable() throws IOException {
+		installExecutable(super.getTorExecutableFile(), torLib, TOR_LIB_NAME);
+	}
+
+	@Override
+	protected void installObfs4Executable() throws IOException {
+		installExecutable(super.getObfs4ExecutableFile(), obfs4Lib,
+				OBFS4_LIB_NAME);
+	}
+
+	@Override
+	protected void installSnowflakeExecutable() throws IOException {
+		installExecutable(super.getSnowflakeExecutableFile(), snowflakeLib,
+				SNOWFLAKE_LIB_NAME);
+	}
+
+	private void installExecutable(File extracted, File lib, String libName)
+			throws IOException {
+		if (lib.exists()) {
+			// If an older version left behind a binary, delete it
+			if (extracted.exists()) {
+				if (extracted.delete()) LOG.info("Deleted old binary");
+				else LOG.info("Failed to delete old binary");
+			}
+		} else if (SDK_INT < 29) {
+			// The binary wasn't extracted at install time. Try to extract it
+			extractLibraryFromApk(libName, extracted);
+		} else {
+			// No point extracting the binary, we won't be allowed to execute it
+			throw new FileNotFoundException(lib.getAbsolutePath());
+		}
+	}
+
+	private void extractLibraryFromApk(String libName, File dest)
+			throws IOException {
+		File sourceDir = new File(app.getApplicationInfo().sourceDir);
+		if (sourceDir.isFile()) {
+			// Look for other APK files in the same directory, if we're allowed
+			File parent = sourceDir.getParentFile();
+			if (parent != null) sourceDir = parent;
+		}
+		List<String> libPaths = getSupportedLibraryPaths(libName);
+		for (File apk : findApkFiles(sourceDir)) {
+			@SuppressWarnings("IOStreamConstructor")
+			ZipInputStream zin = new ZipInputStream(new FileInputStream(apk));
+			for (ZipEntry e = zin.getNextEntry(); e != null;
+			     e = zin.getNextEntry()) {
+				if (libPaths.contains(e.getName())) {
+					if (LOG.isLoggable(INFO)) {
+						LOG.info("Extracting " + e.getName()
+								+ " from " + apk.getAbsolutePath());
+					}
+					extract(zin, dest); // Zip input stream will be closed
+					return;
+				}
+			}
+			zin.close();
+		}
+		throw new FileNotFoundException(libName);
+	}
+
+	/**
+	 * Returns all files with the extension .apk or .APK under the given root.
+	 */
+	private List<File> findApkFiles(File root) {
+		List<File> files = new ArrayList<>();
+		findApkFiles(root, files);
+		return files;
+	}
+
+	private void findApkFiles(File f, List<File> files) {
+		if (f.isFile() && f.getName().toLowerCase().endsWith(".apk")) {
+			files.add(f);
+		} else if (f.isDirectory()) {
+			File[] children = f.listFiles();
+			if (children != null) {
+				for (File child : children) findApkFiles(child, files);
+			}
+		}
+	}
+
+	/**
+	 * Returns the paths at which libraries with the given name would be found
+	 * inside an APK file, for all architectures supported by the device, in
+	 * order of preference.
+	 */
+	private List<String> getSupportedLibraryPaths(String libName) {
+		List<String> architectures = new ArrayList<>();
+		for (String abi : getSupportedArchitectures()) {
+			if (LIBRARY_ARCHITECTURES.contains(abi)) {
+				architectures.add("lib/" + abi + "/" + libName);
+			}
+		}
+		return architectures;
+	}
+
+	private Collection<String> getSupportedArchitectures() {
+		List<String> abis = new ArrayList<>();
+		if (SDK_INT >= 21) {
+			abis.addAll(asList(Build.SUPPORTED_ABIS));
+		} else {
+			abis.add(Build.CPU_ABI);
+			if (Build.CPU_ABI2 != null) abis.add(Build.CPU_ABI2);
+		}
+		return abis;
+	}
+}
-- 
GitLab