// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.chrome.browser.webapps;

import android.app.Activity;
import android.app.ActivityOptions;
import android.app.PendingIntent;
import android.content.Intent;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.Messenger;
import android.os.RemoteException;

import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;

import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.BuildInfo;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.base.Log;
import org.chromium.base.task.TaskTraits;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.browserservices.intents.WebApkExtras;
import org.chromium.chrome.browser.browserservices.metrics.WebApkUmaRecorder;
import org.chromium.chrome.browser.browserservices.permissiondelegation.InstalledWebappPermissionManager;
import org.chromium.chrome.browser.browserservices.permissiondelegation.PermissionStatus;
import org.chromium.chrome.browser.notifications.NotificationBuilderBase;
import org.chromium.chrome.browser.notifications.NotificationUmaTracker;
import org.chromium.chrome.browser.notifications.channels.ChromeChannelDefinitions;
import org.chromium.components.browser_ui.notifications.NotificationMetadata;
import org.chromium.components.content_settings.ContentSettingValues;
import org.chromium.components.content_settings.ContentSettingsType;
import org.chromium.components.embedder_support.util.Origin;
import org.chromium.webapk.lib.client.WebApkServiceConnectionManager;
import org.chromium.webapk.lib.runtime_library.IWebApkApi;

/**
 * Provides APIs for browsers to communicate with WebAPK services. Each WebAPK has its own "WebAPK
 * service".
 */
public class WebApkServiceClient {
    // Callback which catches RemoteExceptions thrown due to IWebApkApi failure.
    @FunctionalInterface
    private interface ApiUseCallback extends WebApkServiceConnectionManager.ConnectionCallback {
        void useApi(IWebApkApi api) throws RemoteException;

        @Override
        default void onConnected(IBinder api) {
            if (api == null) {
                return;
            }

            try {
                useApi(IWebApkApi.Stub.asInterface(api));
            } catch (RemoteException e) {
                Log.w(TAG, "WebApkAPI use failed.", e);
            }
        }
    }

    @VisibleForTesting
    public static final String CATEGORY_WEBAPK_API = "android.intent.category.WEBAPK_API";
    private static final String TAG = "WebApkServiceClient";

    // An intent extra for a {@link Messenger}.
    private static final String EXTRA_MESSENGER = "messenger";

    // A bundle key for a {@link PermissionStatus}.
    private static final String KEY_PERMISSION_STATUS = "permissionStatus";

    private static WebApkServiceClient sInstance;

    /** Manages connections between the browser application and WebAPK services. */
    private WebApkServiceConnectionManager mConnectionManager;

    public static WebApkServiceClient getInstance() {
        if (sInstance == null) {
            sInstance = new WebApkServiceClient();
        }
        return sInstance;
    }

    private WebApkServiceClient() {
        mConnectionManager = new WebApkServiceConnectionManager(
                TaskTraits.UI_DEFAULT, CATEGORY_WEBAPK_API, null /* action */);
    }

    /**
     * Gets the notification permission setting for the package.
     * @param permissionCallback To be called on a background thread with a permission setting, one
     *         of {@link ContentSettingValues}.
     */
    public void checkNotificationPermission(
            String webApkPackage, Callback<Integer> permissionCallback) {
        connect(webApkPackage, api -> {
            @ContentSettingValues
            int settingValue = toContentSettingValue(api.checkNotificationPermission());
            permissionCallback.onResult(settingValue);
        });
    }

    /**
     * Requests the notification permission for the package.
     * @param permissionCallback To be called on a background thread with a permission setting, one
     *         of {@link ContentSettingValues}.
     */
    public void requestNotificationPermission(
            String webApkPackage, Callback<Integer> permissionCallback) {
        if (!BuildInfo.isAtLeastT()) {
            Log.w(TAG, "Requesting notification permission is not supported before T.");
            return;
        }
        connect(webApkPackage, api -> {
            String channelName = ContextUtils.getApplicationContext().getString(
                    R.string.webapk_notification_channel_name);

            PendingIntent permissionRequestIntent = api.requestNotificationPermission(
                    channelName, ChromeChannelDefinitions.CHANNEL_ID_WEBAPKS);
            if (permissionRequestIntent == null) {
                permissionCallback.onResult(ContentSettingValues.ASK);
                return;
            }

            Handler handler = new Handler(Looper.getMainLooper(), message -> {
                @ContentSettingValues
                int settingValue = toContentSettingValue(
                        message.getData().getInt(KEY_PERMISSION_STATUS, PermissionStatus.BLOCK));
                permissionCallback.onResult(settingValue);
                return true;
            });
            Intent extraIntent = new Intent();
            extraIntent.putExtra(EXTRA_MESSENGER, new Messenger(handler));
            try {
                ActivityOptions options = ActivityOptions.makeBasic();
                ApiCompatibilityUtils.setActivityOptionsBackgroundActivityStartMode(options);
                permissionRequestIntent.send(ContextUtils.getApplicationContext(), 0, extraIntent,
                        null, null, null, options.toBundle());
            } catch (PendingIntent.CanceledException e) {
                Log.e(TAG, "The PendingIntent was canceled.", e);
            }
        });
    }

