/*
 * Copyright 2020 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.quickaccesswallet;

import static android.service.quickaccesswallet.QuickAccessWalletService.ACTION_VIEW_WALLET;
import static android.service.quickaccesswallet.QuickAccessWalletService.ACTION_VIEW_WALLET_SETTINGS;
import static android.service.quickaccesswallet.QuickAccessWalletService.SERVICE_INTERFACE;

import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.ActivityManager;
import android.app.PendingIntent;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.content.pm.PackageManager.NameNotFoundException;
import android.content.pm.ResolveInfo;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.Settings;
import android.text.TextUtils;
import android.util.Log;

import com.android.internal.widget.LockPatternUtils;

import java.io.IOException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.UUID;
import java.util.concurrent.Executor;

/**
 * Implements {@link QuickAccessWalletClient}. The client connects, performs requests, waits for
 * responses, and disconnects automatically one minute after the last call is performed.
 *
 * @hide
 */
public class QuickAccessWalletClientImpl implements QuickAccessWalletClient, ServiceConnection {

    private static final String TAG = "QAWalletSClient";
    public static final String SETTING_KEY = "lockscreen_show_wallet";
    private final Handler mHandler;
    private final Context mContext;
    private final Queue<ApiCaller> mRequestQueue;
    private final Map<WalletServiceEventListener, String> mEventListeners;
    private final Executor mLifecycleExecutor;
    private boolean mIsConnected;
    /** Timeout for active service connections (1 minute) */
    private static final long SERVICE_CONNECTION_TIMEOUT_MS = 60 * 1000;

    @Nullable
    private IQuickAccessWalletService mService;

    @Nullable
    private final QuickAccessWalletServiceInfo mServiceInfo;

    private static final int MSG_TIMEOUT_SERVICE = 5;

    QuickAccessWalletClientImpl(@NonNull Context context, @Nullable Executor bgExecutor) {
        mContext = context.getApplicationContext();
        mServiceInfo = QuickAccessWalletServiceInfo.tryCreate(context);
        mHandler = new Handler(Looper.getMainLooper());
        mLifecycleExecutor = (bgExecutor == null) ? Runnable::run : bgExecutor;
        mRequestQueue = new ArrayDeque<>();
        mEventListeners = new HashMap<>(1);
    }

    @Override
    public boolean isWalletServiceAvailable() {
        return mServiceInfo != null;
    }

    @Override
    public boolean isWalletFeatureAvailable() {
        int currentUser = ActivityManager.getCurrentUser();
        return currentUser == UserHandle.USER_SYSTEM
                && checkUserSetupComplete()
                && !new LockPatternUtils(mContext).isUserInLockdown(currentUser);
    }

    @Override
    public boolean isWalletFeatureAvailableWhenDeviceLocked() {
        return checkSecureSetting(SETTING_KEY);
    }

    @Override
    public void getWalletCards(
            @NonNull GetWalletCardsRequest request,
            @NonNull OnWalletCardsRetrievedCallback callback) {
        getWalletCards(mContext.getMainExecutor(), request, callback);
    }

