/*
 * Copyright 2017 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.wifi;

import android.content.Context;
import android.database.ContentObserver;
import android.net.wifi.ScanResult;
import android.net.wifi.WifiConfiguration;
import android.net.wifi.WifiManager;
import android.net.wifi.WifiNetworkSuggestion;
import android.net.wifi.WifiScanner;
import android.os.Handler;
import android.os.Process;
import android.provider.Settings;
import android.util.Log;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.modules.utils.HandlerExecutor;
import com.android.server.wifi.util.LastCallerInfoManager;
import com.android.server.wifi.util.WifiPermissionsUtil;

import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;


/**
 * WakeupController is responsible managing Auto Wifi.
 *
 * <p>It determines if and when to re-enable wifi after it has been turned off by the user.
 */
public class WakeupController {

    private static final String TAG = "WakeupController";

    private static final boolean USE_PLATFORM_WIFI_WAKE = true;
    private static final int INIT_WAKEUP_LOCK_SCAN_RESULT_VALID_DURATION_MS =
            10 * 60 * 1000; // 10 minutes

    private final Context mContext;
    private final Handler mHandler;
    private final FrameworkFacade mFrameworkFacade;
    private final ContentObserver mContentObserver;
    private final WakeupLock mWakeupLock;
    private final WakeupEvaluator mWakeupEvaluator;
    private final WakeupOnboarding mWakeupOnboarding;
    private final WifiConfigManager mWifiConfigManager;
    private final WifiNetworkSuggestionsManager mWifiNetworkSuggestionsManager;
    private final WifiInjector mWifiInjector;
    private final WakeupConfigStoreData mWakeupConfigStoreData;
    private final WifiWakeMetrics mWifiWakeMetrics;
    private final Clock mClock;
    private final ActiveModeWarden mActiveModeWarden;
    private final LastCallerInfoManager mLastCallerInfoManager;
    private final WifiMetrics mWifiMetrics;
    private final WifiPermissionsUtil mWifiPermissionsUtil;
    private final WifiSettingsStore mSettingsStore;
    private final Object mLock = new Object();

    private final WifiScanner.ScanListener mScanListener = new WifiScanner.ScanListener() {
        @Override
        public void onPeriodChanged(int periodInMs) {
            // no-op
        }

        @Override
        public void onResults(WifiScanner.ScanData[] results) {
            // We treat any full band scans (with DFS or not) as "full".
            if (results.length == 1
                    && WifiScanner.isFullBandScan(results[0].getScannedBandsInternal(), true)) {
                handleScanResults(filterDfsScanResults(Arrays.asList(results[0].getResults())));
            }
        }

        @Override
        public void onFullResult(ScanResult fullScanResult) {
            // no-op
        }

        @Override
        public void onSuccess() {
            // no-op
        }

        @Override
        public void onFailure(int reason, String description) {
            Log.e(TAG, "ScanListener onFailure: " + reason + ": " + description);
        }
    };

    /** Whether this feature is enabled in Settings. */
    @GuardedBy("mLock")
    private boolean mWifiWakeupEnabled;

    /** Whether the WakeupController is currently active. */
    private boolean mIsActive = false;

    /**
     *  The number of scans that have been handled by the controller since last
     * {@link #onWifiEnabled()}.
     */
    private int mNumScansHandled = 0;

    /** Whether Wifi verbose logging is enabled. */
    private boolean mVerboseLoggingEnabled;

    /**
     * The timestamp of when the Wifi network was last disconnected (either device disconnected
     * from the network or Wifi was turned off entirely).
     * Note: mLastDisconnectTimestampMillis and mLastDisconnectInfo must always be updated together.
     */
    private long mLastDisconnectTimestampMillis;

    /**
     * The SSID of the last Wifi network the device was connected to (either device disconnected
     * from the network or Wifi was turned off entirely).
     * Note: mLastDisconnectTimestampMillis and mLastDisconnectInfo must always be updated together.
     */
    private ScanResultMatchInfo mLastDisconnectInfo;

