Merge "Improve performance of file copy in PackageInstaller." into udc-dev

This commit is contained in:
Alex Buynytskyy
2023-05-01 18:59:56 +00:00
committed by Android (Google) Code Review
9 changed files with 337 additions and 168 deletions

View File

@@ -2072,6 +2072,25 @@ public class PackageInstaller {
return new InstallInfo(result); return new InstallInfo(result);
} }
/**
* Parse a single APK file passed as an FD to get install relevant information about
* the package wrapped in {@link InstallInfo}.
* @throws PackageParsingException if the package source file(s) provided is(are) not valid,
* or the parser isn't able to parse the supplied source(s).
* @hide
*/
@NonNull
public InstallInfo readInstallInfo(@NonNull ParcelFileDescriptor pfd,
@Nullable String debugPathName, int flags) throws PackageParsingException {
final ParseTypeImpl input = ParseTypeImpl.forDefaultParsing();
final ParseResult<PackageLite> result = ApkLiteParseUtils.parseMonolithicPackageLite(input,
pfd.getFileDescriptor(), debugPathName, flags);
if (result.isError()) {
throw new PackageParsingException(result.getErrorCode(), result.getErrorMessage());
}
return new InstallInfo(result);
}
// (b/239722738) This class serves as a bridge between the PackageLite class, which // (b/239722738) This class serves as a bridge between the PackageLite class, which
// is a hidden class, and the consumers of this class. (e.g. InstallInstalling.java) // is a hidden class, and the consumers of this class. (e.g. InstallInstalling.java)
// This is a part of an effort to remove dependency on hidden APIs and use SystemAPIs or // This is a part of an effort to remove dependency on hidden APIs and use SystemAPIs or
@@ -2125,6 +2144,21 @@ public class PackageInstaller {
public long calculateInstalledSize(@NonNull SessionParams params) throws IOException { public long calculateInstalledSize(@NonNull SessionParams params) throws IOException {
return InstallLocationUtils.calculateInstalledSize(mPkg, params.abiOverride); return InstallLocationUtils.calculateInstalledSize(mPkg, params.abiOverride);
} }
/**
* @param params {@link SessionParams} of the installation
* @param pfd of an APK opened for read
* @return Total disk space occupied by an application after installation.
* Includes the size of the raw APKs, possibly unpacked resources, raw dex metadata files,
* and all relevant native code.
* @throws IOException when size of native binaries cannot be calculated.
* @hide
*/
public long calculateInstalledSize(@NonNull SessionParams params,
@NonNull ParcelFileDescriptor pfd) throws IOException {
return InstallLocationUtils.calculateInstalledSize(mPkg, params.abiOverride,
pfd.getFileDescriptor());
}
} }
/** /**

View File

@@ -109,7 +109,7 @@ public class ApkLiteParseUtils {
} }
/** /**
* Parse lightweight details about a single APK files. * Parse lightweight details about a single APK file.
*/ */
public static ParseResult<PackageLite> parseMonolithicPackageLite(ParseInput input, public static ParseResult<PackageLite> parseMonolithicPackageLite(ParseInput input,
File packageFile, int flags) { File packageFile, int flags) {
@@ -134,6 +134,33 @@ public class ApkLiteParseUtils {
} }
} }
/**
* Parse lightweight details about a single APK file passed as an FD.
*/
public static ParseResult<PackageLite> parseMonolithicPackageLite(ParseInput input,
FileDescriptor packageFd, String debugPathName, int flags) {
Trace.traceBegin(TRACE_TAG_PACKAGE_MANAGER, "parseApkLite");
try {
final ParseResult<ApkLite> result = parseApkLite(input, packageFd, debugPathName,
flags);
if (result.isError()) {
return input.error(result);
}
final ApkLite baseApk = result.getResult();
final String packagePath = debugPathName;
return input.success(
new PackageLite(packagePath, baseApk.getPath(), baseApk, null /* splitNames */,
null /* isFeatureSplits */, null /* usesSplitNames */,
null /* configForSplit */, null /* splitApkPaths */,
null /* splitRevisionCodes */, baseApk.getTargetSdkVersion(),
null /* requiredSplitTypes */, null, /* splitTypes */
baseApk.isAllowUpdateOwnership()));
} finally {
Trace.traceEnd(TRACE_TAG_PACKAGE_MANAGER);
}
}
/** /**
* Parse lightweight details about a directory of APKs. * Parse lightweight details about a directory of APKs.
* *

View File

@@ -16,14 +16,14 @@
package com.android.packageinstaller; package com.android.packageinstaller;
import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_STAGED_SESSION_ID;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.io.File;
/** /**
* Trampoline activity. Calls PackageInstallerActivity and deletes staged install file onResult. * Trampoline activity. Calls PackageInstallerActivity and deletes staged install file onResult.
*/ */
@@ -52,8 +52,13 @@ public class DeleteStagedFileOnResult extends Activity {
super.onDestroy(); super.onDestroy();
if (isFinishing()) { if (isFinishing()) {
File sourceFile = new File(getIntent().getData().getPath()); // While we expect PIA/InstallStaging to abandon/commit the session, still there
new Thread(sourceFile::delete).start(); // might be cases when the session becomes orphan.
int sessionId = getIntent().getIntExtra(EXTRA_STAGED_SESSION_ID, 0);
try {
getPackageManager().getPackageInstaller().abandonSession(sessionId);
} catch (SecurityException ignored) {
}
} }
} }
} }

