/*
 * Copyright (C) 2021 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 android.service.displayhash;

import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemApi;
import android.app.Service;
import android.content.Intent;
import android.graphics.Rect;
import android.hardware.HardwareBuffer;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteCallback;
import android.view.displayhash.DisplayHash;
import android.view.displayhash.DisplayHashResultCallback;
import android.view.displayhash.VerifiedDisplayHash;

import java.util.Map;

/**
 * A service that handles generating and verify {@link DisplayHash}.
 *
 * The service will generate a DisplayHash based on arguments passed in. Then later that
 * same DisplayHash can be verified to determine that it was created by the system.
 *
 * @hide
 */
@SystemApi
public abstract class DisplayHashingService extends Service {

    /** @hide **/
    public static final String EXTRA_VERIFIED_DISPLAY_HASH =
            "android.service.displayhash.extra.VERIFIED_DISPLAY_HASH";

    /** @hide **/
    public static final String EXTRA_INTERVAL_BETWEEN_REQUESTS =
            "android.service.displayhash.extra.INTERVAL_BETWEEN_REQUESTS";

    /**
     * The {@link Intent} action that must be declared as handled by a service in its manifest
     * for the system to recognize it as a DisplayHash providing service.
     *
     * @hide
     */
    @SystemApi
    public static final String SERVICE_INTERFACE =
            "android.service.displayhash.DisplayHashingService";

    private DisplayHashingServiceWrapper mWrapper;
    private Handler mHandler;

    @Override
    public void onCreate() {
        super.onCreate();
        mWrapper = new DisplayHashingServiceWrapper();
        mHandler = new Handler(Looper.getMainLooper(), null, true);
    }

    @NonNull
    @Override
    public final IBinder onBind(@NonNull Intent intent) {
        return mWrapper;
    }

    /**
     * Generates the DisplayHash that can be used to validate that the system generated the
     * token.
     *
     * @param salt          The salt to use when generating the hmac. This should be unique to the
     *                      caller so the token cannot be verified by any other process.
     * @param buffer        The buffer for the content to generate the hash for.
     * @param bounds        The size and position of the content in window space.
     * @param hashAlgorithm The String for the hashing algorithm to use based values in
     *                      {@link #getDisplayHashAlgorithms(RemoteCallback)}.
     * @param callback      The callback to invoke
     *                      {@link DisplayHashResultCallback#onDisplayHashResult(DisplayHash)}
     *                      if successfully generated a DisplayHash or {@link
     *                      DisplayHashResultCallback#onDisplayHashError(int)} if failed.
     */
    public abstract void onGenerateDisplayHash(@NonNull byte[] salt,
            @NonNull HardwareBuffer buffer, @NonNull Rect bounds,
            @NonNull String hashAlgorithm, @NonNull DisplayHashResultCallback callback);

    /**
     * Returns a map of supported algorithms and their {@link DisplayHashParams}
     */
    @NonNull
    public abstract Map<String, DisplayHashParams> onGetDisplayHashAlgorithms();

    /**
     * Call to verify that the DisplayHash passed in was generated by the system.
     *
     * @param salt        The salt value to use when verifying the hmac. This should be the
     *                    same value that was passed to
     *                    {@link #onGenerateDisplayHash(byte[],
     *                    HardwareBuffer, Rect, String, DisplayHashResultCallback)} to
     *                    generate the token.
     * @param displayHash The token to verify that it was generated by the system.
     * @return a {@link VerifiedDisplayHash} if the provided display hash was originally generated
     * by the system or null if the system did not generate the display hash.
     */
    @Nullable
    public abstract VerifiedDisplayHash onVerifyDisplayHash(@NonNull byte[] salt,
            @NonNull DisplayHash displayHash);

    private void verifyDisplayHash(byte[] salt, DisplayHash displayHash,
            RemoteCallback callback) {
        VerifiedDisplayHash verifiedDisplayHash = onVerifyDisplayHash(salt,
                displayHash);
        final Bundle data = new Bundle();
        data.putParcelable(EXTRA_VERIFIED_DISPLAY_HASH, verifiedDisplayHash);
        callback.sendResult(data);
    }

    private void getDisplayHashAlgorithms(RemoteCallback callback) {
        Map<String, DisplayHashParams> displayHashParams = onGetDisplayHashAlgorithms();
        final Bundle data = new Bundle();
        for (Map.Entry<String, DisplayHashParams> entry : displayHashParams.entrySet()) {
            data.putParcelable(entry.getKey(), entry.getValue());
        }
        callback.sendResult(data);
    }

    /**
     * Call to get the interval required between display hash requests. Requests made faster than
     * this will be throttled.
     *
     * @return the interval value required between requests.
     */
    public abstract int onGetIntervalBetweenRequestsMillis();

    private void getDurationBetweenRequestsMillis(RemoteCallback callback) {
        int durationBetweenRequestMillis = onGetIntervalBetweenRequestsMillis();
        Bundle data = new Bundle();
        data.putInt(EXTRA_INTERVAL_BETWEEN_REQUESTS, durationBetweenRequestMillis);
        callback.sendResult(data);
    }

    private final class DisplayHashingServiceWrapper extends IDisplayHashingService.Stub {
        @Override
        public void generateDisplayHash(byte[] salt, HardwareBuffer buffer, Rect bounds,
                String hashAlgorithm, RemoteCallback callback) {
            mHandler.sendMessage(
                    obtainMessage(DisplayHashingService::onGenerateDisplayHash,
                            DisplayHashingService.this, salt, buffer, bounds,
                            hashAlgorithm, new DisplayHashResultCallback() {
                                @Override
                                public void onDisplayHashResult(
                                        @NonNull DisplayHash displayHash) {
                                    Bundle result = new Bundle();
                                    result.putParcelable(EXTRA_DISPLAY_HASH, displayHash);
                                    callback.sendResult(result);
                                }

                                @Override
                                public void onDisplayHashError(int errorCode) {
                                    Bundle result = new Bundle();
                                    result.putInt(EXTRA_DISPLAY_HASH_ERROR_CODE, errorCode);
                                    callback.sendResult(result);
                                }
                            }));
        }

        @Override
        public void verifyDisplayHash(byte[] salt, DisplayHash displayHash,
                RemoteCallback callback) {
            mHandler.sendMessage(
                    obtainMessage(DisplayHashingService::verifyDisplayHash,
                            DisplayHashingService.this, salt, displayHash, callback));
        }

        @Override
        public void getDisplayHashAlgorithms(RemoteCallback callback) {
            mHandler.sendMessage(obtainMessage(DisplayHashingService::getDisplayHashAlgorithms,
                    DisplayHashingService.this, callback));
        }

        @Override
        public void getIntervalBetweenRequestsMillis(RemoteCallback callback) {
            mHandler.sendMessage(
                    obtainMessage(DisplayHashingService::getDurationBetweenRequestsMillis,
                            DisplayHashingService.this, callback));
        }
    }
}
