Add flashlight to quick settings

Bug: 15934851

Change-Id: I86f61fa11fe64e76adb032391ce7e7170f59549d
diff --git a/packages/SystemUI/AndroidManifest.xml b/packages/SystemUI/AndroidManifest.xml
index e12549a..ad27e41 100644
--- a/packages/SystemUI/AndroidManifest.xml
+++ b/packages/SystemUI/AndroidManifest.xml
@@ -106,6 +106,8 @@
     <!-- Wifi Display -->
     <uses-permission android:name="android.permission.CONFIGURE_WIFI_DISPLAY" />
 
+    <uses-permission android:name="android.permission.CAMERA" />
+
     <application
         android:name=".SystemUIApplication"
         android:persistent="true"
diff --git a/packages/SystemUI/res/drawable/ic_qs_flashlight_off.xml b/packages/SystemUI/res/drawable/ic_qs_flashlight_off.xml
new file mode 100644
index 0000000..49eba22
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_qs_flashlight_off.xml
@@ -0,0 +1,28 @@
+<!--
+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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android" >
+    <size
+        android:width="64.0dp"
+        android:height="64.0dp"/>
+
+    <viewport
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"/>
+
+    <path
+        android:fill="#4DFFFFFF"
+        android:pathData="M3.3,3.0L2.0,4.3l5.0,5.0L7.0,13.0l3.0,0.0l0.0,9.0l3.6,-6.1l4.1,4.1l1.3,-1.3L3.3,3.0zM17.0,10.0l-4.0,0.0l4.0,-8.0L7.0,2.0l0.0,2.2l8.5,8.5L17.0,10.0z"/>
+</vector>
diff --git a/packages/SystemUI/res/drawable/ic_qs_flashlight_on.xml b/packages/SystemUI/res/drawable/ic_qs_flashlight_on.xml
new file mode 100644
index 0000000..101ca84
--- /dev/null
+++ b/packages/SystemUI/res/drawable/ic_qs_flashlight_on.xml
@@ -0,0 +1,28 @@
+<!--
+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.
+-->
+<vector xmlns:android="http://schemas.android.com/apk/res/android" >
+    <size
+        android:width="64.0dp"
+        android:height="64.0dp"/>
+
+    <viewport
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0"/>
+
+    <path
+        android:fill="#FFFFFFFF"
+        android:pathData="M7.0,2.0l0.0,11.0 3.0,0.0 0.0,9.0 7.0,-12.0 -4.0,0.0 4.0,-8.0z"/>
+</vector>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index f021253..a8799f7 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -540,6 +540,8 @@
     <string name="quick_settings_hotspot_label">Hotspot</string>
     <!-- QuickSettings: Notifications [CHAR LIMIT=NONE] -->
     <string name="quick_settings_notifications_label">Notifications</string>
+    <!-- QuickSettings: Flashlight [CHAR LIMIT=NONE] -->
+    <string name="quick_settings_flashlight_label">Flashlight</string>
 
     <!-- Recents: The empty recents string. [CHAR LIMIT=NONE] -->
     <string name="recents_empty_message">No recent apps</string>
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
index 786cd9e..ba350e5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSTile.java
@@ -30,6 +30,7 @@
 import com.android.systemui.qs.QSTile.State;
 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.Listenable;
 import com.android.systemui.statusbar.policy.LocationController;
 import com.android.systemui.statusbar.policy.NetworkController;
@@ -221,6 +222,7 @@
         TetheringController getTetheringController();
         CastController getCastController();
         VolumeComponent getVolumeComponent();