    @Override
    public void getWalletCards(
            @NonNull @CallbackExecutor Executor executor,
            @NonNull GetWalletCardsRequest request,
            @NonNull OnWalletCardsRetrievedCallback callback) {
        if (!isWalletServiceAvailable()) {
            executor.execute(
                    () -> callback.onWalletCardRetrievalError(new GetWalletCardsError(null, null)));
            return;
        }

        BaseCallbacks serviceCallback = new BaseCallbacks() {
            @Override
            public void onGetWalletCardsSuccess(GetWalletCardsResponse response) {
                executor.execute(() -> callback.onWalletCardsRetrieved(response));
            }

            @Override
            public void onGetWalletCardsFailure(GetWalletCardsError error) {
                executor.execute(() -> callback.onWalletCardRetrievalError(error));
            }
        };

        executeApiCall(new ApiCaller("onWalletCardsRequested") {
            @Override
            public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
                service.onWalletCardsRequested(request, serviceCallback);
            }

            @Override
            public void onApiError() {
                serviceCallback.onGetWalletCardsFailure(new GetWalletCardsError(null, null));
            }
        });
    }

    @Override
    public void selectWalletCard(@NonNull SelectWalletCardRequest request) {
        if (!isWalletServiceAvailable()) {
            return;
        }
        executeApiCall(new ApiCaller("onWalletCardSelected") {
            @Override
            public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
                service.onWalletCardSelected(request);
            }
        });
    }

    @Override
    public void notifyWalletDismissed() {
        if (!isWalletServiceAvailable()) {
            return;
        }
        executeApiCall(new ApiCaller("onWalletDismissed") {
            @Override
            public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
                service.onWalletDismissed();
            }
        });
    }

    @Override
    public void addWalletServiceEventListener(WalletServiceEventListener listener) {
        addWalletServiceEventListener(mContext.getMainExecutor(), listener);
    }

    @Override
    public void addWalletServiceEventListener(
            @NonNull @CallbackExecutor Executor executor,
            @NonNull WalletServiceEventListener listener) {
        if (!isWalletServiceAvailable()) {
            return;
        }
        BaseCallbacks callback = new BaseCallbacks() {
            @Override
            public void onWalletServiceEvent(WalletServiceEvent event) {
                executor.execute(() -> listener.onWalletServiceEvent(event));
            }
        };

        executeApiCall(new ApiCaller("registerListener") {
            @Override
            public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
                String listenerId = UUID.randomUUID().toString();
                WalletServiceEventListenerRequest request =
                        new WalletServiceEventListenerRequest(listenerId);
                mEventListeners.put(listener, listenerId);
                service.registerWalletServiceEventListener(request, callback);
            }
        });
    }

    @Override
    public void removeWalletServiceEventListener(WalletServiceEventListener listener) {
        if (!isWalletServiceAvailable()) {
            return;
        }
        executeApiCall(new ApiCaller("unregisterListener") {
            @Override
            public void performApiCall(IQuickAccessWalletService service) throws RemoteException {
                String listenerId = mEventListeners.remove(listener);
                if (listenerId == null) {
                    return;
                }
                WalletServiceEventListenerRequest request =
                        new WalletServiceEventListenerRequest(listenerId);
                service.unregisterWalletServiceEventListener(request);
            }
        });
    }

    @Override
    public void close() throws IOException {
        disconnect();
    }

    @Override
    public void disconnect() {
        mHandler.post(() -> disconnectInternal(true));
    }

    @Override
    @Nullable
    public Intent createWalletIntent() {
        if (mServiceInfo == null) {
            return null;
        }
        String packageName = mServiceInfo.getComponentName().getPackageName();
        String walletActivity = mServiceInfo.getWalletActivity();
        return createIntent(walletActivity, packageName, ACTION_VIEW_WALLET);
    }

    @Override
    public void getWalletPendingIntent(
            @NonNull @CallbackExecutor Executor executor,
            @NonNull WalletPendingIntentCallback pendingIntentCallback) {
        BaseCallbacks callbacks = new BaseCallbacks() {
            @Override
            public void onTargetActivityPendingIntentReceived(PendingIntent pendingIntent) {
                executor.execute(
                        () -> pendingIntentCallback.onWalletPendingIntentRetrieved(pendingIntent));
            }
        };
        executeApiCall(new ApiCaller("getTargetActivityPendingIntent") {
            @Override
            void performApiCall(IQuickAccessWalletService service) throws RemoteException {
                service.onTargetActivityIntentRequested(callbacks);
            }
        });
    }

    @Override
    @Nullable
    public Intent createWalletSettingsIntent() {
        if (mServiceInfo == null) {
            return null;
        }
        String packageName = mServiceInfo.getComponentName().getPackageName();
        String settingsActivity = mServiceInfo.getSettingsActivity();
        return createIntent(settingsActivity, packageName, ACTION_VIEW_WALLET_SETTINGS);
    }

    @Nullable
    private Intent createIntent(@Nullable String activityName, String packageName, String action) {
        PackageManager pm = mContext.getPackageManager();
        if (TextUtils.isEmpty(activityName)) {
            activityName = queryActivityForAction(pm, packageName, action);
        }
        if (TextUtils.isEmpty(activityName)) {
            return null;
        }
        ComponentName component = new ComponentName(packageName, activityName);
        if (!isActivityEnabled(pm, component)) {
            return null;
        }
        return new Intent(action).setComponent(component);
    }

    @Nullable
    private static String queryActivityForAction(PackageManager pm, String packageName,
            String action) {
        Intent intent = new Intent(action).setPackage(packageName);
        ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
        if (resolveInfo == null
                || resolveInfo.activityInfo == null
                || !resolveInfo.activityInfo.exported) {
            return null;
        }
        return resolveInfo.activityInfo.name;
    }

    private static boolean isActivityEnabled(PackageManager pm, ComponentName component) {
        int setting = pm.getComponentEnabledSetting(component);
        if (setting == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
            return true;
        }
        if (setting != PackageManager.COMPONENT_ENABLED_STATE_DEFAULT) {
            return false;
        }
        try {
            return pm.getActivityInfo(component, 0).isEnabled();
        } catch (NameNotFoundException e) {
            return false;
        }
    }

    @Override
    @Nullable
    public Drawable getLogo() {
        return mServiceInfo == null ? null : mServiceInfo.getWalletLogo(mContext);
    }

    @Nullable
    @Override
    public Drawable getTileIcon() {
        return mServiceInfo == null ? null : mServiceInfo.getTileIcon();
    }

    @Override
    @Nullable
    public CharSequence getServiceLabel() {
        return mServiceInfo == null ? null : mServiceInfo.getServiceLabel(mContext);
    }

    @Override
    @Nullable
    public CharSequence getShortcutShortLabel() {
        return mServiceInfo == null ? null : mServiceInfo.getShortcutShortLabel(mContext);
    }

    @Override
    public CharSequence getShortcutLongLabel() {
        return mServiceInfo == null ? null : mServiceInfo.getShortcutLongLabel(mContext);
    }

    private void connect() {
        mHandler.post(this::connectInternal);
    }

    private void connectInternal() {
        if (mServiceInfo == null) {
            Log.w(TAG, "Wallet service unavailable");
            return;
        }
        if (mIsConnected) {
            return;
        }
        mIsConnected = true;
        Intent intent = new Intent(SERVICE_INTERFACE);
        intent.setComponent(mServiceInfo.getComponentName());
        int flags = Context.BIND_AUTO_CREATE | Context.BIND_WAIVE_PRIORITY;
        mLifecycleExecutor.execute(() -> mContext.bindService(intent, this, flags));
        resetServiceConnectionTimeout();
    }

    private void onConnectedInternal(IQuickAccessWalletService service) {
        if (!mIsConnected) {
            Log.w(TAG, "onConnectInternal but connection closed");
            mService = null;
            return;
        }
        mService = service;
        for (ApiCaller apiCaller : new ArrayList<>(mRequestQueue)) {
            performApiCallInternal(apiCaller, mService);
            mRequestQueue.remove(apiCaller);
        }
    }

    /**
     * Resets the idle timeout for this connection by removing any pending timeout messages and
     * posting a new delayed message.
     */
    private void resetServiceConnectionTimeout() {
        mHandler.removeMessages(MSG_TIMEOUT_SERVICE);
        mHandler.postDelayed(
                () -> disconnectInternal(true),
                MSG_TIMEOUT_SERVICE,
                SERVICE_CONNECTION_TIMEOUT_MS);
    }

    private void disconnectInternal(boolean clearEventListeners) {
        if (!mIsConnected) {
            Log.w(TAG, "already disconnected");
            return;
        }
        if (clearEventListeners && !mEventListeners.isEmpty()) {
            for (WalletServiceEventListener listener : mEventListeners.keySet()) {
                removeWalletServiceEventListener(listener);
            }
            mHandler.post(() -> disconnectInternal(false));
            return;
        }
        mIsConnected = false;
        mLifecycleExecutor.execute(() -> mContext.unbindService(/*conn=*/ this));
        mService = null;
        mEventListeners.clear();
        mRequestQueue.clear();
    }

    private void executeApiCall(ApiCaller apiCaller) {
        mHandler.post(() -> executeInternal(apiCaller));
    }

    private void executeInternal(ApiCaller apiCaller) {
        if (mIsConnected && mService != null) {
            performApiCallInternal(apiCaller, mService);
        } else {
            mRequestQueue.add(apiCaller);
            connect();
        }
    }

    private void performApiCallInternal(ApiCaller apiCaller, IQuickAccessWalletService service) {
        if (service == null) {
            apiCaller.onApiError();
            return;
        }
        try {
            apiCaller.performApiCall(service);
            resetServiceConnectionTimeout();
        } catch (RemoteException e) {
            Log.w(TAG, "executeInternal error: " + apiCaller.mDesc, e);
            apiCaller.onApiError();
            disconnect();
        }
    }

    private abstract static class ApiCaller {
        private final String mDesc;

        private ApiCaller(String desc) {
            this.mDesc = desc;
        }

        abstract void performApiCall(IQuickAccessWalletService service)
                throws RemoteException;

        void onApiError() {
            Log.w(TAG, "api error: " + mDesc);
        }
    }

    @Override // ServiceConnection
    public void onServiceConnected(ComponentName name, IBinder binder) {
        IQuickAccessWalletService service = IQuickAccessWalletService.Stub.asInterface(binder);
        mHandler.post(() -> onConnectedInternal(service));
    }

    @Override // ServiceConnection
    public void onServiceDisconnected(ComponentName name) {
        // Do not disconnect, as we may later be re-connected
    }

    @Override // ServiceConnection
    public void onBindingDied(ComponentName name) {
        // This is a recoverable error but the client will need to reconnect.
        disconnect();
    }

    @Override // ServiceConnection
    public void onNullBinding(ComponentName name) {
        disconnect();
    }

    private boolean checkSecureSetting(String name) {
        return Settings.Secure.getInt(mContext.getContentResolver(), name, 0) == 1;
    }

    private boolean checkUserSetupComplete() {
        return Settings.Secure.getIntForUser(
                mContext.getContentResolver(),
                Settings.Secure.USER_SETUP_COMPLETE, 0,
                UserHandle.USER_CURRENT) == 1;
    }

    private static class BaseCallbacks extends IQuickAccessWalletServiceCallbacks.Stub {
        public void onGetWalletCardsSuccess(GetWalletCardsResponse response) {
            throw new IllegalStateException();
        }

        public void onGetWalletCardsFailure(GetWalletCardsError error) {
            throw new IllegalStateException();
        }

        public void onWalletServiceEvent(WalletServiceEvent event) {
            throw new IllegalStateException();
        }

        public void onTargetActivityPendingIntentReceived(PendingIntent pendingIntent) {
            throw new IllegalStateException();
        }
    }
}
