Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f76fffda authored by Sumedh Sen's avatar Sumedh Sen Committed by Song Chun Fan
Browse files

[ADI][UserConfirmation][2/4] Implement new dialogs in Pia to handle incomplete...

[ADI][UserConfirmation][2/4] Implement new dialogs in Pia to handle incomplete and failed verification

In certain types of failures and the type of installer (priv or
non-priv), the user needs to be shown a dialog to get their response for
continuing or aborting an install. This change implements those dialogs
in Pia.

Bug: 360130528
Test: TBD
Flag: android.content.pm.verification_service
Merged-In: If8b12ee8db04e686a00d8a1389eb3a201db18747
Change-Id: If8b12ee8db04e686a00d8a1389eb3a201db18747
parent 30b02c46
Loading
Loading
Loading
Loading
+8 −1
Original line number Diff line number Diff line
@@ -25,8 +25,8 @@
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
    <uses-permission android:name="android.permission.READ_SYSTEM_GRAMMATICAL_GENDER" />
    <uses-permission android:name="android.permission.RESOLVE_COMPONENT_FOR_UID" />

    <uses-permission android:name="com.google.android.permission.INSTALL_WEARABLE_PACKAGES" />
    <uses-permission android:name="android.permission.SET_VERIFICATION_USER_RESPONSE" />

    <application android:name=".PackageInstallerApplication"
            android:label="@string/app_name"
@@ -73,6 +73,10 @@
                <action android:name="android.content.pm.action.CONFIRM_PRE_APPROVAL" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
            <intent-filter android:priority="1">
                <action android:name="android.content.pm.action.NOTIFY_VERIFICATION_INCOMPLETE" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

        <activity android:name=".InstallStaging"
@@ -85,6 +89,9 @@
                android:exported="false"
                android:enableOnBackInvokedCallback="false" />

        <activity android:name=".ConfirmVerification"
            android:exported="false" />

        <activity android:name=".InstallInstalling"
                android:exported="false"
                android:enableOnBackInvokedCallback="false" />
+12 −0
Original line number Diff line number Diff line
@@ -26,6 +26,12 @@
    <string name="done">Done</string>
    <!-- [CHAR LIMIT=15] -->
    <string name="cancel">Cancel</string>
    <!-- [CHAR LIMIT=15] -->
    <string name="try_again">Try again</string>
    <!-- [CHAR LIMIT=15] -->
    <string name="install_anyway">Install anyway</string>
    <!-- [CHAR LIMIT=15] -->
    <string name="dont_install">Don\'t install</string>
    <!-- [CHAR LIMIT=50] -->
    <string name="installing">Installing\u2026</string>
    <!-- [CHAR LIMIT=50] -->
@@ -82,6 +88,12 @@
        user</string>
    <!-- Message presented in a dialog box when the user restriction set by the system restricts the installation of apps. [CHAR LIMIT=none] -->
    <string name="install_apps_user_restriction_dlg_text">This user is not allowed to install apps</string>
    <!-- Message presented in a dialog box when verification failed due to problems with the network. [CHAR LIMIT=none]-->
    <string name="verification_incomplete_summary">App is not yet installed because verification could not take place. Please try again.</string>
    <!-- Message presented in a dialog box when verification failed due to internal failures. [CHAR LIMIT=none]-->
    <string name="cannot_install_verification_unavailable_summary"><p>Warning: App verification could not take place.</p><p><a href="">Learn more</a> about package verification</p></string>
    <!-- Message presented in a dialog box when the verifier blocked an app from being installed. [CHAR LIMIT=none]-->
    <string name="cannot_install_package_summary"><p>This app can’t be installed because it has been blocked.</p><p><a href="">Learn more</a> about package verification</p></string>

    <!-- [CHAR LIMIT=15] -->
    <string name="ok">OK</string>
