chromium/chrome/android/webapk/libs/client/junit/src/org/chromium/webapk/lib/client/WebApkServiceConnectionManagerTest.java

// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.webapk.lib.client;

import android.content.ComponentName;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;
import android.text.TextUtils;

import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.annotation.LooperMode;
import org.robolectric.shadows.ShadowApplication;

import org.chromium.base.task.PostTask;
import org.chromium.base.task.TaskTraits;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.concurrent.Executor;

/** Unit tests for {@link org.chromium.webapk.lib.client.WebApkServiceConnectionManager}. */
@RunWith(RobolectricTestRunner.class)
@Config(manifest = Config.NONE)
@LooperMode(LooperMode.Mode.LEGACY)
public class WebApkServiceConnectionManagerTest {
    private static class TestExecutor implements Executor {
        private LinkedList<Runnable> mPendingTasks = new LinkedList<>();

        @Override
        public void execute(Runnable command) {
            mPendingTasks.add(command);
        }

        public void runPendingTasks() {
            while (!mPendingTasks.isEmpty()) {
                mPendingTasks.pop().run();
            }
        }
    }

    private static final String WEBAPK_PACKAGE = "com.webapk.package";

    private static final String CATEGORY_WEBAPK_SERVICE_API = "android.intent.category.WEBAPK_API";

    private ShadowApplication mShadowApplication;
    private TestExecutor mTestExecutor = new TestExecutor();
    private WebApkServiceConnectionManager mConnectionManager;

    private class TestCallback implements WebApkServiceConnectionManager.ConnectionCallback {
        public boolean mGotResult;
        public IBinder mService;

        @Override
        public void onConnected(IBinder service) {
            mGotResult = true;
            mService = service;
        }
    }

    @Before
    public void setUp() {
        mShadowApplication = Shadows.shadowOf(RuntimeEnvironment.application);
        mShadowApplication.setComponentNameAndServiceForBindService(
                new ComponentName(WEBAPK_PACKAGE, ""), Mockito.mock(IBinder.class));
        PostTask.setPrenativeThreadPoolExecutorForTesting(mTestExecutor);
        mConnectionManager =
                new WebApkServiceConnectionManager(
                        TaskTraits.BEST_EFFORT_MAY_BLOCK,
                        CATEGORY_WEBAPK_SERVICE_API,
                        /* action= */ null);
    }

    @After
    public void tearDown() {
        mConnectionManager.disconnectAll(RuntimeEnvironment.application);
        mTestExecutor.runPendingTasks();
    }

    /**
     * Test that a connection request to a WebAPK's service does not create a new connection if one
     * already exists.
     */
    @Test
    public void testAfterConnectionEstablished() {
        TestCallback callback1 = new TestCallback();
        TestCallback callback2 = new TestCallback();

        mConnectionManager.connect(RuntimeEnvironment.application, WEBAPK_PACKAGE, callback1);
        mTestExecutor.runPendingTasks();
        mConnectionManager.connect(RuntimeEnvironment.application, WEBAPK_PACKAGE, callback2);
        mTestExecutor.runPendingTasks();

        // Only one connection should have been created.
        Assert.assertEquals(WEBAPK_PACKAGE, getNextStartedServicePackage());
        Assert.assertEquals(null, getNextStartedServicePackage());

        // Both callbacks should have been called.
        Assert.assertTrue(callback1.mGotResult);
        Assert.assertTrue(callback2.mGotResult);
    }

    /**
     * Test connecting to a WebAPK when Chrome is in the process of establishing a connection to the
     * WebAPK but has not established a connection yet.
     */
    @Test
    public void testConnectWhileConnectionBeingEstablished() {
        // Context for testing {@link Context#bindService()} occurring asynchronously.
        class AsyncBindContext extends ContextWrapper {
            private ServiceConnection mConnection;

            public AsyncBindContext() {
                super(null);
            }

            // Establish pending connection created in {@link #bindService}.
            public void establishServiceConnection() {
                if (mConnection != null) {
                    mConnection.onServiceConnected(null, null);
                }
            }

            @Override
            public Context getApplicationContext() {
                // Need to return real context so that ContextUtils#fetchAppSharedPreferences() does
                // not crash.
                return RuntimeEnvironment.application;
            }

            // Create pending connection.
            @Override
            public boolean bindService(Intent service, ServiceConnection connection, int flags) {
                mConnection = connection;
                return true;
            }
        }

        AsyncBindContext asyncBindContext = new AsyncBindContext();

        TestCallback callback1 = new TestCallback();
        TestCallback callback2 = new TestCallback();
        TestCallback callback3 = new TestCallback();

        mConnectionManager.connect(asyncBindContext, WEBAPK_PACKAGE, callback1);
        mConnectionManager.connect(asyncBindContext, WEBAPK_PACKAGE, callback2);
        mTestExecutor.runPendingTasks();

        mConnectionManager.connect(asyncBindContext, WEBAPK_PACKAGE, callback3);
        mTestExecutor.runPendingTasks();

        // The connection has not been established yet. None of the callbacks should have been
        // called.
        Assert.assertFalse(callback1.mGotResult);
        Assert.assertFalse(callback2.mGotResult);
        Assert.assertFalse(callback3.mGotResult);

        // Establishing the connection should cause all of the callbacks to be called.
        asyncBindContext.establishServiceConnection();
        Assert.assertTrue(callback1.mGotResult);
        Assert.assertTrue(callback2.mGotResult);
        Assert.assertTrue(callback3.mGotResult);
    }