    public WakeupController(
            Context context,
            Handler handler,
            WakeupLock wakeupLock,
            WakeupEvaluator wakeupEvaluator,
            WakeupOnboarding wakeupOnboarding,
            WifiConfigManager wifiConfigManager,
            WifiConfigStore wifiConfigStore,
            WifiNetworkSuggestionsManager wifiNetworkSuggestionsManager,
            WifiWakeMetrics wifiWakeMetrics,
            WifiInjector wifiInjector,
            FrameworkFacade frameworkFacade,
            Clock clock,
            ActiveModeWarden activeModeWarden) {
        mContext = context;
        mHandler = handler;
        mWakeupLock = wakeupLock;
        mWakeupEvaluator = wakeupEvaluator;
        mWakeupOnboarding = wakeupOnboarding;
        mWifiConfigManager = wifiConfigManager;
        mWifiNetworkSuggestionsManager = wifiNetworkSuggestionsManager;
        mWifiWakeMetrics = wifiWakeMetrics;
        mFrameworkFacade = frameworkFacade;
        mWifiInjector = wifiInjector;
        mActiveModeWarden = activeModeWarden;
        mLastCallerInfoManager = wifiInjector.getLastCallerInfoManager();
        mWifiMetrics = wifiInjector.getWifiMetrics();
        mWifiPermissionsUtil = wifiInjector.getWifiPermissionsUtil();
        mSettingsStore = wifiInjector.getWifiSettingsStore();
        mContentObserver = new ContentObserver(mHandler) {
            @Override
            public void onChange(boolean selfChange) {
                readWifiWakeupEnabledFromSettings();
                mWakeupOnboarding.setOnboarded();
            }
        };
        mFrameworkFacade.registerContentObserver(mContext, Settings.Global.getUriFor(
                Settings.Global.WIFI_WAKEUP_ENABLED), true, mContentObserver);
        readWifiWakeupEnabledFromSettings();

        // registering the store data here has the effect of reading the persisted value of the
        // data sources after system boot finishes
        mWakeupConfigStoreData = new WakeupConfigStoreData(
                new IsActiveDataSource(),
                mWakeupOnboarding.getIsOnboadedDataSource(),
                mWakeupOnboarding.getNotificationsDataSource(),
                mWakeupLock.getDataSource());
        wifiConfigStore.registerStoreData(mWakeupConfigStoreData);
        mClock = clock;
        mLastDisconnectTimestampMillis = 0;
        mLastDisconnectInfo = null;

        mActiveModeWarden.registerPrimaryClientModeManagerChangedCallback(
                (prevPrimaryClientModeManager, newPrimaryClientModeManager) -> {
                    // reset when the primary CMM changes
                    if (newPrimaryClientModeManager != null) {
                        onWifiEnabled();
                    }
                });
    }

    private void readWifiWakeupEnabledFromSettings() {
        synchronized (mLock) {
            mWifiWakeupEnabled = mFrameworkFacade.getIntegerSetting(
                    mContext, Settings.Global.WIFI_WAKEUP_ENABLED, 0) == 1;
            Log.d(TAG, "WifiWake " + (mWifiWakeupEnabled ? "enabled" : "disabled"));
        }
    }

    private void setActive(boolean isActive) {
        if (mIsActive != isActive) {
            Log.d(TAG, "Setting active to " + isActive);
            mIsActive = isActive;
            mWifiConfigManager.saveToStore();
        }
    }

    /**
     * Enable/Disable the feature.
     */
    public void setEnabled(boolean enable) {
        synchronized (mLock) {
            mFrameworkFacade.setIntegerSetting(
                    mContext, Settings.Global.WIFI_WAKEUP_ENABLED, enable ? 1 : 0);
        }
    }

    /**
     * Whether the feature is currently enabled.
     */
    public boolean isEnabled() {
        synchronized (mLock) {
            return mWifiWakeupEnabled;
        }
    }

