diff --git a/.github/workflows/debug_build.yml b/.github/workflows/debug_build.yml
index 47ba0edd86..dc215d8626 100644
--- a/.github/workflows/debug_build.yml
+++ b/.github/workflows/debug_build.yml
@@ -1,4 +1,4 @@
-name: Build
+name: APK
on:
push:
@@ -14,6 +14,10 @@ jobs:
steps:
- name: Clone repository
uses: actions/checkout@v2
+ - name: Setup java
+ uses: actions/setup-java@v1
+ with:
+ java-version: 11
- name: Build
run: |
./gradlew assembleDebug
diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml
index 87ccc68611..d2f7674a4a 100644
--- a/.github/workflows/run_tests.yml
+++ b/.github/workflows/run_tests.yml
@@ -14,6 +14,10 @@ jobs:
steps:
- name: Clone repository
uses: actions/checkout@v2
+ - name: Setup java
+ uses: actions/setup-java@v1
+ with:
+ java-version: 11
- name: Execute tests
run: |
./gradlew test
diff --git a/app/build.gradle b/app/build.gradle
index 7ad7cb9a57..d185092de8 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -19,17 +19,6 @@ android {
targetSdkVersion project.properties.targetSdkVersion.toInteger()
versionCode 98
versionName "0.98"
-
- externalNativeBuild {
- ndkBuild {
- cFlags "-std=c11", "-Wall", "-Wextra", "-Werror", "-Os", "-fno-stack-protector", "-Wl,--gc-sections"
- }
- }
-
- ndk {
- abiFilters 'x86', 'x86_64', 'armeabi-v7a', 'arm64-v8a'
- }
-
}
signingConfigs {
@@ -58,12 +47,6 @@ android {
targetCompatibility JavaVersion.VERSION_1_8
}
- externalNativeBuild {
- ndkBuild {
- path "src/main/cpp/Android.mk"
- }
- }
-
testOptions {
unitTests {
includeAndroidResources = true
@@ -82,67 +65,107 @@ task versionName {
}
}
-def downloadBootstrap(String arch, String expectedChecksum, int version) {
+def setupBootstrap(String arch, String expectedChecksum, int version) {
def digest = java.security.MessageDigest.getInstance("SHA-256")
- def localUrl = "src/main/cpp/bootstrap-" + arch + ".zip"
- def file = new File(projectDir, localUrl)
- if (file.exists()) {
+ def zipDownloadFile = new File(project.buildDir, "./gradle/bootstrap-" + arch + "-" + version + ".zip")
+
+ if (zipDownloadFile.exists()) {
def buffer = new byte[8192]
- def input = new FileInputStream(file)
+ def input = new FileInputStream(zipDownloadFile)
while (true) {
def readBytes = input.read(buffer)
if (readBytes < 0) break
digest.update(buffer, 0, readBytes)
}
def checksum = new BigInteger(1, digest.digest()).toString(16)
- if (checksum == expectedChecksum) {
- return
- } else {
- logger.quiet("Deleting old local file with wrong hash: " + localUrl)
- file.delete()
+ if (checksum != expectedChecksum) {
+ logger.quiet("Deleting old local file with wrong hash: " + zipDownloadFile.getAbsolutePath())
+ zipDownloadFile.delete()
}
}
- def remoteUrl = "https://bintray.com/termux/bootstrap/download_file?file_path=bootstrap-" + arch + "-v" + version + ".zip"
- logger.quiet("Downloading " + remoteUrl + " ...")
+ if (!zipDownloadFile.exists()) {
+ def remoteUrl = "https://bintray.com/termux/bootstrap/download_file?file_path=android10-v" + version + "-bootstrap-" + arch + ".zip"
+ logger.quiet("Downloading " + remoteUrl + " ...")
- file.parentFile.mkdirs()
- def out = new BufferedOutputStream(new FileOutputStream(file))
+ zipDownloadFile.parentFile.mkdirs()
+ def out = new BufferedOutputStream(new FileOutputStream(zipDownloadFile))
- def connection = new URL(remoteUrl).openConnection()
- connection.setInstanceFollowRedirects(true)
- def digestStream = new java.security.DigestInputStream(connection.inputStream, digest)
- out << digestStream
- out.close()
+ def connection = new URL(remoteUrl).openConnection()
+ connection.setInstanceFollowRedirects(true)
+ def digestStream = new java.security.DigestInputStream(connection.inputStream, digest)
+ out << digestStream
+ out.close()
- def checksum = new BigInteger(1, digest.digest()).toString(16)
- if (checksum != expectedChecksum) {
- file.delete()
- throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
+ def checksum = new BigInteger(1, digest.digest()).toString(16)
+ if (checksum != expectedChecksum) {
+ zipDownloadFile.delete()
+ throw new GradleException("Wrong checksum for " + remoteUrl + ": expected: " + expectedChecksum + ", actual: " + checksum)
+ }
}
-}
-clean {
- doLast {
- def tree = fileTree(new File(projectDir, 'src/main/cpp'))
- tree.include 'bootstrap-*.zip'
- tree.each { it.delete() }
+ def doneMarkerFile = new File(zipDownloadFile.getAbsolutePath() + "." + expectedChecksum + ".done")
+
+ if (doneMarkerFile.exists()) return
+
+ def archDirName
+ if (arch == "aarch64") archDirName = "arm64-v8a";
+ if (arch == "arm") archDirName = "armeabi-v7a";
+ if (arch == "i686") archDirName = "x86";
+ if (arch == "x86_64") archDirName = "x86_64";
+
+ def outputPath = project.getRootDir().getAbsolutePath() + "/app/src/main/jniLibs/" + archDirName + "/"
+ def outputDir = new File(outputPath).getAbsoluteFile()
+ if (!outputDir.exists()) outputDir.mkdirs()
+
+ def symlinksFile = new File(outputDir, "symlinks.so").getAbsoluteFile()
+ if (symlinksFile.exists()) symlinksFile.delete();
+
+ def mappingsFile = new File(outputDir, "files.so").getAbsoluteFile()
+ if (mappingsFile.exists()) mappingsFile.delete()
+ mappingsFile.createNewFile()
+ def mappingsFileWriter = new BufferedWriter(new FileWriter(mappingsFile))
+
+ def counter = 100
+ new java.util.zip.ZipInputStream(new FileInputStream(zipDownloadFile)).withCloseable { zipInput ->
+ java.util.zip.ZipEntry zipEntry
+ while ((zipEntry = zipInput.getNextEntry()) != null) {
+ if (zipEntry.getName() == "SYMLINKS.txt") {
+ zipInput.transferTo(new FileOutputStream(symlinksFile))
+ } else if (!zipEntry.isDirectory()) {
+ def soName = counter + ".so"
+ def targetFile = new File(outputDir, soName).getAbsoluteFile()
+
+ zipInput.transferTo(new FileOutputStream(targetFile))
+
+ if (zipEntry.getName().endsWith("/pkg")) {
+ def pkgScript = new FileInputStream(project.getRootDir().getAbsolutePath() + "/pkg.sh")
+ pkgScript.transferTo(new FileOutputStream(targetFile))
+ }
+
+ mappingsFileWriter.writeLine(soName + "←" + zipEntry.getName())
+ counter++
+ }
+ }
}
+
+ mappingsFileWriter.close()
+ doneMarkerFile.createNewFile()
}
-task downloadBootstraps(){
+task setupBootstraps(){
doLast {
- def version = 27
- downloadBootstrap("aarch64", "517fb3aa215f7b96961f9377822d7f1b5e86c831efb4ab096ed65d0b1cdf02e9", version)
- downloadBootstrap("arm", "94d17183afdd017cf8ab885b9103a370b16bec1d3cb641884511d545ee009b90", version)
- downloadBootstrap("i686", "7f27723d2f0afbe7e90f203b3ca2e80871a8dfa08b136229476aa5e7ba3e988f", version)
- downloadBootstrap("x86_64", "b19b2721bae5fb3a3fb0754c49611ce4721221e1e7997e7fd98940776ad88c3d", version)
+ def version = 12
+ setupBootstrap("aarch64", "5e07239cad78050f56a28f9f88a0b485cead45864c6c00e1a654c728152b0244", version)
+ setupBootstrap("arm", "fc72279c480c1eea46b6f0fcf78dc57599116c16dcf3b2b970a9ef828f0ec30b", version)
+ setupBootstrap("i686", "895680fc967aecfa4ed77b9dc03aab95d86345be69df48402c63bfc0178337f6", version)
+ setupBootstrap("x86_64", "8714ab8a5ff4e1f5f3ec01e7d0294776bfcffb187c84fa95270ec67ede8f682e", version)
}
}
afterEvaluate {
android.applicationVariants.all { variant ->
- variant.javaCompileProvider.get().dependsOn(downloadBootstraps)
+ variant.javaCompileProvider.get().dependsOn(setupBootstraps)
}
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index b293037649..54404131c2 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -21,6 +21,7 @@
+
-
-extern jbyte blob[];
-extern int blob_size;
-
-JNIEXPORT jbyteArray JNICALL Java_com_termux_app_TermuxInstaller_getZip(JNIEnv *env, __attribute__((__unused__)) jobject This)
-{
- jbyteArray ret = (*env)->NewByteArray(env, blob_size);
- (*env)->SetByteArrayRegion(env, ret, 0, blob_size, blob);
- return ret;
-}
diff --git a/app/src/main/java/com/termux/app/ApkInstaller.java b/app/src/main/java/com/termux/app/ApkInstaller.java
new file mode 100644
index 0000000000..eda091ea5e
--- /dev/null
+++ b/app/src/main/java/com/termux/app/ApkInstaller.java
@@ -0,0 +1,47 @@
+package com.termux.app;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.util.Log;
+
+import com.termux.terminal.EmulatorDebug;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.net.URL;
+
+public class ApkInstaller {
+ static void installPackageApk(String packageName, Context context) {
+ // FIXME: Different file names in case of multiple packages being installed at same time.
+ new Thread() {
+ @Override
+ public void run() {
+ try {
+ String urlString = "https://termux.net/apks/" + packageName + ".apk";
+ Log.e(EmulatorDebug.LOG_TAG, "Installing " + packageName + ", url is " + urlString);
+ File downloadFile = new File(TermuxService.FILES_PATH, "tmp.apk");
+ URL url = new URL(urlString);
+ try (FileOutputStream out = new FileOutputStream(downloadFile)) {
+ try (InputStream in = url.openStream()) {
+ byte[] buffer = new byte[8196];
+ int read;
+ while ((read = in.read(buffer)) >= 0) {
+ out.write(buffer, 0, read);
+ }
+ }
+ }
+
+ Intent installIntent = new Intent(Intent.ACTION_INSTALL_PACKAGE);
+ installIntent.setData(Uri.parse("content://com.termux.files" + downloadFile.getAbsolutePath()));
+ installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ context.startActivity(installIntent);
+ } catch (Exception e) {
+ Log.e("termux", "Error installing " + packageName, e);
+ }
+ }
+ }.start();
+
+ }
+}
diff --git a/app/src/main/java/com/termux/app/BackgroundJob.java b/app/src/main/java/com/termux/app/BackgroundJob.java
index 657cce7df6..fd62c4766c 100644
--- a/app/src/main/java/com/termux/app/BackgroundJob.java
+++ b/app/src/main/java/com/termux/app/BackgroundJob.java
@@ -138,6 +138,7 @@ static String[] buildEnvironment(boolean failSafe, String cwd) {
List environment = new ArrayList<>();
+ environment.add("TERMUX_ANDROID10=1");
environment.add("TERM=xterm-256color");
environment.add("COLORTERM=truecolor");
environment.add("HOME=" + TermuxService.HOME_PATH);
diff --git a/app/src/main/java/com/termux/app/TermuxActivity.java b/app/src/main/java/com/termux/app/TermuxActivity.java
index 129895394b..eb8b9e904d 100644
--- a/app/src/main/java/com/termux/app/TermuxActivity.java
+++ b/app/src/main/java/com/termux/app/TermuxActivity.java
@@ -487,19 +487,17 @@ public View getView(int position, View convertView, @NonNull ViewGroup parent) {
if (mTermService.getSessions().isEmpty()) {
if (mIsVisible) {
- TermuxInstaller.setupIfNeeded(TermuxActivity.this, () -> {
- if (mTermService == null) return; // Activity might have been destroyed.
- try {
- Bundle bundle = getIntent().getExtras();
- boolean launchFailsafe = false;
- if (bundle != null) {
- launchFailsafe = bundle.getBoolean(TERMUX_FAILSAFE_SESSION_ACTION, false);
- }
- addNewSession(launchFailsafe, null);
- } catch (WindowManager.BadTokenException e) {
- // Activity finished - ignore.
+ if (mTermService == null) return; // Activity might have been destroyed.
+ try {
+ Bundle bundle = getIntent().getExtras();
+ boolean launchFailsafe = false;
+ if (bundle != null) {
+ launchFailsafe = bundle.getBoolean(TERMUX_FAILSAFE_SESSION_ACTION, false);
}
- });
+ addNewSession(launchFailsafe, null);
+ } catch (WindowManager.BadTokenException e) {
+ // Activity finished - ignore.
+ }
} else {
// The service connected while not in foreground - just bail out.
finish();
diff --git a/app/src/main/java/com/termux/app/TermuxInstaller.java b/app/src/main/java/com/termux/app/TermuxInstaller.java
index 6e50b22dd6..07593c4274 100644
--- a/app/src/main/java/com/termux/app/TermuxInstaller.java
+++ b/app/src/main/java/com/termux/app/TermuxInstaller.java
@@ -1,177 +1,24 @@
package com.termux.app;
-import android.app.Activity;
-import android.app.AlertDialog;
-import android.app.ProgressDialog;
import android.content.Context;
import android.os.Environment;
-import android.os.UserManager;
import android.system.Os;
import android.util.Log;
-import android.util.Pair;
-import android.view.WindowManager;
-import com.termux.R;
-import com.termux.terminal.EmulatorDebug;
-
-import java.io.BufferedReader;
-import java.io.ByteArrayInputStream;
import java.io.File;
-import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.InputStreamReader;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipInputStream;
/**
- * Install the Termux bootstrap packages if necessary by following the below steps:
- *
- * (1) If $PREFIX already exist, assume that it is correct and be done. Note that this relies on that we do not create a
- * broken $PREFIX folder below.
- *
- * (2) A progress dialog is shown with "Installing..." message and a spinner.
- *
- * (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below.
- *
- * (4) The zip file is loaded from a shared library.
- *
- * (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
- * continuously encountering zip file entries:
- *
- * (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup.
- *
- * (5.2) For every other zip entry, extract it into $STAGING_PREFIX and set execute permissions if necessary.
+ * Install the Termux bootstrap packages if necessary.
*/
final class TermuxInstaller {
- /** Performs setup if necessary. */
- static void setupIfNeeded(final Activity activity, final Runnable whenDone) {
- // Termux can only be run as the primary user (device owner) since only that
- // account has the expected file system paths. Verify that:
- UserManager um = (UserManager) activity.getSystemService(Context.USER_SERVICE);
- boolean isPrimaryUser = um.getSerialNumberForUser(android.os.Process.myUserHandle()) == 0;
- if (!isPrimaryUser) {
- new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_not_primary_user_message)
- .setOnDismissListener(dialog -> System.exit(0)).setPositiveButton(android.R.string.ok, null).show();
- return;
- }
-
- final File PREFIX_FILE = new File(TermuxService.PREFIX_PATH);
- if (PREFIX_FILE.isDirectory()) {
- whenDone.run();
- return;
- }
-
- final ProgressDialog progress = ProgressDialog.show(activity, null, activity.getString(R.string.bootstrap_installer_body), true, false);
- new Thread() {
- @Override
- public void run() {
- try {
- final String STAGING_PREFIX_PATH = TermuxService.FILES_PATH + "/usr-staging";
- final File STAGING_PREFIX_FILE = new File(STAGING_PREFIX_PATH);
-
- if (STAGING_PREFIX_FILE.exists()) {
- deleteFolder(STAGING_PREFIX_FILE);
- }
-
- final byte[] buffer = new byte[8096];
- final List> symlinks = new ArrayList<>(50);
-
- final byte[] zipBytes = loadZipBytes();
- try (ZipInputStream zipInput = new ZipInputStream(new ByteArrayInputStream(zipBytes))) {
- ZipEntry zipEntry;
- while ((zipEntry = zipInput.getNextEntry()) != null) {
- if (zipEntry.getName().equals("SYMLINKS.txt")) {
- BufferedReader symlinksReader = new BufferedReader(new InputStreamReader(zipInput));
- String line;
- while ((line = symlinksReader.readLine()) != null) {
- String[] parts = line.split("←");
- if (parts.length != 2)
- throw new RuntimeException("Malformed symlink line: " + line);
- String oldPath = parts[0];
- String newPath = STAGING_PREFIX_PATH + "/" + parts[1];
- symlinks.add(Pair.create(oldPath, newPath));
-
- ensureDirectoryExists(new File(newPath).getParentFile());
- }
- } else {
- String zipEntryName = zipEntry.getName();
- File targetFile = new File(STAGING_PREFIX_PATH, zipEntryName);
- boolean isDirectory = zipEntry.isDirectory();
-
- ensureDirectoryExists(isDirectory ? targetFile : targetFile.getParentFile());
-
- if (!isDirectory) {
- try (FileOutputStream outStream = new FileOutputStream(targetFile)) {
- int readBytes;
- while ((readBytes = zipInput.read(buffer)) != -1)
- outStream.write(buffer, 0, readBytes);
- }
- if (zipEntryName.startsWith("bin/") || zipEntryName.startsWith("libexec") || zipEntryName.startsWith("lib/apt/methods")) {
- //noinspection OctalInteger
- Os.chmod(targetFile.getAbsolutePath(), 0700);
- }
- }
- }
- }
- }
-
- if (symlinks.isEmpty())
- throw new RuntimeException("No SYMLINKS.txt encountered");
- for (Pair symlink : symlinks) {
- Os.symlink(symlink.first, symlink.second);
- }
-
- if (!STAGING_PREFIX_FILE.renameTo(PREFIX_FILE)) {
- throw new RuntimeException("Unable to rename staging folder");
- }
-
- activity.runOnUiThread(whenDone);
- } catch (final Exception e) {
- Log.e(EmulatorDebug.LOG_TAG, "Bootstrap error", e);
- activity.runOnUiThread(() -> {
- try {
- new AlertDialog.Builder(activity).setTitle(R.string.bootstrap_error_title).setMessage(R.string.bootstrap_error_body)
- .setNegativeButton(R.string.bootstrap_error_abort, (dialog, which) -> {
- dialog.dismiss();
- activity.finish();
- }).setPositiveButton(R.string.bootstrap_error_try_again, (dialog, which) -> {
- dialog.dismiss();
- TermuxInstaller.setupIfNeeded(activity, whenDone);
- }).show();
- } catch (WindowManager.BadTokenException e1) {
- // Activity already dismissed - ignore.
- }
- });
- } finally {
- activity.runOnUiThread(() -> {
- try {
- progress.dismiss();
- } catch (RuntimeException e) {
- // Activity already dismissed - ignore.
- }
- });
- }
- }
- }.start();
- }
-
- private static void ensureDirectoryExists(File directory) {
+ static void ensureDirectoryExists(File directory) {
if (!directory.isDirectory() && !directory.mkdirs()) {
throw new RuntimeException("Unable to create directory: " + directory.getAbsolutePath());
}
}
- public static byte[] loadZipBytes() {
- // Only load the shared library when necessary to save memory usage.
- System.loadLibrary("termux-bootstrap");
- return getZip();
- }
-
- public static native byte[] getZip();
-
/** Delete a folder and all its content or throw. Don't follow symlinks. */
static void deleteFolder(File fileOrDirectory) throws IOException {
if (fileOrDirectory.getCanonicalPath().equals(fileOrDirectory.getAbsolutePath()) && fileOrDirectory.isDirectory()) {
diff --git a/app/src/main/java/com/termux/app/TermuxPackageInstaller.java b/app/src/main/java/com/termux/app/TermuxPackageInstaller.java
new file mode 100644
index 0000000000..5d2ebbfcfd
--- /dev/null
+++ b/app/src/main/java/com/termux/app/TermuxPackageInstaller.java
@@ -0,0 +1,135 @@
+package com.termux.app;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.os.Process;
+import android.system.Os;
+import android.util.Log;
+
+import com.termux.terminal.EmulatorDebug;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileReader;
+import java.io.IOException;
+
+public class TermuxPackageInstaller extends BroadcastReceiver {
+
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ try {
+ String packageName = intent.getData().getSchemeSpecificPart();
+ String action = intent.getAction();
+ PackageManager packageManager = context.getPackageManager();
+
+ if (Intent.ACTION_PACKAGE_ADDED.equals(action)) {
+ ApplicationInfo info = packageManager.getApplicationInfo(packageName, 0);
+ if (Process.myUid() == info.uid) {
+ installPackage(info);
+ }
+ } else if (Intent.ACTION_PACKAGE_REMOVED.equals(action)) {
+ if (Process.myUid() == intent.getIntExtra(Intent.EXTRA_UID, -1)) {
+ uninstallPackage(packageName);
+ }
+
+ }
+ } catch (Exception e) {
+ Log.e("termux", "Error in package management: " + e);
+ }
+ }
+
+ static void installPackage(ApplicationInfo info) throws Exception {
+ File filesMappingFile = new File(info.nativeLibraryDir, "files.so");
+ if (!filesMappingFile.exists()) {
+ Log.e("termux", "No file mapping at " + filesMappingFile.getAbsolutePath());
+ return;
+ }
+
+ Log.e("termux", "Installing: " + info.packageName);
+ BufferedReader reader = new BufferedReader(new FileReader(filesMappingFile));
+ String line;
+ while ((line = reader.readLine()) != null) {
+ String[] parts = line.split("←");
+ if (parts.length != 2) {
+ Log.e(EmulatorDebug.LOG_TAG, "Malformed line " + line + " in " + filesMappingFile.getAbsolutePath());
+ continue;
+ }
+
+ String oldPath = info.nativeLibraryDir + "/" + parts[0];
+ String newPath = TermuxService.PREFIX_PATH + "/" + parts[1];
+
+ TermuxInstaller.ensureDirectoryExists(new File(newPath).getParentFile());
+
+ Log.e(EmulatorDebug.LOG_TAG, "About to setup link: " + oldPath + " ← " + newPath);
+ new File(newPath).delete();
+ Os.symlink(oldPath, newPath);
+ }
+
+ File symlinksFile = new File(info.nativeLibraryDir, "symlinks.so");
+ if (!symlinksFile.exists()) {
+ Log.e("termux", "No symlinks mapping at " + symlinksFile.getAbsolutePath());
+ }
+
+ reader = new BufferedReader(new FileReader(symlinksFile));
+ while ((line = reader.readLine()) != null) {
+ String[] parts = line.split("←");
+ if (parts.length != 2) {
+ Log.e(EmulatorDebug.LOG_TAG, "Malformed line " + line + " in " + symlinksFile.getAbsolutePath());
+ continue;
+ }
+
+ String oldPath = parts[0];
+ String newPath = TermuxService.PREFIX_PATH + "/" + parts[1];
+
+ TermuxInstaller.ensureDirectoryExists(new File(newPath).getParentFile());
+
+ Log.e(EmulatorDebug.LOG_TAG, "About to setup link: " + oldPath + " ← " + newPath);
+ new File(newPath).delete();
+ Os.symlink(oldPath, newPath);
+ }
+ }
+
+ private static void uninstallPackage(String packageName) throws IOException {
+ Log.e("termux", "Uninstalling: " + packageName);
+ // We're currently visiting the whole $PREFIX.
+ // If we store installed symlinks in installPackage() we could just visit those,
+ // at the cost of increased complexity and risk for errors.
+ File prefixDir = new File(TermuxService.PREFIX_PATH);
+ removeBrokenSymlinks(prefixDir);
+ }
+
+ private static void removeBrokenSymlinks(File parentDir) throws IOException {
+ File[] children = parentDir.listFiles();
+ if (children == null) {
+ return;
+ }
+ for (File child : children) {
+ if (!child.exists()) {
+ Log.e("termux", "Removing broken symlink: " + child.getAbsolutePath());
+ child.delete();
+ } else if (child.isDirectory()) {
+ removeBrokenSymlinks(child);
+ }
+ }
+ }
+
+ public static void setupAllInstalledPackages(Context context) {
+ try {
+ removeBrokenSymlinks(new File(TermuxService.PREFIX_PATH));
+
+ PackageManager packageManager = context.getPackageManager();
+ for (PackageInfo info : packageManager.getInstalledPackages(0)) {
+ if (info.sharedUserId != null && info.sharedUserId.equals("com.termux")) {
+ installPackage(info.applicationInfo);
+ }
+ }
+ } catch (Exception e) {
+ Log.e("termux", "Error setting up all packages", e);
+ }
+
+ }
+}
diff --git a/app/src/main/java/com/termux/app/TermuxService.java b/app/src/main/java/com/termux/app/TermuxService.java
index 955ce8656b..ad3833f5a0 100644
--- a/app/src/main/java/com/termux/app/TermuxService.java
+++ b/app/src/main/java/com/termux/app/TermuxService.java
@@ -9,11 +9,13 @@
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.res.Resources;
import android.net.Uri;
import android.net.wifi.WifiManager;
import android.os.Binder;
import android.os.Build;
+import android.os.Environment;
import android.os.Handler;
import android.os.IBinder;
import android.os.PowerManager;
@@ -27,6 +29,9 @@
import com.termux.terminal.TerminalSession.SessionChangedCallback;
import java.io.File;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.net.URL;
import java.util.ArrayList;
import java.util.List;
@@ -55,6 +60,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
private static final int NOTIFICATION_ID = 1337;
private static final String ACTION_STOP_SERVICE = "com.termux.service_stop";
+ private static final String ACTION_INSTALL_PACKAGES = "com.termux.install_packages";
private static final String ACTION_LOCK_WAKE = "com.termux.service_wake_lock";
private static final String ACTION_UNLOCK_WAKE = "com.termux.service_wake_unlock";
/** Intent action to launch a new terminal session. Executed from TermuxWidgetProvider. */
@@ -94,6 +100,8 @@ class LocalBinder extends Binder {
/** If the user has executed the {@link #ACTION_STOP_SERVICE} intent. */
boolean mWantsToStop = false;
+ private final TermuxPackageInstaller packageInstaller = new TermuxPackageInstaller();
+
@SuppressLint("Wakelock")
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
@@ -103,6 +111,15 @@ public int onStartCommand(Intent intent, int flags, int startId) {
for (int i = 0; i < mTerminalSessions.size(); i++)
mTerminalSessions.get(i).finishIfRunning();
stopSelf();
+ } else if (ACTION_INSTALL_PACKAGES.equals(action)) {
+ String[] packages = intent.getStringArrayExtra("packages");
+ if (packages == null || packages.length == 0) {
+ Log.e(EmulatorDebug.LOG_TAG, ACTION_INSTALL_PACKAGES + " called without packages");
+ } else {
+ for (String pkg : packages) {
+ ApkInstaller.installPackageApk(pkg, this);
+ }
+ }
} else if (ACTION_LOCK_WAKE.equals(action)) {
if (mWakeLock == null) {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
@@ -185,8 +202,16 @@ public IBinder onBind(Intent intent) {
@Override
public void onCreate() {
+ TermuxPackageInstaller.setupAllInstalledPackages(this);
setupNotificationChannel();
startForeground(NOTIFICATION_ID, buildNotification());
+
+ IntentFilter addedFilter = new IntentFilter(Intent.ACTION_PACKAGE_ADDED);
+ addedFilter.addDataScheme("package");
+ IntentFilter removedFilter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
+ removedFilter.addDataScheme("package");
+ this.registerReceiver(packageInstaller, addedFilter);
+ this.registerReceiver(packageInstaller, removedFilter);
}
/** Update the shown foreground service notification after making any changes that affect it. */
@@ -254,6 +279,8 @@ private Notification buildNotification() {
@Override
public void onDestroy() {
+ unregisterReceiver(packageInstaller);
+
File termuxTmpDir = new File(TermuxService.PREFIX_PATH + "/tmp");
if (termuxTmpDir.exists()) {
diff --git a/gradle.properties b/gradle.properties
index 945ab5d248..247ae7222f 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -16,6 +16,6 @@ org.gradle.jvmargs=-Xmx2048M
android.useAndroidX=true
minSdkVersion=24
-targetSdkVersion=28
+targetSdkVersion=29
ndkVersion=21.3.6528147
-compileSdkVersion=28
+compileSdkVersion=29
diff --git a/pkg.sh b/pkg.sh
new file mode 100644
index 0000000000..9c09729d7c
--- /dev/null
+++ b/pkg.sh
@@ -0,0 +1,34 @@
+#!/data/data/com.termux/files/usr/bin/bash
+set -e -u
+
+show_help() {
+ echo 'Usage: pkg command [arguments]'
+ echo ''
+ echo 'A tool for managing packages. Commands:'
+ echo ''
+ echo ' install '
+ exit 1
+}
+
+if [ $# = 0 ]; then
+ show_help
+fi
+
+CMD="$1"
+shift 1
+
+install_packages() {
+ ALL_PACKAGES="$@"
+ am startservice \
+ --user 0 \
+ --esa packages "${ALL_PACKAGES// /,}" \
+ -a com.termux.install_packages \
+ com.termux/com.termux.app.TermuxService \
+ > /dev/null
+}
+
+case "$CMD" in
+ h*) show_help;;
+ add|i*) install_packages "$@";;
+ *) echo "Unknown command: '$CMD' (run 'pkg help' for usage information)"; exit 1;;
+esac