android 10 compatibility: APK-based package management (#1715)

A basic implementation of APK-based package management.
Can install/uninstall Termux packages or output a list of installed ones into Android log.
This commit is contained in:
Prakhar Shukla 2020-08-22 01:51:58 +05:30 committed by GitHub
parent 531c32f3c9
commit f6c3b6f38a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
20 changed files with 1108 additions and 308 deletions

View File

@ -1,4 +1,4 @@
name: Build
name: APK
on:
push:
@ -16,6 +16,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

View File

@ -16,6 +16,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

View File

@ -1,6 +1,12 @@
import java.security.DigestInputStream
import java.util.zip.ZipEntry
import java.util.zip.ZipInputStream
plugins {
id "com.android.application"
id "com.android.application"
}
apply plugin: 'kotlin-android'
android {
compileSdkVersion project.properties.compileSdkVersion.toInteger()
@ -19,17 +25,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 {
@ -57,11 +52,8 @@ android {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
externalNativeBuild {
ndkBuild {
path "src/main/cpp/Android.mk"
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
}
testOptions {
@ -74,75 +66,132 @@ android {
dependencies {
testImplementation 'junit:junit:4.13'
testImplementation 'org.robolectric:robolectric:4.3.1'
//kotlin
implementation "androidx.core:core-ktx:1.3.1"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.8'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.8'
}
task versionName {
doLast {
print android.defaultConfig.versionName
}
doLast {
print android.defaultConfig.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 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)
}
}
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, "libsymlinks.so").getAbsoluteFile()
if (symlinksFile.exists()) symlinksFile.delete();
def mappingsFile = new File(outputDir, "libfiles.so").getAbsoluteFile()
if (mappingsFile.exists()) mappingsFile.delete()
mappingsFile.createNewFile()
def mappingsFileWriter = new BufferedWriter(new FileWriter(mappingsFile))
def counter = 100
new ZipInputStream(new FileInputStream(zipDownloadFile)).withCloseable { zipInput ->
ZipEntry zipEntry
while ((zipEntry = zipInput.getNextEntry()) != null) {
if (zipEntry.getName() == "SYMLINKS.txt") {
zipInput.transferTo(new FileOutputStream(symlinksFile))
} else if (!zipEntry.isDirectory()) {
def soName = "lib" + counter + ".so"
def targetFile = new File(outputDir, soName).getAbsoluteFile()
println "target file path is ${targetFile}"
try {
zipInput.transferTo(new FileOutputStream(targetFile))
} catch (Exception e) {
println "Error ${e}"
}
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()
}
clean {
task setupBootstraps() {
doLast {
def tree = fileTree(new File(projectDir, 'src/main/cpp'))
tree.include 'bootstrap-*.zip'
tree.each { it.delete() }
}
}
task downloadBootstraps(){
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)
}
android.applicationVariants.all { variant ->
variant.javaCompileProvider.get().dependsOn(setupBootstraps)
}
}
repositories {
mavenCentral()
}

View File

@ -3,15 +3,20 @@
package="com.termux"
android:installLocation="internalOnly"
android:sharedUserId="com.termux"
android:sharedUserLabel="@string/shared_user_label" >
android:sharedUserLabel="@string/shared_user_label">
<uses-feature android:name="android.hardware.touchscreen" android:required="false" />
<uses-feature android:name="android.software.leanback" android:required="false" />
<uses-feature
android:name="android.hardware.touchscreen"
android:required="false" />
<uses-feature
android:name="android.software.leanback"
android:required="false" />
<permission android:name="com.termux.permission.RUN_COMMAND"
android:label="@string/run_command_permission_label"
<permission
android:name="com.termux.permission.RUN_COMMAND"
android:description="@string/run_command_permission_description"
android:icon="@drawable/ic_launcher"
android:label="@string/run_command_permission_label"
android:protectionLevel="dangerous" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
@ -20,28 +25,32 @@
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<application
android:extractNativeLibs="true"
android:allowBackup="false"
android:icon="@drawable/ic_launcher"
android:banner="@drawable/banner"
android:extractNativeLibs="true"
android:icon="@drawable/ic_launcher"
android:label="@string/application_name"
android:theme="@style/Theme.Termux"
android:supportsRtl="false" >
android:supportsRtl="false"
android:theme="@style/Theme.Termux">
<!-- This (or rather, value 2.1 or higher) is needed to make the Samsung Galaxy S8
mark the app with "This app is optimized to run in full screen." -->
<meta-data android:name="android.max_aspect" android:value="10.0" />
<meta-data
android:name="android.max_aspect"
android:value="10.0" />
<activity
android:name="com.termux.app.TermuxActivity"
android:label="@string/application_name"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
android:configChanges="orientation|screenSize|smallestScreenSize|density|screenLayout|uiMode|keyboard|keyboardHidden|navigation"
android:label="@string/application_name"
android:launchMode="singleTask"
android:resizeableActivity="true"
android:windowSoftInputMode="adjustResize|stateAlwaysVisible" >
android:windowSoftInputMode="adjustResize|stateAlwaysVisible">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
@ -50,28 +59,33 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts" />
<meta-data
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
</activity>
<activity
android:name="com.termux.app.TermuxHelpActivity"
android:exported="false"
android:theme="@android:style/Theme.Material.Light.DarkActionBar"
android:label="@string/application_name"
android:parentActivityName=".app.TermuxActivity"
android:resizeableActivity="true"
android:label="@string/application_name" />
android:theme="@android:style/Theme.Material.Light.DarkActionBar" />
<activity
android:name="com.termux.filepicker.TermuxFileReceiverActivity"
android:label="@string/application_name"
android:taskAffinity="com.termux.filereceiver"
android:excludeFromRecents="true"
android:label="@string/application_name"
android:noHistory="true"
android:resizeableActivity="true"
android:noHistory="true">
android:taskAffinity="com.termux.filereceiver">
<!-- Accept multiple file types when sending. -->
<intent-filter>
<action android:name="android.intent.action.SEND"/>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="application/*" />
<data android:mimeType="audio/*" />
<data android:mimeType="image/*" />
@ -82,8 +96,9 @@
</intent-filter>
<!-- Be more restrictive for viewing files, restricting ourselves to text files. -->
<intent-filter tools:ignore="AppLinkUrlError">
<action android:name="android.intent.action.VIEW"/>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/*" />
<data android:mimeType="application/*log*" />
<data android:mimeType="application/json" />
@ -99,17 +114,18 @@
<!-- Launch activity automatically on boot on Android Things devices -->
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.IOT_LAUNCHER"/>
<category android:name="android.intent.category.DEFAULT"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.IOT_LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity-alias>
<provider
android:name=".filepicker.TermuxDocumentsProvider"
android:authorities="com.termux.documents"
android:grantUriPermissions="true"
android:exported="true"
android:grantUriPermissions="true"
android:permission="android.permission.MANAGE_DOCUMENTS">
<intent-filter>
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
@ -123,7 +139,7 @@
<service
android:name=".app.RunCommandService"
android:exported="true"
android:permission="com.termux.permission.RUN_COMMAND" >
android:permission="com.termux.permission.RUN_COMMAND">
<intent-filter>
<action android:name="com.termux.RUN_COMMAND" />
</intent-filter>
@ -131,13 +147,19 @@
<receiver android:name=".app.TermuxOpenReceiver" />
<provider android:authorities="com.termux.files"
android:readPermission="android.permission.permRead"
android:exported="true"
android:grantUriPermissions="true"
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider" />
<meta-data android:name="com.sec.android.support.multiwindow" android:value="true" />
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
<provider
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider"
android:authorities="com.termux.files"
android:exported="true"
android:grantUriPermissions="true"
android:readPermission="android.permission.permRead" />
<meta-data
android:name="com.sec.android.support.multiwindow"
android:value="true" />
<meta-data
android:name="com.samsung.android.multidisplay.keep_process_alive"
android:value="true" />
</application>
</manifest>

View File

@ -1,5 +0,0 @@
LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := libtermux-bootstrap
LOCAL_SRC_FILES := termux-bootstrap-zip.S termux-bootstrap.c
include $(BUILD_SHARED_LIBRARY)

View File

@ -1,18 +0,0 @@
.global blob
.global blob_size
.section .rodata
blob:
#if defined __i686__
.incbin "bootstrap-i686.zip"
#elif defined __x86_64__
.incbin "bootstrap-x86_64.zip"
#elif defined __aarch64__
.incbin "bootstrap-aarch64.zip"
#elif defined __arm__
.incbin "bootstrap-arm.zip"
#else
# error Unsupported arch
#endif
1:
blob_size:
.int 1b - blob

View File

@ -1,11 +0,0 @@
#include <jni.h>
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;
}

View File

@ -138,6 +138,7 @@ public final class BackgroundJob {
List<String> environment = new ArrayList<>();
environment.add("TERMUX_ANDROID10=1");
environment.add("TERM=xterm-256color");
environment.add("COLORTERM=truecolor");
environment.add("HOME=" + TermuxService.HOME_PATH);

View File

@ -0,0 +1,297 @@
package com.termux.app
import android.app.NotificationManager
import android.content.Context
import android.os.Handler
import android.os.StatFs
import android.util.Log
import androidx.core.app.NotificationCompat
import com.termux.R
import com.termux.app.PackageInstaller.Companion.log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileNotFoundException
import java.io.FileOutputStream
import java.net.ConnectException
import java.net.URL
import java.net.URLConnection
import java.net.UnknownHostException
// Download status constants
const val ENTERED = -1
const val QUEUED = 0
const val STARTED = 1
const val RUNNING = 2
const val COMPLETED = 3
const val ERROR = 4
const val NOTIFICATION_CHANNEL_ID = "termux_notification_channel"
class PackageDownloader(val context: Context) {
private lateinit var notificationManager: NotificationManager
private lateinit var builder: NotificationCompat.Builder
interface ProgressListener {
fun onProgress(data: DownloadData)
}
interface StartListener {
fun onStart(data: DownloadData)
}
interface CompleteListener {
fun onComplete(data: DownloadData)
}
interface ErrorListener {
fun onError(data: ErrorData)
}
private lateinit var progressListener: ProgressListener
private lateinit var errorListener: ErrorListener
private lateinit var completeListener: CompleteListener
private lateinit var startListener: StartListener
private lateinit var downloadingJob: Job
fun initListeners(progressL: ProgressListener, errorL: ErrorListener, completeL: CompleteListener, startL: StartListener) {
this.progressListener = progressL
this.completeListener = completeL
this.errorListener = errorL
this.startListener = startL
}
fun download(packageName: String) {
notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
builder = NotificationCompat.Builder(context, "termux_notification_channel").setChannelId(NOTIFICATION_CHANNEL_ID)
var isStartNotified = false
var percent20 = false
var percent40 = false
var percent60 = false
var percent80 = false
//val fileUrl = "https://termux.net/apks/$packageName.apk"
val fileUrl = "https://staging.termux-mirror.ml/android-10/$packageName.apk"
"URL -> $fileUrl".log()
try {
downloadingJob = GlobalScope.launch(Dispatchers.IO) {
try {
val downloadData = DownloadData(packageName, 0, 0, 0, ENTERED)
showNotification(downloadData)
val downloadFile = File("${TermuxService.FILES_PATH}/${packageName}.apk")
deleteFileIfExists(downloadFile)
"Fetching the file size...".log()
val url = URL(fileUrl)
val connection: URLConnection = url.openConnection()
connection.connect()
val lengthOfFile: Int = connection.contentLength
var total = 0
if ((getFreeSpace() * 2) > lengthOfFile) {
downloadData.totalKB = lengthOfFile.toKB()
downloadData.Status = QUEUED
"Queuing the download...".log()
FileOutputStream(downloadFile).use { out ->
url.openStream().use { `in` ->
val buffer = ByteArray(1024)
var read: Int
while (`in`.read(buffer).also { read = it } >= 0) {
total += read
out.write(buffer, 0, read)
downloadData.progressInKB = total.toKB()
if (total != 0 && !isStartNotified) {
downloadData.Status = STARTED
startListener.onStart(downloadData)
isStartNotified = true
}
downloadData.Status = RUNNING
GlobalScope.launch(Dispatchers.Main) {
}
val percent = (total * 100) / lengthOfFile
fun updateProgress(percent: Int) {
downloadData.progressPercent = percent
progressListener.onProgress(downloadData)
}
if (percent % 20 == 0 && total != lengthOfFile) {
// Can be simplified
percent.let {
if (it == 20 && !percent20) {
updateNotification(downloadData)
percent20 = true
updateProgress(it)
} else if (it == 40 && !percent40) {
updateNotification(downloadData)
percent40 = true
updateProgress(it)
} else if (it == 60 && !percent60) {
updateNotification(downloadData)
percent60 = true
updateProgress(it)
} else if (it == 80 && !percent80) {
updateNotification(downloadData)
percent80 = true
updateProgress(it)
}
}
}
if (total == lengthOfFile) {
downloadData.progressPercent = percent
downloadData.Status = COMPLETED
removeNotification(downloadData)
completeListener.onComplete(downloadData)
}
}
}
}
} else {
throw InsufficientStorageException("Insufficient Storage. Please clear some data before installing.")
}
} catch (e: FileNotFoundException) {
packageName.clearThingsUp()
errorListener.onError(ErrorData(packageName = packageName, Status = ERROR, error = "Package $packageName does not exists!"))
} catch (e: UnknownHostException) {
packageName.clearThingsUp()
errorListener.onError(ErrorData(packageName = packageName, Status = ERROR, error = "Cannot connect to internet or server unavailable. Aborting the installation."))
} catch (e: ConnectException) {
packageName.clearThingsUp()
errorListener.onError(ErrorData(packageName = packageName, Status = ERROR, error = "Cannot connect to internet or server unavailable. Aborting the installation."))
} catch (e: InsufficientStorageException) {
packageName.clearThingsUp()
errorListener.onError(ErrorData(packageName = packageName, Status = ERROR, error = "Insufficient Storage. Please clear some data before installing."))
} catch (e: Exception) {
packageName.clearThingsUp()
Log.e("termux", "Error installing $packageName", e)
if (this@PackageDownloader::downloadingJob.isInitialized) {
if (downloadingJob.isActive) {
downloadingJob.cancel()
}
}
errorListener.onError(ErrorData(packageName = packageName, Status = ERROR, error = e.toString()))
}
}
} catch (e: FileNotFoundException) {
packageName.clearThingsUp()
errorListener.onError(ErrorData(packageName = packageName, Status = ERROR, error = "Package $packageName does not exists!"))
} catch (e: ConnectException) {
packageName.clearThingsUp()
errorListener.onError(ErrorData(packageName = packageName, Status = ERROR, error = "Cannot connect to internet or server unavailable. Aborting the installation."))
} catch (e: UnknownHostException) {
packageName.clearThingsUp()
errorListener.onError(ErrorData(packageName = packageName, Status = ERROR, error = "Cannot connect to internet. Aborting the installation."))
} catch (e: InsufficientStorageException) {
packageName.clearThingsUp()
errorListener.onError(ErrorData(packageName = packageName, Status = ERROR, error = "Insufficient Storage. Please clear some data before installing."))
} catch (e: Exception) {
packageName.clearThingsUp()
Log.e("termux", "Error installing $packageName", e)
if (this::downloadingJob.isInitialized) {
if (downloadingJob.isActive) {
downloadingJob.cancel()
}
}
errorListener.onError(ErrorData(packageName = packageName, Status = ERROR, error = e.toString()))
}
}
private fun getFreeSpace(): Long {
val path = context.dataDir
val stat = StatFs(path.path)
val blockSize: Long
val availableBlocks: Long
blockSize = stat.blockSizeLong
availableBlocks = stat.availableBlocksLong
return availableBlocks * blockSize
}
private fun deleteFileIfExists(downloadFile: File) {
if (downloadFile.exists()) {
if (downloadFile.delete())
"File Deleted!".log()
}
}
private fun Int.toKB(): Long {
return (this * 0.001).toLong()
}
private fun String.clearThingsUp() {
val downloadFile = File("${TermuxService.FILES_PATH}/${this}.apk")
deleteFileIfExists(downloadFile)
notificationManager.cancelAll()
}
/*
Notification
*/
private fun showNotification(downloadData: DownloadData) {
builder
.setContentTitle("Downloading ${downloadData.packageName}")
.setSmallIcon(R.drawable.ic_service_notification)
.setProgress(0, 0, true)
//setting indeterminate progress
getNotificationID().let {
downloadData.notificationID = it
notificationManager.notify(it, builder.build())
}
}
private fun removeNotification(downloadData: DownloadData) {
GlobalScope.launch(Dispatchers.Main) {
if (downloadData.Status == COMPLETED) {
builder.setContentTitle("Package Downloaded.")
builder.setContentTitle("${downloadData.packageName} has been download. You can install the package now.")
builder.setProgress(0, 0, false)
notificationManager.notify(downloadData.notificationID, builder.build())
Handler().postDelayed({
notificationManager.cancel(downloadData.notificationID)
}, 5000)
} else
notificationManager.cancel(downloadData.notificationID)
}
}
private fun updateNotification(downloadData: DownloadData) {
downloadData.let {
builder.setContentTitle("Downloading ${downloadData.packageName}")
builder.setProgress(it.totalKB.toInt(), it.progressInKB.toInt(), false)
notificationManager.notify(it.notificationID, builder.build())
}
}
private fun getNotificationID(): Int {
return (100..999).random()
}
}
data class DownloadData(
var packageName: String,
var totalKB: Long = 0,
var progressInKB: Long = 0,
var progressPercent: Int = 0,
var Status: Int,
var notificationID: Int = 0
)
class InsufficientStorageException(message: String) : Exception(message)
data class ErrorData(var packageName: String, var error: String, var extraLogs: String = "", var Status: Int)

View File

@ -0,0 +1,276 @@
package com.termux.app
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageInstaller
import android.net.Uri
import android.os.Build
import android.provider.Settings
import android.util.Log
import android.widget.Toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.OutputStream
const val PACKAGE_INSTALLED_ACTION = "com.termux.SESSION_API_PACKAGE_INSTALLED"
class PackageInstaller(val context: Context) : PackageDownloader.ErrorListener, PackageDownloader.ProgressListener, PackageDownloader.StartListener, PackageDownloader.CompleteListener {
private val downloadHashMap: HashMap<String, LocalDownloadData> = hashMapOf()
private val installationResponseHashMap: HashMap<String, String> = hashMapOf()
private var packagesToInstall: ArrayList<String> = arrayListOf()
private val packageDownloader = PackageDownloader(context)
private var currentPosition = 0
private var totalLength = 0
fun initDownloader(packageList: Array<String>) {
if (isInstallationOfApkAllowed()) {
context.registerReceiver(broadcastReceiver, IntentFilter(PACKAGE_INSTALLED_ACTION))
packageDownloader.initListeners(this, this, this, this)
val verifiedPackageList = packageList.removeRepetition()
verifiedPackageList.forEach { packageName ->
startDownload(packageName)
}
} else {
GlobalScope.launch(Dispatchers.Main) {
"Permission Insufficient. Please provide the following permission and rerun the command.".log()
Toast.makeText(context, "Please allow installation from unknown sources for Termux.", Toast.LENGTH_SHORT).show()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
//This weirdly messes up the activity stack
val permIntent = Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:com.termux"))
permIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(permIntent)
}
}
}
}
private fun startDownload(packageName: String) {
downloadHashMap[packageName] = LocalDownloadData(packageName, null)
packageDownloader.download(packageName)
}
private fun Array<String>.removeRepetition(): Array<String> {
return this.toList().distinct().toTypedArray()
}
override fun onProgress(data: DownloadData) {
"${data.packageName} has been ${data.progressPercent}% downloaded.".log()
}
override fun onStart(data: DownloadData) {
"Downloading ${data.packageName}...".log()
}
override fun onComplete(data: DownloadData) {
downloadHashMap[data.packageName] = LocalDownloadData(data.packageName, true, "Successfully downloaded!")
"Completed downloading ${data.packageName}...".log()
checkIfAllPackagesAreDownloaded()
}
override fun onError(data: ErrorData) {
downloadHashMap[data.packageName] = LocalDownloadData(data.packageName, true, "download aborted -> ${data.error}")
"Error downloading ${data.packageName} --> ${data.error}...".log()
checkIfAllPackagesAreDownloaded()
}
private fun checkIfAllPackagesAreDownloaded() {
var counter = 0
downloadHashMap.forEach { (_, installData) ->
if (installData.isDownloaded == null) {
//packageLeft
++counter
}
}
if (counter == 0) {
endDownloadSession()
proceedToInstallation()
}
}
/*---------------------------------------- INSTALLATION------------------------------------------*/
private fun installAPK(packageName: String) {
"Proceeding to write $packageName".log()
if (isInstallationOfApkAllowed()) {
GlobalScope.launch(Dispatchers.IO) {
var session: PackageInstaller.Session? = null
try {
val packageInstaller: PackageInstaller = context.packageManager.packageInstaller
val params = PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val sessionId = packageInstaller.createSession(params)
session = packageInstaller.openSession(sessionId)
addApkToInstallSession(session, packageName)
val installBroadcast = PendingIntent.getBroadcast(context, 0, Intent(PACKAGE_INSTALLED_ACTION).putExtra("packageName", packageName), PendingIntent.FLAG_UPDATE_CURRENT)
session.commit(installBroadcast.intentSender)
session.close()
} catch (e: IOException) {
throw RuntimeException("Couldn't install package", e)
} catch (e: RuntimeException) {
session?.abandon()
throw e
} finally {
session?.close()
}
}
} else {
GlobalScope.launch(Dispatchers.Main) {
"Permission Insufficient. Please provide the following permission and rerun the command.".log()
Toast.makeText(context, "Please allow installation from unknown sources for Termux.", Toast.LENGTH_SHORT).show()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startActivity(Intent(Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES, Uri.parse("package:com.termux")))
}
}
}
}
private fun addApkToInstallSession(session: PackageInstaller.Session,
packageName: String) {
val file = File("${TermuxService.FILES_PATH}/$packageName.apk")
val packageInSession: OutputStream = session.openWrite(packageName, 0, -1)
val inputStream = FileInputStream(file)
try {
var c: Int
val buffer = ByteArray(16384)
while (inputStream.read(buffer).also { c = it } >= 0) {
packageInSession.write(buffer, 0, c)
}
} catch (e: IOException) {
"IOEX".log()
} finally {
try {
packageInSession.close()
inputStream.close()
} catch (e: IOException) {
("IOEX in closing the stream").log()
}
}
}
private val broadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val extras = intent.extras
val status = extras!!.getInt(PackageInstaller.EXTRA_STATUS)
val message = extras.getString(PackageInstaller.EXTRA_STATUS_MESSAGE)
val packageName = extras.getString("packageName")!!
if (PACKAGE_INSTALLED_ACTION == intent.action) {
when (status) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
// This test app isn't privileged, so the user has to confirm the install.
val confirmIntent = extras[Intent.EXTRA_INTENT] as Intent
confirmIntent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
context.startActivity(confirmIntent)
}
PackageInstaller.STATUS_SUCCESS -> {
Toast.makeText(context, "Install succeeded!", Toast.LENGTH_LONG).show()
("$packageName Install succeeded!").log()
installationResponseHashMap[packageName] = "installation successful!"
proceedToInstallation(true)
}
PackageInstaller.STATUS_FAILURE, PackageInstaller.STATUS_FAILURE_ABORTED, PackageInstaller.STATUS_FAILURE_BLOCKED, PackageInstaller.STATUS_FAILURE_CONFLICT, PackageInstaller.STATUS_FAILURE_INCOMPATIBLE, PackageInstaller.STATUS_FAILURE_INVALID, PackageInstaller.STATUS_FAILURE_STORAGE -> {
Toast.makeText(context, "Install failed! $status, $message",
Toast.LENGTH_LONG).show()
("$packageName Install failed!").log()
//can separate cases if that's important
installationResponseHashMap[packageName] = "installation failed! | $message"
proceedToInstallation(true)
}
else -> {
("$packageName Unrecognized status received from installer: $status").log()
Toast.makeText(context, "Unrecognized status received from installer: $status",
Toast.LENGTH_LONG).show()
installationResponseHashMap[packageName] = "installation failed! | $message"
proceedToInstallation(true)
// exitActivity("Package failed to install -> Unknown Error!")
}
}
}
}
}
private fun proceedToInstallation(next: Boolean = false) {
getApkListInFileSystem()
if (!next) {
if (packagesToInstall.isEmpty()) {
endInstallationSession()
} else {
totalLength = packagesToInstall.size - 1
installAPK(packagesToInstall[currentPosition])
}
} else {
if (currentPosition == totalLength) {
endInstallationSession()
} else {
installAPK(packagesToInstall[++currentPosition])
}
}
}
private fun getApkListInFileSystem() {
downloadHashMap.forEach { (packageName) ->
//Setting up a default response
installationResponseHashMap[packageName] = "the request package was either not downloaded or just doesn't exist!"
val apkFileToBeInstalled = File("${TermuxService.FILES_PATH}/$packageName.apk")
if (apkFileToBeInstalled.exists()) {
packagesToInstall.add(packageName)
}
}
}
private fun endDownloadSession() {
"DOWNLOADS COMPLETED".log()
"Here are the logs...".log()
downloadHashMap.forEach { (packageName, installData) ->
"$packageName -> ${installData.extraLogs}".log()
}
}
private fun endInstallationSession() {
"INSTALLATION COMPLETED".log()
"Here are the logs...".log()
installationResponseHashMap.forEach { (packageName, response) ->
"$packageName -> $response".log()
}
context.unregisterReceiver(broadcastReceiver)
this.packagesToInstall.clear()
this.installationResponseHashMap.clear()
this.downloadHashMap.clear()
}
private fun isInstallationOfApkAllowed(): Boolean {
val packageManager = context.packageManager
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
packageManager.canRequestPackageInstalls()
} else
true
}
companion object {
private const val TERMUX_PACKAGE_TAG = "Termux Package Management"
fun Any.log() {
Log.i(TERMUX_PACKAGE_TAG, this.toString())
}
}
}
data class LocalDownloadData(var packageName: String, var isDownloaded: Boolean?, var extraLogs: String = "")

