/*
 * Copyright (C) 2010 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 android.content.Context;
import android.content.res.Resources;
import android.content.res.Resources.NotFoundException;
import android.os.FileUtils;
import android.os.storage.OnObbStateChangeListener;
import android.os.storage.StorageManager;
import android.test.AndroidTestCase;
import android.test.ComparisonFailure;
import android.test.suitebuilder.annotation.LargeTest;
import android.util.Log;

import static com.android.server.MountService.buildObbPath;

import com.android.frameworks.servicestests.R;

import java.io.File;
import java.io.InputStream;

public class MountServiceTests extends AndroidTestCase {
    private static final String TAG = "MountServiceTests";

    private static final long MAX_WAIT_TIME = 25*1000;
    private static final long WAIT_TIME_INCR = 5*1000;

    private static final String OBB_MOUNT_PREFIX = "/mnt/obb/";

    @Override
    protected void setUp() throws Exception {
        super.setUp();
    }

    @Override
    protected void tearDown() throws Exception {
        super.tearDown();
    }

    private static void assertStartsWith(String message, String prefix, String actual) {
        if (!actual.startsWith(prefix)) {
            throw new ComparisonFailure(message, prefix, actual);
        }
    }

    private static class ObbObserver extends OnObbStateChangeListener {
        private String path;

        public int state = -1;
        boolean done = false;

        @Override
        public void onObbStateChange(String path, int state) {
            Log.d(TAG, "Received message.  path=" + path + ", state=" + state);
            synchronized (this) {
                this.path = path;
                this.state = state;
                done = true;
                notifyAll();
            }
        }

        public String getPath() {
            assertTrue("Expected ObbObserver to have received a state change.", done);
            return path;
        }

        public int getState() {
            assertTrue("Expected ObbObserver to have received a state change.", done);
            return state;
        }

        public void reset() {
            this.path = null;
            this.state = -1;
            done = false;
        }

        public boolean isDone() {
            return done;
        }

        public boolean waitForCompletion() {
            long waitTime = 0;
            synchronized (this) {
                while (!isDone() && waitTime < MAX_WAIT_TIME) {
                    try {
                        wait(WAIT_TIME_INCR);
                        waitTime += WAIT_TIME_INCR;
                    } catch (InterruptedException e) {
                        Log.i(TAG, "Interrupted during sleep", e);
                    }
                }
            }

            return isDone();
        }
    }

    private File getFilePath(String name) {
        final File filesDir = mContext.getFilesDir();
        final File outFile = new File(filesDir, name);
        return outFile;
    }

    private void copyRawToFile(int rawResId, File outFile) {
        Resources res = mContext.getResources();
        InputStream is = null;
        try {
            is = res.openRawResource(rawResId);
        } catch (NotFoundException e) {
            fail("Failed to load resource with id: " + rawResId);
        }
        FileUtils.setPermissions(outFile.getPath(), FileUtils.S_IRWXU | FileUtils.S_IRWXG
                | FileUtils.S_IRWXO, -1, -1);
        assertTrue(FileUtils.copyToFile(is, outFile));
        FileUtils.setPermissions(outFile.getPath(), FileUtils.S_IRWXU | FileUtils.S_IRWXG
                | FileUtils.S_IRWXO, -1, -1);
    }

    private StorageManager getStorageManager() {
        return (StorageManager) getContext().getSystemService(Context.STORAGE_SERVICE);
    }

    private void mountObb(StorageManager sm, final int resource, final File file,
            int expectedState) {
        copyRawToFile(resource, file);

        final ObbObserver observer = new ObbObserver();
        assertTrue("mountObb call on " + file.getPath() + " should succeed",
                sm.mountObb(file.getPath(), null, observer));

        assertTrue("Mount should have completed",
                observer.waitForCompletion());

        if (expectedState == OnObbStateChangeListener.MOUNTED) {
            assertTrue("OBB should be mounted", sm.isObbMounted(file.getPath()));
        }

        assertEquals("Actual file and resolved file should be the same",
                file.getPath(), observer.getPath());

        assertEquals(expectedState, observer.getState());
    }

    private ObbObserver mountObbWithoutWait(final StorageManager sm, final int resource,
            final File file) {
        copyRawToFile(resource, file);

        final ObbObserver observer = new ObbObserver();
        assertTrue("mountObb call on " + file.getPath() + " should succeed", sm.mountObb(file
                .getPath(), null, observer));

        return observer;
    }

    private void waitForObbActionCompletion(final StorageManager sm, final File file,
            final ObbObserver observer, int expectedState, boolean checkPath) {
        assertTrue("Mount should have completed", observer.waitForCompletion());

        assertTrue("OBB should be mounted", sm.isObbMounted(file.getPath()));

        if (checkPath) {
            assertEquals("Actual file and resolved file should be the same", file.getPath(),
                    observer.getPath());
        }

        assertEquals(expectedState, observer.getState());
    }

    private String checkMountedPath(final StorageManager sm, final File file) {
        final String mountPath = sm.getMountedObbPath(file.getPath());
        assertStartsWith("Path should be in " + OBB_MOUNT_PREFIX,
                OBB_MOUNT_PREFIX,
                mountPath);
        return mountPath;
    }

    private void unmountObb(final StorageManager sm, final File file, int expectedState) {
        final ObbObserver observer = new ObbObserver();

        assertTrue("unmountObb call on test1.obb should succeed",
 sm.unmountObb(file.getPath(),
                false, observer));

        assertTrue("Unmount should have completed",
                observer.waitForCompletion());

        assertEquals(expectedState, observer.getState());

        if (expectedState == OnObbStateChangeListener.UNMOUNTED) {
            assertFalse("OBB should not be mounted", sm.isObbMounted(file.getPath()));
        }
    }

    @LargeTest
    public void testMountAndUnmountObbNormal() {
        StorageManager sm = getStorageManager();

        final File outFile = getFilePath("test1.obb");

        mountObb(sm, R.raw.test1, outFile, OnObbStateChangeListener.MOUNTED);

        mountObb(sm, R.raw.test1, outFile, OnObbStateChangeListener.ERROR_ALREADY_MOUNTED);

        final String mountPath = checkMountedPath(sm, outFile);
        final File mountDir = new File(mountPath);

        assertTrue("OBB mounted path should be a directory",
                mountDir.isDirectory());

        unmountObb(sm, outFile, OnObbStateChangeListener.UNMOUNTED);
    }

    @LargeTest
    public void testAttemptMountNonObb() {
        StorageManager sm = getStorageManager();

        final File outFile = getFilePath("test1_nosig.obb");

        mountObb(sm, R.raw.test1_nosig, outFile, OnObbStateChangeListener.ERROR_INTERNAL);

        assertFalse("OBB should not be mounted",
                sm.isObbMounted(outFile.getPath()));

        assertNull("OBB's mounted path should be null",
                sm.getMountedObbPath(outFile.getPath()));
    }

    @LargeTest
    public void testAttemptMountObbWrongPackage() {
        StorageManager sm = getStorageManager();

        final File outFile = getFilePath("test1_wrongpackage.obb");

        mountObb(sm, R.raw.test1_wrongpackage, outFile,
                OnObbStateChangeListener.ERROR_PERMISSION_DENIED);

        assertFalse("OBB should not be mounted",
                sm.isObbMounted(outFile.getPath()));

        assertNull("OBB's mounted path should be null",
                sm.getMountedObbPath(outFile.getPath()));
    }

    @LargeTest
    public void testMountAndUnmountTwoObbs() {
        StorageManager sm = getStorageManager();

        final File file1 = getFilePath("test1.obb");
        final File file2 = getFilePath("test2.obb");

        ObbObserver oo1 = mountObbWithoutWait(sm, R.raw.test1, file1);
        ObbObserver oo2 = mountObbWithoutWait(sm, R.raw.test1, file2);

        Log.d(TAG, "Waiting for OBB #1 to complete mount");
        waitForObbActionCompletion(sm, file1, oo1, OnObbStateChangeListener.MOUNTED, false);
        Log.d(TAG, "Waiting for OBB #2 to complete mount");
        waitForObbActionCompletion(sm, file2, oo2, OnObbStateChangeListener.MOUNTED, false);

        final String mountPath1 = checkMountedPath(sm, file1);
        final File mountDir1 = new File(mountPath1);
        assertTrue("OBB mounted path should be a directory", mountDir1.isDirectory());

        final String mountPath2 = checkMountedPath(sm, file2);
        final File mountDir2 = new File(mountPath2);
        assertTrue("OBB mounted path should be a directory", mountDir2.isDirectory());

        unmountObb(sm, file1, OnObbStateChangeListener.UNMOUNTED);
        unmountObb(sm, file2, OnObbStateChangeListener.UNMOUNTED);
    }

    public void testBuildObbPath() {
        final int userId = 10;

        // Paths outside external storage should remain untouched
        assertEquals("/storage/random/foo",
                buildObbPath("/storage/random/foo", userId, true));
        assertEquals("/storage/random/foo",
                buildObbPath("/storage/random/foo", userId, false));

        // Paths on user-specific emulated storage
        assertEquals("/mnt/shell/emulated/10/foo",
                buildObbPath("/storage/emulated_legacy/foo", userId, true));
        assertEquals("/storage/emulated/10/foo",
                buildObbPath("/storage/emulated_legacy/foo", userId, false));
        assertEquals("/mnt/shell/emulated/10/foo",
                buildObbPath("/storage/emulated/10/foo", userId, true));
        assertEquals("/storage/emulated/10/foo",
                buildObbPath("/storage/emulated/10/foo", userId, false));

        // Paths on shared OBB emulated storage
        assertEquals("/mnt/shell/emulated/obb/foo",
                buildObbPath("/storage/emulated_legacy/Android/obb/foo", userId, true));
        assertEquals("/storage/emulated/0/Android/obb/foo",
                buildObbPath("/storage/emulated_legacy/Android/obb/foo", userId, false));
        assertEquals("/mnt/shell/emulated/obb/foo",
                buildObbPath("/storage/emulated/10/Android/obb/foo", userId, true));
        assertEquals("/storage/emulated/0/Android/obb/foo",
                buildObbPath("/storage/emulated/10/Android/obb/foo", userId, false));
    }
}