View File

@@ -16,17 +16,17 @@
package com.android.packageinstaller; package com.android.packageinstaller;
import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_STAGED_SESSION_ID;
import android.app.PendingIntent; import android.app.PendingIntent;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ApplicationInfo; import android.content.pm.ApplicationInfo;
import android.content.pm.PackageInstaller; import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.InstallInfo;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.Process;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.Button; import android.widget.Button;
@@ -34,10 +34,7 @@ import android.widget.Button;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/** /**
* Send package to the package manager and handle results from package manager. Once the * Send package to the package manager and handle results from package manager. Once the
@@ -77,7 +74,7 @@ public class InstallInstalling extends AlertActivity {
.getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO); .getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO);
mPackageURI = getIntent().getData(); mPackageURI = getIntent().getData();
if ("package".equals(mPackageURI.getScheme())) { if (PackageInstallerActivity.SCHEME_PACKAGE.equals(mPackageURI.getScheme())) {
try { try {
getPackageManager().installExistingPackage(appInfo.packageName); getPackageManager().installExistingPackage(appInfo.packageName);
launchSuccess(); launchSuccess();
@@ -86,6 +83,8 @@ public class InstallInstalling extends AlertActivity {
PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null); PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null);
} }
} else { } else {
// ContentResolver.SCHEME_FILE
// STAGED_SESSION_ID extra contains an ID of a previously staged install session.
final File sourceFile = new File(mPackageURI.getPath()); final File sourceFile = new File(mPackageURI.getPath());
PackageUtil.AppSnippet as = PackageUtil.getAppSnippet(this, appInfo, sourceFile); PackageUtil.AppSnippet as = PackageUtil.getAppSnippet(this, appInfo, sourceFile);
@@ -122,41 +121,6 @@ public class InstallInstalling extends AlertActivity {
// Does not happen // Does not happen
} }
} else { } else {
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
final Uri referrerUri = getIntent().getParcelableExtra(Intent.EXTRA_REFERRER);
params.setPackageSource(
referrerUri != null ? PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE
: PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE);
params.setInstallAsInstantApp(false);
params.setReferrerUri(referrerUri);
params.setOriginatingUri(getIntent()
.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI));
params.setOriginatingUid(getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID,
Process.INVALID_UID));
params.setInstallerPackageName(getIntent().getStringExtra(
Intent.EXTRA_INSTALLER_PACKAGE_NAME));
params.setInstallReason(PackageManager.INSTALL_REASON_USER);
File file = new File(mPackageURI.getPath());
try {
final InstallInfo result = getPackageManager().getPackageInstaller()
.readInstallInfo(file, 0);
params.setAppPackageName(result.getPackageName());
params.setInstallLocation(result.getInstallLocation());
try {
params.setSize(result.calculateInstalledSize(params));
} catch (IOException e) {
e.printStackTrace();
params.setSize(file.length());
}
} catch (PackageInstaller.PackageParsingException e) {
Log.e(LOG_TAG, "Cannot parse package " + file + ". Assuming defaults.", e);
Log.e(LOG_TAG,
"Cannot calculate installed size " + file + ". Try only apk size.");
params.setSize(file.length());
}
try { try {
mInstallId = InstallEventReceiver mInstallId = InstallEventReceiver
.addObserver(this, EventResultPersister.GENERATE_NEW_ID, .addObserver(this, EventResultPersister.GENERATE_NEW_ID,
@@ -166,9 +130,14 @@ public class InstallInstalling extends AlertActivity {
PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null); PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null);
} }
try { mSessionId = getIntent().getIntExtra(EXTRA_STAGED_SESSION_ID, 0);
mSessionId = getPackageManager().getPackageInstaller().createSession(params); // Try to open session previously staged in InstallStaging.
} catch (IOException e) { try (PackageInstaller.Session ignored =
getPackageManager().getPackageInstaller().openSession(
mSessionId)) {
Log.d(LOG_TAG, "Staged session is valid, proceeding with the install");
} catch (IOException | SecurityException e) {
Log.e(LOG_TAG, "Invalid session id passed", e);
launchFailure(PackageInstaller.STATUS_FAILURE, launchFailure(PackageInstaller.STATUS_FAILURE,
PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null); PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null);
} }
@@ -293,57 +262,9 @@ public class InstallInstalling extends AlertActivity {
@Override @Override
protected PackageInstaller.Session doInBackground(Void... params) { protected PackageInstaller.Session doInBackground(Void... params) {
PackageInstaller.Session session;
try { try {
session = getPackageManager().getPackageInstaller().openSession(mSessionId); return getPackageManager().getPackageInstaller().openSession(mSessionId);
} catch (IOException e) { } catch (IOException e) {
synchronized (this) {
isDone = true;
notifyAll();
}
return null;
}
session.setStagingProgress(0);
try {
File file = new File(mPackageURI.getPath());
try (InputStream in = new FileInputStream(file)) {
long sizeBytes = file.length();
long totalRead = 0;
try (OutputStream out = session
.openWrite("PackageInstaller", 0, sizeBytes)) {
byte[] buffer = new byte[1024 * 1024];
while (true) {
int numRead = in.read(buffer);
if (numRead == -1) {
session.fsync(out);
break;
}
if (isCancelled()) {
session.close();
break;
}
out.write(buffer, 0, numRead);
if (sizeBytes > 0) {
totalRead += numRead;
float fraction = ((float) totalRead / (float) sizeBytes);
session.setStagingProgress(fraction);
}
}
}
}
return session;
} catch (IOException | SecurityException e) {
Log.e(LOG_TAG, "Could not write package", e);
session.close();
return null; return null;
} finally { } finally {
synchronized (this) { synchronized (this) {

View File

@@ -16,6 +16,10 @@
package com.android.packageinstaller; package com.android.packageinstaller;
import static android.content.res.AssetFileDescriptor.UNKNOWN_LENGTH;
import static com.android.packageinstaller.PackageInstallerActivity.EXTRA_STAGED_SESSION_ID;
import android.app.Activity; import android.app.Activity;
import android.app.AlertDialog; import android.app.AlertDialog;
import android.app.Dialog; import android.app.Dialog;
@@ -23,40 +27,49 @@ import android.app.DialogFragment;
import android.content.Context; import android.content.Context;
import android.content.DialogInterface; import android.content.DialogInterface;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager; import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.net.Uri; import android.net.Uri;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Bundle; import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.util.Log; import android.util.Log;
import android.view.View; import android.view.View;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.io.File; import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
/** /**
* If a package gets installed from an content URI this step loads the package and turns it into * If a package gets installed from a content URI this step stages the installation session
* and installation from a file. Then it re-starts the installation as usual. * reading bytes from the URI.
*/ */
public class InstallStaging extends AlertActivity { public class InstallStaging extends AlertActivity {
private static final String LOG_TAG = InstallStaging.class.getSimpleName(); private static final String LOG_TAG = InstallStaging.class.getSimpleName();
private static final String STAGED_FILE = "STAGED_FILE"; private static final String STAGED_SESSION_ID = "STAGED_SESSION_ID";
private @Nullable PackageInstaller mInstaller;
/** Currently running task that loads the file from the content URI into a file */ /** Currently running task that loads the file from the content URI into a file */
private @Nullable StagingAsyncTask mStagingTask; private @Nullable StagingAsyncTask mStagingTask;
/** The file the package is in */ /** The session the package is in */
private @Nullable File mStagedFile; private int mStagedSessionId;
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
mInstaller = getPackageManager().getPackageInstaller();
setFinishOnTouchOutside(true); setFinishOnTouchOutside(true);
mAlert.setIcon(R.drawable.ic_file_download); mAlert.setIcon(R.drawable.ic_file_download);
mAlert.setTitle(getString(R.string.app_name_unknown)); mAlert.setTitle(getString(R.string.app_name_unknown));
@@ -66,6 +79,9 @@ public class InstallStaging extends AlertActivity {
if (mStagingTask != null) { if (mStagingTask != null) {
mStagingTask.cancel(true); mStagingTask.cancel(true);
} }
cleanupStagingSession();
setResult(RESULT_CANCELED); setResult(RESULT_CANCELED);
finish(); finish();
}, null); }, null);
@@ -73,11 +89,7 @@ public class InstallStaging extends AlertActivity {
requireViewById(R.id.staging).setVisibility(View.VISIBLE); requireViewById(R.id.staging).setVisibility(View.VISIBLE);
if (savedInstanceState != null) { if (savedInstanceState != null) {
mStagedFile = new File(savedInstanceState.getString(STAGED_FILE)); mStagedSessionId = savedInstanceState.getInt(STAGED_SESSION_ID, 0);
if (!mStagedFile.exists()) {
mStagedFile = null;
}
} }
} }
@@ -85,21 +97,41 @@ public class InstallStaging extends AlertActivity {
protected void onResume() { protected void onResume() {
super.onResume(); super.onResume();
// This is the first onResume in a single life of the activity // This is the first onResume in a single life of the activity.
if (mStagingTask == null) { if (mStagingTask == null) {
// File does not exist, or became invalid if (mStagedSessionId > 0) {
if (mStagedFile == null) { final PackageInstaller.SessionInfo info = mInstaller.getSessionInfo(
// Create file delayed to be able to show error mStagedSessionId);
if (info == null || !info.isActive() || info.getResolvedBaseApkPath() == null) {
Log.w(LOG_TAG, "Session " + mStagedSessionId + " in funky state; ignoring");
if (info != null) {
cleanupStagingSession();
}
mStagedSessionId = 0;
}
}
// Session does not exist, or became invalid.
if (mStagedSessionId <= 0) {
// Create session here to be able to show error.
final Uri packageUri = getIntent().getData();
final AssetFileDescriptor afd = openAssetFileDescriptor(packageUri);
try { try {
mStagedFile = TemporaryFileManager.getStagedFile(this); ParcelFileDescriptor pfd = afd != null ? afd.getParcelFileDescriptor() : null;
PackageInstaller.SessionParams params = createSessionParams(
mInstaller, getIntent(), pfd, packageUri.toString());
mStagedSessionId = mInstaller.createSession(params);
} catch (IOException e) { } catch (IOException e) {
Log.w(LOG_TAG, "Failed to create a staging session", e);
showError(); showError();
return; return;
} finally {
PackageUtil.safeClose(afd);
} }
} }
mStagingTask = new StagingAsyncTask(); mStagingTask = new StagingAsyncTask();
mStagingTask.execute(getIntent().getData()); mStagingTask.execute();
} }
} }
@@ -107,7 +139,7 @@ public class InstallStaging extends AlertActivity {
protected void onSaveInstanceState(Bundle outState) { protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putString(STAGED_FILE, mStagedFile.getPath()); outState.putInt(STAGED_SESSION_ID, mStagedSessionId);
} }
@Override @Override
@@ -119,6 +151,65 @@ public class InstallStaging extends AlertActivity {
super.onDestroy(); super.onDestroy();
} }
private AssetFileDescriptor openAssetFileDescriptor(Uri uri) {
try {
return getContentResolver().openAssetFileDescriptor(uri, "r");
} catch (Exception e) {
Log.w(LOG_TAG, "Failed to open asset file descriptor", e);
return null;
}
}
private static PackageInstaller.SessionParams createSessionParams(
@NonNull PackageInstaller installer, @NonNull Intent intent,
@Nullable ParcelFileDescriptor pfd, @NonNull String debugPathName) {
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
final Uri referrerUri = intent.getParcelableExtra(Intent.EXTRA_REFERRER);
params.setPackageSource(
referrerUri != null ? PackageInstaller.PACKAGE_SOURCE_DOWNLOADED_FILE
: PackageInstaller.PACKAGE_SOURCE_LOCAL_FILE);
params.setInstallAsInstantApp(false);
params.setReferrerUri(referrerUri);
params.setOriginatingUri(intent
.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI));
params.setOriginatingUid(intent.getIntExtra(Intent.EXTRA_ORIGINATING_UID,
Process.INVALID_UID));
params.setInstallerPackageName(intent.getStringExtra(
Intent.EXTRA_INSTALLER_PACKAGE_NAME));
params.setInstallReason(PackageManager.INSTALL_REASON_USER);
if (pfd != null) {
try {
final PackageInstaller.InstallInfo result = installer.readInstallInfo(pfd,
debugPathName, 0);
params.setAppPackageName(result.getPackageName());
params.setInstallLocation(result.getInstallLocation());
params.setSize(result.calculateInstalledSize(params, pfd));
} catch (PackageInstaller.PackageParsingException | IOException e) {
Log.e(LOG_TAG, "Cannot parse package " + debugPathName + ". Assuming defaults.", e);
Log.e(LOG_TAG,
"Cannot calculate installed size " + debugPathName
+ ". Try only apk size.");
params.setSize(pfd.getStatSize());
}
} else {
Log.e(LOG_TAG, "Cannot parse package " + debugPathName + ". Assuming defaults.");
}
return params;
}
private void cleanupStagingSession() {
if (mStagedSessionId > 0) {
try {
mInstaller.abandonSession(mStagedSessionId);
} catch (SecurityException ignored) {
}
mStagedSessionId = 0;
}
}
/** /**
* Show an error message and set result as error. * Show an error message and set result as error.
*/ */
@@ -165,58 +256,109 @@ public class InstallStaging extends AlertActivity {
} }
} }
private final class StagingAsyncTask extends AsyncTask<Uri, Void, Boolean> { private final class StagingAsyncTask extends
@Override AsyncTask<Void, Integer, PackageInstaller.SessionInfo> {
protected Boolean doInBackground(Uri... params) { private ProgressBar mProgressBar = null;
if (params == null || params.length <= 0) {
return false;
}
Uri packageUri = params[0];
try (InputStream in = getContentResolver().openInputStream(packageUri)) {
// Despite the comments in ContentResolver#openInputStream the returned stream can
// be null.
if (in == null) {
return false;
}
try (OutputStream out = new FileOutputStream(mStagedFile)) { private long getContentSizeBytes() {
byte[] buffer = new byte[1024 * 1024]; try (AssetFileDescriptor afd = openAssetFileDescriptor(getIntent().getData())) {
int bytesRead; return afd != null ? afd.getLength() : UNKNOWN_LENGTH;
while ((bytesRead = in.read(buffer)) >= 0) { } catch (IOException ignored) {
// Be nice and respond to a cancellation return UNKNOWN_LENGTH;
if (isCancelled()) {
return false;
}
out.write(buffer, 0, bytesRead);
}
}
} catch (IOException | SecurityException | IllegalStateException
| IllegalArgumentException e) {
Log.w(LOG_TAG, "Error staging apk from content URI", e);
return false;
} }
return true;
} }
@Override @Override
protected void onPostExecute(Boolean success) { protected void onPreExecute() {
if (success) { final long sizeBytes = getContentSizeBytes();
// Now start the installation again from a file
Intent installIntent = new Intent(getIntent());
installIntent.setClass(InstallStaging.this, DeleteStagedFileOnResult.class);
installIntent.setData(Uri.fromFile(mStagedFile));
if (installIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) { mProgressBar = sizeBytes > 0 ? requireViewById(R.id.progress_indeterminate) : null;
installIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); if (mProgressBar != null) {
mProgressBar.setProgress(0);
mProgressBar.setMax(100);
mProgressBar.setIndeterminate(false);
}
}
@Override
protected PackageInstaller.SessionInfo doInBackground(Void... params) {
Uri packageUri = getIntent().getData();
try (PackageInstaller.Session session = mInstaller.openSession(mStagedSessionId);
InputStream in = getContentResolver().openInputStream(packageUri)) {
session.setStagingProgress(0);
if (in == null) {
return null;
} }
installIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); long sizeBytes = getContentSizeBytes();
startActivity(installIntent);
InstallStaging.this.finish(); long totalRead = 0;
} else { try (OutputStream out = session.openWrite("PackageInstaller", 0, sizeBytes)) {
showError(); byte[] buffer = new byte[1024 * 1024];
while (true) {
int numRead = in.read(buffer);
if (numRead == -1) {
session.fsync(out);
break;
}
if (isCancelled()) {
break;
}
out.write(buffer, 0, numRead);
if (sizeBytes > 0) {
totalRead += numRead;
float fraction = ((float) totalRead / (float) sizeBytes);
session.setStagingProgress(fraction);
publishProgress((int) (fraction * 100.0));
}
}
}
return mInstaller.getSessionInfo(mStagedSessionId);
} catch (IOException | SecurityException | IllegalStateException
| IllegalArgumentException e) {
Log.w(LOG_TAG, "Error staging apk from content URI", e);
return null;
} }
} }
@Override
protected void onProgressUpdate(Integer... progress) {
if (mProgressBar != null && progress != null && progress.length > 0) {
mProgressBar.setProgress(progress[0], true);
}
}
@Override
protected void onPostExecute(PackageInstaller.SessionInfo sessionInfo) {
if (sessionInfo == null || !sessionInfo.isActive()
|| sessionInfo.getResolvedBaseApkPath() == null) {
Log.w(LOG_TAG, "Session info is invalid: " + sessionInfo);
cleanupStagingSession();
showError();
return;
}
// Pass the staged session to the installer.
Intent installIntent = new Intent(getIntent());
installIntent.setClass(InstallStaging.this, DeleteStagedFileOnResult.class);
installIntent.setData(Uri.fromFile(new File(sessionInfo.getResolvedBaseApkPath())));
installIntent.putExtra(EXTRA_STAGED_SESSION_ID, mStagedSessionId);
if (installIntent.getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
installIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
}
installIntent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
startActivity(installIntent);
InstallStaging.this.finish();
}
} }
} }