+        FlashlightController getFlashlightController();
     }
 
     public static class State {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
new file mode 100644
index 0000000..b610cf3
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/FlashlightTile.java
@@ -0,0 +1,80 @@
+/*
+ * 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.qs.tiles;
+
+import com.android.systemui.R;
+import com.android.systemui.qs.QSTile;
+import com.android.systemui.qs.SecureSetting;
+import com.android.systemui.statusbar.policy.FlashlightController;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.provider.Settings.Secure;
+
+/** Quick settings tile: Control flashlight **/
+public class FlashlightTile extends QSTile<QSTile.BooleanState> implements
+        FlashlightController.FlashlightListener {
+
+    private final FlashlightController mFlashlightController;
+
+    public FlashlightTile(Host host) {
+        super(host);
+        mFlashlightController = host.getFlashlightController();
+        mFlashlightController.addListener(this);
+    }
+
+    @Override
+    protected BooleanState newTileState() {
+        return new BooleanState();
+    }
+
+    @Override
+    public void setListening(boolean listening) {
+    }
+
+    @Override
+    protected void handleUserSwitch(int newUserId) {
+    }
+
+    @Override
+    protected void handleClick() {
+        boolean newState = !mState.value;
+        mFlashlightController.setFlashlight(newState);
+        refreshState(newState);
+    }
+
+    @Override
+    protected void handleUpdateState(BooleanState state, Object arg) {
+        if (arg instanceof Boolean) {
+            state.value = (Boolean) arg;
+        }
+        state.visible = mFlashlightController.isAvailable();
+        state.label = mHost.getContext().getString(R.string.quick_settings_flashlight_label);
+        state.iconId = state.value
+                ? R.drawable.ic_qs_flashlight_on : R.drawable.ic_qs_flashlight_off;
+    }
+
+    @Override
+    public void onFlashlightOff() {
+        refreshState(false);
+    }
+
+    @Override
+    public void onFlashlightError() {
+        refreshState(false);
+    }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
index 5c6d279..387f5a7 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/PhoneStatusBar.java
@@ -122,6 +122,7 @@
 import com.android.systemui.statusbar.policy.BluetoothControllerImpl;
 import com.android.systemui.statusbar.policy.CastControllerImpl;
 import com.android.systemui.statusbar.policy.DateView;
+import com.android.systemui.statusbar.policy.FlashlightController;
 import com.android.systemui.statusbar.policy.HeadsUpNotificationView;
 import com.android.systemui.statusbar.policy.KeyguardUserSwitcher;
 import com.android.systemui.statusbar.policy.UserInfoController;
@@ -193,6 +194,7 @@
     CastControllerImpl mCastController;
     VolumeComponent mVolumeComponent;
     KeyguardUserSwitcher mKeyguardUserSwitcher;
+    FlashlightController mFlashlightController;
 
     int mNaturalBarHeight = -1;
     int mIconSize = -1;
@@ -710,6 +712,7 @@
         }
 
         mBatteryController.setStatusBarHeaderView(mHeader);
+        mFlashlightController = new FlashlightController(mContext);
 
         // Set up the quick settings tile panel
         mQSPanel = (QSPanel) mStatusBarWindow.findViewById(R.id.quick_settings_panel);
@@ -725,7 +728,7 @@
             final QSTileHost qsh = new QSTileHost(mContext, this,
                     mBluetoothController, mLocationController, mRotationLockController,
                     mNetworkController, mZenModeController, null /*tethering*/,
-                    mCastController, mVolumeComponent);
+                    mCastController, mVolumeComponent, mFlashlightController);
             for (QSTile<?> tile : qsh.getTiles()) {
                 mQSPanel.addTile(tile);
             }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
index 7c87580..60f38b5 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/QSTileHost.java
@@ -29,6 +29,7 @@
 import com.android.systemui.qs.tiles.CastTile;
 import com.android.systemui.qs.tiles.CellularTile;
 import com.android.systemui.qs.tiles.ColorInversionTile;
+import com.android.systemui.qs.tiles.FlashlightTile;
 import com.android.systemui.qs.tiles.LocationTile;
 import com.android.systemui.qs.tiles.NotificationsTile;
 import com.android.systemui.qs.tiles.RotationLockTile;
@@ -37,6 +38,7 @@
 import com.android.systemui.settings.CurrentUserTracker;
 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.LocationController;
 import com.android.systemui.statusbar.policy.NetworkController;
 import com.android.systemui.statusbar.policy.RotationLockController;
@@ -64,12 +66,13 @@
     private final VolumeComponent mVolume;
     private final ArrayList<QSTile<?>> mTiles = new ArrayList<QSTile<?>>();
     private final int mFeedbackStartDelay;
