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

import static android.Manifest.permission.READ_SEARCH_INDEXABLES;
import static android.app.slice.Slice.HINT_PARTIAL;

import android.app.PendingIntent;
import android.app.settings.SettingsEnums;
import android.app.slice.SliceManager;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Binder;
import android.os.StrictMode;
import android.os.UserManager;
import android.provider.Settings;
import android.provider.SettingsSlicesContract;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.KeyValueListParser;
import android.util.Log;
import android.util.Pair;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import androidx.collection.ArraySet;
import androidx.slice.Slice;
import androidx.slice.SliceProvider;

import com.android.settings.R;
import com.android.settings.Utils;
import com.android.settings.bluetooth.BluetoothSliceBuilder;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.notification.VolumeSeekBarPreferenceController;
import com.android.settings.notification.zen.ZenModeSliceBuilder;
import com.android.settings.overlay.FeatureFactory;
import com.android.settingslib.SliceBroadcastRelay;
import com.android.settingslib.utils.ThreadUtils;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.stream.Collectors;

/**
 * A {@link SliceProvider} for Settings to enabled inline results in system apps.
 *
 * <p>{@link SettingsSliceProvider} accepts a {@link Uri} with {@link #SLICE_AUTHORITY} and a
 * {@code String} key based on the setting intended to be changed. This provider builds a
 * {@link Slice} and responds to Slice actions through the database defined by
 * {@link SlicesDatabaseHelper}, whose data is written by {@link SlicesIndexer}.
 *
 * <p>When a {@link Slice} is requested, we start loading {@link SliceData} in the background and
 * return an stub {@link Slice} with the correct {@link Uri} immediately. In the background, the
 * data corresponding to the key in the {@link Uri} is read by {@link SlicesDatabaseAccessor}, and
 * the entire row is converted into a {@link SliceData}. Once complete, it is stored in
 * {@link #mSliceWeakDataCache}, and then an update sent via the Slice framework to the Slice.
 * The {@link Slice} displayed by the Slice-presenter will re-query this Slice-provider and find
 * the {@link SliceData} cached to build the full {@link Slice}.
 *
 * <p>When an action is taken on that {@link Slice}, we receive the action in
 * {@link SliceBroadcastReceiver}, and use the
 * {@link com.android.settings.core.BasePreferenceController} indexed as
 * {@link SlicesDatabaseHelper.IndexColumns#CONTROLLER} to manipulate the setting.
 */
public class SettingsSliceProvider extends SliceProvider {

    private static final String TAG = "SettingsSliceProvider";

    /**
     * Authority for Settings slices not officially supported by the platform, but extensible for
     * OEMs.
     */
    public static final String SLICE_AUTHORITY = "com.android.settings.slices";

    /**
     * Action passed for changes to Toggle Slices.
     */
    public static final String ACTION_TOGGLE_CHANGED =
            "com.android.settings.slice.action.TOGGLE_CHANGED";

    /**
     * Action passed for changes to Slider Slices.
     */
    public static final String ACTION_SLIDER_CHANGED =
            "com.android.settings.slice.action.SLIDER_CHANGED";

    /**
     * Intent Extra passed for the key identifying the Setting Slice.
     */
    public static final String EXTRA_SLICE_KEY = "com.android.settings.slice.extra.key";

    /**
     * A list of custom slice uris that are supported publicly. This is a subset of slices defined
     * in {@link CustomSliceRegistry}. Things here are exposed publicly so all clients with proper
     * permission can use them.
     */
    private static final List<Uri> PUBLICLY_SUPPORTED_CUSTOM_SLICE_URIS =
            android.app.Flags.modesUi()
                    ?
                    Arrays.asList(
                            CustomSliceRegistry.BLUETOOTH_URI,
                            CustomSliceRegistry.FLASHLIGHT_SLICE_URI,
                            CustomSliceRegistry.LOCATION_SLICE_URI,
                            CustomSliceRegistry.MOBILE_DATA_SLICE_URI,
                            CustomSliceRegistry.WIFI_CALLING_URI,
                            CustomSliceRegistry.WIFI_SLICE_URI
                    ) :
            Arrays.asList(
                    CustomSliceRegistry.BLUETOOTH_URI,
                    CustomSliceRegistry.FLASHLIGHT_SLICE_URI,
                    CustomSliceRegistry.LOCATION_SLICE_URI,
                    CustomSliceRegistry.MOBILE_DATA_SLICE_URI,
                    CustomSliceRegistry.WIFI_CALLING_URI,
                    CustomSliceRegistry.WIFI_SLICE_URI,
                    CustomSliceRegistry.ZEN_MODE_SLICE_URI
            );