View File

@@ -148,10 +148,11 @@ public class InstallStart extends Activity {
// [IMPORTANT] This path is deprecated, but should still work. Only necessary // [IMPORTANT] This path is deprecated, but should still work. Only necessary
// features should be added. // features should be added.
// Copy file to prevent it from being changed underneath this process // Stage a session with this file to prevent it from being changed underneath
// this process.
nextActivity.setClass(this, InstallStaging.class); nextActivity.setClass(this, InstallStaging.class);
} else if (packageUri != null && packageUri.getScheme().equals( } else if (packageUri != null && PackageInstallerActivity.SCHEME_PACKAGE.equals(
PackageInstallerActivity.SCHEME_PACKAGE)) { packageUri.getScheme())) {
nextActivity.setClass(this, PackageInstallerActivity.class); nextActivity.setClass(this, PackageInstallerActivity.class);
} else { } else {
Intent result = new Intent(); Intent result = new Intent();

View File

@@ -82,6 +82,7 @@ public class PackageInstallerActivity extends AlertActivity {
static final String EXTRA_CALLING_PACKAGE = "EXTRA_CALLING_PACKAGE"; static final String EXTRA_CALLING_PACKAGE = "EXTRA_CALLING_PACKAGE";
static final String EXTRA_CALLING_ATTRIBUTION_TAG = "EXTRA_CALLING_ATTRIBUTION_TAG"; static final String EXTRA_CALLING_ATTRIBUTION_TAG = "EXTRA_CALLING_ATTRIBUTION_TAG";
static final String EXTRA_ORIGINAL_SOURCE_INFO = "EXTRA_ORIGINAL_SOURCE_INFO"; static final String EXTRA_ORIGINAL_SOURCE_INFO = "EXTRA_ORIGINAL_SOURCE_INFO";
static final String EXTRA_STAGED_SESSION_ID = "EXTRA_STAGED_SESSION_ID";
private static final String ALLOW_UNKNOWN_SOURCES_KEY = private static final String ALLOW_UNKNOWN_SOURCES_KEY =
PackageInstallerActivity.class.getName() + "ALLOW_UNKNOWN_SOURCES_KEY"; PackageInstallerActivity.class.getName() + "ALLOW_UNKNOWN_SOURCES_KEY";
@@ -403,6 +404,10 @@ public class PackageInstallerActivity extends AlertActivity {
mReferrerURI = null; mReferrerURI = null;
mPendingUserActionReason = info.getPendingUserActionReason(); mPendingUserActionReason = info.getPendingUserActionReason();
} else { } else {
// Two possible callers:
// 1. InstallStart with "SCHEME_PACKAGE".
// 2. InstallStaging with "SCHEME_FILE" and EXTRA_STAGED_SESSION_ID with staged
// session id.
mSessionId = -1; mSessionId = -1;
packageSource = intent.getData(); packageSource = intent.getData();
mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI); mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);
@@ -721,14 +726,16 @@ public class PackageInstallerActivity extends AlertActivity {
} }
private void startInstall() { private void startInstall() {
String installerPackageName = getIntent().getStringExtra(
Intent.EXTRA_INSTALLER_PACKAGE_NAME);
int stagedSessionId = getIntent().getIntExtra(EXTRA_STAGED_SESSION_ID, 0);
// Start subactivity to actually install the application // Start subactivity to actually install the application
Intent newIntent = new Intent(); Intent newIntent = new Intent();
newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO, newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO,
mPkgInfo.applicationInfo); mPkgInfo.applicationInfo);
newIntent.setData(mPackageURI); newIntent.setData(mPackageURI);
newIntent.setClass(this, InstallInstalling.class); newIntent.setClass(this, InstallInstalling.class);
String installerPackageName = getIntent().getStringExtra(
Intent.EXTRA_INSTALLER_PACKAGE_NAME);
if (mOriginatingURI != null) { if (mOriginatingURI != null) {
newIntent.putExtra(Intent.EXTRA_ORIGINATING_URI, mOriginatingURI); newIntent.putExtra(Intent.EXTRA_ORIGINATING_URI, mOriginatingURI);
} }
@@ -745,6 +752,9 @@ public class PackageInstallerActivity extends AlertActivity {
if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) { if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {
newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true); newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);
} }
if (stagedSessionId > 0) {
newIntent.putExtra(EXTRA_STAGED_SESSION_ID, stagedSessionId);
}
newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);
if (mLocalLOGV) Log.i(TAG, "downloaded app uri=" + mPackageURI); if (mLocalLOGV) Log.i(TAG, "downloaded app uri=" + mPackageURI);
startActivity(newIntent); startActivity(newIntent);

