CastTile becomes unavailable when not connected to Wifi

CastTile registers with NetworkController and shows unavailable
status when not connected to WiFi (regardless of internet connectivity).
Includes accessibility context for this state.

Added tests for behavior.

Test: manual && atest
Change-Id: I3004ed18e545d8d8c448f01d33eb70bcfd9831b2
Fixes: 78152102
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e1c71fa..42e19aa 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -761,6 +761,8 @@
     <string name="quick_settings_cast_device_default_description">Ready to cast</string>
     <!-- QuickSettings: Cast detail panel, text when there are no items [CHAR LIMIT=NONE] -->
     <string name="quick_settings_cast_detail_empty_text">No devices available</string>
+    <!-- QuickSettings: Cast unavailable, text when not connected to WiFi [CHAR LIMIT=NONE] -->
+    <string name="quick_settings_cast_no_wifi">Wi\u2011Fi not connected</string>
     <!-- QuickSettings: Brightness dialog title [CHAR LIMIT=NONE] -->
     <string name="quick_settings_brightness_dialog_title">Brightness</string>
     <!-- QuickSettings: Brightness dialog auto brightness button [CHAR LIMIT=NONE] -->
@@ -1999,6 +2001,9 @@
     <!-- accessibility label for quick settings items that open a details page [CHAR LIMIT=NONE] -->
     <string name="accessibility_quick_settings_open_details">Open details.</string>
 
+    <!-- accessibility label for quick settings items that are currently disabled. Must have a reason [CHAR LIMIT=NONE] -->
+    <string name="accessibility_quick_settings_not_available">Unvailable due to <xliff:g name="reason" id="reason" example="Wifi not available">%s</xliff:g></string>
+
     <!-- accessibility label for quick settings items that open a details page [CHAR LIMIT=NONE] -->
     <string name="accessibility_quick_settings_open_settings">Open <xliff:g name="page" example="Bluetooth">%s</xliff:g> settings.</string>
 
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
index ed78048..921db69 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/CastTile.java
@@ -46,6 +46,7 @@
 import com.android.systemui.statusbar.policy.CastController;
 import com.android.systemui.statusbar.policy.CastController.CastDevice;
 import com.android.systemui.statusbar.policy.KeyguardMonitor;
+import com.android.systemui.statusbar.policy.NetworkController;
 
 import java.util.LinkedHashMap;
 import java.util.Set;
@@ -58,16 +59,18 @@
     private final CastController mController;
     private final CastDetailAdapter mDetailAdapter;
     private final KeyguardMonitor mKeyguard;
+    private final NetworkController mNetworkController;
     private final Callback mCallback = new Callback();
     private final ActivityStarter mActivityStarter;
     private Dialog mDialog;
-    private boolean mRegistered;
+    private boolean mWifiConnected;
 
     public CastTile(QSHost host) {
         super(host);
         mController = Dependency.get(CastController.class);
         mDetailAdapter = new CastDetailAdapter();
         mKeyguard = Dependency.get(KeyguardMonitor.class);
+        mNetworkController = Dependency.get(NetworkController.class);
         mActivityStarter = Dependency.get(ActivityStarter.class);
     }
 
@@ -87,10 +90,12 @@
         if (listening) {
             mController.addCallback(mCallback);
             mKeyguard.addCallback(mCallback);
+            mNetworkController.addCallback(mSignalCallback);
         } else {
             mController.setDiscovering(false);
             mController.removeCallback(mCallback);
             mKeyguard.removeCallback(mCallback);
+            mNetworkController.removeCallback(mSignalCallback);
         }
     }
 
