Merge "Disable moving 3rd party apps to internal if not allowed."
diff --git a/core/java/android/app/ApplicationPackageManager.java b/core/java/android/app/ApplicationPackageManager.java
index f3185a8..20f7e63 100644
--- a/core/java/android/app/ApplicationPackageManager.java
+++ b/core/java/android/app/ApplicationPackageManager.java
@@ -24,6 +24,7 @@
 import android.app.admin.DevicePolicyManager;
 import android.content.ComponentName;
 import android.content.ContentResolver;
+import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
 import android.content.IntentSender;
@@ -1386,7 +1387,7 @@
         }
     }
 
-    ApplicationPackageManager(ContextImpl context,
+    protected ApplicationPackageManager(ContextImpl context,
                               IPackageManager pm) {
         mContext = context;
         mPM = pm;
@@ -1820,6 +1821,12 @@
     @Override
     public @Nullable VolumeInfo getPackageCurrentVolume(ApplicationInfo app) {
         final StorageManager storage = mContext.getSystemService(StorageManager.class);
+        return getPackageCurrentVolume(app, storage);
+    }
+
+    @VisibleForTesting
+    protected @Nullable VolumeInfo getPackageCurrentVolume(ApplicationInfo app,
+            StorageManager storage) {
         if (app.isInternal()) {
             return storage.findVolumeById(VolumeInfo.ID_PRIVATE_INTERNAL);
         } else if (app.isExternalAsec()) {
@@ -1831,25 +1838,43 @@
 
     @Override
     public @NonNull List<VolumeInfo> getPackageCandidateVolumes(ApplicationInfo app) {
-        final StorageManager storage = mContext.getSystemService(StorageManager.class);
-        final VolumeInfo currentVol = getPackageCurrentVolume(app);
-        final List<VolumeInfo> vols = storage.getVolumes();
+        final StorageManager storageManager = mContext.getSystemService(StorageManager.class);
+        return getPackageCandidateVolumes(app, storageManager, mPM);
+    }
+
+    @VisibleForTesting
+    protected @NonNull List<VolumeInfo> getPackageCandidateVolumes(ApplicationInfo app,
+            StorageManager storageManager, IPackageManager pm) {
+        final VolumeInfo currentVol = getPackageCurrentVolume(app, storageManager);
+        final List<VolumeInfo> vols = storageManager.getVolumes();
         final List<VolumeInfo> candidates = new ArrayList<>();
         for (VolumeInfo vol : vols) {
-            if (Objects.equals(vol, currentVol) || isPackageCandidateVolume(mContext, app, vol)) {
+            if (Objects.equals(vol, currentVol)
+                    || isPackageCandidateVolume(mContext, app, vol, pm)) {
                 candidates.add(vol);
             }
         }
         return candidates;
     }
 
-    private boolean isPackageCandidateVolume(
-            ContextImpl context, ApplicationInfo app, VolumeInfo vol) {
-        final boolean forceAllowOnExternal = Settings.Global.getInt(
+    @VisibleForTesting
+    protected boolean isForceAllowOnExternal(Context context) {
+        return Settings.Global.getInt(
                 context.getContentResolver(), Settings.Global.FORCE_ALLOW_ON_EXTERNAL, 0) != 0;
-        // Private internal is always an option
+    }
+
+    @VisibleForTesting
+    protected boolean isAllow3rdPartyOnInternal(Context context) {
+        return context.getResources().getBoolean(
+                com.android.internal.R.bool.config_allow3rdPartyAppOnInternal);
+    }
+
+    private boolean isPackageCandidateVolume(
+            ContextImpl context, ApplicationInfo app, VolumeInfo vol, IPackageManager pm) {
+        final boolean forceAllowOnExternal = isForceAllowOnExternal(context);
+
         if (VolumeInfo.ID_PRIVATE_INTERNAL.equals(vol.getId())) {
-            return true;
+            return app.isSystemApp() || isAllow3rdPartyOnInternal(context);
         }
 
         // System apps and apps demanding internal storage can't be moved
@@ -1875,7 +1900,7 @@
 
         // Some apps can't be moved. (e.g. device admins)
         try {
-            if (mPM.isPackageDeviceAdminOnAnyUser(app.packageName)) {
+            if (pm.isPackageDeviceAdminOnAnyUser(app.packageName)) {
                 return false;
             }
         } catch (RemoteException e) {
diff --git a/core/java/android/content/pm/PackageManager.java b/core/java/android/content/pm/PackageManager.java
index 04e649c..c3fe182 100644
--- a/core/java/android/content/pm/PackageManager.java
+++ b/core/java/android/content/pm/PackageManager.java
@@ -1360,6 +1360,14 @@
     public static final int MOVE_FAILED_DEVICE_ADMIN = -8;
 
     /**
+     * Error code that is passed to the {@link IPackageMoveObserver} if system does not allow
+     * non-system apps to be moved to internal storage.
+     *
+     * @hide
+     */
+    public static final int MOVE_FAILED_3RD_PARTY_NOT_ALLOWED_ON_INTERNAL = -9;
+
+    /**
      * Flag parameter for {@link #movePackage} to indicate that
      * the package should be moved to internal storage if its
      * been installed on external media.
diff --git a/core/tests/coretests/src/android/app/ApplicationPackageManagerTest.java b/core/tests/coretests/src/android/app/ApplicationPackageManagerTest.java
new file mode 100644
index 0000000..f60bf94
--- /dev/null
+++ b/core/tests/coretests/src/android/app/ApplicationPackageManagerTest.java
@@ -0,0 +1,242 @@
+/*
+ * Copyright (C) 2017 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.app;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageInfo;
+import android.os.storage.StorageManager;
+import android.os.storage.VolumeInfo;
+
+import junit.framework.TestCase;
+
+import org.mockito.Mockito;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import static android.os.storage.VolumeInfo.STATE_MOUNTED;
+import static android.os.storage.VolumeInfo.STATE_UNMOUNTED;
+
+public class ApplicationPackageManagerTest extends TestCase {
+    private static final String sInternalVolPath = "/data";
+    private static final String sAdoptedVolPath = "/mnt/expand/123";
+    private static final String sPublicVolPath = "/emulated";
+    private static final String sPrivateUnmountedVolPath = "/private";
+
+    private static final String sInternalVolUuid = null; //StorageManager.UUID_PRIVATE_INTERNAL
+    private static final String sAdoptedVolUuid = "adopted";
+    private static final String sPublicVolUuid = "emulated";
+    private static final String sPrivateUnmountedVolUuid = "private";
+
+    private static final VolumeInfo sInternalVol = new VolumeInfo("private",
+            VolumeInfo.TYPE_PRIVATE, null /*DiskInfo*/, null /*partGuid*/);
+
+    private static final VolumeInfo sAdoptedVol = new VolumeInfo("adopted",
+            VolumeInfo.TYPE_PRIVATE, null /*DiskInfo*/, null /*partGuid*/);
+
+    private static final VolumeInfo sPublicVol = new VolumeInfo("public",
+            VolumeInfo.TYPE_PUBLIC, null /*DiskInfo*/, null /*partGuid*/);
+
+    private static final VolumeInfo sPrivateUnmountedVol = new VolumeInfo("private2",
+            VolumeInfo.TYPE_PRIVATE, null /*DiskInfo*/, null /*partGuid*/);
+
+    private static final List<VolumeInfo> sVolumes = new ArrayList<>();
+
+    static {
+        sInternalVol.path = sInternalVolPath;
+        sInternalVol.state = STATE_MOUNTED;
+        sInternalVol.fsUuid = sInternalVolUuid;
+
+        sAdoptedVol.path = sAdoptedVolPath;
+        sAdoptedVol.state = STATE_MOUNTED;
+        sAdoptedVol.fsUuid = sAdoptedVolUuid;
+
+        sPublicVol.state = STATE_MOUNTED;
+        sPublicVol.path = sPublicVolPath;
+        sPublicVol.fsUuid = sPublicVolUuid;
+
+        sPrivateUnmountedVol.state = STATE_UNMOUNTED;
+        sPrivateUnmountedVol.path = sPrivateUnmountedVolPath;
+        sPrivateUnmountedVol.fsUuid = sPrivateUnmountedVolUuid;
+
+        sVolumes.add(sInternalVol);
+        sVolumes.add(sAdoptedVol);
+        sVolumes.add(sPublicVol);
+        sVolumes.add(sPrivateUnmountedVol);
+    }
+
+    private static final class MockedApplicationPackageManager extends ApplicationPackageManager {
+        private boolean mForceAllowOnExternal = false;
+        private boolean mAllow3rdPartyOnInternal = true;
+
+        public MockedApplicationPackageManager() {
+            super(null, null);
+        }
+
+        public void setForceAllowOnExternal(boolean forceAllowOnExternal) {
+            mForceAllowOnExternal = forceAllowOnExternal;
+        }
+
+        public void setAllow3rdPartyOnInternal(boolean allow3rdPartyOnInternal) {
+            mAllow3rdPartyOnInternal = allow3rdPartyOnInternal;
+        }
+
+        @Override
+        public boolean isForceAllowOnExternal(Context context) {
+            return mForceAllowOnExternal;
+        }
+
+        @Override
+        public boolean isAllow3rdPartyOnInternal(Context context) {
+            return mAllow3rdPartyOnInternal;
+        }
+    }
+
+    private StorageManager getMockedStorageManager() {
+        StorageManager storageManager = Mockito.mock(StorageManager.class);
+        Mockito.when(storageManager.getVolumes()).thenReturn(sVolumes);
+        Mockito.when(storageManager.findVolumeById(VolumeInfo.ID_PRIVATE_INTERNAL))
+                .thenReturn(sInternalVol);
+        Mockito.when(storageManager.findVolumeByUuid(sAdoptedVolUuid))
+                .thenReturn(sAdoptedVol);
+        Mockito.when(storageManager.findVolumeByUuid(sPublicVolUuid))
+                .thenReturn(sPublicVol);
+        Mockito.when(storageManager.findVolumeByUuid(sPrivateUnmountedVolUuid))
+                .thenReturn(sPrivateUnmountedVol);
+        return storageManager;
+    }
+
+    private void verifyReturnedVolumes(List<VolumeInfo> actualVols, VolumeInfo... exptectedVols) {
+        boolean failed = false;
+        if (actualVols.size() != exptectedVols.length) {
+            failed = true;
+        } else {
+            for (VolumeInfo vol : exptectedVols) {
+                if (!actualVols.contains(vol)) {
+                    failed = true;
+                    break;
+                }
+            }
+        }
+
+        if (failed) {
+            fail("Wrong volumes returned.\n Expected: " + Arrays.toString(exptectedVols)
+                    + "\n Actual: " + Arrays.toString(actualVols.toArray()));
+        }
+    }
+
+    public void testGetCandidateVolumes_systemApp() throws Exception {
+        ApplicationInfo sysAppInfo = new ApplicationInfo();
+        sysAppInfo.flags = ApplicationInfo.FLAG_SYSTEM;
+
+        StorageManager storageManager = getMockedStorageManager();
+        IPackageManager pm = Mockito.mock(IPackageManager.class);
+
+        MockedApplicationPackageManager appPkgMgr = new MockedApplicationPackageManager();
+
+        appPkgMgr.setAllow3rdPartyOnInternal(true);
+        appPkgMgr.setForceAllowOnExternal(true);
+        List<VolumeInfo> candidates =
+                appPkgMgr.getPackageCandidateVolumes(sysAppInfo, storageManager, pm);
+        verifyReturnedVolumes(candidates, sInternalVol);
+
+        appPkgMgr.setAllow3rdPartyOnInternal(true);
+        appPkgMgr.setForceAllowOnExternal(false);
+        candidates = appPkgMgr.getPackageCandidateVolumes(sysAppInfo, storageManager, pm);
+        verifyReturnedVolumes(candidates, sInternalVol);
+
+        appPkgMgr.setAllow3rdPartyOnInternal(false);
+        appPkgMgr.setForceAllowOnExternal(false);
+        candidates = appPkgMgr.getPackageCandidateVolumes(sysAppInfo, storageManager, pm);
+        verifyReturnedVolumes(candidates, sInternalVol);
+
+        appPkgMgr.setAllow3rdPartyOnInternal(false);
+        appPkgMgr.setForceAllowOnExternal(true);
+        candidates = appPkgMgr.getPackageCandidateVolumes(sysAppInfo, storageManager, pm);
+        verifyReturnedVolumes(candidates, sInternalVol);
+    }
+
+    public void testGetCandidateVolumes_3rdParty_internalOnly() throws Exception {
+        ApplicationInfo appInfo = new ApplicationInfo();
+        StorageManager storageManager = getMockedStorageManager();
+
+        IPackageManager pm = Mockito.mock(IPackageManager.class);
+        Mockito.when(pm.isPackageDeviceAdminOnAnyUser(Mockito.anyString())).thenReturn(false);
+
+        MockedApplicationPackageManager appPkgMgr = new MockedApplicationPackageManager();
+
+        // must allow 3rd party on internal, otherwise the app wouldn't have been installed before.
+        appPkgMgr.setAllow3rdPartyOnInternal(true);
+
+        // INSTALL_LOCATION_INTERNAL_ONLY AND INSTALL_LOCATION_UNSPECIFIED are treated the same.
+        int[] locations = {PackageInfo.INSTALL_LOCATION_INTERNAL_ONLY,
+                PackageInfo.INSTALL_LOCATION_UNSPECIFIED};
+
+        for (int location : locations) {
+            appInfo.installLocation = location;
+            appPkgMgr.setForceAllowOnExternal(true);
+            List<VolumeInfo> candidates = appPkgMgr.getPackageCandidateVolumes(
+                    appInfo, storageManager, pm);
+            verifyReturnedVolumes(candidates, sInternalVol, sAdoptedVol);
+
+            appPkgMgr.setForceAllowOnExternal(false);
+            candidates = appPkgMgr.getPackageCandidateVolumes(appInfo, storageManager, pm);
+            verifyReturnedVolumes(candidates, sInternalVol);
+        }
+    }
+
+    public void testGetCandidateVolumes_3rdParty_auto() throws Exception {
+        ApplicationInfo appInfo = new ApplicationInfo();
+        StorageManager storageManager = getMockedStorageManager();
+
+        IPackageManager pm = Mockito.mock(IPackageManager.class);
+
+        MockedApplicationPackageManager appPkgMgr = new MockedApplicationPackageManager();
+        appPkgMgr.setForceAllowOnExternal(true);
+
+        // INSTALL_LOCATION_AUTO AND INSTALL_LOCATION_PREFER_EXTERNAL are treated the same.
+        int[] locations = {PackageInfo.INSTALL_LOCATION_AUTO,
+                PackageInfo.INSTALL_LOCATION_PREFER_EXTERNAL};
+
+        for (int location : locations) {
+            appInfo.installLocation = location;
+            appInfo.flags = 0;
+
+            appInfo.volumeUuid = sInternalVolUuid;
+            Mockito.when(pm.isPackageDeviceAdminOnAnyUser(Mockito.anyString())).thenReturn(false);
+            appPkgMgr.setAllow3rdPartyOnInternal(true);
+            List<VolumeInfo> candidates = appPkgMgr.getPackageCandidateVolumes(
+                    appInfo, storageManager, pm);
+            verifyReturnedVolumes(candidates, sInternalVol, sAdoptedVol);
+
+            appInfo.volumeUuid = sInternalVolUuid;
+            appPkgMgr.setAllow3rdPartyOnInternal(true);
+            Mockito.when(pm.isPackageDeviceAdminOnAnyUser(Mockito.anyString())).thenReturn(true);
+            candidates = appPkgMgr.getPackageCandidateVolumes(appInfo, storageManager, pm);
+            verifyReturnedVolumes(candidates, sInternalVol);
+
+            appInfo.flags = ApplicationInfo.FLAG_EXTERNAL_STORAGE;
+            appInfo.volumeUuid = sAdoptedVolUuid;
+            appPkgMgr.setAllow3rdPartyOnInternal(false);
+            candidates = appPkgMgr.getPackageCandidateVolumes(appInfo, storageManager, pm);
+            verifyReturnedVolumes(candidates, sAdoptedVol);
+        }
+    }
+}
diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
index 6669889..9d2085a 100644
--- a/services/core/java/com/android/server/pm/PackageManagerService.java
+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
@@ -68,6 +68,7 @@
 import static android.content.pm.PackageManager.MATCH_KNOWN_PACKAGES;
 import static android.content.pm.PackageManager.MATCH_SYSTEM_ONLY;
 import static android.content.pm.PackageManager.MATCH_UNINSTALLED_PACKAGES;
+import static android.content.pm.PackageManager.MOVE_FAILED_3RD_PARTY_NOT_ALLOWED_ON_INTERNAL;
 import static android.content.pm.PackageManager.MOVE_FAILED_DEVICE_ADMIN;
 import static android.content.pm.PackageManager.MOVE_FAILED_DOESNT_EXIST;
 import static android.content.pm.PackageManager.MOVE_FAILED_INTERNAL_ERROR;
@@ -21042,6 +21043,14 @@
                         "Cannot move system application");
             }
 
+            final boolean isInternalStorage = VolumeInfo.ID_PRIVATE_INTERNAL.equals(volumeUuid);
+            final boolean allow3rdPartyOnInternal = mContext.getResources().getBoolean(
+                    com.android.internal.R.bool.config_allow3rdPartyAppOnInternal);
+            if (isInternalStorage && !allow3rdPartyOnInternal) {
+                throw new PackageManagerException(MOVE_FAILED_3RD_PARTY_NOT_ALLOWED_ON_INTERNAL,
+                        "3rd party apps are not allowed on internal storage");
+            }
+
             if (pkg.applicationInfo.isExternalAsec()) {
                 currentAsec = true;
                 currentVolumeUuid = StorageManager.UUID_PRIMARY_PHYSICAL;