/*
* Copyright (C) 2015 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.internal.telephony;

import android.annotation.NonNull;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.ServiceConnection;
import android.content.pm.ComponentInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerExecutor;
import android.os.IBinder;
import android.os.Message;
import android.os.Process;
import android.os.SystemClock;
import android.os.UserHandle;
import android.service.carrier.CarrierService;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.LocalLog;
import android.util.Log;
import android.util.SparseArray;

import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.telephony.util.TelephonyUtils;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Set;

/**
 * Manages long-lived bindings to carrier services
 * @hide
 */
public class CarrierServiceBindHelper {
    private static final String LOG_TAG = "CarrierSvcBindHelper";

    // TODO(b/201423849): Remove the UNBIND_DELAY_MILLIS and switch to CarrierPrivilegesCallback
    // The grace period has been replaced by CarrierPrivilegesTracker. CarrierPrivilegesCallback has
    // provided the callback for both carrier privileges change and carrier service change (with
    // awareness of the grace period), the delay based logic here should be cleaned up.
    /**
     * How long to linger a binding after an app loses carrier privileges, as long as no new
     * binding comes in to take its place.
     */
    private static final int UNBIND_DELAY_MILLIS = 30 * 1000; // 30 seconds

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    private Context mContext;
    @VisibleForTesting
    public SparseArray<AppBinding> mBindings = new SparseArray();
    @VisibleForTesting
    public SparseArray<String> mLastSimState = new SparseArray<>();
    // TODO(b/201423849): Clean up PackageChangeReceiver/UserUnlockedReceiver/SIM State change if
    // CarrierServiceChangeCallback can cover the cases
    private final PackageChangeReceiver mPackageMonitor = new CarrierServicePackageMonitor();
    private final LocalLog mLocalLog = new LocalLog(100);

    private BroadcastReceiver mUserUnlockedReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();
            logdWithLocalLog("Received " + action);

