Add UID isolation CTS test.

CTS test that tests that isolated services cannot access files or make network
requests.

Change-Id: I0f952e04cfb08f301ee282c9dfe132fe206f0a9f
diff --git a/tests/tests/uidisolation/Android.mk b/tests/tests/uidisolation/Android.mk
new file mode 100644
index 0000000..ba82eb5
--- /dev/null
+++ b/tests/tests/uidisolation/Android.mk
@@ -0,0 +1,32 @@
+# Copyright (C) 2012 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.
+
+LOCAL_PATH:= $(call my-dir)
+
+include $(CLEAR_VARS)
+
+# don't include this package in any target
+LOCAL_MODULE_TAGS := optional
+# and when built explicitly put it in the data partition
+LOCAL_MODULE_PATH := $(TARGET_OUT_DATA_APPS)
+
+LOCAL_JAVA_LIBRARIES := android.test.runner
+
+LOCAL_STATIC_JAVA_LIBRARIES := ctstestrunner ctstestserver
+
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+
+LOCAL_PACKAGE_NAME := CtsUidIsolationTestCases
+
+include $(BUILD_CTS_PACKAGE)
diff --git a/tests/tests/uidisolation/AndroidManifest.xml b/tests/tests/uidisolation/AndroidManifest.xml
new file mode 100644
index 0000000..e456a50
--- /dev/null
+++ b/tests/tests/uidisolation/AndroidManifest.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ * Copyright (C) 2012 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.
+ -->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.android.cts.uidisolation">
+
+    <application android:label="UidIsolationTest">
+      <activity android:name="android.uidisolation.cts.ServiceRunnerActivity"
+                android:label="UidIsolationTest"/>
+      <service android:name="android.uidisolation.cts.PermissionTestService"
+               android:process=":remote"/>
+      <service android:name="android.uidisolation.cts.IsolatedPermissionTestService"
+               android:process=":remote"
+               android:isolatedProcess="true"/>
+      <uses-library android:name="android.test.runner" />
+    </application>
+
+   <uses-permission android:name="android.permission.INTERNET"/>
+
+    <instrumentation android:name="android.test.InstrumentationCtsTestRunner"
+                     android:targetPackage="com.android.cts.uidisolation"
+                     android:label="CTS tests of android.uidisolation"/>
+</manifest>
+
diff --git a/tests/tests/uidisolation/assets/hello.html b/tests/tests/uidisolation/assets/hello.html
new file mode 100644
index 0000000..3c4cf1f
--- /dev/null
+++ b/tests/tests/uidisolation/assets/hello.html
@@ -0,0 +1,3 @@
+<html>
+Hello!
+</html>
diff --git a/tests/tests/uidisolation/src/android/uidisolation/cts/IsolatedPermissionTestService.java b/tests/tests/uidisolation/src/android/uidisolation/cts/IsolatedPermissionTestService.java
new file mode 100644
index 0000000..4b1c059
--- /dev/null
+++ b/tests/tests/uidisolation/src/android/uidisolation/cts/IsolatedPermissionTestService.java
@@ -0,0 +1,23 @@
+/*
+ * Copyright (C) 2012 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.uidisolation.cts;
+
+public class IsolatedPermissionTestService extends PermissionTestService {
+    public IsolatedPermissionTestService() {
+        super(false);
+    }
+}
diff --git a/tests/tests/uidisolation/src/android/uidisolation/cts/IsolatedServiceTest.java b/tests/tests/uidisolation/src/android/uidisolation/cts/IsolatedServiceTest.java
new file mode 100644
index 0000000..5b5cc19
--- /dev/null
+++ b/tests/tests/uidisolation/src/android/uidisolation/cts/IsolatedServiceTest.java
@@ -0,0 +1,95 @@
+/*
+ * Copyright (C) 2012 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.uidisolation.cts;
+
+import android.app.Activity;
+import android.app.Instrumentation;
+import android.test.ActivityInstrumentationTestCase2;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class IsolatedServiceTest extends ActivityInstrumentationTestCase2<ServiceRunnerActivity> {
+
+    private ServiceRunnerActivity mActivity;
+
+    private Instrumentation mInstrumentation;
+
+    // The time we are ready to wait for the service to be done running the tests.
+    private static int SERVICE_TIMEOUT = 15000;
+
+    public IsolatedServiceTest() {
+        super(ServiceRunnerActivity.class);
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        mInstrumentation = getInstrumentation();
+        mActivity = getActivity();
+
+        // We create a file with some content, the test will try to access it.
+        FileOutputStream fos = mActivity.openFileOutput(PermissionTestService.FILE_NAME, 0);
+        byte[] content = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+        fos.write(content, 0, content.length);
+        fos.close();
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        try {
+            File f = mActivity.getFileStreamPath(PermissionTestService.FILE_NAME);
+            f.delete();
+            mActivity.finish();
+        } finally {
+            super.tearDown();
+        }
+    }
+
+    private void runServiceTest(final boolean isolated) {
+        mInstrumentation.runOnMainSync(new Runnable() {
+            public void run() {
+                if (isolated) {
+                    mActivity.startIsolatedService();
+                } else {
+                    mActivity.startNonIsolatedService();
+                }
+            }
+        });
+        synchronized (mActivity) {
+            if (mActivity.getSuccess() == null) {
+                try {
+                    mActivity.wait(SERVICE_TIMEOUT);
+                } catch (InterruptedException ie) {
+                }
+            }
+        }
+        Boolean success = mActivity.getSuccess();
+        assertNotNull("Test error: No success status available.", success);
+        assertTrue(success.booleanValue());
+    }
+
+    public void testNonIsolatedService() {
+        runServiceTest(false);
+    }
+
+    public void testIsolatedService() {
+        runServiceTest(true);
+    }
+}
+
diff --git a/tests/tests/uidisolation/src/android/uidisolation/cts/PermissionTestService.java b/tests/tests/uidisolation/src/android/uidisolation/cts/PermissionTestService.java
new file mode 100644
index 0000000..5d7d07d
--- /dev/null
+++ b/tests/tests/uidisolation/src/android/uidisolation/cts/PermissionTestService.java
@@ -0,0 +1,261 @@
+/*
+ * Copyright (C) 2012 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.uidisolation.cts;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.AsyncTask;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.util.Log;
+import android.webkit.cts.CtsTestServer;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.IOException;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class PermissionTestService extends Service {
+    private static String TAG = PermissionTestService.class.getName();
+
+    static final String FILE_NAME = "test_file";
+
+    // Message receieved from the client.
+    static final int MSG_START_TEST = 1;
+
+    // Messages sent to the client.
+    static final int MSG_NOTIFY_TEST_SUCCESS = 2;
+    static final int MSG_NOTIFY_TEST_FAILURE = 3;
+
+    // The different tests types we run.
+    static final int FILE_READ_TEST = 1;
+    static final int FILE_WRITE_TEST = 2;
+    static final int NETWORK_ACCESS_TEST = 3;
+
+    private Messenger mClient;
+
+    // Whether we expect to have permissions to access files, network...
+    boolean mExpectPermissionsAllowed = true;
+
+    class IncomingHandler extends Handler {
+        private PermissionTestService mService;
+
+        IncomingHandler(PermissionTestService service) {
+            mService = service;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            if (msg.what != MSG_START_TEST) {
+                Log.e(TAG, "PermissionTestService received bad message: " + msg.what);
+                super.handleMessage(msg);
+                return;
+            }
+            mService.mClient = msg.replyTo;
+            mService.startTests();
+        }
+    }
+
+    final Messenger mMessenger = new Messenger(new IncomingHandler(this));
+
+    class NetworkTestAsyncTask extends AsyncTask<Void, Void, Boolean> {
+        protected Boolean doInBackground(Void... nothing) {
+            return testNetworkAccess();
+        }
+
+        protected void onPostExecute(Boolean success) {
+            testNetworkAccessDone(success);
+        }
+    }
+
+    public PermissionTestService() {
+        this(true);
+    }
+
+    protected PermissionTestService(boolean expectPermissionsAllowed) {
+        mExpectPermissionsAllowed = expectPermissionsAllowed;
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        return mMessenger.getBinder();
+    }
+
+    private void notifyClientOfFailure(int failingTest) {
+        try {
+            mClient.send(Message.obtain(null, MSG_NOTIFY_TEST_FAILURE, failingTest, 0, null));
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to send message back to client.");
+        }
+    }
+
+    private void notifyClientOfSuccess() {
+        try {
+            mClient.send(Message.obtain(null, MSG_NOTIFY_TEST_SUCCESS));
+        } catch (RemoteException e) {
+            Log.e(TAG, "Failed to send message back to client.");
+        }
+    }
+
+    private void startTests() {
+        if (testFileReadAccess() != mExpectPermissionsAllowed) {
+            notifyClientOfFailure(FILE_READ_TEST);
+            return;
+        }
+        if (testFileWriteAccess() != mExpectPermissionsAllowed) {
+            notifyClientOfFailure(FILE_WRITE_TEST);
+            return;
+        }
+
+        // testNetworkAccess is performed asynchronously and calls testNetworkAccessDone.
+        new NetworkTestAsyncTask().execute();
+    }
+
+    private void testNetworkAccessDone(boolean success) {
+        if (success != mExpectPermissionsAllowed) {
+            notifyClientOfFailure(NETWORK_ACCESS_TEST);
+            return;
+        }
+        notifyClientOfSuccess();
+    }
+
+    private boolean testFileReadAccess() {
+        File f = getApplication().getFileStreamPath(FILE_NAME);
+        if (!f.exists()) {
+            Log.e(TAG, "testFileReadAccess: test file does not exists.");
+            return false;
+        }
+        if (!f.canRead()) {
+            Log.e(TAG, "testFileReadAccess: no permission to read test file.");
+            return false;
+        }
+
+        FileInputStream fis = null;
+        try {
+            fis = new FileInputStream(f);
+            for (int i = 0; i < 10; i++) {
+                int value;
+                try {
+                    value = fis.read();
+                } catch (IOException ioe) {
+                    Log.e(TAG, "testFileReadAccess: failed to read test file, IOException.");
+                    return false;
+                }
+                if (value == -1) {
+                    Log.e(TAG, "testFileReadAccess: failed to read test file.");
+                    return false;
+                }
+                if (value != i) {
+                    Log.e(TAG, "testFileReadAccess: wrong data read from test file.");
+                    return false;
+                }
+            }
+        } catch (FileNotFoundException fnfe) {
+            Log.e(TAG, "testFileReadAccess: failed to read test file, FileNotFoundException.");
+            return false;
+        } finally {
+            if (fis != null) {
+                try {
+                    fis.close();
+                } catch (IOException ioe) {
+                }
+            }
+        }
+    return true;
+}
+
+    private boolean testFileWriteAccess() {
+        FileOutputStream fos = null;
+        try {
+            fos = getApplication().openFileOutput("writeable_file", 0);
+            byte[] content = new byte[] { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
+            fos.write(content, 0, content.length);
+        } catch (FileNotFoundException fnfe) {
+            Log.e(TAG, "Failed to open writable file.");
+            return false;
+        } catch (IOException ioe) {
+            Log.e(TAG, "Failed to write to writable file.");
+            return false;
+        } finally {
+            if (fos != null) {
+                try {
+                    fos.close();
+                } catch (IOException ioe) {
+                }
+            }
+        }
+        return true;
+    }
+
+    private boolean testNetworkAccess() {
+        CtsTestServer webServer = null;
+        try {
+            try {
+                webServer = new CtsTestServer(getApplication());
+            } catch (Exception e) {
+                Log.e(TAG, "Failed to create CtsTestServer.");
+                return false;
+            }
+            URL url;
+            try {
+                url = new URL(webServer.getAssetUrl("hello.html"));
+            } catch (MalformedURLException mue) {
+                Log.e(TAG, "Test is bad, could not create the URL in "
+                        + "PermissionTestService.testNetworkAccess().");
+                return false;
+            }
+            InputStream is = null;
+            try {
+                is = url.openStream();
+                // Attempt to read some bytes.
+                for (int i = 0; i < 10; i++) {
+                    int value = is.read();
+                    if (value == -1) {
+                        Log.e(TAG, "Failed to read byte " + i + " from network connection");
+                        return false;
+                    }
+                }
+            } catch (IOException ioe) {
+                Log.e(TAG, "Failed to read from network connection: " + ioe);
+                return false;
+            } catch (SecurityException se) {
+                Log.e(TAG, "Failed to read from network connection: " + se);
+                return false;
+            } finally {
+                if (is != null) {
+                    try {
+                        is.close();
+                    } catch (IOException ioe) {
+                    }
+                }
+            }
+            Log.i(TAG, "Successfully accessed network.");
+            return true;
+        } finally {
+            if (webServer != null) {
+                webServer.shutdown();
+            }
+        }
+    }
+}
diff --git a/tests/tests/uidisolation/src/android/uidisolation/cts/ServiceRunnerActivity.java b/tests/tests/uidisolation/src/android/uidisolation/cts/ServiceRunnerActivity.java
new file mode 100644
index 0000000..a8e5434
--- /dev/null
+++ b/tests/tests/uidisolation/src/android/uidisolation/cts/ServiceRunnerActivity.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright (C) 2012 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.uidisolation.cts;
+
+import android.app.Activity;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.RemoteException;
+import android.util.Log;
+
+public class ServiceRunnerActivity extends Activity {
+    private static String TAG = ServiceRunnerActivity.class.getName();
+
+    private Messenger mService;
+    private boolean mIsBound;
+
+    private Boolean mSuccess;
+
+    public synchronized Boolean getSuccess() {
+        return mSuccess;
+    }
+
+    // Handler of incoming messages from service.
+    class IncomingHandler extends Handler {
+        private ServiceRunnerActivity mActivity;
+
+        IncomingHandler(ServiceRunnerActivity activity) {
+            mActivity = activity;
+        }
+
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case PermissionTestService.MSG_NOTIFY_TEST_SUCCESS:
+                    synchronized (mActivity) {
+                        mActivity.mSuccess = Boolean.TRUE;
+                        mActivity.notify();
+                    }
+                    doUnbindService();
+                   break;
+                case PermissionTestService.MSG_NOTIFY_TEST_FAILURE:
+                    synchronized (mActivity) {
+                        mActivity.mSuccess = Boolean.FALSE;
+                        mActivity.notify();
+                    }
+                    doUnbindService();
+                    break;
+               default:
+                   super.handleMessage(msg);
+                   return;
+            }
+        }
+    }
+
+    // Target we publish for clients to send messages to IncomingHandler.
+    final Messenger mMessenger = new Messenger(new IncomingHandler(this));
+
+    private ServiceConnection mConnection = new ServiceConnection() {
+        @Override
+        public void onServiceConnected(ComponentName className, IBinder service) {
+            mService = new Messenger(service);
+
+            // Send a message to the service to register.
+            try {
+                Message msg = Message.obtain(null, PermissionTestService.MSG_START_TEST);
+                msg.replyTo = mMessenger;
+                mService.send(msg);
+            } catch (RemoteException e) {
+                // In this case the service has crashed before we could even do anything.
+                Log.e(TAG, "Failed to send start message to service.");
+            }
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName className) {
+            // This is called when the connection with the service has been unexpectedly
+            // disconnected -- that is, its process crashed.
+            Log.e(TAG, "Service disconnected.");
+            mService = null;
+        }
+    };
+
+
+    void doBindService(boolean isolated) {
+        bindService(new Intent(this, isolated
+                ? IsolatedPermissionTestService.class
+                : PermissionTestService.class),
+                mConnection, Context.BIND_AUTO_CREATE);
+        mIsBound = true;
+    }
+
+    void doUnbindService() {
+        // Detach our existing connection.
+        unbindService(mConnection);
+        mIsBound = false;
+    }
+
+    void startNonIsolatedService() {
+        doBindService(false);
+    }
+
+    void startIsolatedService() {
+        doBindService(true);
+    }
+}