    /**
     * Context which records order of {@link Context#bindService()} and {@link
     * Context#unbindService()} calls.
     */
    private static class BindUnbindRecordingContext extends ContextWrapper {
        private String mRecordPackage;
        private ArrayList<Boolean> mStartStopServiceSequence = new ArrayList<>();
        private HashSet<ServiceConnection> mTrackedConnections = new HashSet<>();

        public BindUnbindRecordingContext(String recordPackage) {
            super(null);
            mRecordPackage = recordPackage;
        }

        public ArrayList<Boolean> getStartStopServiceSequence() {
            return mStartStopServiceSequence;
        }

        @Override
        public Context getApplicationContext() {
            // Need to return real context so that ContextUtils#fetchAppSharedPreferences() does
            // not crash.
            return RuntimeEnvironment.application;
        }

        // Create pending connection.
        @Override
        public boolean bindService(Intent intent, ServiceConnection connection, int flags) {
            connection.onServiceConnected(
                    new ComponentName(mRecordPackage, "random"), Mockito.mock(IBinder.class));
            if (TextUtils.equals(intent.getPackage(), mRecordPackage)) {
                mTrackedConnections.add(connection);
                mStartStopServiceSequence.add(true);
            }
            return true;
        }

        @Override
        public void unbindService(ServiceConnection connection) {
            connection.onServiceDisconnected(new ComponentName(mRecordPackage, "random"));
            if (mTrackedConnections.contains(connection)) {
                mStartStopServiceSequence.add(false);
            }
        }
    }

    /** Test reconnecting to a WebAPK's service. */
    @Test
    public void testConnectDisconnectConnect() {
        final int flagRunBackgroundTasksAfterConnect = 0x1;
        final int flagRunBackgroundTasksAfterDisconnect = 0x2;

        final int[] testCases =
                new int[] {
                    0,
                    flagRunBackgroundTasksAfterConnect,
                    flagRunBackgroundTasksAfterDisconnect,
                    flagRunBackgroundTasksAfterConnect | flagRunBackgroundTasksAfterDisconnect
                };

        for (int testCase : testCases) {
            BindUnbindRecordingContext recordingContext =
                    new BindUnbindRecordingContext(WEBAPK_PACKAGE);
            TestCallback callback1 = new TestCallback();
            TestCallback callback2 = new TestCallback();

            mConnectionManager.connect(recordingContext, WEBAPK_PACKAGE, callback1);
            if ((testCase & flagRunBackgroundTasksAfterConnect) != 0) {
                mTestExecutor.runPendingTasks();
            }
            mConnectionManager.disconnectAll(recordingContext);
            if ((testCase & flagRunBackgroundTasksAfterDisconnect) != 0) {
                mTestExecutor.runPendingTasks();
            }
            mConnectionManager.connect(recordingContext, WEBAPK_PACKAGE, callback2);
            mTestExecutor.runPendingTasks();

            Assert.assertArrayEquals(
                    new Boolean[] {true, false, true},
                    recordingContext.getStartStopServiceSequence().toArray(new Boolean[0]));
            Assert.assertTrue(callback1.mGotResult);
            // |callback1.mService| can be null.
            Assert.assertTrue(callback2.mGotResult);
            Assert.assertNotNull(callback2.mService);

            mConnectionManager.disconnectAll(recordingContext);
            mTestExecutor.runPendingTasks();
        }
    }

    /** Returns the package name of the next started service. */
    public String getNextStartedServicePackage() {
        Intent intent = mShadowApplication.getNextStartedService();
        return (intent == null) ? null : intent.getPackage();
    }
}