diff --git a/res/drawable/ic_signal_flashlight.xml b/res/drawable/ic_signal_flashlight.xml
new file mode 100644
index 00000000000..e63595300d5
--- /dev/null
+++ b/res/drawable/ic_signal_flashlight.xml
@@ -0,0 +1,28 @@
+
+
+
+
+
+
diff --git a/src/com/android/settings/flashlight/FlashlightSliceBuilder.java b/src/com/android/settings/flashlight/FlashlightSliceBuilder.java
new file mode 100644
index 00000000000..6317ce760f4
--- /dev/null
+++ b/src/com/android/settings/flashlight/FlashlightSliceBuilder.java
@@ -0,0 +1,156 @@
+/*
+ * 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.settings.flashlight;
+
+import static android.app.slice.Slice.EXTRA_TOGGLE_STATE;
+import static androidx.slice.builders.ListBuilder.ICON_IMAGE;
+
+import android.annotation.ColorInt;
+import android.app.PendingIntent;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.hardware.camera2.CameraAccessException;
+import android.hardware.camera2.CameraCharacteristics;
+import android.hardware.camera2.CameraManager;
+import android.net.Uri;
+import android.provider.Settings;
+import android.provider.Settings.Secure;
+import android.provider.SettingsSlicesContract;
+import android.util.Log;
+
+import androidx.core.graphics.drawable.IconCompat;
+import com.android.settings.R;
+import com.android.settings.Utils;
+import com.android.settings.slices.SettingsSliceProvider;
+import com.android.settings.slices.SliceBroadcastReceiver;
+
+import androidx.slice.Slice;
+import androidx.slice.builders.ListBuilder;
+import androidx.slice.builders.SliceAction;
+
+import android.app.StatusBarManager;
+
+
+/**
+ * Utility class to build a Flashlight Slice, and handle all associated actions.
+ */
+public class FlashlightSliceBuilder {
+
+ private static final String TAG = "FlashlightSliceBuilder";
+
+ public static final String KEY_FLASHLIGHT = "flashlight";
+
+ /**
+ * Backing Uri for the Flashlight Slice.
+ */
+ public static final Uri FLASHLIGHT_URI = new Uri.Builder()
+ .scheme(ContentResolver.SCHEME_CONTENT)
+ .authority(SettingsSliceProvider.SLICE_AUTHORITY)
+ .appendPath(SettingsSlicesContract.PATH_SETTING_ACTION)
+ .appendPath(KEY_FLASHLIGHT)
+ .build();
+
+ /**
+ * Action notifying a change on the Flashlight Slice.
+ */
+ public static final String ACTION_FLASHLIGHT_SLICE_CHANGED =
+ "com.android.settings.flashlight.action.FLASHLIGHT_SLICE_CHANGED";
+
+ /**
+ * Action broadcasting a change on whether flashlight is on or off.
+ */
+ public static final String ACTION_FLASHLIGHT_CHANGED =
+ "com.android.settings.flashlight.action.FLASHLIGHT_CHANGED";
+
+ public static final IntentFilter INTENT_FILTER = new IntentFilter(ACTION_FLASHLIGHT_CHANGED);
+
+ private FlashlightSliceBuilder() {}
+
+ /**
+ * Return a Flashlight Slice bound to {@link #FLASHLIGHT_URI}.
+ */
+ public static Slice getSlice(Context context) {
+ if (!isFlashlightAvailable(context)) {
+ return null;
+ }
+ final PendingIntent toggleAction = getBroadcastIntent(context);
+ @ColorInt final int color = Utils.getColorAccentDefaultColor(context);
+ final IconCompat icon =
+ IconCompat.createWithResource(context, R.drawable.ic_signal_flashlight);
+ return new ListBuilder(context, FLASHLIGHT_URI, ListBuilder.INFINITY)
+ .setAccentColor(color)
+ .addRow(b -> b
+ .setTitle(context.getText(R.string.power_flashlight))
+ .setTitleItem(icon, ICON_IMAGE)
+ .setPrimaryAction(
+ new SliceAction(toggleAction, null, isFlashlightEnabled(context))))
+ .build();
+ }
+
+ /**
+ * Update the current flashlight status to the boolean value keyed by
+ * {@link android.app.slice.Slice#EXTRA_TOGGLE_STATE} on {@param intent}.
+ */
+ public static void handleUriChange(Context context, Intent intent) {
+ try {
+ final String cameraId = getCameraId(context);
+ if (cameraId != null) {
+ final boolean state = intent.getBooleanExtra(
+ EXTRA_TOGGLE_STATE, isFlashlightEnabled(context));
+ final CameraManager cameraManager = context.getSystemService(CameraManager.class);
+ cameraManager.setTorchMode(cameraId, state);
+ }
+ } catch (CameraAccessException e) {
+ Log.e(TAG, "Camera couldn't set torch mode.", e);
+ }
+ context.getContentResolver().notifyChange(FLASHLIGHT_URI, null);
+ }
+
+ private static String getCameraId(Context context) throws CameraAccessException {
+ final CameraManager cameraManager = context.getSystemService(CameraManager.class);
+ final String[] ids = cameraManager.getCameraIdList();
+ for (String id : ids) {
+ CameraCharacteristics c = cameraManager.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 static PendingIntent getBroadcastIntent(Context context) {
+ final Intent intent = new Intent(ACTION_FLASHLIGHT_SLICE_CHANGED);
+ intent.setClass(context, SliceBroadcastReceiver.class);
+ return PendingIntent.getBroadcast(context, 0 /* requestCode */, intent,
+ PendingIntent.FLAG_CANCEL_CURRENT);
+ }
+
+ private static boolean isFlashlightAvailable(Context context) {
+ return Settings.Secure.getInt(
+ context.getContentResolver(), Secure.FLASHLIGHT_AVAILABLE, 0) == 1;
+ }
+
+ private static boolean isFlashlightEnabled(Context context) {
+ return Settings.Secure.getInt(
+ context.getContentResolver(), Secure.FLASHLIGHT_ENABLED, 0) == 1;
+ }
+}
diff --git a/src/com/android/settings/slices/SettingsSliceProvider.java b/src/com/android/settings/slices/SettingsSliceProvider.java
index db09a378e1b..7eda82b902f 100644
--- a/src/com/android/settings/slices/SettingsSliceProvider.java
+++ b/src/com/android/settings/slices/SettingsSliceProvider.java
@@ -34,6 +34,7 @@ import android.util.Pair;
import com.android.settings.bluetooth.BluetoothSliceBuilder;
import com.android.settings.core.BasePreferenceController;
+import com.android.settings.flashlight.FlashlightSliceBuilder;
import com.android.settings.location.LocationSliceBuilder;
import com.android.settings.notification.ZenModeSliceBuilder;
import com.android.settings.mobilenetwork.Enhanced4gLteSliceHelper;
@@ -160,6 +161,10 @@ public class SettingsSliceProvider extends SliceProvider {
} else if (BluetoothSliceBuilder.BLUETOOTH_URI.equals(sliceUri)) {
registerIntentToUri(BluetoothSliceBuilder.INTENT_FILTER, sliceUri);
return;
+ } else if (FlashlightSliceBuilder.FLASHLIGHT_URI.equals(sliceUri)) {
+ registerIntentToUri(FlashlightSliceBuilder.INTENT_FILTER , sliceUri);
+ mRegisteredUris.add(sliceUri);
+ return;
}
// Start warming the slice, we expect someone will want it soon.
@@ -191,32 +196,35 @@ public class SettingsSliceProvider extends SliceProvider {
Log.e(TAG, "Requested blocked slice with Uri: " + sliceUri);
return null;
}
- // If adding a new Slice, do not directly match Slice URIs.
- // Use {@link SlicesDatabaseAccessor}.
- if (WifiCallingSliceHelper.WIFI_CALLING_URI.equals(sliceUri)) {
- return FeatureFactory.getFactory(getContext())
- .getSlicesFeatureProvider()
- .getNewWifiCallingSliceHelper(getContext())
- .createWifiCallingSlice(sliceUri);
- } else if (WifiSliceBuilder.WIFI_URI.equals(sliceUri)) {
- return WifiSliceBuilder.getSlice(getContext());
- } else if (ZenModeSliceBuilder.ZEN_MODE_URI.equals(sliceUri)) {
- return ZenModeSliceBuilder.getSlice(getContext());
- } else if (BluetoothSliceBuilder.BLUETOOTH_URI.equals(sliceUri)) {
- return BluetoothSliceBuilder.getSlice(getContext());
- } else if (LocationSliceBuilder.LOCATION_URI.equals(sliceUri)) {
- return LocationSliceBuilder.getSlice(getContext());
- } else if (Enhanced4gLteSliceHelper.SLICE_URI.equals(sliceUri)) {
- return FeatureFactory.getFactory(getContext())
- .getSlicesFeatureProvider()
- .getNewEnhanced4gLteSliceHelper(getContext())
- .createEnhanced4gLteSlice(sliceUri);
- } else if (WifiCallingSliceHelper.WIFI_CALLING_PREFERENCE_URI.equals(sliceUri)) {
- return FeatureFactory.getFactory(getContext())
- .getSlicesFeatureProvider()
- .getNewWifiCallingSliceHelper(getContext())
- .createWifiCallingPreferenceSlice(sliceUri);
- }
+
+ // If adding a new Slice, do not directly match Slice URIs.
+ // Use {@link SlicesDatabaseAccessor}.
+ if (WifiCallingSliceHelper.WIFI_CALLING_URI.equals(sliceUri)) {
+ return FeatureFactory.getFactory(getContext())
+ .getSlicesFeatureProvider()
+ .getNewWifiCallingSliceHelper(getContext())
+ .createWifiCallingSlice(sliceUri);
+ } else if (WifiSliceBuilder.WIFI_URI.equals(sliceUri)) {
+ return WifiSliceBuilder.getSlice(getContext());
+ } else if (ZenModeSliceBuilder.ZEN_MODE_URI.equals(sliceUri)) {
+ return ZenModeSliceBuilder.getSlice(getContext());
+ } else if (BluetoothSliceBuilder.BLUETOOTH_URI.equals(sliceUri)) {
+ return BluetoothSliceBuilder.getSlice(getContext());
+ } else if (LocationSliceBuilder.LOCATION_URI.equals(sliceUri)) {
+ return LocationSliceBuilder.getSlice(getContext());
+ } else if (Enhanced4gLteSliceHelper.SLICE_URI.equals(sliceUri)) {
+ return FeatureFactory.getFactory(getContext())
+ .getSlicesFeatureProvider()
+ .getNewEnhanced4gLteSliceHelper(getContext())
+ .createEnhanced4gLteSlice(sliceUri);
+ } else if (WifiCallingSliceHelper.WIFI_CALLING_PREFERENCE_URI.equals(sliceUri)) {
+ return FeatureFactory.getFactory(getContext())
+ .getSlicesFeatureProvider()
+ .getNewWifiCallingSliceHelper(getContext())
+ .createWifiCallingPreferenceSlice(sliceUri);
+ } else if (FlashlightSliceBuilder.FLASHLIGHT_URI.equals(sliceUri)) {
+ return FlashlightSliceBuilder.getSlice(getContext());
+ }
SliceData cachedSliceData = mSliceWeakDataCache.get(sliceUri);
if (cachedSliceData == null) {
@@ -381,7 +389,8 @@ public class SettingsSliceProvider extends SliceProvider {
private List getSpecialCaseOemUris() {
return Arrays.asList(
- ZenModeSliceBuilder.ZEN_MODE_URI
+ ZenModeSliceBuilder.ZEN_MODE_URI,
+ FlashlightSliceBuilder.FLASHLIGHT_URI
);
}
diff --git a/src/com/android/settings/slices/SliceBroadcastReceiver.java b/src/com/android/settings/slices/SliceBroadcastReceiver.java
index 9f1ef769d7d..95f053844e5 100644
--- a/src/com/android/settings/slices/SliceBroadcastReceiver.java
+++ b/src/com/android/settings/slices/SliceBroadcastReceiver.java
@@ -17,6 +17,7 @@
package com.android.settings.slices;
import static com.android.settings.bluetooth.BluetoothSliceBuilder.ACTION_BLUETOOTH_SLICE_CHANGED;
+import static com.android.settings.flashlight.FlashlightSliceBuilder.ACTION_FLASHLIGHT_SLICE_CHANGED;
import static com.android.settings.notification.ZenModeSliceBuilder.ACTION_ZEN_MODE_SLICE_CHANGED;
import static com.android.settings.slices.SettingsSliceProvider.ACTION_SLIDER_CHANGED;
import static com.android.settings.slices.SettingsSliceProvider.ACTION_TOGGLE_CHANGED;
@@ -45,6 +46,7 @@ import com.android.settings.bluetooth.BluetoothSliceBuilder;
import com.android.settings.core.BasePreferenceController;
import com.android.settings.core.SliderPreferenceController;
import com.android.settings.core.TogglePreferenceController;
+import com.android.settings.flashlight.FlashlightSliceBuilder;
import com.android.settings.notification.ZenModeSliceBuilder;
import com.android.settings.overlay.FeatureFactory;
import com.android.settings.wifi.WifiSliceBuilder;
@@ -102,6 +104,9 @@ public class SliceBroadcastReceiver extends BroadcastReceiver {
.getNewWifiCallingSliceHelper(context)
.handleWifiCallingPreferenceChanged(intent);
break;
+ case ACTION_FLASHLIGHT_SLICE_CHANGED:
+ FlashlightSliceBuilder.handleUriChange(context, intent);
+ break;
default:
final String uriString = intent.getStringExtra(SliceBroadcastRelay.EXTRA_URI);
if (!TextUtils.isEmpty(uriString)) {
diff --git a/tests/robotests/src/com/android/settings/flashlight/FlashlightSliceBuilderTest.java b/tests/robotests/src/com/android/settings/flashlight/FlashlightSliceBuilderTest.java
new file mode 100644
index 00000000000..5862ab3d286
--- /dev/null
+++ b/tests/robotests/src/com/android/settings/flashlight/FlashlightSliceBuilderTest.java
@@ -0,0 +1,81 @@
+/*
+ * 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.settings.flashlight;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+
+import com.android.settings.R;
+import com.android.settings.testutils.SettingsRobolectricTestRunner;
+import com.android.settings.testutils.SliceTester;
+
+import android.content.res.Resources;
+import android.provider.Settings;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.robolectric.RuntimeEnvironment;
+
+import java.util.List;
+
+import androidx.slice.Slice;
+import androidx.slice.SliceItem;
+import androidx.slice.SliceMetadata;
+import androidx.slice.SliceProvider;
+import androidx.slice.core.SliceAction;
+import androidx.slice.widget.SliceLiveData;
+
+
+@RunWith(SettingsRobolectricTestRunner.class)
+public class FlashlightSliceBuilderTest {
+
+ private Context mContext;
+
+ @Before
+ public void setUp() {
+ mContext = spy(RuntimeEnvironment.application);
+
+ // Prevent crash in SliceMetadata.
+ Resources resources = spy(mContext.getResources());
+ doReturn(60).when(resources).getDimensionPixelSize(anyInt());
+ doReturn(resources).when(mContext).getResources();
+
+ // Set-up specs for SliceMetadata.
+ SliceProvider.setSpecs(SliceLiveData.SUPPORTED_SPECS);
+ }
+
+ @Test
+ public void getFlashlightSlice_correctData() {
+ Settings.Secure.putInt(
+ mContext.getContentResolver(), Settings.Secure.FLASHLIGHT_AVAILABLE, 1);
+ final Slice slice = FlashlightSliceBuilder.getSlice(mContext);
+ final SliceMetadata metadata = SliceMetadata.from(mContext, slice);
+
+ final List toggles = metadata.getToggles();
+ assertThat(toggles).hasSize(1);
+
+ final List sliceItems = slice.getItems();
+ SliceTester.assertTitle(sliceItems, mContext.getString(R.string.power_flashlight));
+ }
+}
diff --git a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java
index 6b1262a1e2c..c72e9f6c542 100644
--- a/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java
+++ b/tests/robotests/src/com/android/settings/slices/SettingsSliceProviderTest.java
@@ -33,10 +33,12 @@ import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;
import android.os.StrictMode;
+import android.provider.Settings;
import android.provider.SettingsSlicesContract;
import android.util.ArraySet;
import com.android.settings.bluetooth.BluetoothSliceBuilder;
+import com.android.settings.flashlight.FlashlightSliceBuilder;
import com.android.settings.location.LocationSliceBuilder;
import com.android.settings.notification.ZenModeSliceBuilder;
import com.android.settings.testutils.DatabaseTestUtils;
@@ -95,7 +97,8 @@ public class SettingsSliceProviderTest {
);
private static final List SPECIAL_CASE_OEM_URIS = Arrays.asList(
- ZenModeSliceBuilder.ZEN_MODE_URI
+ ZenModeSliceBuilder.ZEN_MODE_URI,
+ FlashlightSliceBuilder.FLASHLIGHT_URI
);
@Before
@@ -444,6 +447,16 @@ public class SettingsSliceProviderTest {
assertThat(wifiSlice.getUri()).isEqualTo(WifiSliceBuilder.WIFI_URI);
}
+ @Test
+ public void bindSlice_flashlightSlice_returnsFlashlightSlice() {
+ Settings.Secure.putInt(
+ mContext.getContentResolver(), Settings.Secure.FLASHLIGHT_AVAILABLE, 1);
+
+ final Slice flashlightSlice = mProvider.onBindSlice(FlashlightSliceBuilder.FLASHLIGHT_URI);
+
+ assertThat(flashlightSlice.getUri()).isEqualTo(FlashlightSliceBuilder.FLASHLIGHT_URI);
+ }
+
@Test
public void onSlicePinned_noIntentRegistered_specialCaseUri_doesNotCrash() {
final Uri uri = new Uri.Builder()