View File

@@ -33,7 +33,9 @@ import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.io.Closeable;
import java.io.File; import java.io.File;
import java.io.IOException;
/** /**
* This is a utility class for defining some utility methods and constants * This is a utility class for defining some utility methods and constants
@@ -190,4 +192,19 @@ public class PackageUtil {
} }
return targetSdkVersion; return targetSdkVersion;
} }
/**
* Quietly close a closeable resource (e.g. a stream or file). The input may already
* be closed and it may even be null.
*/
static void safeClose(Closeable resource) {
if (resource != null) {
try {
resource.close();
} catch (IOException ioe) {
// Catch and discard the error
}
}
}
} }

View File

@@ -1148,12 +1148,21 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
info.userId = userId; info.userId = userId;
info.installerPackageName = mInstallSource.mInstallerPackageName; info.installerPackageName = mInstallSource.mInstallerPackageName;
info.installerAttributionTag = mInstallSource.mInstallerAttributionTag; info.installerAttributionTag = mInstallSource.mInstallerAttributionTag;
info.resolvedBaseCodePath = null;
if (mContext.checkCallingOrSelfPermission( if (mContext.checkCallingOrSelfPermission(
Manifest.permission.READ_INSTALLED_SESSION_PATHS) Manifest.permission.READ_INSTALLED_SESSION_PATHS)
== PackageManager.PERMISSION_GRANTED && mResolvedBaseFile != null) { == PackageManager.PERMISSION_GRANTED) {
info.resolvedBaseCodePath = mResolvedBaseFile.getAbsolutePath(); File file = mResolvedBaseFile;
} else { if (file == null) {
info.resolvedBaseCodePath = null; // Try to guess mResolvedBaseFile file.
final List<File> addedFiles = getAddedApksLocked();
if (addedFiles.size() > 0) {
file = addedFiles.get(0);
}
}
if (file != null) {
info.resolvedBaseCodePath = file.getAbsolutePath();
}
} }
info.progress = progress; info.progress = progress;
info.sealed = mSealed; info.sealed = mSealed;
@@ -1355,9 +1364,12 @@ public class PackageInstallerSession extends IPackageInstallerSession.Stub {
@GuardedBy("mLock") @GuardedBy("mLock")
private String[] getStageDirContentsLocked() { private String[] getStageDirContentsLocked() {
if (stageDir == null) {
return EmptyArray.STRING;
}
String[] result = stageDir.list(); String[] result = stageDir.list();
if (result == null) { if (result == null) {
result = EmptyArray.STRING; return EmptyArray.STRING;
} }
return result; return result;
} }