/*
 * Copyright (C) 2020 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.managedprovisioning.ota;

import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static android.os.Build.VERSION_CODES.P;
import static android.os.Build.VERSION_CODES.Q;
import static android.os.Build.VERSION_CODES.R;

import static com.google.common.base.Preconditions.checkNotNull;

import android.Manifest;
import android.Manifest.permission;
import android.annotation.NonNull;
import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.app.AppOpsManager;
import android.app.AppOpsManager.Mode;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.pm.CrossProfileApps;
import android.content.pm.ICrossProfileApps;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.graphics.drawable.ColorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Process;
import android.os.UserHandle;
import android.text.TextUtils;

import com.google.common.collect.ImmutableList;
import com.google.common.collect.Iterables;

import org.robolectric.annotation.Implementation;
import org.robolectric.annotation.Implements;
import org.robolectric.annotation.Resetter;
import org.robolectric.shadows.ShadowCrossProfileApps;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;

import javax.annotation.Nullable;

/** Robolectric implementation of {@link CrossProfileApps}. 
* This class is debt, and it seems intents should be used in tests instead,
* as exposed by robolectric's version of this Shadows.
*/
@Implements(value = CrossProfileApps.class, minSdk = P)
public class ExtendsShadowCrossProfileApps {

  // BEGIN-INTERNAL
  private final static String INTERACT_ACROSS_PROFILES_APPOP = AppOpsManager.permissionToOp(
          Manifest.permission.INTERACT_ACROSS_PROFILES);
  private static final Set<String> configurableInteractAcrossProfilePackages = new HashSet<>();
  // END-INTERNAL

  private final Set<UserHandle> targetUserProfiles = new LinkedHashSet<>();
  private final List<StartedMainActivity> startedMainActivities = new ArrayList<>();
  private final List<StartedActivity> startedActivities =
          Collections.synchronizedList(new ArrayList<>());
  private final Map<String, Integer> packageNameAppOpModes = new HashMap<>();

  private Context context;
  private PackageManager packageManager;

  @Implementation
  protected void __constructor__(Context context, ICrossProfileApps service) {
    this.context = context;
    this.packageManager = context.getPackageManager();
  }

  /**
   * Returns a list of {@link UserHandle}s currently accessible. This list is populated from calls
   * to {@link #addTargetUserProfile(UserHandle)}.
   */
  @Implementation
  protected List<UserHandle> getTargetUserProfiles() {
    return ImmutableList.copyOf(targetUserProfiles);
  }

  /**
   * Returns a {@link Drawable} that can be shown for profile switching, which is guaranteed to
   * always be the same for a particular user and to be distinct between users.
   */
  @Implementation
  protected Drawable getProfileSwitchingIconDrawable(UserHandle userHandle) {
    verifyCanAccessUser(userHandle);
    return new ColorDrawable(userHandle.getIdentifier());
  }

  /**
   * Returns a {@link CharSequence} that can be shown as a label for profile switching, which is
   * guaranteed to always be the same for a particular user and to be distinct between users.
   */
  @Implementation
  protected CharSequence getProfileSwitchingLabel(UserHandle userHandle) {
    verifyCanAccessUser(userHandle);
    return "Switch to " + userHandle;
  }

  /**
   * Simulates starting the main activity specified in the specified profile, performing the same
   * security checks done by the real {@link CrossProfileApps}.
   *
   * <p>The most recent main activity started can be queried by {@link #peekNextStartedActivity()}
   * ()}.
   */
  @Implementation
  protected void startMainActivity(ComponentName componentName, UserHandle targetUser) {
    verifyCanAccessUser(targetUser);
    verifyActivityInManifest(componentName, /* requireMainActivity= */ true);
    startedMainActivities.add(new StartedMainActivity(componentName, targetUser));
    startedActivities.add(new StartedActivity(componentName, targetUser));
  }

  /**
   * Simulates starting the activity specified in the specified profile, performing the same
   * security checks done by the real {@link CrossProfileApps}.
   *
   * <p>The most recent main activity started can be queried by {@link #peekNextStartedActivity()}
   * ()}.
   */
  @Implementation(minSdk = Q)
  @SystemApi
  @RequiresPermission(permission.INTERACT_ACROSS_PROFILES)
  protected void startActivity(ComponentName componentName, UserHandle targetUser) {
    verifyCanAccessUser(targetUser);
    verifyActivityInManifest(componentName, /* requireMainActivity= */ false);
    verifyHasInteractAcrossProfilesPermission();
    startedActivities.add(new StartedActivity(componentName, targetUser));
  }

  /** Adds {@code userHandle} to the list of accessible handles. */
  public void addTargetUserProfile(UserHandle userHandle) {
    if (userHandle.equals(Process.myUserHandle())) {
      throw new IllegalArgumentException("Cannot target current user");
    }
    targetUserProfiles.add(userHandle);
  }

