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

Commit 4ab7dcd6 authored by Jakob Schneider's avatar Jakob Schneider Committed by Android (Google) Code Review
Browse files

Merge "Add a new Activity for unarchive errors to PIA." into main

parents 887262ed 144fb7a6
Loading
Loading
Loading
Loading
+12 −0
Original line number Diff line number Diff line
@@ -210,6 +210,18 @@
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

        <activity android:name=".UnarchiveErrorActivity"
                  android:configChanges="orientation|keyboardHidden|screenSize"
                  android:theme="@style/Theme.AlertDialogActivity.NoActionBar"
                  android:excludeFromRecents="true"
                  android:noHistory="true"
                  android:exported="true">
            <intent-filter android:priority="1">
                <action android:name="com.android.intent.action.UNARCHIVE_ERROR_DIALOG" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>
    </application>

</manifest>
+82 −0
Original line number Diff line number Diff line
@@ -267,4 +267,86 @@
    <!-- The action to restore (i.e. re-install, re-download) an app after parts of the app have been previously moved
         into the cloud for temporary storage. [CHAR LIMIT=15] -->
    <string name="restore">Restore</string>

    <!-- Dialog title shown when the user is trying to restore an app but the associated download
         cannot happen immediately because the device is offline (has no internet connection.
         [CHAR LIMIT=50] -->
    <string name="unarchive_error_offline_title">You\'re offline</string>

    <!-- Dialog body test shown when the user is trying to restore an app but the associated download
         cannot happen immediately because the device is offline (has no internet connection.
         [CHAR LIMIT=none] -->
    <string name="unarchive_error_offline_body">
        This app will automatically restore when you\'re connected to the internet
    </string>

    <!-- Dialog title shown when the user is trying to restore an app but a generic error happened.
         [CHAR LIMIT=50] -->
    <string name="unarchive_error_generic_title">Something went wrong</string>

    <!-- Dialog body shown when the user is trying to restore an app but a generic error happened.
         [CHAR LIMIT=none] -->
    <string name="unarchive_error_generic_body">
        There was a problem trying to restore this app
    </string>

    <!-- Dialog title shown when the user is trying to restore an app but the device is out of
         storage. [CHAR LIMIT=50] -->
    <string name="unarchive_error_storage_title">Not enough storage</string>

    <!-- Dialog body shown when the user is trying to restore an app but the device is out of
         storage. [CHAR LIMIT=none] -->
    <string name="unarchive_error_storage_body">
        To restore this app, you can free up space on this device. Storage
        required: <xliff:g id="bytes" example="100 MB">%1$s</xliff:g>
    </string>

    <!-- Dialog title shown when the user is trying to restore an app but the installer needs to
         perform an additional action. [CHAR LIMIT=50] -->
    <string name="unarchive_action_required_title">Action required</string>

    <!-- Dialog body shown when the user is trying to restore an app but the installer needs to
         perform an additional action. [CHAR LIMIT=none] -->
    <string name="unarchive_action_required_body">
        Follow the next steps to restore this app
    </string>
    <!-- Dialog title shown when the user is trying to restore an app but the installer responsible
         for the action is in a disabled state. [CHAR LIMIT=50] -->
    <string name="unarchive_error_installer_disabled_title">
        <xliff:g id="installername" example="App Store">%1$s</xliff:g> is disabled
    </string>

    <!-- Dialog body shown when the user is trying to restore an app but the installer responsible
         for the action is in a disabled state. [CHAR LIMIT=none] -->
    <string name="unarchive_error_installer_disabled_body">
        To restore this app, enable the
        <xliff:g id="installername" example="App Store">%1$s</xliff:g> in Settings
    </string>

    <!-- Dialog title shown when the user is trying to restore an app but the installer responsible
     for the action is uninstalled. [CHAR LIMIT=50] -->
    <string name="unarchive_error_installer_uninstalled_title">
        <xliff:g id="installername" example="App Store">%1$s</xliff:g> is uninstalled
    </string>

    <!-- Dialog body shown when the user is trying to restore an app but the installer responsible
         for the action is uninstalled. [CHAR LIMIT=none] -->
    <string name="unarchive_error_installer_uninstalled_body">
        To restore this app, you\'ll need to install
        <xliff:g id="installername" example="App Store">%1$s</xliff:g>
    </string>

    <!-- Dialog action to continue with the next action. [CHAR LIMIT=30] -->
    <string name="unarchive_action_required_continue">Continue</string>

    <!-- Dialog action to clear device storage, as in deleting some apps or photos to free up bytes
         [CHAR LIMIT=30] -->
    <string name="unarchive_clear_storage_button">Clear storage</string>

    <!-- Dialog action to open the Settings app. [CHAR LIMIT=30] -->
    <string name="unarchive_settings_button">Settings</string>

    <!-- Dialog action to close a dialog. [CHAR LIMIT=30] -->
    <string name="close">Close</string>

</resources>
+79 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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 android.app.Activity;
import android.app.DialogFragment;
import android.app.Fragment;
import android.app.FragmentTransaction;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.PackageInstaller;
import android.os.Bundle;

import java.util.Objects;

public class UnarchiveErrorActivity extends Activity {

    static final String EXTRA_REQUIRED_BYTES =
            "com.android.content.pm.extra.UNARCHIVE_EXTRA_REQUIRED_BYTES";
    static final String EXTRA_INSTALLER_PACKAGE_NAME =
            "com.android.content.pm.extra.UNARCHIVE_INSTALLER_PACKAGE_NAME";
    static final String EXTRA_INSTALLER_TITLE =
            "com.android.content.pm.extra.UNARCHIVE_INSTALLER_TITLE";

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(null);

        Bundle extras = getIntent().getExtras();
        int unarchivalStatus = extras.getInt(PackageInstaller.EXTRA_UNARCHIVE_STATUS);
        long requiredBytes = extras.getLong(EXTRA_REQUIRED_BYTES);
        PendingIntent intent = extras.getParcelable(Intent.EXTRA_INTENT, PendingIntent.class);
        String installerPackageName = extras.getString(EXTRA_INSTALLER_PACKAGE_NAME);
        // We cannot derive this from the package name because the installer might not be installed
        // anymore.
        String installerAppTitle = extras.getString(EXTRA_INSTALLER_TITLE);

        if (unarchivalStatus == PackageInstaller.UNARCHIVAL_ERROR_USER_ACTION_NEEDED) {
            Objects.requireNonNull(intent);
        }

        FragmentTransaction ft = getFragmentManager().beginTransaction();
        Fragment prev = getFragmentManager().findFragmentByTag("dialog");
        if (prev != null) {
            ft.remove(prev);
        }

        Bundle args = new Bundle();
        args.putInt(PackageInstaller.EXTRA_UNARCHIVE_STATUS, unarchivalStatus);
        args.putLong(EXTRA_REQUIRED_BYTES, requiredBytes);
        if (intent != null) {
            args.putParcelable(Intent.EXTRA_INTENT, intent);
        }
        if (installerPackageName != null) {
            args.putString(EXTRA_INSTALLER_PACKAGE_NAME, installerPackageName);
        }
        if (installerAppTitle != null) {
            args.putString(EXTRA_INSTALLER_TITLE, installerAppTitle);
        }

        DialogFragment fragment = new UnarchiveErrorFragment();
        fragment.setArguments(args);
        fragment.show(ft, "dialog");
    }
}
+196 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2023 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.Intent.FLAG_ACTIVITY_NEW_TASK;

import android.app.Activity;
import android.app.AlertDialog;
import android.app.Dialog;
import android.app.DialogFragment;
import android.app.PendingIntent;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentSender;
import android.content.pm.PackageInstaller;
import android.net.Uri;
import android.os.Bundle;
import android.provider.Settings;
import android.text.format.Formatter;
import android.util.Log;

import androidx.annotation.Nullable;

public class UnarchiveErrorFragment extends DialogFragment implements
        DialogInterface.OnClickListener {

    private static final String TAG = "UnarchiveErrorFragment";

    private int mStatus;

    @Nullable
    private PendingIntent mExtraIntent;

    @Nullable
    private String mInstallerPackageName;

    @Override
    public Dialog onCreateDialog(Bundle savedInstanceState) {
        Bundle args = getArguments();
        mStatus = args.getInt(PackageInstaller.EXTRA_UNARCHIVE_STATUS, -1);
        mExtraIntent = args.getParcelable(Intent.EXTRA_INTENT, PendingIntent.class);
        long requiredBytes = args.getLong(UnarchiveErrorActivity.EXTRA_REQUIRED_BYTES);
        mInstallerPackageName = args.getString(
                UnarchiveErrorActivity.EXTRA_INSTALLER_PACKAGE_NAME);
        String installerAppTitle = args.getString(UnarchiveErrorActivity.EXTRA_INSTALLER_TITLE);

        AlertDialog.Builder dialogBuilder = new AlertDialog.Builder(getActivity());

        dialogBuilder.setTitle(getDialogTitle(mStatus, installerAppTitle));
        dialogBuilder.setMessage(getBodyText(mStatus, installerAppTitle, requiredBytes));

        addButtons(dialogBuilder, mStatus);

        return dialogBuilder.create();
    }

    private void addButtons(AlertDialog.Builder dialogBuilder, int status) {
        switch (status) {
            case PackageInstaller.UNARCHIVAL_ERROR_USER_ACTION_NEEDED:
                dialogBuilder.setPositiveButton(R.string.unarchive_action_required_continue, this);
                dialogBuilder.setNegativeButton(R.string.close, this);
                break;
            case PackageInstaller.UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE:
                dialogBuilder.setPositiveButton(R.string.unarchive_clear_storage_button, this);
                dialogBuilder.setNegativeButton(R.string.close, this);
                break;
            case PackageInstaller.UNARCHIVAL_ERROR_INSTALLER_DISABLED:
                dialogBuilder.setPositiveButton(R.string.external_sources_settings, this);
                dialogBuilder.setNegativeButton(R.string.close, this);
                break;
            case PackageInstaller.UNARCHIVAL_ERROR_NO_CONNECTIVITY:
            case PackageInstaller.UNARCHIVAL_ERROR_INSTALLER_UNINSTALLED:
            case PackageInstaller.UNARCHIVAL_GENERIC_ERROR:
                dialogBuilder.setPositiveButton(android.R.string.ok, this);
                break;
            default:
                // This should never happen through normal API usage.
                throw new IllegalArgumentException("Invalid unarchive status " + status);
        }
    }

    private String getBodyText(int status, String installerAppTitle, long requiredBytes) {
        switch (status) {
            case PackageInstaller.UNARCHIVAL_ERROR_USER_ACTION_NEEDED:
                return getString(R.string.unarchive_action_required_body);
            case PackageInstaller.UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE:
                return String.format(getString(R.string.unarchive_error_storage_body),
                        Formatter.formatShortFileSize(getActivity(), requiredBytes));
            case PackageInstaller.UNARCHIVAL_ERROR_NO_CONNECTIVITY:
                return getString(R.string.unarchive_error_offline_body);
            case PackageInstaller.UNARCHIVAL_ERROR_INSTALLER_DISABLED:
                return String.format(getString(R.string.unarchive_error_installer_disabled_body),
                        installerAppTitle);
            case PackageInstaller.UNARCHIVAL_ERROR_INSTALLER_UNINSTALLED:
                return String.format(
                        getString(R.string.unarchive_error_installer_uninstalled_body),
                        installerAppTitle);
            case PackageInstaller.UNARCHIVAL_GENERIC_ERROR:
                return getString(R.string.unarchive_error_generic_body);
            default:
                // This should never happen through normal API usage.
                throw new IllegalArgumentException("Invalid unarchive status " + status);
        }
    }

    private String getDialogTitle(int status, String installerAppTitle) {
        switch (status) {
            case PackageInstaller.UNARCHIVAL_ERROR_USER_ACTION_NEEDED:
                return getString(R.string.unarchive_action_required_title);
            case PackageInstaller.UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE:
                return getString(R.string.unarchive_error_storage_title);
            case PackageInstaller.UNARCHIVAL_ERROR_NO_CONNECTIVITY:
                return getString(R.string.unarchive_error_offline_title);
            case PackageInstaller.UNARCHIVAL_ERROR_INSTALLER_DISABLED:
                return String.format(getString(R.string.unarchive_error_installer_disabled_title),
                        installerAppTitle);
            case PackageInstaller.UNARCHIVAL_ERROR_INSTALLER_UNINSTALLED:
                return String.format(
                        getString(R.string.unarchive_error_installer_uninstalled_title),
                        installerAppTitle);
            case PackageInstaller.UNARCHIVAL_GENERIC_ERROR:
                return getString(R.string.unarchive_error_generic_title);
            default:
                // This should never happen through normal API usage.
                throw new IllegalArgumentException("Invalid unarchive status " + status);
        }
    }

    @Override
    public void onClick(DialogInterface dialog, int which) {
        if (which != Dialog.BUTTON_POSITIVE) {
            return;
        }

        try {
            onClickInternal();
        } catch (IntentSender.SendIntentException e) {
            Log.e(TAG, "Failed to start intent after onClick.", e);
        }
    }

    private void onClickInternal() throws IntentSender.SendIntentException {
        Activity activity = getActivity();
        if (activity == null) {
            // This probably shouldn't happen in practice.
            Log.i(TAG, "Lost reference to activity, cannot act onClick.");
            return;
        }

        switch (mStatus) {
            case PackageInstaller.UNARCHIVAL_ERROR_USER_ACTION_NEEDED:
                activity.startIntentSender(mExtraIntent.getIntentSender(), /* fillInIntent= */
                        null, /* flagsMask= */ 0, FLAG_ACTIVITY_NEW_TASK, /* extraFlags= */ 0);
                break;
            case PackageInstaller.UNARCHIVAL_ERROR_INSUFFICIENT_STORAGE:
                if (mExtraIntent != null) {
                    activity.startIntentSender(mExtraIntent.getIntentSender(), /* fillInIntent= */
                            null, /* flagsMask= */ 0, FLAG_ACTIVITY_NEW_TASK, /* extraFlags= */ 0);
                } else {
                    Intent intent = new Intent("android.intent.action.MANAGE_PACKAGE_STORAGE");
                    startActivity(intent);
                }
                break;
            case PackageInstaller.UNARCHIVAL_ERROR_INSTALLER_DISABLED:
                Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS);
                Uri uri = Uri.fromParts("package", mInstallerPackageName, null);
                intent.setData(uri);
                startActivity(intent);
                break;
            default:
                // Do nothing. The rest of the dialogs are purely informational.
        }
    }

    @Override
    public void onDismiss(DialogInterface dialog) {
        super.onDismiss(dialog);
        if (isAdded()) {
            getActivity().finish();
        }
    }
}