/*
 * Copyright (C) 2019 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.intentresolver.chooser;

import android.app.Activity;
import android.app.prediction.AppTarget;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ResolveInfo;
import android.content.pm.ShortcutInfo;
import android.graphics.drawable.Icon;
import android.os.Bundle;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.service.chooser.ChooserTarget;
import android.text.SpannableStringBuilder;
import android.util.HashedStringCache;
import android.util.Log;

import androidx.annotation.Nullable;

import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;

import java.util.ArrayList;
import java.util.List;

/**
 * Live target, currently selectable by the user.
 * @see NotSelectableTargetInfo
 */
public final class SelectableTargetInfo extends ChooserTargetInfo {
    private static final String TAG = "SelectableTargetInfo";

    private interface TargetHashProvider {
        HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context);
    }

    private interface TargetActivityStarter {
        boolean start(Activity activity, Bundle options);
        boolean startAsCaller(Activity activity, Bundle options, int userId);
        boolean startAsUser(Activity activity, Bundle options, UserHandle user);
    }

    private static final String HASHED_STRING_CACHE_TAG = "ChooserActivity";  // For legacy reasons.
    private static final int DEFAULT_SALT_EXPIRATION_DAYS = 7;

    private final int mMaxHashSaltDays = DeviceConfig.getInt(
            DeviceConfig.NAMESPACE_SYSTEMUI,
            SystemUiDeviceConfigFlags.HASH_SALT_MAX_DAYS,
            DEFAULT_SALT_EXPIRATION_DAYS);

    @Nullable
    private final DisplayResolveInfo mSourceInfo;
    @Nullable
    private final ResolveInfo mBackupResolveInfo;
    private final Intent mResolvedIntent;
    private final String mDisplayLabel;
    @Nullable
    private final AppTarget mAppTarget;
    @Nullable
    private final ShortcutInfo mShortcutInfo;

    private final ComponentName mChooserTargetComponentName;
    private final CharSequence mChooserTargetUnsanitizedTitle;
    private final Icon mChooserTargetIcon;
    private final Bundle mChooserTargetIntentExtras;
    private final boolean mIsPinned;
    private final float mModifiedScore;
    private final boolean mIsSuspended;
    private final ComponentName mResolvedComponentName;
    private final Intent mBaseIntentToSend;
    private final ResolveInfo mResolveInfo;
    private final List<Intent> mAllSourceIntents;
    private final IconHolder mDisplayIconHolder = new SettableIconHolder();
    private final TargetHashProvider mHashProvider;
    private final TargetActivityStarter mActivityStarter;

    /**
     * An intent containing referrer URI (see {@link Activity#getReferrer()} (possibly {@code null})
     * in its extended data under the key {@link Intent#EXTRA_REFERRER}.
     */
    private final Intent mReferrerFillInIntent;

    /**
     * Create a new {@link TargetInfo} instance representing a selectable target. Some target
     * parameters are copied over from the (deprecated) legacy {@link ChooserTarget} structure.
     *
     * @deprecated Use the overload that doesn't call for a {@link ChooserTarget}.
     */
    @Deprecated
    public static TargetInfo newSelectableTargetInfo(
            @Nullable DisplayResolveInfo sourceInfo,
            @Nullable ResolveInfo backupResolveInfo,
            Intent resolvedIntent,
            ChooserTarget chooserTarget,
            float modifiedScore,
            @Nullable ShortcutInfo shortcutInfo,
            @Nullable AppTarget appTarget,
            Intent referrerFillInIntent) {
        return newSelectableTargetInfo(
                sourceInfo,
                backupResolveInfo,
                resolvedIntent,
                chooserTarget.getComponentName(),
                chooserTarget.getTitle(),
                chooserTarget.getIcon(),
                chooserTarget.getIntentExtras(),
                modifiedScore,
                shortcutInfo,
                appTarget,
                referrerFillInIntent);
    }

    /**
     * Create a new {@link TargetInfo} instance representing a selectable target. `chooserTarget*`
     * parameters were historically retrieved from (now-deprecated) {@link ChooserTarget} structures
     * even when the {@link TargetInfo} was a system (internal) synthesized target that never needed
     * to be represented as a {@link ChooserTarget}. The values passed here are copied in directly
     * as if they had been provided in the legacy representation.
     *
     * TODO: clarify semantics of how clients use the `getChooserTarget*()` methods; refactor/rename
     * to avoid making reference to the legacy type; and reflect the improved semantics in the
     * signature (and documentation) of this method.
     */
    public static TargetInfo newSelectableTargetInfo(
            @Nullable DisplayResolveInfo sourceInfo,
            @Nullable ResolveInfo backupResolveInfo,
            Intent resolvedIntent,
            ComponentName chooserTargetComponentName,
            CharSequence chooserTargetUnsanitizedTitle,
            Icon chooserTargetIcon,
            @Nullable Bundle chooserTargetIntentExtras,
            float modifiedScore,
            @Nullable ShortcutInfo shortcutInfo,
            @Nullable AppTarget appTarget,
            Intent referrerFillInIntent) {
        return new SelectableTargetInfo(
                sourceInfo,
                backupResolveInfo,
                resolvedIntent,
                null,
                chooserTargetComponentName,
                chooserTargetUnsanitizedTitle,
                chooserTargetIcon,
                chooserTargetIntentExtras,
                modifiedScore,
                shortcutInfo,
                appTarget,
                referrerFillInIntent);
    }

    private SelectableTargetInfo(
            @Nullable DisplayResolveInfo sourceInfo,
            @Nullable ResolveInfo backupResolveInfo,
            Intent resolvedIntent,
            @Nullable Intent baseIntentToSend,
            ComponentName chooserTargetComponentName,
            CharSequence chooserTargetUnsanitizedTitle,
            Icon chooserTargetIcon,
            Bundle chooserTargetIntentExtras,
            float modifiedScore,
            @Nullable ShortcutInfo shortcutInfo,
            @Nullable AppTarget appTarget,
            Intent referrerFillInIntent) {
        mSourceInfo = sourceInfo;
        mBackupResolveInfo = backupResolveInfo;
        mResolvedIntent = resolvedIntent;
        mModifiedScore = modifiedScore;
        mShortcutInfo = shortcutInfo;
        mAppTarget = appTarget;
        mReferrerFillInIntent = referrerFillInIntent;
        mChooserTargetComponentName = chooserTargetComponentName;
        mChooserTargetUnsanitizedTitle = chooserTargetUnsanitizedTitle;
        mChooserTargetIcon = chooserTargetIcon;
        mChooserTargetIntentExtras = chooserTargetIntentExtras;

        mIsPinned = (shortcutInfo != null) && shortcutInfo.isPinned();
        mDisplayLabel = sanitizeDisplayLabel(mChooserTargetUnsanitizedTitle);
        mIsSuspended = (mSourceInfo != null) && mSourceInfo.isSuspended();
        mResolveInfo = (mSourceInfo != null) ? mSourceInfo.getResolveInfo() : mBackupResolveInfo;

        mResolvedComponentName = getResolvedComponentName(mSourceInfo, mBackupResolveInfo);

        mBaseIntentToSend = getBaseIntentToSend(
                baseIntentToSend,
                mResolvedIntent,
                mReferrerFillInIntent);

        mAllSourceIntents = getAllSourceIntents(sourceInfo, mBaseIntentToSend);

        mHashProvider = context -> {
            final String plaintext =
                    getChooserTargetComponentName().getPackageName()
                    + mChooserTargetUnsanitizedTitle;
            return HashedStringCache.getInstance().hashString(
                    context,
                    HASHED_STRING_CACHE_TAG,
                    plaintext,
                    mMaxHashSaltDays);
        };

        mActivityStarter = new TargetActivityStarter() {
            @Override
            public boolean start(Activity activity, Bundle options) {
                throw new RuntimeException("ChooserTargets should be started as caller.");
            }

            @Override
            public boolean startAsCaller(Activity activity, Bundle options, int userId) {
                final Intent intent = mBaseIntentToSend;
                if (intent == null) {
                    return false;
                }
                intent.setComponent(getChooserTargetComponentName());
                intent.putExtras(mChooserTargetIntentExtras);
                TargetInfo.prepareIntentForCrossProfileLaunch(intent, userId);

                // Important: we will ignore the target security checks in ActivityManager if and
                // only if the ChooserTarget's target package is the same package where we got the
                // ChooserTargetService that provided it. This lets a ChooserTargetService provide
                // a non-exported or permission-guarded target for the user to pick.
                //
                // If mSourceInfo is null, we got this ChooserTarget from the caller or elsewhere
                // so we'll obey the caller's normal security checks.
                final boolean ignoreTargetSecurity = (mSourceInfo != null)
                        && mSourceInfo.getResolvedComponentName().getPackageName()
                                .equals(getChooserTargetComponentName().getPackageName());
                activity.startActivityAsCaller(intent, options, ignoreTargetSecurity, userId);
                return true;
            }

            @Override
            public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
                throw new RuntimeException("ChooserTargets should be started as caller.");
            }
        };
    }

    private SelectableTargetInfo(SelectableTargetInfo other, Intent baseIntentToSend) {
        this(
                other.mSourceInfo,
                other.mBackupResolveInfo,
                other.mResolvedIntent,
                baseIntentToSend,
                other.mChooserTargetComponentName,
                other.mChooserTargetUnsanitizedTitle,
                other.mChooserTargetIcon,
                other.mChooserTargetIntentExtras,
                other.mModifiedScore,
                other.mShortcutInfo,
                other.mAppTarget,
                other.mReferrerFillInIntent);
    }

    @Override
    @Nullable
    public TargetInfo tryToCloneWithAppliedRefinement(Intent proposedRefinement) {
        Intent matchingBase =
                getAllSourceIntents()
                        .stream()
                        .filter(i -> i.filterEquals(proposedRefinement))
                        .findFirst()
                        .orElse(null);
        if (matchingBase == null) {
            return null;
        }

        return new SelectableTargetInfo(
                this,
                TargetInfo.mergeRefinementIntoMatchingBaseIntent(matchingBase, proposedRefinement));
    }

    @Override
    public HashedStringCache.HashResult getHashedTargetIdForMetrics(Context context) {
        return mHashProvider.getHashedTargetIdForMetrics(context);
    }

    @Override
    public boolean isSelectableTargetInfo() {
        return true;
    }

    @Override
    public boolean isSuspended() {
        return mIsSuspended;
    }

    @Override
    @Nullable
    public DisplayResolveInfo getDisplayResolveInfo() {
        return mSourceInfo;
    }

    @Override
    public float getModifiedScore() {
        return mModifiedScore;
    }

    @Override
    public Intent getResolvedIntent() {
        return mResolvedIntent;
    }

    @Override
    public ComponentName getResolvedComponentName() {
        return mResolvedComponentName;
    }

    @Override
    public ComponentName getChooserTargetComponentName() {
        return mChooserTargetComponentName;
    }

    @Nullable
    public Icon getChooserTargetIcon() {
        return mChooserTargetIcon;
    }

    @Override
    public boolean startAsCaller(Activity activity, Bundle options, int userId) {
        return mActivityStarter.startAsCaller(activity, options, userId);
    }

    @Override
    public boolean startAsUser(Activity activity, Bundle options, UserHandle user) {
        return mActivityStarter.startAsUser(activity, options, user);
    }

    @Nullable
    @Override
    public Intent getTargetIntent() {
        return mBaseIntentToSend;
    }

    @Override
    public ResolveInfo getResolveInfo() {
        return mResolveInfo;
    }

    @Override
    public CharSequence getDisplayLabel() {
        return mDisplayLabel;
    }

    @Override
    public CharSequence getExtendedInfo() {
        // ChooserTargets have badge icons, so we won't show the extended info to disambiguate.
        return null;
    }

    @Override
    public IconHolder getDisplayIconHolder() {
        return mDisplayIconHolder;
    }

    @Override
    @Nullable
    public ShortcutInfo getDirectShareShortcutInfo() {
        return mShortcutInfo;
    }

    @Override
    @Nullable
    public AppTarget getDirectShareAppTarget() {
        return mAppTarget;
    }

    @Override
    public List<Intent> getAllSourceIntents() {
        return mAllSourceIntents;
    }

    @Override
    public boolean isPinned() {
        return mIsPinned;
    }

    private static String sanitizeDisplayLabel(CharSequence label) {
        SpannableStringBuilder sb = new SpannableStringBuilder(label);
        sb.clearSpans();
        return sb.toString();
    }

    private static List<Intent> getAllSourceIntents(
            @Nullable DisplayResolveInfo sourceInfo, Intent fallbackSourceIntent) {
        final List<Intent> results = new ArrayList<>();
        if (sourceInfo != null) {
            results.addAll(sourceInfo.getAllSourceIntents());
        } else {
            // This target wasn't joined to a `DisplayResolveInfo` result from our intent-resolution
            // step, so it was provided directly by the caller. We don't support alternate intents
            // in this case, but we still permit refinement of the intent we'll dispatch; e.g.,
            // clients may use this hook to defer the computation of "lazy" extras in their share
            // payload. Note this accommodation isn't strictly "necessary" because clients could
            // always implement equivalent behavior by pointing custom targets back at their own app
            // for any amount of further refinement/modification outside of the Sharesheet flow;
            // nevertheless, it's offered as a convenience for clients who may expect their normal
            // refinement logic to apply equally in the case of these "special targets."
            results.add(fallbackSourceIntent);
        }
        return results;
    }

    private static ComponentName getResolvedComponentName(
            @Nullable DisplayResolveInfo sourceInfo, ResolveInfo backupResolveInfo) {
        if (sourceInfo != null) {
            return sourceInfo.getResolvedComponentName();
        } else if (backupResolveInfo != null) {
            return new ComponentName(
                    backupResolveInfo.activityInfo.packageName,
                    backupResolveInfo.activityInfo.name);
        }
        return null;
    }

    @Nullable
    private static Intent getBaseIntentToSend(
            @Nullable Intent providedBase,
            @Nullable Intent fallbackBase,
            Intent referrerFillInIntent) {
        Intent result = (providedBase != null) ? providedBase : fallbackBase;
        if (result == null) {
            Log.e(TAG, "ChooserTargetInfo: no base intent available to send");
        } else {
            result = new Intent(result);
            result.fillIn(referrerFillInIntent, 0);
        }
        return result;
    }
}