content: /*
   * Copyright (C) 2019 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.policy;
  
  import static android.Manifest.permission.READ_EXTERNAL_STORAGE;
  import static android.Manifest.permission.WRITE_EXTERNAL_STORAGE;
  import static android.app.AppOpsManager.MODE_ALLOWED;
  import static android.app.AppOpsManager.MODE_DEFAULT;
  import static android.app.AppOpsManager.MODE_IGNORED;
  import static android.app.AppOpsManager.OP_LEGACY_STORAGE;
  import static android.app.AppOpsManager.OP_NONE;
  import static android.content.pm.PackageManager.FLAG_PERMISSION_APPLY_RESTRICTION;
  import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT;
  import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT;
  import static android.content.pm.PackageManager.FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT;
  
  import static java.lang.Integer.min;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.app.AppOpsManager;
  import android.content.Context;
  import android.content.pm.ApplicationInfo;
  import android.content.pm.PackageManager;
  import android.os.Build;
  import android.os.UserHandle;
  
  /**
   * The behavior of soft restricted permissions is different for each permission. This class collects
   * the policies in one place.
   *
   * This is the twin of
   * {@link com.android.packageinstaller.permission.utils.SoftRestrictedPermissionPolicy}
   */
  public abstract class SoftRestrictedPermissionPolicy {
      private static final int FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT =
              FLAG_PERMISSION_RESTRICTION_SYSTEM_EXEMPT
                      | FLAG_PERMISSION_RESTRICTION_UPGRADE_EXEMPT
                      | FLAG_PERMISSION_RESTRICTION_INSTALLER_EXEMPT;
  
      private static final SoftRestrictedPermissionPolicy DUMMY_POLICY =
              new SoftRestrictedPermissionPolicy() {
                  @Override
                  public int resolveAppOp() {
                      return OP_NONE;
                  }
  
                  @Override
                  public int getDesiredOpMode() {
                      return MODE_DEFAULT;
                  }
  
                  @Override
                  public boolean shouldSetAppOpIfNotDefault() {
                      return false;
                  }
  
                  @Override
                  public boolean canBeGranted() {
                      return true;
                  }
              };
  
      /**
       * TargetSDK is per package. To make sure two apps int the same shared UID do not fight over
       * what to set, always compute the combined targetSDK.
       *
       * @param context A context
       * @param appInfo The app that is changed
       * @param user The user the app belongs to
       *
       * @return The minimum targetSDK of all apps sharing the uid of the app
       */
      private static int getMinimumTargetSDK(@NonNull Context context,
              @NonNull ApplicationInfo appInfo, @NonNull UserHandle user) {
          PackageManager pm = context.getPackageManager();
  
          int minimumTargetSDK = appInfo.targetSdkVersion;
  
          String[] uidPkgs = pm.getPackagesForUid(appInfo.uid);
          if (uidPkgs != null) {
              for (String uidPkg : uidPkgs) {
                  if (!uidPkg.equals(appInfo.packageName)) {
                      ApplicationInfo uidPkgInfo;
                      try {
                          uidPkgInfo = pm.getApplicationInfoAsUser(uidPkg, 0, user);
                      } catch (PackageManager.NameNotFoundException e) {
                          continue;
                      }
  
                      minimumTargetSDK = min(minimumTargetSDK, uidPkgInfo.targetSdkVersion);
                  }
              }
          }
  
          return minimumTargetSDK;
      }
  
      /**
       * Get the policy for a soft restricted permission.
       *
       * @param context A context to use
       * @param appInfo The application the permission belongs to. Can be {@code null}, but then
       *                only {@link #resolveAppOp} will work.
       * @param user The user the app belongs to. Can be {@code null}, but then only
       *             {@link #resolveAppOp} will work.
       * @param permission The name of the permission
       *
       * @return The policy for this permission
       */
      public static @NonNull SoftRestrictedPermissionPolicy forPermission(@NonNull Context context,
              @Nullable ApplicationInfo appInfo, @Nullable UserHandle user,
              @NonNull String permission) {
          switch (permission) {
              // Storage uses a special app op to decide the mount state and supports soft restriction
              // where the restricted state allows the permission but only for accessing the medial
              // collections.
              case READ_EXTERNAL_STORAGE: {
                  final int flags;
                  final boolean applyRestriction;
                  final boolean isWhiteListed;
                  final boolean hasRequestedLegacyExternalStorage;
                  final int targetSDK;
  
                  if (appInfo != null) {
                      PackageManager pm = context.getPackageManager();
                      flags = pm.getPermissionFlags(permission, appInfo.packageName, user);
                      applyRestriction = (flags & FLAG_PERMISSION_APPLY_RESTRICTION) != 0;
                      isWhiteListed = (flags & FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) != 0;
                      targetSDK = getMinimumTargetSDK(context, appInfo, user);
  
                      boolean hasAnyRequestedLegacyExternalStorage =
                              appInfo.hasRequestedLegacyExternalStorage();
  
                      // hasRequestedLegacyExternalStorage is per package. To make sure two apps in
                      // the same shared UID do not fight over what to set, always compute the
                      // combined hasRequestedLegacyExternalStorage
                      String[] uidPkgs = pm.getPackagesForUid(appInfo.uid);
                      if (uidPkgs != null) {
                          for (String uidPkg : uidPkgs) {
                              if (!uidPkg.equals(appInfo.packageName)) {
                                  ApplicationInfo uidPkgInfo;
                                  try {
                                      uidPkgInfo = pm.getApplicationInfoAsUser(uidPkg, 0, user);
                                  } catch (PackageManager.NameNotFoundException e) {
                                      continue;
                                  }
  
                                  hasAnyRequestedLegacyExternalStorage |=
                                          uidPkgInfo.hasRequestedLegacyExternalStorage();
                              }
                          }
                      }
  
                      hasRequestedLegacyExternalStorage = hasAnyRequestedLegacyExternalStorage;
                  } else {
                      flags = 0;
                      applyRestriction = false;
                      isWhiteListed = false;
                      hasRequestedLegacyExternalStorage = false;
                      targetSDK = 0;
                  }
  
                  return new SoftRestrictedPermissionPolicy() {
                      @Override
                      public int resolveAppOp() {
                          return OP_LEGACY_STORAGE;
                      }
  
                      @Override
                      public int getDesiredOpMode() {
                          if (applyRestriction) {
                              return MODE_DEFAULT;
                          } else if (hasRequestedLegacyExternalStorage) {
                              return MODE_ALLOWED;
                          } else {
                              return MODE_IGNORED;
                          }
                      }
  
                      @Override
                      public boolean shouldSetAppOpIfNotDefault() {
                          // Do not switch from allowed -> ignored as this would mean to retroactively
                          // turn on isolated storage. This will make the app loose all its files.
                          return getDesiredOpMode() != MODE_IGNORED;
                      }
  
                      @Override
                      public boolean canBeGranted() {
                          if (isWhiteListed || targetSDK >= Build.VERSION_CODES.Q) {
                              return true;
                          } else {
                              return false;
                          }
                      }
                  };
              }
              case WRITE_EXTERNAL_STORAGE: {
                  final boolean isWhiteListed;
                  final int targetSDK;
  
                  if (appInfo != null) {
                      final int flags = context.getPackageManager().getPermissionFlags(permission,
                              appInfo.packageName, user);
                      isWhiteListed = (flags & FLAGS_PERMISSION_RESTRICTION_ANY_EXEMPT) != 0;
                      targetSDK = getMinimumTargetSDK(context, appInfo, user);
                  } else {
                      isWhiteListed = false;
                      targetSDK = 0;
                  }
  
                  return new SoftRestrictedPermissionPolicy() {
                      @Override
                      public int resolveAppOp() {
                          return OP_NONE;
                      }
  
                      @Override
                      public int getDesiredOpMode() {
                          return MODE_DEFAULT;
                      }
  
                      @Override
                      public boolean shouldSetAppOpIfNotDefault() {
                          return false;
                      }
  
                      @Override
                      public boolean canBeGranted() {
                          return isWhiteListed || targetSDK >= Build.VERSION_CODES.Q;
                      }
                  };
              }
              default:
                  return DUMMY_POLICY;
          }
      }
  
      /**
       * @return An app op to be changed based on the state of the permission or
       * {@link AppOpsManager#OP_NONE} if not app-op should be set.
       */
      public abstract int resolveAppOp();
  
      /**
       * @return The mode the {@link #resolveAppOp() app op} should be in.
       */
      public abstract @AppOpsManager.Mode int getDesiredOpMode();
  
      /**
       * @return If the {@link #resolveAppOp() app op} should be set even if the app-op is currently
       * not {@link AppOpsManager#MODE_DEFAULT}.
       */
      public abstract boolean shouldSetAppOpIfNotDefault();
  
      /**
       * @return If the permission can be granted
       */
      public abstract boolean canBeGranted();
  }
  