            if (Intent.ACTION_USER_UNLOCKED.equals(action)) {
                // On user unlock, new components might become available, so reevaluate all
                // bindings.
                for (int phoneId = 0; phoneId < mBindings.size(); phoneId++) {
                    mBindings.get(phoneId).rebind();
                }
            }
        }
    };

    private class CarrierServiceChangeCallback implements
            TelephonyManager.CarrierPrivilegesCallback {
        final int mPhoneId;

        CarrierServiceChangeCallback(int phoneId) {
            this.mPhoneId = phoneId;
        }

        @Override
        public void onCarrierPrivilegesChanged(Set<String> privilegedPackageNames,
                Set<Integer> privilegedUids) {
            // Ignored, not interested here
        }

        @Override
        public void onCarrierServiceChanged(String carrierServicePackageName,
                int carrierServiceUid) {
            logdWithLocalLog("onCarrierServiceChanged, carrierServicePackageName="
                    + carrierServicePackageName + ", carrierServiceUid=" + carrierServiceUid
                    + ", mPhoneId=" + mPhoneId);
            mHandler.sendMessage(mHandler.obtainMessage(EVENT_REBIND, mPhoneId));
        }
    }

    private static final int EVENT_REBIND = 0;
    @VisibleForTesting
    public static final int EVENT_PERFORM_IMMEDIATE_UNBIND = 1;
    @VisibleForTesting
    public static final int EVENT_MULTI_SIM_CONFIG_CHANGED = 2;

    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    @VisibleForTesting
    public Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            int phoneId;
            AppBinding binding;
            logdWithLocalLog("mHandler: " + msg.what);

            switch (msg.what) {
                case EVENT_REBIND:
                    phoneId = (int) msg.obj;
                    binding = mBindings.get(phoneId);
                    if (binding == null) return;
                    logdWithLocalLog("Rebinding if necessary for phoneId: " + binding.getPhoneId());
                    binding.rebind();
                    break;
                case EVENT_PERFORM_IMMEDIATE_UNBIND:
                    phoneId = (int) msg.obj;
                    binding = mBindings.get(phoneId);
                    if (binding == null) return;
                    logdWithLocalLog("Unbind immediate with phoneId: " + binding.getPhoneId());
                    binding.performImmediateUnbind();
                    break;
                case EVENT_MULTI_SIM_CONFIG_CHANGED:
                    updateBindingsAndSimStates();
                    break;
                default:
                    Log.e(LOG_TAG, "Unsupported event received: " + msg.what);
            }
        }
    };

    public CarrierServiceBindHelper(Context context) {
        mContext = context.createContextAsUser(Process.myUserHandle(), 0);

        updateBindingsAndSimStates();

        PhoneConfigurationManager.registerForMultiSimConfigChange(
                mHandler, EVENT_MULTI_SIM_CONFIG_CHANGED, null);

        mPackageMonitor.register(
                context, mHandler.getLooper(), UserHandle.ALL);
        try {
            Context contextAsUser = mContext.createPackageContextAsUser(mContext.getPackageName(),
                0, UserHandle.SYSTEM);
            contextAsUser.registerReceiver(mUserUnlockedReceiver,
                new IntentFilter(Intent.ACTION_USER_UNLOCKED), null /* broadcastPermission */,
                mHandler);
        } catch (PackageManager.NameNotFoundException e) {
            logeWithLocalLog("Package name not found: " + e.getMessage());
        }
    }

    // Create or dispose mBindings and mLastSimState objects.
    private void updateBindingsAndSimStates() {
        int prevLen = mBindings.size();
        int newLen = ((TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE))
                .getActiveModemCount();

        // If prevLen < newLen, allocate AppBinding and simState objects.
        for (int phoneId = prevLen; phoneId < newLen; phoneId++) {
            mBindings.put(phoneId, new AppBinding(phoneId));
            mLastSimState.put(phoneId, new String());
        }

        // If prevLen > newLen, dispose AppBinding and simState objects.
        for (int phoneId = newLen; phoneId < prevLen; phoneId++) {
            mBindings.get(phoneId).tearDown();
            mBindings.get(phoneId).unbind(true);
            mBindings.delete(phoneId);
            mLastSimState.delete(phoneId);
        }
    }

    /**
     * Update SIM state.
     *
     * @param phoneId The phone id.
     * @param simState The legacy SIM state.
     */
    public void updateForPhoneId(int phoneId, @NonNull String simState) {
        logdWithLocalLog("update binding for phoneId: " + phoneId + " simState: " + simState);
        if (!SubscriptionManager.isValidPhoneId(phoneId)) {
            return;
        }
        if (TextUtils.isEmpty(simState) || phoneId >= mLastSimState.size()) return;
        if (simState.equals(mLastSimState.get(phoneId))) {
            // ignore consecutive duplicated events
            return;
        } else {
            mLastSimState.put(phoneId, simState);
        }
        mHandler.sendMessage(mHandler.obtainMessage(EVENT_REBIND, phoneId));
    }

    private class AppBinding {
        private int phoneId;
        private CarrierServiceConnection connection;
        private int bindCount;
        private long lastBindStartMillis;
        private int unbindCount;
        private long lastUnbindMillis;
        private String carrierPackage;
        private String carrierServiceClass;
        private long mUnbindScheduledUptimeMillis = -1;
        private final CarrierServiceChangeCallback mCarrierServiceChangeCallback;

        public AppBinding(int phoneId) {
            this.phoneId = phoneId;
            this.mCarrierServiceChangeCallback = new CarrierServiceChangeCallback(phoneId);
            TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
            if (tm != null) {
                tm.registerCarrierPrivilegesCallback(phoneId, new HandlerExecutor(mHandler),
                        mCarrierServiceChangeCallback);
            }
        }

        public void tearDown() {
            TelephonyManager tm = mContext.getSystemService(TelephonyManager.class);
            if (tm != null && mCarrierServiceChangeCallback != null) {
                tm.unregisterCarrierPrivilegesCallback(mCarrierServiceChangeCallback);
            }
        }

        public int getPhoneId() {
            return phoneId;
        }

        /** Return the package that is currently being bound to, or null if there is no binding. */
        public String getPackage() {
            return carrierPackage;
        }

        /**
         * Update the bindings for the current carrier app for this phone.
         *
         * <p>Safe to call even if a binding already exists. If the current binding is invalid, it
         * will be dropped. If it is valid, it will be left untouched.
         */
        void rebind() {
            // Get the package name for the carrier app
            String carrierPackageName = TelephonyManager.from(
                    mContext).getCarrierServicePackageNameForLogicalSlot(phoneId);

            if (carrierPackageName == null) {
                logdWithLocalLog("No carrier app for: " + phoneId);
                // Unbind after a delay in case this is a temporary blip in carrier privileges.
                unbind(false /* immediate */);
                return;
            }

            logdWithLocalLog("Found carrier app: " + carrierPackageName);
            // If we are binding to a different package, unbind immediately from the current one.
            if (!TextUtils.equals(carrierPackage, carrierPackageName)) {
                unbind(true /* immediate */);
            }

            // Look up the carrier service
            Intent carrierService = new Intent(CarrierService.CARRIER_SERVICE_INTERFACE);
            carrierService.setPackage(carrierPackageName);

            ResolveInfo carrierResolveInfo = mContext.getPackageManager().resolveService(
                carrierService, PackageManager.GET_META_DATA);
            Bundle metadata = null;
            String candidateServiceClass = null;
            if (carrierResolveInfo != null) {
                metadata = carrierResolveInfo.serviceInfo.metaData;
                ComponentInfo componentInfo = TelephonyUtils.getComponentInfo(carrierResolveInfo);
                candidateServiceClass = new ComponentName(componentInfo.packageName,
                    componentInfo.name).getClassName();
            }

            // Only bind if the service wants it
            if (metadata == null ||
                !metadata.getBoolean("android.service.carrier.LONG_LIVED_BINDING", false)) {
                logdWithLocalLog("Carrier app does not want a long lived binding");
                unbind(true /* immediate */);
                return;
            }

            if (!TextUtils.equals(carrierServiceClass, candidateServiceClass)) {
                logdWithLocalLog("CarrierService class changed, unbind immediately.");
                // Unbind immediately if the carrier service component has changed.
                unbind(true /* immediate */);
            } else if (connection != null) {
                logdWithLocalLog(
                        "CarrierService class unchanged with connection up, cancelScheduledUnbind");
                // Component is unchanged and connection is up - do nothing, but cancel any
                // scheduled unbinds.
                cancelScheduledUnbind();
                return;
            }

            carrierPackage = carrierPackageName;
            carrierServiceClass = candidateServiceClass;

            logdWithLocalLog("Binding to " + carrierPackage + " for phone " + phoneId);

            // Log debug information
            bindCount++;
            lastBindStartMillis = System.currentTimeMillis();

            connection = new CarrierServiceConnection();

            String error;
            try {
                if (mContext.bindService(
                        carrierService,
                        Context.BIND_AUTO_CREATE
                                | Context.BIND_FOREGROUND_SERVICE
                                | Context.BIND_INCLUDE_CAPABILITIES,
                        (r) -> mHandler.post(r),
                        connection)) {
                    logdWithLocalLog("service bound");
                    return;
                }

                error = "bindService returned false";
            } catch (SecurityException ex) {
                error = ex.getMessage();
            }

            logdWithLocalLog("Unable to bind to " + carrierPackage + " for phone " + phoneId
                    + ". Error: " + error);
            unbind(true /* immediate */);
        }

        /**
         * Release the binding.
         *
         * @param immediate whether the binding should be released immediately or after a short
         *                  delay. This should be true unless the reason for the unbind is that no
         *                  app has carrier privileges, in which case it is useful to delay
         *                  unbinding in case this is a temporary SIM blip.
         */
        void unbind(boolean immediate) {
            if (connection == null) {
                // Already fully unbound.
                return;
            }

            // Only let the binding linger if a delayed unbind is requested *and* the connection is
            // currently active. If the connection is down, unbind immediately as the app is likely
            // not running anyway and it may be a permanent disconnection (e.g. the app was
            // disabled).
            if (immediate || !connection.connected) {
                logdWithLocalLog("unbind immediately or with disconnected connection");
                cancelScheduledUnbind();
                performImmediateUnbind();
            } else if (mUnbindScheduledUptimeMillis == -1) {
                long currentUptimeMillis = SystemClock.uptimeMillis();
                mUnbindScheduledUptimeMillis = currentUptimeMillis + UNBIND_DELAY_MILLIS;
                logdWithLocalLog("Scheduling unbind in " + UNBIND_DELAY_MILLIS + " millis");
                mHandler.sendMessageAtTime(
                        mHandler.obtainMessage(EVENT_PERFORM_IMMEDIATE_UNBIND, phoneId),
                        mUnbindScheduledUptimeMillis);
            }
        }

        private void performImmediateUnbind() {
            // Log debug information
            unbindCount++;
            lastUnbindMillis = System.currentTimeMillis();

            // Clear package state now that no binding is desired.
            carrierPackage = null;
            carrierServiceClass = null;

            // Always call unbindService, no matter if bindService succeed.
            if (connection != null) {
                mContext.unbindService(connection);
                logdWithLocalLog("Unbinding from carrier app");
                connection = null;
                mUnbindScheduledUptimeMillis = -1;
            }
        }

        private void cancelScheduledUnbind() {
            logdWithLocalLog("cancelScheduledUnbind");
            mHandler.removeMessages(EVENT_PERFORM_IMMEDIATE_UNBIND);
            mUnbindScheduledUptimeMillis = -1;
        }

        public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
            pw.println("Carrier app binding for phone " + phoneId);
            pw.println("  connection: " + connection);
            pw.println("  bindCount: " + bindCount);
            pw.println("  lastBindStartMillis: " + lastBindStartMillis);
            pw.println("  unbindCount: " + unbindCount);
            pw.println("  lastUnbindMillis: " + lastUnbindMillis);
            pw.println("  mUnbindScheduledUptimeMillis: " + mUnbindScheduledUptimeMillis);
            pw.println("  mCarrierServiceChangeCallback: " + mCarrierServiceChangeCallback);
            pw.println();
        }
    }

    private class CarrierServiceConnection implements ServiceConnection {
        private boolean connected;

        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            logdWithLocalLog("Connected to carrier app: " + name.flattenToString());
            connected = true;
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            logdWithLocalLog("Disconnected from carrier app: " + name.flattenToString());
            connected = false;
        }

        @Override
        public void onBindingDied(ComponentName name) {
            logdWithLocalLog("Binding from carrier app died: " + name.flattenToString());
            connected = false;
        }

        @Override
        public void onNullBinding(ComponentName name) {
            logdWithLocalLog("Null binding from carrier app: " + name.flattenToString());
            connected = false;
        }

        @Override
        public String toString() {
            return "CarrierServiceConnection[connected=" + connected + "]";
        }
    }

    private class CarrierServicePackageMonitor extends PackageChangeReceiver {
        @Override
        public void onPackageAdded(String packageName) {
            logdWithLocalLog("onPackageAdded: " + packageName);
            evaluateBinding(packageName, true /* forceUnbind */);
        }

        @Override
        public void onPackageRemoved(String packageName) {
            logdWithLocalLog("onPackageRemoved: " + packageName);
            evaluateBinding(packageName, true /* forceUnbind */);
        }

        @Override
        public void onPackageUpdateFinished(String packageName) {
            logdWithLocalLog("onPackageUpdateFinished: " + packageName);
            evaluateBinding(packageName, true /* forceUnbind */);
        }

        @Override
        public void onPackageModified(String packageName) {
            logdWithLocalLog("onPackageModified: " + packageName);
            evaluateBinding(packageName, false /* forceUnbind */);
        }

        @Override
        public void onHandleForceStop(String[] packages, boolean doit) {
            if (doit) {
                logdWithLocalLog("onHandleForceStop: " + Arrays.toString(packages));
                for (String packageName : packages) {
                    evaluateBinding(packageName, true /* forceUnbind */);
                }
            }
        }

        private void evaluateBinding(String carrierPackageName, boolean forceUnbind) {
            for (int i = 0; i < mBindings.size(); i++) {
                AppBinding appBinding = mBindings.get(i);
                String appBindingPackage = appBinding.getPackage();
                boolean isBindingForPackage = carrierPackageName.equals(appBindingPackage);
                // Only log if this package was a carrier package to avoid log spam in the common
                // case that there are no carrier packages, but evaluate the binding if the package
                // is unset, in case this package change resulted in a new carrier package becoming
                // available for binding.
                if (isBindingForPackage) {
                    logdWithLocalLog(
                            carrierPackageName + " changed and corresponds to a phone. Rebinding.");
                }
                if (appBindingPackage == null || isBindingForPackage) {
                    if (forceUnbind) {
                        appBinding.unbind(true /* immediate */);
                    }
                    appBinding.rebind();
                }
            }
        }
    }

    private void logdWithLocalLog(String msg) {
        Log.d(LOG_TAG, msg);
        mLocalLog.log(msg);
    }

    private void logeWithLocalLog(String msg) {
        Log.e(LOG_TAG, msg);
        mLocalLog.log(msg);
    }

    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println("CarrierServiceBindHelper:");
        for (int i = 0; i < mBindings.size(); i++) {
            mBindings.get(i).dump(fd, pw, args);
        }
        pw.println("CarrierServiceBindHelperLogs=");
        mLocalLog.dump(fd, pw, args);
    }
}
