Cleanup and refactoring of test utilities

 - Make leak checking faster by converting to fakes
    - Requires making clean interfaces for all CallbackControllers
 - Integrate leak checking into the TestableContext

Test: runtest systemui
Change-Id: Ic57a06360d01a0323ef26735a543e9d1805459e2
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
new file mode 100644
index 0000000..008d837
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightControllerImpl.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2014 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.statusbar.policy;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.systemui.statusbar.policy.FlashlightController.FlashlightListener;
+
+import java.io.FileDescriptor;
+import java.io.PrintWriter;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Manages the flashlight.
+ */
+public class FlashlightControllerImpl implements FlashlightController {
+
+    private static final String TAG = "FlashlightController";
+    private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG);
+
+    private static final int DISPATCH_ERROR = 0;
+    private static final int DISPATCH_CHANGED = 1;
+    private static final int DISPATCH_AVAILABILITY_CHANGED = 2;
+
+    private final CameraManager mCameraManager;
+    private final Context mContext;
+    /** Call {@link #ensureHandler()} before using */
+    private Handler mHandler;
+
+    /** Lock on mListeners when accessing */
+    private final ArrayList<WeakReference<FlashlightListener>> mListeners = new ArrayList<>(1);
+
+    /** Lock on {@code this} when accessing */
+    private boolean mFlashlightEnabled;
+
+    private String mCameraId;
+    private boolean mTorchAvailable;
+
+    public FlashlightControllerImpl(Context context) {
+        mContext = context;
+        mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
+
+        tryInitCamera();
+    }
+
+    private void tryInitCamera() {
+        try {
+            mCameraId = getCameraId();
+        } catch (Throwable e) {
+            Log.e(TAG, "Couldn't initialize.", e);
+            return;
+        }
+
+        if (mCameraId != null) {
+            ensureHandler();
+            mCameraManager.registerTorchCallback(mTorchCallback, mHandler);
+        }
+    }
+
+    public void setFlashlight(boolean enabled) {
+        boolean pendingError = false;
+        synchronized (this) {
+            if (mCameraId == null) return;
+            if (mFlashlightEnabled != enabled) {
+                mFlashlightEnabled = enabled;
+                try {
+                    mCameraManager.setTorchMode(mCameraId, enabled);
+                } catch (CameraAccessException e) {
+                    Log.e(TAG, "Couldn't set torch mode", e);
+                    mFlashlightEnabled = false;
+                    pendingError = true;
+                }
+            }
+        }
+        dispatchModeChanged(mFlashlightEnabled);
+        if (pendingError) {
+            dispatchError();
+        }
+    }
+
+    public boolean hasFlashlight() {
+        return mContext.getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA_FLASH);
+    }
+
+    public synchronized boolean isEnabled() {
+        return mFlashlightEnabled;
+    }
+
+    public synchronized boolean isAvailable() {
+        return mTorchAvailable;
+    }
+
+    public void addCallback(FlashlightListener l) {
+        synchronized (mListeners) {
+            if (mCameraId == null) {
+                tryInitCamera();
+            }
+            cleanUpListenersLocked(l);
+            mListeners.add(new WeakReference<>(l));
+        }
+    }
+
+    public void removeCallback(FlashlightListener l) {
+        synchronized (mListeners) {
+            cleanUpListenersLocked(l);
+        }
+    }
+
+    private synchronized void ensureHandler() {
+        if (mHandler == null) {
+            HandlerThread thread = new HandlerThread(TAG, Process.THREAD_PRIORITY_BACKGROUND);
+            thread.start();
+            mHandler = new Handler(thread.getLooper());
+        }
+    }
+
+    private String getCameraId() throws CameraAccessException {
+        String[] ids = mCameraManager.getCameraIdList();
+        for (String id : ids) {
+            CameraCharacteristics c = mCameraManager.getCameraCharacteristics(id);
+            Boolean flashAvailable = c.get(CameraCharacteristics.FLASH_INFO_AVAILABLE);
+            Integer lensFacing = c.get(CameraCharacteristics.LENS_FACING);
+            if (flashAvailable != null && flashAvailable
+                    && lensFacing != null && lensFacing == CameraCharacteristics.LENS_FACING_BACK) {
+                return id;
+            }
+        }
+        return null;
+    }
+
+    private void dispatchModeChanged(boolean enabled) {
+        dispatchListeners(DISPATCH_CHANGED, enabled);
+    }
+
+    private void dispatchError() {
+        dispatchListeners(DISPATCH_CHANGED, false /* argument (ignored) */);
+    }
+
+    private void dispatchAvailabilityChanged(boolean available) {
+        dispatchListeners(DISPATCH_AVAILABILITY_CHANGED, available);
+    }
+
+    private void dispatchListeners(int message, boolean argument) {
+        synchronized (mListeners) {
+            final int N = mListeners.size();
+            boolean cleanup = false;
+            for (int i = 0; i < N; i++) {
+                FlashlightListener l = mListeners.get(i).get();
+                if (l != null) {
+                    if (message == DISPATCH_ERROR) {
+                        l.onFlashlightError();
+                    } else if (message == DISPATCH_CHANGED) {
+                        l.onFlashlightChanged(argument);
+                    } else if (message == DISPATCH_AVAILABILITY_CHANGED) {
+                        l.onFlashlightAvailabilityChanged(argument);
+                    }
+                } else {
+                    cleanup = true;
+                }
+            }
+            if (cleanup) {
+                cleanUpListenersLocked(null);
+            }
+        }
+    }
+
+    private void cleanUpListenersLocked(FlashlightListener listener) {
+        for (int i = mListeners.size() - 1; i >= 0; i--) {
+            FlashlightListener found = mListeners.get(i).get();
+            if (found == null || found == listener) {
+                mListeners.remove(i);
+            }
+        }
+    }
+
+    private final CameraManager.TorchCallback mTorchCallback =
+            new CameraManager.TorchCallback() {
+
+        @Override
+        public void onTorchModeUnavailable(String cameraId) {
+            if (TextUtils.equals(cameraId, mCameraId)) {
+                setCameraAvailable(false);
+            }
+        }
+
+        @Override
+        public void onTorchModeChanged(String cameraId, boolean enabled) {
+            if (TextUtils.equals(cameraId, mCameraId)) {
+                setCameraAvailable(true);
+                setTorchMode(enabled);
+            }
+        }
+
+        private void setCameraAvailable(boolean available) {
+            boolean changed;
+            synchronized (FlashlightControllerImpl.this) {
+                changed = mTorchAvailable != available;
+                mTorchAvailable = available;
+            }
+            if (changed) {
+                if (DEBUG) Log.d(TAG, "dispatchAvailabilityChanged(" + available + ")");
+                dispatchAvailabilityChanged(available);
+            }
+        }
+
+        private void setTorchMode(boolean enabled) {
+            boolean changed;
+            synchronized (FlashlightControllerImpl.this) {
+                changed = mFlashlightEnabled != enabled;
+                mFlashlightEnabled = enabled;
+            }
+            if (changed) {
+                if (DEBUG) Log.d(TAG, "dispatchModeChanged(" + enabled + ")");
+                dispatchModeChanged(enabled);
+            }
+        }
+    };
+
+    public void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
+        pw.println("FlashlightController state:");
+
+        pw.print("  mCameraId=");
+        pw.println(mCameraId);
+        pw.print("  mFlashlightEnabled=");
+        pw.println(mFlashlightEnabled);
+        pw.print("  mTorchAvailable=");
+        pw.println(mTorchAvailable);
+    }
+}