    /**
     * Whether the feature is currently usable.
     */
    public boolean isUsable() {
        return isEnabled()
                && mSettingsStore.isScanAlwaysAvailableToggleEnabled()
                && mWifiPermissionsUtil.isLocationModeEnabled();
    }

    /**
     * Saves the SSID of the last Wifi network that was disconnected. Should only be called before
     * WakeupController is active.
     */
    public void setLastDisconnectInfo(ScanResultMatchInfo scanResultMatchInfo) {
        if (mIsActive) {
            Log.e(TAG, "Unexpected setLastDisconnectInfo when WakeupController is active!");
            return;
        }
        if (scanResultMatchInfo == null) {
            Log.e(TAG, "Unexpected setLastDisconnectInfo(null)");
            return;
        }
        mLastDisconnectTimestampMillis = mClock.getElapsedSinceBootMillis();
        mLastDisconnectInfo = scanResultMatchInfo;
        if (mVerboseLoggingEnabled) {
            Log.d(TAG, "mLastDisconnectInfo set to " + scanResultMatchInfo);
        }
    }

    /**
     * If Wifi was disabled within LAST_DISCONNECT_TIMEOUT_MILLIS of losing a Wifi connection,
     * add that Wifi connection to the Wakeup Lock as if Wifi was disabled while connected to that
     * connection.
     * Often times, networks with poor signal intermittently connect and disconnect, causing the
     * user to manually turn off Wifi. If the Wifi was turned off during the disconnected phase of
     * the intermittent connection, then that connection normally would not be added to the Wakeup
     * Lock. This constant defines the timeout after disconnecting, in milliseconds, within which
     * if Wifi was disabled, the network would still be added to the wakeup lock.
     */
    @VisibleForTesting
    static final long LAST_DISCONNECT_TIMEOUT_MILLIS = 5 * 1000;

    /**
     * Starts listening for incoming scans.
     *
     * <p>Should only be called upon entering ScanMode. WakeupController registers its listener with
     * the WifiScanner. If the WakeupController is already active, then it returns early. Otherwise
     * it performs its initialization steps and sets {@link #mIsActive} to true.
     */
    public void start() {
        Log.d(TAG, "start()");
        // If already active, we don't want to restart the session, so return early.
        if (mIsActive) {
            mWifiWakeMetrics.recordIgnoredStart();
            return;
        }
        if (getGoodSavedNetworksAndSuggestions().isEmpty()) {
            Log.i(TAG, "Ignore wakeup start since there are no good networks.");
            return;
        }
        mWifiInjector.getWifiScanner().registerScanListener(
                new HandlerExecutor(mHandler), mScanListener);

        setActive(true);

        // ensure feature is enabled and store data has been read before performing work
        if (isEnabledAndReady()) {
            mWakeupOnboarding.maybeShowNotification();

            List<ScanResult> scanResults = filterDfsScanResults(
                    mWifiConfigManager.getMostRecentScanResultsForConfiguredNetworks(
                            INIT_WAKEUP_LOCK_SCAN_RESULT_VALID_DURATION_MS));
            Set<ScanResultMatchInfo> matchInfos = toMatchInfos(scanResults);
            matchInfos.retainAll(getGoodSavedNetworksAndSuggestions());

            // ensure that the last disconnected network is added to the wakeup lock, since we don't
            // want to automatically reconnect to the same network that the user manually
            // disconnected from
            long now = mClock.getElapsedSinceBootMillis();
            if (mLastDisconnectInfo != null && ((now - mLastDisconnectTimestampMillis)
                    <= LAST_DISCONNECT_TIMEOUT_MILLIS)) {
                matchInfos.add(mLastDisconnectInfo);
                if (mVerboseLoggingEnabled) {
                    Log.d(TAG, "Added last connected network to lock: " + mLastDisconnectInfo);
                }
            }

            if (mVerboseLoggingEnabled) {
                Log.d(TAG, "Saved networks in most recent scan:" + matchInfos);
            }

            mWifiWakeMetrics.recordStartEvent(matchInfos.size());
            mWakeupLock.setLock(matchInfos);
            // TODO(b/77291248): request low latency scan here
        }
    }

