blob: 483fb8c451ae73016b565d9d848d7692d010b612 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
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.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageManager;
import android.content.res.AssetFileDescriptor;
import android.Manifest;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Bundle;
import android.os.ParcelFileDescriptor;
import android.os.Process;
import android.util.Log;
import android.view.View;
import android.widget.ProgressBar;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
/**
* If a package gets installed from a content URI this step stages the installation session
* reading bytes from the URI.
*/
public class InstallStaging extends AlertActivity {
private static final String LOG_TAG = InstallStaging.class.getSimpleName();
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 */
private @Nullable StagingAsyncTask mStagingTask;
/** The session the package is in */
private int mStagedSessionId;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mInstaller = getPackageManager().getPackageInstaller();
setFinishOnTouchOutside(true);
mAlert.setIcon(R.drawable.ic_file_download);
mAlert.setTitle(getString(R.string.app_name_unknown));
mAlert.setView(R.layout.install_content_view);
mAlert.setButton(DialogInterface.BUTTON_NEGATIVE, getString(R.string.cancel),
(ignored, ignored2) -> {
if (mStagingTask != null) {
mStagingTask.cancel(true);
}
cleanupStagingSession();
setResult(RESULT_CANCELED);
finish();
}, null);
setupAlert();
requireViewById(R.id.staging).setVisibility(View.VISIBLE);
if (savedInstanceState != null) {
mStagedSessionId = savedInstanceState.getInt(STAGED_SESSION_ID, 0);
}
}
@Override
protected void onResume() {
super.onResume();
// This is the first onResume in a single life of the activity.
if (mStagingTask == null) {
if (mStagedSessionId > 0) {
final PackageInstaller.SessionInfo info = mInstaller.getSessionInfo(
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 {
ParcelFileDescriptor pfd = afd != null ? afd.getParcelFileDescriptor() : null;
PackageInstaller.SessionParams params = createSessionParams(
mInstaller, getIntent(), pfd, packageUri.toString());
mStagedSessionId = mInstaller.createSession(params);
} catch (IOException e) {
Log.w(LOG_TAG, "Failed to create a staging session", e);
showError();
return;
} finally {
PackageUtil.safeClose(afd);
}
}
mStagingTask = new StagingAsyncTask();
mStagingTask.execute();
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putInt(STAGED_SESSION_ID, mStagedSessionId);
}
@Override
protected void onDestroy() {
if (mStagingTask != null) {
mStagingTask.cancel(true);
}
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);
// Disable full screen intent usage by for sideloads.
params.setPermissionState(Manifest.permission.USE_FULL_SCREEN_INTENT,
PackageInstaller.SessionParams.PERMISSION_STATE_DENIED);
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.
*/
private void showError() {
getFragmentManager().beginTransaction()
.add(new ErrorDialog(), "error").commitAllowingStateLoss();
Intent result = new Intent();
result.putExtra(Intent.EXTRA_INSTALL_RESULT,
PackageManager.INSTALL_FAILED_INVALID_APK);
setResult(RESULT_FIRST_USER, result);
}
/**
* Dialog for errors while staging.
*/
public static class ErrorDialog extends DialogFragment {
private Activity mActivity;
@Override
public void onAttach(Context context) {
super.onAttach(context);
mActivity = (Activity) context;
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
AlertDialog alertDialog = new AlertDialog.Builder(mActivity)
.setMessage(R.string.Parse_error_dlg_text)
.setPositiveButton(R.string.ok,
(dialog, which) -> mActivity.finish())
.create();
alertDialog.setCanceledOnTouchOutside(false);
return alertDialog;
}
@Override
public void onCancel(DialogInterface dialog) {
super.onCancel(dialog);
mActivity.finish();
}
}
private final class StagingAsyncTask extends
AsyncTask<Void, Integer, PackageInstaller.SessionInfo> {
private ProgressBar mProgressBar = null;
private long getContentSizeBytes() {
try (AssetFileDescriptor afd = openAssetFileDescriptor(getIntent().getData())) {
return afd != null ? afd.getLength() : UNKNOWN_LENGTH;
} catch (IOException ignored) {
return UNKNOWN_LENGTH;
}
}
@Override
protected void onPreExecute() {
final long sizeBytes = getContentSizeBytes();
mProgressBar = sizeBytes > 0 ? requireViewById(R.id.progress_indeterminate) : null;
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;
}
long sizeBytes = getContentSizeBytes();
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()) {
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();
}
}
}