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

import android.content.Context;
import android.content.SharedPreferences;
import android.database.ContentObserver;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Handler;
import android.preference.PreferenceManager;
import android.provider.Settings;
import android.text.TextUtils;
import android.webkit.GeolocationPermissions;
import android.webkit.ValueCallback;

import java.util.HashSet;
import java.util.Set;

/**
 * Manages the interaction between the secure system setting for default geolocation
 * permissions and the browser.
 */
class SystemAllowGeolocationOrigins {

    // Preference key for the value of the system setting last read by the browser
    private final static String LAST_READ_ALLOW_GEOLOCATION_ORIGINS =
            "last_read_allow_geolocation_origins";

    // The application context
    private final Context mContext;

    // The observer used to listen to the system setting.
    private final SettingObserver mSettingObserver;

    public SystemAllowGeolocationOrigins(Context context) {
        mContext = context.getApplicationContext();
        mSettingObserver = new SettingObserver();
    }

    /**
     * Checks whether the setting has changed and installs an observer to listen for
     * future changes. Must be called on the application main thread.
     */
    public void start() {
        // Register to receive notifications when the system settings change.
        Uri uri = Settings.Secure.getUriFor(Settings.Secure.ALLOWED_GEOLOCATION_ORIGINS);
        mContext.getContentResolver().registerContentObserver(uri, false, mSettingObserver);

        // Read and apply the setting if needed.
        maybeApplySettingAsync();
    }

    /**
     * Stops the manager.
     */
    public void stop() {
        mContext.getContentResolver().unregisterContentObserver(mSettingObserver);
    }

    void maybeApplySettingAsync() {
        BackgroundHandler.execute(mMaybeApplySetting);
    }

    /**
     * Checks to see if the system setting has changed and if so,
     * updates the Geolocation permissions accordingly.
     */
    private Runnable mMaybeApplySetting = new Runnable() {

        @Override
        public void run() {
         // Get the new value
            String newSetting = getSystemSetting();

            // Get the last read value
            SharedPreferences preferences = BrowserSettings.getInstance()
                    .getPreferences();
            String lastReadSetting =
                    preferences.getString(LAST_READ_ALLOW_GEOLOCATION_ORIGINS, "");

            // If the new value is the same as the last one we read, we're done.
            if (TextUtils.equals(lastReadSetting, newSetting)) {
                return;
            }

            // Save the new value as the last read value
            preferences.edit()
                    .putString(LAST_READ_ALLOW_GEOLOCATION_ORIGINS, newSetting)
                    .apply();

            Set<String> oldOrigins = parseAllowGeolocationOrigins(lastReadSetting);
            Set<String> newOrigins = parseAllowGeolocationOrigins(newSetting);
            Set<String> addedOrigins = setMinus(newOrigins, oldOrigins);
            Set<String> removedOrigins = setMinus(oldOrigins, newOrigins);

            // Remove the origins in the last read value
            removeOrigins(removedOrigins);

            // Add the origins in the new value
            addOrigins(addedOrigins);
        }
    };

    /**
     * Parses the value of the default geolocation permissions setting.
     *
     * @param setting A space-separated list of origins.
     * @return A mutable set of origins.
     */
    private static HashSet<String> parseAllowGeolocationOrigins(String setting) {
        HashSet<String> origins = new HashSet<String>();
        if (!TextUtils.isEmpty(setting)) {
            for (String origin : setting.split("\\s+")) {
                if (!TextUtils.isEmpty(origin)) {
                    origins.add(origin);
                }
            }
        }
        return origins;
    }

    /**
     * Gets the difference between two sets. Does not modify any of the arguments.
     *
     * @return A set containing all elements in {@code x} that are not in {@code y}.
     */
    private <A> Set<A> setMinus(Set<A> x, Set<A> y) {
        HashSet<A> z = new HashSet<A>(x.size());
        for (A a : x) {
            if (!y.contains(a)) {
                z.add(a);
            }
        }
        return z;
    }

    /**
     * Gets the current system setting for default allowed geolocation origins.
     *
     * @return The default allowed origins. Returns {@code ""} if not set.
     */
    private String getSystemSetting() {
        String value = Settings.Secure.getString(mContext.getContentResolver(),
                Settings.Secure.ALLOWED_GEOLOCATION_ORIGINS);
        return value == null ? "" : value;
    }

    /**
     * Adds geolocation permissions for the given origins.
     */
    private void addOrigins(Set<String> origins) {
        for (String origin : origins) {
            GeolocationPermissions.getInstance().allow(origin);
        }
    }

    /**
     * Removes geolocation permissions for the given origins, if they are allowed.
     * If they are denied or not set, nothing is done.
     */
    private void removeOrigins(Set<String> origins) {
        for (final String origin : origins) {
            GeolocationPermissions.getInstance().getAllowed(origin, new ValueCallback<Boolean>() {
                public void onReceiveValue(Boolean value) {
                    if (value != null && value.booleanValue()) {
                        GeolocationPermissions.getInstance().clear(origin);
                    }
                }
            });
        }
    }

    /**
     * Listens for changes to the system setting.
     */
    private class SettingObserver extends ContentObserver {

        SettingObserver() {
            super(new Handler());
        }

        @Override
        public void onChange(boolean selfChange) {
            maybeApplySettingAsync();
        }
    }

}