    /**
     * Stops listening for scans.
     *
     * <p>Should only be called upon leaving ScanMode. It deregisters the listener from
     * WifiScanner.
     */
    public void stop() {
        Log.d(TAG, "stop()");
        mLastDisconnectTimestampMillis = 0;
        mLastDisconnectInfo = null;
        mWifiInjector.getWifiScanner().unregisterScanListener(mScanListener);
        mWakeupOnboarding.onStop();
    }

    /**
     * This is called at the end of a Wifi Wake session, after Wifi Wake successfully turned Wifi
     * back on.
     */
    private void onWifiEnabled() {
        Log.d(TAG, "onWifiEnabled()");
        mWifiWakeMetrics.recordResetEvent(mNumScansHandled);
        mNumScansHandled = 0;
        setActive(false);
    }

    /** Sets verbose logging flag based on verbose level. */
    public void enableVerboseLogging(boolean verboseEnabled) {
        mVerboseLoggingEnabled = verboseEnabled;
        mWakeupLock.enableVerboseLogging(mVerboseLoggingEnabled);
    }

    /** Returns a list of ScanResults with DFS channels removed. */
    private List<ScanResult> filterDfsScanResults(Collection<ScanResult> scanResults) {
        int[] dfsChannels = mWifiInjector.getWifiNative()
                .getChannelsForBand(WifiScanner.WIFI_BAND_5_GHZ_DFS_ONLY);
        if (dfsChannels == null) {
            dfsChannels = new int[0];
        }

        final Set<Integer> dfsChannelSet = Arrays.stream(dfsChannels).boxed()
                .collect(Collectors.toSet());

        return scanResults.stream()
                .filter(scanResult -> !dfsChannelSet.contains(scanResult.frequency))
                .collect(Collectors.toList());
    }

    /** Returns a filtered set of saved networks from WifiConfigManager & suggestions
     * from WifiNetworkSuggestionsManager. */
    private Set<ScanResultMatchInfo> getGoodSavedNetworksAndSuggestions() {
        List<WifiConfiguration> savedNetworks = mWifiConfigManager.getSavedNetworks(
                Process.WIFI_UID);

        Set<ScanResultMatchInfo> goodNetworks = new HashSet<>(savedNetworks.size());
        for (WifiConfiguration config : savedNetworks) {
            if (config.hasNoInternetAccess()
                    || config.noInternetAccessExpected
                    || !config.getNetworkSelectionStatus().hasEverConnected()
                    || !config.allowAutojoin
                    || config.getNetworkSelectionStatus().isNetworkPermanentlyDisabled()
                    || (!config.getNetworkSelectionStatus().hasNeverDetectedCaptivePortal()
                    && !config.validatedInternetAccess)) {
                continue;
            }
            goodNetworks.add(ScanResultMatchInfo.fromWifiConfiguration(config));
        }

        Set<WifiNetworkSuggestion> networkSuggestions =
                mWifiNetworkSuggestionsManager.getAllApprovedNetworkSuggestions();
        for (WifiNetworkSuggestion suggestion : networkSuggestions) {
            // TODO(b/127799111): Do we need to filter the list similar to saved networks above?
            goodNetworks.add(
                    ScanResultMatchInfo.fromWifiConfiguration(suggestion.wifiConfiguration));
        }
        return goodNetworks;
    }