+284 −0
Original line number Diff line number Diff line
/*
 * Copyright 2024 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.EXTRA_SESSION_ID;
import static android.content.pm.PackageInstaller.SessionInfo;
import static android.content.pm.PackageInstaller.SessionInfo.INVALID_ID;
import static android.content.pm.PackageInstaller.VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE;
import static android.content.pm.PackageInstaller.VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED;
import static android.content.pm.PackageInstaller.VERIFICATION_FAILED_REASON_UNKNOWN;
import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_OPEN;
import static android.content.pm.PackageInstaller.VERIFICATION_POLICY_BLOCK_FAIL_WARN;
import static android.content.pm.PackageInstaller.VERIFICATION_USER_RESPONSE_CANCEL;
import static android.content.pm.PackageInstaller.VERIFICATION_USER_RESPONSE_ERROR;
import static android.content.pm.PackageInstaller.VERIFICATION_USER_RESPONSE_INSTALL_ANYWAY;
import static android.content.pm.PackageInstaller.VERIFICATION_USER_RESPONSE_OK;
import static android.content.pm.PackageInstaller.VERIFICATION_USER_RESPONSE_RETRY;

import static com.android.packageinstaller.PackageUtil.AppSnippet;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageInstaller;
import android.content.pm.PackageInstaller.VerificationUserConfirmationInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.Log;
import android.widget.Button;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.io.File;

public class ConfirmVerification extends Activity {

    private static final String TAG = ConfirmVerification.class.getSimpleName();
    public static final int FLAG_VERIFICATION_FAILED_MAY_RETRY = 1 << 0;
    public static final int FLAG_VERIFICATION_FAILED_MAY_BYPASS = 1 << 1;
    public static final int FLAG_VERIFICATION_FAILED_MAY_RETRY_MAY_BYPASS =
            FLAG_VERIFICATION_FAILED_MAY_RETRY | FLAG_VERIFICATION_FAILED_MAY_BYPASS;

    private final boolean mLocalLOGV = false;
    private PackageManager mPackageManager;
    private PackageInstaller mPackageInstaller;
    private AlertDialog mDialog;
    private AppSnippet mAppSnippet;
    private Button mPositiveBtn;
    private Button mNegativeBtn;
    private Button mNeutralBtn;

    @Override
    @SuppressLint("MissingPermission")
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mPackageManager = getPackageManager();
        mPackageInstaller = mPackageManager.getPackageInstaller();

        Intent intent = getIntent();
        int sessionId = intent.getIntExtra(EXTRA_SESSION_ID, INVALID_ID);

        SessionInfo sessionInfo = mPackageInstaller.getSessionInfo(sessionId);
        String resolvedPath = sessionInfo != null ? sessionInfo.getResolvedBaseApkPath() : null;
        if (sessionInfo == null || !sessionInfo.isSealed() || resolvedPath == null) {
            Log.e(TAG, "Session " + sessionId + " in funky state; ignoring");
            mPackageInstaller.setVerificationUserResponse(sessionId,
                    VERIFICATION_USER_RESPONSE_ERROR);
            finish();
            return;
        }

        VerificationUserConfirmationInfo verificationInfo =
                mPackageInstaller.getVerificationUserConfirmationInfo(sessionId);
        if (verificationInfo == null) {
            Log.e(TAG, "Could not get VerificationInfo for sessionId " + sessionId);
            mPackageInstaller.setVerificationUserResponse(sessionId,
                    VERIFICATION_USER_RESPONSE_ERROR);
            finish();
            return;
        }

        mAppSnippet = generateAppSnippet(resolvedPath);
        if (mAppSnippet == null) {
            Log.e(TAG, "Could not generate AppSnippet for session " + sessionId);
            if (mLocalLOGV) {
                Log.d(TAG, "Failed to generate AppSnippet for path " + resolvedPath);
            }
            mPackageInstaller.setVerificationUserResponse(sessionId,
                    VERIFICATION_USER_RESPONSE_ERROR);
            finish();
            return;
        }

        int dialogTypeFlag = getUserConfirmationDialogFlag(verificationInfo);
        int failureReason = verificationInfo.getVerificationFailureReason();
        int msgResId = getDialogMessageResourceId(failureReason);

        AlertDialog.Builder builder = new AlertDialog.Builder(this)
                .setIcon(mAppSnippet.icon)
                .setTitle(mAppSnippet.label)
                .setMessage(msgResId);

        if ((dialogTypeFlag & FLAG_VERIFICATION_FAILED_MAY_RETRY_MAY_BYPASS)
                == FLAG_VERIFICATION_FAILED_MAY_RETRY_MAY_BYPASS) {
            // allow retry and bypass
            builder.setPositiveButton(R.string.try_again, (dialog, which) -> {
                mPackageInstaller.setVerificationUserResponse(sessionId,
                        VERIFICATION_USER_RESPONSE_RETRY);
                finish();
            }).setNegativeButton(R.string.dont_install, (dialog, which) -> {
                mPackageInstaller.setVerificationUserResponse(sessionId,
                        VERIFICATION_USER_RESPONSE_CANCEL);
                finish();
            }).setNeutralButton(R.string.install_anyway, (dialog, which) -> {
                mPackageInstaller.setVerificationUserResponse(sessionId,
                        VERIFICATION_USER_RESPONSE_INSTALL_ANYWAY);
                finish();
            });
        } else if ((dialogTypeFlag & FLAG_VERIFICATION_FAILED_MAY_RETRY) != 0) {
            // only allow retry
            builder.setPositiveButton(R.string.ok, (dialog, which) -> {
                mPackageInstaller.setVerificationUserResponse(sessionId,
                        VERIFICATION_USER_RESPONSE_OK);
                finish();
            }).setNegativeButton(R.string.try_again, (dialog, which) -> {
                mPackageInstaller.setVerificationUserResponse(sessionId,
                        VERIFICATION_USER_RESPONSE_RETRY);
                finish();
            });
        } else if ((dialogTypeFlag & FLAG_VERIFICATION_FAILED_MAY_BYPASS) != 0) {
            // only allow bypass
            builder.setPositiveButton(R.string.dont_install, (dialog, which) -> {
                mPackageInstaller.setVerificationUserResponse(sessionId,
                        VERIFICATION_USER_RESPONSE_CANCEL);
                finish();
            }).setNegativeButton(R.string.install_anyway, (dialog, which) -> {
                mPackageInstaller.setVerificationUserResponse(sessionId,
                        VERIFICATION_USER_RESPONSE_INSTALL_ANYWAY);
                finish();
            });
        } else {
            // allow only acknowledging the error
            builder.setPositiveButton(
                    (failureReason == VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED)
                            ? R.string.close
                            : R.string.ok,
                    (dialog, which) -> {
                        mPackageInstaller.setVerificationUserResponse(sessionId,
                                VERIFICATION_USER_RESPONSE_OK);
                        finish();
                    });
        }

        mDialog = builder.create();
        mPositiveBtn = mDialog.getButton(DialogInterface.BUTTON_POSITIVE);
        mNegativeBtn = mDialog.getButton(DialogInterface.BUTTON_NEGATIVE);
        mNeutralBtn = mDialog.getButton(DialogInterface.BUTTON_NEUTRAL);
        mDialog.show();

        mDialog.setOnCancelListener(dialog -> {
            mPackageInstaller.setVerificationUserResponse(sessionId,
                    VERIFICATION_USER_RESPONSE_CANCEL);
            finish();
        });

    }

    @Nullable
    private AppSnippet generateAppSnippet(@NonNull String resolvedPath) {
        File sourceFile = new File(resolvedPath);
        final PackageInfo packageInfo = PackageUtil.getPackageInfo(this, sourceFile,
                PackageManager.GET_PERMISSIONS);

        // Check for parse errors
        if (packageInfo == null) {
            Log.e(TAG, "Parse error when parsing manifest. Discontinuing installation");
            //show error to user?
            return null;
        }
        if (mLocalLOGV) {
            Log.i(TAG, "Creating snippet for local file " + sourceFile);
        }
        return PackageUtil.getAppSnippet(this, packageInfo.applicationInfo, sourceFile);
    }

    /**
     * Returns the correct type of dialog based on the verification policy and the failure reason
     */
    public static int getUserConfirmationDialogFlag(
            VerificationUserConfirmationInfo verificationInfo) {
        int verificationFailureReason = verificationInfo.getVerificationFailureReason();
        int verificationPolicy = verificationInfo.getVerificationPolicy();

        return switch (verificationFailureReason) {
            case VERIFICATION_FAILED_REASON_PACKAGE_BLOCKED -> 0;

            case VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE -> {
                int flag = FLAG_VERIFICATION_FAILED_MAY_RETRY;
                if (verificationPolicy == VERIFICATION_POLICY_BLOCK_FAIL_OPEN
                        || verificationPolicy == VERIFICATION_POLICY_BLOCK_FAIL_WARN) {
                    flag |= FLAG_VERIFICATION_FAILED_MAY_BYPASS;
                }
                yield flag;
            }

            case VERIFICATION_FAILED_REASON_UNKNOWN -> {
                int flag = 0;
                if (verificationPolicy == VERIFICATION_POLICY_BLOCK_FAIL_OPEN
                        || verificationPolicy == VERIFICATION_POLICY_BLOCK_FAIL_WARN) {
                    flag |= FLAG_VERIFICATION_FAILED_MAY_BYPASS;
                }
                yield flag;
            }

            default -> {
                Log.e(TAG, "Unknown failure reason: " + verificationFailureReason);
                yield 0;
            }
        };
    }

    private int getDialogMessageResourceId(int failureReason) {
        return switch (failureReason) {
            case VERIFICATION_FAILED_REASON_UNKNOWN ->
                    R.string.cannot_install_verification_unavailable_summary;

            case VERIFICATION_FAILED_REASON_NETWORK_UNAVAILABLE ->
                    R.string.verification_incomplete_summary;

            default -> R.string.cannot_install_package_summary;
        };
    }

    @Override
    protected void onPause() {
        super.onPause();
        // Don't allow the buttons to be clicked as there might be overlays
        Button[] buttons = {mPositiveBtn, mNegativeBtn, mNeutralBtn};
        for (Button button : buttons) {
            if (button != null) {
                button.setEnabled(false);
            }
        }
    }

    @Override
    protected void onResume() {
        super.onResume();
        // Re-enable the buttons since they were disabled when activity was paused
        Button[] buttons = {mPositiveBtn, mNegativeBtn, mNeutralBtn};
        for (Button button : buttons) {
            if (button != null) {
                button.setEnabled(true);
            }
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (mDialog != null) {
            mDialog.dismiss();
        }
    }
}
+10 −3
Original line number Diff line number Diff line
@@ -105,9 +105,12 @@ public class InstallStart extends Activity {
        // be PIA.
        int originatingUid = callingUid;

        String intentAction = intent.getAction();
        final boolean isSessionInstall =
                PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intent.getAction())
                        || PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intent.getAction());
                PackageInstaller.ACTION_CONFIRM_PRE_APPROVAL.equals(intentAction)
                || PackageInstaller.ACTION_CONFIRM_INSTALL.equals(intentAction)
                || (Flags.verificationService()
                && PackageInstaller.ACTION_NOTIFY_VERIFICATION_INCOMPLETE.equals(intentAction));

        // If the activity was started via a PackageInstaller session, we retrieve the originating
        // UID from that session
@@ -222,7 +225,11 @@ public class InstallStart extends Activity {
                originatingUidFromSession);
        nextActivity.putExtra(PackageInstallerActivity.EXTRA_IS_TRUSTED_SOURCE, isTrustedSource);

        if (isSessionInstall) {
        if (Flags.verificationService()
                && PackageInstaller.ACTION_NOTIFY_VERIFICATION_INCOMPLETE.equals(intentAction)) {
            nextActivity.setClass(this, ConfirmVerification.class);
            nextActivity.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
        } else if (isSessionInstall) {
            nextActivity.setClass(this, PackageInstallerActivity.class);
            nextActivity.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION);
        } else {