  /** Removes {@code userHandle} from the list of accessible handles, if present. */
  public void removeTargetUserProfile(UserHandle userHandle) {
    if (userHandle.equals(Process.myUserHandle())) {
      throw new IllegalArgumentException("Cannot target current user");
    }
    targetUserProfiles.remove(userHandle);
  }

  /** Clears the list of accessible handles. */
  public void clearTargetUserProfiles() {
    targetUserProfiles.clear();
  }

  /**
   * Returns the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link
   * CrossProfileApps#startMainActivity(ComponentName, UserHandle)}, wrapped in {@link
   * StartedMainActivity}.
   *
   * @deprecated Use {@link #peekNextStartedActivity()} instead.
   */
  @Nullable
  @Deprecated
  public StartedMainActivity peekNextStartedMainActivity() {
    if (startedMainActivities.isEmpty()) {
      return null;
    } else {
      return Iterables.getLast(startedMainActivities);
    }
  }

  /**
   * Returns the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link
   * CrossProfileApps#startMainActivity(ComponentName, UserHandle)} or {@link
   * CrossProfileApps#startActivity(ComponentName, UserHandle)}, wrapped in {@link StartedActivity}.
   */
  @Nullable
  public StartedActivity peekNextStartedActivity() {
    if (startedActivities.isEmpty()) {
      return null;
    } else {
      return Iterables.getLast(startedActivities);
    }
  }

  /**
   * Consumes the most recent {@link ComponentName}, {@link UserHandle} pair started by {@link
   * CrossProfileApps#startMainActivity(ComponentName, UserHandle)} or {@link
   * CrossProfileApps#startActivity(ComponentName, UserHandle)}, and returns it wrapped in {@link
   * StartedActivity}.
   */
  @Nullable
  public StartedActivity getNextStartedActivity() {
    if (startedActivities.isEmpty()) {
      return null;
    } else {
      return startedActivities.remove(startedActivities.size() - 1);
    }
  }

  /**
   * Clears all records of {@link StartedActivity}s from calls to {@link
   * CrossProfileApps#startActivity(ComponentName, UserHandle)} or {@link
   * CrossProfileApps#startMainActivity(ComponentName, UserHandle)}.
   */
  public void clearNextStartedActivities() {
    startedActivities.clear();
  }

  private void verifyCanAccessUser(UserHandle userHandle) {
    if (!targetUserProfiles.contains(userHandle)) {
      throw new SecurityException(
              "Not allowed to access "
                      + userHandle
                      + " (did you forget to call addTargetUserProfile?)");
    }
  }

  private void verifyHasInteractAcrossProfilesPermission() {
    if (context.checkSelfPermission(permission.INTERACT_ACROSS_PROFILES)
            != PackageManager.PERMISSION_GRANTED) {
      throw new SecurityException(
              "Attempt to launch activity without required "
                      + permission.INTERACT_ACROSS_PROFILES
                      + " permission");
    }
  }

  /**
   * Ensures that {@code component} is present in the manifest as an exported and enabled activity.
   * This check and the error thrown are the same as the check done by the real {@link
   * CrossProfileApps}.
   *
   * <p>If {@code requireMainActivity} is true, then this also asserts that the activity is a
   * launcher activity.
   */
  private void verifyActivityInManifest(ComponentName component, boolean requireMainActivity) {
    Intent launchIntent = new Intent();
    if (requireMainActivity) {
      launchIntent
              .setAction(Intent.ACTION_MAIN)
              .addCategory(Intent.CATEGORY_LAUNCHER)
              .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED)
              .setPackage(component.getPackageName());
    } else {
      launchIntent.setComponent(component);
    }