@@ -112,6 +117,9 @@
 
     @Override
     protected void handleClick() {
+        if (getState().state == Tile.STATE_UNAVAILABLE) {
+            return;
+        }
         if (mKeyguard.isSecure() && !mKeyguard.canSkipBouncer()) {
             mActivityStarter.postQSRunnableDismissingKeyguard(() -> {
                 showDetail(true);
@@ -164,13 +172,22 @@
         if (!state.value && connecting) {
             state.label = mContext.getString(R.string.quick_settings_connecting);
         }
-        state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
         state.icon = ResourceIcon.get(state.value ? R.drawable.ic_qs_cast_on
                 : R.drawable.ic_qs_cast_off);
+        if (mWifiConnected) {
+            state.state = state.value ? Tile.STATE_ACTIVE : Tile.STATE_INACTIVE;
+            state.secondaryLabel = "";
+            state.contentDescription = state.contentDescription + ","
+                    + mContext.getString(R.string.accessibility_quick_settings_open_details);
+            state.expandedAccessibilityClassName = Button.class.getName();
+        } else {
+            state.state = Tile.STATE_UNAVAILABLE;
+            String noWifi = mContext.getString(R.string.quick_settings_cast_no_wifi);
+            state.secondaryLabel = noWifi;
+            state.contentDescription = state.contentDescription + ", " + mContext.getString(
+                    R.string.accessibility_quick_settings_not_available, noWifi);
+        }
         mDetailAdapter.updateItems(devices);
-        state.expandedAccessibilityClassName = Button.class.getName();
-        state.contentDescription = state.contentDescription + ","
-                + mContext.getString(R.string.accessibility_quick_settings_open_details);
     }
 
     @Override
@@ -192,6 +209,22 @@
                 : mContext.getString(R.string.quick_settings_cast_device_default_name);
     }
 
+    private final NetworkController.SignalCallback mSignalCallback =
+            new NetworkController.SignalCallback() {
+                @Override
+                public void setWifiIndicators(boolean enabled,
+                        NetworkController.IconState statusIcon,
+                        NetworkController.IconState qsIcon, boolean activityIn, boolean activityOut,
+                        String description, boolean isTransient, String statusLabel) {
+                    // statusIcon.visible has the connected status information
+                    boolean enabledAndConnected = enabled && qsIcon.visible;
+                    if (enabledAndConnected != mWifiConnected) {
+                        mWifiConnected = enabledAndConnected;
+                        refreshState();
+                    }
+                }
+            };
+
     private final class Callback implements CastController.Callback, KeyguardMonitor.Callback {
         @Override
         public void onCastDevicesChanged() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java
new file mode 100644
index 0000000..d9412ec
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/CastTileTest.java
@@ -0,0 +1,149 @@
+/*
+ * Copyright (C) 2018 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.systemui.qs.tiles;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertTrue;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.service.quicksettings.Tile;
+import android.support.test.filters.SmallTest;
+import android.testing.AndroidTestingRunner;
+import android.testing.TestableLooper;
+
+import com.android.systemui.Dependency;
+import com.android.systemui.SysuiTestCase;
+import com.android.systemui.plugins.ActivityStarter;
+import com.android.systemui.qs.QSTileHost;
+import com.android.systemui.statusbar.policy.CastController;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+import com.android.systemui.statusbar.policy.NetworkController;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.HashSet;
+import java.util.Set;
+
+
+@RunWith(AndroidTestingRunner.class)
+@TestableLooper.RunWithLooper
+@SmallTest
+public class CastTileTest extends SysuiTestCase {
+
+    @Mock
+    private CastController mController;
+    @Mock
+    private ActivityStarter mActivityStarter;
+    @Mock
+    private KeyguardMonitor mKeyguard;
+    @Mock
+    private NetworkController mNetworkController;
+    @Mock
+    private QSTileHost mHost;
+    @Mock
+    NetworkController.SignalCallback mCallback;
+
+    private TestableLooper mTestableLooper;
+    private CastTile mCastTile;
+
+    @Before
+    public void setUp() throws Exception {
+        MockitoAnnotations.initMocks(this);
+        mTestableLooper = TestableLooper.get(this);
+
+        mDependency.injectTestDependency(Dependency.BG_LOOPER, mTestableLooper.getLooper());
+        mController = mDependency.injectMockDependency(CastController.class);
+        mActivityStarter = mDependency.injectMockDependency(ActivityStarter.class);
+        mKeyguard = mDependency.injectMockDependency(KeyguardMonitor.class);
+        mNetworkController = mDependency.injectMockDependency(NetworkController.class);
+
+        when(mHost.getContext()).thenReturn(mContext);
+
+        mCastTile = new CastTile(mHost);
+
+        // We are not setting the mocks to listening, so we trigger a first refresh state to
+        // set the initial state
+        mCastTile.refreshState();
+
+        mCastTile.handleSetListening(true);
+        ArgumentCaptor<NetworkController.SignalCallback> signalCallbackArgumentCaptor =
+                ArgumentCaptor.forClass(NetworkController.SignalCallback.class);
+        verify(mNetworkController).addCallback(signalCallbackArgumentCaptor.capture());
+        mCallback = signalCallbackArgumentCaptor.getValue();
+
+    }
+
+    @Test
+    public void testStateUnavailable_wifiDisabled() {
+        NetworkController.IconState qsIcon =
+                new NetworkController.IconState(false, 0, "");
+        mCallback.setWifiIndicators(false, mock(NetworkController.IconState.class),
+                qsIcon, false,false, "",
+                false, "");
+        mTestableLooper.processAllMessages();
+
+        assertEquals(Tile.STATE_UNAVAILABLE, mCastTile.getState().state);
+    }
+
+    @Test
+    public void testStateUnavailable_wifiNotConnected() {
+        NetworkController.IconState qsIcon =
+                new NetworkController.IconState(false, 0, "");
+        mCallback.setWifiIndicators(true, mock(NetworkController.IconState.class),
+                qsIcon, false,false, "",
+                false, "");
+        mTestableLooper.processAllMessages();
+
+        assertEquals(Tile.STATE_UNAVAILABLE, mCastTile.getState().state);
+    }
+
+    @Test
+    public void testStateActive_wifiEnabledAndCasting() {
+        CastController.CastDevice device = mock(CastController.CastDevice.class);
+        device.state = CastController.CastDevice.STATE_CONNECTED;
+        Set<CastController.CastDevice> devices = new HashSet<>();
+        devices.add(device);
+        when(mController.getCastDevices()).thenReturn(devices);
+
+        NetworkController.IconState qsIcon =
+                new NetworkController.IconState(true, 0, "");
+        mCallback.setWifiIndicators(true, mock(NetworkController.IconState.class),
+                qsIcon, false,false, "",
+                false, "");
+        mTestableLooper.processAllMessages();
+
+        assertEquals(Tile.STATE_ACTIVE, mCastTile.getState().state);
+    }
+
+    @Test
+    public void testStateInactive_wifiEnabledNotCasting() {
+        NetworkController.IconState qsIcon =
+                new NetworkController.IconState(true, 0, "");
+        mCallback.setWifiIndicators(true, mock(NetworkController.IconState.class),
+                qsIcon, false,false, "",
+                false, "");
+        mTestableLooper.processAllMessages();
+
+        assertEquals(Tile.STATE_INACTIVE, mCastTile.getState().state);
+    }
+}