    private static final KeyValueListParser KEY_VALUE_LIST_PARSER = new KeyValueListParser(',');

    @VisibleForTesting
    SlicesDatabaseAccessor mSlicesDatabaseAccessor;

    @VisibleForTesting
    Map<Uri, SliceData> mSliceWeakDataCache;

    @VisibleForTesting
    final Map<Uri, SliceBackgroundWorker> mPinnedWorkers = new ArrayMap<>();

    private Boolean mNightMode;
    private boolean mFirstSlicePinned;
    private boolean mFirstSliceBound;

    public SettingsSliceProvider() {
        super(READ_SEARCH_INDEXABLES);
        Log.d(TAG, "init");
    }

    @Override
    public boolean onCreateSliceProvider() {
        Log.d(TAG, "onCreateSliceProvider");
        mSlicesDatabaseAccessor = new SlicesDatabaseAccessor(getContext());
        mSliceWeakDataCache = new WeakHashMap<>();
        return true;
    }

    @Override
    public void onSlicePinned(Uri sliceUri) {
        if (!mFirstSlicePinned) {
            Log.d(TAG, "onSlicePinned: " + sliceUri);
            mFirstSlicePinned = true;
        }
        FeatureFactory.getFeatureFactory().getMetricsFeatureProvider()
                .action(SettingsEnums.PAGE_UNKNOWN,
                        SettingsEnums.ACTION_SETTINGS_SLICE_REQUESTED,
                        SettingsEnums.PAGE_UNKNOWN,
                        sliceUri.getLastPathSegment(),
                        0);

        if (CustomSliceRegistry.isValidUri(sliceUri)) {
            final Context context = getContext();
            final CustomSliceable sliceable = FeatureFactory.getFeatureFactory()
                    .getSlicesFeatureProvider().getSliceableFromUri(context, sliceUri);
            final IntentFilter filter = sliceable.getIntentFilter();
            if (filter != null) {
                registerIntentToUri(filter, sliceUri);
            }
            ThreadUtils.postOnMainThread(() -> startBackgroundWorker(sliceable, sliceUri));
            return;
        }

        if (CustomSliceRegistry.ZEN_MODE_SLICE_URI.equals(sliceUri)) {
            if (!android.app.Flags.modesUi()) {
                registerIntentToUri(ZenModeSliceBuilder.INTENT_FILTER, sliceUri);
            }
            return;
        } else if (CustomSliceRegistry.BLUETOOTH_URI.equals(sliceUri)) {
            registerIntentToUri(BluetoothSliceBuilder.INTENT_FILTER, sliceUri);
            return;
        }

        // Start warming the slice, we expect someone will want it soon.
        loadSliceInBackground(sliceUri);
    }

    @Override
    public void onSliceUnpinned(Uri sliceUri) {
        mSliceWeakDataCache.remove(sliceUri);
        final Context context = getContext();
        if (!VolumeSliceHelper.unregisterUri(context, sliceUri)) {
            SliceBroadcastRelay.unregisterReceivers(context, sliceUri);
        }
        ThreadUtils.postOnMainThread(() -> stopBackgroundWorker(sliceUri));
    }