+    private final FlashlightController mFlashlight;
 
     public QSTileHost(Context context, PhoneStatusBar statusBar,
             BluetoothController bluetooth, LocationController location,
             RotationLockController rotation, NetworkController network,
             ZenModeController zen, TetheringController tethering,
-            CastController cast, VolumeComponent volume) {
+            CastController cast, VolumeComponent volume, FlashlightController flashlight) {
         mContext = context;
         mStatusBar = statusBar;
         mBluetooth = bluetooth;
@@ -80,6 +83,7 @@
         mTethering = tethering;
         mCast = cast;
         mVolume = volume;
+        mFlashlight = flashlight;
 
         final HandlerThread ht = new HandlerThread(QSTileHost.class.getSimpleName());
         ht.start();
@@ -95,6 +99,7 @@
         mTiles.add(new LocationTile(this));
         mTiles.add(new CastTile(this));
         mTiles.add(new HotspotTile(this));
+        mTiles.add(new FlashlightTile(this));
 
         mUserTracker = new CurrentUserTracker(mContext) {
             @Override
@@ -177,4 +182,9 @@
     public VolumeComponent getVolumeComponent() {
         return mVolume;
     }
+
+    @Override
+    public FlashlightController getFlashlightController() {
+        return mFlashlight;
+    }
 }
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java
new file mode 100644
index 0000000..b059043
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/FlashlightController.java
@@ -0,0 +1,308 @@
+/*
+ * 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.graphics.SurfaceTexture;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCaptureSession;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraDevice;
+import android.hardware.camera2.CameraManager;
+import android.hardware.camera2.CameraMetadata;
+import android.hardware.camera2.CaptureRequest;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.util.Log;
+import android.util.Size;
+import android.view.Surface;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Manages the flashlight.
+ */
+public class FlashlightController {
+
+    private static final String TAG = "FlashlightController";
+
+    private final CameraManager mCameraManager;
+    /** 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 CameraDevice mCameraDevice;
+    private CaptureRequest mFlashlightRequest;
+    private CameraCaptureSession mSession;
+    private SurfaceTexture mSurfaceTexture;
+    private Surface mSurface;
+
+    public FlashlightController(Context mContext) {
+        mCameraManager = (CameraManager) mContext.getSystemService(Context.CAMERA_SERVICE);
+    }
+
+    public synchronized void setFlashlight(boolean enabled) {
+        if (mFlashlightEnabled != enabled) {
+            mFlashlightEnabled = enabled;
+            postUpdateFlashlight();
+        }
+    }
+
+    public boolean isAvailable() {
+        try {
+            return getCameraId() != null;
+        } catch (CameraAccessException e) {
+            return false;
+        }
+    }
+
+    public void addListener(FlashlightListener l) {
+        synchronized (mListeners) {
+            cleanUpListenersLocked(l);
+            mListeners.add(new WeakReference<>(l));
+        }
+    }
+
+    public void removeListener(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 void startDevice() throws CameraAccessException {
+        mCameraManager.openCamera(getCameraId(), mCameraListener, mHandler);
+    }
+
+    private void startSession() throws CameraAccessException {
+        mSurfaceTexture = new SurfaceTexture(false);
+        Size size = getSmallestSize(mCameraDevice.getId());
+        mSurfaceTexture.setDefaultBufferSize(size.getWidth(), size.getHeight());
+        mSurface = new Surface(mSurfaceTexture);
+        ArrayList<Surface> outputs = new ArrayList<>(1);
+        outputs.add(mSurface);
+        mCameraDevice.createCaptureSession(outputs, mSessionListener, mHandler);
+    }
+
+    private Size getSmallestSize(String cameraId) throws CameraAccessException {
+        Size[] outputSizes = mCameraManager.getCameraCharacteristics(cameraId)
+                .get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP)
+                .getOutputSizes(SurfaceTexture.class);
+        if (outputSizes == null || outputSizes.length == 0) {
+            throw new IllegalStateException(
+                    "Camera " + cameraId + "doesn't support any outputSize.");
+        }
+        Size chosen = outputSizes[0];
+        for (Size s : outputSizes) {
+            if (chosen.getWidth() >= s.getWidth() && chosen.getHeight() >= s.getHeight()) {
+                chosen = s;
+            }
+        }
+        return chosen;
+    }
+
+    private void postUpdateFlashlight() {
+        ensureHandler();
+        mHandler.post(mUpdateFlashlightRunnable);
+    }
+
+    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 updateFlashlight(boolean forceDisable) {
+        try {
+            boolean enabled;
+            synchronized (this) {
+                enabled = mFlashlightEnabled && !forceDisable;
+            }
+            if (enabled) {
+                if (mCameraDevice == null) {
+                    startDevice();
+                    return;
+                }
+                if (mSession == null) {
+                    startSession();
+                    return;
+                }
+                if (mFlashlightRequest == null) {
+                    CaptureRequest.Builder builder = mCameraDevice.createCaptureRequest(
+                            CameraDevice.TEMPLATE_PREVIEW);
+                    builder.set(CaptureRequest.FLASH_MODE, CameraMetadata.FLASH_MODE_TORCH);
+                    builder.addTarget(mSurface);
+                    CaptureRequest request = builder.build();
+                    mSession.capture(request, null, mHandler);
+                    mFlashlightRequest = request;
+                }
+            } else {
+                if (mCameraDevice != null) {
+                    mCameraDevice.close();
+                    teardown();
+                }
+            }
+
+        } catch (CameraAccessException|IllegalStateException|UnsupportedOperationException e) {
+            Log.e(TAG, "Error in updateFlashlight", e);
+            handleError();
+        }
+    }
+
+    private void teardown() {
+        mCameraDevice = null;
+        mSession = null;
+        mFlashlightRequest = null;
+        if (mSurface != null) {
+            mSurface.release();
+            mSurfaceTexture.release();
+        }
+        mSurface = null;
+        mSurfaceTexture = null;
+    }
+
+    private void handleError() {
+        synchronized (this) {
+            mFlashlightEnabled = false;
+        }
+        dispatchError();
+        dispatchOff();
+        updateFlashlight(true /* forceDisable */);
+    }
+
+    private void dispatchOff() {
+        dispatchListeners(false, true /* off */);
+    }
+
+    private void dispatchError() {
+        dispatchListeners(true /* error */, false);
+    }
+
+    private void dispatchListeners(boolean error, boolean off) {
+        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 (error) {
+                        l.onFlashlightError();
+                    } else if (off) {
+                        l.onFlashlightOff();
+                    }
+                } 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 CameraDevice.StateListener mCameraListener = new CameraDevice.StateListener() {
+        @Override
+        public void onOpened(CameraDevice camera) {
+            mCameraDevice = camera;
+            postUpdateFlashlight();
+        }
+
+        @Override
+        public void onDisconnected(CameraDevice camera) {
+            if (mCameraDevice == camera) {
+                dispatchOff();
+                teardown();
+            }
+        }
+
+        @Override
+        public void onError(CameraDevice camera, int error) {
+            Log.e(TAG, "Camera error: camera=" + camera + " error=" + error);
+            if (camera == mCameraDevice || mCameraDevice == null) {
+                handleError();
+            }
+        }
+    };
+
+    private final CameraCaptureSession.StateListener mSessionListener =
+            new CameraCaptureSession.StateListener() {
+        @Override
+        public void onConfigured(CameraCaptureSession session) {
+            mSession = session;
+            postUpdateFlashlight();
+        }
+
+        @Override
+        public void onConfigureFailed(CameraCaptureSession session) {
+            Log.e(TAG, "Configure failed.");
+            if (mSession == null || mSession == session) {
+                handleError();
+            }
+        }
+    };
+
+    private final Runnable mUpdateFlashlightRunnable = new Runnable() {
+        @Override
+        public void run() {
+            updateFlashlight(false /* forceDisable */);
+        }
+    };
+
+    public interface FlashlightListener {
+
+        /**
+         * Called when the flashlight turns off unexpectedly.
+         */
+        void onFlashlightOff();
+
+        /**
+         * Called when there is an error that turns the flashlight off.
+         */
+        void onFlashlightError();
+    }
+}