WifiManager: implement start/stopLocalOnlyHotspot

Implement new calls to startLocalOnlyHotspot and
cancelLocalOnlyHotspotRequest along with the classes
LocalOnlyHotspotCallback and LocalOnlyHotspotReservation.

Added tests for starting LOHS and cancelling a LOHS request.

The calls will be exposed in a later CL.

Bug: 36704763
Test: compiles
Test: frameworks/base/wifi/tests/runtests.sh
Test: frameworks/opt/net/wifi/tests/wifitests/runtests.sh

Change-Id: If54a89cb8dfd235bc18ef3e6c89f9d30882136a3
diff --git a/wifi/java/android/net/wifi/WifiManager.java b/wifi/java/android/net/wifi/WifiManager.java
index 6f1324e..f3e5493 100644
--- a/wifi/java/android/net/wifi/WifiManager.java
+++ b/wifi/java/android/net/wifi/WifiManager.java
@@ -40,6 +40,7 @@
 import android.util.Log;
 import android.util.SparseArray;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.AsyncChannel;
 import com.android.internal.util.Protocol;
@@ -862,6 +863,12 @@
     /** @hide */
     public static final int HOTSPOT_OBSERVER_REGISTERED = 3;
 
+    private final Object mLock = new Object(); // lock guarding access to the following vars
+    @GuardedBy("mLock")
+    private LocalOnlyHotspotCallbackProxy mLOHSCallbackProxy;
+    @GuardedBy("mLock")
+    private LocalOnlyHotspotObserverProxy mLOHSObserverProxy;
+
     /**
      * Create a new WifiManager instance.
      * Applications will almost always want to use
@@ -1880,7 +1887,24 @@
      */
     public void startLocalOnlyHotspot(LocalOnlyHotspotCallback callback,
             @Nullable Handler handler) {
-        throw new UnsupportedOperationException("LocalOnlyHotspot is still in development");
+        synchronized (mLock) {
+            Looper looper = (handler == null) ? mContext.getMainLooper() : handler.getLooper();
+            LocalOnlyHotspotCallbackProxy proxy =
+                    new LocalOnlyHotspotCallbackProxy(this, looper, callback);
+            try {
+                WifiConfiguration config = mService.startLocalOnlyHotspot(
+                        proxy.getMessenger(), new Binder());
+                if (config == null) {
+                    // Send message to the proxy to make sure we call back on the correct thread
+                    proxy.notifyFailed(
+                            LocalOnlyHotspotCallback.ERROR_INCOMPATIBLE_MODE);
+                    return;
+                }
+                mLOHSCallbackProxy = proxy;
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
     }
 
     /**
@@ -1897,7 +1921,9 @@
      * @hide
      */
     public void cancelLocalOnlyHotspotRequest() {
-        throw new UnsupportedOperationException("LocalOnlyHotspot is still in development");
+        synchronized (mLock) {
+            stopLocalOnlyHotspot();
+        }
     }
 
     /**
@@ -1911,7 +1937,18 @@
      *  method on their LocalOnlyHotspotReservation.
      */
     private void stopLocalOnlyHotspot() {
-        throw new UnsupportedOperationException("LocalOnlyHotspot is still in development");
+        synchronized (mLock) {
+            if (mLOHSCallbackProxy == null) {
+                // nothing to do, the callback was already cleaned up.
+                return;
+            }
+            mLOHSCallbackProxy = null;
+            try {
+                mService.stopLocalOnlyHotspot();
+            } catch (RemoteException e) {
+                throw e.rethrowFromSystemServer();
+            }
+        }
     }
 
     /**
diff --git a/wifi/tests/src/android/net/wifi/WifiManagerTest.java b/wifi/tests/src/android/net/wifi/WifiManagerTest.java
new file mode 100644
index 0000000..75cd095
--- /dev/null
+++ b/wifi/tests/src/android/net/wifi/WifiManagerTest.java
@@ -0,0 +1,582 @@
+/*
+ * 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.net.wifi;
+
+import static android.net.wifi.WifiManager.HOTSPOT_FAILED;
+import static android.net.wifi.WifiManager.HOTSPOT_STARTED;
+import static android.net.wifi.WifiManager.HOTSPOT_STOPPED;
+import static android.net.wifi.WifiManager.LocalOnlyHotspotCallback.ERROR_GENERIC;
+import static android.net.wifi.WifiManager.LocalOnlyHotspotCallback.ERROR_INCOMPATIBLE_MODE;
+import static android.net.wifi.WifiManager.LocalOnlyHotspotCallback.ERROR_NO_CHANNEL;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.*;
+
+import android.content.Context;
+import android.content.pm.ApplicationInfo;
+import android.net.wifi.WifiManager.LocalOnlyHotspotCallback;
+import android.net.wifi.WifiManager.LocalOnlyHotspotObserver;
+import android.net.wifi.WifiManager.LocalOnlyHotspotReservation;
+import android.net.wifi.WifiManager.LocalOnlyHotspotSubscription;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.Messenger;
+import android.os.test.TestLooper;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link android.net.wifi.WifiManager}.
+ */
+@SmallTest
+public class WifiManagerTest {
+
+    private static final int ERROR_NOT_SET = -1;
+    private static final int ERROR_TEST_REASON = 5;
+
+    @Mock Context mContext;
+    @Mock IWifiManager mWifiService;
+    @Mock ApplicationInfo mApplicationInfo;
+    @Mock WifiConfiguration mApConfig;
+    @Mock IBinder mAppBinder;
+
+    private Handler mHandler;
+    private TestLooper mLooper;
+    private WifiManager mWifiManager;
+    private Messenger mWifiServiceMessenger;
+    final ArgumentCaptor<Messenger> mMessengerCaptor = ArgumentCaptor.forClass(Messenger.class);
+
+    @Before public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mLooper = new TestLooper();
+        mHandler = spy(new Handler(mLooper.getLooper()));
+        when(mContext.getApplicationInfo()).thenReturn(mApplicationInfo);
+
+        mWifiServiceMessenger = new Messenger(mHandler);
+        mWifiManager = new WifiManager(mContext, mWifiService, mLooper.getLooper());
+    }
+
+    /**
+     * Check the call to startSoftAp calls WifiService to startSoftAp with the provided
+     * WifiConfiguration.  Verify that the return value is propagated to the caller.
+     */
+    @Test
+    public void testStartSoftApCallsServiceWithWifiConfig() throws Exception {
+        when(mWifiService.startSoftAp(eq(mApConfig))).thenReturn(true);
+        assertTrue(mWifiManager.startSoftAp(mApConfig));
+
+        when(mWifiService.startSoftAp(eq(mApConfig))).thenReturn(false);
+        assertFalse(mWifiManager.startSoftAp(mApConfig));
+    }
+
+    /**
+     * Check the call to startSoftAp calls WifiService to startSoftAp with a null config.  Verify
+     * that the return value is propagated to the caller.
+     */
+    @Test
+    public void testStartSoftApCallsServiceWithNullConfig() throws Exception {
+        when(mWifiService.startSoftAp(eq(null))).thenReturn(true);
+        assertTrue(mWifiManager.startSoftAp(null));
+
+        when(mWifiService.startSoftAp(eq(null))).thenReturn(false);
+        assertFalse(mWifiManager.startSoftAp(null));
+    }
+
+    /**
+     * Check the call to stopSoftAp calls WifiService to stopSoftAp.
+     */
+    @Test
+    public void testStopSoftApCallsService() throws Exception {
+        when(mWifiService.stopSoftAp()).thenReturn(true);
+        assertTrue(mWifiManager.stopSoftAp());
+
+        when(mWifiService.stopSoftAp()).thenReturn(false);
+        assertFalse(mWifiManager.stopSoftAp());
+    }
+
+    /**
+     * Test creation of a LocalOnlyHotspotReservation and verify that close properly calls
+     * WifiService.stopLocalOnlyHotspot.
+     */
+    @Test
+    public void testCreationAndCloseOfLocalOnlyHotspotReservation() throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        when(mWifiService.startLocalOnlyHotspot(any(Messenger.class), any(IBinder.class)))
+                            .thenReturn(mApConfig);
+        mWifiManager.startLocalOnlyHotspot(callback, mHandler);
+
+        callback.onStarted(mWifiManager.new LocalOnlyHotspotReservation(mApConfig));
+
+        assertEquals(mApConfig, callback.mRes.getConfig());
+        callback.mRes.close();
+        verify(mWifiService).stopLocalOnlyHotspot();
+    }
+
+    /**
+     * Verify stopLOHS is called when try-with-resources is used properly.
+     */
+    @Test
+    public void testLocalOnlyHotspotReservationCallsStopProperlyInTryWithResources()
+            throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        when(mWifiService.startLocalOnlyHotspot(any(Messenger.class), any(IBinder.class)))
+                .thenReturn(mApConfig);
+        mWifiManager.startLocalOnlyHotspot(callback, mHandler);
+
+        callback.onStarted(mWifiManager.new LocalOnlyHotspotReservation(mApConfig));
+
+        try (WifiManager.LocalOnlyHotspotReservation res = callback.mRes) {
+            assertEquals(mApConfig, res.getConfig());
+        }
+
+        verify(mWifiService).stopLocalOnlyHotspot();
+    }
+
+    /**
+     * Test creation of a LocalOnlyHotspotSubscription.
+     * TODO: when registrations are tracked, verify removal on close.
+     */
+    @Test
+    public void testCreationOfLocalOnlyHotspotSubscription() throws Exception {
+        try (WifiManager.LocalOnlyHotspotSubscription sub =
+                mWifiManager.new LocalOnlyHotspotSubscription()) {
+            sub.close();
+        }
+    }
+
+    public class TestLocalOnlyHotspotCallback extends LocalOnlyHotspotCallback {
+        public boolean mOnStartedCalled = false;
+        public boolean mOnStoppedCalled = false;
+        public int mFailureReason = -1;
+        public LocalOnlyHotspotReservation mRes = null;
+        public long mCallingThreadId = -1;
+
+        @Override
+        public void onStarted(LocalOnlyHotspotReservation r) {
+            mRes = r;
+            mOnStartedCalled = true;
+            mCallingThreadId = Thread.currentThread().getId();
+        }
+
+        @Override
+        public void onStopped() {
+            mOnStoppedCalled = true;
+            mCallingThreadId = Thread.currentThread().getId();
+        }
+
+        @Override
+        public void onFailed(int reason) {
+            mFailureReason = reason;
+            mCallingThreadId = Thread.currentThread().getId();
+        }
+    }
+
+    /**
+     * Verify callback is properly plumbed when called.
+     */
+    @Test
+    public void testLocalOnlyHotspotCallback() {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        assertFalse(callback.mOnStartedCalled);
+        assertFalse(callback.mOnStoppedCalled);
+        assertEquals(ERROR_NOT_SET, callback.mFailureReason);
+        assertEquals(null, callback.mRes);
+
+        // test onStarted
+        WifiManager.LocalOnlyHotspotReservation res =
+                mWifiManager.new LocalOnlyHotspotReservation(mApConfig);
+        callback.onStarted(res);
+        assertEquals(res, callback.mRes);
+        assertTrue(callback.mOnStartedCalled);
+        assertFalse(callback.mOnStoppedCalled);
+        assertEquals(ERROR_NOT_SET, callback.mFailureReason);
+
+        // test onStopped
+        callback.onStopped();
+        assertEquals(res, callback.mRes);
+        assertTrue(callback.mOnStartedCalled);
+        assertTrue(callback.mOnStoppedCalled);
+        assertEquals(ERROR_NOT_SET, callback.mFailureReason);
+
+        // test onFailed
+        callback.onFailed(ERROR_TEST_REASON);
+        assertEquals(res, callback.mRes);
+        assertTrue(callback.mOnStartedCalled);
+        assertTrue(callback.mOnStoppedCalled);
+        assertEquals(ERROR_TEST_REASON, callback.mFailureReason);
+    }
+
+    public class TestLocalOnlyHotspotObserver extends LocalOnlyHotspotObserver {
+        public boolean mOnRegistered = false;
+        public boolean mOnStartedCalled = false;
+        public boolean mOnStoppedCalled = false;
+        public WifiConfiguration mConfig = null;
+        public LocalOnlyHotspotSubscription mSub = null;
+
+        @Override
+        public void onRegistered(LocalOnlyHotspotSubscription sub) {
+            mOnRegistered = true;
+            mSub = sub;
+        }
+
+        @Override
+        public void onStarted(WifiConfiguration config) {
+            mOnStartedCalled = true;
+            mConfig = config;
+        }
+
+        @Override
+        public void onStopped() {
+            mOnStoppedCalled = true;
+        }
+    }
+
+    /**
+     * Verify observer is properly plumbed when called.
+     */
+    @Test
+    public void testLocalOnlyHotspotObserver() {
+        TestLocalOnlyHotspotObserver observer = new TestLocalOnlyHotspotObserver();
+        assertFalse(observer.mOnRegistered);
+        assertFalse(observer.mOnStartedCalled);
+        assertFalse(observer.mOnStoppedCalled);
+        assertEquals(null, observer.mConfig);
+        assertEquals(null, observer.mSub);
+
+        WifiManager.LocalOnlyHotspotSubscription sub =
+                mWifiManager.new LocalOnlyHotspotSubscription();
+        observer.onRegistered(sub);
+        assertTrue(observer.mOnRegistered);
+        assertFalse(observer.mOnStartedCalled);
+        assertFalse(observer.mOnStoppedCalled);
+        assertEquals(null, observer.mConfig);
+        assertEquals(sub, observer.mSub);
+
+        observer.onStarted(mApConfig);
+        assertTrue(observer.mOnRegistered);
+        assertTrue(observer.mOnStartedCalled);
+        assertFalse(observer.mOnStoppedCalled);
+        assertEquals(mApConfig, observer.mConfig);
+        assertEquals(sub, observer.mSub);
+
+        observer.onStopped();
+        assertTrue(observer.mOnRegistered);
+        assertTrue(observer.mOnStartedCalled);
+        assertTrue(observer.mOnStoppedCalled);
+        assertEquals(mApConfig, observer.mConfig);
+        assertEquals(sub, observer.mSub);
+    }
+
+    /**
+     * Verify call to startLocalOnlyHotspot goes to WifiServiceImpl.
+     */
+    @Test
+    public void testStartLocalOnlyHotspot() throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        mWifiManager.startLocalOnlyHotspot(callback, mHandler);
+
+        verify(mWifiService).startLocalOnlyHotspot(any(Messenger.class), any(IBinder.class));
+    }
+
+    /**
+     * Verify a SecurityException is thrown for callers without proper permissions for
+     * startLocalOnlyHotspot.
+     */
+    @Test(expected = SecurityException.class)
+    public void testStartLocalOnlyHotspotThrowsSecurityException() throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        doThrow(new SecurityException()).when(mWifiService)
+                .startLocalOnlyHotspot(any(Messenger.class), any(IBinder.class));
+        mWifiManager.startLocalOnlyHotspot(callback, mHandler);
+    }
+
+    /**
+     * Verify an IllegalStateException is thrown for callers that already have a pending request for
+     * startLocalOnlyHotspot.
+     */
+    @Test(expected = IllegalStateException.class)
+    public void testStartLocalOnlyHotspotThrowsIllegalStateException() throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        doThrow(new IllegalStateException()).when(mWifiService)
+                .startLocalOnlyHotspot(any(Messenger.class), any(IBinder.class));
+        mWifiManager.startLocalOnlyHotspot(callback, mHandler);
+    }
+
+    /**
+     * Verify the watchLocalOnlyHotspot call currently throws an UnsupportedOperationException.
+     */
+    @Test(expected = UnsupportedOperationException.class)
+    public void testWatchLocalOnlyHotspot() throws Exception {
+        TestLocalOnlyHotspotObserver observer = new TestLocalOnlyHotspotObserver();
+        mWifiManager.watchLocalOnlyHotspot(observer, mHandler);
+    }
+
+    /**
+     * Verify that the handler provided by the caller is used for the callbacks.
+     */
+    @Test
+    public void testCorrectLooperIsUsedForHandler() throws Exception {
+        // record thread from looper.getThread and check ids.
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        when(mWifiService.startLocalOnlyHotspot(any(Messenger.class), any(IBinder.class)))
+                .thenReturn(null);
+        mWifiManager.startLocalOnlyHotspot(callback, mHandler);
+        mLooper.dispatchAll();
+        assertEquals(ERROR_INCOMPATIBLE_MODE, callback.mFailureReason);
+        assertEquals(mLooper.getLooper().getThread().getId(), callback.mCallingThreadId);
+    }
+
+    /**
+     * Verify that the main looper's thread is used if a handler is not provided by the reqiestomg
+     * application.
+     */
+    @Test
+    public void testMainLooperIsUsedWhenHandlerNotProvided() throws Exception {
+        // record thread from looper.getThread and check ids.
+        TestLooper altLooper = new TestLooper();
+        when(mContext.getMainLooper()).thenReturn(altLooper.getLooper());
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        when(mWifiService.startLocalOnlyHotspot(any(Messenger.class), any(IBinder.class)))
+                .thenReturn(null);
+        mWifiManager.startLocalOnlyHotspot(callback, null);
+        altLooper.dispatchAll();
+        assertEquals(ERROR_INCOMPATIBLE_MODE, callback.mFailureReason);
+        assertEquals(altLooper.getLooper().getThread().getId(), callback.mCallingThreadId);
+    }
+
+    /**
+     * Verify the LOHS onStarted callback is triggered when WifiManager receives a HOTSPOT_STARTED
+     * message from WifiServiceImpl.
+     */
+    @Test
+    public void testOnStartedIsCalledWithReservation() throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        TestLooper callbackLooper = new TestLooper();
+        Handler callbackHandler = new Handler(callbackLooper.getLooper());
+        when(mWifiService.startLocalOnlyHotspot(mMessengerCaptor.capture(),
+                  any(IBinder.class))).thenReturn(mApConfig);
+        mWifiManager.startLocalOnlyHotspot(callback, callbackHandler);
+        callbackLooper.dispatchAll();
+        mLooper.dispatchAll();
+        assertFalse(callback.mOnStartedCalled);
+        assertEquals(null, callback.mRes);
+        // now trigger the callback
+        Message msg = new Message();
+        msg.what = HOTSPOT_STARTED;
+        msg.obj = mApConfig;
+        mMessengerCaptor.getValue().send(msg);
+        mLooper.dispatchAll();
+        callbackLooper.dispatchAll();
+        assertTrue(callback.mOnStartedCalled);
+        assertEquals(mApConfig, callback.mRes.getConfig());
+    }
+
+    /**
+     * Verify onFailed is called if WifiServiceImpl sends a HOTSPOT_STARTED message with a null
+     * config.
+     */
+    @Test
+    public void testOnStartedIsCalledWithNullConfig() throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        TestLooper callbackLooper = new TestLooper();
+        Handler callbackHandler = new Handler(callbackLooper.getLooper());
+        when(mWifiService.startLocalOnlyHotspot(mMessengerCaptor.capture(),
+                  any(IBinder.class))).thenReturn(mApConfig);
+        mWifiManager.startLocalOnlyHotspot(callback, callbackHandler);
+        callbackLooper.dispatchAll();
+        mLooper.dispatchAll();
+        assertFalse(callback.mOnStartedCalled);
+        assertEquals(null, callback.mRes);
+        // now trigger the callback
+        Message msg = new Message();
+        msg.what = HOTSPOT_STARTED;
+        mMessengerCaptor.getValue().send(msg);
+        mLooper.dispatchAll();
+        callbackLooper.dispatchAll();
+        assertFalse(callback.mOnStartedCalled);
+        assertEquals(ERROR_GENERIC, callback.mFailureReason);
+    }
+
+    /**
+     * Verify onStopped is called if WifiServiceImpl sends a HOTSPOT_STOPPED message.
+     */
+    @Test
+    public void testOnStoppedIsCalled() throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        TestLooper callbackLooper = new TestLooper();
+        Handler callbackHandler = new Handler(callbackLooper.getLooper());
+        when(mWifiService.startLocalOnlyHotspot(mMessengerCaptor.capture(),
+                  any(IBinder.class))).thenReturn(mApConfig);
+        mWifiManager.startLocalOnlyHotspot(callback, callbackHandler);
+        callbackLooper.dispatchAll();
+        mLooper.dispatchAll();
+        assertFalse(callback.mOnStoppedCalled);
+        // now trigger the callback
+        Message msg = new Message();
+        msg.what = HOTSPOT_STOPPED;
+        mMessengerCaptor.getValue().send(msg);
+        mLooper.dispatchAll();
+        callbackLooper.dispatchAll();
+        assertTrue(callback.mOnStoppedCalled);
+    }
+
+    /**
+     * Verify onFailed is called if WifiServiceImpl sends a HOTSPOT_FAILED message.
+     */
+    @Test
+    public void testOnFailedIsCalled() throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        TestLooper callbackLooper = new TestLooper();
+        Handler callbackHandler = new Handler(callbackLooper.getLooper());
+        when(mWifiService.startLocalOnlyHotspot(mMessengerCaptor.capture(),
+                  any(IBinder.class))).thenReturn(mApConfig);
+        mWifiManager.startLocalOnlyHotspot(callback, callbackHandler);
+        callbackLooper.dispatchAll();
+        mLooper.dispatchAll();
+        assertEquals(ERROR_NOT_SET, callback.mFailureReason);
+        // now trigger the callback
+        Message msg = new Message();
+        msg.what = HOTSPOT_FAILED;
+        msg.arg1 = ERROR_NO_CHANNEL;
+        mMessengerCaptor.getValue().send(msg);
+        mLooper.dispatchAll();
+        callbackLooper.dispatchAll();
+        assertEquals(ERROR_NO_CHANNEL, callback.mFailureReason);
+    }
+
+    /**
+     * Verify the handler passed in to startLocalOnlyHotspot is correctly used for callbacks when a
+     * null WifiConfig is returned.
+     */
+    @Test
+    public void testLocalOnlyHotspotCallbackFullOnNullConfig() throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        when(mWifiService.startLocalOnlyHotspot(any(Messenger.class), any(IBinder.class)))
+                .thenReturn(null);
+        mWifiManager.startLocalOnlyHotspot(callback, mHandler);
+        mLooper.dispatchAll();
+        assertEquals(ERROR_INCOMPATIBLE_MODE, callback.mFailureReason);
+        assertFalse(callback.mOnStartedCalled);
+        assertFalse(callback.mOnStoppedCalled);
+        assertEquals(null, callback.mRes);
+    }
+
+    /**
+     * Verify a SecurityException resulting from an application without necessary permissions will
+     * bubble up through the call to start LocalOnlyHotspot and will not trigger other callbacks.
+     */
+    @Test(expected = SecurityException.class)
+    public void testLocalOnlyHotspotCallbackFullOnSecurityException() throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        doThrow(new SecurityException()).when(mWifiService)
+                .startLocalOnlyHotspot(any(Messenger.class), any(IBinder.class));
+        try {
+            mWifiManager.startLocalOnlyHotspot(callback, mHandler);
+        } catch (SecurityException e) {
+            assertEquals(ERROR_NOT_SET, callback.mFailureReason);
+            assertFalse(callback.mOnStartedCalled);
+            assertFalse(callback.mOnStoppedCalled);
+            assertEquals(null, callback.mRes);
+            throw e;
+        }
+
+    }
+
+    /**
+     * Verify the handler passed to startLocalOnlyHotspot is correctly used for callbacks when
+     * SoftApMode fails due to a underlying error.
+     */
+    @Test
+    public void testLocalOnlyHotspotCallbackFullOnNoChannelError() throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        when(mWifiService.startLocalOnlyHotspot(any(Messenger.class), any(IBinder.class)))
+                .thenReturn(mApConfig);
+        mWifiManager.startLocalOnlyHotspot(callback, mHandler);
+        mLooper.dispatchAll();
+        //assertEquals(ERROR_NO_CHANNEL, callback.mFailureReason);
+        assertFalse(callback.mOnStartedCalled);
+        assertFalse(callback.mOnStoppedCalled);
+        assertEquals(null, callback.mRes);
+    }
+
+    /**
+     * Verify that the call to cancel a LOHS request does call stopLOHS.
+     */
+    @Test
+    public void testCancelLocalOnlyHotspotRequestCallsStopOnWifiService() throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        when(mWifiService.startLocalOnlyHotspot(any(Messenger.class), any(IBinder.class)))
+                .thenReturn(mApConfig);
+        mWifiManager.startLocalOnlyHotspot(callback, mHandler);
+        mWifiManager.cancelLocalOnlyHotspotRequest();
+        verify(mWifiService).stopLocalOnlyHotspot();
+    }
+
+    /**
+     * Verify that we do not crash if cancelLocalOnlyHotspotRequest is called without an existing
+     * callback stored.
+     */
+    @Test
+    public void testCancelLocalOnlyHotspotReturnsWithoutExistingRequest() {
+        mWifiManager.cancelLocalOnlyHotspotRequest();
+    }
+
+    /**
+     * Verify that the callback is not triggered if the LOHS request was already cancelled.
+     */
+    @Test
+    public void testCallbackAfterLocalOnlyHotspotWasCancelled() throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        when(mWifiService.startLocalOnlyHotspot(any(Messenger.class), any(IBinder.class)))
+                .thenReturn(mApConfig);
+        mWifiManager.startLocalOnlyHotspot(callback, mHandler);
+        mWifiManager.cancelLocalOnlyHotspotRequest();
+        verify(mWifiService).stopLocalOnlyHotspot();
+        mLooper.dispatchAll();
+        assertEquals(ERROR_NOT_SET, callback.mFailureReason);
+        assertFalse(callback.mOnStartedCalled);
+        assertFalse(callback.mOnStoppedCalled);
+        assertEquals(null, callback.mRes);
+    }
+
+    /**
+     * Verify that calling cancel LOHS request does not crash if an error callback was already
+     * handled.
+     */
+    @Test
+    public void testCancelAfterLocalOnlyHotspotCallbackTriggered() throws Exception {
+        TestLocalOnlyHotspotCallback callback = new TestLocalOnlyHotspotCallback();
+        when(mWifiService.startLocalOnlyHotspot(any(Messenger.class), any(IBinder.class)))
+                .thenReturn(null);
+        mWifiManager.startLocalOnlyHotspot(callback, mHandler);
+        mLooper.dispatchAll();
+        assertEquals(ERROR_INCOMPATIBLE_MODE, callback.mFailureReason);
+        assertFalse(callback.mOnStartedCalled);
+        assertFalse(callback.mOnStoppedCalled);
+        assertEquals(null, callback.mRes);
+        mWifiManager.cancelLocalOnlyHotspotRequest();
+        verify(mWifiService, never()).stopLocalOnlyHotspot();
+    }
+}