    /**
     * Handles incoming scan results.
     *
     * <p>The controller updates the WakeupLock with the incoming scan results. If WakeupLock is not
     * yet fully initialized, it adds the current scanResults to the lock and returns. If WakeupLock
     * is initialized but not empty, the controller updates the lock with the current scan. If it is
     * both initialized and empty, it evaluates scan results for a match with saved networks. If a
     * match exists, it enables wifi.
     *
     * <p>The feature must be enabled and the store data must be loaded in order for the controller
     * to handle scan results.
     *
     * @param scanResults The scan results with which to update the controller
     */
    private void handleScanResults(Collection<ScanResult> scanResults) {
        if (!isEnabledAndReady() || !mIsActive) {
            Log.d(TAG, "Attempted to handleScanResults while not enabled");
            return;
        }

        // only count scan as handled if isEnabledAndReady
        mNumScansHandled++;
        if (mVerboseLoggingEnabled) {
            Log.d(TAG, "Incoming scan #" + mNumScansHandled);
        }

        // need to show notification here in case user turns phone on while wifi is off
        mWakeupOnboarding.maybeShowNotification();

        // filter out unknown networks
        Set<ScanResultMatchInfo> goodNetworks = getGoodSavedNetworksAndSuggestions();
        Set<ScanResultMatchInfo> matchInfos = toMatchInfos(scanResults);
        matchInfos.retainAll(goodNetworks);

        mWakeupLock.update(matchInfos);
        if (!mWakeupLock.isUnlocked()) {
            return;
        }

        ScanResult network = mWakeupEvaluator.findViableNetwork(scanResults, goodNetworks);

        if (network != null) {
            Log.d(TAG, "Enabling wifi for network: " + network.SSID);
            enableWifi();
        }
    }

    /**
     * Converts ScanResults to ScanResultMatchInfos.
     */
    private static Set<ScanResultMatchInfo> toMatchInfos(Collection<ScanResult> scanResults) {
        return scanResults.stream()
                .map(ScanResultMatchInfo::fromScanResult)
                .collect(Collectors.toSet());
    }

    /**
     * Enables wifi.
     *
     * <p>This method ignores all checks and assumes that {@link ActiveModeWarden} is currently
     * in ScanModeState.
     */
    private void enableWifi() {
        if (USE_PLATFORM_WIFI_WAKE) {
            // TODO(b/72180295): ensure that there is no race condition with WifiServiceImpl here
            if (mWifiInjector.getWifiSettingsStore().handleWifiToggled(true /* wifiEnabled */)) {
                mActiveModeWarden.wifiToggled(
                        // Assumes user toggled it on from settings before.
                        mFrameworkFacade.getSettingsWorkSource(mContext));
                mWifiWakeMetrics.recordWakeupEvent(mNumScansHandled);
                mWifiMetrics.reportWifiStateChanged(true, isUsable(), true);
                mLastCallerInfoManager.put(WifiManager.API_WIFI_ENABLED, Process.myTid(),
                        Process.WIFI_UID, -1, "android_wifi_wake", true);
            }
        }
    }

    /**
     * Whether the feature is currently enabled and usable.
     *
     * <p>This method checks both the Settings values and the store data to ensure that it has been
     * read.
     */
    @VisibleForTesting
    boolean isEnabledAndReady() {
        return isUsable() && mWakeupConfigStoreData.hasBeenRead();
    }

    /** Dumps wakeup controller state. */
    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
        pw.println("Dump of WakeupController");
        pw.println("USE_PLATFORM_WIFI_WAKE: " + USE_PLATFORM_WIFI_WAKE);
        pw.println("mWifiWakeupEnabled: " + mWifiWakeupEnabled);
        pw.println("isOnboarded: " + mWakeupOnboarding.isOnboarded());
        pw.println("configStore hasBeenRead: " + mWakeupConfigStoreData.hasBeenRead());
        pw.println("mIsActive: " + mIsActive);
        pw.println("mNumScansHandled: " + mNumScansHandled);

        mWakeupLock.dump(fd, pw, args);
    }

    private class IsActiveDataSource implements WakeupConfigStoreData.DataSource<Boolean> {

        @Override
        public Boolean getData() {
            return mIsActive;
        }

        @Override
        public void setData(Boolean data) {
            mIsActive = data;
        }
    }

    public void resetNotification() {
        mWakeupOnboarding.onStop();
    }
}
