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