/*
 * Copyright (C) 2007 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.Manifest.permission.ACCESS_MTP;
import static android.Manifest.permission.INSTALL_PACKAGES;
import static android.Manifest.permission.MANAGE_EXTERNAL_STORAGE;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.OP_LEGACY_STORAGE;
import static android.app.AppOpsManager.OP_MANAGE_EXTERNAL_STORAGE;
import static android.app.AppOpsManager.OP_REQUEST_INSTALL_PACKAGES;
import static android.app.PendingIntent.FLAG_CANCEL_CURRENT;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_ONE_SHOT;
import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
import static android.content.pm.PackageManager.MATCH_ANY_USER;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_AWARE;
import static android.content.pm.PackageManager.MATCH_DIRECT_BOOT_UNAWARE;
import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
import static android.content.pm.PackageManager.PERMISSION_GRANTED;
import static android.mmd.flags.Flags.mmdEnabled;
import static android.os.IInstalld.IFsveritySetupAuthToken;
import static android.os.ParcelFileDescriptor.MODE_READ_WRITE;
import static android.os.storage.OnObbStateChangeListener.ERROR_ALREADY_MOUNTED;
import static android.os.storage.OnObbStateChangeListener.ERROR_COULD_NOT_MOUNT;
import static android.os.storage.OnObbStateChangeListener.ERROR_COULD_NOT_UNMOUNT;
import static android.os.storage.OnObbStateChangeListener.ERROR_NOT_MOUNTED;
import static android.os.storage.OnObbStateChangeListener.ERROR_PERMISSION_DENIED;
import static android.os.storage.OnObbStateChangeListener.MOUNTED;
import static android.os.storage.OnObbStateChangeListener.UNMOUNTED;

import static com.android.internal.util.XmlUtils.readStringAttribute;
import static com.android.internal.util.XmlUtils.writeStringAttribute;

import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT;
import static org.xmlpull.v1.XmlPullParser.START_TAG;

import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.ActivityOptions;
import android.app.AnrController;
import android.app.AppOpsManager;
import android.app.IActivityManager;
import android.app.KeyguardManager;
import android.app.PendingIntent;
import android.app.admin.SecurityLog;
import android.app.usage.StorageStatsManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.GosPackageState;
import android.content.pm.GosPackageStateFlag;
import android.content.pm.IPackageManager;
import android.content.pm.IPackageMoveObserver;
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.content.pm.ProviderInfo;
import android.content.pm.UserInfo;
import android.content.res.ObbInfo;
import android.database.ContentObserver;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.net.Uri;
import android.os.BatteryManager;
import android.os.Binder;
import android.os.Build;
import android.os.DropBoxManager;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.os.IStoraged;
import android.os.IVold;
import android.os.IVoldListener;
import android.os.IVoldMountCallback;
import android.os.IVoldTaskListener;
import android.os.Looper;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.os.ParcelableException;
import android.os.PermissionEnforcer;
import android.os.PersistableBundle;
import android.os.Process;
import android.os.RemoteCallbackList;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.ServiceSpecificException;
import android.os.SystemClock;
import android.os.SystemProperties;
import android.os.Trace;
import android.os.UserHandle;
import android.os.UserManager;
import android.os.storage.DiskInfo;
import android.os.storage.IObbActionListener;
import android.os.storage.IStorageEventListener;
import android.os.storage.IStorageManager;
import android.os.storage.IStorageShutdownObserver;
import android.os.storage.OnObbStateChangeListener;
import android.os.storage.StorageManager;
import android.os.storage.StorageManager.StorageFlags;
import android.os.storage.StorageVolume;
import android.os.storage.VolumeInfo;
import android.os.storage.VolumeRecord;
import android.provider.DeviceConfig;
import android.provider.DocumentsContract;
import android.provider.Downloads;
import android.provider.MediaStore;
import android.provider.Settings;
import android.service.storage.ExternalStorageService;
import android.sysprop.MmdProperties;
import android.text.TextUtils;
import android.text.format.DateUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.AtomicFile;
import android.util.DataUnit;
import android.util.EventLog;
import android.util.Log;
import android.util.Pair;
import android.util.Slog;
import android.util.SparseArray;
import android.util.SparseIntArray;
import android.util.TimeUtils;
import android.util.Xml;

import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.app.IAppOpsService;
import com.android.internal.content.PackageMonitor;
import com.android.internal.os.AppFuseMount;
import com.android.internal.os.BackgroundThread;
import com.android.internal.os.FuseUnavailableMountException;
import com.android.internal.os.SomeArgs;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
import com.android.server.memory.ZramMaintenance;
import com.android.server.pm.Installer;
import com.android.server.pm.UserManagerInternal;
import com.android.server.storage.AppFuseBridge;
import com.android.server.storage.ImmutableVolumeInfo;
import com.android.server.storage.StorageSessionController;
import com.android.server.storage.StorageSessionController.ExternalStorageServiceException;
import com.android.server.storage.WatchedVolumeInfo;
import com.android.server.utils.Watchable;
import com.android.server.utils.WatchedArrayMap;
import com.android.server.utils.Watcher;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.ActivityTaskManagerInternal.ScreenObserver;

import libcore.io.IoUtils;
import libcore.util.EmptyArray;

import org.xmlpull.v1.XmlPullParserException;

import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Service responsible for various storage media. Connects to {@code vold} to
 * watch for and manage dynamically added storage, such as SD cards and USB mass
 * storage. Also decides how storage should be presented to users on the device.
 */
