/*
 * Copyright (C) 2018 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.internal.app;

import static android.app.admin.flags.Flags.crossUserSuspensionEnabledRo;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static android.content.pm.SuspendDialogInfo.BUTTON_ACTION_MORE_DETAILS;
import static android.content.pm.SuspendDialogInfo.BUTTON_ACTION_UNSUSPEND;
import static android.content.res.Resources.ID_NULL;

import android.Manifest;
import android.annotation.Nullable;
import android.app.ActivityOptions;
import android.app.AlertDialog;
import android.app.AppGlobals;
import android.app.KeyguardManager;
import android.app.usage.UsageStatsManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.IntentSender;
import android.content.pm.IPackageManager;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.SuspendDialogInfo;
import android.content.pm.UserPackage;
import android.content.res.Resources;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.RemoteException;
import android.os.UserHandle;
import android.util.Slog;
import android.view.WindowManager;

import com.android.internal.R;
import com.android.internal.util.ArrayUtils;

public class SuspendedAppActivity extends AlertActivity
        implements DialogInterface.OnClickListener {
    private static final String TAG = SuspendedAppActivity.class.getSimpleName();
    private static final String PACKAGE_NAME = "com.android.internal.app";

    public static final String EXTRA_SUSPENDED_PACKAGE = PACKAGE_NAME + ".extra.SUSPENDED_PACKAGE";
    public static final String EXTRA_SUSPENDING_PACKAGE =
            PACKAGE_NAME + ".extra.SUSPENDING_PACKAGE";
    public static final String EXTRA_SUSPENDING_USER = PACKAGE_NAME + ".extra.SUSPENDING_USER";
    public static final String EXTRA_DIALOG_INFO = PACKAGE_NAME + ".extra.DIALOG_INFO";
    public static final String EXTRA_ACTIVITY_OPTIONS = PACKAGE_NAME + ".extra.ACTIVITY_OPTIONS";
    public static final String EXTRA_UNSUSPEND_INTENT = PACKAGE_NAME + ".extra.UNSUSPEND_INTENT";

    private Intent mMoreDetailsIntent;
    private IntentSender mOnUnsuspend;
    private String mSuspendedPackage;
    private String mSuspendingPackage;
    private int mSuspendingUserId;
    private int mNeutralButtonAction;
    private int mUserId;
    private PackageManager mPm;
    private UsageStatsManager mUsm;
    private Resources mSuspendingAppResources;
    private SuspendDialogInfo mSuppliedDialogInfo;
    private Bundle mOptions;
    private BroadcastReceiver mSuspendModifiedReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (Intent.ACTION_PACKAGES_SUSPENSION_CHANGED.equals(intent.getAction())) {
                // Suspension conditions were modified, dismiss any related visible dialogs.
                final String[] modified = intent.getStringArrayExtra(
                        Intent.EXTRA_CHANGED_PACKAGE_LIST);
                if (ArrayUtils.contains(modified, mSuspendedPackage)
                        && !isPackageSuspended(mSuspendedPackage)) {
                    if (!isFinishing()) {
                        Slog.w(TAG, "Package " + mSuspendedPackage + " has modified"
                                + " suspension conditions while dialog was visible. Finishing.");
                        SuspendedAppActivity.this.finish();
                        // TODO (b/198201994): reload the suspend dialog to show most relevant info
                    }
                }
            }
        }
    };

    private boolean isPackageSuspended(String packageName) {
        try {
            return mPm.isPackageSuspended(packageName);
        } catch (PackageManager.NameNotFoundException ne) {
            Slog.e(TAG, "Package " + packageName + " not found", ne);
        }
        return false;
    }

    private CharSequence getAppLabel(String packageName) {
        try {
            return mPm.getApplicationInfoAsUser(packageName, 0, mUserId).loadLabel(mPm);
        } catch (PackageManager.NameNotFoundException ne) {
            Slog.e(TAG, "Package " + packageName + " not found", ne);
        }
        return packageName;
    }

    private Intent getMoreDetailsActivity() {
        final Intent moreDetailsIntent = new Intent(Intent.ACTION_SHOW_SUSPENDED_APP_DETAILS)
                .setPackage(mSuspendingPackage);
        final String requiredPermission = Manifest.permission.SEND_SHOW_SUSPENDED_APP_DETAILS;
        final ResolveInfo resolvedInfo = mPm.resolveActivityAsUser(moreDetailsIntent,
                MATCH_DIRECT_BOOT_UNAWARE | MATCH_DIRECT_BOOT_AWARE, mSuspendingUserId);
        if (resolvedInfo != null && resolvedInfo.activityInfo != null
                && requiredPermission.equals(resolvedInfo.activityInfo.permission)) {
            moreDetailsIntent.putExtra(Intent.EXTRA_PACKAGE_NAME, mSuspendedPackage)
                    .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
            return moreDetailsIntent;
        }
        return null;
    }

    private Drawable resolveIcon() {
        final int iconId = (mSuppliedDialogInfo != null) ? mSuppliedDialogInfo.getIconResId()
                : ID_NULL;
        if (iconId != ID_NULL && mSuspendingAppResources != null) {
            try {
                return mSuspendingAppResources.getDrawable(iconId, getTheme());
            } catch (Resources.NotFoundException nfe) {
                Slog.e(TAG, "Could not resolve drawable resource id " + iconId);
            }
        }
        return null;
    }

    private String resolveTitle() {
        if (mSuppliedDialogInfo != null) {
            final int titleId = mSuppliedDialogInfo.getTitleResId();
            final String title = mSuppliedDialogInfo.getTitle();
            if (titleId != ID_NULL && mSuspendingAppResources != null) {
                try {
                    return mSuspendingAppResources.getString(titleId);
                } catch (Resources.NotFoundException nfe) {
                    Slog.e(TAG, "Could not resolve string resource id " + titleId);
                }
            } else if (title != null) {
                return title;
            }
        }
        return getString(R.string.app_suspended_title);
    }

    private String resolveDialogMessage() {
        final CharSequence suspendedAppLabel = getAppLabel(mSuspendedPackage);
        if (mSuppliedDialogInfo != null) {
            final int messageId = mSuppliedDialogInfo.getDialogMessageResId();
            final String message = mSuppliedDialogInfo.getDialogMessage();
            if (messageId != ID_NULL && mSuspendingAppResources != null) {
                try {
                    return mSuspendingAppResources.getString(messageId, suspendedAppLabel);
                } catch (Resources.NotFoundException nfe) {
                    Slog.e(TAG, "Could not resolve string resource id " + messageId);
                }
            } else if (message != null) {
                return String.format(getResources().getConfiguration().getLocales().get(0), message,
                        suspendedAppLabel);
            }
        }
        return getString(R.string.app_suspended_default_message, suspendedAppLabel,
                getAppLabel(mSuspendingPackage));
    }

    /**
     * Returns a text to be displayed on the neutral button or {@code null} if the button should
     * not be shown.
     */
    @Nullable
    private String resolveNeutralButtonText() {
        final int defaultButtonTextId;
        switch (mNeutralButtonAction) {
            case BUTTON_ACTION_MORE_DETAILS:
                if (mMoreDetailsIntent == null) {
                    return null;
                }
                defaultButtonTextId = R.string.app_suspended_more_details;
                break;
            case BUTTON_ACTION_UNSUSPEND:
                defaultButtonTextId = R.string.app_suspended_unsuspend_message;
                break;
            default:
                Slog.w(TAG, "Unknown neutral button action: " + mNeutralButtonAction);
                return null;
        }
        if (mSuppliedDialogInfo != null) {
            final int buttonTextId = mSuppliedDialogInfo.getNeutralButtonTextResId();
            final String buttonText = mSuppliedDialogInfo.getNeutralButtonText();
            if (buttonTextId != ID_NULL && mSuspendingAppResources != null) {
                try {
                    return mSuspendingAppResources.getString(buttonTextId);
                } catch (Resources.NotFoundException nfe) {
                    Slog.e(TAG, "Could not resolve string resource id " + buttonTextId);
                }
            } else if (buttonText != null) {
                return buttonText;
            }
        }
        return getString(defaultButtonTextId);
    }

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        mPm = getPackageManager();
        mUsm = getSystemService(UsageStatsManager.class);
        getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_DIALOG);

        final Intent intent = getIntent();
        mOptions = intent.getBundleExtra(EXTRA_ACTIVITY_OPTIONS);
        mUserId = intent.getIntExtra(Intent.EXTRA_USER_ID, -1);
        if (mUserId < 0) {
            Slog.wtf(TAG, "Invalid user: " + mUserId);
            finish();
            return;
        }
        mSuspendedPackage = intent.getStringExtra(EXTRA_SUSPENDED_PACKAGE);
        mSuspendingPackage = intent.getStringExtra(EXTRA_SUSPENDING_PACKAGE);
        if (crossUserSuspensionEnabledRo()) {
            mSuspendingUserId = intent.getIntExtra(EXTRA_SUSPENDING_USER, mUserId);
        } else {
            mSuspendingUserId = mUserId;
        }
        mSuppliedDialogInfo = intent.getParcelableExtra(EXTRA_DIALOG_INFO, android.content.pm.SuspendDialogInfo.class);
        mOnUnsuspend = intent.getParcelableExtra(EXTRA_UNSUSPEND_INTENT, android.content.IntentSender.class);
        if (mSuppliedDialogInfo != null) {
            try {
                mSuspendingAppResources = createContextAsUser(
                        UserHandle.of(mSuspendingUserId), /* flags */ 0).getPackageManager()
                        .getResourcesForApplication(mSuspendingPackage);
            } catch (PackageManager.NameNotFoundException ne) {
                Slog.e(TAG, "Could not find resources for " + mSuspendingPackage, ne);
            }
        }
        mNeutralButtonAction = (mSuppliedDialogInfo != null)
                ? mSuppliedDialogInfo.getNeutralButtonAction() : BUTTON_ACTION_MORE_DETAILS;
        mMoreDetailsIntent = (mNeutralButtonAction == BUTTON_ACTION_MORE_DETAILS)
                ? getMoreDetailsActivity() : null;

        final AlertController.AlertParams ap = mAlertParams;
        ap.mIcon = resolveIcon();
        ap.mTitle = resolveTitle();
        ap.mMessage = resolveDialogMessage();
        ap.mPositiveButtonText = getString(android.R.string.ok);
        ap.mNeutralButtonText = resolveNeutralButtonText();
        ap.mPositiveButtonListener = ap.mNeutralButtonListener = this;

        requestDismissKeyguardIfNeeded(ap.mMessage);

        setupAlert();

        final IntentFilter suspendModifiedFilter =
                new IntentFilter(Intent.ACTION_PACKAGES_SUSPENSION_CHANGED);
        registerReceiverAsUser(mSuspendModifiedReceiver, UserHandle.of(mUserId),
                suspendModifiedFilter, null, null);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        unregisterReceiver(mSuspendModifiedReceiver);
    }

    private void requestDismissKeyguardIfNeeded(CharSequence dismissMessage) {
        final KeyguardManager km = getSystemService(KeyguardManager.class);
        if (km.isKeyguardLocked()) {
            km.requestDismissKeyguard(this, dismissMessage,
                    new KeyguardManager.KeyguardDismissCallback() {
                        @Override
                        public void onDismissError() {
                            Slog.e(TAG, "Error while dismissing keyguard."
                                    + " Keeping the dialog visible.");
                        }

                        @Override
                        public void onDismissCancelled() {
                            Slog.w(TAG, "Keyguard dismiss was cancelled. Finishing.");
                            SuspendedAppActivity.this.finish();
                        }
                    });
        }
    }

    @Override
    public void onClick(DialogInterface dialog, int which) {
        switch (which) {
            case AlertDialog.BUTTON_NEUTRAL:
                switch (mNeutralButtonAction) {
                    case BUTTON_ACTION_MORE_DETAILS:
                        if (mMoreDetailsIntent != null) {
                            startActivityAsUser(mMoreDetailsIntent, mOptions,
                                    UserHandle.of(mSuspendingUserId));
                        } else {
                            Slog.wtf(TAG, "Neutral button should not have existed!");
                        }
                        break;
                    case BUTTON_ACTION_UNSUSPEND:
                        final IPackageManager ipm = AppGlobals.getPackageManager();
                        try {
                            final String[] errored = ipm.setPackagesSuspendedAsUser(
                                    new String[]{mSuspendedPackage}, false, null, null, null, 0,
                                    mSuspendingPackage, mUserId /* suspendingUserId */,
                                    mUserId /* targetUserId */);
                            if (ArrayUtils.contains(errored, mSuspendedPackage)) {
                                Slog.e(TAG, "Could not unsuspend " + mSuspendedPackage);
                                break;
                            }
                        } catch (RemoteException re) {
                            Slog.e(TAG, "Can't talk to system process", re);
                            break;
                        }
                        final Intent reportUnsuspend = new Intent()
                                .setAction(Intent.ACTION_PACKAGE_UNSUSPENDED_MANUALLY)
                                .putExtra(Intent.EXTRA_PACKAGE_NAME, mSuspendedPackage)
                                .setPackage(mSuspendingPackage)
                                .addFlags(Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
                        sendBroadcastAsUser(reportUnsuspend, UserHandle.of(mSuspendingUserId));

                        if (mOnUnsuspend != null) {
                            Bundle activityOptions =
                                    ActivityOptions.makeBasic()
                                            .setPendingIntentBackgroundActivityStartMode(
                                                    ActivityOptions
                                                            .MODE_BACKGROUND_ACTIVITY_START_ALLOWED)
                                            .toBundle();
                            try {
                                mOnUnsuspend.sendIntent(this, 0, null, null, null, null,
                                        activityOptions);
                            } catch (IntentSender.SendIntentException e) {
                                Slog.e(TAG, "Error while starting intent " + mOnUnsuspend, e);
                            }
                        }
                        break;
                    default:
                        Slog.e(TAG, "Unexpected action on neutral button: " + mNeutralButtonAction);
                        break;
                }
                break;
        }
        mUsm.reportUserInteraction(mSuspendingPackage, mUserId);
        finish();
    }

    public static Intent createSuspendedAppInterceptIntent(String suspendedPackage,
            UserPackage suspendingPackage, SuspendDialogInfo dialogInfo, Bundle options,
            IntentSender onUnsuspend, int userId) {
        Intent intent = new Intent()
                .setClassName("android", SuspendedAppActivity.class.getName())
                .putExtra(EXTRA_SUSPENDED_PACKAGE, suspendedPackage)
                .putExtra(EXTRA_DIALOG_INFO, dialogInfo)
                .putExtra(EXTRA_SUSPENDING_PACKAGE,
                        suspendingPackage != null ? suspendingPackage.packageName : null)
                .putExtra(EXTRA_UNSUSPEND_INTENT, onUnsuspend)
                .putExtra(EXTRA_ACTIVITY_OPTIONS, options)
                .putExtra(Intent.EXTRA_USER_ID, userId)
                .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                        | Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
        if (crossUserSuspensionEnabledRo() && suspendingPackage != null) {
            intent.putExtra(EXTRA_SUSPENDING_USER, suspendingPackage.userId);
        }
        return intent;
    }
}
