/*
 * Copyright (C) 2011 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.content.pm;

import android.content.Context;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.os.storage.IStorageManager;
import android.os.storage.StorageManager;
import android.os.storage.VolumeInfo;
import android.test.AndroidTestCase;
import android.util.Log;

import com.android.internal.content.PackageHelper;

import org.mockito.Mockito;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;

import static android.net.TrafficStats.MB_IN_BYTES;
import static android.os.storage.VolumeInfo.STATE_MOUNTED;

public class PackageHelperTests extends AndroidTestCase {
    private static final boolean localLOGV = true;
    public static final String TAG = "PackageHelperTests";
    protected final String PREFIX = "android.content.pm";
    private IStorageManager mSm;
    private String fullId;
    private String fullId2;

    private static final String sInternalVolPath = "/data";
    private static final String sAdoptedVolPath = "/mnt/expand/123";
    private static final String sPublicVolPath = "/emulated";

    private static final String sInternalVolUuid = StorageManager.UUID_PRIVATE_INTERNAL;
    private static final String sAdoptedVolUuid = "adopted";
    private static final String sPublicVolUuid = "emulated";

    private static final long sInternalSize = 20000;
    private static final long sAdoptedSize = 10000;
    private static final long sPublicSize = 1000000;

    private static StorageManager sStorageManager;

    private static StorageManager createStorageManagerMock() throws Exception {
        VolumeInfo internalVol = new VolumeInfo("private",
                VolumeInfo.TYPE_PRIVATE, null /*DiskInfo*/, null /*partGuid*/);
        internalVol.path = sInternalVolPath;
        internalVol.state = STATE_MOUNTED;
        internalVol.fsUuid = sInternalVolUuid;

        VolumeInfo adoptedVol = new VolumeInfo("adopted",
                VolumeInfo.TYPE_PRIVATE, null /*DiskInfo*/, null /*partGuid*/);
        adoptedVol.path = sAdoptedVolPath;
        adoptedVol.state = STATE_MOUNTED;
        adoptedVol.fsUuid = sAdoptedVolUuid;

        VolumeInfo publicVol = new VolumeInfo("public",
                VolumeInfo.TYPE_PUBLIC, null /*DiskInfo*/, null /*partGuid*/);
        publicVol.state = STATE_MOUNTED;
        publicVol.path = sPublicVolPath;
        publicVol.fsUuid = sPublicVolUuid;

        List<VolumeInfo> volumes = new ArrayList<>();
        volumes.add(internalVol);
        volumes.add(adoptedVol);
        volumes.add(publicVol);

        StorageManager storageManager = Mockito.mock(StorageManager.class);
        Mockito.when(storageManager.getVolumes()).thenReturn(volumes);

        File internalFile = new File(sInternalVolPath);
        File adoptedFile = new File(sAdoptedVolPath);
        File publicFile = new File(sPublicVolPath);
        UUID internalUuid = UUID.randomUUID();
        UUID adoptedUuid = UUID.randomUUID();
        UUID publicUuid = UUID.randomUUID();
        Mockito.when(storageManager.getStorageBytesUntilLow(internalFile)).thenReturn(sInternalSize);
        Mockito.when(storageManager.getStorageBytesUntilLow(adoptedFile)).thenReturn(sAdoptedSize);
        Mockito.when(storageManager.getStorageBytesUntilLow(publicFile)).thenReturn(sPublicSize);
        Mockito.when(storageManager.getUuidForPath(Mockito.eq(internalFile))).thenReturn(internalUuid);
        Mockito.when(storageManager.getUuidForPath(Mockito.eq(adoptedFile))).thenReturn(adoptedUuid);
        Mockito.when(storageManager.getUuidForPath(Mockito.eq(publicFile))).thenReturn(publicUuid);
        Mockito.when(storageManager.getAllocatableBytes(Mockito.eq(internalUuid), Mockito.anyInt()))
                .thenReturn(sInternalSize);
        Mockito.when(storageManager.getAllocatableBytes(Mockito.eq(adoptedUuid), Mockito.anyInt()))
                .thenReturn(sAdoptedSize);
        Mockito.when(storageManager.getAllocatableBytes(Mockito.eq(publicUuid), Mockito.anyInt()))
                .thenReturn(sPublicSize);
        return storageManager;
    }

    private static final class MockedInterface extends PackageHelper.TestableInterface {
        private boolean mForceAllowOnExternal = false;
        private boolean mAllow3rdPartyOnInternal = true;
        private ApplicationInfo mApplicationInfo = null;

        public void setMockValues(ApplicationInfo applicationInfo,
                boolean forceAllowOnExternal, boolean allow3rdPartyOnInternal) {
            mForceAllowOnExternal = forceAllowOnExternal;
            mAllow3rdPartyOnInternal = allow3rdPartyOnInternal;
            mApplicationInfo = applicationInfo;
        }

        @Override
        public StorageManager getStorageManager(Context context) {
            return sStorageManager;
        }

        @Override
        public boolean getForceAllowOnExternalSetting(Context context) {
            return mForceAllowOnExternal;
        }

        @Override
        public boolean getAllow3rdPartyOnInternalConfig(Context context) {
            return mAllow3rdPartyOnInternal;
        }

        @Override
        public ApplicationInfo getExistingAppInfo(Context context, String packagename) {
            return mApplicationInfo;
        }

        @Override
        public File getDataDirectory() {
            return new File(sInternalVolPath);
        }
    }

    private IStorageManager getSm() {
        IBinder service = ServiceManager.getService("mount");
        if (service != null) {
            return IStorageManager.Stub.asInterface(service);
        } else {
            Log.e(TAG, "Can't get mount service");
        }
        return null;
    }

    private void cleanupContainers() throws RemoteException {
        Log.d(TAG,"cleanUp");
        IStorageManager sm = getSm();
        String[] containers = sm.getSecureContainerList();
        for (int i = 0; i < containers.length; i++) {
            if (containers[i].startsWith(PREFIX)) {
                Log.d(TAG,"cleaing up "+containers[i]);
                sm.destroySecureContainer(containers[i], true);
            }
        }
    }

    @Override
    protected void setUp() throws Exception {
        super.setUp();
        sStorageManager = createStorageManagerMock();
        if (localLOGV) Log.i(TAG, "Cleaning out old test containers");
        cleanupContainers();
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
        sStorageManager = null;
        if (localLOGV) Log.i(TAG, "Cleaning out old test containers");
        cleanupContainers();
    }

    public void testMountAndPullSdCard() throws Exception {
        fullId = PREFIX;
        fullId2 = PackageHelper.createSdDir(1024 * MB_IN_BYTES, fullId, "none",
                android.os.Process.myUid(), true);

        Log.d(TAG, "getSdDir=" + PackageHelper.getSdDir(fullId));
        PackageHelper.unMountSdDir(fullId);

        Runnable r1 = getMountRunnable();
        Runnable r2 = getDestroyRunnable();
        Thread thread = new Thread(r1);
        Thread thread2 = new Thread(r2);
        thread2.start();
        thread.start();
    }

    public Runnable getMountRunnable() {
        Runnable r = new Runnable () {
            public void run () {
                try {
                    Thread.sleep(5);
                    String path = PackageHelper.mountSdDir(fullId, "none",
                            android.os.Process.myUid());
                    Log.e(TAG, "mount done " + path);
                } catch (IllegalArgumentException iae) {
                    throw iae;
                } catch (Throwable t) {
                    Log.e(TAG, "mount failed", t);
                }
            }
        };
        return r;
    }

    public Runnable getDestroyRunnable() {
        Runnable r = new Runnable () {
            public void run () {
                try {
                    PackageHelper.destroySdDir(fullId);
                    Log.e(TAG, "destroy done: " + fullId);
                } catch (Throwable t) {
                    Log.e(TAG, "destroy failed", t);
                }
            }
        };
        return r;
    }

    public void testResolveInstallVolumeInternal_SystemApp() throws IOException {
        ApplicationInfo systemAppInfo = new ApplicationInfo();
        systemAppInfo.flags = ApplicationInfo.FLAG_SYSTEM;

        // All test cases for when the system app fits on internal.
        MockedInterface mockedInterface = new MockedInterface();
        mockedInterface.setMockValues(systemAppInfo, false /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        String volume = null;
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
            1 /*install location*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(StorageManager.UUID_PRIVATE_INTERNAL, volume);

        mockedInterface.setMockValues(systemAppInfo, true /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                1 /*install location*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(StorageManager.UUID_PRIVATE_INTERNAL, volume);

        mockedInterface.setMockValues(systemAppInfo, false /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                1 /*install location*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(StorageManager.UUID_PRIVATE_INTERNAL, volume);

        mockedInterface.setMockValues(systemAppInfo, true /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                1 /*install location*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(StorageManager.UUID_PRIVATE_INTERNAL, volume);


        // All test cases for when the system app does not fit on internal.
        // Exception should be thrown.
        mockedInterface.setMockValues(systemAppInfo, true /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        try {
            PackageHelper.resolveInstallVolume(getContext(), "package.name",
                    1 /*install location*/, 1000000 /*size bytes*/, mockedInterface);
            fail("Expected exception in resolveInstallVolume was not thrown");
        } catch(IOException e) {
            // expected
        }

        mockedInterface.setMockValues(systemAppInfo, false /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        try {
            PackageHelper.resolveInstallVolume(getContext(), "package.name",
                    1 /*install location*/, 1000000 /*size bytes*/, mockedInterface);
            fail("Expected exception in resolveInstallVolume was not thrown");
        } catch(IOException e) {
            // expected
        }

        mockedInterface.setMockValues(systemAppInfo, false /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        try {
            PackageHelper.resolveInstallVolume(getContext(), "package.name",
                    1 /*install location*/, 1000000 /*size bytes*/, mockedInterface);
            fail("Expected exception in resolveInstallVolume was not thrown");
        } catch(IOException e) {
            // expected
        }

        mockedInterface.setMockValues(systemAppInfo, true /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        try {
            PackageHelper.resolveInstallVolume(getContext(), "package.name",
                    1 /*install location*/, 1000000 /*size bytes*/, mockedInterface);
            fail("Expected exception in resolveInstallVolume was not thrown");
        } catch(IOException e) {
            // expected
        }
    }

    public void testResolveInstallVolumeInternal_3rdParty_existing_not_too_big()
            throws IOException {
        // Existing apps always stay on the same volume.
        // Test cases for existing app on internal.
        ApplicationInfo appInfo = new ApplicationInfo();
        appInfo.volumeUuid = sInternalVolUuid;
        MockedInterface mockedInterface = new MockedInterface();
        mockedInterface.setMockValues(appInfo, false /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        String volume = null;
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                0 /*install location*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(sInternalVolUuid, volume);

        mockedInterface.setMockValues(appInfo, true /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                0 /*install location*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(sInternalVolUuid, volume);
    }

    public void testResolveInstallVolumeInternal_3rdParty_existing_not_too_big_adopted()
            throws IOException {
        // Test cases for existing app on the adopted media.
        ApplicationInfo appInfo = new ApplicationInfo();
        MockedInterface mockedInterface = new MockedInterface();
        String volume;
        appInfo.volumeUuid = sAdoptedVolUuid;
        mockedInterface.setMockValues(appInfo, false /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
            0 /*install location*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(sAdoptedVolUuid, volume);

        mockedInterface.setMockValues(appInfo, true /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                0 /*install location*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(sAdoptedVolUuid, volume);

        mockedInterface.setMockValues(appInfo, false /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                0 /*install location*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(sAdoptedVolUuid, volume);

        mockedInterface.setMockValues(appInfo, true /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                0 /*install location*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(sAdoptedVolUuid, volume);
    }

    public void testResolveInstallVolumeAdopted_3rdParty_existing_too_big() {
        // Test: update size too big, will throw exception.
        ApplicationInfo appInfo = new ApplicationInfo();
        appInfo.volumeUuid = sAdoptedVolUuid;

        MockedInterface mockedInterface = new MockedInterface();
        mockedInterface.setMockValues(appInfo, false /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        try {
            PackageHelper.resolveInstallVolume(getContext(), "package.name",
                    0 /*install location*/, 10000001 /*BIG size, won't fit*/, mockedInterface);
            fail("Expected exception was not thrown " + appInfo.volumeUuid);
        } catch (IOException e) {
            //expected
        }

        mockedInterface.setMockValues(appInfo, false /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        try {
            PackageHelper.resolveInstallVolume(getContext(), "package.name",
                    0 /*install location*/, 10000001 /*BIG size, won't fit*/, mockedInterface);
            fail("Expected exception was not thrown " + appInfo.volumeUuid);
        } catch (IOException e) {
            //expected
        }

        mockedInterface.setMockValues(appInfo, true /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        try {
            PackageHelper.resolveInstallVolume(getContext(), "package.name",
                    0 /*install location*/, 10000001 /*BIG size, won't fit*/, mockedInterface);
            fail("Expected exception was not thrown " + appInfo.volumeUuid);
        } catch (IOException e) {
            //expected
        }

        mockedInterface.setMockValues(appInfo, true /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        try {
            PackageHelper.resolveInstallVolume(getContext(), "package.name",
                    0 /*install location*/, 10000001 /*BIG size, won't fit*/, mockedInterface);
            fail("Expected exception was not thrown " + appInfo.volumeUuid);
        } catch (IOException e) {
            //expected
        }
    }

    public void testResolveInstallVolumeInternal_3rdParty_auto() throws IOException {
        ApplicationInfo appInfo = null;
        MockedInterface mockedInterface = new MockedInterface();
        mockedInterface.setMockValues(appInfo, false /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        String volume = null;
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
            0 /*install location auto*/, 1000 /*size bytes*/, mockedInterface);
        // Should return the volume with bigger available space.
        assertEquals(sInternalVolUuid, volume);

        mockedInterface.setMockValues(appInfo, true /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                0 /*install location auto*/, 1000 /*size bytes*/, mockedInterface);
        // Should return the volume with bigger available space.
        assertEquals(sInternalVolUuid, volume);

        mockedInterface.setMockValues(appInfo, true /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                0 /*install location auto*/, 1000 /*size bytes*/, mockedInterface);
        // Should return the volume with bigger available space.
        assertEquals(sAdoptedVolUuid, volume);

        mockedInterface.setMockValues(appInfo, false /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                0 /*install location auto*/, 1000 /*size bytes*/, mockedInterface);
        // Should return the volume with bigger available space.
        assertEquals(sAdoptedVolUuid, volume);


    }

    public void testResolveInstallVolumeInternal_3rdParty_internal_only() throws IOException {
        ApplicationInfo appInfo = null;
        MockedInterface mockedInterface = new MockedInterface();
        mockedInterface.setMockValues(appInfo, false /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        String volume = null;
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
            1 /*install location internal ONLY*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(sInternalVolUuid, volume);

        mockedInterface.setMockValues(appInfo, true /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                1 /*install location internal ONLY*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(sInternalVolUuid, volume);

        mockedInterface.setMockValues(appInfo, false /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        try {
            volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                    1 /*install location internal only*/, 1000 /*size bytes*/, mockedInterface);
            fail("Expected exception in resolveInstallVolume was not thrown");
        } catch (IOException e) {
            //expected
        }

        appInfo = null;
        mockedInterface.setMockValues(appInfo, true /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        volume = null;
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                1 /*install location internal only*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(sAdoptedVolUuid, volume);
    }

    public void testResolveInstallVolumeInternal_3rdParty_not_allowed_on_internal()
        throws IOException {
        ApplicationInfo appInfo = null;
        MockedInterface mockedInterface = new MockedInterface();
        mockedInterface.setMockValues(appInfo, false /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        String volume = null;
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
            0 /*install location auto*/, 1000 /*size bytes*/, mockedInterface);
        // Should return the non-internal volume.
        assertEquals(sAdoptedVolUuid, volume);

        appInfo = null;
        mockedInterface.setMockValues(appInfo, true /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                0 /*install location auto*/, 1000 /*size bytes*/, mockedInterface);
        // Should return the non-internal volume.
        assertEquals(sAdoptedVolUuid, volume);
    }

    public void testResolveInstallVolumeInternal_3rdParty_internal_only_too_big() {
        ApplicationInfo appInfo = null;
        MockedInterface mockedInterface = new MockedInterface();
        mockedInterface.setMockValues(appInfo, false /*force allow on external*/,
                true /*allow 3rd party on internal*/);
        String volume = null;
        try {
            volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                    1 /*install location internal ONLY*/,
                    1000000 /*size too big*/, mockedInterface);
            fail("Expected exception in resolveInstallVolume was not thrown");
        } catch (IOException e) {
            //expected
        }
    }

    public void testResolveInstallVolumeInternal_3rdParty_internal_only_not_allowed()
        throws IOException {
        ApplicationInfo appInfo = null;
        MockedInterface mockedInterface = new MockedInterface();
        mockedInterface.setMockValues(appInfo, false /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        String volume = null;
        try {
            volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
                    1 /*install location internal only*/, 1000 /*size bytes*/, mockedInterface);
            fail("Expected exception in resolveInstallVolume was not thrown");
        } catch (IOException e) {
            //expected
        }

        appInfo = null;
        mockedInterface.setMockValues(appInfo, true /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        volume = null;
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
            1 /*install location internal only*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(sAdoptedVolUuid, volume);

    }

    public void testResolveInstallVolumeInternal_3rdParty_internal_only_forced_to_external()
        throws IOException {
        // New/existing installation: New
        // app request location: Internal Only
        // 3rd party allowed on internal: False
        // Force allow external in setting: True
        // Size fit? Yes
        ApplicationInfo appInfo = null;
        MockedInterface mockedInterface = new MockedInterface();
        mockedInterface.setMockValues(appInfo, true /*force allow on external*/,
                false /*allow 3rd party on internal*/);
        String volume = null;
        volume = PackageHelper.resolveInstallVolume(getContext(), "package.name",
            1 /*install location internal only*/, 1000 /*size bytes*/, mockedInterface);
        assertEquals(sAdoptedVolUuid, volume);
    }
}
