Skip to content

Commit

Permalink
+SAI can now open .apks files (renamed zips) and install APKs from them
Browse files Browse the repository at this point in the history
*rewrote apks supplying to installers scheme
  • Loading branch information
Aefyr committed Mar 18, 2019
1 parent 84e95ff commit c295f29
Show file tree
Hide file tree
Showing 19 changed files with 565 additions and 198 deletions.
10 changes: 5 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ android {
applicationId "com.aefyr.sai"
minSdkVersion 21
targetSdkVersion 28
versionCode 17
versionName "1.16"
versionCode 18
versionName "1.17"
}
buildTypes {
release {
Expand All @@ -24,13 +24,13 @@ android {

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])
implementation 'androidx.appcompat:appcompat:1.1.0-alpha02'
implementation 'androidx.appcompat:appcompat:1.1.0-alpha03'
implementation 'com.google.android.material:material:1.1.0-alpha04'
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
implementation 'com.android.support:preference-v7:28.0.0'
implementation 'com.google.firebase:firebase-core:16.0.7'
implementation 'com.google.firebase:firebase-core:16.0.8'
implementation 'com.crashlytics.sdk.android:crashlytics:2.9.9'
implementation 'com.github.aefyr:pseudoapksigner:1.1'
implementation 'com.github.aefyr:pseudoapksigner:1.2'
}

apply plugin: 'com.google.gms.google-services'
25 changes: 22 additions & 3 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,38 @@
android:roundIcon="@mipmap/icon"
android:theme="@style/AppTheme.Light"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".ui.activities.MainActivity">
<activity
android:name=".ui.activities.MainActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>

<intent-filter>
<action android:name="android.intent.action.VIEW" />

<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />

<data
android:scheme="file"
android:mimeType="*/*"
android:host="*"
android:pathPattern=".*\\.apks" />
<data
android:scheme="content"
android:mimeType="*/*"
android:host="*"
android:pathPattern=".*\\.apks" />
</intent-filter>
</activity>

<service android:name=".installer.rootless.RootlessSAIPIService" />

<activity android:name=".ui.activities.PreferencesActivity" />

<activity android:name=".ui.activities.AboutActivity"/>
<activity android:name=".ui.activities.AboutActivity" />
</application>

</manifest>
132 changes: 8 additions & 124 deletions app/src/main/java/com/aefyr/sai/installer/QueuedInstallation.java
Original file line number Diff line number Diff line change
@@ -1,147 +1,31 @@
package com.aefyr.sai.installer;

import android.content.Context;
import android.util.Log;

import com.aefyr.pseudoapksigner.PseudoApkSigner;
import com.aefyr.sai.R;
import com.aefyr.sai.utils.IOUtils;
import com.aefyr.sai.model.apksource.ApkSource;
import com.aefyr.sai.model.apksource.SignerApkSource;
import com.aefyr.sai.utils.PreferencesHelper;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