View File

@ -0,0 +1,31 @@
package com.termux.app
import android.content.Context
import android.content.pm.PackageManager
import com.termux.app.PackageInstaller.Companion.log
class PackageLister(val context: Context) {
// This class can be used to implement other stuff in the future relating to packages
fun listPackages() {
val termuxPackagesList: ArrayList<String> = arrayListOf()
val pm: PackageManager = context.packageManager
val packages = pm.getInstalledApplications(PackageManager.GET_META_DATA)
packages.forEach { packageInfo ->
val packageName = packageInfo.packageName
if (packageName.startsWith(TERMUX_APK_SUFFIX)) {
termuxPackagesList.add(packageName.replace(TERMUX_APK_SUFFIX, ""))
}
}
if (termuxPackagesList.isEmpty())
("No package is currently installed").log()
else {
("Here are the installed termux packages -> ").log()
termuxPackagesList.forEach { (it).log() }
}
}
}

View File

@ -0,0 +1,40 @@
package com.termux.app
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import com.termux.app.PackageInstaller.Companion.log
const val TERMUX_APK_SUFFIX = "net.termux."
class PackageUninstaller(var context: Context) {
fun uninstallPackages(packageList: Array<String>) {
registerBroadcast()
packageList.forEach { uninstallAPK(it) }
}
private fun uninstallAPK(packageName: String) {
val intent = Intent(Intent.ACTION_DELETE)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.data = Uri.parse("package:${TERMUX_APK_SUFFIX}${packageName}")
context.startActivity(intent)
}
private fun registerBroadcast() {
val uninstallApplication: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val packageName = intent.data!!.encodedSchemeSpecificPart.toString()
if (packageName.startsWith(TERMUX_APK_SUFFIX)) packageName.replace(TERMUX_APK_SUFFIX, "")
("Package Uninstalled --> $packageName").log()
}
}
val intentFilter = IntentFilter()
intentFilter.addAction(Intent.ACTION_PACKAGE_REMOVED)
intentFilter.addDataScheme("package")
context.registerReceiver(uninstallApplication, intentFilter)
}
}

