/* * 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.pm.PackageInstaller.SessionParams.UID_UNKNOWN; import android.annotation.Nullable; import android.app.PendingIntent; import android.content.DialogInterface; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageInstaller; import android.content.pm.PackageManager; import android.content.pm.PackageParser; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.ProgressBar; import com.android.internal.app.AlertActivity; import com.android.internal.content.PackageHelper; import java.io.File; import java.io.FileInputStream; 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 * installation succeeds, start {@link InstallSuccess} or {@link InstallFailed}. *

This has two phases: First send the data to the package manager, then wait until the package * manager processed the result.

*/ public class InstallInstalling extends AlertActivity { private static final String LOG_TAG = InstallInstalling.class.getSimpleName(); private static final String SESSION_ID = "com.android.packageinstaller.SESSION_ID"; private static final String INSTALL_ID = "com.android.packageinstaller.INSTALL_ID"; private static final String BROADCAST_ACTION = "com.android.packageinstaller.ACTION_INSTALL_COMMIT"; /** Listens to changed to the session and updates progress bar */ private PackageInstaller.SessionCallback mSessionCallback; /** Task that sends the package to the package installer */ private InstallingAsyncTask mInstallingTask; /** Id of the session to install the package */ private int mSessionId; /** Id of the install event we wait for */ private int mInstallId; /** URI of package to install */ private Uri mPackageURI; /** The button that can cancel this dialog */ private Button mCancelButton; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); ApplicationInfo appInfo = getIntent() .getParcelableExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO); mPackageURI = getIntent().getData(); if ("package".equals(mPackageURI.getScheme())) { try { getPackageManager().installExistingPackage(appInfo.packageName); launchSuccess(); } catch (PackageManager.NameNotFoundException e) { launchFailure(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null); } } else { final File sourceFile = new File(mPackageURI.getPath()); PackageUtil.AppSnippet as = PackageUtil.getAppSnippet(this, appInfo, sourceFile); mAlert.setIcon(as.icon); mAlert.setTitle(as.label); mAlert.setView(R.layout.install_content_view); mAlert.setButton(DialogInterface.BUTTON_NEGATIVE, getString(R.string.cancel), (ignored, ignored2) -> { if (mInstallingTask != null) { mInstallingTask.cancel(true); } if (mSessionId > 0) { getPackageManager().getPackageInstaller().abandonSession(mSessionId); mSessionId = 0; } setResult(RESULT_CANCELED); finish(); }, null); setupAlert(); requireViewById(R.id.installing).setVisibility(View.VISIBLE); if (savedInstanceState != null) { mSessionId = savedInstanceState.getInt(SESSION_ID); mInstallId = savedInstanceState.getInt(INSTALL_ID); // Reregister for result; might instantly call back if result was delivered while // activity was destroyed try { InstallEventReceiver.addObserver(this, mInstallId, this::launchFinishBasedOnResult); } catch (EventResultPersister.OutOfIdsException e) { // Does not happen } } else { PackageInstaller.SessionParams params = new PackageInstaller.SessionParams( PackageInstaller.SessionParams.MODE_FULL_INSTALL); params.setInstallAsInstantApp(false); params.setReferrerUri(getIntent().getParcelableExtra(Intent.EXTRA_REFERRER)); params.setOriginatingUri(getIntent() .getParcelableExtra(Intent.EXTRA_ORIGINATING_URI)); params.setOriginatingUid(getIntent().getIntExtra(Intent.EXTRA_ORIGINATING_UID, UID_UNKNOWN)); params.setInstallerPackageName(getIntent().getStringExtra( Intent.EXTRA_INSTALLER_PACKAGE_NAME)); params.setInstallReason(PackageManager.INSTALL_REASON_USER); File file = new File(mPackageURI.getPath()); try { PackageParser.PackageLite pkg = PackageParser.parsePackageLite(file, 0); params.setAppPackageName(pkg.packageName); params.setInstallLocation(pkg.installLocation); params.setSize( PackageHelper.calculateInstalledSize(pkg, false, params.abiOverride)); } catch (PackageParser.PackageParserException e) { Log.e(LOG_TAG, "Cannot parse package " + file + ". Assuming defaults."); Log.e(LOG_TAG, "Cannot calculate installed size " + file + ". Try only apk size."); params.setSize(file.length()); } catch (IOException e) { Log.e(LOG_TAG, "Cannot calculate installed size " + file + ". Try only apk size."); params.setSize(file.length()); } try { mInstallId = InstallEventReceiver .addObserver(this, EventResultPersister.GENERATE_NEW_ID, this::launchFinishBasedOnResult); } catch (EventResultPersister.OutOfIdsException e) { launchFailure(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null); } try { mSessionId = getPackageManager().getPackageInstaller().createSession(params); } catch (IOException e) { launchFailure(PackageManager.INSTALL_FAILED_INTERNAL_ERROR, null); } } mCancelButton = mAlert.getButton(DialogInterface.BUTTON_NEGATIVE); mSessionCallback = new InstallSessionCallback(); } } /** * Launch the "success" version of the final package installer dialog */ private void launchSuccess() { Intent successIntent = new Intent(getIntent()); successIntent.setClass(this, InstallSuccess.class); successIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); startActivity(successIntent); finish(); } /** * Launch the "failure" version of the final package installer dialog * * @param legacyStatus The status as used internally in the package manager. * @param statusMessage The status description. */ private void launchFailure(int legacyStatus, String statusMessage) { Intent failureIntent = new Intent(getIntent()); failureIntent.setClass(this, InstallFailed.class); failureIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT); failureIntent.putExtra(PackageInstaller.EXTRA_LEGACY_STATUS, legacyStatus); failureIntent.putExtra(PackageInstaller.EXTRA_STATUS_MESSAGE, statusMessage); startActivity(failureIntent); finish(); } @Override protected void onStart() { super.onStart(); getPackageManager().getPackageInstaller().registerSessionCallback(mSessionCallback); } @Override protected void onResume() { super.onResume(); // This is the first onResume in a single life of the activity if (mInstallingTask == null) { PackageInstaller installer = getPackageManager().getPackageInstaller(); PackageInstaller.SessionInfo sessionInfo = installer.getSessionInfo(mSessionId); if (sessionInfo != null && !sessionInfo.isActive()) { mInstallingTask = new InstallingAsyncTask(); mInstallingTask.execute(); } else { // we will receive a broadcast when the install is finished mCancelButton.setEnabled(false); setFinishOnTouchOutside(false); } } } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putInt(SESSION_ID, mSessionId); outState.putInt(INSTALL_ID, mInstallId); } @Override public void onBackPressed() { if (mCancelButton.isEnabled()) { super.onBackPressed(); } } @Override protected void onStop() { super.onStop(); getPackageManager().getPackageInstaller().unregisterSessionCallback(mSessionCallback); } @Override protected void onDestroy() { if (mInstallingTask != null) { mInstallingTask.cancel(true); synchronized (mInstallingTask) { while (!mInstallingTask.isDone) { try { mInstallingTask.wait(); } catch (InterruptedException e) { Log.i(LOG_TAG, "Interrupted while waiting for installing task to cancel", e); } } } } InstallEventReceiver.removeObserver(this, mInstallId); super.onDestroy(); } /** * Launch the appropriate finish activity (success or failed) for the installation result. * * @param statusCode The installation result. * @param legacyStatus The installation as used internally in the package manager. * @param statusMessage The detailed installation result. */ private void launchFinishBasedOnResult(int statusCode, int legacyStatus, String statusMessage) { if (statusCode == PackageInstaller.STATUS_SUCCESS) { launchSuccess(); } else { launchFailure(legacyStatus, statusMessage); } } private class InstallSessionCallback extends PackageInstaller.SessionCallback { @Override public void onCreated(int sessionId) { // empty } @Override public void onBadgingChanged(int sessionId) { // empty } @Override public void onActiveChanged(int sessionId, boolean active) { // empty } @Override public void onProgressChanged(int sessionId, float progress) { if (sessionId == mSessionId) { ProgressBar progressBar = requireViewById(R.id.progress); progressBar.setMax(Integer.MAX_VALUE); progressBar.setProgress((int) (Integer.MAX_VALUE * progress)); } } @Override public void onFinished(int sessionId, boolean success) { // empty, finish is handled by InstallResultReceiver } } /** * Send the package to the package installer and then register a event result observer that * will call {@link #launchFinishBasedOnResult(int, int, String)} */ private final class InstallingAsyncTask extends AsyncTask { volatile boolean isDone; @Override protected PackageInstaller.Session doInBackground(Void... params) { PackageInstaller.Session session; try { session = getPackageManager().getPackageInstaller().openSession(mSessionId); } catch (IOException e) { return null; } session.setStagingProgress(0); try { File file = new File(mPackageURI.getPath()); try (InputStream in = new FileInputStream(file)) { long sizeBytes = file.length(); 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) { float fraction = ((float) numRead / (float) sizeBytes); session.addProgress(fraction); } } } } return session; } catch (IOException | SecurityException e) { Log.e(LOG_TAG, "Could not write package", e); session.close(); return null; } finally { synchronized (this) { isDone = true; notifyAll(); } } } @Override protected void onPostExecute(PackageInstaller.Session session) { if (session != null) { Intent broadcastIntent = new Intent(BROADCAST_ACTION); broadcastIntent.setFlags(Intent.FLAG_RECEIVER_FOREGROUND); broadcastIntent.setPackage(getPackageName()); broadcastIntent.putExtra(EventResultPersister.EXTRA_ID, mInstallId); PendingIntent pendingIntent = PendingIntent.getBroadcast( InstallInstalling.this, mInstallId, broadcastIntent, PendingIntent.FLAG_UPDATE_CURRENT); session.commit(pendingIntent.getIntentSender()); mCancelButton.setEnabled(false); setFinishOnTouchOutside(false); } else { getPackageManager().getPackageInstaller().abandonSession(mSessionId); if (!isCancelled()) { launchFailure(PackageManager.INSTALL_FAILED_INVALID_APK, null); } } } } }