    boolean existsMatchingActivity =
            Iterables.any(
                    packageManager.queryIntentActivities(
                            launchIntent, MATCH_DIRECT_BOOT_AWARE | MATCH_DIRECT_BOOT_UNAWARE),
                    resolveInfo -> {
                      ActivityInfo activityInfo = resolveInfo.activityInfo;
                      return TextUtils.equals(activityInfo.packageName, component.getPackageName())
                              && TextUtils.equals(activityInfo.name, component.getClassName())
                              && activityInfo.exported;
                    });
    if (!existsMatchingActivity) {
      throw new SecurityException(
              "Attempt to launch activity without "
                      + " category Intent.CATEGORY_LAUNCHER or activity is not exported"
                      + component);
    }
  }

  // BEGIN-INTERNAL
  @Implementation(minSdk = R)
  @RequiresPermission(
          allOf={android.Manifest.permission.MANAGE_APP_OPS_MODES,
                  android.Manifest.permission.INTERACT_ACROSS_USERS})
  protected void setInteractAcrossProfilesAppOp(String packageName, @Mode int newMode) {
    packageNameAppOpModes.put(packageName, newMode);
  }

  /**
   * Returns the app-op mode associated with the given package name. If not set, returns {@code
   * null}.
   */
  @Nullable
  public @Mode Integer getInteractAcrossProfilesAppOp(String packageName) {
    return packageNameAppOpModes.get(packageName);
  }

  public void addCrossProfilePackage(String packageName){
    configurableInteractAcrossProfilePackages.add(packageName);
  }

  @Implementation(minSdk = R)
  protected void resetInteractAcrossProfilesAppOps(
          @NonNull Collection<String> previousCrossProfilePackages,
          @NonNull Set<String> newCrossProfilePackages) {

    final List<String> unsetCrossProfilePackages =
            previousCrossProfilePackages.stream()
                    .filter(packageName -> !newCrossProfilePackages.contains(packageName))
                    .collect(Collectors.toList());

    for (String packageName : unsetCrossProfilePackages) {
      if (!canConfigureInteractAcrossProfiles(packageName)) {
        setInteractAcrossProfilesAppOp(packageName,
                AppOpsManager.opToDefaultMode(INTERACT_ACROSS_PROFILES_APPOP));
      }
    }
  }

  // BEGIN-INTERNAL
  @Implementation(minSdk = R)
  protected void clearInteractAcrossProfilesAppOps() {
    findAllPackageNames().forEach(
        packageName -> setInteractAcrossProfilesAppOp(
            packageName, AppOpsManager.opToDefaultMode(INTERACT_ACROSS_PROFILES_APPOP)));
  }

  private List<String> findAllPackageNames() {
    return context.getPackageManager()
        .getInstalledApplications(/* flags= */ 0)
        .stream()
        .map(applicationInfo -> applicationInfo.packageName)
        .collect(Collectors.toList());
  }
  // END-INTERNAL

  @Implementation
  protected boolean canConfigureInteractAcrossProfiles(@NonNull String packageName) {
    return configurableInteractAcrossProfilePackages.contains(packageName);
  }

  @Implementation
  protected boolean canUserAttemptToConfigureInteractAcrossProfiles(@NonNull String packageName) {
    PackageInfo packageInfo;
    try {
      packageInfo = packageManager.getPackageInfo(packageName, /* flags= */ 0);
    } catch (PackageManager.NameNotFoundException e) {
      return false;
    }
    if (packageInfo == null || packageInfo.requestedPermissions == null) {
      return false;
    }
    return Arrays.asList(packageInfo.requestedPermissions).contains(
            Manifest.permission.INTERACT_ACROSS_PROFILES);
  }

  @Resetter
  public static void reset() {
    configurableInteractAcrossProfilePackages.clear();
  }
  // END-INTERNAL

  /**
   * Container object to hold parameters passed to {@link #startMainActivity(ComponentName,
   * UserHandle)}.
   *
   * @deprecated Use {@link #peekNextStartedActivity()} and {@link StartedActivity} instead.
   */
  @Deprecated
  public static class StartedMainActivity {

    private final ComponentName componentName;
    private final UserHandle userHandle;

    public StartedMainActivity(ComponentName componentName, UserHandle userHandle) {
      this.componentName = checkNotNull(componentName);
      this.userHandle = checkNotNull(userHandle);
    }

    public ComponentName getComponentName() {
      return componentName;
    }

    public UserHandle getUserHandle() {
      return userHandle;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      StartedMainActivity that = (StartedMainActivity) o;
      return Objects.equals(componentName, that.componentName)
              && Objects.equals(userHandle, that.userHandle);
    }

    @Override
    public int hashCode() {
      return Objects.hash(componentName, userHandle);
    }
  }

  /**
   * Container object to hold parameters passed to {@link #startMainActivity(ComponentName,
   * UserHandle)} or {@link #startActivity(ComponentName, UserHandle)}.
   */
  public static final class StartedActivity {

    private final ComponentName componentName;
    private final UserHandle userHandle;

    public StartedActivity(ComponentName componentName, UserHandle userHandle) {
      this.componentName = checkNotNull(componentName);
      this.userHandle = checkNotNull(userHandle);
    }

    public ComponentName getComponentName() {
      return componentName;
    }

    public UserHandle getUserHandle() {
      return userHandle;
    }

    @Override
    public boolean equals(Object o) {
      if (this == o) {
        return true;
      }
      if (o == null || getClass() != o.getClass()) {
        return false;
      }
      StartedActivity that = (StartedActivity) o;
      return Objects.equals(componentName, that.componentName)
              && Objects.equals(userHandle, that.userHandle);
    }

    @Override
    public int hashCode() {
      return Objects.hash(componentName, userHandle);
    }
  }
}