    @Override
    public Slice onBindSlice(Uri sliceUri) {
        if (!mFirstSliceBound) {
            Log.d(TAG, "onBindSlice start: " + sliceUri);
        }
        final StrictMode.ThreadPolicy oldPolicy = StrictMode.getThreadPolicy();
        try {
            if (!ThreadUtils.isMainThread()) {
                StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                        .permitAll()
                        .build());
            }
            final Set<String> blockedKeys = getBlockedKeys();
            final String key = sliceUri.getLastPathSegment();
            if (blockedKeys.contains(key)) {
                Log.e(TAG, "Requested blocked slice with Uri: " + sliceUri);
                return null;
            }

            final boolean nightMode = Utils.isNightMode(getContext());
            if (mNightMode == null) {
                mNightMode = nightMode;
                getContext().setTheme(com.android.settingslib.widget.theme.R.style.Theme_SettingsBase);
            } else if (mNightMode != nightMode) {
                Log.d(TAG, "Night mode changed, reload theme");
                mNightMode = nightMode;
                getContext().getTheme().rebase();
            }

            // Checking if some semi-sensitive slices are requested by a guest user. If so, will
            // return an empty slice.
            final UserManager userManager = getContext().getSystemService(UserManager.class);
            if (userManager.isGuestUser() && RestrictedSliceUtils.isGuestRestricted(sliceUri)) {
                Log.i(TAG, "Guest user access denied.");
                return null;
            }

            // Before adding a slice to {@link CustomSliceManager}, please get approval
            // from the Settings team.
            if (CustomSliceRegistry.isValidUri(sliceUri)) {
                final Context context = getContext();
                return FeatureFactory.getFeatureFactory()
                        .getSlicesFeatureProvider().getSliceableFromUri(context, sliceUri)
                        .getSlice();
            }

            if (CustomSliceRegistry.WIFI_CALLING_URI.equals(sliceUri)) {
                return FeatureFactory.getFeatureFactory()
                        .getSlicesFeatureProvider()
                        .getNewWifiCallingSliceHelper(getContext())
                        .createWifiCallingSlice(sliceUri);
            } else if (!android.app.Flags.modesUi()
                    && CustomSliceRegistry.ZEN_MODE_SLICE_URI.equals(sliceUri)) {
                return ZenModeSliceBuilder.getSlice(getContext());
            } else if (CustomSliceRegistry.BLUETOOTH_URI.equals(sliceUri)) {
                return BluetoothSliceBuilder.getSlice(getContext());
            } else if (CustomSliceRegistry.ENHANCED_4G_SLICE_URI.equals(sliceUri)) {
                return FeatureFactory.getFeatureFactory()
                        .getSlicesFeatureProvider()
                        .getNewEnhanced4gLteSliceHelper(getContext())
                        .createEnhanced4gLteSlice(sliceUri);
            } else if (CustomSliceRegistry.WIFI_CALLING_PREFERENCE_URI.equals(sliceUri)) {
                return FeatureFactory.getFeatureFactory()
                        .getSlicesFeatureProvider()
                        .getNewWifiCallingSliceHelper(getContext())
                        .createWifiCallingPreferenceSlice(sliceUri);
            }

            final SliceData cachedSliceData = mSliceWeakDataCache.get(sliceUri);
            if (cachedSliceData == null) {
                loadSliceInBackground(sliceUri);
                return getSliceStub(sliceUri);
            }
            return SliceBuilderUtils.buildSlice(getContext(), cachedSliceData);
        } finally {
            StrictMode.setThreadPolicy(oldPolicy);
            if (!mFirstSliceBound) {
                Log.v(TAG, "onBindSlice end");
                mFirstSliceBound = true;
            }
        }
    }

    /**
     * Get a list of all valid Uris based on the keys indexed in the Slices database.
     * <p>
     * This will return a list of {@link Uri uris} depending on {@param uri}, following:
     * 1. Authority & Full Path -> Only {@param uri}. It is only a prefix for itself.
     * 2. Authority & No path -> A list of authority/action/$KEY$, where
     * {@code $KEY$} is a list of all Slice-enabled keys for the authority.
     * 3. Authority & action path -> A list of authority/action/$KEY$, where
     * {@code $KEY$} is a list of all Slice-enabled keys for the authority.
     * 4. Empty authority & path -> A list of Uris with all keys for both supported authorities.
     * 5. Else -> Empty list.
     * <p>
     * Note that the authority will stay consistent with {@param uri}, and the list of valid Slice
     * keys depends on if the authority is {@link SettingsSlicesContract#AUTHORITY} or
     * {@link #SLICE_AUTHORITY}.
     *
     * @param uri The uri to look for descendants under.
     * @returns all valid Settings uris for which {@param uri} is a prefix.
     */
    @Override
    public Collection<Uri> onGetSliceDescendants(Uri uri) {
        final List<Uri> descendants = new ArrayList<>();
        Uri finalUri = uri;

        if (isPrivateSlicesNeeded(finalUri)) {
            descendants.addAll(
                    mSlicesDatabaseAccessor.getSliceUris(finalUri.getAuthority(),
                            false /* isPublicSlice */));
            Log.d(TAG, "provide " + descendants.size() + " non-public slices");
            finalUri = new Uri.Builder()
                    .scheme(ContentResolver.SCHEME_CONTENT)
                    .authority(finalUri.getAuthority())
                    .build();
        }

        final Pair<Boolean, String> pathData = SliceBuilderUtils.getPathData(finalUri);

        if (pathData != null) {
            // Uri has a full path and will not have any descendants.
            descendants.add(finalUri);
            return descendants;
        }

        final String authority = finalUri.getAuthority();
        final String path = finalUri.getPath();
        final boolean isPathEmpty = path.isEmpty();

        // Path is anything but empty, "action", or "intent". Return empty list.
        if (!isPathEmpty
                && !TextUtils.equals(path, "/" + SettingsSlicesContract.PATH_SETTING_ACTION)
                && !TextUtils.equals(path, "/" + SettingsSlicesContract.PATH_SETTING_INTENT)) {
            // Invalid path prefix, there are no valid Uri descendants.
            return descendants;
        }

        // Add all descendants from db with matching authority.
        descendants.addAll(mSlicesDatabaseAccessor.getSliceUris(authority, true /*isPublicSlice*/));

        if (isPathEmpty && TextUtils.isEmpty(authority)) {
            // No path nor authority. Return all possible Uris by adding all special slice uri
            descendants.addAll(PUBLICLY_SUPPORTED_CUSTOM_SLICE_URIS);
        } else {
            // Can assume authority belongs to the provider. Return all Uris for the authority.
            final List<Uri> customSlices = PUBLICLY_SUPPORTED_CUSTOM_SLICE_URIS.stream()
                    .filter(sliceUri -> TextUtils.equals(authority, sliceUri.getAuthority()))
                    .collect(Collectors.toList());
            descendants.addAll(customSlices);
        }
        grantAllowlistedPackagePermissions(getContext(), descendants);
        return descendants;
    }

    @Nullable
    @Override
    public PendingIntent onCreatePermissionRequest(@NonNull Uri sliceUri,
            @NonNull String callingPackage) {
        final Intent settingsIntent = new Intent(Settings.ACTION_SETTINGS)
                .setPackage(Utils.SETTINGS_PACKAGE_NAME);
        final PendingIntent noOpIntent = PendingIntent.getActivity(getContext(),
                0 /* requestCode */, settingsIntent, PendingIntent.FLAG_IMMUTABLE);
        return noOpIntent;
    }

    @VisibleForTesting
    static void grantAllowlistedPackagePermissions(Context context, List<Uri> descendants) {
        if (descendants == null) {
            Log.d(TAG, "No descendants to grant permission with, skipping.");
        }
        final String[] allowlistPackages =
                context.getResources().getStringArray(R.array.slice_allowlist_package_names);
        if (allowlistPackages == null || allowlistPackages.length == 0) {
            Log.d(TAG, "No packages to allowlist, skipping.");
            return;
        } else {
            Log.d(TAG, String.format(
                    "Allowlisting %d uris to %d pkgs.",
                    descendants.size(), allowlistPackages.length));
        }
        final SliceManager sliceManager = context.getSystemService(SliceManager.class);
        for (Uri descendant : descendants) {
            for (String toPackage : allowlistPackages) {
                sliceManager.grantSlicePermission(toPackage, descendant);
            }
        }
    }

    @Override
    public void shutdown() {
        ThreadUtils.postOnMainThread(() -> {
            SliceBackgroundWorker.shutdown();
        });
    }

    @VisibleForTesting
    void loadSlice(Uri uri) {
        if (mSliceWeakDataCache.containsKey(uri)) {
            Log.d(TAG, uri + " loaded from cache");
            return;
        }
        long startBuildTime = System.currentTimeMillis();

        final SliceData sliceData;
        try {
            sliceData = mSlicesDatabaseAccessor.getSliceDataFromUri(uri);
        } catch (IllegalStateException e) {
            Log.d(TAG, "Could not create slicedata for uri: " + uri, e);
            return;
        }

        final BasePreferenceController controller = SliceBuilderUtils.getPreferenceController(
                getContext(), sliceData);

        final IntentFilter filter = controller.getIntentFilter();
        if (filter != null) {
            if (controller instanceof VolumeSeekBarPreferenceController) {
                // Register volume slices to a broadcast relay to reduce unnecessary UI updates
                VolumeSliceHelper.registerIntentToUri(getContext(), filter, uri,
                        ((VolumeSeekBarPreferenceController) controller).getAudioStream());
            } else {
                registerIntentToUri(filter, uri);
            }
        }

        ThreadUtils.postOnMainThread(() -> startBackgroundWorker(controller, uri));

        mSliceWeakDataCache.put(uri, sliceData);
        getContext().getContentResolver().notifyChange(uri, null /* content observer */);

        Log.d(TAG, "Built slice (" + uri + ") in: " +
                (System.currentTimeMillis() - startBuildTime));
    }

    @VisibleForTesting
    void loadSliceInBackground(Uri uri) {
        ThreadUtils.postOnBackgroundThread(() -> loadSlice(uri));
    }

    @VisibleForTesting
    /**
     * Registers an IntentFilter in SysUI to notify changes to {@param sliceUri} when broadcasts to
     * {@param intentFilter} happen.
     */
    void registerIntentToUri(IntentFilter intentFilter, Uri sliceUri) {
        SliceBroadcastRelay.registerReceiver(getContext(), sliceUri, SliceRelayReceiver.class,
                intentFilter);
    }

    @VisibleForTesting
    Set<String> getBlockedKeys() {
        final String value = Settings.Global.getString(getContext().getContentResolver(),
                Settings.Global.BLOCKED_SLICES);
        final Set<String> set = new ArraySet<>();

        try {
            KEY_VALUE_LIST_PARSER.setString(value);
        } catch (IllegalArgumentException e) {
            Log.e(TAG, "Bad Settings Slices Allowlist flags", e);
            return set;
        }

        final String[] parsedValues = parseStringArray(value);
        Collections.addAll(set, parsedValues);
        return set;
    }

    @VisibleForTesting
    boolean isPrivateSlicesNeeded(Uri uri) {
        final Context context = getContext();
        final String queryUri = context.getString(R.string.config_non_public_slice_query_uri);

        if (!TextUtils.isEmpty(queryUri) && TextUtils.equals(uri.toString(), queryUri)) {
            // check if the calling package is eligible for private slices
            final int callingUid = Binder.getCallingUid();
            final boolean hasPermission =
                    context.checkPermission(
                                    android.Manifest.permission.READ_SEARCH_INDEXABLES,
                                    Binder.getCallingPid(),
                                    callingUid)
                            == PackageManager.PERMISSION_GRANTED;
            final String[] packages = context.getPackageManager().getPackagesForUid(callingUid);
            final String callingPackage =
                    packages != null && packages.length > 0 ? packages[0] : null;
            return hasPermission
                    && TextUtils.equals(
                            callingPackage,
                            context.getString(R.string.config_settingsintelligence_package_name));
        }
        return false;
    }

    private void startBackgroundWorker(Sliceable sliceable, Uri uri) {
        final Class workerClass = sliceable.getBackgroundWorkerClass();
        if (workerClass == null) {
            return;
        }

        if (mPinnedWorkers.containsKey(uri)) {
            return;
        }

        Log.d(TAG, "Starting background worker for: " + uri);
        final SliceBackgroundWorker worker = SliceBackgroundWorker.getInstance(
                getContext(), sliceable, uri);
        mPinnedWorkers.put(uri, worker);
        worker.pin();
    }

    private void stopBackgroundWorker(Uri uri) {
        final SliceBackgroundWorker worker = mPinnedWorkers.get(uri);
        if (worker != null) {
            Log.d(TAG, "Stopping background worker for: " + uri);
            worker.unpin();
            mPinnedWorkers.remove(uri);
        }
    }

    /**
     * @return an empty {@link Slice} with {@param uri} to be used as a stub while the real
     * {@link SliceData} is loaded from {@link SlicesDatabaseHelper.Tables#TABLE_SLICES_INDEX}.
     */
    private static Slice getSliceStub(Uri uri) {
        // TODO: Switch back to ListBuilder when slice loading states are fixed.
        return new Slice.Builder(uri).addHints(HINT_PARTIAL).build();
    }

    private static String[] parseStringArray(String value) {
        if (value != null) {
            String[] parts = value.split(":");
            if (parts.length > 0) {
                return parts;
            }
        }
        return new String[0];
    }
}
