/*
 * Copyright (C) 2024 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.server;

import static com.android.tradeinmode.flags.Flags.enableTradeInMode;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.accounts.OnAccountsUpdateListener;
import android.annotation.RequiresPermission;
import android.content.ContentResolver;
import android.content.Context;
import android.database.ContentObserver;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
import android.net.ConnectivityManager;
import android.net.Network;
import android.net.NetworkCapabilities;
import android.net.NetworkRequest;
import android.net.Uri;
import android.os.Binder;
import android.os.ITradeInMode;
import android.os.RemoteException;
import android.os.SystemProperties;
import android.provider.Settings;
import android.provider.Settings.SettingNotFoundException;
import android.service.persistentdata.PersistentDataBlockManager;
import android.util.Slog;
import android.view.SurfaceControl;

import com.android.server.display.DisplayControl;
import com.android.server.health.HealthServiceWrapper;

import java.io.FileWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.NoSuchElementException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public final class TradeInModeService extends SystemService {
    private static final String TAG = "TradeInModeService";

    private static final String TIM_PROP = "persist.adb.tradeinmode";
    private static final String TIM_TEST_PROP = "persist.adb.test_tradeinmode";

    private static final int TIM_STATE_UNSET = 0;

    // adbd_tradeinmode was stopped.
    private static final int TIM_STATE_DISABLED = -1;

    // adbd_tradeinmode has started.
    private static final int TIM_STATE_FOYER = 1;

    // Full non-root adb granted; factory reset is guaranteed.
    private static final int TIM_STATE_EVALUATION_MODE = 2;

    // This file contains a single integer counter of how many boot attempts
    // have been made since entering evaluation mode.
    private static final String WIPE_INDICATOR_FILE = "/metadata/tradeinmode/wipe";

    private final Context mContext;
    private TradeInMode mTradeInMode;

    private ConnectivityManager mConnectivityManager;
    private ConnectivityManager.NetworkCallback mNetworkCallback = null;

    private AccountManager mAccountManager;
    private OnAccountsUpdateListener mAccountsListener = null;

    public TradeInModeService(Context context) {
        super(context);

        mContext = context;
    }

    @Override
    public void onStart() {
        if (!enableTradeInMode()) {
            return;
        }

        mTradeInMode = new TradeInMode();
        publishBinderService("tradeinmode", mTradeInMode);
    }

    @Override
    public void onBootPhase(@BootPhase int phase) {
        if (phase == PHASE_SYSTEM_SERVICES_READY) {
            final int state = getTradeInModeState();

            if (isAdbEnabled() && !isDebuggable() && !isDeviceSetup()
                    && state == TIM_STATE_DISABLED) {
                // If we fail to start trade-in mode, the persist property may linger
                // past reboot. If we detect this, disable ADB and clear TIM state.
                Slog.i(TAG, "Resetting trade-in mode state.");
                SystemProperties.set(TIM_PROP, "");

                final ContentResolver cr = mContext.getContentResolver();
                Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, 0);
            } else if (state == TIM_STATE_FOYER) {
                // If zygote crashed or we rebooted, and TIM is still enabled, make
                // sure it's allowed to be enabled. If it is, we need to re-add our
                // setup completion observer.
                if (isDeviceSetup()) {
                    stopTradeInMode();
                } else if (isDebuggable() && !isForceEnabledForTesting()) {
                    // The device was made debuggable after entering TIM. This
                    // can happen while flashing. For convenience, leave test mode.
                    leaveTestMode();
                } else {
                    watchForSetupCompletion();
                    watchForNetworkChange();
                    watchForAccountsCreated();
                }
            }
        }
    }

    private final class TradeInMode extends ITradeInMode.Stub {
        @Override
        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
        public boolean start() {
            enforceEnterTradeInModePermission();
            final int state = getTradeInModeState();
            if (state == TIM_STATE_FOYER) {
                return true;
            }

            if (state != TIM_STATE_UNSET) {
                Slog.e(TAG, "Cannot enter trade-in mode in state: " + state);
                return false;
            }

            if (isDeviceSetup()) {
                Slog.i(TAG, "Not starting trade-in mode, device is setup.");
                return false;
            }
            if (isDebuggable()) {
                if (!isForceEnabledForTesting()) {
                    // We don't want to force adbd into TIM on debug builds.
                    Slog.e(TAG, "Not starting trade-in mode, device is debuggable.");
                    return false;
                }
            } else if (isAdbEnabled()) {
                Slog.e(TAG, "Not starting trade-in mode, adb is already enabled.");
                return false;
            }

            final long callingId = Binder.clearCallingIdentity();
            try {
                startTradeInMode();
            } finally {
                Binder.restoreCallingIdentity(callingId);
            }
            return true;
        }

        @Override
        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
        public boolean enterEvaluationMode() {
            enforceEnterTradeInModePermission();
            if (!checkEvaluationModePreconditions()) {
                return false;
            }
            if (!scheduleTradeInModeWipe()) {
                return false;
            }

            final long callingId = Binder.clearCallingIdentity();
            try {
                removeNetworkWatch();
                removeAccountsWatch();
            } finally {
                Binder.restoreCallingIdentity(callingId);
            }

            SystemProperties.set(TIM_PROP, Integer.toString(TIM_STATE_EVALUATION_MODE));
            restartAdbd();
            return true;
        }

        @Override
        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
        public boolean isEvaluationModeAllowed() {
            enforceEnterTradeInModePermission();
            return checkEvaluationModePreconditions();
        }

        @Override
        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
        public void scheduleWipeForTesting() {
            enforceTestingPermissions();

            scheduleTradeInModeWipe();
        }

        @Override
        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
        public void startTesting() {
            enforceTestingPermissions();

            enterTestMode();
        }

        @Override
        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
        public void stopTesting() {
            enforceTestingPermissions();

            if (!isForceEnabledForTesting()) {
                throw new IllegalStateException("testing must have been started");
            }

            final long callingId = Binder.clearCallingIdentity();
            try {
                leaveTestMode();
            } finally {
                Binder.restoreCallingIdentity(callingId);
            }
        }

        @Override
        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
        public boolean isTesting() {
            enforceTestingPermissions();

            return isForceEnabledForTesting();
        }

        @Override
        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
        public int[] getScreenPartStatus() throws RemoteException {
            enforceEnterTradeInModePermission();
            int[] statuses = new int[DisplayControl.getPhysicalDisplayIds().length];
            int index = 0;
            // loop through all displayId to find id of internal display
            for (long physicalDisplayId : DisplayControl.getPhysicalDisplayIds()) {
                SurfaceControl.StaticDisplayInfo info = SurfaceControl.getStaticDisplayInfo(physicalDisplayId);
                if (info != null && info.isInternal) {
                    statuses[index++] = info.screenPartStatus;
                }
            }
            return statuses;
        }

        @Override
        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
        public int getHingeCount() throws RemoteException {
            enforceEnterTradeInModePermission();
            android.hardware.health.HingeInfo[] info = getHealthService().getHingeInfo();
            return (info == null) ? 0 : info.length;
        }

        @Override
        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
        public int getFoldCount(int hingeId) throws RemoteException {
            enforceEnterTradeInModePermission();
            int hingeCount = getHingeCount();
            if (hingeId >= hingeCount) {
                Slog.e(TAG, "Hinge " + hingeId + " is greater than hinge count: " + hingeCount);
                return -1;
            }
            return getHealthService().getHingeInfo()[hingeId].numTimesFolded;
        }

        @Override
        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
        public int getHingeLifeSpan(int hingeId) throws RemoteException {
            enforceEnterTradeInModePermission();
            int hingeCount = getHingeCount();
            if (hingeId >= hingeCount) {
                Slog.e(TAG, "Hinge " + hingeId + " is greater than hinge count: " + hingeCount);
                return -1;
            }
            return getHealthService().getHingeInfo()[hingeId].expectedHingeLifespan;
        }

        @Override
        @RequiresPermission(android.Manifest.permission.ENTER_TRADE_IN_MODE)
        public int getMoistureIntrusionDetected(long timeoutMillis) throws RemoteException {
            enforceEnterTradeInModePermission();
            SensorManager m = (SensorManager) mContext.getSystemService(Context.SENSOR_SERVICE);
            Sensor moistureDetectionSensor = m.getDefaultSensor(Sensor.TYPE_MOISTURE_INTRUSION);

            final CountDownLatch latch = new CountDownLatch(1);
            final AtomicInteger sensorValueHolder = new AtomicInteger(); // Default to timeout

            SensorEventListener listener = new SensorEventListener() {
                @Override
                public void onSensorChanged(SensorEvent event) {
                    if (event.values[0] == 0.0) {
                        sensorValueHolder.set(0);
                    } else if (event.values[0] == 1.0) {
                        sensorValueHolder.set(1);
                    } else {
                        Slog.e(TAG, "Moisture Sensor returned unexpected value: " + event.values[0]);
                    }
                    latch.countDown();
                }

                @Override
                public void onAccuracyChanged(Sensor sensor, int accuracy) {
                }
            };
            if (moistureDetectionSensor != null) {
                m.registerListener(listener, moistureDetectionSensor,
                        SensorManager.SENSOR_DELAY_NORMAL);
            }
            try {
                if (latch.await(timeoutMillis, TimeUnit.MILLISECONDS)) {
                    return sensorValueHolder.get();
                } else {
                    m.unregisterListener(listener, moistureDetectionSensor);
                    return ITradeInMode.MoistureIntrusionStatus.UNSUPPORTED;
                }
            } catch (InterruptedException e) {
                return ITradeInMode.MoistureIntrusionStatus.UNSUPPORTED;
            } finally {
                m.unregisterListener(listener, moistureDetectionSensor);
            }
        }

        private void enforceEnterTradeInModePermission() {
            mContext.enforceCallingOrSelfPermission("android.permission.ENTER_TRADE_IN_MODE",
                    "caller missing ENTER_TRADE_IN_MODE permission");
        }

        private void enforceTestingPermissions() {
            enforceEnterTradeInModePermission();
            if (!isDebuggable()) {
                throw new SecurityException("ro.debuggable must be set to 1");
            }
        }
    }

    private void startTradeInMode() {
        Slog.i(TAG, "Enabling trade-in mode.");

        SystemProperties.set(TIM_PROP, Integer.toString(TIM_STATE_FOYER));

        setAdbEnabled(true);

        watchForSetupCompletion();
        watchForNetworkChange();
        watchForAccountsCreated();
    }

    private void stopTradeInMode() {
        Slog.i(TAG, "Stopping trade-in mode.");

        SystemProperties.set(TIM_PROP, Integer.toString(TIM_STATE_DISABLED));

        removeNetworkWatch();
        removeAccountsWatch();

        if (isForceEnabledForTesting()) {
            // If testing in a debug build, we need to re-enable ADB.
            restartAdbd();
        } else {
            // Otherwise, ADB must not be enabled.
            setAdbEnabled(false);
        }
    }

    private void enterTestMode() {
        SystemProperties.set(TIM_TEST_PROP, "1");
        SystemProperties.set(TIM_PROP, Integer.toString(TIM_STATE_FOYER));
    }

    private void leaveTestMode() {
        if (getTradeInModeState() == TIM_STATE_FOYER) {
            stopTradeInMode();
        }

        SystemProperties.set(TIM_TEST_PROP, "");
        SystemProperties.set(TIM_PROP, "");
        try {
            Files.deleteIfExists(Paths.get(WIPE_INDICATOR_FILE));
        } catch (IOException e) {
            Slog.e(TAG, "Failed to remove wipe indicator", e);
        }
    }

    private boolean scheduleTradeInModeWipe() {
        try (FileWriter fw = new FileWriter(WIPE_INDICATOR_FILE,
                StandardCharsets.US_ASCII)) {
            fw.write("0");
        } catch (IOException e) {
            Slog.e(TAG, "Failed to write " + WIPE_INDICATOR_FILE, e);
            return false;
        }
        return true;
    }

    private void restartAdbd() {
        SystemProperties.set("ctl.restart", "adbd");
    }

    private void setAdbEnabled(boolean enabled) {
        final ContentResolver cr = mContext.getContentResolver();
        Settings.Global.putInt(cr, Settings.Global.ADB_ENABLED, enabled ? 1 : 0);
    }

    private HealthServiceWrapper getHealthService() {
        try {
            HealthServiceWrapper health = HealthServiceWrapper.create(null);
            return health;
        } catch (RemoteException ex) {
            Slog.e(TAG, "health: (RemoteException)");
            throw ex.rethrowFromSystemServer();
        } catch (NoSuchElementException ex) {
            Slog.e(TAG, "health: cannot register callback. (no supported health HAL service)");
            throw ex;
        }
    }

    private boolean checkEvaluationModePreconditions() {
        final int state = getTradeInModeState();
        if (!(state == TIM_STATE_FOYER || (isDebuggable() && state == TIM_STATE_UNSET))) {
            Slog.i(TAG, "Cannot enter evaluation mode in state: " + state);
            return false;
        }
        if (isFrpActive()) {
            Slog.i(TAG, "Cannot enter evaluation mode, FRP lock is present.");
            return false;
        }
        return true;
    }

    private int getTradeInModeState() {
        return SystemProperties.getInt(TIM_PROP, TIM_STATE_UNSET);
    }

    private boolean isDebuggable() {
        return SystemProperties.getInt("ro.debuggable", 0) == 1;
    }

    private boolean isForceEnabledForTesting() {
        return isDebuggable() && SystemProperties.getInt(TIM_TEST_PROP, 0) == 1;
    }

    private boolean isAdbEnabled() {
        final ContentResolver cr = mContext.getContentResolver();
        return Settings.Global.getInt(cr, Settings.Global.ADB_ENABLED, 0) == 1;
    }

    private boolean isFrpActive() {
        try {
            PersistentDataBlockManager pdb = mContext.getSystemService(PersistentDataBlockManager.class);
            if (pdb == null) {
                return false;
            }
            return pdb.isFactoryResetProtectionActive();
        } catch (Exception e) {
            Slog.e(TAG, "Could not read PDB", e);
            return false;
        }
    }

    // This returns true if the device has progressed far enough into Setup Wizard
    // that it no longer makes sense to enable trade-in mode. As a last stop, we
    // check the SUW completion bits.
    private boolean isDeviceSetup() {
        final ContentResolver cr = mContext.getContentResolver();
        try {
            if (Settings.Secure.getIntForUser(cr, Settings.Secure.USER_SETUP_COMPLETE, 0) != 0) {
                return true;
            }
        } catch (SettingNotFoundException e) {
            Slog.e(TAG, "Could not find USER_SETUP_COMPLETE setting", e);
        }

        if (Settings.Global.getInt(cr, Settings.Global.DEVICE_PROVISIONED, 0) != 0) {
            return true;
        }
        return false;
    }

    private final class SettingsObserver extends ContentObserver {
        SettingsObserver() {
            super(null);
        }

        @Override
        public void onChange(boolean selfChange) {
            if (getTradeInModeState() == TIM_STATE_FOYER && isDeviceSetup()) {
                stopTradeInMode();
            }
        }
    }

    private void watchForSetupCompletion() {
        final Uri userSetupComplete = Settings.Secure.getUriFor(
                Settings.Secure.USER_SETUP_COMPLETE);
        final Uri deviceProvisioned = Settings.Global.getUriFor(
                Settings.Global.DEVICE_PROVISIONED);
        final ContentResolver cr = mContext.getContentResolver();
        final SettingsObserver observer = new SettingsObserver();

        cr.registerContentObserver(userSetupComplete, false, observer);
        cr.registerContentObserver(deviceProvisioned, false, observer);
    }

    private void watchForNetworkChange() {
        mConnectivityManager = mContext.getSystemService(ConnectivityManager.class);
        NetworkRequest networkRequest = new NetworkRequest.Builder()
                .addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
                .addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
                .addTransportType(NetworkCapabilities.TRANSPORT_CELLULAR)
                .build();

        mNetworkCallback = new ConnectivityManager.NetworkCallback() {
            @Override
            public void onAvailable(Network network) {
                super.onAvailable(network);
                stopTradeInMode();
            }
        };

        mConnectivityManager.registerNetworkCallback(networkRequest, mNetworkCallback);
    }

    private void removeNetworkWatch() {
        if (mNetworkCallback != null) {
            mConnectivityManager.unregisterNetworkCallback(mNetworkCallback);
            mNetworkCallback = null;
        }
    }

    private void watchForAccountsCreated() {
        mAccountManager = mContext.getSystemService(AccountManager.class);
        mAccountsListener = new OnAccountsUpdateListener() {
            @Override
            public void onAccountsUpdated(Account[] accounts) {
                stopTradeInMode();
            }
        };
        mAccountManager.addOnAccountsUpdatedListener(mAccountsListener, null, false);
    }

    private void removeAccountsWatch() {
        if (mAccountsListener != null) {
            mAccountManager.removeOnAccountsUpdatedListener(mAccountsListener);
            mAccountsListener = null;
        }
    }
}
