Unit testing for fragments.

Set up a base class for testing fragments that will generate the host
and run the fragment through some lifecycle checks to make sure it
does ok with standard lifecycle.

Fragment tests will also automatically check for any sort of leaks
related to bindings, receivers, or other callbacks in sysui. This
requires changing the statusbar.policy classes with callbacks to
have a common interface.

Lastly also fixes a few lifecycle bugs in QS found from the above
tests.

Bug: 32609190
Test: runtest systemui
Change-Id: I52007c696c2fd41914bba4ba9d8055f2b564a7d8
diff --git a/packages/SystemUI/tests/src/com/android/systemui/FragmentTestCase.java b/packages/SystemUI/tests/src/com/android/systemui/FragmentTestCase.java
new file mode 100644
index 0000000..f87336c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/FragmentTestCase.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2016 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;
+
+import android.annotation.Nullable;
+import android.app.Fragment;
+import android.app.FragmentController;
+import android.app.FragmentHostCallback;
+import android.app.FragmentManagerNonConfig;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Parcelable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.FrameLayout;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+
+/**
+ * Base class for fragment class tests.  Just adding one for any fragment will push it through
+ * general lifecycle events and ensure no basic leaks are happening.  This class also implements
+ * the host for subclasses, so they can push it into desired states and do any unit testing
+ * required.
+ */
+public abstract class FragmentTestCase extends LeakCheckedTest {
+
+    private static final int VIEW_ID = 42;
+    private final Class<? extends Fragment> mCls;
+    private HandlerThread mHandlerThread;
+    private Handler mHandler;
+    private FrameLayout mView;
+    protected FragmentController mFragments;
+    protected Fragment mFragment;
+
+    public FragmentTestCase(Class<? extends Fragment> cls) {
+        mCls = cls;
+    }
+
+    @Before
+    public void setupFragment() throws IllegalAccessException, InstantiationException {
+        mView = new FrameLayout(mContext);
+        mView.setId(VIEW_ID);
+        mHandlerThread = new HandlerThread("FragmentTestThread");
+        mHandlerThread.start();
+        mHandler = new Handler(mHandlerThread.getLooper());
+        mFragment = mCls.newInstance();
+        postAndWait(() -> {
+            mFragments = FragmentController.createController(new HostCallbacks());
+            mFragments.attachHost(null);
+            mFragments.getFragmentManager().beginTransaction()
+                    .replace(VIEW_ID, mFragment)
+                    .commit();
+        });
+    }
+
+    @After
+    public void tearDown() {
+        if (mFragments != null) {
+            // Set mFragments to null to let it know not to destroy.
+            postAndWait(() -> mFragments.dispatchDestroy());
+        }
+        mHandlerThread.quit();
+    }
+
+    @Test
+    public void testCreateDestroy() {
+        postAndWait(() -> mFragments.dispatchCreate());
+        destroyFragments();
+    }
+
+    @Test
+    public void testStartStop() {
+        postAndWait(() -> mFragments.dispatchStart());
+        postAndWait(() -> mFragments.dispatchStop());
+    }
+
+    @Test
+    public void testResumePause() {
+        postAndWait(() -> mFragments.dispatchResume());
+        postAndWait(() -> mFragments.dispatchPause());
+    }
+
+    @Test
+    public void testRecreate() {
+        postAndWait(() -> mFragments.dispatchResume());
+        postAndWait(() -> {
+            mFragments.dispatchPause();
+            Parcelable p = mFragments.saveAllState();
+            mFragments.dispatchDestroy();
+
+            mFragments = FragmentController.createController(new HostCallbacks());
+            mFragments.attachHost(null);
+            mFragments.restoreAllState(p, (FragmentManagerNonConfig) null);
+            mFragments.dispatchResume();
+        });
+    }
+
+    @Test
+    public void testMultipleResumes() {
+        postAndWait(() -> mFragments.dispatchResume());
+        postAndWait(() -> mFragments.dispatchStop());
+        postAndWait(() -> mFragments.dispatchResume());
+    }
+
+    protected void destroyFragments() {
+        postAndWait(() -> mFragments.dispatchDestroy());
+        mFragments = null;
+    }
+
+    protected void postAndWait(Runnable r) {
+        mHandler.post(r);
+        waitForFragments();
+    }
+
+    protected void waitForFragments() {
+        waitForIdleSync(mHandler);
+    }
+
+    private View findViewById(int id) {
+        return mView.findViewById(id);
+    }
+
+    private class HostCallbacks extends FragmentHostCallback<FragmentTestCase> {
+        public HostCallbacks() {
+            super(getTrackedContext(), FragmentTestCase.this.mHandler, 0);
+        }
+
+        @Override
+        public FragmentTestCase onGetHost() {
+            return FragmentTestCase.this;
+        }
+
+        @Override
+        public void onDump(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) {
+        }
+
+        @Override
+        public boolean onShouldSaveFragmentState(Fragment fragment) {
+            return true; // True for now.
+        }
+
+        @Override
+        public LayoutInflater onGetLayoutInflater() {
+            return LayoutInflater.from(mContext);
+        }
+
+        @Override
+        public boolean onUseFragmentManagerInflaterFactory() {
+            return true;
+        }
+
+        @Override
+        public boolean onHasWindowAnimations() {
+            return false;
+        }
+
+        @Override
+        public int onGetWindowAnimations() {
+            return 0;
+        }
+
+        @Override
+        public void onAttachFragment(Fragment fragment) {
+        }
+
+        @Nullable
+        @Override
+        public View onFindViewById(int id) {
+            return FragmentTestCase.this.findViewById(id);
+        }
+
+        @Override
+        public boolean onHasView() {
+            return true;
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/LeakCheckedTest.java b/packages/SystemUI/tests/src/com/android/systemui/LeakCheckedTest.java
new file mode 100644
index 0000000..d64669d
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/LeakCheckedTest.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright (C) 2016 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;
+
+import static org.mockito.Matchers.any;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.content.BroadcastReceiver;
+import android.content.ComponentCallbacks;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.ServiceConnection;
+import android.os.Handler;
+import android.os.UserHandle;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.android.systemui.statusbar.phone.ManagedProfileController;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.BluetoothController;
+import com.android.systemui.statusbar.policy.CastController;
+import com.android.systemui.statusbar.policy.DataSaverController;
+import com.android.systemui.statusbar.policy.FlashlightController;
+import com.android.systemui.statusbar.policy.HotspotController;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+import com.android.systemui.statusbar.policy.LocationController;
+import com.android.systemui.statusbar.policy.NetworkController;
+import com.android.systemui.statusbar.policy.NextAlarmController;
+import com.android.systemui.statusbar.policy.RotationLockController;
+import com.android.systemui.statusbar.policy.SecurityController;
+import com.android.systemui.statusbar.policy.CallbackController;
+import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.statusbar.policy.ZenModeController;
+
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+import org.mockito.invocation.InvocationOnMock;
+import org.mockito.stubbing.Answer;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Base class for tests to check if receivers are left registered, services bound, or other
+ * listeners listening.
+ */
+public class LeakCheckedTest extends SysuiTestCase {
+    private static final String TAG = "LeakCheckedTest";
+
+    private final Map<String, Tracker> mTrackers = new HashMap<>();
+    private final Map<Class, Object> mLeakCheckers = new ArrayMap<>();
+    private TrackingContext mTrackedContext;
+
+    @Rule
+    public TestWatcher successWatcher = new TestWatcher() {
+        @Override
+        protected void succeeded(Description description) {
+            verify();
+        }
+    };
+
+    @Before
+    public void setup() {
+        mTrackedContext = new TrackingContext(mContext);
+        addSupportedLeakCheckers();
+    }
+
+    public <T> T getLeakChecker(Class<T> cls) {
+        T obj = (T) mLeakCheckers.get(cls);
+        if (obj == null) {
+            Assert.fail(cls.getName() + " is not supported by LeakCheckedTest yet");
+        }
+        return obj;
+    }
+
+    public Context getTrackedContext() {
+        return mTrackedContext;
+    }
+
+    private Tracker getTracker(String tag) {
+        Tracker t = mTrackers.get(tag);
+        if (t == null) {
+            t = new Tracker();
+            mTrackers.put(tag, t);
+        }
+        return t;
+    }
+
+    public void verify() {
+        mTrackers.values().forEach(Tracker::verify);
+    }
+
+    public static class Tracker {
+        private Map<Object, LeakInfo> mObjects = new ArrayMap<>();
+
+        LeakInfo getLeakInfo(Object object) {
+            LeakInfo leakInfo = mObjects.get(object);
+            if (leakInfo == null) {
+                leakInfo = new LeakInfo();
+                mObjects.put(object, leakInfo);
+            }
+            return leakInfo;
+        }
+
+        private void verify() {
+            mObjects.values().forEach(LeakInfo::verify);
+        }
+    }
+
+    public static class LeakInfo {
+        private List<Throwable> mThrowables = new ArrayList<>();
+
+        private LeakInfo() {
+        }
+
+        private void addAllocation(Throwable t) {
+            // TODO: Drop off the first element in the stack trace here to have a cleaner stack.
+            mThrowables.add(t);
+        }
+
+        private void clearAllocations() {
+            mThrowables.clear();
+        }
+
+        public void verify() {
+            if (mThrowables.size() == 0) return;
+            Log.e(TAG, "Listener or binding not properly released");
+            for (Throwable t : mThrowables) {
+                Log.e(TAG, "Allocation found", t);
+            }
+            StringWriter writer = new StringWriter();
+            mThrowables.get(0).printStackTrace(new PrintWriter(writer));
+            Assert.fail("Listener or binding not properly released\n"
+                    + writer.toString());
+        }
+    }
+
+    private void addSupportedLeakCheckers() {
+        addListening("bluetooth", BluetoothController.class);
+        addListening("location", LocationController.class);
+        addListening("rotation", RotationLockController.class);
+        addListening("zen", ZenModeController.class);
+        addListening("cast", CastController.class);
+        addListening("hotspot", HotspotController.class);
+        addListening("flashlight", FlashlightController.class);
+        addListening("user", UserInfoController.class);
+        addListening("keyguard", KeyguardMonitor.class);
+        addListening("battery", BatteryController.class);
+        addListening("security", SecurityController.class);
+        addListening("profile", ManagedProfileController.class);
+        addListening("alarm", NextAlarmController.class);
+        NetworkController network = addListening("network", NetworkController.class);
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                getTracker("emergency").getLeakInfo(invocation.getArguments()[0])
+                        .addAllocation(new Throwable());
+                return null;
+            }
+        }).when(network).addEmergencyListener(any());
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                getTracker("emergency").getLeakInfo(invocation.getArguments()[0]).clearAllocations();
+                return null;
+            }
+        }).when(network).removeEmergencyListener(any());
+        DataSaverController datasaver = addListening("datasaver", DataSaverController.class);
+        when(network.getDataSaverController()).thenReturn(datasaver);
+    }
+
+    private <T extends CallbackController> T addListening(final String tag, Class<T> cls) {
+        T mock = mock(cls);
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                getTracker(tag).getLeakInfo(invocation.getArguments()[0])
+                        .addAllocation(new Throwable());
+                return null;
+            }
+        }).when(mock).addCallback(any());
+        doAnswer(new Answer<Void>() {
+            @Override
+            public Void answer(InvocationOnMock invocation) throws Throwable {
+                getTracker(tag).getLeakInfo(invocation.getArguments()[0]).clearAllocations();
+                return null;
+            }
+        }).when(mock).removeCallback(any());
+        mLeakCheckers.put(cls, mock);
+        return mock;
+    }
+
+    class TrackingContext extends ContextWrapper {
+        public TrackingContext(Context base) {
+            super(base);
+        }
+
+        @Override
+        public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter) {
+            getTracker("receiver").getLeakInfo(receiver).addAllocation(new Throwable());
+            return super.registerReceiver(receiver, filter);
+        }
+
+        @Override
+        public Intent registerReceiver(BroadcastReceiver receiver, IntentFilter filter,
+                String broadcastPermission, Handler scheduler) {
+            getTracker("receiver").getLeakInfo(receiver).addAllocation(new Throwable());
+            return super.registerReceiver(receiver, filter, broadcastPermission, scheduler);
+        }
+
+        @Override
+        public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user,
+                IntentFilter filter, String broadcastPermission, Handler scheduler) {
+            getTracker("receiver").getLeakInfo(receiver).addAllocation(new Throwable());
+            return super.registerReceiverAsUser(receiver, user, filter, broadcastPermission,
+                    scheduler);
+        }
+
+        @Override
+        public void unregisterReceiver(BroadcastReceiver receiver) {
+            getTracker("receiver").getLeakInfo(receiver).clearAllocations();
+            super.unregisterReceiver(receiver);
+        }
+
+        @Override
+        public boolean bindService(Intent service, ServiceConnection conn, int flags) {
+            getTracker("service").getLeakInfo(conn).addAllocation(new Throwable());
+            return super.bindService(service, conn, flags);
+        }
+
+        @Override
+        public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags,
+                Handler handler, UserHandle user) {
+            getTracker("service").getLeakInfo(conn).addAllocation(new Throwable());
+            return super.bindServiceAsUser(service, conn, flags, handler, user);
+        }
+
+        @Override
+        public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags,
+                UserHandle user) {
+            getTracker("service").getLeakInfo(conn).addAllocation(new Throwable());
+            return super.bindServiceAsUser(service, conn, flags, user);
+        }
+
+        @Override
+        public void unbindService(ServiceConnection conn) {
+            getTracker("service").getLeakInfo(conn).clearAllocations();
+            super.unbindService(conn);
+        }
+
+        @Override
+        public void registerComponentCallbacks(ComponentCallbacks callback) {
+            getTracker("component").getLeakInfo(callback).addAllocation(new Throwable());
+            super.registerComponentCallbacks(callback);
+        }
+
+        @Override
+        public void unregisterComponentCallbacks(ComponentCallbacks callback) {
+            getTracker("component").getLeakInfo(callback).clearAllocations();
+            super.unregisterComponentCallbacks(callback);
+        }
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
new file mode 100644
index 0000000..6ceaead
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSFragmentTest.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright (C) 2016 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;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import android.os.Handler;
+import android.support.test.runner.AndroidJUnit4;
+
+import com.android.systemui.FragmentTestCase;
+import com.android.systemui.statusbar.phone.PhoneStatusBar;
+import com.android.systemui.statusbar.phone.QSTileHost;
+import com.android.systemui.statusbar.phone.StatusBarIconController;
+import com.android.systemui.statusbar.policy.BatteryController;
+import com.android.systemui.statusbar.policy.BluetoothController;
+import com.android.systemui.statusbar.policy.CastController;
+import com.android.systemui.statusbar.policy.FlashlightController;
+import com.android.systemui.statusbar.policy.HotspotController;
+import com.android.systemui.statusbar.policy.KeyguardMonitor;
+import com.android.systemui.statusbar.policy.LocationController;
+import com.android.systemui.statusbar.policy.NetworkController;
+import com.android.systemui.statusbar.policy.NextAlarmController;
+import com.android.systemui.statusbar.policy.RotationLockController;
+import com.android.systemui.statusbar.policy.SecurityController;
+import com.android.systemui.statusbar.policy.UserInfoController;
+import com.android.systemui.statusbar.policy.UserSwitcherController;
+import com.android.systemui.statusbar.policy.ZenModeController;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import java.util.ArrayList;
+
+@RunWith(AndroidJUnit4.class)
+public class QSFragmentTest extends FragmentTestCase {
+
+    public QSFragmentTest() {
+        super(QSFragment.class);
+    }
+
+    @Test
+    public void testListening() {
+        QSFragment qs = (QSFragment) mFragment;
+        postAndWait(() -> mFragments.dispatchResume());
+        UserSwitcherController userSwitcher = mock(UserSwitcherController.class);
+        KeyguardMonitor keyguardMonitor = getLeakChecker(KeyguardMonitor.class);
+        when(userSwitcher.getKeyguardMonitor()).thenReturn(keyguardMonitor);
+        when(userSwitcher.getUsers()).thenReturn(new ArrayList<>());
+        QSTileHost host = new QSTileHost(getTrackedContext(),
+                mock(PhoneStatusBar.class),
+                getLeakChecker(BluetoothController.class),
+                getLeakChecker(LocationController.class),
+                getLeakChecker(RotationLockController.class),
+                getLeakChecker(NetworkController.class),
+                getLeakChecker(ZenModeController.class),
+                getLeakChecker(HotspotController.class),
+                getLeakChecker(CastController.class),
+                getLeakChecker(FlashlightController.class),
+                userSwitcher,
+                getLeakChecker(UserInfoController.class),
+                keyguardMonitor,
+                getLeakChecker(SecurityController.class),
+                getLeakChecker(BatteryController.class),
+                mock(StatusBarIconController.class),
+                getLeakChecker(NextAlarmController.class));
+        qs.setHost(host);
+        Handler h = new Handler(host.getLooper());
+
+        qs.setListening(true);
+        waitForIdleSync(h);
+
+        qs.setListening(false);
+        waitForIdleSync(h);
+
+        host.destroy();
+    }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java
index 6c9cfe0..136e7c0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/NetworkControllerBaseTest.java
@@ -35,7 +35,7 @@
 import com.android.systemui.statusbar.policy.NetworkControllerImpl.Config;
 import com.android.systemui.statusbar.policy.NetworkControllerImpl.SubscriptionDefaults;
 import com.android.systemui.SysuiTestCase;
-import org.junit.After;
+
 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.rules.TestWatcher;
@@ -118,7 +118,7 @@
 
         // Trigger blank callbacks to always get the current state (some tests don't trigger
         // changes from default state).
-        mNetworkController.addSignalCallback(mock(SignalCallback.class));
+        mNetworkController.addCallback(mock(SignalCallback.class));
         mNetworkController.addEmergencyListener(null);
     }