class QueuedInstallation {
private static final String TAG = "SAIQueuedInstallation";
private static final String FILE_NAME_PAST = "testkey.past";
private static final String FILE_NAME_PRIVATE_KEY = "testkey.pk8";

private Context mContext;
private File mZipWithApkFiles;
private boolean mShouldExtractZip;
private List<File> mApkFiles;
private File mCacheDirectory;
private ApkSource mApkSource;
private long mId;

QueuedInstallation(Context c, List<File> apkFiles, long id) {
mContext = c;
mApkFiles = apkFiles;
mId = id;
}

QueuedInstallation(Context c, File zipWithApkFiles, long id) {
QueuedInstallation(Context c, ApkSource apkSource, long id) {
mContext = c;
mZipWithApkFiles = zipWithApkFiles;
mShouldExtractZip = true;
mApkSource = apkSource;
mId = id;
}

long getId() {
return mId;
}

List<File> getApkFiles() throws Exception {
if (mShouldExtractZip)
extractZip();

ApkSource getApkSource() {
if (PreferencesHelper.getInstance(mContext).shouldSignApks())
signApks();

return mApkFiles;
}

void clear() {
if (mCacheDirectory != null) {
deleteFile(mCacheDirectory);
}
}

private void deleteFile(File f) {
if (f.isDirectory()) {
for (File child : f.listFiles())
deleteFile(child);
}
f.delete();
}

private void extractZip() throws Exception {
createCacheDir();

ZipFile zipFile = new ZipFile(mZipWithApkFiles);
Enumeration<? extends ZipEntry> entries = zipFile.entries();
mApkFiles = new ArrayList<>(zipFile.size());

while (entries.hasMoreElements()) {
ZipEntry entry = entries.nextElement();

if (entry.isDirectory() || !entry.getName().endsWith(".apk"))
throw new IllegalArgumentException(mContext.getString(R.string.installer_error_zip_contains_non_apks));


File tempApkFile = new File(mCacheDirectory, entry.getName());

FileOutputStream outputStream = new FileOutputStream(tempApkFile);
InputStream inputStream = zipFile.getInputStream(entry);
IOUtils.copyStream(inputStream, outputStream);

outputStream.close();
inputStream.close();

mApkFiles.add(tempApkFile);
}
zipFile.close();
}

private void signApks() throws Exception {
if (mCacheDirectory == null)
createCacheDir();

checkAndPrepareSigningEnvironment();

ArrayList<File> originalApkFiles = new ArrayList<>(mApkFiles);
mApkFiles.clear();

PseudoApkSigner apkSigner = new PseudoApkSigner(new File(getSigningEnvironmentDir(), FILE_NAME_PAST), new File(getSigningEnvironmentDir(), FILE_NAME_PRIVATE_KEY));
for (File apkFile : originalApkFiles) {
String rawFileName = apkFile.getName();
int indexOfLastDot = rawFileName.lastIndexOf('.');
String fileName = rawFileName.substring(0, indexOfLastDot);
String fileExtension = rawFileName.substring(indexOfLastDot + 1);

File signedApkFile = new File(mCacheDirectory, String.format("%s_signed.%s", fileName, fileExtension));
apkSigner.sign(apkFile, signedApkFile);

mApkFiles.add(signedApkFile);
}
}

private void checkAndPrepareSigningEnvironment() throws Exception {
File signingEnvironment = getSigningEnvironmentDir();
File pastFile = new File(signingEnvironment, FILE_NAME_PAST);
File privateKeyFile = new File(signingEnvironment, FILE_NAME_PRIVATE_KEY);

if (pastFile.exists() && privateKeyFile.exists())
return;

Log.d(TAG, "Preparing signing environment...");
signingEnvironment.mkdir();

IOUtils.copyFileFromAssets(mContext, FILE_NAME_PAST, pastFile);
IOUtils.copyFileFromAssets(mContext, FILE_NAME_PRIVATE_KEY, privateKeyFile);
}

private File getSigningEnvironmentDir() {
return new File(mContext.getFilesDir(), "signing");
}
return new SignerApkSource(mContext, mApkSource);

private void createCacheDir() {
mCacheDirectory = new File(mContext.getCacheDir(), String.valueOf(System.currentTimeMillis()));
mCacheDirectory.mkdirs();
return mApkSource;
}
}
34 changes: 5 additions & 29 deletions app/src/main/java/com/aefyr/sai/installer/SAIPackageInstaller.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import android.util.LongSparseArray;

import com.aefyr.sai.R;
import com.aefyr.sai.model.apksource.ApkSource;

import java.io.File;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

Expand Down Expand Up @@ -57,15 +54,9 @@ public void removeStatusListener(InstallationStatusListener listener) {
mListeners.remove(listener);
}

public long createInstallationSession(List<File> apkFiles) {
public long createInstallationSession(ApkSource apkSource) {
long installationID = mLastInstallationID++;
mCreatedInstallationSessions.put(installationID, new QueuedInstallation(getContext(), apkFiles, installationID));
return installationID;
}

public long createInstallationSession(File zipWithApkFiles) {
long installationID = mLastInstallationID++;
mCreatedInstallationSessions.put(installationID, new QueuedInstallation(getContext(), zipWithApkFiles, installationID));
mCreatedInstallationSessions.put(installationID, new QueuedInstallation(getContext(), apkSource, installationID));
return installationID;
}

Expand Down Expand Up @@ -94,28 +85,13 @@ private void processQueue() {

dispatchCurrentSessionUpdate(InstallationStatus.INSTALLING, null);

mExecutor.execute(() -> {

List<File> apkFiles;
try {
apkFiles = installation.getApkFiles();
} catch (Exception e) {
Log.w(TAG, e);
dispatchCurrentSessionUpdate(InstallationStatus.INSTALLATION_FAILED, getContext().getString(R.string.installer_error_while_extracting, e.getMessage()));
installationCompleted();
return;
}

installApkFiles(apkFiles);
});
mExecutor.execute(() -> installApkFiles(installation.getApkSource()));
}

protected abstract void installApkFiles(List<File> apkFiles);
protected abstract void installApkFiles(ApkSource apkSource);

protected void installationCompleted() {
mInstallationInProgress = false;
QueuedInstallation lastInstallation = mOngoingInstallation;
mMiscExecutor.submit(lastInstallation::clear);
mOngoingInstallation = null;
processQueue();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

import com.aefyr.sai.R;
import com.aefyr.sai.installer.SAIPackageInstaller;
import com.aefyr.sai.model.apksource.ApkSource;
import com.aefyr.sai.utils.Root;

import java.io.File;
import java.io.FileInputStream;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand All @@ -32,7 +32,7 @@ private RootedSAIPackageInstaller(Context c) {

@SuppressLint("DefaultLocale")
@Override
protected void installApkFiles(List<File> apkFiles) {
protected void installApkFiles(ApkSource apkSource) {
try {
if (!Root.requestRoot()) {
//I don't know if this can even happen, because InstallerViewModel calls PackageInstallerProvider.getInstaller, which checks root access and returns correct installer in response, before every installation
Expand All @@ -41,22 +41,19 @@ protected void installApkFiles(List<File> apkFiles) {
return;
}

int totalSize = 0;
for (File apkFile : apkFiles)
totalSize += apkFile.length();

String result = ensureCommandSucceeded(Root.exec(String.format("pm install-create -r -S %d", totalSize)));
String result = ensureCommandSucceeded(Root.exec("pm install-create -r"));
Pattern sessionIdPattern = Pattern.compile("(\\d+)");
Matcher sessionIdMatcher = sessionIdPattern.matcher(result);
sessionIdMatcher.find();
int sessionId = Integer.parseInt(sessionIdMatcher.group(1));

for (File apkFile : apkFiles)
ensureCommandSucceeded(Root.exec(String.format("pm install-write -S %d %d \"%s\"", apkFile.length(), sessionId, apkFile.getName()), new FileInputStream(apkFile)));
while (apkSource.nextApk())
ensureCommandSucceeded(Root.exec(String.format("pm install-write -S %d %d \"%s\"", apkSource.getApkLength(), sessionId, apkSource.getApkName()), apkSource.openApkInputStream()));


result = ensureCommandSucceeded(Root.exec(String.format("pm install-commit %d ", sessionId)));
if (result.toLowerCase().contains("success"))
dispatchCurrentSessionUpdate(InstallationStatus.INSTALLATION_SUCCEED, getPackageNameFromApk(apkFiles));
dispatchCurrentSessionUpdate(InstallationStatus.INSTALLATION_SUCCEED, "null");
else
dispatchCurrentSessionUpdate(InstallationStatus.INSTALLATION_FAILED, getContext().getString(R.string.installer_error_root, result));

Expand All @@ -70,7 +67,7 @@ protected void installApkFiles(List<File> apkFiles) {

private String ensureCommandSucceeded(Root.Result result) {
if (!result.isSuccessful())
throw new RuntimeException(result.err);
throw new RuntimeException(result.toString());
return result.out;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,13 @@
import android.content.pm.PackageInstaller;
import android.util.Log;

import com.aefyr.sai.R;
import com.aefyr.sai.installer.SAIPackageInstaller;
import com.aefyr.sai.model.apksource.ApkSource;
import com.aefyr.sai.utils.IOUtils;

import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;

public class RootlessSAIPackageInstaller extends SAIPackageInstaller {
private static final String TAG = "RootlessSAIPI";
Expand Down Expand Up @@ -51,16 +50,16 @@ private RootlessSAIPackageInstaller(Context c) {
}

@Override
protected void installApkFiles(List<File> apkFiles) {
protected void installApkFiles(ApkSource apkSource) {
PackageInstaller packageInstaller = getContext().getPackageManager().getPackageInstaller();
try {
PackageInstaller.SessionParams sessionParams = new PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL);
int sessionID = packageInstaller.createSession(sessionParams);

PackageInstaller.Session session = packageInstaller.openSession(sessionID);
for (File apkFile : apkFiles) {
InputStream inputStream = new FileInputStream(apkFile);
OutputStream outputStream = session.openWrite(apkFile.getName(), 0, apkFile.length());
while (apkSource.nextApk()) {
InputStream inputStream = apkSource.openApkInputStream();
OutputStream outputStream = session.openWrite(apkSource.getApkName(), 0, apkSource.getApkLength());
IOUtils.copyStream(inputStream, outputStream);
session.fsync(outputStream);
inputStream.close();
Expand All @@ -73,7 +72,7 @@ protected void installApkFiles(List<File> apkFiles) {
session.close();
} catch (Exception e) {
Log.w(TAG, e);
dispatchCurrentSessionUpdate(SAIPackageInstaller.InstallationStatus.INSTALLATION_FAILED, null);
dispatchCurrentSessionUpdate(SAIPackageInstaller.InstallationStatus.INSTALLATION_FAILED, getContext().getString(R.string.installer_error_rootless, e.getMessage()));
installationCompleted();
}
}
Expand Down
Loading

0 comments on commit c295f29

Please sign in to comment.