content: /*
   * Copyright (C) 2016 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;
  
  import static android.app.ActivityManager.UID_OBSERVER_ACTIVE;
  import static android.app.ActivityManager.UID_OBSERVER_GONE;
  
  import android.annotation.IntDef;
  import android.annotation.Nullable;
  
  import android.app.ActivityManager;
  import android.app.ActivityManagerInternal;
  import android.app.IActivityManager;
  import android.app.IUidObserver;
  import android.content.BroadcastReceiver;
  import android.content.Context;
  import android.content.Intent;
  import android.content.IntentFilter;
  import android.content.pm.ActivityInfo;
  import android.content.pm.ApplicationInfo;
  import android.content.pm.PackageManager;
  import android.content.pm.ResolveInfo;
  import android.database.ContentObserver;
  import android.net.Uri;
  import android.os.Binder;
  import android.os.Build;
  import android.os.Handler;
  import android.os.Looper;
  import android.os.Message;
  import android.os.RemoteException;
  import android.os.SystemProperties;
  import android.os.UserHandle;
  import android.os.UserManager;
  import android.provider.MediaStore;
  import android.provider.Settings;
  import android.system.ErrnoException;
  import android.system.Os;
  import android.system.OsConstants;
  import android.util.ArrayMap;
  import android.util.ArraySet;
  import android.util.Slog;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.app.ResolverActivity;
  import com.android.internal.os.BackgroundThread;
  import com.android.internal.util.DumpUtils;
  import com.android.internal.util.function.pooled.PooledLambda;
  
  import com.android.server.wm.ActivityTaskManagerInternal;
  import dalvik.system.DexFile;
  import dalvik.system.VMRuntime;
  
  import java.io.FileDescriptor;
  import java.io.Closeable;
  import java.io.InputStream;
  import java.io.DataInputStream;
  import java.io.IOException;
  import java.io.PrintWriter;
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.List;
  import java.util.ArrayList;
  
  import java.util.zip.ZipFile;
  import java.util.zip.ZipEntry;
  
  /**
   * <p>PinnerService pins important files for key processes in memory.</p>
   * <p>Files to pin are specified in the config_defaultPinnerServiceFiles
   * overlay.</p>
   * <p>Pin the default camera application if specified in config_pinnerCameraApp.</p>
   */
  public final class PinnerService extends SystemService {
      private static final boolean DEBUG = false;
      private static final String TAG = "PinnerService";
  
      private static final String PIN_META_FILENAME = "pinlist.meta";
      private static final int PAGE_SIZE = (int) Os.sysconf(OsConstants._SC_PAGESIZE);
      private static final int MATCH_FLAGS = PackageManager.MATCH_DEFAULT_ONLY
              | PackageManager.MATCH_DIRECT_BOOT_AWARE
              | PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
  
      private static final int KEY_CAMERA = 0;
      private static final int KEY_HOME = 1;
  
      private static final int MAX_CAMERA_PIN_SIZE = 80 * (1 << 20); // 80MB max for camera app.
      private static final int MAX_HOME_PIN_SIZE = 6 * (1 << 20); // 6MB max for home app.
  
      @IntDef({KEY_CAMERA, KEY_HOME})
      @Retention(RetentionPolicy.SOURCE)
      public @interface AppKey {}
  
      private final Context mContext;
      private final ActivityTaskManagerInternal mAtmInternal;
      private final ActivityManagerInternal mAmInternal;
      private final IActivityManager mAm;
      private final UserManager mUserManager;
  
      /** The list of the statically pinned files. */
      @GuardedBy("this")
      private final ArrayList<PinnedFile> mPinnedFiles = new ArrayList<>();
  
      /** The list of the pinned apps. This is a map from {@link AppKey} to a pinned app. */
      @GuardedBy("this")
      private final ArrayMap<Integer, PinnedApp> mPinnedApps = new ArrayMap<>();
  
      /**
       * The list of the pinned apps that need to be repinned as soon as the all processes of a given
       * uid are no longer active. Note that with background dex opt, the new dex/vdex files are only
       * loaded into the processes once it restarts. So in case background dex opt recompiled these
       * files, we still need to keep the old ones pinned until the processes restart.
       * <p>
       * This is a map from uid to {@link AppKey}
       */
      @GuardedBy("this")
      private final ArrayMap<Integer, Integer> mPendingRepin = new ArrayMap<>();
  
      /**
       * A set of {@link AppKey} that are configured to be pinned.
       */
      private final ArraySet<Integer> mPinKeys = new ArraySet<>();
  
      private BinderService mBinderService;
      private PinnerHandler mPinnerHandler = null;
  
      private final BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
          @Override
          public void onReceive(Context context, Intent intent) {
            // If an app has updated, update pinned files accordingly.
            if (Intent.ACTION_PACKAGE_REPLACED.equals(intent.getAction())) {
                  Uri packageUri = intent.getData();
                  String packageName = packageUri.getSchemeSpecificPart();
                  ArraySet<String> updatedPackages = new ArraySet<>();
                  updatedPackages.add(packageName);
                  update(updatedPackages, true /* force */);
              }
          }
      };
  
      public PinnerService(Context context) {
          super(context);
  
          mContext = context;
          boolean shouldPinCamera = context.getResources().getBoolean(
                  com.android.internal.R.bool.config_pinnerCameraApp);
          boolean shouldPinHome = context.getResources().getBoolean(
                  com.android.internal.R.bool.config_pinnerHomeApp);
          if (shouldPinCamera) {
              mPinKeys.add(KEY_CAMERA);
          }
          if (shouldPinHome) {
              mPinKeys.add(KEY_HOME);
          }
          mPinnerHandler = new PinnerHandler(BackgroundThread.get().getLooper());
  
          mAtmInternal = LocalServices.getService(ActivityTaskManagerInternal.class);
          mAmInternal = LocalServices.getService(ActivityManagerInternal.class);
          mAm = ActivityManager.getService();
  
          mUserManager = mContext.getSystemService(UserManager.class);
  
          IntentFilter filter = new IntentFilter();
          filter.addAction(Intent.ACTION_PACKAGE_REPLACED);
          filter.addDataScheme("package");
          mContext.registerReceiver(mBroadcastReceiver, filter);
  
          registerUidListener();
          registerUserSetupCompleteListener();
      }
  
      @Override
      public void onStart() {
          if (DEBUG) {
              Slog.i(TAG, "Starting PinnerService");
          }
          mBinderService = new BinderService();
          publishBinderService("pinner", mBinderService);
          publishLocalService(PinnerService.class, this);
  
          mPinnerHandler.obtainMessage(PinnerHandler.PIN_ONSTART_MSG).sendToTarget();
          sendPinAppsMessage(UserHandle.USER_SYSTEM);
      }
  
      /**
       * Repin apps on user switch.
       * <p>
       * If more than one user is using the device each user may set a different preference for the
       * individual apps. Make sure that user's preference is pinned into memory.
       */
      @Override
      public void onSwitchUser(int userHandle) {
          if (!mUserManager.isManagedProfile(userHandle)) {
              sendPinAppsMessage(userHandle);
          }
      }
  
      @Override
      public void onUnlockUser(int userHandle) {
          if (!mUserManager.isManagedProfile(userHandle)) {
              sendPinAppsMessage(userHandle);
          }
      }
  
      /**
       * Update the currently pinned files.
       * Specifically, this only updates pinning for the apps that need to be pinned.
       * The other files pinned in onStart will not need to be updated.
       */
      public void update(ArraySet<String> updatedPackages, boolean force) {
          int currentUser = ActivityManager.getCurrentUser();
          for (int i = mPinKeys.size() - 1; i >= 0; i--) {
              int key = mPinKeys.valueAt(i);
              ApplicationInfo info = getInfoForKey(key, currentUser);
              if (info != null && updatedPackages.contains(info.packageName)) {
                  Slog.i(TAG, "Updating pinned files for " + info.packageName + " force=" + force);
                  sendPinAppMessage(key, currentUser, force);
              }
          }
      }
  
      /**
       * Handler for on start pinning message
       */
      private void handlePinOnStart() {
          final String bootImage = SystemProperties.get("dalvik.vm.boot-image", "");
          String[] filesToPin = null;
          if (bootImage.endsWith("apex.art")) {
              // Use the files listed for that specific boot image
              filesToPin = mContext.getResources().getStringArray(
                    com.android.internal.R.array.config_apexBootImagePinnerServiceFiles);
          } else {
              // Files to pin come from the overlay and can be specified per-device config
              filesToPin = mContext.getResources().getStringArray(
                    com.android.internal.R.array.config_defaultPinnerServiceFiles);
          }
          // Continue trying to pin each file even if we fail to pin some of them
          for (String fileToPin : filesToPin) {
              PinnedFile pf = pinFile(fileToPin,
                                      Integer.MAX_VALUE,
                                      /*attemptPinIntrospection=*/false);
              if (pf == null) {
                  Slog.e(TAG, "Failed to pin file = " + fileToPin);
                  continue;
              }
  
              synchronized (this) {
                  mPinnedFiles.add(pf);
              }
          }
      }
  
      /**
       * Registers a listener to repin the home app when user setup is complete, as the home intent
       * initially resolves to setup wizard, but once setup is complete, it will resolve to the
       * regular home app.
       */
      private void registerUserSetupCompleteListener() {
          Uri userSetupCompleteUri = Settings.Secure.getUriFor(
                  Settings.Secure.USER_SETUP_COMPLETE);
          mContext.getContentResolver().registerContentObserver(userSetupCompleteUri,
                  false, new ContentObserver(null) {
                      @Override
                      public void onChange(boolean selfChange, Uri uri) {
                          if (userSetupCompleteUri.equals(uri)) {
                              sendPinAppMessage(KEY_HOME, ActivityManager.getCurrentUser(),
                                      true /* force */);
                          }
                      }
                  }, UserHandle.USER_ALL);
      }
  
      private void registerUidListener() {
          try {
              mAm.registerUidObserver(new IUidObserver.Stub() {
                  @Override
                  public void onUidGone(int uid, boolean disabled) throws RemoteException {
                      mPinnerHandler.sendMessage(PooledLambda.obtainMessage(
                              PinnerService::handleUidGone, PinnerService.this, uid));
                  }
  
                  @Override
                  public void onUidActive(int uid) throws RemoteException {
                      mPinnerHandler.sendMessage(PooledLambda.obtainMessage(
                              PinnerService::handleUidActive, PinnerService.this, uid));
                  }
  
                  @Override
                  public void onUidIdle(int uid, boolean disabled) throws RemoteException {
                  }
  
                  @Override
                  public void onUidStateChanged(int uid, int procState, long procStateSeq)
                          throws RemoteException {
                  }
  
                  @Override
                  public void onUidCachedChanged(int uid, boolean cached) throws RemoteException {
                  }
              }, UID_OBSERVER_GONE | UID_OBSERVER_ACTIVE, 0, "system");
          } catch (RemoteException e) {
              Slog.e(TAG, "Failed to register uid observer", e);
          }
      }
  
      private void handleUidGone(int uid) {
          updateActiveState(uid, false /* active */);
          int key;
          synchronized (this) {
  
              // In case we have a pending repin, repin now. See mPendingRepin for more information.
              key = mPendingRepin.getOrDefault(uid, -1);
              if (key == -1) {
                  return;
              }
              mPendingRepin.remove(uid);
          }
          pinApp(key, ActivityManager.getCurrentUser(), false /* force */);
      }
  
      private void handleUidActive(int uid) {
          updateActiveState(uid, true /* active */);
      }
  
      private void updateActiveState(int uid, boolean active) {
          synchronized (this) {
              for (int i = mPinnedApps.size() - 1; i >= 0; i--) {
                  PinnedApp app = mPinnedApps.valueAt(i);
                  if (app.uid == uid) {
                      app.active = active;
                  }
              }
          }
      }
  
      private void unpinApp(@AppKey int key) {
          ArrayList<PinnedFile> pinnedAppFiles;
          synchronized (this) {
              PinnedApp app = mPinnedApps.get(key);
              if (app == null) {
                  return;
              }
              mPinnedApps.remove(key);
              pinnedAppFiles = new ArrayList<>(app.mFiles);
          }
          for (PinnedFile pinnedFile : pinnedAppFiles) {
              pinnedFile.close();
          }
      }
  
      private boolean isResolverActivity(ActivityInfo info) {
          return ResolverActivity.class.getName().equals(info.name);
      }
  
      private ApplicationInfo getCameraInfo(int userHandle) {
          Intent cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
          ApplicationInfo info = getApplicationInfoForIntent(cameraIntent, userHandle,
              false /* defaultToSystemApp */);
  
          // If the STILL_IMAGE_CAMERA intent doesn't resolve, try the _SECURE intent.
          // We don't use _SECURE first because it will never get set on a device
          // without File-based Encryption. But if the user has only set the intent
          // before unlocking their device, we may still be able to identify their
          // preference using this intent.
          if (info == null) {
              cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA_SECURE);
              info = getApplicationInfoForIntent(cameraIntent, userHandle,
                  false /* defaultToSystemApp */);
          }
  
          // If the _SECURE intent doesn't resolve, try the original intent but request
          // the system app for camera if there was more than one result.
          if (info == null) {
              cameraIntent = new Intent(MediaStore.INTENT_ACTION_STILL_IMAGE_CAMERA);
              info = getApplicationInfoForIntent(cameraIntent, userHandle,
                  true /* defaultToSystemApp */);
          }
          return info;
      }
  
      private ApplicationInfo getHomeInfo(int userHandle) {
          Intent intent = mAtmInternal.getHomeIntent();
          return getApplicationInfoForIntent(intent, userHandle, false);
      }
  
      private ApplicationInfo getApplicationInfoForIntent(Intent intent, int userHandle,
              boolean defaultToSystemApp) {
          if (intent == null) {
              return null;
          }
  
          ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivityAsUser(intent,
                  MATCH_FLAGS, userHandle);
  
          // If this intent can resolve to only one app, choose that one.
          // Otherwise, if we've requested to default to the system app, return it;
          // if we have not requested that default, return null if there's more than one option.
          // If there's more than one system app, return null since we don't know which to pick.
          if (resolveInfo == null) {
              return null;
          }
  
          if (!isResolverActivity(resolveInfo.activityInfo)) {
              return resolveInfo.activityInfo.applicationInfo;
          }
  
          if (defaultToSystemApp) {
              List<ResolveInfo> infoList = mContext.getPackageManager()
                  .queryIntentActivitiesAsUser(intent, MATCH_FLAGS, userHandle);
              ApplicationInfo systemAppInfo = null;
              for (ResolveInfo info : infoList) {
                  if ((info.activityInfo.applicationInfo.flags
                        & ApplicationInfo.FLAG_SYSTEM) != 0) {
                      if (systemAppInfo == null) {
                          systemAppInfo = info.activityInfo.applicationInfo;
                      } else {
                          // If there's more than one system app, return null due to ambiguity.
                          return null;
                      }
                  }
              }
              return systemAppInfo;
          }
  
          return null;
      }
  
      private void sendPinAppsMessage(int userHandle) {
          mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinApps, this,
                  userHandle));
      }
  
      private void pinApps(int userHandle) {
          for (int i = mPinKeys.size() - 1; i >= 0; i--) {
              int key = mPinKeys.valueAt(i);
              pinApp(key, userHandle, true /* force */);
          }
      }
  
      /**
       * @see #pinApp(int, int, boolean)
       */
      private void sendPinAppMessage(int key, int userHandle, boolean force) {
          mPinnerHandler.sendMessage(PooledLambda.obtainMessage(PinnerService::pinApp, this,
                  key, userHandle, force));
      }
  
      /**
       * Pins an app of a specific type {@code key}.
       *
       * @param force If false, this will not repin the app if it's currently active. See
       *              {@link #mPendingRepin}.
       */
      private void pinApp(int key, int userHandle, boolean force) {
          int uid = getUidForKey(key);
  
          // In case the app is currently active, don't repin until next process restart. See
          // mPendingRepin for more information.
          if (!force && uid != -1) {
              synchronized (this) {
                  mPendingRepin.put(uid, key);
              }
              return;
          }
          unpinApp(key);
          ApplicationInfo info = getInfoForKey(key, userHandle);
          if (info != null) {
              pinApp(key, info);
          }
      }
  
      /**
       * Checks whether the pinned package with {@code key} is active or not.
  
       * @return The uid of the pinned app, or {@code -1} otherwise.
       */
      private int getUidForKey(@AppKey int key) {
          synchronized (this) {
              PinnedApp existing = mPinnedApps.get(key);
              return existing != null && existing.active
                      ? existing.uid
                      : -1;
          }
      }
  
      /**
       * Retrieves the current application info for the given app type.
       *
       * @param key The app type to retrieve the info for.
       * @param userHandle The user id of the current user.
       */
      private @Nullable ApplicationInfo getInfoForKey(@AppKey int key, int userHandle) {
          switch (key) {
              case KEY_CAMERA:
                  return getCameraInfo(userHandle);
              case KEY_HOME:
                  return getHomeInfo(userHandle);
              default:
                  return null;
          }
      }
  
      /**
       * @return The app type name for {@code key}.
       */
      private String getNameForKey(@AppKey int key) {
          switch (key) {
              case KEY_CAMERA:
                  return "Camera";
              case KEY_HOME:
                  return "Home";
              default:
                  return null;
          }
      }
  
      /**
       * @return The maximum amount of bytes to be pinned for an app of type {@code key}.
       */
      private int getSizeLimitForKey(@AppKey int key) {
          switch (key) {
              case KEY_CAMERA:
                  return MAX_CAMERA_PIN_SIZE;
              case KEY_HOME:
                  return MAX_HOME_PIN_SIZE;
              default:
                  return 0;
          }
      }
  
      /**
       * Pins an application.
       *
       * @param key The key of the app to pin.
       * @param appInfo The corresponding app info.
       */
      private void pinApp(@AppKey int key, @Nullable ApplicationInfo appInfo) {
          if (appInfo == null) {
              return;
          }
  
          PinnedApp pinnedApp = new PinnedApp(appInfo);
          synchronized (this) {
              mPinnedApps.put(key, pinnedApp);
          }
  
          // pin APK
          int pinSizeLimit = getSizeLimitForKey(key);
          String apk = appInfo.sourceDir;
          PinnedFile pf = pinFile(apk, pinSizeLimit, /*attemptPinIntrospection=*/true);
          if (pf == null) {
              Slog.e(TAG, "Failed to pin " + apk);
              return;
          }
          if (DEBUG) {
              Slog.i(TAG, "Pinned " + pf.fileName);
          }
          synchronized (this) {
              pinnedApp.mFiles.add(pf);
          }
  
          // determine the ABI from either ApplicationInfo or Build
          String arch = "arm";
          if (appInfo.primaryCpuAbi != null) {
              if (VMRuntime.is64BitAbi(appInfo.primaryCpuAbi)) {
                  arch = arch + "64";
              }
          } else {
              if (VMRuntime.is64BitAbi(Build.SUPPORTED_ABIS[0])) {
                  arch = arch + "64";
              }
          }
  
          // get the path to the odex or oat file
          String baseCodePath = appInfo.getBaseCodePath();
          String[] files = null;
          try {
              files = DexFile.getDexFileOutputPaths(baseCodePath, arch);
          } catch (IOException ioe) {}
          if (files == null) {
              return;
          }
  
          //not pinning the oat/odex is not a fatal error
          for (String file : files) {
              pf = pinFile(file, pinSizeLimit, /*attemptPinIntrospection=*/false);
              if (pf != null) {
                  synchronized (this) {
                      pinnedApp.mFiles.add(pf);
                  }
                  if (DEBUG) {
                      Slog.i(TAG, "Pinned " + pf.fileName);
                  }
              }
          }
      }
  
      /** mlock length bytes of fileToPin in memory
       *
       * If attemptPinIntrospection is true, then treat the file to pin as a zip file and
       * look for a "pinlist.meta" file in the archive root directory. The structure of this
       * file is a PINLIST_META as described below:
       *
       * <pre>
       *   PINLIST_META: PIN_RANGE*
       *   PIN_RANGE: PIN_START PIN_LENGTH
       *   PIN_START: big endian i32: offset in bytes of pin region from file start
       *   PIN_LENGTH: big endian i32: length of pin region in bytes
       * </pre>
       *
       * (We use big endian because that's what DataInputStream is hardcoded to use.)
       *
       * If attemptPinIntrospection is false, then we use a single implicit PIN_RANGE of (0,
       * maxBytesToPin); that is, we attempt to pin the first maxBytesToPin bytes of the file.
       *
       * After we open a file, we march through the list of pin ranges and attempt to pin
       * each one, stopping after we've pinned maxBytesToPin bytes. (We may truncate the last
       * pinned range to fit.)  In this way, by choosing to emit certain PIN_RANGE pairs
       * before others, file generators can express pins in priority order, making most
       * effective use of the pinned-page quota.
       *
       * N.B. Each PIN_RANGE is clamped to the actual bounds of the file; all inputs have a
       * meaningful interpretation. Also, a range locking a single byte of a page locks the
       * whole page. Any truncated PIN_RANGE at EOF is ignored. Overlapping pinned entries
       * are legal, but each pin of a byte counts toward the pin quota regardless of whether
       * that byte has already been pinned, so the generator of PINLIST_META ought to ensure
       * that ranges are non-overlapping.
       *
       * @param fileToPin Path to file to pin
       * @param maxBytesToPin Maximum number of bytes to pin
       * @param attemptPinIntrospection If true, try to open file as a
       *   zip in order to extract the
       * @return Pinned memory resource owner thing or null on error
       */
      private static PinnedFile pinFile(String fileToPin,
                                        int maxBytesToPin,
                                        boolean attemptPinIntrospection) {
          ZipFile fileAsZip = null;
          InputStream pinRangeStream = null;
          try {
              if (attemptPinIntrospection) {
                  fileAsZip = maybeOpenZip(fileToPin);
              }
  
              if (fileAsZip != null) {
                  pinRangeStream = maybeOpenPinMetaInZip(fileAsZip, fileToPin);
              }
  
              Slog.d(TAG, "pinRangeStream: " + pinRangeStream);
  
              PinRangeSource pinRangeSource = (pinRangeStream != null)
                  ? new PinRangeSourceStream(pinRangeStream)
                  : new PinRangeSourceStatic(0, Integer.MAX_VALUE /* will be clipped */);
              return pinFileRanges(fileToPin, maxBytesToPin, pinRangeSource);
          } finally {
              safeClose(pinRangeStream);
              safeClose(fileAsZip);  // Also closes any streams we've opened
          }
      }
  
      /**
       * Attempt to open a file as a zip file. On any sort of corruption, log, swallow the
       * error, and return null.
       */
      private static ZipFile maybeOpenZip(String fileName) {
          ZipFile zip = null;
          try {
              zip = new ZipFile(fileName);
          } catch (IOException ex) {
              Slog.w(TAG,
                     String.format(
                         "could not open \"%s\" as zip: pinning as blob",
                                   fileName),
                     ex);
          }
          return zip;
      }
  
      /**
       * Open a pin metadata file in the zip if one is present.
       *
       * @param zipFile Zip file to search
       * @return Open input stream or null on any error
       */
      private static InputStream maybeOpenPinMetaInZip(ZipFile zipFile, String fileName) {
          ZipEntry pinMetaEntry = zipFile.getEntry(PIN_META_FILENAME);
          InputStream pinMetaStream = null;
          if (pinMetaEntry != null) {
              try {
                  pinMetaStream = zipFile.getInputStream(pinMetaEntry);
              } catch (IOException ex) {
                  Slog.w(TAG,
                         String.format("error reading pin metadata \"%s\": pinning as blob",
                                       fileName),
                         ex);
              }
          }
          return pinMetaStream;
      }
  
      private static abstract class PinRangeSource {
          /** Retrive a range to pin.
           *
           * @param outPinRange Receives the pin region
           * @return True if we filled in outPinRange or false if we're out of pin entries
           */
          abstract boolean read(PinRange outPinRange);
      }
  
      private static final class PinRangeSourceStatic extends PinRangeSource {
          private final int mPinStart;
          private final int mPinLength;
          private boolean mDone = false;
  
          PinRangeSourceStatic(int pinStart, int pinLength) {
              mPinStart = pinStart;
              mPinLength = pinLength;
          }
  
          @Override
          boolean read(PinRange outPinRange) {
              outPinRange.start = mPinStart;
              outPinRange.length = mPinLength;
              boolean done = mDone;
              mDone = true;
              return !done;
          }
      }
  
      private static final class PinRangeSourceStream extends PinRangeSource {
          private final DataInputStream mStream;
          private boolean mDone = false;
  
          PinRangeSourceStream(InputStream stream) {
              mStream = new DataInputStream(stream);
          }
  
          @Override
          boolean read(PinRange outPinRange) {
              if (!mDone) {
                  try {
                      outPinRange.start = mStream.readInt();
                      outPinRange.length = mStream.readInt();
                  } catch (IOException ex) {
                      mDone = true;
                  }
              }
              return !mDone;
          }
      }
  
      /**
       * Helper for pinFile.
       *
       * @param fileToPin Name of file to pin
       * @param maxBytesToPin Maximum number of bytes to pin
       * @param pinRangeSource Read PIN_RANGE entries from this stream to tell us what bytes
       *   to pin.
       * @return PinnedFile or null on error
       */
      private static PinnedFile pinFileRanges(
          String fileToPin,
          int maxBytesToPin,
          PinRangeSource pinRangeSource)
      {
          FileDescriptor fd = new FileDescriptor();
          long address = -1;
          int mapSize = 0;
  
          try {
              int openFlags = (OsConstants.O_RDONLY |
                               OsConstants.O_CLOEXEC |
                               OsConstants.O_NOFOLLOW);
              fd = Os.open(fileToPin, openFlags, 0);
              mapSize = (int) Math.min(Os.fstat(fd).st_size, Integer.MAX_VALUE);
              address = Os.mmap(0, mapSize,
                                OsConstants.PROT_READ,
                                OsConstants.MAP_SHARED,
                                fd, /*offset=*/0);
  
              PinRange pinRange = new PinRange();
              int bytesPinned = 0;
  
              // We pin at page granularity, so make sure the limit is page-aligned
              if (maxBytesToPin % PAGE_SIZE != 0) {
                  maxBytesToPin -= maxBytesToPin % PAGE_SIZE;
              }
  
              while (bytesPinned < maxBytesToPin && pinRangeSource.read(pinRange)) {
                  int pinStart = pinRange.start;
                  int pinLength = pinRange.length;
                  pinStart = clamp(0, pinStart, mapSize);
                  pinLength = clamp(0, pinLength, mapSize - pinStart);
                  pinLength = Math.min(maxBytesToPin - bytesPinned, pinLength);
  
                  // mlock doesn't require the region to be page-aligned, but we snap the
                  // lock region to page boundaries anyway so that we don't under-count
                  // locking a single byte of a page as a charge of one byte even though the
                  // OS will retain the whole page. Thanks to this adjustment, we slightly
                  // over-count the pin charge of back-to-back pins touching the same page,
                  // but better that than undercounting. Besides: nothing stops pin metafile
                  // creators from making the actual regions page-aligned.
                  pinLength += pinStart % PAGE_SIZE;
                  pinStart -= pinStart % PAGE_SIZE;
                  if (pinLength % PAGE_SIZE != 0) {
                      pinLength += PAGE_SIZE - pinLength % PAGE_SIZE;
                  }
                  pinLength = clamp(0, pinLength, maxBytesToPin - bytesPinned);
  
                  if (pinLength > 0) {
                      if (DEBUG) {
                          Slog.d(TAG,
                                 String.format(
                                     "pinning at %s %s bytes of %s",
                                     pinStart, pinLength, fileToPin));
                      }
                      Os.mlock(address + pinStart, pinLength);
                  }
                  bytesPinned += pinLength;
              }
  
              PinnedFile pinnedFile = new PinnedFile(address, mapSize, fileToPin, bytesPinned);
              address = -1;  // Ownership transferred
              return pinnedFile;
          } catch (ErrnoException ex) {
              Slog.e(TAG, "Could not pin file " + fileToPin, ex);
              return null;
          } finally {
              safeClose(fd);
              if (address >= 0) {
                  safeMunmap(address, mapSize);
              }
          }
      }
  
      private static int clamp(int min, int value, int max) {
          return Math.max(min, Math.min(value, max));
      }
  
      private static void safeMunmap(long address, long mapSize) {
          try {
              Os.munmap(address, mapSize);
          } catch (ErrnoException ex) {
              Slog.w(TAG, "ignoring error in unmap", ex);
          }
      }
  
      /**
       * Close FD, swallowing irrelevant errors.
       */
      private static void safeClose(@Nullable FileDescriptor fd) {
          if (fd != null && fd.valid()) {
              try {
                  Os.close(fd);
              } catch (ErrnoException ex) {
                  // Swallow the exception: non-EBADF errors in close(2)
                  // indicate deferred paging write errors, which we
                  // don't care about here. The underlying file
                  // descriptor is always closed.
                  if (ex.errno == OsConstants.EBADF) {
                      throw new AssertionError(ex);
                  }
              }
          }
      }
  
      /**
       * Close closeable thing, swallowing errors.
       */
      private static void safeClose(@Nullable Closeable thing) {
          if (thing != null) {
              try {
                  thing.close();
              } catch (IOException ex) {
                  Slog.w(TAG, "ignoring error closing resource: " + thing, ex);
              }
          }
      }
  
      private final class BinderService extends Binder {
          @Override
          protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
              if (!DumpUtils.checkDumpPermission(mContext, TAG, pw)) return;
              synchronized (PinnerService.this) {
                  long totalSize = 0;
                  for (PinnedFile pinnedFile : mPinnedFiles) {
                      pw.format("%s %s\n", pinnedFile.fileName, pinnedFile.bytesPinned);
                      totalSize += pinnedFile.bytesPinned;
                  }
                  pw.println();
                  for (int key : mPinnedApps.keySet()) {
                      PinnedApp app = mPinnedApps.get(key);
                      pw.print(getNameForKey(key));
                      pw.print(" uid="); pw.print(app.uid);
                      pw.print(" active="); pw.print(app.active);
                      pw.println();
                      for (PinnedFile pf : mPinnedApps.get(key).mFiles) {
                          pw.print("  "); pw.format("%s %s\n", pf.fileName, pf.bytesPinned);
                          totalSize += pf.bytesPinned;
                      }
                  }
                  pw.format("Total size: %s\n", totalSize);
                  pw.println();
                  if (!mPendingRepin.isEmpty()) {
                      pw.print("Pending repin: ");
                      for (int key : mPendingRepin.values()) {
                          pw.print(getNameForKey(key)); pw.print(' ');
                      }
                      pw.println();
                  }
              }
          }
      }
  
      private static final class PinnedFile implements AutoCloseable {
          private long mAddress;
          final int mapSize;
          final String fileName;
          final int bytesPinned;
  
          PinnedFile(long address, int mapSize, String fileName, int bytesPinned) {
               mAddress = address;
               this.mapSize = mapSize;
               this.fileName = fileName;
               this.bytesPinned = bytesPinned;
          }
  
          @Override
          public void close() {
              if (mAddress >= 0) {
                  safeMunmap(mAddress, mapSize);
                  mAddress = -1;
              }
          }
  
          @Override
          public void finalize() {
              close();
          }
      }
  
      final static class PinRange {
          int start;
          int length;
      }
  
      /**
       * Represents an app that was pinned.
       */
      private final class PinnedApp {
  
          /**
           * The uid of the package being pinned. This stays constant while the package stays
           * installed.
           */
          final int uid;
  
          /** Whether it is currently active, i.e. there is a running process from that package. */
          boolean active;
  
          /** List of pinned files. */
          final ArrayList<PinnedFile> mFiles = new ArrayList<>();
  
          private PinnedApp(ApplicationInfo appInfo) {
              uid = appInfo.uid;
              active = mAmInternal.isUidActive(uid);
          }
      }
  
      final class PinnerHandler extends Handler {
          static final int PIN_ONSTART_MSG = 4001;
  
          public PinnerHandler(Looper looper) {
              super(looper, null, true);
          }
  
          @Override
          public void handleMessage(Message msg) {
              switch (msg.what) {
                  case PIN_ONSTART_MSG:
                  {
                      handlePinOnStart();
                  }
                  break;
  
                  default:
                      super.handleMessage(msg);
              }
          }
      }
  
  }
  