mirror of
https://github.com/termux/termux-app
synced 2024-06-14 13:26:39 +00:00
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:
parent
531c32f3c9
commit
f6c3b6f38a
6
.github/workflows/debug_build.yml
vendored
6
.github/workflows/debug_build.yml
vendored
|
@ -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
|
||||
|
|
4
.github/workflows/run_tests.yml
vendored
4
.github/workflows/run_tests.yml
vendored
|
@ -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
|
||||
|
|
139
app/build.gradle
139
app/build.gradle
|
@ -1,6 +1,12 @@
|
|||
import java.security.DigestInputStream
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
|
||||
plugins {
|
||||
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,6 +66,13 @@ 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 {
|
||||
|
@ -82,67 +81,117 @@ 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"
|
||||
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)
|
||||
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()
|
||||
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, "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()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
|
|
@ -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: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"
|
||||
<provider
|
||||
android:name="com.termux.app.TermuxOpenReceiver$ContentProvider"
|
||||
android:authorities="com.termux.files"
|
||||
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"/>
|
||||
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>
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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;
|
||||
}
|
|
@ -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);
|
||||
|
|
297
app/src/main/java/com/termux/app/PackageDownloader.kt
Normal file
297
app/src/main/java/com/termux/app/PackageDownloader.kt
Normal 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)
|
276
app/src/main/java/com/termux/app/PackageInstaller.kt
Normal file
276
app/src/main/java/com/termux/app/PackageInstaller.kt
Normal 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 = "")
|
31
app/src/main/java/com/termux/app/PackageLister.kt
Normal file
31
app/src/main/java/com/termux/app/PackageLister.kt
Normal 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() }
|
||||
}
|
||||
}
|
||||
}
|
40
app/src/main/java/com/termux/app/PackageUninstaller.kt
Normal file
40
app/src/main/java/com/termux/app/PackageUninstaller.kt
Normal 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)
|
||||
}
|
||||
}
|
|
@ -487,7 +487,6 @@ 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();
|
||||
|
@ -499,7 +498,6 @@ public final class TermuxActivity extends Activity implements ServiceConnection
|
|||
} catch (WindowManager.BadTokenException e) {
|
||||
// Activity finished - ignore.
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// The service connected while not in foreground - just bail out.
|
||||
finish();
|
||||
|
|
|
@ -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()) {
|
||||
|
|
135
app/src/main/java/com/termux/app/TermuxPackageInstaller.java
Normal file
135
app/src/main/java/com/termux/app/TermuxPackageInstaller.java
Normal 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);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
3
gradle/wrapper/gradle-wrapper.properties
vendored
3
gradle/wrapper/gradle-wrapper.properties
vendored
|
@ -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
68
pkg.sh
Normal 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
|
Loading…
Reference in New Issue
Block a user