    /**
     * Connects to a WebAPK's bound service, builds a notification and hands it over to the WebAPK
     * to display. Handing over the notification makes the notification look like it originated from
     * the WebAPK - not Chrome - in the Android UI.
     */
    public void notifyNotification(final String originString, final String webApkPackage,
            final NotificationBuilderBase notificationBuilder, final String platformTag,
            final int platformID) {
        connect(webApkPackage, api -> {
            fallbackToWebApkIconIfNecessary(
                    notificationBuilder, webApkPackage, api.getSmallIconId());

            @ContentSettingValues
            int settingValue = toContentSettingValue(api.checkNotificationPermission());

            // See http://crbug.com/1340854. Temporary fallback in case the shell has not yet been
            // updated to support checkNotificationPermission(). Delete this after shell v154 has
            // been fully launched.
            if (settingValue != ContentSettingValues.ALLOW && api.notificationPermissionEnabled()) {
                Log.d(TAG, "Fallback to notificationPermissionEnabled().");
                settingValue = ContentSettingValues.ALLOW;
            }

            WebApkUmaRecorder.recordNotificationPermissionStatus(settingValue);

            if (settingValue != ContentSettingValues.ALLOW) {
                Origin origin = Origin.create(originString);
                if (origin == null) {
                    Log.w(TAG, "String (%s) could not be parsed as Origin.", originString);
                    return;
                }
                if (BuildInfo.isAtLeastT()) {
                    InstalledWebappPermissionManager.get().updatePermission(
                            origin, webApkPackage, ContentSettingsType.NOTIFICATIONS, settingValue);
                }
                return;
            }

            String channelName = null;
            if (webApkTargetsAtLeastO(webApkPackage)) {
                notificationBuilder.setChannelId(ChromeChannelDefinitions.CHANNEL_ID_WEBAPKS);
                channelName = ContextUtils.getApplicationContext().getString(
                        R.string.webapk_notification_channel_name);
            }
            NotificationMetadata metadata = new NotificationMetadata(
                    NotificationUmaTracker.SystemNotificationType.WEBAPK, platformTag, platformID);

            api.notifyNotificationWithChannel(platformTag, platformID,
                    notificationBuilder.build(metadata).getNotification(), channelName);
        });
    }

    private void fallbackToWebApkIconIfNecessary(NotificationBuilderBase builder,
            String webApkPackage, int iconId) {
        Bitmap icon = decodeImageResourceFromPackage(webApkPackage, iconId);
        if (!builder.hasSmallIconForContent()) {
            builder.setContentSmallIconForRemoteApp(icon);
        }
        if (!builder.hasStatusBarIconBitmap()) {
            builder.setStatusBarIconForRemoteApp(iconId, icon);
        }
    }

    /** Cancels notification previously shown by WebAPK. */
    public void cancelNotification(
            String webApkPackage, final String platformTag, final int platformID) {
        connect(webApkPackage, api -> api.cancelNotification(platformTag, platformID));
    }

    /** Finishes and removes the WebAPK's task. */
    public void finishAndRemoveTaskSdk23(final Activity activity, WebApkExtras webApkExtras) {
        connect(webApkExtras.webApkPackageName, api -> {
            if (activity.isFinishing() || activity.isDestroyed()) return;

            if (!api.finishAndRemoveTaskSdk23()) {
                // If |activity| is not the root of the task, hopefully the activities below
                // this one will close themselves.
                activity.finish();
            }
        });
    }

    /** Returns whether there are any WebAPK service API calls in progress. */
    public static boolean hasPendingWork() {
        return sInstance != null && !sInstance.mConnectionManager.didAllConnectCallbacksRun();
    }

    /** Disconnects all the connections to WebAPK services. */
    public static void disconnectAll() {
        if (sInstance == null) return;

        sInstance.mConnectionManager.disconnectAll(ContextUtils.getApplicationContext());
    }

    /** Returns whether the WebAPK targets SDK 26+. */
    private boolean webApkTargetsAtLeastO(String webApkPackage) {
        try {
            ApplicationInfo info =
                    ContextUtils.getApplicationContext().getPackageManager().getApplicationInfo(
                            webApkPackage, 0);
            return info.targetSdkVersion >= Build.VERSION_CODES.O;
        } catch (PackageManager.NameNotFoundException e) {
        }

        return false;
    }

    /** Decodes into a Bitmap an Image resource stored in an APK with the given package name. */
    private static @Nullable Bitmap decodeImageResourceFromPackage(
            String packageName, int resourceId) {
        PackageManager packageManager = ContextUtils.getApplicationContext().getPackageManager();
        try {
            Resources resources = packageManager.getResourcesForApplication(packageName);
            return BitmapFactory.decodeResource(resources, resourceId);
        } catch (PackageManager.NameNotFoundException e) {
            return null;
        }
    }

    private void connect(String webApkPackage, ApiUseCallback connectionCallback) {
        mConnectionManager.connect(
                ContextUtils.getApplicationContext(), webApkPackage, connectionCallback);
    }

    private static @ContentSettingValues int toContentSettingValue(
            @PermissionStatus int permissionStatus) {
        if (permissionStatus == PermissionStatus.ALLOW) {
            return ContentSettingValues.ALLOW;
        }

        if (permissionStatus == PermissionStatus.ASK) {
            return ContentSettingValues.ASK;
        }

        return ContentSettingValues.BLOCK;
    }
}