View File

@ -487,19 +487,17 @@ public final class TermuxActivity extends Activity implements ServiceConnection
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();

View File

@ -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:
* <p/>
* (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.
* <p/>
* (2) A progress dialog is shown with "Installing..." message and a spinner.
* <p/>
* (3) A staging folder, $STAGING_PREFIX, is {@link #deleteFolder(File)} if left over from broken installation below.
* <p/>
* (4) The zip file is loaded from a shared library.
* <p/>
* (5) The zip, containing entries relative to the $PREFIX, is is downloaded and extracted by a zip input stream
* continuously encountering zip file entries:
* <p/>
* (5.1) If the zip entry encountered is SYMLINKS.txt, go through it and remember all symlinks to setup.
* <p/>
* (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<Pair<String, String>> 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<String, String> 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()) {

View File

@ -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, "libfiles.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, "libsymlinks.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);
}
}
}

View File

@ -9,6 +9,7 @@ import android.app.Service;
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;
@ -46,7 +47,9 @@ public final class TermuxService extends Service implements SessionChangedCallba
private static final String NOTIFICATION_CHANNEL_ID = "termux_notification_channel";
/** Note that this is a symlink on the Android M preview. */
/**
* Note that this is a symlink on the Android M preview.
*/
@SuppressLint("SdCardPath")
public static final String FILES_PATH = "/data/data/com.termux/files";
public static final String PREFIX_PATH = FILES_PATH + "/usr";
@ -55,9 +58,12 @@ 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_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. */
/**
* Intent action to launch a new terminal session. Executed from TermuxWidgetProvider.
*/
public static final String ACTION_EXECUTE = "com.termux.service_execute";
public static final String EXTRA_ARGUMENTS = "com.termux.execute.arguments";
@ -65,7 +71,17 @@ public final class TermuxService extends Service implements SessionChangedCallba
public static final String EXTRA_CURRENT_WORKING_DIRECTORY = "com.termux.execute.cwd";
public static final String EXTRA_EXECUTE_IN_BACKGROUND = "com.termux.execute.background";
/** This service is only bound from inside the same process and never uses IPC. */
/*
* APK service intents
* */
private static final String ACTION_INSTALL_PACKAGES = "com.termux.install_packages";
private static final String ACTION_LIST_PACKAGES = "com.termux.list_packages";
private static final String ACTION_UNINSTALL_PACKAGES = "com.termux.uninstall_packages";
/**
* This service is only bound from inside the same process and never uses IPC.
*/
class LocalBinder extends Binder {
public final TermuxService service = TermuxService.this;
}
@ -84,16 +100,25 @@ public final class TermuxService extends Service implements SessionChangedCallba
final List<BackgroundJob> mBackgroundTasks = new ArrayList<>();
/** Note that the service may often outlive the activity, so need to clear this reference. */
/**
* Note that the service may often outlive the activity, so need to clear this reference.
*/
SessionChangedCallback mSessionChangeCallback;
/** The wake lock and wifi lock are always acquired and released together. */
/**
* The wake lock and wifi lock are always acquired and released together.
*/
private PowerManager.WakeLock mWakeLock;
private WifiManager.WifiLock mWifiLock;
/** If the user has executed the {@link #ACTION_STOP_SERVICE} intent. */
/**
* 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 +128,25 @@ public final class TermuxService extends Service implements SessionChangedCallba
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 {
PackageInstaller downloaderTest = new PackageInstaller(this);
downloaderTest.initDownloader(packages);
}
} else if (ACTION_LIST_PACKAGES.equals(action)) {
PackageLister packageLister = new PackageLister(this);
packageLister.listPackages();
} else if (ACTION_UNINSTALL_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 {
PackageUninstaller packageUninstaller = new PackageUninstaller(this);
packageUninstaller.uninstallPackages(packages);
}
} else if (ACTION_LOCK_WAKE.equals(action)) {
if (mWakeLock == null) {
PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
@ -185,11 +229,21 @@ public final class TermuxService extends Service implements SessionChangedCallba
@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. */
/**
* Update the shown foreground service notification after making any changes that affect it.
*/
void updateNotification() {
if (mWakeLock == null && mTerminalSessions.isEmpty() && mBackgroundTasks.isEmpty()) {
// Exit if we are updating after the user disabled all locks with no sessions or tasks running.
@ -254,6 +308,8 @@ public final class TermuxService extends Service implements SessionChangedCallba
@Override
public void onDestroy() {
unregisterReceiver(packageInstaller);
File termuxTmpDir = new File(TermuxService.PREFIX_PATH + "/tmp");
if (termuxTmpDir.exists()) {
@ -313,7 +369,8 @@ public final class TermuxService extends Service implements SessionChangedCallba
String[] args = new String[processArgs.length];
args[0] = processName;
if (processArgs.length > 1) System.arraycopy(processArgs, 1, args, 1, processArgs.length - 1);
if (processArgs.length > 1)
System.arraycopy(processArgs, 1, args, 1, processArgs.length - 1);
TerminalSession session = new TerminalSession(executablePath, cwd, args, env, this);
mTerminalSessions.add(session);
@ -385,7 +442,7 @@ public final class TermuxService extends Service implements SessionChangedCallba
String channelDescription = "Notifications from Termux";
int importance = NotificationManager.IMPORTANCE_LOW;
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName,importance);
NotificationChannel channel = new NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, importance);
channel.setDescription(channelDescription);
NotificationManager manager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
manager.createNotificationChannel(channel);

View File

@ -1,10 +1,14 @@
buildscript {
ext.kotlin_version = '1.4.0'
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}

View File

@ -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

View File

@ -1,5 +1,6 @@
#Sat Aug 15 22:30:46 IST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.5.1-all.zip

68
pkg.sh Normal file
View File

@ -0,0 +1,68 @@
#!/data/data/com.termux/files/usr/bin/bash
set -e -u
# The services can be tested without installing pkg by setting the export flag of Termux Service in the manifest and using adb am
# Example commands -
# adb shell am startservice --user 0 --esa packages openssh,vim -a com.termux.install_packages com.termux/com.termux.app.TermuxService
# adb shell am startservice --user 0 --esa packages openssh,vim -a com.termux.uninstall_packages com.termux/com.termux.app.TermuxService
# adb shell am startservice --user 0 -a com.termux.list_packages com.termux/com.termux.app.TermuxService
show_help() {
echo 'Usage: pkg command [arguments]'
echo
echo 'A tool for managing packages. Commands:'
echo
echo ' install <packages> - Install specified packages'
echo ' uninstall <packages> - Uninstall specified packages'
echo ' list-installed - List installed packages'
echo
}
if [ $# = 0 ]; then
show_help
exit 1
fi
CMD="$1"
shift 1
install_packages() {
local all_packages="$*"
am startservice \
--user 0 \
--esa packages "${all_packages// /,}" \
-a com.termux.install_packages \
com.termux/com.termux.app.TermuxService \
>/dev/null
}
uninstall_packages() {
local all_packages="$*"
am startservice \
--user 0 \
--esa packages "${all_packages// /,}" \
-a com.termux.uninstall_packages \
com.termux/com.termux.app.TermuxService \
>/dev/null
}
list_packages() {
am startservice \
--user 0 \
-a com.termux.list_packages \
com.termux/com.termux.app.TermuxService \
>/dev/null
}
case "$CMD" in
help) show_help;;
install) install_packages "$@";;
uninstall) uninstall_packages "$@";;
list-installed) list_packages;;
*)
echo "Unknown command: '$CMD' (run 'pkg help' for usage information)"
exit 1
;;
esac