class StorageManagerService extends IStorageManager.Stub
        implements Watchdog.Monitor, ScreenObserver {

    // Static direct instance pointer for the tightly-coupled idle service to use
    static StorageManagerService sSelf = null;

    /* Read during boot to decide whether to enable zram when available */
    private static final String ZRAM_ENABLED_PROPERTY =
            "persist.sys.zram_enabled";

    // A system property to control if obb app data isolation is enabled in vold.
    private static final String ANDROID_VOLD_APP_DATA_ISOLATION_ENABLED_PROPERTY =
            "persist.sys.vold_app_data_isolation_enabled";

    // How long we wait to reset storage, if we failed to call onMount on the
    // external storage service.
    public static final int FAILED_MOUNT_RESET_TIMEOUT_SECONDS = 10;

    /** Extended timeout for the system server watchdog. */
    private static final int SLOW_OPERATION_WATCHDOG_TIMEOUT_MS = 30 * 1000;

    /** Extended timeout for the system server watchdog for vold#partition operation. */
    private static final int PARTITION_OPERATION_WATCHDOG_TIMEOUT_MS = 3 * 60 * 1000;

    private static final Pattern OBB_FILE_PATH = Pattern.compile(
            "(?i)(^/storage/[^/]+/(?:([0-9]+)/)?Android/obb/)([^/]+)/([^/]+\\.obb)");

    @GuardedBy("mLock")
    private final Set<Integer> mFuseMountedUser = new ArraySet<>();

    @GuardedBy("mLock")
    private final Set<Integer> mCeStoragePreparedUsers = new ArraySet<>();

    private volatile long mInternalStorageSize = 0;

    public static class Lifecycle extends SystemService {
        private StorageManagerService mStorageManagerService;

        public Lifecycle(Context context) {
            super(context);
        }

        @Override
        public void onStart() {
            mStorageManagerService = new StorageManagerService(getContext());
            publishBinderService("mount", mStorageManagerService);
            mStorageManagerService.start();
        }

        @Override
        public void onBootPhase(int phase) {
            if (phase == SystemService.PHASE_SYSTEM_SERVICES_READY) {
                mStorageManagerService.servicesReady();
            } else if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
                mStorageManagerService.systemReady();
            } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
                mStorageManagerService.bootCompleted();
            }
        }

        @Override
        public void onUserSwitching(@Nullable TargetUser from, @NonNull TargetUser to) {
            int currentUserId = to.getUserIdentifier();
            mStorageManagerService.mCurrentUserId = currentUserId;

            UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
            if (umInternal.isUserUnlocked(currentUserId)) {
                Slog.d(TAG, "Attempt remount volumes for user: " + currentUserId);
                mStorageManagerService.maybeRemountVolumes(currentUserId);
                mStorageManagerService.mRemountCurrentUserVolumesOnUnlock = false;
            } else {
                Slog.d(TAG, "Attempt remount volumes for user: " + currentUserId + " on unlock");
                mStorageManagerService.mRemountCurrentUserVolumesOnUnlock = true;
            }
        }

        @Override
        public void onUserUnlocking(@NonNull TargetUser user) {
            mStorageManagerService.onUserUnlocking(user.getUserIdentifier());
        }

        @Override
        public void onUserStopped(@NonNull TargetUser user) {
            mStorageManagerService.onUserStopped(user.getUserIdentifier());
        }

        @Override
        public void onUserStopping(@NonNull TargetUser user) {
            mStorageManagerService.onUserStopping(user.getUserIdentifier());
        }

        @Override
        public void onUserStarting(TargetUser user) {
            mStorageManagerService.snapshotAndMonitorLegacyStorageAppOp(user.getUserHandle());
        }
    }

    private static final boolean DEBUG_OBB = false;

    /**
     * We now talk to vold over Binder, and it has its own internal lock to
     * serialize certain calls. All long-running operations have been migrated
     * to be async with callbacks, so we want watchdog to fire if vold wedges.
     */
    private static final boolean WATCHDOG_ENABLE = true;

    private static final String TAG = "StorageManagerService";
    private static final boolean LOCAL_LOGV = Log.isLoggable(TAG, Log.VERBOSE);

    private static final String TAG_STORAGE_BENCHMARK = "storage_benchmark";
    private static final String TAG_STORAGE_TRIM = "storage_trim";

    /** Magic value sent by MoveTask.cpp */
    private static final int MOVE_STATUS_COPY_FINISHED = 82;

    private static final int VERSION_INIT = 1;
    private static final int VERSION_ADD_PRIMARY = 2;
    private static final int VERSION_FIX_PRIMARY = 3;

    private static final String TAG_VOLUMES = "volumes";
    private static final String ATTR_VERSION = "version";
    private static final String ATTR_PRIMARY_STORAGE_UUID = "primaryStorageUuid";
    private static final String TAG_VOLUME = "volume";
    private static final String ATTR_TYPE = "type";
    private static final String ATTR_FS_UUID = "fsUuid";
    private static final String ATTR_PART_GUID = "partGuid";
    private static final String ATTR_NICKNAME = "nickname";
    private static final String ATTR_USER_FLAGS = "userFlags";
    private static final String ATTR_CREATED_MILLIS = "createdMillis";
    private static final String ATTR_LAST_SEEN_MILLIS = "lastSeenMillis";
    private static final String ATTR_LAST_TRIM_MILLIS = "lastTrimMillis";
    private static final String ATTR_LAST_BENCH_MILLIS = "lastBenchMillis";

    @Nullable public static String sMediaStoreAuthorityProcessName;

    // Smart idle maintenance running period in minute
    static volatile int sSmartIdleMaintPeriod = 60;

    private final AtomicFile mSettingsFile;
    private final AtomicFile mWriteRecordFile;

    // 72 hours (3 days)
    private static final int MAX_PERIOD_WRITE_RECORD = 72 * 60;
    private volatile int mMaxWriteRecords;

    /**
     * Default config values for smart idle maintenance
     * Actual values will be controlled by DeviceConfig
     */
    // Decide whether smart idle maintenance is enabled or not
    private static final boolean DEFAULT_SMART_IDLE_MAINT_ENABLED = false;
    // Run period in minute for smart idle maintenance
    private static final int DEFAULT_SMART_IDLE_MAINT_PERIOD = 60;
    private static final int MIN_SMART_IDLE_MAINT_PERIOD = 10;
    private static final int MAX_SMART_IDLE_MAINT_PERIOD = 24 * 60;
    // Storage lifetime percentage threshold to decide to turn off the feature
    private static final int DEFAULT_LIFETIME_PERCENT_THRESHOLD = 70;
    // Minimum required number of dirty + free segments to trigger GC
    private static final int DEFAULT_MIN_SEGMENTS_THRESHOLD = 512;
    // Determine how much portion of current dirty segments will be GCed
    private static final float DEFAULT_DIRTY_RECLAIM_RATE = 0.5F;
    // Multiplier to amplify the target segment number for GC
    private static final float DEFAULT_SEGMENT_RECLAIM_WEIGHT = 1.0F;
    // Low battery level threshold to decide to turn off the feature
    private static final float DEFAULT_LOW_BATTERY_LEVEL = 20F;
    // Decide whether charging is required to turn on the feature
    private static final boolean DEFAULT_CHARGING_REQUIRED = true;
    // Minimum GC interval sleep time in ms
    private static final int DEFAULT_MIN_GC_SLEEPTIME = 10000;
    // Target dirty segment ratio to aim to
    private static final int DEFAULT_TARGET_DIRTY_RATIO = 80;

    private volatile int mLifetimePercentThreshold;
    private volatile int mMinSegmentsThreshold;
    private volatile float mDirtyReclaimRate;
    private volatile float mSegmentReclaimWeight;
    private volatile float mLowBatteryLevel;
    private volatile boolean mChargingRequired;
    private volatile int mMinGCSleepTime;
    private volatile int mTargetDirtyRatio;
    private volatile boolean mNeedGC = true;

    private volatile boolean mPassedLifetimeThresh;
    // Tracking storage write amounts in one period
    private volatile int[] mStorageWriteRecords;

    /**
     * <em>Never</em> hold the lock while performing downcalls into vold, since
     * unsolicited events can suddenly appear to update data structures.
     */
    private final Object mLock = LockGuard.installNewLock(LockGuard.INDEX_STORAGE);

    /**
     * mCeUnlockedUsers affects the return value of {@link UserManager#isUserUnlocked}.  If any
     * value in the array changes, then the binder cache for {@link UserManager#isUserUnlocked} must
     * be invalidated.  When adding mutating methods to this class, be sure to invalidate the cache
     * in the new methods.
     *
     * Since we could report mounting state of already mounted emulated volume as unmounted
     * in getVolumeList method if it belongs to not unlocked users,
     * we also need to invalidate VolumeListCache when mutation happens to CE unlocked users array.
     */
    private static class WatchedUnlockedUsers {
        private int[] users = EmptyArray.INT;
        public WatchedUnlockedUsers() {
            invalidateIsUserUnlockedCache();
        }
        public void append(int userId) {
            users = ArrayUtils.appendInt(users, userId);
            invalidateIsUserUnlockedCache();
            StorageManager.invalidateVolumeListCache();
        }
        public void appendAll(int[] userIds) {
            for (int userId : userIds) {
                users = ArrayUtils.appendInt(users, userId);
            }
            invalidateIsUserUnlockedCache();
            StorageManager.invalidateVolumeListCache();
        }
        public void remove(int userId) {
            users = ArrayUtils.removeInt(users, userId);
            invalidateIsUserUnlockedCache();
            StorageManager.invalidateVolumeListCache();
        }
        public boolean contains(int userId) {
            return ArrayUtils.contains(users, userId);
        }
        public int[] all() {
            return users;
        }
        @Override
        public String toString() {
            return Arrays.toString(users);
        }
        private void invalidateIsUserUnlockedCache() {
            UserManager.invalidateIsUserUnlockedCache();
        }
    }

    /** Set of users whose CE storage is unlocked. */
    @GuardedBy("mLock")
    private WatchedUnlockedUsers mCeUnlockedUsers = new WatchedUnlockedUsers();

    /**
     * Set of users that are in the RUNNING_UNLOCKED state.  This differs from {@link
     * mCeUnlockedUsers} in that a user can be stopped but still have its CE storage unlocked.
     */
    @GuardedBy("mLock")
    private int[] mSystemUnlockedUsers = EmptyArray.INT;

    /** Map from disk ID to disk */
    @GuardedBy("mLock")
    private ArrayMap<String, DiskInfo> mDisks = new ArrayMap<>();
    /** Map from volume ID to disk */
    @GuardedBy("mLock")
    private final WatchedArrayMap<String, WatchedVolumeInfo> mVolumes = new WatchedArrayMap<>();

    /** Map from UUID to record */
    @GuardedBy("mLock")
    private ArrayMap<String, VolumeRecord> mRecords = new ArrayMap<>();
    @GuardedBy("mLock")
    private String mPrimaryStorageUuid;

    /** Map from disk ID to latches */
    @GuardedBy("mLock")
    private ArrayMap<String, CountDownLatch> mDiskScanLatches = new ArrayMap<>();

    @GuardedBy("mLock")
    private IPackageMoveObserver mMoveCallback;
    @GuardedBy("mLock")
    private String mMoveTargetUuid;

    @GuardedBy("mCloudMediaProviders")
    private final SparseArray<String> mCloudMediaProviders = new SparseArray<>();

    private volatile int mMediaStoreAuthorityAppId = -1;

    private volatile int mDownloadsAuthorityAppId = -1;

    private volatile int mExternalStorageAuthorityAppId = -1;

    private volatile int mCurrentUserId = UserHandle.USER_SYSTEM;

    private volatile boolean mRemountCurrentUserVolumesOnUnlock = false;

    private final Installer mInstaller;

    /** Holding lock for AppFuse business */
    private final Object mAppFuseLock = new Object();

    @GuardedBy("mAppFuseLock")
    private int mNextAppFuseName = 0;

    @GuardedBy("mAppFuseLock")
    private AppFuseBridge mAppFuseBridge = null;

    private final SparseIntArray mUserSharesMediaWith = new SparseIntArray();

    /** Matches known application dir paths. The first group contains the generic part of the path,
     * the second group contains the user id (or null if it's a public volume without users), the
     * third group contains the package name, and the fourth group the remainder of the path.
     */
    public static final Pattern KNOWN_APP_DIR_PATHS = Pattern.compile(
            "(?i)(^/storage/[^/]+/(?:([0-9]+)/)?Android/(?:data|media|obb|sandbox)/)([^/]+)(/.*)?");

    @VisibleForTesting
    protected WatchedVolumeInfo findVolumeByIdOrThrow(String id) {
        synchronized (mLock) {
            final WatchedVolumeInfo vol = mVolumes.get(id);
            if (vol != null) {
                return vol;
            }
        }
        throw new IllegalArgumentException("No volume found for ID " + id);
    }

    private VolumeRecord findRecordForPath(String path) {
        synchronized (mLock) {
            for (int i = 0; i < mVolumes.size(); i++) {
                final WatchedVolumeInfo vol = mVolumes.valueAt(i);
                if (vol.getFsPath() != null && path.startsWith(vol.getFsPath())) {
                    return mRecords.get(vol.getFsUuid());
                }
            }
        }
        return null;
    }

    private String scrubPath(String path) {
        if (path.startsWith(Environment.getDataDirectory().getAbsolutePath())) {
            return "internal";
        }
        final VolumeRecord rec = findRecordForPath(path);
        if (rec == null || rec.createdMillis == 0) {
            return "unknown";
        } else {
            return "ext:" + (int) ((System.currentTimeMillis() - rec.createdMillis)
                    / DateUtils.WEEK_IN_MILLIS) + "w";
        }
    }

    private @Nullable VolumeInfo findStorageForUuidAsUser(String volumeUuid,
            @UserIdInt int userId) {
        final StorageManager storage = mContext.getSystemService(StorageManager.class);
        if (Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, volumeUuid)) {
            return storage.findVolumeById(VolumeInfo.ID_EMULATED_INTERNAL + ";" + userId);
        } else if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, volumeUuid)) {
            return storage.getPrimaryPhysicalVolume();
        } else {
            VolumeInfo info = storage.findVolumeByUuid(volumeUuid);
            if (info == null) {
                Slog.w(TAG, "findStorageForUuidAsUser cannot find volumeUuid:" + volumeUuid);
                return null;
            }
            String emulatedUuid = info.getId().replace("private", "emulated") + ";" + userId;
            return storage.findVolumeById(emulatedUuid);
        }
    }

    private CountDownLatch findOrCreateDiskScanLatch(String diskId) {
        synchronized (mLock) {
            CountDownLatch latch = mDiskScanLatches.get(diskId);
            if (latch == null) {
                latch = new CountDownLatch(1);
                mDiskScanLatches.put(diskId, latch);
            }
            return latch;
        }
    }

    private final Context mContext;

    private volatile IVold mVold;
    private volatile IStoraged mStoraged;

    private volatile boolean mBootCompleted = false;
    private volatile boolean mDaemonConnected = false;
    private volatile boolean mSecureKeyguardShowing = true;

    private PackageManagerInternal mPmInternal;

    private IPackageManager mIPackageManager;
    private IAppOpsService mIAppOpsService;

    private final Callbacks mCallbacks;

    private static final String ANR_DELAY_MILLIS_DEVICE_CONFIG_KEY =
            "anr_delay_millis";

    private static final String ANR_DELAY_NOTIFY_EXTERNAL_STORAGE_SERVICE_DEVICE_CONFIG_KEY =
            "anr_delay_notify_external_storage_service";

    /**
     * Mounted OBB tracking information. Used to track the current state of all
     * OBBs.
     */
    final private Map<IBinder, List<ObbState>> mObbMounts = new HashMap<IBinder, List<ObbState>>();

    /** Map from raw paths to {@link ObbState}. */
    final private Map<String, ObbState> mObbPathToStateMap = new HashMap<String, ObbState>();

    // Not guarded by a lock.
    private final StorageManagerInternalImpl mStorageManagerInternal
            = new StorageManagerInternalImpl();

    // Not guarded by a lock.
    private final StorageSessionController mStorageSessionController;

    private final boolean mVoldAppDataIsolationEnabled;

    @GuardedBy("mLock")
    private final Set<Integer> mUidsWithLegacyExternalStorage = new ArraySet<>();
    // Not guarded by lock, always used on the ActivityManager thread
    private final SparseArray<PackageMonitor> mPackageMonitorsForUser = new SparseArray<>();

    class ObbState implements IBinder.DeathRecipient {
        public ObbState(String rawPath, String canonicalPath, int callingUid,
                IObbActionListener token, int nonce, String volId) {
            this.rawPath = rawPath;
            this.canonicalPath = canonicalPath;
            this.ownerGid = UserHandle.getSharedAppGid(callingUid);
            this.token = token;
            this.nonce = nonce;
            this.volId = volId;
        }

        final String rawPath;
        final String canonicalPath;

        final int ownerGid;

        // Token of remote Binder caller
        final IObbActionListener token;

        // Identifier to pass back to the token
        final int nonce;

        String volId;

        public IBinder getBinder() {
            return token.asBinder();
        }

        @Override
        public void binderDied() {
            ObbAction action = new UnmountObbAction(this, true);
            mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage(OBB_RUN_ACTION, action));
        }

        public void link() throws RemoteException {
            getBinder().linkToDeath(this, 0);
        }

        public void unlink() {
            getBinder().unlinkToDeath(this, 0);
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder("ObbState{");
            sb.append("rawPath=").append(rawPath);
            sb.append(",canonicalPath=").append(canonicalPath);
            sb.append(",ownerGid=").append(ownerGid);
            sb.append(",token=").append(token);
            sb.append(",binder=").append(getBinder());
            sb.append(",volId=").append(volId);
            sb.append('}');
            return sb.toString();
        }
    }

    // OBB Action Handler
    final private ObbActionHandler mObbActionHandler;

    // OBB action handler messages
    private static final int OBB_RUN_ACTION = 1;
    private static final int OBB_FLUSH_MOUNT_STATE = 2;

    // Last fstrim operation tracking
    private static final String LAST_FSTRIM_FILE = "last-fstrim";
    private final File mLastMaintenanceFile;
    private long mLastMaintenance;

    // Handler messages
    private static final int H_SYSTEM_READY = 1;
    private static final int H_DAEMON_CONNECTED = 2;
    private static final int H_SHUTDOWN = 3;
    private static final int H_FSTRIM = 4;
    private static final int H_VOLUME_MOUNT = 5;
    private static final int H_VOLUME_BROADCAST = 6;
    private static final int H_INTERNAL_BROADCAST = 7;
    private static final int H_VOLUME_UNMOUNT = 8;
    private static final int H_PARTITION_FORGET = 9;
    private static final int H_RESET = 10;
    private static final int H_RUN_IDLE_MAINT = 11;
    private static final int H_ABORT_IDLE_MAINT = 12;
    private static final int H_BOOT_COMPLETED = 13;
    private static final int H_COMPLETE_UNLOCK_USER = 14;
    private static final int H_VOLUME_STATE_CHANGED = 15;
    private static final int H_CLOUD_MEDIA_PROVIDER_CHANGED = 16;
    private static final int H_SECURE_KEYGUARD_STATE_CHANGED = 17;
    private static final int H_REMOUNT_VOLUMES_ON_MOVE = 18;

    class StorageManagerServiceHandler extends Handler {
        public StorageManagerServiceHandler(Looper looper) {
            super(looper);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case H_SYSTEM_READY: {
                    handleSystemReady();
                    break;
                }
                case H_BOOT_COMPLETED: {
                    handleBootCompleted();
                    break;
                }
                case H_DAEMON_CONNECTED: {
                    handleDaemonConnected();
                    break;
                }
                case H_FSTRIM: {
                    Slog.i(TAG, "Running fstrim idle maintenance");

                    // Remember when we kicked it off
                    try {
                        mLastMaintenance = System.currentTimeMillis();
                        mLastMaintenanceFile.setLastModified(mLastMaintenance);
                    } catch (Exception e) {
                        Slog.e(TAG, "Unable to record last fstrim!");
                    }

                    // TODO: Reintroduce shouldBenchmark() test
                    fstrim(0, null);

                    // invoke the completion callback, if any
                    // TODO: fstrim is non-blocking, so remove this useless callback
                    Runnable callback = (Runnable) msg.obj;
                    if (callback != null) {
                        callback.run();
                    }
                    break;
                }
                case H_SHUTDOWN: {
                    final IStorageShutdownObserver obs = (IStorageShutdownObserver) msg.obj;
                    boolean success = false;
                    try {
                        mVold.shutdown();
                        success = true;
                    } catch (Exception e) {
                        Slog.wtf(TAG, e);
                    }
                    if (obs != null) {
                        try {
                            obs.onShutDownComplete(success ? 0 : -1);
                        } catch (Exception ignored) {
                        }
                    }
                    break;
                }
                case H_VOLUME_MOUNT: {
                    final WatchedVolumeInfo vol = (WatchedVolumeInfo) msg.obj;
                    if (isMountDisallowed(vol)) {
                        Slog.i(TAG, "Ignoring mount " + vol.getId() + " due to policy");
                        break;
                    }

                    mount(vol);
                    break;
                }
                case H_VOLUME_UNMOUNT: {
                    final ImmutableVolumeInfo vol = (ImmutableVolumeInfo) msg.obj;
                    unmount(vol);
                    break;
                }
                case H_VOLUME_BROADCAST: {
                    final StorageVolume userVol = (StorageVolume) msg.obj;
                    final String envState = userVol.getState();
                    Slog.d(TAG, "Volume " + userVol.getId() + " broadcasting " + envState + " to "
                            + userVol.getOwner());

                    final String action = VolumeInfo.getBroadcastForEnvironment(envState);
                    if (action != null) {
                        final Intent intent = new Intent(action,
                                Uri.fromFile(userVol.getPathFile()));
                        intent.putExtra(StorageVolume.EXTRA_STORAGE_VOLUME, userVol);
                        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
                                | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
                        mContext.sendBroadcastAsUser(intent, userVol.getOwner());
                    }
                    break;
                }
                case H_INTERNAL_BROADCAST: {
                    // Internal broadcasts aimed at system components, not for
                    // third-party apps.
                    final Intent intent = (Intent) msg.obj;
                    mContext.sendBroadcastAsUser(intent, UserHandle.ALL,
                            android.Manifest.permission.WRITE_MEDIA_STORAGE);
                    break;
                }
                case H_PARTITION_FORGET: {
                    final VolumeRecord rec = (VolumeRecord) msg.obj;
                    forgetPartition(rec.partGuid, rec.fsUuid);
                    break;
                }
                case H_RESET: {
                    resetIfBootedAndConnected();
                    break;
                }
                case H_RUN_IDLE_MAINT: {
                    Slog.i(TAG, "Running idle maintenance");
                    runIdleMaint((Runnable)msg.obj);
                    break;
                }
                case H_ABORT_IDLE_MAINT: {
                    Slog.i(TAG, "Aborting idle maintenance");
                    abortIdleMaint((Runnable)msg.obj);
                    break;
                }
                case H_COMPLETE_UNLOCK_USER: {
                    completeUnlockUser(msg.arg1);
                    break;
                }
                case H_VOLUME_STATE_CHANGED: {
                    final SomeArgs args = (SomeArgs) msg.obj;
                    onVolumeStateChangedAsync((WatchedVolumeInfo) args.arg1, args.argi1,
                            args.argi2);
                    args.recycle();
                    break;
                }
                case H_CLOUD_MEDIA_PROVIDER_CHANGED: {
                    // We send this message in two cases:
                    // 1. After the cloud provider has been set/updated for a user.
                    //    In this case Message's #arg1 is set to UserId, and #obj is set to the
                    //    authority of the new cloud provider.
                    // 2. After a new CloudProviderChangeListener is registered.
                    //    In this case Message's #obj is set to the CloudProviderChangeListener.
                    if (msg.obj instanceof StorageManagerInternal.CloudProviderChangeListener) {
                        final StorageManagerInternal.CloudProviderChangeListener listener =
                                (StorageManagerInternal.CloudProviderChangeListener) msg.obj;
                        notifyCloudMediaProviderChangedAsync(listener);
                    } else {
                        final int userId = msg.arg1;
                        final String authority = (String) msg.obj;
                        onCloudMediaProviderChangedAsync(userId, authority);
                    }
                    break;
                }
                case H_SECURE_KEYGUARD_STATE_CHANGED: {
                    try {
                        mVold.onSecureKeyguardStateChanged((boolean) msg.obj);
                    } catch (Exception e) {
                        Slog.wtf(TAG, e);
                    }
                    break;
                }
                case H_REMOUNT_VOLUMES_ON_MOVE: {
                    remountVolumesForRunningUsersOnMove();
                    break;
                }
            }
        }
    }

    private final Handler mHandler;

    private BroadcastReceiver mUserReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            final String action = intent.getAction();
            final int userId = intent.getIntExtra(Intent.EXTRA_USER_HANDLE, -1);
            Preconditions.checkArgument(userId >= 0);

            try {
                if (Intent.ACTION_USER_ADDED.equals(action)) {
                    final UserManager um = mContext.getSystemService(UserManager.class);
                    final int userSerialNumber = um.getUserSerialNumber(userId);
                    final UserInfo userInfo = um.getUserInfo(userId);
                    if (userInfo.isCloneProfile()) {
                        // Only clone profiles share storage with their parent
                        mVold.onUserAdded(userId, userSerialNumber,
                                userInfo.profileGroupId /* sharesStorageWithUserId */);
                    } else {
                        mVold.onUserAdded(userId, userSerialNumber,
                                -1 /* shareStorageWithUserId */);
                    }
                } else if (Intent.ACTION_USER_REMOVED.equals(action)) {
                    synchronized (mLock) {
                        final int size = mVolumes.size();
                        for (int i = 0; i < size; i++) {
                            final WatchedVolumeInfo vol = mVolumes.valueAt(i);
                            if (vol.getMountUserId() == userId) {
                                // Capture the volume before we set mount user id to null,
                                // so that StorageSessionController remove the session from
                                // the correct user (old mount user id)
                                final ImmutableVolumeInfo volToUnmount
                                        = vol.getClonedImmutableVolumeInfo();
                                vol.setMountUserId(UserHandle.USER_NULL);
                                mHandler.obtainMessage(H_VOLUME_UNMOUNT, volToUnmount)
                                        .sendToTarget();
                            }
                        }
                    }
                    mVold.onUserRemoved(userId);
                }
            } catch (Exception e) {
                Slog.wtf(TAG, e);
            }
        }
    };

    private void waitForLatch(CountDownLatch latch, String condition, long timeoutMillis)
            throws TimeoutException {
        final long startMillis = SystemClock.elapsedRealtime();
        while (true) {
            try {
                if (latch.await(5000, TimeUnit.MILLISECONDS)) {
                    return;
                } else {
                    Slog.w(TAG, "Thread " + Thread.currentThread().getName()
                            + " still waiting for " + condition + "...");
                }
            } catch (InterruptedException e) {
                Slog.w(TAG, "Interrupt while waiting for " + condition);
            }
            if (timeoutMillis > 0 && SystemClock.elapsedRealtime() > startMillis + timeoutMillis) {
                throw new TimeoutException("Thread " + Thread.currentThread().getName()
                        + " gave up waiting for " + condition + " after " + timeoutMillis + "ms");
            }
        }
    }

    private void handleSystemReady() {
        if (prepareSmartIdleMaint()) {
            SmartStorageMaintIdler.scheduleSmartIdlePass(mContext, sSmartIdleMaintPeriod);
        }

        // Start scheduling nominally-daily fstrim operations
        MountServiceIdler.scheduleIdlePass(mContext);

        if (mmdEnabled() && MmdProperties.mmd_zram_enabled().orElse(false)) {
            ZramMaintenance.startZramMaintenance(mContext);
        } else {
            // Toggle zram-enable system property in response to settings
            mContext.getContentResolver().registerContentObserver(
                    Settings.Global.getUriFor(Settings.Global.ZRAM_ENABLED),
                    false /*notifyForDescendants*/,
                    new ContentObserver(null /* current thread */) {
                        @Override
                        public void onChange(boolean selfChange) {
                            refreshZramSettings();
                        }
                    });
            refreshZramSettings();

            // Schedule zram writeback unless zram is disabled by persist.sys.zram_enabled
            String zramPropValue = SystemProperties.get(ZRAM_ENABLED_PROPERTY);
            if (!zramPropValue.equals("0")
                    && mContext.getResources().getBoolean(
                    com.android.internal.R.bool.config_zramWriteback)) {
                ZramWriteback.scheduleZramWriteback(mContext);
            }
        }

        configureTranscoding();
    }

    /**
     * Update the zram_enabled system property (which init reads to
     * decide whether to enable zram) to reflect the zram_enabled
     * preference (which we can change for experimentation purposes).
     */
    private void refreshZramSettings() {
        String propertyValue = SystemProperties.get(ZRAM_ENABLED_PROPERTY);
        if ("".equals(propertyValue)) {
            return;  // System doesn't have zram toggling support
        }
        String desiredPropertyValue =
            Settings.Global.getInt(mContext.getContentResolver(),
                                   Settings.Global.ZRAM_ENABLED,
                                   1) != 0
            ? "1" : "0";
        if (!desiredPropertyValue.equals(propertyValue)) {
            // Avoid redundant disk writes by setting only if we're
            // changing the property value. There's no race: we're the
            // sole writer.
            SystemProperties.set(ZRAM_ENABLED_PROPERTY, desiredPropertyValue);
            // Schedule writeback only if zram is being enabled.
            if (desiredPropertyValue.equals("1")
                    && mContext.getResources().getBoolean(
                        com.android.internal.R.bool.config_zramWriteback)) {
                ZramWriteback.scheduleZramWriteback(mContext);
            }
        }
    }

    private boolean isHevcDecoderSupported() {
        MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
        MediaCodecInfo[] codecInfos = codecList.getCodecInfos();
        for (MediaCodecInfo codecInfo : codecInfos) {
            if (codecInfo.isEncoder()) {
                continue;
            }
            String[] supportedTypes = codecInfo.getSupportedTypes();
            for (String type : supportedTypes) {
                if (type.equalsIgnoreCase(MediaFormat.MIMETYPE_VIDEO_HEVC)) {
                    return true;
                }
            }
        }
        return false;
    }

    private void configureTranscoding() {
        // See MediaProvider TranscodeHelper#getBooleanProperty for more information
        boolean transcodeEnabled = false;
        boolean defaultValue = isHevcDecoderSupported() ? true : false;

        if (SystemProperties.getBoolean("persist.sys.fuse.transcode_user_control", false)) {
            transcodeEnabled = SystemProperties.getBoolean("persist.sys.fuse.transcode_enabled",
                    defaultValue);
        } else {
            transcodeEnabled = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
                    "transcode_enabled", defaultValue);
        }
        SystemProperties.set("sys.fuse.transcode_enabled", String.valueOf(transcodeEnabled));

        if (transcodeEnabled) {
            LocalServices.getService(ActivityManagerInternal.class)
                .registerAnrController(new ExternalStorageServiceAnrController());
        }
    }

    private class ExternalStorageServiceAnrController implements AnrController {
        @Override
        public long getAnrDelayMillis(String packageName, int uid) {
            if (!isAppIoBlocked(uid)) {
                return 0;
            }

            int delay = DeviceConfig.getInt(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
                    ANR_DELAY_MILLIS_DEVICE_CONFIG_KEY, 5000);
            Slog.v(TAG, "getAnrDelayMillis for " + packageName + ". " + delay + "ms");
            return delay;
        }

        @Override
        public void onAnrDelayStarted(String packageName, int uid) {
            if (!isAppIoBlocked(uid)) {
                return;
            }

            boolean notifyExternalStorageService = DeviceConfig.getBoolean(
                    DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
                    ANR_DELAY_NOTIFY_EXTERNAL_STORAGE_SERVICE_DEVICE_CONFIG_KEY, true);
            if (notifyExternalStorageService) {
                Slog.d(TAG, "onAnrDelayStarted for " + packageName
                        + ". Notifying external storage service");
                try {
                    mStorageSessionController.notifyAnrDelayStarted(packageName, uid, 0 /* tid */,
                            StorageManager.APP_IO_BLOCKED_REASON_TRANSCODING);
                } catch (ExternalStorageServiceException e) {
                    Slog.e(TAG, "Failed to notify ANR delay started for " + packageName, e);
                }
            } else {
                // TODO(b/170973510): Implement framework spinning dialog for ANR delay
            }
        }

        @Override
        public boolean onAnrDelayCompleted(String packageName, int uid) {
            if (isAppIoBlocked(uid)) {
                Slog.d(TAG, "onAnrDelayCompleted for " + packageName + ". Showing ANR dialog...");
                return true;
            } else {
                Slog.d(TAG, "onAnrDelayCompleted for " + packageName + ". Skipping ANR dialog...");
                return false;
            }
        }
    }

    @GuardedBy("mLock")
    private void addInternalVolumeLocked() {
        // Create a stub volume that represents internal storage
        final VolumeInfo internal = new VolumeInfo(VolumeInfo.ID_PRIVATE_INTERNAL,
                VolumeInfo.TYPE_PRIVATE, null, null);
        internal.state = VolumeInfo.STATE_MOUNTED;
        internal.path = Environment.getDataDirectory().getAbsolutePath();
        mVolumes.put(internal.id, WatchedVolumeInfo.fromVolumeInfo(internal));
    }

    private void resetIfBootedAndConnected() {
        Slog.d(TAG, "Thinking about reset, mBootCompleted=" + mBootCompleted
                + ", mDaemonConnected=" + mDaemonConnected);
        if (mBootCompleted && mDaemonConnected) {
            final UserManager userManager = mContext.getSystemService(UserManager.class);
            final List<UserInfo> users = userManager.getUsers();

            extendWatchdogTimeout("#onReset might be slow");
            mStorageSessionController.onReset(mVold, () -> {
                mHandler.removeCallbacksAndMessages(null);
            });

            final int[] systemUnlockedUsers;
            synchronized (mLock) {
                // make copy as sorting can change order
                systemUnlockedUsers = Arrays.copyOf(mSystemUnlockedUsers,
                        mSystemUnlockedUsers.length);

                mDisks.clear();
                mVolumes.clear();

                addInternalVolumeLocked();
            }

            try {
                // Reset vold to tear down existing disks/volumes and start from
                // a clean state.  Exception: already-unlocked user storage will
                // remain unlocked and is not affected by the reset.
                //
                // TODO(b/135341433): Remove cautious logging when FUSE is stable
                Slog.i(TAG, "Resetting vold...");
                mVold.reset();
                Slog.i(TAG, "Reset vold");

                // Tell vold about all existing and started users
                for (UserInfo user : users) {
                    if (user.isCloneProfile()) {
                        mVold.onUserAdded(user.id, user.serialNumber, user.profileGroupId);
                    } else {
                        mVold.onUserAdded(user.id, user.serialNumber, -1);
                    }
                }
                for (int userId : systemUnlockedUsers) {
                    mVold.onUserStarted(userId);
                    mStoraged.onUserStarted(userId);
                }
                restoreSystemUnlockedUsers(userManager, users, systemUnlockedUsers);
                mVold.onSecureKeyguardStateChanged(mSecureKeyguardShowing);
                mStorageManagerInternal.onReset(mVold);
            } catch (Exception e) {
                Slog.wtf(TAG, e);
            }
        }
    }

    private void restoreSystemUnlockedUsers(UserManager userManager, List<UserInfo> allUsers,
            int[] systemUnlockedUsers) throws Exception {
        Arrays.sort(systemUnlockedUsers);
        UserManager.invalidateIsUserUnlockedCache();
        for (UserInfo user : allUsers) {
            int userId = user.id;
            if (!userManager.isUserRunning(userId)) {
                continue;
            }
            if (Arrays.binarySearch(systemUnlockedUsers, userId) >= 0) {
                continue;
            }
            boolean unlockingOrUnlocked = userManager.isUserUnlockingOrUnlocked(userId);
            if (!unlockingOrUnlocked) {
                continue;
            }
            Slog.w(TAG, "UNLOCK_USER lost from vold reset, will retry, user:" + userId);
            mVold.onUserStarted(userId);
            mStoraged.onUserStarted(userId);
            mHandler.obtainMessage(H_COMPLETE_UNLOCK_USER, userId, /* arg2 (unusued) */ 0)
                    .sendToTarget();
        }
    }

    // If vold knows that some users have their CE storage unlocked already (which can happen after
    // a "userspace reboot"), then add those users to mCeUnlockedUsers.  Do this right away and
    // don't wait until PHASE_BOOT_COMPLETED, since the system may unlock users before then.
    private void restoreCeUnlockedUsers() {
        final int[] userIds;
        try {
            userIds = mVold.getUnlockedUsers();
        } catch (Exception e) {
            Slog.e(TAG, "Failed to get unlocked users from vold", e);
            return;
        }
        if (!ArrayUtils.isEmpty(userIds)) {
            Slog.d(TAG, "CE storage for users " + Arrays.toString(userIds)
                    + " is already unlocked");
            synchronized (mLock) {
                // Append rather than replace, just in case we're actually
                // reconnecting to vold after it crashed and was restarted, in
                // which case things will be the other way around --- we'll know
                // about the unlocked users but vold won't.
                mCeUnlockedUsers.appendAll(userIds);
            }
        }
    }

    private void onUserUnlocking(int userId) {
        Slog.d(TAG, "onUserUnlocking " + userId);
        Trace.instant(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.onUserUnlocking: " + userId);

        if (userId != UserHandle.USER_SYSTEM) {
            // Check if this user shares media with another user
            try {
                Context userContext = mContext.createPackageContextAsUser("system", 0,
                        UserHandle.of(userId));
                UserManager um = userContext.getSystemService(UserManager.class);
                if (um != null && um.isMediaSharedWithParent()) {
                    int parentUserId = um.getProfileParent(userId).id;
                    mUserSharesMediaWith.put(userId, parentUserId);
                    mUserSharesMediaWith.put(parentUserId, userId);
                }
            } catch (PackageManager.NameNotFoundException e) {
                Log.e(TAG, "Failed to create user context for user " + userId);
            }
        }
        // We purposefully block here to make sure that user-specific
        // staging area is ready so it's ready for zygote-forked apps to
        // bind mount against.
        try {
            mStorageSessionController.onUnlockUser(userId);
            mVold.onUserStarted(userId);
            mStoraged.onUserStarted(userId);
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }

        mHandler.obtainMessage(H_COMPLETE_UNLOCK_USER, userId, /* arg2 (unusued) */ 0)
                .sendToTarget();
        if (mRemountCurrentUserVolumesOnUnlock && userId == mCurrentUserId) {
            maybeRemountVolumes(userId);
            mRemountCurrentUserVolumesOnUnlock = false;
        }
    }

    private void completeUnlockUser(int userId) {
        onKeyguardStateChanged(false);

        // Record user as started so newly mounted volumes kick off events
        // correctly, then synthesize events for any already-mounted volumes.
        synchronized (mLock) {
            for (int unlockedUser : mSystemUnlockedUsers) {
                if (unlockedUser == userId) {
                    // This can happen as restoreAllUnlockedUsers can double post the message.
                    Log.i(TAG, "completeUnlockUser called for already unlocked user:" + userId);
                    return;
                }
            }
            for (int i = 0; i < mVolumes.size(); i++) {
                final WatchedVolumeInfo vol = mVolumes.valueAt(i);
                if (vol.isVisibleForUser(userId) && vol.isMountedReadable()) {
                    final StorageVolume userVol = vol.buildStorageVolume(mContext, userId, false);
                    mHandler.obtainMessage(H_VOLUME_BROADCAST, userVol).sendToTarget();

                    final String envState = VolumeInfo.getEnvironmentForState(vol.getState());
                    mCallbacks.notifyStorageStateChanged(userVol.getPath(), envState, envState);
                }
            }
            mSystemUnlockedUsers = ArrayUtils.appendInt(mSystemUnlockedUsers, userId);
        }
        // Invalidate the StorageManager cache to ensure that
        // getVolumeList function returns the latest volumes.
        // This is needed as we intentionally report the volume as unmounted in getVolumeList,
        // if the user is not unlocked, even though it might have been mounted already.
        StorageManager.invalidateVolumeListCache();
    }

    private void extendWatchdogTimeout(String reason) {
        Watchdog w = Watchdog.getInstance();
        w.pauseWatchingMonitorsFor(SLOW_OPERATION_WATCHDOG_TIMEOUT_MS, reason);
        w.pauseWatchingCurrentThreadFor(SLOW_OPERATION_WATCHDOG_TIMEOUT_MS, reason);
    }

    private void onUserStopped(int userId) {
        Slog.d(TAG, "onUserStopped " + userId);

        extendWatchdogTimeout("#onUserStopped might be slow");
        try {
            mVold.onUserStopped(userId);
            mStoraged.onUserStopped(userId);
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }

        synchronized (mLock) {
            mSystemUnlockedUsers = ArrayUtils.removeInt(mSystemUnlockedUsers, userId);
        }
    }

    private void onUserStopping(int userId) {
        Slog.i(TAG, "onUserStopping " + userId);
        try {
            mStorageSessionController.onUserStopping(userId);
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }
        PackageMonitor monitor = mPackageMonitorsForUser.removeReturnOld(userId);
        if (monitor != null) {
            monitor.unregister();
        }
    }

    private void maybeRemountVolumes(int userId) {
        // We need to keep 2 lists
        // 1. List of volumes before we set the mount user Id so that
        // StorageSessionController is able to remove the session from the correct user (old one)
        // 2. List of volumes to mount which should have the up to date info
        List<ImmutableVolumeInfo> volumesToUnmount = new ArrayList<>();
        List<WatchedVolumeInfo> volumesToMount = new ArrayList<>();
        synchronized (mLock) {
            for (int i = 0; i < mVolumes.size(); i++) {
                final WatchedVolumeInfo vol = mVolumes.valueAt(i);
                if (!vol.isPrimary() && vol.isMountedWritable() && vol.isVisible()
                        && vol.getMountUserId() != mCurrentUserId) {
                    // If there's a visible secondary volume mounted,
                    // we need to update the currentUserId and remount
                    // But capture the volume with the old user id first to use it in unmounting
                    volumesToUnmount.add(vol.getClonedImmutableVolumeInfo());
                    vol.setMountUserId(mCurrentUserId);
                    volumesToMount.add(vol);
                }
            }
        }

        for (int i = 0; i < volumesToMount.size(); i++) {
            Slog.i(TAG, "Remounting volume for user: " + userId + ". Volume: "
                    + volumesToUnmount.get(i));
            mHandler.obtainMessage(H_VOLUME_UNMOUNT, volumesToUnmount.get(i)).sendToTarget();
            mHandler.obtainMessage(H_VOLUME_MOUNT, volumesToMount.get(i)).sendToTarget();
        }
    }

    /**
     * This method checks if the volume is public and the volume is visible and the volume it is
     * trying to mount doesn't have the same mount user id as the current user being maintained by
     * StorageManagerService and change the mount Id. The checks are same as
     * {@link StorageManagerService#maybeRemountVolumes(int)}
     * @param vol {@link WatchedVolumeInfo} object to consider for changing the mountId
     */
    private void updateVolumeMountIdIfRequired(WatchedVolumeInfo vol) {
        synchronized (mLock) {
            if (!vol.isPrimary() && vol.isVisible() && vol.getMountUserId() != mCurrentUserId) {
                vol.setMountUserId(mCurrentUserId);
            }
        }
    }

    /**
     * This method informs vold and storaged that the user has stopped and started whenever move
     * storage is performed. This ensures that the correct emulated volumes are mounted for the
     * users other than the current user. This solves an edge case wherein the correct emulated
     * volumes are not mounted, this will cause the media data to be still stored on internal
     * storage whereas the data should be stored in the adopted primary storage. This method stops
     * the users at vold first which will remove the old volumes which and starts the users at vold
     * which will reattach the correct volumes. This does not performs a full reset as full reset
     * clears every state from vold and SMS {@link #resetIfRebootedAndConnected} which is expensive
     * and causes instability.
     */
    private void remountVolumesForRunningUsersOnMove() {
        // Do not want to hold the lock for long
        final List<Integer> unlockedUsers = new ArrayList<>();
        synchronized (mLock) {
            for (int userId : mSystemUnlockedUsers) {
                if (userId == mCurrentUserId) continue;
                unlockedUsers.add(userId);
            }
        }
        extendWatchdogTimeout("#onUserStopped might be slow");
        for (Integer userId : unlockedUsers) {
            try {
                mVold.onUserStopped(userId);
                mStoraged.onUserStopped(userId);
            } catch (Exception e) {
                Slog.wtf(TAG, e);
            }
        }
        for (Integer userId : unlockedUsers) {
            try {
                mVold.onUserStarted(userId);
                mStoraged.onUserStarted(userId);
            } catch (Exception e) {
                Slog.wtf(TAG, e);
            }
        }
    }

    private boolean supportsBlockCheckpoint() throws RemoteException {
        enforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS);
        return mVold.supportsBlockCheckpoint();
    }

    private void prepareUserStorageForMoveInternal(String fromVolumeUuid, String toVolumeUuid,
            List<UserInfo> users) throws Exception {

        final int flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
        for (UserInfo user : users) {
            prepareUserStorageInternal(fromVolumeUuid, user.id, flags);
            prepareUserStorageInternal(toVolumeUuid, user.id, flags);
        }
    }

    @Override
    public void onAwakeStateChanged(boolean isAwake) {
        // Ignored
    }

    @Override
    public void onKeyguardStateChanged(boolean isShowing) {
        // Push down current secure keyguard status so that we ignore malicious
        // USB devices while locked.
        boolean isSecureKeyguardShowing = isShowing
                && mContext.getSystemService(KeyguardManager.class).isDeviceSecure(mCurrentUserId);
        if (mSecureKeyguardShowing != isSecureKeyguardShowing) {
            mSecureKeyguardShowing = isSecureKeyguardShowing;
            mHandler.obtainMessage(H_SECURE_KEYGUARD_STATE_CHANGED, mSecureKeyguardShowing)
                    .sendToTarget();
        }
    }

    void runIdleMaintenance(Runnable callback) {
        mHandler.sendMessage(mHandler.obtainMessage(H_FSTRIM, callback));
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS)
    // Binder entry point for kicking off an immediate fstrim
    @Override
    public void runMaintenance() {
        super.runMaintenance_enforcePermission();

        runIdleMaintenance(null);
    }

    @Override
    public long lastMaintenance() {
        return mLastMaintenance;
    }

    public void onDaemonConnected() {
        mDaemonConnected = true;
        mHandler.obtainMessage(H_DAEMON_CONNECTED).sendToTarget();
    }

    private void handleDaemonConnected() {
        resetIfBootedAndConnected();
    }

    private final IVoldListener mListener = new IVoldListener.Stub() {
        @Override
        public void onDiskCreated(String diskId, int flags) {
            synchronized (mLock) {
                mDisks.put(diskId, new DiskInfo(diskId, flags));
            }
        }

        @Override
        public void onDiskScanned(String diskId) {
            synchronized (mLock) {
                final DiskInfo disk = mDisks.get(diskId);
                if (disk != null) {
                    onDiskScannedLocked(disk);
                }
            }
        }

        @Override
        public void onDiskMetadataChanged(String diskId, long sizeBytes, String label,
                String sysPath) {
            synchronized (mLock) {
                final DiskInfo disk = mDisks.get(diskId);
                if (disk != null) {
                    disk.size = sizeBytes;
                    disk.label = label;
                    disk.sysPath = sysPath;
                }
            }
        }

        @Override
        public void onDiskDestroyed(String diskId) {
            synchronized (mLock) {
                final DiskInfo disk = mDisks.remove(diskId);
                if (disk != null) {
                    mCallbacks.notifyDiskDestroyed(disk);
                }
            }
        }

        @Override
        public void onVolumeCreated(String volId, int type, String diskId, String partGuid,
                int userId) {
            Trace.instant(Trace.TRACE_TAG_SYSTEM_SERVER,
                    "SMS.onVolumeCreated: " + volId + ", " + userId);
            synchronized (mLock) {
                final DiskInfo disk = mDisks.get(diskId);
                final VolumeInfo vol = new VolumeInfo(volId, type, disk, partGuid);
                vol.mountUserId = userId;
                WatchedVolumeInfo watchedVol = WatchedVolumeInfo.fromVolumeInfo(vol);
                mVolumes.put(volId, watchedVol);
                onVolumeCreatedLocked(watchedVol);
            }
        }

        @Override
        public void onVolumeStateChanged(String volId, final int newState, final int userId) {
            synchronized (mLock) {
                final WatchedVolumeInfo vol = mVolumes.get(volId);
                if (vol != null) {
                    final int oldState = vol.getState();
                    vol.setState(newState);
                    final WatchedVolumeInfo vInfo = new WatchedVolumeInfo(vol);
                    vInfo.setMountUserId(userId);
                    final SomeArgs args = SomeArgs.obtain();
                    args.arg1 = vInfo;
                    args.argi1 = oldState;
                    args.argi2 = newState;
                    mHandler.obtainMessage(H_VOLUME_STATE_CHANGED, args).sendToTarget();
                    onVolumeStateChangedLocked(vInfo, newState);
                }
            }
        }

        @Override
        public void onVolumeMetadataChanged(String volId, String fsType, String fsUuid,
                String fsLabel) {
            synchronized (mLock) {
                final WatchedVolumeInfo vol = mVolumes.get(volId);
                if (vol != null) {
                    vol.setFsType(fsType);
                    vol.setFsUuid(fsUuid);
                    vol.setFsLabel(fsLabel);
                }
            }
        }

        @Override
        public void onVolumePathChanged(String volId, String path) {
            synchronized (mLock) {
                final WatchedVolumeInfo vol = mVolumes.get(volId);
                if (vol != null) {
                    vol.setFsPath(path);
                }
            }
        }

        @Override
        public void onVolumeInternalPathChanged(String volId, String internalPath) {
            synchronized (mLock) {
                final WatchedVolumeInfo vol = mVolumes.get(volId);
                if (vol != null) {
                    vol.setInternalPath(internalPath);
                }
            }
        }

        @Override
        public void onVolumeDestroyed(String volId) {
            WatchedVolumeInfo vol = null;
            synchronized (mLock) {
                vol = mVolumes.remove(volId);
            }

            if (vol != null) {
                mStorageSessionController.onVolumeRemove(vol.getImmutableVolumeInfo());
                try {
                    if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
                        mInstaller.onPrivateVolumeRemoved(vol.getFsUuid());
                    }
                } catch (Installer.InstallerException e) {
                    Slog.i(TAG, "Failed when private volume unmounted " + vol, e);
                }
            }
        }
    };

    @GuardedBy("mLock")
    private void onDiskScannedLocked(DiskInfo disk) {
        int volumeCount = 0;
        for (int i = 0; i < mVolumes.size(); i++) {
            final WatchedVolumeInfo vol = mVolumes.valueAt(i);
            if (Objects.equals(disk.id, vol.getDiskId())) {
                volumeCount++;
            }
        }

        final Intent intent = new Intent(DiskInfo.ACTION_DISK_SCANNED);
        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
                | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
        intent.putExtra(DiskInfo.EXTRA_DISK_ID, disk.id);
        intent.putExtra(DiskInfo.EXTRA_VOLUME_COUNT, volumeCount);
        mHandler.obtainMessage(H_INTERNAL_BROADCAST, intent).sendToTarget();

        final CountDownLatch latch = mDiskScanLatches.remove(disk.id);
        if (latch != null) {
            latch.countDown();
        }

        disk.volumeCount = volumeCount;
        mCallbacks.notifyDiskScanned(disk, volumeCount);
    }

    @GuardedBy("mLock")
    private void onVolumeCreatedLocked(WatchedVolumeInfo vol) {
        final ActivityManagerInternal amInternal =
                LocalServices.getService(ActivityManagerInternal.class);

        if (vol.getMountUserId() >= 0 && !amInternal.isUserRunning(vol.getMountUserId(), 0)) {
            Slog.d(TAG, "Ignoring volume " + vol.getId() + " because user "
                    + Integer.toString(vol.getMountUserId()) + " is no longer running.");
            return;
        }

        if (vol.getType() == VolumeInfo.TYPE_EMULATED) {
            final Context volumeUserContext = mContext.createContextAsUser(
                    UserHandle.of(vol.getMountUserId()), 0);

            boolean isMediaSharedWithParent =
                    (volumeUserContext != null) ? volumeUserContext.getSystemService(
                            UserManager.class).isMediaSharedWithParent() : false;

            // For all the users where media is shared with parent, creation of emulated volume
            // should not be skipped even if media provider instance is not running in that user
            // space
            if (!isMediaSharedWithParent
                    && !mStorageSessionController.supportsExternalStorage(vol.getMountUserId())) {
                Slog.d(TAG, "Ignoring volume " + vol.getId() + " because user "
                        + Integer.toString(vol.getMountUserId())
                        + " does not support external storage.");
                return;
            }

            final StorageManager storage = mContext.getSystemService(StorageManager.class);
            final VolumeInfo privateVol = storage.findPrivateForEmulated(vol.getVolumeInfo());

            if ((Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, mPrimaryStorageUuid)
                    && VolumeInfo.ID_PRIVATE_INTERNAL.equals(privateVol.id))
                    || Objects.equals(privateVol.getFsUuid(), mPrimaryStorageUuid)) {
                Slog.v(TAG, "Found primary storage at " + vol);
                vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_PRIMARY);
                vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE);
                mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
            }

        } else if (vol.getType() == VolumeInfo.TYPE_PUBLIC) {
            // TODO: only look at first public partition
            if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, mPrimaryStorageUuid)
                    && vol.getDisk().isDefaultPrimary()) {
                Slog.v(TAG, "Found primary storage at " + vol);
                vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_PRIMARY);
                vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE);
            }

            // Adoptable public disks are visible to apps, since they meet
            // public API requirement of being in a stable location.
            if (vol.getDisk().isAdoptable()) {
                vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE);
            }

            vol.setMountUserId(mCurrentUserId);
            mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();

        } else if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
            mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();

        } else if (vol.getType() == VolumeInfo.TYPE_STUB) {
            if (vol.getDisk().isStubVisible()) {
                vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_WRITE);
            } else {
                vol.setMountFlags(vol.getMountFlags() | VolumeInfo.MOUNT_FLAG_VISIBLE_FOR_READ);
            }
            vol.setMountUserId(mCurrentUserId);
            mHandler.obtainMessage(H_VOLUME_MOUNT, vol).sendToTarget();
        } else {
            Slog.d(TAG, "Skipping automatic mounting of " + vol);
        }
    }

    private boolean isBroadcastWorthy(WatchedVolumeInfo vol) {
        switch (vol.getType()) {
            case VolumeInfo.TYPE_PRIVATE:
            case VolumeInfo.TYPE_PUBLIC:
            case VolumeInfo.TYPE_EMULATED:
            case VolumeInfo.TYPE_STUB:
                break;
            default:
                return false;
        }

        switch (vol.getState()) {
            case VolumeInfo.STATE_MOUNTED:
            case VolumeInfo.STATE_MOUNTED_READ_ONLY:
            case VolumeInfo.STATE_EJECTING:
            case VolumeInfo.STATE_UNMOUNTED:
            case VolumeInfo.STATE_UNMOUNTABLE:
            case VolumeInfo.STATE_BAD_REMOVAL:
                break;
            default:
                return false;
        }

        return true;
    }

    @GuardedBy("mLock")
    private void onVolumeStateChangedLocked(WatchedVolumeInfo vol, int newState) {
        if (vol.getType() == VolumeInfo.TYPE_EMULATED) {
            if (newState != VolumeInfo.STATE_MOUNTED) {
                mFuseMountedUser.remove(vol.getMountUserId());
            } else if (mVoldAppDataIsolationEnabled){
                final int userId = vol.getMountUserId();
                // Async remount app storage so it won't block the main thread.
                new Thread(() -> {

                    // If user 0 has completed unlock, perform a one-time migration of legacy
                    // obb data to its new location. This may take time depending on the size of
                    // the data to be copied so it's done on the StorageManager worker thread.
                    // This needs to be finished before start mounting obb directories.
                    if (userId == 0
                            && Build.VERSION.DEVICE_INITIAL_SDK_INT < Build.VERSION_CODES.Q) {
                        mPmInternal.migrateLegacyObbData();
                    }

                    // Add fuse mounted user after migration to prevent ProcessList tries to
                    // create obb directory before migration is done.
                    synchronized (mLock) {
                        mFuseMountedUser.add(userId);
                    }

                    Map<Integer, String> pidPkgMap = null;
                    // getProcessesWithPendingBindMounts() could fail when a new app process is
                    // starting and it's not planning to mount storage dirs in zygote, but it's
                    // rare, so we retry 5 times and hope we can get the result successfully.
                    for (int i = 0; i < 5; i++) {
                        try {
                            pidPkgMap = LocalServices.getService(ActivityManagerInternal.class)
                                    .getProcessesWithPendingBindMounts(vol.getMountUserId());
                            break;
                        } catch (IllegalStateException e) {
                            Slog.i(TAG, "Some processes are starting, retry");
                            // Wait 100ms and retry so hope the pending process is started.
                            SystemClock.sleep(100);
                        }
                    }
                    if (pidPkgMap != null) {
                        remountAppStorageDirs(pidPkgMap, userId);
                    } else {
                        Slog.wtf(TAG, "Not able to getStorageNotOptimizedProcesses() after"
                                + " 5 retries");
                    }
                }).start();
            }
        }
    }

    private void onVolumeStateChangedAsync(WatchedVolumeInfo vol, int oldState, int newState) {
        if (newState == VolumeInfo.STATE_MOUNTED) {
            // Private volumes can be unmounted and re-mounted even after a user has
            // been unlocked; on devices that support encryption keys tied to the filesystem,
            // this requires setting up the keys again.
            try {
                prepareUserStorageIfNeeded(vol);
            } catch (Exception e) {
                // Unusable partition, unmount.
                try {
                    mVold.unmount(vol.getId());
                } catch (Exception ee) {
                    Slog.wtf(TAG, ee);
                }
                return;
            }
        }

        synchronized (mLock) {
            // Remember that we saw this volume so we're ready to accept user
            // metadata, or so we can annoy them when a private volume is ejected
            if (!TextUtils.isEmpty(vol.getFsUuid())) {
                VolumeRecord rec = mRecords.get(vol.getFsUuid());
                if (rec == null) {
                    rec = new VolumeRecord(vol.getType(), vol.getFsUuid());
                    rec.partGuid = vol.getPartGuid();
                    rec.createdMillis = System.currentTimeMillis();
                    if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
                        rec.nickname = vol.getDisk().getDescription();
                    }
                    mRecords.put(rec.fsUuid, rec);
                } else {
                    // Handle upgrade case where we didn't store partition GUID
                    if (TextUtils.isEmpty(rec.partGuid)) {
                        rec.partGuid = vol.getPartGuid();
                    }
                }

                rec.lastSeenMillis = System.currentTimeMillis();
                writeSettingsLocked();
            }
        }

        // This is a blocking call to Storage Service which needs to process volume state changed
        // before notifying other listeners.
        // Intentionally called without the mLock to avoid deadlocking from the Storage Service.
        try {
            mStorageSessionController.notifyVolumeStateChanged(vol.getImmutableVolumeInfo());
        } catch (ExternalStorageServiceException e) {
            Log.e(TAG, "Failed to notify volume state changed to the Storage Service", e);
        }
        synchronized (mLock) {
            mCallbacks.notifyVolumeStateChanged(vol, oldState, newState);

            // Do not broadcast before boot has completed to avoid launching the
            // processes that receive the intent unnecessarily.
            if (mBootCompleted && isBroadcastWorthy(vol)) {
                final Intent intent = new Intent(VolumeInfo.ACTION_VOLUME_STATE_CHANGED);
                intent.putExtra(VolumeInfo.EXTRA_VOLUME_ID, vol.getId());
                intent.putExtra(VolumeInfo.EXTRA_VOLUME_STATE, newState);
                intent.putExtra(VolumeRecord.EXTRA_FS_UUID, vol.getFsUuid());
                intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY_BEFORE_BOOT
                        | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND);
                mHandler.obtainMessage(H_INTERNAL_BROADCAST, intent).sendToTarget();
            }

            final String oldStateEnv = VolumeInfo.getEnvironmentForState(oldState);
            final String newStateEnv = VolumeInfo.getEnvironmentForState(newState);

            if (!Objects.equals(oldStateEnv, newStateEnv)) {
                // Kick state changed event towards all started users. Any users
                // started after this point will trigger additional
                // user-specific broadcasts.
                for (int userId : mSystemUnlockedUsers) {
                    if (vol.isVisibleForUser(userId)) {
                        final StorageVolume userVol = vol.buildStorageVolume(mContext, userId,
                                false);
                        mHandler.obtainMessage(H_VOLUME_BROADCAST, userVol).sendToTarget();

                        mCallbacks.notifyStorageStateChanged(userVol.getPath(), oldStateEnv,
                                newStateEnv);
                    }
                }
            }

            if ((vol.getType() == VolumeInfo.TYPE_PUBLIC || vol.getType() == VolumeInfo.TYPE_STUB)
                    && vol.getState() == VolumeInfo.STATE_EJECTING) {
                // TODO: this should eventually be handled by new ObbVolume state changes
                /*
                 * Some OBBs might have been unmounted when this volume was
                 * unmounted, so send a message to the handler to let it know to
                 * remove those from the list of mounted OBBS.
                 */
                mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage(
                        OBB_FLUSH_MOUNT_STATE, vol.getFsPath()));
            }
            maybeLogMediaMount(vol, newState);
        }
    }

    private void notifyCloudMediaProviderChangedAsync(
            @NonNull StorageManagerInternal.CloudProviderChangeListener listener) {
        synchronized (mCloudMediaProviders) {
            for (int i = mCloudMediaProviders.size() - 1; i >= 0; --i) {
                final int userId = mCloudMediaProviders.keyAt(i);
                final String authority = mCloudMediaProviders.valueAt(i);
                listener.onCloudProviderChanged(userId, authority);
            }
        }
    }

    private void onCloudMediaProviderChangedAsync(
            @UserIdInt int userId, @Nullable String authority) {
        for (StorageManagerInternal.CloudProviderChangeListener listener :
                mStorageManagerInternal.mCloudProviderChangeListeners) {
            listener.onCloudProviderChanged(userId, authority);
        }
    }

    private void maybeLogMediaMount(WatchedVolumeInfo vol, int newState) {
        if (!SecurityLog.isLoggingEnabled()) {
            return;
        }

        final DiskInfo disk = vol.getDisk();
        if (disk == null || (disk.flags & (DiskInfo.FLAG_SD | DiskInfo.FLAG_USB)) == 0) {
            return;
        }

        // Sometimes there is a newline character.
        final String label = disk.label != null ? disk.label.trim() : "";

        if (newState == VolumeInfo.STATE_MOUNTED
                || newState == VolumeInfo.STATE_MOUNTED_READ_ONLY) {
            SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_MOUNT, vol.getFsPath(), label);
        } else if (newState == VolumeInfo.STATE_UNMOUNTED
                || newState == VolumeInfo.STATE_BAD_REMOVAL) {
            SecurityLog.writeEvent(SecurityLog.TAG_MEDIA_UNMOUNT, vol.getFsPath(), label);
        }
    }

    @GuardedBy("mLock")
    private void onMoveStatusLocked(int status) {
        if (mMoveCallback == null) {
            Slog.w(TAG, "Odd, status but no move requested");
            return;
        }

        // TODO: estimate remaining time
        try {
            mMoveCallback.onStatusChanged(-1, status, -1);
        } catch (RemoteException ignored) {
        }

        // We've finished copying and we're about to clean up old data, so
        // remember that move was successful if we get rebooted
        if (status == MOVE_STATUS_COPY_FINISHED) {
            Slog.d(TAG, "Move to " + mMoveTargetUuid + " copy phase finshed; persisting");

            mPrimaryStorageUuid = mMoveTargetUuid;
            writeSettingsLocked();
            mHandler.obtainMessage(H_REMOUNT_VOLUMES_ON_MOVE).sendToTarget();
        }

        if (PackageManager.isMoveStatusFinished(status)) {
            Slog.d(TAG, "Move to " + mMoveTargetUuid + " finished with status " + status);

            mMoveCallback = null;
            mMoveTargetUuid = null;
        }
    }

    private void enforcePermission(String perm) {
        mContext.enforceCallingOrSelfPermission(perm, perm);
    }

    /**
     * Decide if volume is mountable per device policies.
     */
    private boolean isMountDisallowed(WatchedVolumeInfo vol) {
        UserManager userManager = mContext.getSystemService(UserManager.class);
        UserHandle mountUserHandle = UserHandle.of(vol.getMountUserId());

        boolean isUsbRestricted = false;
        if (vol.getDisk() != null && vol.getDisk().isUsb()) {
            isUsbRestricted = userManager.hasUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER,
                    mountUserHandle);
        }

        boolean isTypeRestricted = false;
        if (vol.getType() == VolumeInfo.TYPE_PUBLIC || vol.getType() == VolumeInfo.TYPE_PRIVATE
                || vol.getType() == VolumeInfo.TYPE_STUB) {
            isTypeRestricted = userManager
                    .hasUserRestriction(UserManager.DISALLOW_MOUNT_PHYSICAL_MEDIA,
                    mountUserHandle);
        }

        return isUsbRestricted || isTypeRestricted;
    }

    private void enforceAdminUser() {
        UserManager um = (UserManager) mContext.getSystemService(Context.USER_SERVICE);
        final int callingUserId = UserHandle.getCallingUserId();
        boolean isAdmin;
        final long token = Binder.clearCallingIdentity();
        try {
            isAdmin = um.getUserInfo(callingUserId).isAdmin();
        } finally {
            Binder.restoreCallingIdentity(token);
        }
        if (!isAdmin) {
            throw new SecurityException("Only admin users can adopt sd cards");
        }
    }

    /**
     * Constructs a new StorageManagerService instance
     *
     * @param context  Binder context for this service
     */
    public StorageManagerService(Context context) {
        sSelf = this;
        mVoldAppDataIsolationEnabled = SystemProperties.getBoolean(
                ANDROID_VOLD_APP_DATA_ISOLATION_ENABLED_PROPERTY, false);
        mContext = context;
        mCallbacks = new Callbacks(FgThread.get().getLooper());

        mVolumes.registerObserver(new Watcher() {
            @Override
            public void onChange(Watchable what) {
                // When we change the list or any volume contained in it, invalidate the cache
                StorageManager.invalidateVolumeListCache();
            }
        });
        HandlerThread hthread = new HandlerThread(TAG);
        hthread.start();
        mHandler = new StorageManagerServiceHandler(hthread.getLooper());

        // Add OBB Action Handler to StorageManagerService thread.
        mObbActionHandler = new ObbActionHandler(IoThread.get().getLooper());

        mStorageSessionController = new StorageSessionController(mContext);

        mInstaller = new Installer(mContext);
        mInstaller.onStart();

        // Initialize the last-fstrim tracking if necessary
        File dataDir = Environment.getDataDirectory();
        File systemDir = new File(dataDir, "system");
        mLastMaintenanceFile = new File(systemDir, LAST_FSTRIM_FILE);
        if (!mLastMaintenanceFile.exists()) {
            // Not setting mLastMaintenance here means that we will force an
            // fstrim during reboot following the OTA that installs this code.
            try {
                (new FileOutputStream(mLastMaintenanceFile)).close();
            } catch (IOException e) {
                Slog.e(TAG, "Unable to create fstrim record " + mLastMaintenanceFile.getPath());
            }
        } else {
            mLastMaintenance = mLastMaintenanceFile.lastModified();
        }

        mSettingsFile = new AtomicFile(
                new File(Environment.getDataSystemDirectory(), "storage.xml"), "storage-settings");
        mWriteRecordFile = new AtomicFile(
                new File(Environment.getDataSystemDirectory(), "storage-write-records"));

        sSmartIdleMaintPeriod = DeviceConfig.getInt(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
            "smart_idle_maint_period", DEFAULT_SMART_IDLE_MAINT_PERIOD);
        if (sSmartIdleMaintPeriod < MIN_SMART_IDLE_MAINT_PERIOD) {
            sSmartIdleMaintPeriod = MIN_SMART_IDLE_MAINT_PERIOD;
        } else if (sSmartIdleMaintPeriod > MAX_SMART_IDLE_MAINT_PERIOD) {
            sSmartIdleMaintPeriod = MAX_SMART_IDLE_MAINT_PERIOD;
        }

        mMaxWriteRecords = MAX_PERIOD_WRITE_RECORD / sSmartIdleMaintPeriod;
        mStorageWriteRecords = new int[mMaxWriteRecords];

        synchronized (mLock) {
            readSettingsLocked();
        }

        LocalServices.addService(StorageManagerInternal.class, mStorageManagerInternal);

        final IntentFilter userFilter = new IntentFilter();
        userFilter.addAction(Intent.ACTION_USER_ADDED);
        userFilter.addAction(Intent.ACTION_USER_REMOVED);
        mContext.registerReceiver(mUserReceiver, userFilter, null, mHandler);

        synchronized (mLock) {
            addInternalVolumeLocked();
        }

        // Add ourself to the Watchdog monitors if enabled.
        if (WATCHDOG_ENABLE) {
            Watchdog.getInstance().addMonitor(this);
        }
    }

    private void start() {
        connectStoraged();
        connectVold();
    }

    private void connectStoraged() {
        IBinder binder = ServiceManager.getService("storaged");
        if (binder != null) {
            try {
                binder.linkToDeath(new DeathRecipient() {
                    @Override
                    public void binderDied() {
                        Slog.w(TAG, "storaged died; reconnecting");
                        mStoraged = null;
                        connectStoraged();
                    }
                }, 0);
            } catch (RemoteException e) {
                binder = null;
            }
        }

        if (binder != null) {
            mStoraged = IStoraged.Stub.asInterface(binder);
        } else {
            Slog.w(TAG, "storaged not found; trying again");
        }

        if (mStoraged == null) {
            BackgroundThread.getHandler().postDelayed(() -> {
                connectStoraged();
            }, DateUtils.SECOND_IN_MILLIS);
        } else {
            onDaemonConnected();
        }
    }

    private void connectVold() {
        IBinder binder = ServiceManager.getService("vold");
        if (binder != null) {
            try {
                binder.linkToDeath(new DeathRecipient() {
                    @Override
                    public void binderDied() {
                        Slog.w(TAG, "vold died; reconnecting");
                        mVold = null;
                        connectVold();
                    }
                }, 0);
            } catch (RemoteException e) {
                binder = null;
            }
        }

        if (binder != null) {
            mVold = IVold.Stub.asInterface(binder);
            try {
                mVold.setListener(mListener);
            } catch (RemoteException e) {
                mVold = null;
                Slog.w(TAG, "vold listener rejected; trying again", e);
            }
        } else {
            Slog.w(TAG, "vold not found; trying again");
        }

        if (mVold == null) {
            BackgroundThread.getHandler().postDelayed(() -> {
                connectVold();
            }, DateUtils.SECOND_IN_MILLIS);
        } else {
            restoreCeUnlockedUsers();
            onDaemonConnected();
        }
    }

    private void servicesReady() {
        mPmInternal = LocalServices.getService(PackageManagerInternal.class);

        mIPackageManager = IPackageManager.Stub.asInterface(
                ServiceManager.getService("package"));
        mIAppOpsService = IAppOpsService.Stub.asInterface(
                ServiceManager.getService(Context.APP_OPS_SERVICE));

        ProviderInfo provider = getProviderInfo(MediaStore.AUTHORITY);
        if (provider != null) {
            mMediaStoreAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
            sMediaStoreAuthorityProcessName = provider.applicationInfo.processName;
        }

        provider = getProviderInfo(Downloads.Impl.AUTHORITY);
        if (provider != null) {
            mDownloadsAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
        }

        provider = getProviderInfo(DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY);
        if (provider != null) {
            mExternalStorageAuthorityAppId = UserHandle.getAppId(provider.applicationInfo.uid);
        }
    }

    private ProviderInfo getProviderInfo(String authority) {
        return mPmInternal.resolveContentProvider(
                authority, PackageManager.MATCH_DIRECT_BOOT_AWARE
                        | PackageManager.MATCH_DIRECT_BOOT_UNAWARE,
                UserHandle.getUserId(UserHandle.USER_SYSTEM), Process.SYSTEM_UID);
    }

    private void updateLegacyStorageApps(String packageName, int uid, boolean hasLegacy) {
        synchronized (mLock) {
            if (hasLegacy) {
                Slog.v(TAG, "Package " + packageName + " has legacy storage");
                mUidsWithLegacyExternalStorage.add(uid);
            } else {
                // TODO(b/149391976): Handle shared user id. Check if there's any other
                // installed app with legacy external storage before removing
                Slog.v(TAG, "Package " + packageName + " does not have legacy storage");
                mUidsWithLegacyExternalStorage.remove(uid);
            }
        }
    }

    private void snapshotAndMonitorLegacyStorageAppOp(UserHandle user) {
        int userId = user.getIdentifier();

        // TODO(b/149391976): Use mIAppOpsService.getPackagesForOps instead of iterating below
        // It should improve performance but the AppOps method doesn't return any app here :(
        // This operation currently takes about ~20ms on a freshly flashed device
        for (ApplicationInfo ai : mPmInternal.getInstalledApplications(MATCH_DIRECT_BOOT_AWARE
                        | MATCH_DIRECT_BOOT_UNAWARE | MATCH_UNINSTALLED_PACKAGES | MATCH_ANY_USER,
                        userId, Process.myUid())) {
            try {
                boolean hasLegacy = mIAppOpsService.checkOperation(OP_LEGACY_STORAGE, ai.uid,
                        ai.packageName) == MODE_ALLOWED;
                updateLegacyStorageApps(ai.packageName, ai.uid, hasLegacy);
            } catch (RemoteException e) {
                Slog.e(TAG, "Failed to check legacy op for package " + ai.packageName, e);
            }
        }

        if (mPackageMonitorsForUser.get(userId) == null) {
            PackageMonitor monitor = new PackageMonitor() {
                @Override
                public void onPackageRemoved(String packageName, int uid) {
                    updateLegacyStorageApps(packageName, uid, false);
                }
            };
            // TODO(b/149391976): Use different handler?
            monitor.register(mContext, user, mHandler);
            mPackageMonitorsForUser.put(userId, monitor);
        } else {
            Slog.w(TAG, "PackageMonitor is already registered for: " + userId);
        }
    }

    private void systemReady() {
        LocalServices.getService(ActivityTaskManagerInternal.class)
                .registerScreenObserver(this);

        mHandler.obtainMessage(H_SYSTEM_READY).sendToTarget();
    }

    private void bootCompleted() {
        mBootCompleted = true;
        mHandler.obtainMessage(H_BOOT_COMPLETED).sendToTarget();
    }

    private void handleBootCompleted() {
        resetIfBootedAndConnected();
    }

    private String getDefaultPrimaryStorageUuid() {
        if (SystemProperties.getBoolean(StorageManager.PROP_PRIMARY_PHYSICAL, false)) {
            return StorageManager.UUID_PRIMARY_PHYSICAL;
        } else {
            return StorageManager.UUID_PRIVATE_INTERNAL;
        }
    }

    @GuardedBy("mLock")
    private void readSettingsLocked() {
        mRecords.clear();
        mPrimaryStorageUuid = getDefaultPrimaryStorageUuid();

        FileInputStream fis = null;
        try {
            fis = mSettingsFile.openRead();
            final TypedXmlPullParser in = Xml.resolvePullParser(fis);

            int type;
            while ((type = in.next()) != END_DOCUMENT) {
                if (type == START_TAG) {
                    final String tag = in.getName();
                    if (TAG_VOLUMES.equals(tag)) {
                        final int version = in.getAttributeInt(null, ATTR_VERSION, VERSION_INIT);
                        final boolean primaryPhysical = SystemProperties.getBoolean(
                                StorageManager.PROP_PRIMARY_PHYSICAL, false);
                        final boolean validAttr = (version >= VERSION_FIX_PRIMARY)
                                || (version >= VERSION_ADD_PRIMARY && !primaryPhysical);
                        if (validAttr) {
                            mPrimaryStorageUuid = readStringAttribute(in,
                                    ATTR_PRIMARY_STORAGE_UUID);
                        }
                    } else if (TAG_VOLUME.equals(tag)) {
                        final VolumeRecord rec = readVolumeRecord(in);
                        mRecords.put(rec.fsUuid, rec);
                    }
                }
            }
        } catch (FileNotFoundException e) {
            // Missing metadata is okay, probably first boot
        } catch (IOException e) {
            Slog.wtf(TAG, "Failed reading metadata", e);
        } catch (XmlPullParserException e) {
            Slog.wtf(TAG, "Failed reading metadata", e);
        } finally {
            IoUtils.closeQuietly(fis);
        }
    }

    @GuardedBy("mLock")
    private void writeSettingsLocked() {
        FileOutputStream fos = null;
        try {
            fos = mSettingsFile.startWrite();

            TypedXmlSerializer out = Xml.resolveSerializer(fos);
            out.startDocument(null, true);
            out.startTag(null, TAG_VOLUMES);
            out.attributeInt(null, ATTR_VERSION, VERSION_FIX_PRIMARY);
            writeStringAttribute(out, ATTR_PRIMARY_STORAGE_UUID, mPrimaryStorageUuid);
            final int size = mRecords.size();
            for (int i = 0; i < size; i++) {
                final VolumeRecord rec = mRecords.valueAt(i);
                writeVolumeRecord(out, rec);
            }
            out.endTag(null, TAG_VOLUMES);
            out.endDocument();

            mSettingsFile.finishWrite(fos);
        } catch (IOException e) {
            if (fos != null) {
                mSettingsFile.failWrite(fos);
            }
        }
    }

    public static VolumeRecord readVolumeRecord(TypedXmlPullParser in)
            throws IOException, XmlPullParserException {
        final int type = in.getAttributeInt(null, ATTR_TYPE);
        final String fsUuid = readStringAttribute(in, ATTR_FS_UUID);
        final VolumeRecord meta = new VolumeRecord(type, fsUuid);
        meta.partGuid = readStringAttribute(in, ATTR_PART_GUID);
        meta.nickname = readStringAttribute(in, ATTR_NICKNAME);
        meta.userFlags = in.getAttributeInt(null, ATTR_USER_FLAGS);
        meta.createdMillis = in.getAttributeLong(null, ATTR_CREATED_MILLIS, 0);
        meta.lastSeenMillis = in.getAttributeLong(null, ATTR_LAST_SEEN_MILLIS, 0);
        meta.lastTrimMillis = in.getAttributeLong(null, ATTR_LAST_TRIM_MILLIS, 0);
        meta.lastBenchMillis = in.getAttributeLong(null, ATTR_LAST_BENCH_MILLIS, 0);
        return meta;
    }

    public static void writeVolumeRecord(TypedXmlSerializer out, VolumeRecord rec)
            throws IOException {
        out.startTag(null, TAG_VOLUME);
        out.attributeInt(null, ATTR_TYPE, rec.type);
        writeStringAttribute(out, ATTR_FS_UUID, rec.fsUuid);
        writeStringAttribute(out, ATTR_PART_GUID, rec.partGuid);
        writeStringAttribute(out, ATTR_NICKNAME, rec.nickname);
        out.attributeInt(null, ATTR_USER_FLAGS, rec.userFlags);
        out.attributeLong(null, ATTR_CREATED_MILLIS, rec.createdMillis);
        out.attributeLong(null, ATTR_LAST_SEEN_MILLIS, rec.lastSeenMillis);
        out.attributeLong(null, ATTR_LAST_TRIM_MILLIS, rec.lastTrimMillis);
        out.attributeLong(null, ATTR_LAST_BENCH_MILLIS, rec.lastBenchMillis);
        out.endTag(null, TAG_VOLUME);
    }

    /**
     * Exposed API calls below here
     */

    @Override
    public void registerListener(IStorageEventListener listener) {
        mCallbacks.register(listener);
    }

    @Override
    public void unregisterListener(IStorageEventListener listener) {
        mCallbacks.unregister(listener);
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.SHUTDOWN)
    @Override
    public void shutdown(final IStorageShutdownObserver observer) {

        super.shutdown_enforcePermission();

        Slog.i(TAG, "Shutting down");
        mHandler.obtainMessage(H_SHUTDOWN, observer).sendToTarget();
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS)
    @Override
    public void mount(String volId) {

        super.mount_enforcePermission();

        final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId);
        updateVolumeMountIdIfRequired(vol);
        if (isMountDisallowed(vol)) {
            throw new SecurityException("Mounting " + volId + " restricted by policy");
        }
        mount(vol);
    }

    private void remountAppStorageDirs(Map<Integer, String> pidPkgMap, int userId) {
        for (Entry<Integer, String> entry : pidPkgMap.entrySet()) {
            final int pid = entry.getKey();
            final String packageName = entry.getValue();
            Slog.i(TAG, "Remounting storage for pid: " + pid);
            final String[] sharedPackages =
                    mPmInternal.getSharedUserPackagesForPackage(packageName, userId);
            final int uid = mPmInternal.getPackageUid(packageName, 0 /* flags */, userId);
            final String[] packages =
                    sharedPackages.length != 0 ? sharedPackages : new String[]{packageName};
            try {
                mVold.remountAppStorageDirs(uid, pid, packages);
            } catch (RemoteException e) {
                throw e.rethrowAsRuntimeException();
            }
        }
    }

    private void mount(WatchedVolumeInfo vol) {
        try {
            Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER, "SMS.mount: " + vol.getId());
            // TODO(b/135341433): Remove cautious logging when FUSE is stable
            Slog.i(TAG, "Mounting volume " + vol);
            extendWatchdogTimeout("#mount might be slow");
            mVold.mount(vol.getId(), vol.getMountFlags(), vol.getMountUserId(),
                    new IVoldMountCallback.Stub() {
                @Override
                public boolean onVolumeChecking(FileDescriptor fd, String path,
                        String internalPath) {
                    vol.setFsPath(path);
                    vol.setInternalPath(internalPath);
                    ParcelFileDescriptor pfd = new ParcelFileDescriptor(fd);
                    try {
                        Trace.traceBegin(Trace.TRACE_TAG_SYSTEM_SERVER,
                                "SMS.startFuseFileSystem: " + vol.getId());
                        mStorageSessionController.onVolumeMount(pfd, vol.getImmutableVolumeInfo());
                        return true;
                    } catch (ExternalStorageServiceException e) {
                        Slog.e(TAG, "Failed to mount volume " + vol, e);

                        int nextResetSeconds = FAILED_MOUNT_RESET_TIMEOUT_SECONDS;
                        Slog.i(TAG, "Scheduling reset in " + nextResetSeconds + "s");
                        mHandler.removeMessages(H_RESET);
                        mHandler.sendMessageDelayed(mHandler.obtainMessage(H_RESET),
                                TimeUnit.SECONDS.toMillis(nextResetSeconds));
                        return false;
                    } finally {
                        Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
                        try {
                            pfd.close();
                        } catch (Exception e) {
                            Slog.e(TAG, "Failed to close FUSE device fd", e);
                        }
                    }
                }
            });
            Slog.i(TAG, "Mounted volume " + vol);
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS)
    @Override
    public void unmount(String volId) {

        super.unmount_enforcePermission();

        final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId);
        unmount(vol.getClonedImmutableVolumeInfo());
    }

    private void unmount(ImmutableVolumeInfo vol) {
        try {
            try {
                if (vol.getType() == VolumeInfo.TYPE_PRIVATE) {
                    mInstaller.onPrivateVolumeRemoved(vol.getFsUuid());
                }
            } catch (Installer.InstallerException e) {
                Slog.e(TAG, "Failed unmount mirror data", e);
            }
            extendWatchdogTimeout("#unmount might be slow");
            mVold.unmount(vol.getId());
            mStorageSessionController.onVolumeUnmount(vol);
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS)
    @Override
    public void format(String volId) {

        super.format_enforcePermission();

        final WatchedVolumeInfo vol = findVolumeByIdOrThrow(volId);
        final String fsUuid = vol.getFsUuid();
        try {
            mVold.format(vol.getId(), "auto");

            // After a successful format above, we should forget about any
            // records for the old partition, since it'll never appear again
            if (!TextUtils.isEmpty(fsUuid)) {
                forgetVolume(fsUuid);
            }
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS)
    @Override
    public void benchmark(String volId, IVoldTaskListener listener) {

        super.benchmark_enforcePermission();

        try {
            mVold.benchmark(volId, new IVoldTaskListener.Stub() {
                @Override
                public void onStatus(int status, PersistableBundle extras) {
                    dispatchOnStatus(listener, status, extras);
                }

                @Override
                public void onFinished(int status, PersistableBundle extras) {
                    dispatchOnFinished(listener, status, extras);

                    final String path = extras.getString("path");
                    final String ident = extras.getString("ident");
                    final long create = extras.getLong("create");
                    final long run = extras.getLong("run");
                    final long destroy = extras.getLong("destroy");

                    final DropBoxManager dropBox = mContext.getSystemService(DropBoxManager.class);
                    dropBox.addText(TAG_STORAGE_BENCHMARK, scrubPath(path)
                            + " " + ident + " " + create + " " + run + " " + destroy);

                    synchronized (mLock) {
                        final VolumeRecord rec = findRecordForPath(path);
                        if (rec != null) {
                            rec.lastBenchMillis = System.currentTimeMillis();
                            writeSettingsLocked();
                        }
                    }
                }
            });
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS)
    @Override
    public void partitionPublic(String diskId) {
        super.partitionPublic_enforcePermission();

        final CountDownLatch latch = findOrCreateDiskScanLatch(diskId);

        extendWatchdogTimeout("#partition might be slow");
        try {
            mVold.partition(diskId, IVold.PARTITION_TYPE_PUBLIC, -1);
            waitForLatch(latch, "partitionPublic", 3 * DateUtils.MINUTE_IN_MILLIS);
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS)
    @Override
    public void partitionPrivate(String diskId) {
        super.partitionPrivate_enforcePermission();

        enforceAdminUser();

        final CountDownLatch latch = findOrCreateDiskScanLatch(diskId);

        extendWatchdogTimeout("#partition might be slow");
        try {
            mVold.partition(diskId, IVold.PARTITION_TYPE_PRIVATE, -1);
            waitForLatch(latch, "partitionPrivate", 3 * DateUtils.MINUTE_IN_MILLIS);
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS)
    @Override
    public void partitionMixed(String diskId, int ratio) {
        super.partitionMixed_enforcePermission();

        enforceAdminUser();

        final CountDownLatch latch = findOrCreateDiskScanLatch(diskId);

        extendWatchdogTimeout("#partition might be slow");
        try {
            mVold.partition(diskId, IVold.PARTITION_TYPE_MIXED, ratio);
            waitForLatch(latch, "partitionMixed", 3 * DateUtils.MINUTE_IN_MILLIS);
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS)
    @Override
    public void setVolumeNickname(String fsUuid, String nickname) {

        super.setVolumeNickname_enforcePermission();

        Objects.requireNonNull(fsUuid);
        synchronized (mLock) {
            final VolumeRecord rec = mRecords.get(fsUuid);
            rec.nickname = nickname;
            mCallbacks.notifyVolumeRecordChanged(rec);
            writeSettingsLocked();
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS)
    @Override
    public void setVolumeUserFlags(String fsUuid, int flags, int mask) {

        super.setVolumeUserFlags_enforcePermission();

        Objects.requireNonNull(fsUuid);
        synchronized (mLock) {
            final VolumeRecord rec = mRecords.get(fsUuid);
            rec.userFlags = (rec.userFlags & ~mask) | (flags & mask);
            mCallbacks.notifyVolumeRecordChanged(rec);
            writeSettingsLocked();
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS)
    @Override
    public void forgetVolume(String fsUuid) {

        super.forgetVolume_enforcePermission();

        Objects.requireNonNull(fsUuid);

        synchronized (mLock) {
            final VolumeRecord rec = mRecords.remove(fsUuid);
            if (rec != null && !TextUtils.isEmpty(rec.partGuid)) {
                mHandler.obtainMessage(H_PARTITION_FORGET, rec).sendToTarget();
            }
            mCallbacks.notifyVolumeForgotten(fsUuid);

            // If this had been primary storage, revert back to internal and
            // reset vold so we bind into new volume into place.
            if (Objects.equals(mPrimaryStorageUuid, fsUuid)) {
                mPrimaryStorageUuid = getDefaultPrimaryStorageUuid();
                mHandler.obtainMessage(H_RESET).sendToTarget();
            }

            writeSettingsLocked();
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS)
    @Override
    public void forgetAllVolumes() {

        super.forgetAllVolumes_enforcePermission();

        synchronized (mLock) {
            for (int i = 0; i < mRecords.size(); i++) {
                final String fsUuid = mRecords.keyAt(i);
                final VolumeRecord rec = mRecords.valueAt(i);
                if (!TextUtils.isEmpty(rec.partGuid)) {
                    mHandler.obtainMessage(H_PARTITION_FORGET, rec).sendToTarget();
                }
                mCallbacks.notifyVolumeForgotten(fsUuid);
            }
            mRecords.clear();

            if (!Objects.equals(StorageManager.UUID_PRIVATE_INTERNAL, mPrimaryStorageUuid)) {
                mPrimaryStorageUuid = getDefaultPrimaryStorageUuid();
            }

            writeSettingsLocked();
            mHandler.obtainMessage(H_RESET).sendToTarget();
        }
    }

    private void forgetPartition(String partGuid, String fsUuid) {
        try {
            mVold.forgetPartition(partGuid, fsUuid);
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS)
    @Override
    public void fstrim(int flags, IVoldTaskListener listener) {

        super.fstrim_enforcePermission();

        try {
            // Block based checkpoint process runs fstrim. So, if checkpoint is in progress
            // (first boot after OTA), We skip idle maintenance and make sure the last
            // fstrim time is still updated. If file based checkpoints are used, we run
            // idle maintenance (GC + fstrim) regardless of checkpoint status.
            if (!needsCheckpoint() || !supportsBlockCheckpoint()) {
                mVold.fstrim(flags, new IVoldTaskListener.Stub() {
                    @Override
                    public void onStatus(int status, PersistableBundle extras) {
                        dispatchOnStatus(listener, status, extras);

                        // Ignore trim failures
                        if (status != 0) return;

                        final String path = extras.getString("path");
                        final long bytes = extras.getLong("bytes");
                        final long time = extras.getLong("time");

                        final DropBoxManager dropBox = mContext.getSystemService(DropBoxManager.class);
                        dropBox.addText(TAG_STORAGE_TRIM, scrubPath(path) + " " + bytes + " " + time);

                        synchronized (mLock) {
                            final VolumeRecord rec = findRecordForPath(path);
                            if (rec != null) {
                                rec.lastTrimMillis = System.currentTimeMillis();
                                writeSettingsLocked();
                            }
                        }
                    }

                    @Override
                    public void onFinished(int status, PersistableBundle extras) {
                        dispatchOnFinished(listener, status, extras);

                        // TODO: benchmark when desired
                    }
                });
            } else {
                Slog.i(TAG, "Skipping fstrim - block based checkpoint in progress");
            }
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }
    }

    void runIdleMaint(Runnable callback) {
        enforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS);

        try {
            // Block based checkpoint process runs fstrim. So, if checkpoint is in progress
            // (first boot after OTA), We skip idle maintenance and make sure the last
            // fstrim time is still updated. If file based checkpoints are used, we run
            // idle maintenance (GC + fstrim) regardless of checkpoint status.
            if (!needsCheckpoint() || !supportsBlockCheckpoint()) {
                mVold.runIdleMaint(mNeedGC, new IVoldTaskListener.Stub() {
                    @Override
                    public void onStatus(int status, PersistableBundle extras) {
                        // Not currently used
                    }
                    @Override
                    public void onFinished(int status, PersistableBundle extras) {
                        if (callback != null) {
                            BackgroundThread.getHandler().post(callback);
                        }
                    }
                });
            } else {
                Slog.i(TAG, "Skipping idle maintenance - block based checkpoint in progress");
            }
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }
    }

    @Override
    public void runIdleMaintenance() {
        runIdleMaint(null);
    }

    void abortIdleMaint(Runnable callback) {
        enforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS);

        try {
            mVold.abortIdleMaint(new IVoldTaskListener.Stub() {
                @Override
                public void onStatus(int status, PersistableBundle extras) {
                    // Not currently used
                }
                @Override
                public void onFinished(int status, PersistableBundle extras) {
                    if (callback != null) {
                        BackgroundThread.getHandler().post(callback);
                    }
                }
            });
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }
    }

    @Override
    public void abortIdleMaintenance() {
        abortIdleMaint(null);
    }

    private boolean prepareSmartIdleMaint() {
        /**
         * We can choose whether going with a new storage smart idle maintenance job
         * or falling back to the traditional way using DeviceConfig
         */
        boolean smartIdleMaintEnabled = DeviceConfig.getBoolean(
            DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
            "smart_idle_maint_enabled",
                DEFAULT_SMART_IDLE_MAINT_ENABLED)
                && !SystemProperties.getBoolean("ro.boot.zufs_provisioned", false);
        if (smartIdleMaintEnabled) {
            mLifetimePercentThreshold = DeviceConfig.getInt(
                DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
                "lifetime_threshold", DEFAULT_LIFETIME_PERCENT_THRESHOLD);
            mMinSegmentsThreshold = DeviceConfig.getInt(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
                "min_segments_threshold", DEFAULT_MIN_SEGMENTS_THRESHOLD);
            mDirtyReclaimRate = DeviceConfig.getFloat(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
                "dirty_reclaim_rate", DEFAULT_DIRTY_RECLAIM_RATE);
            mSegmentReclaimWeight = DeviceConfig.getFloat(
                DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
                "segment_reclaim_weight", DEFAULT_SEGMENT_RECLAIM_WEIGHT);
            mLowBatteryLevel = DeviceConfig.getFloat(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
                "low_battery_level", DEFAULT_LOW_BATTERY_LEVEL);
            mChargingRequired = DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
                "charging_required", DEFAULT_CHARGING_REQUIRED);
            mMinGCSleepTime = DeviceConfig.getInt(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
                "min_gc_sleeptime", DEFAULT_MIN_GC_SLEEPTIME);
            mTargetDirtyRatio = DeviceConfig.getInt(DeviceConfig.NAMESPACE_STORAGE_NATIVE_BOOT,
                "target_dirty_ratio", DEFAULT_TARGET_DIRTY_RATIO);

            // If we use the smart idle maintenance, we need to turn off GC in the traditional idle
            // maintenance to avoid the conflict
            mNeedGC = false;

            loadStorageWriteRecords();
            try {
                mVold.refreshLatestWrite();
            } catch (Exception e) {
                Slog.wtf(TAG, e);
            }
            refreshLifetimeConstraint();
        }
        return smartIdleMaintEnabled;
    }

    // Return whether storage lifetime exceeds the threshold
    public boolean isPassedLifetimeThresh() {
        return mPassedLifetimeThresh;
    }

    private void loadStorageWriteRecords() {
        FileInputStream fis = null;

        try {
            fis = mWriteRecordFile.openRead();
            ObjectInputStream ois = new ObjectInputStream(fis);

            int periodValue = ois.readInt();
            if (periodValue == sSmartIdleMaintPeriod) {
                mStorageWriteRecords = (int[]) ois.readObject();
            }
        } catch (FileNotFoundException e) {
            // Missing data is okay, probably first boot
        } catch (Exception e) {
            Slog.wtf(TAG, "Failed reading write records", e);
        } finally {
            IoUtils.closeQuietly(fis);
        }
    }

    private int getAverageWriteAmount() {
        return Arrays.stream(mStorageWriteRecords).sum() / mMaxWriteRecords;
    }

    private void updateStorageWriteRecords(int latestWrite) {
        FileOutputStream fos = null;

        System.arraycopy(mStorageWriteRecords, 0, mStorageWriteRecords, 1,
                     mMaxWriteRecords - 1);
        mStorageWriteRecords[0] = latestWrite;
        try {
            fos = mWriteRecordFile.startWrite();
            ObjectOutputStream oos = new ObjectOutputStream(fos);

            oos.writeInt(sSmartIdleMaintPeriod);
            oos.writeObject(mStorageWriteRecords);
            mWriteRecordFile.finishWrite(fos);
        } catch (IOException e) {
            if (fos != null) {
                mWriteRecordFile.failWrite(fos);
            }
        }
    }

    private boolean checkChargeStatus() {
        IntentFilter ifilter = new IntentFilter(Intent.ACTION_BATTERY_CHANGED);
        Intent batteryStatus = mContext.registerReceiver(null, ifilter);

        if (mChargingRequired) {
            int status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1);
            if (status != BatteryManager.BATTERY_STATUS_CHARGING &&
                status != BatteryManager.BATTERY_STATUS_FULL) {
                Slog.w(TAG, "Battery is not being charged");
                return false;
            }
        }

        int level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1);
        int scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1);
        float chargePercent = level * 100f / (float)scale;

        if (chargePercent < mLowBatteryLevel) {
            Slog.w(TAG, "Battery level is " + chargePercent + ", which is lower than threshold: " +
                        mLowBatteryLevel);
            return false;
        }
        return true;
    }

    private boolean refreshLifetimeConstraint() {
        int storageLifeTime = 0;

        try {
            storageLifeTime = mVold.getStorageLifeTime();
        } catch (Exception e) {
            Slog.wtf(TAG, e);
            return false;
        }

        if (storageLifeTime == -1) {
            Slog.w(TAG, "Failed to get storage lifetime");
            return false;
        } else if (storageLifeTime > mLifetimePercentThreshold) {
            Slog.w(TAG, "Ended smart idle maintenance, because of lifetime(" + storageLifeTime +
                        ")" + ", lifetime threshold(" + mLifetimePercentThreshold + ")");
            mPassedLifetimeThresh = true;
            return false;
        }
        Slog.i(TAG, "Storage lifetime: " + storageLifeTime);
        return true;
    }

    synchronized void runSmartIdleMaint(Runnable callback) {
        enforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS);

        try {
            int avgWriteAmount = 0;
            int targetDirtyRatio = mTargetDirtyRatio;
            int latestWrite = mVold.getWriteAmount();
            if (latestWrite == -1) {
                Slog.w(TAG, "Failed to get storage write record");
                return;
            }

            updateStorageWriteRecords(latestWrite);

            // Block based checkpoint process runs fstrim. So, if checkpoint is in progress
            // (first boot after OTA), We skip the smart idle maintenance
            if (!needsCheckpoint() || !supportsBlockCheckpoint()) {
                if (!refreshLifetimeConstraint() || !checkChargeStatus()) {
                    Slog.i(TAG, "Turn off gc_urgent based on checking lifetime and charge status");
                    targetDirtyRatio = 100;
                } else {
                    avgWriteAmount = getAverageWriteAmount();
                }

                Slog.i(TAG, "Set smart idle maintenance: " + "latest write amount: " +
                            latestWrite + ", average write amount: " + avgWriteAmount +
                            ", min segment threshold: " + mMinSegmentsThreshold +
                            ", dirty reclaim rate: " + mDirtyReclaimRate +
                            ", segment reclaim weight: " + mSegmentReclaimWeight +
                            ", period(min): " + sSmartIdleMaintPeriod +
                            ", min gc sleep time(ms): " + mMinGCSleepTime +
                            ", target dirty ratio: " + targetDirtyRatio);
                mVold.setGCUrgentPace(avgWriteAmount, mMinSegmentsThreshold, mDirtyReclaimRate,
                                      mSegmentReclaimWeight, sSmartIdleMaintPeriod,
                                      mMinGCSleepTime, targetDirtyRatio);
            } else {
                Slog.i(TAG, "Skipping smart idle maintenance - block based checkpoint in progress");
            }
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        } finally {
            if (callback != null) {
                callback.run();
            }
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS)
    @Override
    public void setDebugFlags(int flags, int mask) {

        super.setDebugFlags_enforcePermission();

        if ((mask & (StorageManager.DEBUG_ADOPTABLE_FORCE_ON
                | StorageManager.DEBUG_ADOPTABLE_FORCE_OFF)) != 0) {
            final String value;
            if ((flags & StorageManager.DEBUG_ADOPTABLE_FORCE_ON) != 0) {
                value = "force_on";
            } else if ((flags & StorageManager.DEBUG_ADOPTABLE_FORCE_OFF) != 0) {
                value = "force_off";
            } else {
                value = "";
            }

            final long token = Binder.clearCallingIdentity();
            try {
                SystemProperties.set(StorageManager.PROP_ADOPTABLE, value);

                // Reset storage to kick new setting into place
                mHandler.obtainMessage(H_RESET).sendToTarget();
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }

        if ((mask & (StorageManager.DEBUG_SDCARDFS_FORCE_ON
                | StorageManager.DEBUG_SDCARDFS_FORCE_OFF)) != 0) {
            final String value;
            if ((flags & StorageManager.DEBUG_SDCARDFS_FORCE_ON) != 0) {
                value = "force_on";
            } else if ((flags & StorageManager.DEBUG_SDCARDFS_FORCE_OFF) != 0) {
                value = "force_off";
            } else {
                value = "";
            }

            final long token = Binder.clearCallingIdentity();
            try {
                SystemProperties.set(StorageManager.PROP_SDCARDFS, value);

                // Reset storage to kick new setting into place
                mHandler.obtainMessage(H_RESET).sendToTarget();
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }

        if ((mask & StorageManager.DEBUG_VIRTUAL_DISK) != 0) {
            final boolean enabled = (flags & StorageManager.DEBUG_VIRTUAL_DISK) != 0;

            final long token = Binder.clearCallingIdentity();
            try {
                SystemProperties.set(StorageManager.PROP_VIRTUAL_DISK, Boolean.toString(enabled));

                // Reset storage to kick new setting into place
                mHandler.obtainMessage(H_RESET).sendToTarget();
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }
    }

    @Override
    public String getPrimaryStorageUuid() {
        synchronized (mLock) {
            return mPrimaryStorageUuid;
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_UNMOUNT_FILESYSTEMS)
    @Override
    public void setPrimaryStorageUuid(String volumeUuid, IPackageMoveObserver callback) {

        super.setPrimaryStorageUuid_enforcePermission();

        final VolumeInfo from;
        final VolumeInfo to;
        final List<UserInfo> users;

        synchronized (mLock) {
            if (Objects.equals(mPrimaryStorageUuid, volumeUuid)) {
                throw new IllegalArgumentException("Primary storage already at " + volumeUuid);
            }

            if (mMoveCallback != null) {
                throw new IllegalStateException("Move already in progress");
            }
            mMoveCallback = callback;
            mMoveTargetUuid = volumeUuid;

            // We need all the users unlocked to move their primary storage
            users = mContext.getSystemService(UserManager.class).getUsers();
            for (UserInfo user : users) {
                if (StorageManager.isFileEncrypted() && !isCeStorageUnlocked(user.id)) {
                    Slog.w(TAG, "Failing move due to locked user " + user.id);
                    onMoveStatusLocked(PackageManager.MOVE_FAILED_LOCKED_USER);
                    return;
                }
            }

            // When moving to/from primary physical volume, we probably just nuked
            // the current storage location, so we have nothing to move.
            if (Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, mPrimaryStorageUuid)
                    || Objects.equals(StorageManager.UUID_PRIMARY_PHYSICAL, volumeUuid)) {
                Slog.d(TAG, "Skipping move to/from primary physical");
                onMoveStatusLocked(MOVE_STATUS_COPY_FINISHED);
                onMoveStatusLocked(PackageManager.MOVE_SUCCEEDED);
                mHandler.obtainMessage(H_RESET).sendToTarget();
                return;

            } else {
                int currentUserId = mCurrentUserId;
                from = findStorageForUuidAsUser(mPrimaryStorageUuid, currentUserId);
                to = findStorageForUuidAsUser(volumeUuid, currentUserId);

                if (from == null) {
                    Slog.w(TAG, "Failing move due to missing from volume " + mPrimaryStorageUuid);
                    onMoveStatusLocked(PackageManager.MOVE_FAILED_INTERNAL_ERROR);
                    return;
                } else if (to == null) {
                    Slog.w(TAG, "Failing move due to missing to volume " + volumeUuid);
                    onMoveStatusLocked(PackageManager.MOVE_FAILED_INTERNAL_ERROR);
                    return;
                }
            }
        }

        // Prepare the storage before move, this is required to unlock adoptable storage (as the
        // keys are tied to prepare user data step) & also is required for the destination files to
        // end up with the correct SELinux labels and encryption policies for directories
        try {
            prepareUserStorageForMoveInternal(mPrimaryStorageUuid, volumeUuid, users);
        } catch (Exception e) {
            Slog.w(TAG, "Failing move due to failure on prepare user data", e);
            synchronized (mLock) {
                onMoveStatusLocked(PackageManager.MOVE_FAILED_INTERNAL_ERROR);
            }
            return;
        }

        try {
            mVold.moveStorage(from.id, to.id, new IVoldTaskListener.Stub() {
                @Override
                public void onStatus(int status, PersistableBundle extras) {
                    synchronized (mLock) {
                        onMoveStatusLocked(status);
                    }
                }

                @Override
                public void onFinished(int status, PersistableBundle extras) {
                    // Not currently used
                }
            });
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }
    }

    private void warnOnNotMounted() {
        synchronized (mLock) {
            for (int i = 0; i < mVolumes.size(); i++) {
                final WatchedVolumeInfo vol = mVolumes.valueAt(i);
                if (vol.isPrimary() && vol.isMountedWritable()) {
                    // Cool beans, we have a mounted primary volume
                    return;
                }
            }
        }

        Slog.w(TAG, "No primary storage mounted!");
    }

    private boolean isUidOwnerOfPackageOrSystem(String packageName, int callerUid) {
        if (callerUid == android.os.Process.SYSTEM_UID) {
            return true;
        }

        return mPmInternal.isSameApp(packageName, callerUid, UserHandle.getUserId(callerUid));
    }

    @Override
    public String getMountedObbPath(String rawPath) {
        Objects.requireNonNull(rawPath, "rawPath cannot be null");

        warnOnNotMounted();

        final ObbState state;
        synchronized (mObbMounts) {
            state = mObbPathToStateMap.get(rawPath);
        }
        if (state == null) {
            Slog.w(TAG, "Failed to find OBB mounted at " + rawPath);
            return null;
        }

        return findVolumeByIdOrThrow(state.volId).getPath().getAbsolutePath();
    }

    @Override
    public boolean isObbMounted(String rawPath) {
        Objects.requireNonNull(rawPath, "rawPath cannot be null");
        synchronized (mObbMounts) {
            return mObbPathToStateMap.containsKey(rawPath);
        }
    }

    @Override
    public void mountObb(String rawPath, String canonicalPath, IObbActionListener token,
            int nonce, ObbInfo obbInfo) {
        Objects.requireNonNull(rawPath, "rawPath cannot be null");
        Objects.requireNonNull(canonicalPath, "canonicalPath cannot be null");
        Objects.requireNonNull(token, "token cannot be null");
        Objects.requireNonNull(obbInfo, "obbInfo cannot be null");

        validateObbInfo(obbInfo, rawPath);

        final int callingUid = Binder.getCallingUid();
        final ObbState obbState = new ObbState(rawPath, canonicalPath,
                callingUid, token, nonce, null);
        final ObbAction action = new MountObbAction(obbState, callingUid, obbInfo);
        mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage(OBB_RUN_ACTION, action));

        if (DEBUG_OBB)
            Slog.i(TAG, "Send to OBB handler: " + action.toString());
    }

    private void validateObbInfo(ObbInfo obbInfo, String rawPath) {
        String obbFilePath;
        try {
            obbFilePath = new File(rawPath).getCanonicalPath();
        } catch (IOException ex) {
            throw new RuntimeException("Failed to resolve path" + rawPath + " : " + ex);
        }

        Matcher matcher = OBB_FILE_PATH.matcher(obbFilePath);

        if (matcher.matches()) {
            int userId = UserHandle.getUserId(Binder.getCallingUid());
            String pathUserId = matcher.group(2);
            String pathPackageName = matcher.group(3);
            if ((pathUserId != null && Integer.parseInt(pathUserId) != userId)
                    || (pathUserId == null && userId != mCurrentUserId)) {
                throw new SecurityException(
                        "Path " + obbFilePath + "does not correspond to calling userId " + userId);
            }
            if (obbInfo != null && !obbInfo.packageName.equals(pathPackageName)) {
                throw new SecurityException("Path " + obbFilePath
                        + " does not contain package name " + pathPackageName);
            }
        } else {
            throw new SecurityException("Invalid path to Obb file : " + obbFilePath);
        }
    }

    @Override
    public void unmountObb(String rawPath, boolean force, IObbActionListener token, int nonce) {
        Objects.requireNonNull(rawPath, "rawPath cannot be null");

        final ObbState existingState;
        synchronized (mObbMounts) {
            existingState = mObbPathToStateMap.get(rawPath);
        }

        if (existingState != null) {
            // TODO: separate state object from request data
            final int callingUid = Binder.getCallingUid();
            final ObbState newState = new ObbState(rawPath, existingState.canonicalPath,
                    callingUid, token, nonce, existingState.volId);
            final ObbAction action = new UnmountObbAction(newState, force);
            mObbActionHandler.sendMessage(mObbActionHandler.obtainMessage(OBB_RUN_ACTION, action));

            if (DEBUG_OBB)
                Slog.i(TAG, "Send to OBB handler: " + action.toString());
        } else {
            Slog.w(TAG, "Unknown OBB mount at " + rawPath);
        }
    }

    /**
     * Check whether the device supports filesystem checkpointing.
     *
     * @return true if the device supports filesystem checkpointing, false otherwise.
     */
    @Override
    public boolean supportsCheckpoint() throws RemoteException {
        return mVold.supportsCheckpoint();
    }

    /**
     * Signal that checkpointing partitions should start a checkpoint on the next boot.
     *
     * @param numTries Number of times to try booting in checkpoint mode, before we will boot
     *                 non-checkpoint mode and commit all changes immediately. Callers are
     *                 responsible for ensuring that boot is safe (eg, by rolling back updates).
     */
    @Override
    public void startCheckpoint(int numTries) throws RemoteException {
        // Only the root, system_server and shell processes are permitted to start checkpoints
        final int callingUid = Binder.getCallingUid();
        if (callingUid != Process.SYSTEM_UID && callingUid != Process.ROOT_UID
                && callingUid != Process.SHELL_UID) {
            throw new SecurityException("no permission to start filesystem checkpoint");
        }

        mVold.startCheckpoint(numTries);
    }

    /**
     * Signal that checkpointing partitions should commit changes
     */
    @Override
    public void commitChanges() throws RemoteException {
        // Only the system process is permitted to commit checkpoints
        if (Binder.getCallingUid() != android.os.Process.SYSTEM_UID) {
            throw new SecurityException("no permission to commit checkpoint changes");
        }
        extendWatchdogTimeout("vold#commitChanges might be slow");
        mVold.commitChanges();
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.MOUNT_FORMAT_FILESYSTEMS)
    /**
     * Check if we should be mounting with checkpointing or are checkpointing now
     */
    @Override
    public boolean needsCheckpoint() throws RemoteException {
        super.needsCheckpoint_enforcePermission();

        return mVold.needsCheckpoint();
    }

    /**
     * Abort the current set of changes and either try again, or abort entirely
     */
    @Override
    public void abortChanges(String message, boolean retry) throws RemoteException {
        // Only the system process is permitted to abort checkpoints
        if (Binder.getCallingUid() != android.os.Process.SYSTEM_UID) {
            throw new SecurityException("no permission to commit checkpoint changes");
        }

        mVold.abortChanges(message, retry);
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.STORAGE_INTERNAL)
    @Override
    public void lockCeStorage(int userId) {
        super.lockCeStorage_enforcePermission();

        // Never lock the CE storage of a headless system user.
        if (userId == UserHandle.USER_SYSTEM
                && UserManager.isHeadlessSystemUserMode()) {
            throw new IllegalArgumentException("Headless system user data cannot be locked..");
        }

        if (!isCeStorageUnlocked(userId)) {
            Slog.d(TAG, "User " + userId + "'s CE storage is already locked");
            return;
        }

        try {
            mVold.lockCeStorage(userId);
        } catch (Exception e) {
            Slog.wtf(TAG, e);
            return;
        }

        synchronized (mLock) {
            mCeUnlockedUsers.remove(userId);
        }
    }

    @Override
    public boolean isCeStorageUnlocked(int userId) {
        synchronized (mLock) {
            return mCeUnlockedUsers.contains(userId);
        }
    }

    private boolean isSystemUnlocked(int userId) {
        synchronized (mLock) {
            return ArrayUtils.contains(mSystemUnlockedUsers, userId);
        }
    }

    private void prepareUserStorageIfNeeded(WatchedVolumeInfo vol) throws Exception {
        if (vol.getType() != VolumeInfo.TYPE_PRIVATE) {
            return;
        }

        final UserManager um = mContext.getSystemService(UserManager.class);
        final UserManagerInternal umInternal =
                LocalServices.getService(UserManagerInternal.class);

        for (UserInfo user : um.getUsers()) {
            final int flags;
            if (umInternal.isUserUnlockingOrUnlocked(user.id)) {
                flags = StorageManager.FLAG_STORAGE_DE | StorageManager.FLAG_STORAGE_CE;
            } else if (umInternal.isUserRunning(user.id)) {
                flags = StorageManager.FLAG_STORAGE_DE;
            } else {
                continue;
            }

            prepareUserStorageInternal(vol.getFsUuid(), user.id, flags);
        }
    }

    private void prepareUserStorageInternal(String volumeUuid, int userId, int flags)
            throws Exception {
        try {
            mVold.prepareUserStorage(volumeUuid, userId, flags);
            // After preparing user storage, we should check if we should mount data mirror again,
            // and we do it for user 0 only as we only need to do once for all users.
            if (volumeUuid != null) {
                final StorageManager storage = mContext.getSystemService(StorageManager.class);
                VolumeInfo info = storage.findVolumeByUuid(volumeUuid);
                if (info != null && userId == 0 && info.type == VolumeInfo.TYPE_PRIVATE) {
                    mInstaller.tryMountDataMirror(volumeUuid);
                }
            }
        } catch (Exception e) {
            EventLog.writeEvent(0x534e4554, "224585613", -1, "");
            Slog.wtf(TAG, e);
            // Make sure to re-throw this exception; we must not ignore failure
            // to prepare the user storage as it could indicate that encryption
            // wasn't successfully set up.
            //
            // Very unfortunately, these errors need to be ignored for broken
            // users that already existed on-disk from older Android versions.
            UserManagerInternal umInternal = LocalServices.getService(UserManagerInternal.class);
            if (umInternal.shouldIgnorePrepareStorageErrors(userId)) {
                Slog.wtf(TAG, "ignoring error preparing storage for existing user " + userId
                        + "; device may be insecure!");
                return;
            }
            throw e;
        }
    }

    @Override
    public void fixupAppDir(String path) {
        final Matcher matcher = KNOWN_APP_DIR_PATHS.matcher(path);
        if (matcher.matches()) {
            if (matcher.group(2) == null) {
                Log.e(TAG, "Asked to fixup an app dir without a userId: " + path);
                return;
            }
            try {
                int userId = Integer.parseInt(matcher.group(2));
                String packageName = matcher.group(3);
                int uid = mContext.getPackageManager().getPackageUidAsUser(packageName, userId);
                try {
                    mVold.fixupAppDir(path + "/", uid);
                } catch (RemoteException | ServiceSpecificException e) {
                    Log.e(TAG, "Failed to fixup app dir for " + packageName, e);
                }
            } catch (NumberFormatException e) {
                Log.e(TAG, "Invalid userId in path: " + path, e);
            } catch (PackageManager.NameNotFoundException e) {
                Log.e(TAG, "Couldn't find package to fixup app dir " + path, e);
            }
        } else {
            Log.e(TAG, "Path " + path + " is not a valid application-specific directory");
        }
    }

    /*
     * Disable storage's app data isolation for testing.
     */
    @Override
    public void disableAppDataIsolation(String pkgName, int pid, int userId) {
        final int callingUid = Binder.getCallingUid();
        if (callingUid != Process.ROOT_UID && callingUid != Process.SHELL_UID) {
            throw new SecurityException("no permission to enable app visibility");
        }
        final String[] sharedPackages =
                mPmInternal.getSharedUserPackagesForPackage(pkgName, userId);
        final int uid = mPmInternal.getPackageUid(pkgName, 0, userId);
        final String[] packages =
                sharedPackages.length != 0 ? sharedPackages : new String[]{pkgName};
        try {
            mVold.unmountAppStorageDirs(uid, pid, packages);
        } catch (RemoteException e) {
            throw e.rethrowAsRuntimeException();
        }
    }

    /**
     * Returns PendingIntent which can be used by Apps with MANAGE_EXTERNAL_STORAGE permission
     * to launch the manageSpaceActivity of the App specified by packageName.
     */
    @Override
    @Nullable
    public PendingIntent getManageSpaceActivityIntent(
            @NonNull String packageName, int requestCode) {
        // Only Apps with MANAGE_EXTERNAL_STORAGE permission which have package visibility for
        // packageName should be able to call this API.
        int originalUid = Binder.getCallingUidOrThrow();
        try {
            // Get package name for calling app and verify it has MANAGE_EXTERNAL_STORAGE permission
            final String[] packagesFromUid = mIPackageManager.getPackagesForUid(originalUid);
            if (packagesFromUid == null) {
                throw new SecurityException("Unknown uid " + originalUid);
            }
            // Checking first entry in packagesFromUid is enough as using "sharedUserId"
            // mechanism is rare and discouraged. Also, Apps that share same UID share the same
            // permissions.
            if (!mStorageManagerInternal.hasExternalStorageAccess(originalUid,
                    packagesFromUid[0])) {
                throw new SecurityException("Only File Manager Apps permitted");
            }
        } catch (RemoteException re) {
            throw new SecurityException("Unknown uid " + originalUid, re);
        }

        ApplicationInfo appInfo;
        try {
            appInfo = mIPackageManager.getApplicationInfo(packageName, 0,
                    UserHandle.getUserId(originalUid));
            if (appInfo == null) {
                throw new IllegalArgumentException(
                        "Invalid packageName");
            }
            if (appInfo.manageSpaceActivityName == null) {
                Log.i(TAG, packageName + " doesn't have a manageSpaceActivity");
                return null;
            }
        } catch (RemoteException e) {
            throw new SecurityException("Only File Manager Apps permitted");
        }

        // We want to call the manageSpaceActivity as a SystemService and clear identity
        // of the calling App
        final long token = Binder.clearCallingIdentity();
        try {
            Context targetAppContext = mContext.createPackageContextAsUser(packageName,
                    /* flags= */ 0, UserHandle.of(UserHandle.getUserId(originalUid)));
            Intent intent = new Intent(Intent.ACTION_DEFAULT);
            intent.setClassName(packageName,
                    appInfo.manageSpaceActivityName);
            intent.setFlags(FLAG_ACTIVITY_NEW_TASK);

            final ActivityOptions options = ActivityOptions.makeBasic()
                    .setPendingIntentCreatorBackgroundActivityStartMode(
                            ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_DENIED);

            PendingIntent activity = PendingIntent.getActivity(targetAppContext, requestCode,
                    intent,
                    FLAG_ONE_SHOT | FLAG_CANCEL_CURRENT | FLAG_IMMUTABLE, options.toBundle());
            return activity;
        } catch (PackageManager.NameNotFoundException e) {
            throw new IllegalArgumentException(
                    "packageName not found");
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    @Override
    public void notifyAppIoBlocked(String volumeUuid, int uid, int tid, int reason) {
        enforceExternalStorageService();

        mStorageSessionController.notifyAppIoBlocked(volumeUuid, uid, tid, reason);
    }

    @Override
    public void notifyAppIoResumed(String volumeUuid, int uid, int tid, int reason) {
        enforceExternalStorageService();

        mStorageSessionController.notifyAppIoResumed(volumeUuid, uid, tid, reason);
    }

    @Override
    public boolean isAppIoBlocked(String volumeUuid, int uid, int tid,
            @StorageManager.AppIoBlockedReason int reason) {
        return isAppIoBlocked(uid);
    }

    private boolean isAppIoBlocked(int uid) {
        return mStorageSessionController.isAppIoBlocked(uid);
    }

    @Override
    public void setCloudMediaProvider(@Nullable String authority) {
        enforceExternalStorageService();

        final int userId = UserHandle.getUserId(Binder.getCallingUid());
        synchronized (mCloudMediaProviders) {
            final String oldAuthority = mCloudMediaProviders.get(userId);
            if (!Objects.equals(authority, oldAuthority)) {
                mCloudMediaProviders.put(userId, authority);
                mHandler.obtainMessage(H_CLOUD_MEDIA_PROVIDER_CHANGED, userId, 0, authority)
                        .sendToTarget();
            }
        }
    }

    @Override
    @Nullable
    public String getCloudMediaProvider() {
        final int callingUid = Binder.getCallingUid();
        final int userId = UserHandle.getUserId(callingUid);
        final String authority;
        synchronized (mCloudMediaProviders) {
            authority = mCloudMediaProviders.get(userId);
        }
        if (authority == null) {
            return null;
        }
        final ProviderInfo pi = mPmInternal.resolveContentProvider(
                authority, 0, userId, callingUid);
        if (pi == null
                || mPmInternal.filterAppAccess(pi.packageName, callingUid, userId)) {
            return null;
        }
        return authority;
    }

    @Override
    public long getInternalStorageBlockDeviceSize() throws RemoteException {
        if (mInternalStorageSize == 0) {
            mInternalStorageSize = mVold.getStorageSize();
        }

        return mInternalStorageSize;
    }

    @Override
    public int getInternalStorageRemainingLifetime() throws RemoteException {
        PermissionEnforcer.fromContext(mContext)
            .enforcePermissionAnyOf(
                new String[] {
                    android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE,
                    android.Manifest.permission.ALLOCATE_AGGRESSIVE
                },
                getCallingPid(),
                getCallingUid());
        return mVold.getStorageRemainingLifetime();
    }

    /**
     * Enforces that the caller is the {@link ExternalStorageService}
     *
     * @throws SecurityException if the caller doesn't have the
     * {@link android.Manifest.permission.WRITE_MEDIA_STORAGE} permission or is not the
     * {@link ExternalStorageService}
     */
    private void enforceExternalStorageService() {
        enforcePermission(android.Manifest.permission.WRITE_MEDIA_STORAGE);
        int callingAppId = UserHandle.getAppId(Binder.getCallingUid());
        if (callingAppId != mMediaStoreAuthorityAppId) {
            throw new SecurityException("Only the ExternalStorageService is permitted");
        }
    }

    /** Not thread safe */
    class AppFuseMountScope extends AppFuseBridge.MountScope {
        private boolean mMounted = false;

        public AppFuseMountScope(int uid, int mountId) {
            super(uid, mountId);
        }

        @Override
        public ParcelFileDescriptor open() throws AppFuseMountException {
            extendWatchdogTimeout("#open might be slow");
            try {
                final FileDescriptor fd = mVold.mountAppFuse(uid, mountId);
                mMounted = true;
                return new ParcelFileDescriptor(fd);
            } catch (Exception e) {
                throw new AppFuseMountException("Failed to mount", e);
            }
        }

        @Override
        public ParcelFileDescriptor openFile(int mountId, int fileId, int flags)
                throws AppFuseMountException {
            extendWatchdogTimeout("#openFile might be slow");
            try {
                return new ParcelFileDescriptor(
                        mVold.openAppFuseFile(uid, mountId, fileId, flags));
            } catch (Exception e) {
                throw new AppFuseMountException("Failed to open", e);
            }
        }

        @Override
        public void close() throws Exception {
            extendWatchdogTimeout("#close might be slow");
            if (mMounted) {
                BackgroundThread.getHandler().post(() -> {
                    try {
                        // We need to run the unmount on a separate thread to
                        // prevent a possible deadlock, where:
                        // 1. AppFuseThread (this thread) tries to call into vold
                        // 2. the vold lock is held by another thread, which called:
                        //    mVold.openAppFuseFile()
                        //    as part of that call, vold calls open() on the
                        //    underlying file, which is a call that needs to be
                        //    handled by the AppFuseThread, which is stuck waiting
                        //    for the vold lock (see 1.)
                        // It is safe to do the unmount asynchronously, because the mount
                        // path we use is never reused during the current boot cycle;
                        // see mNextAppFuseName. Also,we have anyway stopped serving
                        // requests at this point.
                        mVold.unmountAppFuse(uid, mountId);
                    } catch (RemoteException e) {
                        throw e.rethrowAsRuntimeException();
                    }
                });
                mMounted = false;
            }
        }
    }

    @Override
    public @Nullable AppFuseMount mountProxyFileDescriptorBridge() {
        Slog.v(TAG, "mountProxyFileDescriptorBridge");
        final int uid = Binder.getCallingUid();

        while (true) {
            synchronized (mAppFuseLock) {
                boolean newlyCreated = false;
                if (mAppFuseBridge == null) {
                    mAppFuseBridge = new AppFuseBridge();
                    new Thread(mAppFuseBridge, AppFuseBridge.TAG).start();
                    newlyCreated = true;
                }
                try {
                    final int name = mNextAppFuseName++;
                    try {
                        return new AppFuseMount(
                            name, mAppFuseBridge.addBridge(new AppFuseMountScope(uid, name)));
                    } catch (FuseUnavailableMountException e) {
                        if (newlyCreated) {
                            // If newly created bridge fails, it's a real error.
                            Slog.e(TAG, "", e);
                            return null;
                        }
                        // It seems the thread of mAppFuseBridge has already been terminated.
                        mAppFuseBridge = null;
                    }
                } catch (AppFuseMountException e) {
                    throw e.rethrowAsParcelableException();
                }
            }
        }
    }

    @Override
    public @Nullable ParcelFileDescriptor openProxyFileDescriptor(
            int mountId, int fileId, int mode) {
        Slog.v(TAG, "mountProxyFileDescriptor");

        // We only support a narrow set of incoming mode flags
        mode &= MODE_READ_WRITE;

        try {
            synchronized (mAppFuseLock) {
                if (mAppFuseBridge == null) {
                    Slog.e(TAG, "FuseBridge has not been created");
                    return null;
                }
                return mAppFuseBridge.openFile(mountId, fileId, mode);
            }
        } catch (FuseUnavailableMountException | InterruptedException error) {
            Slog.v(TAG, "The mount point has already been invalid", error);
            return null;
        }
    }

    @Override
    public void mkdirs(String callingPkg, String appPath) {
        final int callingUid = Binder.getCallingUid();
        final int userId = UserHandle.getUserId(callingUid);
        final String propertyName = "sys.user." + userId + ".ce_available";

        // Ignore requests to create directories while CE storage is locked
        if (!isCeStorageUnlocked(userId)) {
            throw new IllegalStateException("Failed to prepare " + appPath);
        }

        // Ignore requests to create directories if CE storage is not available
        if ((userId == UserHandle.USER_SYSTEM)
                && !SystemProperties.getBoolean(propertyName, false)) {
            throw new IllegalStateException("Failed to prepare " + appPath);
        }

        // Validate that reported package name belongs to caller
        final AppOpsManager appOps = (AppOpsManager) mContext.getSystemService(
                Context.APP_OPS_SERVICE);
        appOps.checkPackage(callingUid, callingPkg);

        try {
            final PackageManager.Property noAppStorageProp = mContext.getPackageManager()
                    .getPropertyAsUser(PackageManager.PROPERTY_NO_APP_DATA_STORAGE, callingPkg,
                            null /* className */, userId);
            if (noAppStorageProp != null && noAppStorageProp.getBoolean()) {
                throw new SecurityException(callingPkg + " should not have " + appPath);
            }
        } catch (PackageManager.NameNotFoundException ignore) {
            // Property not found
        }

        File appFile = null;
        try {
            appFile = new File(appPath).getCanonicalFile();
        } catch (IOException e) {
            throw new IllegalStateException("Failed to resolve " + appPath + ": " + e);
        }

        appPath = appFile.getAbsolutePath();
        if (!appPath.endsWith("/")) {
            appPath = appPath + "/";
        }
        // Ensure that the path we're asked to create is a known application directory
        // path.
        final Matcher matcher = KNOWN_APP_DIR_PATHS.matcher(appPath);
        if (matcher.matches()) {
            // And that the package dir matches the calling package
            if (!matcher.group(3).equals(callingPkg)) {
                throw new SecurityException("Invalid mkdirs path: " + appFile
                        + " does not contain calling package " + callingPkg);
            }
            // And that the user id part of the path (if any) matches the calling user id,
            // or if for a public volume (no user id), the user matches the current user
            if ((matcher.group(2) != null && !matcher.group(2).equals(Integer.toString(userId)))
                    || (matcher.group(2) == null && userId != mCurrentUserId)) {
                throw new SecurityException("Invalid mkdirs path: " + appFile
                        + " does not match calling user id " + userId);
            }
            try {
                mVold.setupAppDir(appPath, callingUid);
            } catch (RemoteException e) {
                throw new IllegalStateException("Failed to prepare " + appPath + ": " + e);
            }

            return;
        }
        throw new SecurityException("Invalid mkdirs path: " + appFile
                + " is not a known app path.");
    }

    @Override
    public StorageVolume[] getVolumeList(int userId, String callingPackage, int flags) {
        final int callingUid = Binder.getCallingUid();
        final int callingUserId = UserHandle.getUserId(callingUid);

        if (!isUidOwnerOfPackageOrSystem(callingPackage, callingUid)) {
            throw new SecurityException("callingPackage does not match UID");
        }
        if (callingUserId != userId) {
            // Callers can ask for volumes of different users, but only with the correct permissions
            mContext.enforceCallingOrSelfPermission(
                    android.Manifest.permission.INTERACT_ACROSS_USERS,
                    "Need INTERACT_ACROSS_USERS to get volumes for another user");
        }

        final boolean forWrite = (flags & StorageManager.FLAG_FOR_WRITE) != 0;
        final boolean realState = (flags & StorageManager.FLAG_REAL_STATE) != 0;
        final boolean includeInvisible = (flags & StorageManager.FLAG_INCLUDE_INVISIBLE) != 0;
        final boolean includeRecent = (flags & StorageManager.FLAG_INCLUDE_RECENT) != 0;
        final boolean includeSharedProfile =
                (flags & StorageManager.FLAG_INCLUDE_SHARED_PROFILE) != 0;

        // When the caller is the app actually hosting external storage, we
        // should never attempt to augment the actual storage volume state,
        // otherwise we risk confusing it with race conditions as users go
        // through various unlocked states
        final boolean callerIsMediaStore = UserHandle.isSameApp(callingUid,
                mMediaStoreAuthorityAppId);

        // Only Apps with MANAGE_EXTERNAL_STORAGE should call the API with includeSharedProfile
        if (includeSharedProfile) {
            try {
                // Get package name for calling app and
                // verify it has MANAGE_EXTERNAL_STORAGE permission
                final String[] packagesFromUid = mIPackageManager.getPackagesForUid(callingUid);
                if (packagesFromUid == null) {
                    throw new SecurityException("Unknown uid " + callingUid);
                }
                // Checking first entry in packagesFromUid is enough as using "sharedUserId"
                // mechanism is rare and discouraged. Also, Apps that share same UID share the same
                // permissions.
                // Allowing Media Provider is an exception, Media Provider process should be allowed
                // to query users across profiles, even without MANAGE_EXTERNAL_STORAGE access.
                // Note that ordinarily Media provider process has the above permission, but if they
                // are revoked, Storage Volume(s) should still be returned.
                if (!callerIsMediaStore
                        && !mStorageManagerInternal.hasExternalStorageAccess(callingUid,
                                packagesFromUid[0])) {
                    throw new SecurityException("Only File Manager Apps permitted");
                }
            } catch (RemoteException re) {
                throw new SecurityException("Unknown uid " + callingUid, re);
            }
        }

        // Report all volumes as unmounted until we've recorded that user 0 has unlocked. There
        // are no guarantees that callers will see a consistent view of the volume before that
        // point
        final boolean systemUserUnlocked = isSystemUnlocked(UserHandle.USER_SYSTEM);

        final boolean userIsDemo;
        final boolean storagePermission;
        final boolean ceStorageUnlocked;
        final long token = Binder.clearCallingIdentity();
        try {
            userIsDemo = LocalServices.getService(UserManagerInternal.class)
                    .getUserInfo(userId).isDemo();
            storagePermission = mStorageManagerInternal.hasExternalStorage(callingUid,
                    callingPackage);
            ceStorageUnlocked = isCeStorageUnlocked(userId);
        } finally {
            Binder.restoreCallingIdentity(token);
        }

        boolean foundPrimary = false;

        final ArrayList<StorageVolume> res = new ArrayList<>();
        final ArraySet<String> resUuids = new ArraySet<>();
        final int userIdSharingMedia = mUserSharesMediaWith.get(userId, -1);
        synchronized (mLock) {
            for (int i = 0; i < mVolumes.size(); i++) {
                final String volId = mVolumes.keyAt(i);
                final WatchedVolumeInfo vol = mVolumes.valueAt(i);
                switch (vol.getType()) {
                    case VolumeInfo.TYPE_PUBLIC:
                    case VolumeInfo.TYPE_STUB:
                        break;
                    case VolumeInfo.TYPE_EMULATED:
                        if (vol.getMountUserId() == userId) {
                            break;
                        }
                        if (includeSharedProfile && vol.getMountUserId() == userIdSharingMedia) {
                            // If the volume belongs to a user we share media with,
                            // return it too.
                            break;
                        }
                        // Skip if emulated volume not for userId
                        continue;
                    default:
                        continue;
                }

                boolean match = false;
                if (forWrite) {
                    match = vol.isVisibleForWrite(userId)
                            || (includeSharedProfile && vol.isVisibleForWrite(userIdSharingMedia));
                } else {
                    // Return both read only and write only volumes. When includeSharedProfile is
                    // true, all the volumes of userIdSharingMedia should be returned when queried
                    // from the user it shares media with
                    // Public Volumes will be also be returned if visible to the
                    // userIdSharingMedia with.
                    match = vol.isVisibleForUser(userId)
                            || (!vol.isVisible() && includeInvisible && vol.getPath() != null)
                            || (vol.getType() == VolumeInfo.TYPE_PUBLIC
                                    && vol.isVisibleForUser(userIdSharingMedia))
                            || (includeSharedProfile && vol.isVisibleForUser(userIdSharingMedia));
                }
                if (!match) continue;

                boolean reportUnmounted = false;
                if (callerIsMediaStore) {
                    // When the caller is the app actually hosting external storage, we
                    // should never attempt to augment the actual storage volume state,
                    // otherwise we risk confusing it with race conditions as users go
                    // through various unlocked states
                } else if (!systemUserUnlocked) {
                    reportUnmounted = true;
                    Slog.w(TAG, "Reporting " + volId + " unmounted due to system locked");
                } else if ((vol.getType() == VolumeInfo.TYPE_EMULATED) && !ceStorageUnlocked) {
                    reportUnmounted = true;
                    Slog.w(TAG, "Reporting " + volId + "unmounted due to " + userId + " locked");
                } else if (!storagePermission && !realState) {
                    Slog.w(TAG, "Reporting " + volId + "unmounted due to missing permissions");
                    reportUnmounted = true;
                }

                int volUserId = userId;
                if (volUserId != vol.getMountUserId() && vol.getMountUserId() >= 0) {
                    volUserId = vol.getMountUserId();
                }
                final StorageVolume userVol = vol.buildStorageVolume(mContext, volUserId,
                        reportUnmounted);
                if (vol.isPrimary() && vol.getMountUserId() == userId) {
                    res.add(0, userVol);
                    foundPrimary = true;
                } else {
                    res.add(userVol);
                }
                resUuids.add(userVol.getUuid());
            }

            if (includeRecent) {
                final long lastWeek = System.currentTimeMillis() - DateUtils.WEEK_IN_MILLIS;
                for (int i = 0; i < mRecords.size(); i++) {
                    final VolumeRecord rec = mRecords.valueAt(i);

                    // Skip if we've already included it above
                    if (resUuids.contains(rec.fsUuid)) continue;

                    // Treat as recent if mounted within the last week
                    if (rec.lastSeenMillis > 0 && rec.lastSeenMillis >= lastWeek) {
                        final StorageVolume userVol = rec.buildStorageVolume(mContext);
                        res.add(userVol);
                        resUuids.add(userVol.getUuid());
                    }
                }
            }
        }

        // Synthesize a volume for preloaded media under demo users, so that
        // it's scanned into MediaStore
        if (userIsDemo) {
            final String id = "demo";
            final File path = Environment.getDataPreloadsMediaDirectory();
            final boolean primary = false;
            final boolean removable = false;
            final boolean emulated = true;
            final boolean externallyManaged = false;
            final boolean allowMassStorage = false;
            final long maxFileSize = 0;
            final UserHandle user = new UserHandle(userId);
            final String envState = Environment.MEDIA_MOUNTED_READ_ONLY;
            final String description = mContext.getString(android.R.string.unknownName);

            res.add(new StorageVolume(id, path, path, description, primary, removable, emulated,
                    externallyManaged, allowMassStorage, maxFileSize, user, null /*uuid */, id,
                    envState));
        }

        if (!foundPrimary) {
            Slog.w(TAG, "No primary storage defined yet; hacking together a stub");

            final boolean primaryPhysical = SystemProperties.getBoolean(
                    StorageManager.PROP_PRIMARY_PHYSICAL, false);

            final String id = "stub_primary";
            final File path = Environment.getLegacyExternalStorageDirectory();
            final String description = mContext.getString(android.R.string.unknownName);
            final boolean primary = true;
            final boolean removable = primaryPhysical;
            final boolean emulated = !primaryPhysical;
            final boolean externallyManaged = false;
            final boolean allowMassStorage = false;
            final long maxFileSize = 0L;
            final UserHandle owner = new UserHandle(userId);
            final String fsUuid = null;
            final UUID uuid = null;
            final String state = Environment.MEDIA_REMOVED;

            res.add(0, new StorageVolume(id, path, path,
                    description, primary, removable, emulated, externallyManaged,
                    allowMassStorage, maxFileSize, owner, uuid, fsUuid, state));
        }

        return res.toArray(new StorageVolume[res.size()]);
    }

    @Override
    public DiskInfo[] getDisks() {
        synchronized (mLock) {
            final DiskInfo[] res = new DiskInfo[mDisks.size()];
            for (int i = 0; i < mDisks.size(); i++) {
                res[i] = mDisks.valueAt(i);
            }
            return res;
        }
    }

    @Override
    public VolumeInfo[] getVolumes(int flags) {
        synchronized (mLock) {
            final VolumeInfo[] res = new VolumeInfo[mVolumes.size()];
            for (int i = 0; i < mVolumes.size(); i++) {
                res[i] = mVolumes.valueAt(i).getVolumeInfo();
            }
            return res;
        }
    }

    @Override
    public VolumeRecord[] getVolumeRecords(int flags) {
        synchronized (mLock) {
            final VolumeRecord[] res = new VolumeRecord[mRecords.size()];
            for (int i = 0; i < mRecords.size(); i++) {
                res[i] = mRecords.valueAt(i);
            }
            return res;
        }
    }

    @Override
    public long getCacheQuotaBytes(String volumeUuid, int uid) {
        if (uid != Binder.getCallingUid()) {
            mContext.enforceCallingPermission(android.Manifest.permission.STORAGE_INTERNAL, TAG);
        }
        final long token = Binder.clearCallingIdentity();
        try {
            final StorageStatsManager stats = mContext.getSystemService(StorageStatsManager.class);
            return stats.getCacheQuotaBytes(volumeUuid, uid);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    @Override
    public long getCacheSizeBytes(String volumeUuid, int uid) {
        if (uid != Binder.getCallingUid()) {
            mContext.enforceCallingPermission(android.Manifest.permission.STORAGE_INTERNAL, TAG);
        }
        final long token = Binder.clearCallingIdentity();
        try {
            return mContext.getSystemService(StorageStatsManager.class)
                    .queryStatsForUid(volumeUuid, uid).getCacheBytes();
        } catch (IOException e) {
            throw new ParcelableException(e);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    private int adjustAllocateFlags(int flags, int callingUid, String callingPackage) {
        // Require permission to allocate aggressively
        if ((flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0) {
            mContext.enforceCallingOrSelfPermission(
                    android.Manifest.permission.ALLOCATE_AGGRESSIVE, TAG);
        }

        // Apps normally can't directly defy reserved space
        flags &= ~StorageManager.FLAG_ALLOCATE_DEFY_ALL_RESERVED;
        flags &= ~StorageManager.FLAG_ALLOCATE_DEFY_HALF_RESERVED;

        // However, if app is actively using the camera, then we're willing to
        // clear up to half of the reserved cache space, since the user might be
        // trying to capture an important memory.
        final AppOpsManager appOps = mContext.getSystemService(AppOpsManager.class);
        final long token = Binder.clearCallingIdentity();
        try {
            if (appOps.isOperationActive(AppOpsManager.OP_CAMERA, callingUid, callingPackage)) {
                Slog.d(TAG, "UID " + callingUid + " is actively using camera;"
                        + " letting them defy reserved cached data");
                flags |= StorageManager.FLAG_ALLOCATE_DEFY_HALF_RESERVED;
            }
        } finally {
            Binder.restoreCallingIdentity(token);
        }

        return flags;
    }

    @Override
    public long getAllocatableBytes(String volumeUuid, int flags, String callingPackage) {
        flags = adjustAllocateFlags(flags, Binder.getCallingUid(), callingPackage);

        final StorageManager storage = mContext.getSystemService(StorageManager.class);
        final StorageStatsManager stats = mContext.getSystemService(StorageStatsManager.class);
        final long token = Binder.clearCallingIdentity();
        try {
            // In general, apps can allocate as much space as they want, except
            // we never let them eat into either the minimum cache space or into
            // the low disk warning space. To avoid user confusion, this logic
            // should be kept in sync with getFreeBytes().
            final File path = storage.findPathForUuid(volumeUuid);

            long usable = 0;
            long lowReserved = 0;
            long fullReserved = 0;
            long cacheClearable = 0;

            if ((flags & StorageManager.FLAG_ALLOCATE_CACHE_ONLY) == 0) {
                usable = path.getUsableSpace();
                lowReserved = storage.getStorageLowBytes(path);
                fullReserved = storage.getStorageFullBytes(path);
            }

            if ((flags & StorageManager.FLAG_ALLOCATE_NON_CACHE_ONLY) == 0
                    && stats.isQuotaSupported(volumeUuid)) {
                final long cacheTotal = stats.getCacheBytes(volumeUuid);
                final long cacheReserved = storage.getStorageCacheBytes(path, flags);
                cacheClearable = Math.max(0, cacheTotal - cacheReserved);
            }

            if ((flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0) {
                return Math.max(0, (usable + cacheClearable) - fullReserved);
            } else {
                return Math.max(0, (usable + cacheClearable) - lowReserved);
            }
        } catch (IOException e) {
            throw new ParcelableException(e);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    @Override
    public void allocateBytes(String volumeUuid, long bytes, int flags, String callingPackage) {
        flags = adjustAllocateFlags(flags, Binder.getCallingUid(), callingPackage);

        final long allocatableBytes = getAllocatableBytes(volumeUuid,
                flags | StorageManager.FLAG_ALLOCATE_NON_CACHE_ONLY, callingPackage);
        if (bytes > allocatableBytes) {
            // If we don't have room without taking cache into account, check to see if we'd have
            // room if we included freeable cache space.
            final long cacheClearable = getAllocatableBytes(volumeUuid,
                    flags | StorageManager.FLAG_ALLOCATE_CACHE_ONLY, callingPackage);
            if (bytes > allocatableBytes + cacheClearable) {
                throw new ParcelableException(new IOException("Failed to allocate " + bytes
                    + " because only " + (allocatableBytes + cacheClearable) + " allocatable"));
            }
        }

        final StorageManager storage = mContext.getSystemService(StorageManager.class);
        final long token = Binder.clearCallingIdentity();
        try {
            // Free up enough disk space to satisfy both the requested allocation
            // and our low disk warning space.
            final File path = storage.findPathForUuid(volumeUuid);
            if ((flags & StorageManager.FLAG_ALLOCATE_AGGRESSIVE) != 0) {
                bytes += storage.getStorageFullBytes(path);
            } else {
                bytes += storage.getStorageLowBytes(path);
            }

            mPmInternal.freeStorage(volumeUuid, bytes, flags);
        } catch (IOException e) {
            throw new ParcelableException(e);
        } finally {
            Binder.restoreCallingIdentity(token);
        }
    }

    private void addObbStateLocked(ObbState obbState) throws RemoteException {
        final IBinder binder = obbState.getBinder();
        List<ObbState> obbStates = mObbMounts.get(binder);

        if (obbStates == null) {
            obbStates = new ArrayList<ObbState>();
            mObbMounts.put(binder, obbStates);
        } else {
            for (final ObbState o : obbStates) {
                if (o.rawPath.equals(obbState.rawPath)) {
                    throw new IllegalStateException("Attempt to add ObbState twice. "
                            + "This indicates an error in the StorageManagerService logic.");
                }
            }
        }

        obbStates.add(obbState);
        try {
            obbState.link();
        } catch (RemoteException e) {
            /*
             * The binder died before we could link it, so clean up our state
             * and return failure.
             */
            obbStates.remove(obbState);
            if (obbStates.isEmpty()) {
                mObbMounts.remove(binder);
            }

            // Rethrow the error so mountObb can get it
            throw e;
        }

        mObbPathToStateMap.put(obbState.rawPath, obbState);
    }

    private void removeObbStateLocked(ObbState obbState) {
        final IBinder binder = obbState.getBinder();
        final List<ObbState> obbStates = mObbMounts.get(binder);
        if (obbStates != null) {
            if (obbStates.remove(obbState)) {
                obbState.unlink();
            }
            if (obbStates.isEmpty()) {
                mObbMounts.remove(binder);
            }
        }

        mObbPathToStateMap.remove(obbState.rawPath);
    }

    private class ObbActionHandler extends Handler {

        ObbActionHandler(Looper l) {
            super(l);
        }

        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case OBB_RUN_ACTION: {
                    final ObbAction action = (ObbAction) msg.obj;

                    if (DEBUG_OBB)
                        Slog.i(TAG, "OBB_RUN_ACTION: " + action.toString());

                    action.execute(this);
                    break;
                }
                case OBB_FLUSH_MOUNT_STATE: {
                    final String path = (String) msg.obj;

                    if (DEBUG_OBB)
                        Slog.i(TAG, "Flushing all OBB state for path " + path);

                    synchronized (mObbMounts) {
                        final List<ObbState> obbStatesToRemove = new ArrayList<>();

                        final Iterator<ObbState> i = mObbPathToStateMap.values().iterator();
                        while (i.hasNext()) {
                            final ObbState state = i.next();

                            /*
                             * If this entry's source file is in the volume path
                             * that got unmounted, remove it because it's no
                             * longer valid.
                             */
                            if (state.canonicalPath.startsWith(path)) {
                                obbStatesToRemove.add(state);
                            }
                        }

                        for (final ObbState obbState : obbStatesToRemove) {
                            if (DEBUG_OBB)
                                Slog.i(TAG, "Removing state for " + obbState.rawPath);

                            removeObbStateLocked(obbState);

                            try {
                                obbState.token.onObbResult(obbState.rawPath, obbState.nonce,
                                        OnObbStateChangeListener.UNMOUNTED);
                            } catch (RemoteException e) {
                                Slog.i(TAG, "Couldn't send unmount notification for  OBB: "
                                        + obbState.rawPath);
                            }
                        }
                    }
                    break;
                }
            }
        }
    }

    private static class ObbException extends Exception {
        public final int status;

        public ObbException(int status, String message) {
            super(message);
            this.status = status;
        }

        public ObbException(int status, Throwable cause) {
            super(cause.getMessage(), cause);
            this.status = status;
        }
    }

    private static abstract class ObbAction {

        ObbState mObbState;

        ObbAction(ObbState obbState) {
            mObbState = obbState;
        }

        public void execute(ObbActionHandler handler) {
            try {
                if (DEBUG_OBB)
                    Slog.i(TAG, "Starting to execute action: " + toString());
                handleExecute();
            } catch (ObbException e) {
                notifyObbStateChange(e);
            }
        }

        abstract void handleExecute() throws ObbException;

        protected void notifyObbStateChange(ObbException e) {
            Slog.w(TAG, e);
            notifyObbStateChange(e.status);
        }

        protected void notifyObbStateChange(int status) {
            if (mObbState == null || mObbState.token == null) {
                return;
            }

            try {
                mObbState.token.onObbResult(mObbState.rawPath, mObbState.nonce, status);
            } catch (RemoteException e) {
                Slog.w(TAG, "StorageEventListener went away while calling onObbStateChanged");
            }
        }
    }

    class MountObbAction extends ObbAction {
        private final int mCallingUid;
        private ObbInfo mObbInfo;

        MountObbAction(ObbState obbState, int callingUid, ObbInfo obbInfo) {
            super(obbState);
            mCallingUid = callingUid;
            mObbInfo = obbInfo;
        }

        @Override
        public void handleExecute() throws ObbException {
            warnOnNotMounted();

            if (!isUidOwnerOfPackageOrSystem(mObbInfo.packageName, mCallingUid)) {
                throw new ObbException(ERROR_PERMISSION_DENIED, "Denied attempt to mount OBB "
                        + mObbInfo.filename + " which is owned by " + mObbInfo.packageName);
            }

            final boolean isMounted;
            synchronized (mObbMounts) {
                isMounted = mObbPathToStateMap.containsKey(mObbState.rawPath);
            }
            if (isMounted) {
                throw new ObbException(ERROR_ALREADY_MOUNTED,
                        "Attempt to mount OBB which is already mounted: " + mObbInfo.filename);
            }

            try {
                mObbState.volId = mVold.createObb(mObbState.canonicalPath, mObbState.ownerGid);
                mVold.mount(mObbState.volId, 0, -1, null);

                if (DEBUG_OBB)
                    Slog.d(TAG, "Successfully mounted OBB " + mObbState.canonicalPath);

                synchronized (mObbMounts) {
                    addObbStateLocked(mObbState);
                }

                notifyObbStateChange(MOUNTED);
            } catch (Exception e) {
                throw new ObbException(ERROR_COULD_NOT_MOUNT, e);
            }
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("MountObbAction{");
            sb.append(mObbState);
            sb.append('}');
            return sb.toString();
        }
    }

    class UnmountObbAction extends ObbAction {
        private final boolean mForceUnmount;

        UnmountObbAction(ObbState obbState, boolean force) {
            super(obbState);
            mForceUnmount = force;
        }

        @Override
        public void handleExecute() throws ObbException {
            warnOnNotMounted();

            final ObbState existingState;
            synchronized (mObbMounts) {
                existingState = mObbPathToStateMap.get(mObbState.rawPath);
            }

            if (existingState == null) {
                throw new ObbException(ERROR_NOT_MOUNTED, "Missing existingState");
            }

            if (existingState.ownerGid != mObbState.ownerGid) {
                notifyObbStateChange(new ObbException(ERROR_PERMISSION_DENIED,
                        "Permission denied to unmount OBB " + existingState.rawPath
                                + " (owned by GID " + existingState.ownerGid + ")"));
                return;
            }

            try {
                mVold.unmount(mObbState.volId);
                mVold.destroyObb(mObbState.volId);
                mObbState.volId = null;

                synchronized (mObbMounts) {
                    removeObbStateLocked(existingState);
                }

                notifyObbStateChange(UNMOUNTED);
            } catch (Exception e) {
                throw new ObbException(ERROR_COULD_NOT_UNMOUNT, e);
            }
        }

        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append("UnmountObbAction{");
            sb.append(mObbState);
            sb.append(",force=");
            sb.append(mForceUnmount);
            sb.append('}');
            return sb.toString();
        }
    }

    private void dispatchOnStatus(IVoldTaskListener listener, int status,
            PersistableBundle extras) {
        if (listener != null) {
            try {
                listener.onStatus(status, extras);
            } catch (RemoteException ignored) {
            }
        }
    }

    private void dispatchOnFinished(IVoldTaskListener listener, int status,
            PersistableBundle extras) {
        if (listener != null) {
            try {
                listener.onFinished(status, extras);
            } catch (RemoteException ignored) {
            }
        }
    }

    @android.annotation.EnforcePermission(android.Manifest.permission.WRITE_MEDIA_STORAGE)
    @Override
    public int getExternalStorageMountMode(int uid, String packageName) {
        super.getExternalStorageMountMode_enforcePermission();

        return mStorageManagerInternal.getExternalStorageMountMode(uid, packageName);
    }

    private int getMountModeInternal(int uid, String packageName) {
        try {
            // Get some easy cases out of the way first
            if (Process.isIsolated(uid) || Process.isSdkSandboxUid(uid)) {
                return StorageManager.MOUNT_MODE_EXTERNAL_NONE;
            }

            final String[] packagesForUid = mIPackageManager.getPackagesForUid(uid);
            if (ArrayUtils.isEmpty(packagesForUid)) {
                // It's possible the package got uninstalled already, so just ignore.
                return StorageManager.MOUNT_MODE_EXTERNAL_NONE;
            }
            if (packageName == null) {
                packageName = packagesForUid[0];
            }

            final long token = Binder.clearCallingIdentity();
            try {
                if (mPmInternal.isInstantApp(packageName, UserHandle.getUserId(uid))) {
                    return StorageManager.MOUNT_MODE_EXTERNAL_NONE;
                }
            } finally {
                Binder.restoreCallingIdentity(token);
            }

            if (mStorageManagerInternal.isExternalStorageService(uid)) {
                // Determine if caller requires pass_through mount; note that we do this for
                // all processes that share a UID with MediaProvider; but this is fine, since
                // those processes anyway share the same rights as MediaProvider.
                return StorageManager.MOUNT_MODE_EXTERNAL_PASS_THROUGH;
            }

            if ((mDownloadsAuthorityAppId == UserHandle.getAppId(uid)
                    || mExternalStorageAuthorityAppId == UserHandle.getAppId(uid))) {
                // DownloadManager can write in app-private directories on behalf of apps;
                // give it write access to Android/
                // ExternalStorageProvider can access Android/{data,obb} dirs in managed mode
                return StorageManager.MOUNT_MODE_EXTERNAL_ANDROID_WRITABLE;
            }

            final boolean hasMtp = mIPackageManager.checkUidPermission(ACCESS_MTP, uid) ==
                    PERMISSION_GRANTED;
            if (hasMtp) {
                ApplicationInfo ai = mIPackageManager.getApplicationInfo(packageName,
                        0, UserHandle.getUserId(uid));
                if (ai != null && ai.isSignedWithPlatformKey()) {
                    // Platform processes hosting the MTP server should be able to write in Android/
                    return StorageManager.MOUNT_MODE_EXTERNAL_ANDROID_WRITABLE;
                }
            }

            // We're only willing to give out installer access if they hold
            // runtime permission; this is a firm CDD requirement
            final boolean hasInstall = mIPackageManager.checkUidPermission(INSTALL_PACKAGES,
                    uid) == PERMISSION_GRANTED;
            boolean hasInstallOp = false;
            // OP_REQUEST_INSTALL_PACKAGES is granted/denied per package but vold can't
            // update mountpoints of a specific package. So, check the appop for all packages
            // sharing the uid and allow same level of storage access for all packages even if
            // one of the packages has the appop granted.
            for (String uidPackageName : packagesForUid) {
                if (mIAppOpsService.checkOperation(
                        OP_REQUEST_INSTALL_PACKAGES, uid, uidPackageName) == MODE_ALLOWED) {
                    hasInstallOp = true;
                    break;
                }
            }
            if (hasInstall) {
                return StorageManager.MOUNT_MODE_EXTERNAL_INSTALLER;
            }

            if (hasInstallOp) {
                /*
                Originally, previous check was `if (hasInstall || hasInstallOp)`.

                Use a special flag to control access to just Android/obb directory instead.
                Toggle for this flag is in ExternalSourcesDetails.java in Settings app
                (same screen that grants the REQUEST_INSTALL_PACKAGES permission)
                 */

                GosPackageState ps = mPmInternal.getGosPackageState(packageName, UserHandle.getUserId(uid));
                if (ps.hasFlag(GosPackageStateFlag.ALLOW_ACCESS_TO_OBB_DIRECTORY)) {
                    return StorageManager.MOUNT_MODE_EXTERNAL_INSTALLER;
                }
            }

            return StorageManager.MOUNT_MODE_EXTERNAL_DEFAULT;
        } catch (RemoteException e) {
            // Should not happen
        }
        return StorageManager.MOUNT_MODE_EXTERNAL_NONE;
    }

    private static class Callbacks extends Handler {
        private static final int MSG_STORAGE_STATE_CHANGED = 1;
        private static final int MSG_VOLUME_STATE_CHANGED = 2;
        private static final int MSG_VOLUME_RECORD_CHANGED = 3;
        private static final int MSG_VOLUME_FORGOTTEN = 4;
        private static final int MSG_DISK_SCANNED = 5;
        private static final int MSG_DISK_DESTROYED = 6;

        private final RemoteCallbackList<IStorageEventListener>
                mCallbacks = new RemoteCallbackList<>();

        public Callbacks(Looper looper) {
            super(looper);
        }

        public void register(IStorageEventListener callback) {
            mCallbacks.register(callback);
        }

        public void unregister(IStorageEventListener callback) {
            mCallbacks.unregister(callback);
        }

        @Override
        public void handleMessage(Message msg) {
            final SomeArgs args = (SomeArgs) msg.obj;
            final int n = mCallbacks.beginBroadcast();
            for (int i = 0; i < n; i++) {
                final IStorageEventListener callback = mCallbacks.getBroadcastItem(i);
                try {
                    invokeCallback(callback, msg.what, args);
                } catch (RemoteException ignored) {
                }
            }
            mCallbacks.finishBroadcast();
            args.recycle();
        }

        private void invokeCallback(IStorageEventListener callback, int what, SomeArgs args)
                throws RemoteException {
            switch (what) {
                case MSG_STORAGE_STATE_CHANGED: {
                    callback.onStorageStateChanged((String) args.arg1, (String) args.arg2,
                            (String) args.arg3);
                    break;
                }
                case MSG_VOLUME_STATE_CHANGED: {
                    VolumeInfo volInfo = ((WatchedVolumeInfo) args.arg1).getVolumeInfo();
                    callback.onVolumeStateChanged(volInfo, args.argi2, args.argi3);
                    break;
                }
                case MSG_VOLUME_RECORD_CHANGED: {
                    callback.onVolumeRecordChanged((VolumeRecord) args.arg1);
                    break;
                }
                case MSG_VOLUME_FORGOTTEN: {
                    callback.onVolumeForgotten((String) args.arg1);
                    break;
                }
                case MSG_DISK_SCANNED: {
                    callback.onDiskScanned((DiskInfo) args.arg1, args.argi2);
                    break;
                }
                case MSG_DISK_DESTROYED: {
                    callback.onDiskDestroyed((DiskInfo) args.arg1);
                    break;
                }
            }
        }

        private void notifyStorageStateChanged(String path, String oldState, String newState) {
            final SomeArgs args = SomeArgs.obtain();
            args.arg1 = path;
            args.arg2 = oldState;
            args.arg3 = newState;
            obtainMessage(MSG_STORAGE_STATE_CHANGED, args).sendToTarget();
        }

        private void notifyVolumeStateChanged(WatchedVolumeInfo vol, int oldState, int newState) {
            final SomeArgs args = SomeArgs.obtain();
            args.arg1 = vol.clone();
            args.argi2 = oldState;
            args.argi3 = newState;
            obtainMessage(MSG_VOLUME_STATE_CHANGED, args).sendToTarget();
        }

        private void notifyVolumeRecordChanged(VolumeRecord rec) {
            final SomeArgs args = SomeArgs.obtain();
            args.arg1 = rec.clone();
            obtainMessage(MSG_VOLUME_RECORD_CHANGED, args).sendToTarget();
        }

        private void notifyVolumeForgotten(String fsUuid) {
            final SomeArgs args = SomeArgs.obtain();
            args.arg1 = fsUuid;
            obtainMessage(MSG_VOLUME_FORGOTTEN, args).sendToTarget();
        }

        private void notifyDiskScanned(DiskInfo disk, int volumeCount) {
            final SomeArgs args = SomeArgs.obtain();
            args.arg1 = disk.clone();
            args.argi2 = volumeCount;
            obtainMessage(MSG_DISK_SCANNED, args).sendToTarget();
        }

        private void notifyDiskDestroyed(DiskInfo disk) {
            final SomeArgs args = SomeArgs.obtain();
            args.arg1 = disk.clone();
            obtainMessage(MSG_DISK_DESTROYED, args).sendToTarget();
        }
    }

    @Override
    protected void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
        if (!DumpUtils.checkDumpPermission(mContext, TAG, writer)) return;

        final IndentingPrintWriter pw = new IndentingPrintWriter(writer, "  ", 160);
        synchronized (mLock) {
            pw.println("Disks:");
            pw.increaseIndent();
            for (int i = 0; i < mDisks.size(); i++) {
                final DiskInfo disk = mDisks.valueAt(i);
                disk.dump(pw);
            }
            pw.decreaseIndent();

            pw.println();
            pw.println("Volumes:");
            pw.increaseIndent();
            for (int i = 0; i < mVolumes.size(); i++) {
                final WatchedVolumeInfo vol = mVolumes.valueAt(i);
                if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.getId())) continue;
                vol.dump(pw);
            }
            pw.decreaseIndent();

            pw.println();
            pw.println("Records:");
            pw.increaseIndent();
            for (int i = 0; i < mRecords.size(); i++) {
                final VolumeRecord note = mRecords.valueAt(i);
                note.dump(pw);
            }
            pw.decreaseIndent();

            pw.println();
            pw.println("Primary storage UUID: " + mPrimaryStorageUuid);

            pw.println();
            final Pair<String, Long> pair = StorageManager.getPrimaryStoragePathAndSize();
            if (pair == null) {
                pw.println("Internal storage total size: N/A");
            } else {
                pw.print("Internal storage (");
                pw.print(pair.first);
                pw.print(") total size: ");
                pw.print(pair.second);
                pw.print(" (");
                pw.print(pair.second / DataUnit.MEBIBYTES.toBytes(1L));
                pw.println(" MiB)");
            }

            pw.println();
            pw.println("CE unlocked users: " + mCeUnlockedUsers);
            pw.println("System unlocked users: " + Arrays.toString(mSystemUnlockedUsers));
        }

        synchronized (mObbMounts) {
            pw.println();
            pw.println("mObbMounts:");
            pw.increaseIndent();
            final Iterator<Entry<IBinder, List<ObbState>>> binders = mObbMounts.entrySet()
                    .iterator();
            while (binders.hasNext()) {
                Entry<IBinder, List<ObbState>> e = binders.next();
                pw.println(e.getKey() + ":");
                pw.increaseIndent();
                final List<ObbState> obbStates = e.getValue();
                for (final ObbState obbState : obbStates) {
                    pw.println(obbState);
                }
                pw.decreaseIndent();
            }
            pw.decreaseIndent();

            pw.println();
            pw.println("mObbPathToStateMap:");
            pw.increaseIndent();
            final Iterator<Entry<String, ObbState>> maps =
                    mObbPathToStateMap.entrySet().iterator();
            while (maps.hasNext()) {
                final Entry<String, ObbState> e = maps.next();
                pw.print(e.getKey());
                pw.print(" -> ");
                pw.println(e.getValue());
            }
            pw.decreaseIndent();
        }

        synchronized (mCloudMediaProviders) {
            pw.println();
            pw.print("Media cloud providers: ");
            pw.println(mCloudMediaProviders);
        }

        pw.println();
        pw.print("Last maintenance: ");
        pw.println(TimeUtils.formatForLogging(mLastMaintenance));
    }

    /** {@inheritDoc} */
    @Override
    public void monitor() {
        try {
            mVold.monitor();
        } catch (Exception e) {
            Slog.wtf(TAG, e);
        }
    }

    private final class StorageManagerInternalImpl extends StorageManagerInternal {
        @GuardedBy("mResetListeners")
        private final List<StorageManagerInternal.ResetListener> mResetListeners =
                new ArrayList<>();

        private final CopyOnWriteArraySet<StorageManagerInternal.CloudProviderChangeListener>
                mCloudProviderChangeListeners = new CopyOnWriteArraySet<>();

        @Override
        public boolean isFuseMounted(int userId) {
            synchronized (mLock) {
                return mFuseMountedUser.contains(userId);
            }
        }

        /**
         * Check if fuse is running in target user, if it's running then setup its storage dirs.
         * Return true if storage dirs are mounted.
         */
        @Override
        public boolean prepareStorageDirs(int userId, Set<String> packageList,
                String processName) {
            synchronized (mLock) {
                if (!mFuseMountedUser.contains(userId)) {
                    Slog.w(TAG, "User " + userId + " is not unlocked yet so skip mounting obb");
                    return false;
                }
            }
            try {
                final IVold vold = IVold.Stub.asInterface(
                        ServiceManager.getServiceOrThrow("vold"));
                for (String pkg : packageList) {
                    final String packageObbDir =
                            String.format(Locale.US, "/storage/emulated/%d/Android/obb/%s/",
                                    userId, pkg);
                    final String packageDataDir =
                            String.format(Locale.US, "/storage/emulated/%d/Android/data/%s/",
                                    userId, pkg);

                    // Create package obb and data dir if it doesn't exist.
                    int appUid = UserHandle.getUid(userId, mPmInternal.getPackage(pkg).getUid());
                    vold.ensureAppDirsCreated(new String[] {packageObbDir, packageDataDir}, appUid);
                }
            } catch (ServiceManager.ServiceNotFoundException | RemoteException e) {
                Slog.e(TAG, "Unable to create obb and data directories for " + processName,e);
                return false;
            }
            return true;
        }

        @Override
        public int getExternalStorageMountMode(int uid, String packageName) {
            final int mode = getMountModeInternal(uid, packageName);
            if (LOCAL_LOGV) {
                Slog.v(TAG, "Resolved mode " + mode + " for " + packageName + "/"
                        + UserHandle.formatUid(uid));
            }
            return mode;
        }

        @Override
        public boolean hasExternalStorageAccess(int uid, String packageName) {
            try {
                final int opMode = mIAppOpsService.checkOperation(
                        OP_MANAGE_EXTERNAL_STORAGE, uid, packageName);
                if (opMode == AppOpsManager.MODE_DEFAULT) {
                    return mIPackageManager.checkUidPermission(
                            MANAGE_EXTERNAL_STORAGE, uid) == PERMISSION_GRANTED;
                }

                return opMode == AppOpsManager.MODE_ALLOWED;
            } catch (RemoteException e) {
                Slog.w("Failed to check MANAGE_EXTERNAL_STORAGE access for " + packageName, e);
            }
            return false;
        }

        @Override
        public void addResetListener(StorageManagerInternal.ResetListener listener) {
            synchronized (mResetListeners) {
                mResetListeners.add(listener);
            }
        }

        public void onReset(IVold vold) {
            synchronized (mResetListeners) {
                for (StorageManagerInternal.ResetListener listener : mResetListeners) {
                    listener.onReset(vold);
                }
            }
        }

        @Override
        public void resetUser(int userId) {
            // TODO(b/145931219): ideally, we only reset storage for the user in question,
            // but for now, reset everything.
            mHandler.obtainMessage(H_RESET).sendToTarget();
        }

        @Override
        public boolean hasLegacyExternalStorage(int uid) {
            synchronized (mLock) {
                return mUidsWithLegacyExternalStorage.contains(uid);
            }
        }

        @Override
        public void prepareAppDataAfterInstall(String packageName, int uid) {
            int userId = UserHandle.getUserId(uid);
            final Environment.UserEnvironment userEnv = new Environment.UserEnvironment(userId);

            // The installer may have downloaded OBBs for this newly installed application;
            // make sure the OBB dir for the application is setup correctly, if it exists.
            File[] packageObbDirs = userEnv.buildExternalStorageAppObbDirs(packageName);
            for (File packageObbDir : packageObbDirs) {
                if (packageObbDir.getPath().startsWith(
                                Environment.getDataPreloadsMediaDirectory().getPath())) {
                    Slog.i(TAG, "Skipping app data preparation for " + packageObbDir);
                    continue;
                }
                try {
                    mVold.fixupAppDir(packageObbDir.getCanonicalPath() + "/", uid);
                } catch (IOException e) {
                    Log.e(TAG, "Failed to get canonical path for " + packageName);
                } catch (RemoteException | ServiceSpecificException e) {
                    // TODO(b/149975102) there is a known case where this fails, when a new
                    // user is setup and we try to fixup app dirs for some existing apps.
                    // For now catch the exception and don't crash.
                    Log.e(TAG, "Failed to fixup app dir for " + packageName, e);
                }
            }
        }

        @Override
        public boolean isExternalStorageService(int uid) {
            return mMediaStoreAuthorityAppId == UserHandle.getAppId(uid);
        }

        @Override
        public void freeCache(String volumeUuid, long freeBytes) {
            try {
                mStorageSessionController.freeCache(volumeUuid, freeBytes);
            } catch (ExternalStorageServiceException e) {
                Log.e(TAG, "Failed to free cache of vol : " + volumeUuid, e);
            }
        }

        public boolean hasExternalStorage(int uid, String packageName) {
            // No need to check for system uid. This avoids a deadlock between
            // PackageManagerService and AppOpsService.
            if (uid == Process.SYSTEM_UID) {
                return true;
            }

            return getExternalStorageMountMode(uid, packageName)
                    != StorageManager.MOUNT_MODE_EXTERNAL_NONE;
        }

        private void killAppForOpChange(int code, int uid) {
            final IActivityManager am = ActivityManager.getService();
            try {
                am.killUid(UserHandle.getAppId(uid), UserHandle.USER_ALL,
                        AppOpsManager.opToName(code) + " changed.");
            } catch (RemoteException e) {
            }
        }

        public void onAppOpsChanged(int code, int uid, @Nullable String packageName, int mode,
                int previousMode) {
            final long token = Binder.clearCallingIdentity();
            try {
                // When using FUSE, we may need to kill the app if the op changes
                switch(code) {
                    case OP_REQUEST_INSTALL_PACKAGES:
                        // In R, we used to kill the app here if it transitioned to/from
                        // MODE_ALLOWED, to make sure the app had the correct (writable) OBB
                        // view. But the majority of apps don't handle OBBs anyway, and for those
                        // that do, they can restart themselves. Therefore, starting from S,
                        // only kill the app when it transitions away from MODE_ALLOWED (eg,
                        // when the permission is taken away).
                        if (previousMode == MODE_ALLOWED && mode != MODE_ALLOWED) {
                            killAppForOpChange(code, uid);
                        }
                        return;
                    case OP_MANAGE_EXTERNAL_STORAGE:
                        if (mode != MODE_ALLOWED) {
                            // Only kill if op is denied, to lose external_storage gid
                            // Killing when op is granted to pickup the gid automatically,
                            // results in a bad UX, especially since the gid only gives access
                            // to unreliable volumes, USB OTGs that are rarely mounted. The app
                            // will get the external_storage gid on next organic restart.
                            killAppForOpChange(code, uid);
                        }
                        return;
                    case OP_LEGACY_STORAGE:
                        updateLegacyStorageApps(packageName, uid, mode == MODE_ALLOWED);
                        return;
                }
            } finally {
                Binder.restoreCallingIdentity(token);
            }
        }

        @Override
        public List<String> getPrimaryVolumeIds() {
            final List<String> primaryVolumeIds = new ArrayList<>();
            synchronized (mLock) {
                for (int i = 0; i < mVolumes.size(); i++) {
                    final WatchedVolumeInfo vol = mVolumes.valueAt(i);
                    if (vol.isPrimary()) {
                        primaryVolumeIds.add(vol.getId());
                    }
                }
            }
            return primaryVolumeIds;
        }

        @Override
        public void createUserStorageKeys(int userId, boolean ephemeral) {
            try {
                mVold.createUserStorageKeys(userId, ephemeral);
                // Since the user's CE key was just created, the user's CE storage is now unlocked.
                synchronized (mLock) {
                    mCeUnlockedUsers.append(userId);
                }
            } catch (Exception e) {
                Slog.wtf(TAG, e);
            }
        }

        @Override
        public void destroyUserStorageKeys(int userId) {
            try {
                mVold.destroyUserStorageKeys(userId);
                // Since the user's CE key was just destroyed, the user's CE storage is now locked.
                synchronized (mLock) {
                    mCeUnlockedUsers.remove(userId);
                }
            } catch (Exception e) {
                Slog.wtf(TAG, e);
            }
        }

        @Override
        public void prepareUserStorage(
                @Nullable String volumeUuid, @UserIdInt int userId, @StorageFlags int flags) {
            try {
                prepareUserStorageInternal(volumeUuid, userId, flags);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public void destroyUserStorage(
                @Nullable String volumeUuid, @UserIdInt int userId, @StorageFlags int flags) {
            try {
                mVold.destroyUserStorage(volumeUuid, userId, flags);
            } catch (Exception e) {
                Slog.wtf(TAG, e);
            }
        }

        @Override
        public void markCeStoragePrepared(int userId) {
            synchronized (mLock) {
                mCeStoragePreparedUsers.add(userId);
            }
        }

        @Override
        public boolean isCeStoragePrepared(int userId) {
            synchronized (mLock) {
                return mCeStoragePreparedUsers.contains(userId);
            }
        }

        @Override
        public void setCeStorageProtection(@UserIdInt int userId, byte[] secret) {
            try {
                mVold.setCeStorageProtection(userId, secret);
            } catch (RemoteException e) {
                e.rethrowAsRuntimeException();
            }
        }

        @Override
        public void unlockCeStorage(@UserIdInt int userId, byte[] secret) {
            if (StorageManager.isFileEncrypted()) {
                try {
                    mVold.unlockCeStorage(userId, secret);
                } catch (RemoteException e) {
                    e.rethrowAsRuntimeException();
                }
            }
            synchronized (mLock) {
                mCeUnlockedUsers.append(userId);
            }
        }

        @Override
        public void registerCloudProviderChangeListener(
                @NonNull StorageManagerInternal.CloudProviderChangeListener listener) {
            mCloudProviderChangeListeners.add(listener);
            mHandler.obtainMessage(H_CLOUD_MEDIA_PROVIDER_CHANGED, listener).sendToTarget();
        }

        @Override
        public void prepareUserStorageForMove(String fromVolumeUuid, String toVolumeUuid,
                List<UserInfo> users) {
            try {
                prepareUserStorageForMoveInternal(fromVolumeUuid, toVolumeUuid, users);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        @Override
        public IFsveritySetupAuthToken createFsveritySetupAuthToken(ParcelFileDescriptor authFd,
                int uid) throws IOException {
            try {
                return mInstaller.createFsveritySetupAuthToken(authFd, uid);
            } catch (Installer.InstallerException e) {
                throw new IOException(e);
            }
        }

        @Override
        public int enableFsverity(IFsveritySetupAuthToken authToken, String filePath,
                String packageName) throws IOException {
            try {
                return mInstaller.enableFsverity(authToken, filePath, packageName);
            } catch (Installer.InstallerException e) {
                throw new IOException(e);
            }
        }
    }
}
