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

package org.chromium.ui.base;

import android.app.Activity;
import android.app.PendingIntent;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender.SendIntentException;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.PermissionInfo;
import android.os.Build;
import android.os.Handler;
import android.os.Process;
import android.text.TextUtils;
import android.util.SparseArray;
import android.view.View;

import org.chromium.base.ActivityState;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.ApplicationStatus;
import org.chromium.base.BuildInfo;
import org.chromium.base.Callback;
import org.chromium.base.ContextUtils;
import org.chromium.ui.UiUtils;

import java.lang.ref.WeakReference;

/**
 * The class provides the WindowAndroid's implementation which requires
 * Activity Instance.
 * Only instantiate this class when you need the implemented features.
 */
public class ActivityWindowAndroid
        extends WindowAndroid
        implements ApplicationStatus.ActivityStateListener, View.OnLayoutChangeListener {
    // Constants used for intent request code bounding.
    private static final int REQUEST_CODE_PREFIX = 1000;
    private static final int REQUEST_CODE_RANGE_SIZE = 100;

    private static final String PERMISSION_QUERIED_KEY_PREFIX = "HasRequestedAndroidPermission::";

    private final Handler mHandler;
    private final SparseArray<PermissionCallback> mOutstandingPermissionRequests;

    private int mNextRequestCode;

    /**
     * Creates an Activity-specific WindowAndroid with associated intent functionality.
     * TODO(jdduke): Remove this overload when all callsites have been updated to
     * indicate their activity state listening preference.
     * @param context Context wrapping an activity associated with the WindowAndroid.
     */
    public ActivityWindowAndroid(Context context) {
        this(context, true);
    }

    /**
     * Creates an Activity-specific WindowAndroid with associated intent functionality.
     * @param context Context wrapping an activity associated with the WindowAndroid.
     * @param listenToActivityState Whether to listen to activity state changes.
     */
    public ActivityWindowAndroid(Context context, boolean listenToActivityState) {
        super(context);
        Activity activity = activityFromContext(context);
        if (activity == null) {
            throw new IllegalArgumentException("Context is not and does not wrap an Activity");
        }
        mHandler = new Handler();
        mOutstandingPermissionRequests = new SparseArray<PermissionCallback>();
        if (listenToActivityState) {
            ApplicationStatus.registerStateListenerForActivity(this, activity);
        }

        setAndroidPermissionDelegate(new ActivityAndroidPermissionDelegate());
    }

    @Override
    protected void registerKeyboardVisibilityCallbacks() {
        Activity activity = getActivity().get();
        if (activity == null) return;
        View content = activity.findViewById(android.R.id.content);
        mIsKeyboardShowing = UiUtils.isKeyboardShowing(getActivity().get(), content);
        content.addOnLayoutChangeListener(this);
    }

    @Override
    protected void unregisterKeyboardVisibilityCallbacks() {
        Activity activity = getActivity().get();
        if (activity == null) return;
        activity.findViewById(android.R.id.content).removeOnLayoutChangeListener(this);
    }

    @Override
    public int showCancelableIntent(
            PendingIntent intent, IntentCallback callback, Integer errorId) {
        Activity activity = getActivity().get();
        if (activity == null) return START_INTENT_FAILURE;

        int requestCode = generateNextRequestCode();

        try {
            activity.startIntentSenderForResult(
                    intent.getIntentSender(), requestCode, new Intent(), 0, 0, 0);
        } catch (SendIntentException e) {
            return START_INTENT_FAILURE;
        }

        storeCallbackData(requestCode, callback, errorId);
        return requestCode;
    }

    @Override
    public int showCancelableIntent(Intent intent, IntentCallback callback, Integer errorId) {
        Activity activity = getActivity().get();
        if (activity == null) return START_INTENT_FAILURE;

        int requestCode = generateNextRequestCode();

        try {
            activity.startActivityForResult(intent, requestCode);
        } catch (ActivityNotFoundException e) {
            return START_INTENT_FAILURE;
        }

        storeCallbackData(requestCode, callback, errorId);
        return requestCode;
    }

    @Override
    public int showCancelableIntent(Callback<Integer> intentTrigger, IntentCallback callback,
            Integer errorId) {
        Activity activity = getActivity().get();
        if (activity == null) return START_INTENT_FAILURE;

        int requestCode = generateNextRequestCode();

        intentTrigger.onResult(requestCode);

        storeCallbackData(requestCode, callback, errorId);
        return requestCode;
    }

    @Override
    public void cancelIntent(int requestCode) {
        Activity activity = getActivity().get();
        if (activity == null) return;
        activity.finishActivity(requestCode);
    }

    /**
     * Responds to the intent result if the intent was created by the native window.
     * @param requestCode Request code of the requested intent.
     * @param resultCode Result code of the requested intent.
     * @param data The data returned by the intent.
     * @return Boolean value of whether the intent was started by the native window.
     */
    public boolean onActivityResult(int requestCode, int resultCode, Intent data) {
        IntentCallback callback = mOutstandingIntents.get(requestCode);
        mOutstandingIntents.delete(requestCode);
        String errorMessage = mIntentErrors.remove(requestCode);

        if (callback != null) {
            callback.onIntentCompleted(this, resultCode, data);
            return true;
        } else {
            if (errorMessage != null) {
                showCallbackNonExistentError(errorMessage);
                return true;
            }
        }
        return false;
    }

    private String getHasRequestedPermissionKey(String permission) {
        String permissionQueriedKey = permission;
        // Prior to O, permissions were granted at the group level.  Post O, each permission is
        // granted individually.
        if (!BuildInfo.isAtLeastO()) {
            try {
                // Runtime permissions are controlled at the group level.  So when determining
                // whether we have requested a particular permission before, we should check whether
                // we have requested any permission in that group as that mimics the logic in the
                // Android framework.
                //
                // e.g. Requesting first the permission ACCESS_FINE_LOCATION will result in Chrome
                //      treating ACCESS_COARSE_LOCATION as if it had already been requested as well.
                PermissionInfo permissionInfo =
                        getApplicationContext().getPackageManager().getPermissionInfo(
                                permission, PackageManager.GET_META_DATA);

                if (!TextUtils.isEmpty(permissionInfo.group)) {
                    permissionQueriedKey = permissionInfo.group;
                }
            } catch (NameNotFoundException e) {
                // Unknown permission.  Default back to the permission name instead of the group.
            }
        }

        return PERMISSION_QUERIED_KEY_PREFIX + permissionQueriedKey;
    }

    /**
     * Responds to a pending permission result.
     * @param requestCode The unique code for the permission request.
     * @param permissions The list of permissions in the result.
     * @param grantResults Whether the permissions were granted.
     * @return Whether the permission request corresponding to a pending permission request.
     */
    public boolean onRequestPermissionsResult(int requestCode, String[] permissions,
            int[] grantResults) {
        Activity activity = getActivity().get();
        assert activity != null;

        SharedPreferences.Editor editor = ContextUtils.getAppSharedPreferences().edit();
        for (int i = 0; i < permissions.length; i++) {
            editor.putBoolean(getHasRequestedPermissionKey(permissions[i]), true);
        }
        editor.apply();

        PermissionCallback callback = mOutstandingPermissionRequests.get(requestCode);
        mOutstandingPermissionRequests.delete(requestCode);
        if (callback == null) return false;
        callback.onRequestPermissionsResult(permissions, grantResults);
        return true;
    }

    @Override
    public WeakReference<Activity> getActivity() {
        return new WeakReference<Activity>(activityFromContext(getContext().get()));
    }

    @Override
    public void onActivityStateChange(Activity activity, int newState) {
        if (newState == ActivityState.STOPPED) {
            onActivityStopped();
        } else if (newState == ActivityState.STARTED) {
            onActivityStarted();
        }
    }

    @Override
    public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft,
            int oldTop, int oldRight, int oldBottom) {
        keyboardVisibilityPossiblyChanged(UiUtils.isKeyboardShowing(getActivity().get(), v));
    }

    private int generateNextRequestCode() {
        int requestCode = REQUEST_CODE_PREFIX + mNextRequestCode;
        mNextRequestCode = (mNextRequestCode + 1) % REQUEST_CODE_RANGE_SIZE;
        return requestCode;
    }

    private void storeCallbackData(int requestCode, IntentCallback callback, Integer errorId) {
        mOutstandingIntents.put(requestCode, callback);
        mIntentErrors.put(
                requestCode, errorId == null ? null : mApplicationContext.getString(errorId));
    }

    private class ActivityAndroidPermissionDelegate implements AndroidPermissionDelegate {
        @Override
        public boolean hasPermission(String permission) {
            return ApiCompatibilityUtils.checkPermission(
                    mApplicationContext, permission, Process.myPid(), Process.myUid())
                    == PackageManager.PERMISSION_GRANTED;
        }

        @Override
        public boolean canRequestPermission(String permission) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false;

            Activity activity = getActivity().get();
            if (activity == null) return false;

            if (isPermissionRevokedByPolicy(permission)) {
                return false;
            }

            if (activity.shouldShowRequestPermissionRationale(permission)) {
                return true;
            }

            // Check whether we have ever asked for this permission by checking whether we saved
            // a preference associated with it before.
            String permissionQueriedKey = getHasRequestedPermissionKey(permission);
            SharedPreferences prefs = ContextUtils.getAppSharedPreferences();
            if (!prefs.getBoolean(permissionQueriedKey, false)) return true;

            return false;
        }

        @Override
        public boolean isPermissionRevokedByPolicy(String permission) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false;

            Activity activity = getActivity().get();
            if (activity == null) return false;

            return activity.getPackageManager().isPermissionRevokedByPolicy(
                    permission, activity.getPackageName());
        }

        @Override
        public void requestPermissions(
                final String[] permissions, final PermissionCallback callback) {
            if (requestPermissionsInternal(permissions, callback)) return;

            // If the permission request was not sent successfully, just post a response to the
            // callback with whatever the current permission state is for all the requested
            // permissions.  The response is posted to keep the async behavior of this method
            // consistent.
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    int[] results = new int[permissions.length];
                    for (int i = 0; i < permissions.length; i++) {
                        results[i] = hasPermission(permissions[i])
                                ? PackageManager.PERMISSION_GRANTED
                                : PackageManager.PERMISSION_DENIED;
                    }
                    callback.onRequestPermissionsResult(permissions, results);
                }
            });
        }

        /**
         * Issues the permission request and returns whether it was sent successfully.
         */
        private boolean requestPermissionsInternal(
                String[] permissions, PermissionCallback callback) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) return false;
            Activity activity = getActivity().get();
            if (activity == null) return false;

            int requestCode = generateNextRequestCode();
            mOutstandingPermissionRequests.put(requestCode, callback);
            activity.requestPermissions(permissions, requestCode);
            return true;
        }
    }
}