| /* |
| * Copyright 2019 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 androidx.camera.integration.extensions; |
| |
| import static androidx.camera.core.PreviewSurfaceProviders.createSurfaceTextureProvider; |
| |
| import android.content.Intent; |
| import android.content.pm.PackageInfo; |
| import android.content.pm.PackageManager; |
| import android.content.pm.PackageManager.NameNotFoundException; |
| import android.graphics.SurfaceTexture; |
| import android.net.Uri; |
| import android.os.Bundle; |
| import android.os.Environment; |
| import android.os.StrictMode; |
| import android.util.Log; |
| import android.util.Size; |
| import android.view.TextureView; |
| import android.view.View; |
| import android.view.ViewGroup; |
| import android.widget.Button; |
| import android.widget.Toast; |
| |
| import androidx.annotation.NonNull; |
| import androidx.annotation.Nullable; |
| import androidx.annotation.VisibleForTesting; |
| import androidx.appcompat.app.AppCompatActivity; |
| import androidx.camera.core.CameraSelector; |
| import androidx.camera.core.CameraX; |
| import androidx.camera.core.ImageCapture; |
| import androidx.camera.core.LensFacing; |
| import androidx.camera.core.Preview; |
| import androidx.camera.core.PreviewSurfaceProviders; |
| import androidx.camera.core.UseCase; |
| import androidx.camera.core.impl.utils.executor.CameraXExecutors; |
| import androidx.camera.extensions.AutoImageCaptureExtender; |
| import androidx.camera.extensions.AutoPreviewExtender; |
| import androidx.camera.extensions.BeautyImageCaptureExtender; |
| import androidx.camera.extensions.BeautyPreviewExtender; |
| import androidx.camera.extensions.BokehImageCaptureExtender; |
| import androidx.camera.extensions.BokehPreviewExtender; |
| import androidx.camera.extensions.ExtensionsErrorListener; |
| import androidx.camera.extensions.ExtensionsManager; |
| import androidx.camera.extensions.HdrImageCaptureExtender; |
| import androidx.camera.extensions.HdrPreviewExtender; |
| import androidx.camera.extensions.NightImageCaptureExtender; |
| import androidx.camera.extensions.NightPreviewExtender; |
| import androidx.camera.lifecycle.ProcessCameraProvider; |
| import androidx.concurrent.futures.CallbackToFutureAdapter; |
| import androidx.core.app.ActivityCompat; |
| import androidx.core.content.ContextCompat; |
| import androidx.test.espresso.idling.CountingIdlingResource; |
| |
| import com.google.common.base.Preconditions; |
| import com.google.common.util.concurrent.FutureCallback; |
| import com.google.common.util.concurrent.Futures; |
| import com.google.common.util.concurrent.ListenableFuture; |
| |
| import java.io.File; |
| import java.text.Format; |
| import java.text.SimpleDateFormat; |
| import java.util.ArrayList; |
| import java.util.Calendar; |
| import java.util.List; |
| import java.util.Locale; |
| import java.util.concurrent.Callable; |
| import java.util.concurrent.atomic.AtomicReference; |
| |
| /** An activity that shows off how extensions can be applied */ |
| public class CameraExtensionsActivity extends AppCompatActivity |
| implements ActivityCompat.OnRequestPermissionsResultCallback { |
| private static final String TAG = "CameraExtensionActivity"; |
| private static final int PERMISSIONS_REQUEST_CODE = 42; |
| |
| private static final CameraSelector CAMERA_SELECTOR = |
| new CameraSelector.Builder().requireLensFacing(LensFacing.BACK).build(); |
| |
| boolean mPermissionsGranted = false; |
| private CallbackToFutureAdapter.Completer<Boolean> mPermissionCompleter; |
| |
| /** The cameraId to use. Assume that 0 is the typical back facing camera. */ |
| private String mCurrentCameraId = "0"; |
| |
| private String mCurrentCameraFacing = "BACK"; |
| |
| private Preview mPreview; |
| private ImageCapture mImageCapture; |
| private ImageCaptureType mCurrentImageCaptureType = ImageCaptureType.IMAGE_CAPTURE_TYPE_HDR; |
| |
| // Espresso testing variables |
| @VisibleForTesting |
| CountingIdlingResource mTakePictureIdlingResource = new CountingIdlingResource("TakePicture"); |
| // Synthetic Accessor |
| @SuppressWarnings("WeakerAccess") |
| TextureView mTextureView; |
| |
| ProcessCameraProvider mCameraProvider; |
| |
| /** |
| * Creates a preview use case. |
| * |
| * <p>This use case observes a {@link SurfaceTexture}. The texture is connected to a {@link |
| * TextureView} to display a camera preview. |
| */ |
| private void createPreview() { |
| enablePreview(); |
| Log.i(TAG, "Got UseCase: " + mPreview); |
| } |
| |
| void enablePreview() { |
| if (mPreview != null) { |
| mCameraProvider.unbind(mPreview); |
| } |
| |
| Preview.Builder builder = new Preview.Builder() |
| .setTargetName("Preview"); |
| |
| Log.d(TAG, "Enabling the extended preview"); |
| if (mCurrentImageCaptureType == ImageCaptureType.IMAGE_CAPTURE_TYPE_BOKEH) { |
| Log.d(TAG, "Enabling the extended preview in bokeh mode."); |
| |
| BokehPreviewExtender extender = BokehPreviewExtender.create(builder); |
| if (extender.isExtensionAvailable(CAMERA_SELECTOR)) { |
| extender.enableExtension(CAMERA_SELECTOR); |
| } |
| } else if (mCurrentImageCaptureType == ImageCaptureType.IMAGE_CAPTURE_TYPE_HDR) { |
| Log.d(TAG, "Enabling the extended preview in HDR mode."); |
| |
| HdrPreviewExtender extender = HdrPreviewExtender.create(builder); |
| if (extender.isExtensionAvailable(CAMERA_SELECTOR)) { |
| extender.enableExtension(CAMERA_SELECTOR); |
| } |
| } else if (mCurrentImageCaptureType == ImageCaptureType.IMAGE_CAPTURE_TYPE_NIGHT) { |
| Log.d(TAG, "Enabling the extended preview in night mode."); |
| |
| NightPreviewExtender extender = NightPreviewExtender.create(builder); |
| if (extender.isExtensionAvailable(CAMERA_SELECTOR)) { |
| extender.enableExtension(CAMERA_SELECTOR); |
| } |
| } else if (mCurrentImageCaptureType == ImageCaptureType.IMAGE_CAPTURE_TYPE_BEAUTY) { |
| Log.d(TAG, "Enabling the extended preview in beauty mode."); |
| |
| BeautyPreviewExtender extender = BeautyPreviewExtender.create(builder); |
| if (extender.isExtensionAvailable(CAMERA_SELECTOR)) { |
| extender.enableExtension(CAMERA_SELECTOR); |
| } |
| } else if (mCurrentImageCaptureType == ImageCaptureType.IMAGE_CAPTURE_TYPE_AUTO) { |
| Log.d(TAG, "Enabling the extended preview in auto mode."); |
| |
| AutoPreviewExtender extender = AutoPreviewExtender.create(builder); |
| if (extender.isExtensionAvailable(CAMERA_SELECTOR)) { |
| extender.enableExtension(CAMERA_SELECTOR); |
| } |
| } |
| |
| mPreview = builder.build(); |
| mPreview.setPreviewSurfaceProvider(createSurfaceTextureProvider( |
| new PreviewSurfaceProviders.SurfaceTextureCallback() { |
| @Override |
| public void onSurfaceTextureReady(@NonNull SurfaceTexture surfaceTexture, |
| @NonNull Size resolution) { |
| Log.d(TAG, "onSurfaceTextureReady"); |
| // If TextureView was already created, need to re-add it to change the |
| // SurfaceTexture. |
| ViewGroup viewGroup = (ViewGroup) mTextureView.getParent(); |
| viewGroup.removeView(mTextureView); |
| viewGroup.addView(mTextureView); |
| mTextureView.setSurfaceTexture(surfaceTexture); |
| } |
| |
| @Override |
| public void onSafeToRelease(@NonNull SurfaceTexture surfaceTexture) { |
| Log.d(TAG, "onSafeToRelease"); |
| surfaceTexture.release(); |
| } |
| })); |
| } |
| |
| enum ImageCaptureType { |
| IMAGE_CAPTURE_TYPE_HDR, |
| IMAGE_CAPTURE_TYPE_BOKEH, |
| IMAGE_CAPTURE_TYPE_NIGHT, |
| IMAGE_CAPTURE_TYPE_BEAUTY, |
| IMAGE_CAPTURE_TYPE_AUTO, |
| IMAGE_CAPTURE_TYPE_DEFAULT, |
| IMAGE_CAPTURE_TYPE_NONE, |
| } |
| |
| /** |
| * Creates an image capture use case. |
| * |
| * <p>This use case takes a picture and saves it to a file, whenever the user clicks a button. |
| */ |
| private void createImageCapture() { |
| Button button = findViewById(R.id.PhotoToggle); |
| enableImageCapture(mCurrentImageCaptureType); |
| button.setOnClickListener( |
| new View.OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| disableImageCapture(); |
| // Toggle to next capture type and enable it and set it as current |
| switch (mCurrentImageCaptureType) { |
| case IMAGE_CAPTURE_TYPE_HDR: |
| enableImageCapture(ImageCaptureType.IMAGE_CAPTURE_TYPE_BOKEH); |
| enablePreview(); |
| break; |
| case IMAGE_CAPTURE_TYPE_BOKEH: |
| enableImageCapture(ImageCaptureType.IMAGE_CAPTURE_TYPE_NIGHT); |
| enablePreview(); |
| break; |
| case IMAGE_CAPTURE_TYPE_NIGHT: |
| enableImageCapture(ImageCaptureType.IMAGE_CAPTURE_TYPE_BEAUTY); |
| enablePreview(); |
| break; |
| case IMAGE_CAPTURE_TYPE_BEAUTY: |
| enableImageCapture(ImageCaptureType.IMAGE_CAPTURE_TYPE_AUTO); |
| enablePreview(); |
| break; |
| case IMAGE_CAPTURE_TYPE_AUTO: |
| enableImageCapture(ImageCaptureType.IMAGE_CAPTURE_TYPE_DEFAULT); |
| enablePreview(); |
| break; |
| case IMAGE_CAPTURE_TYPE_DEFAULT: |
| enableImageCapture(ImageCaptureType.IMAGE_CAPTURE_TYPE_NONE); |
| enablePreview(); |
| break; |
| case IMAGE_CAPTURE_TYPE_NONE: |
| enableImageCapture(ImageCaptureType.IMAGE_CAPTURE_TYPE_HDR); |
| enablePreview(); |
| break; |
| } |
| bindUseCases(); |
| showTakePictureButton(); |
| } |
| }); |
| |
| Log.i(TAG, "Got UseCase: " + mImageCapture); |
| } |
| |
| void enableImageCapture(ImageCaptureType imageCaptureType) { |
| mCurrentImageCaptureType = imageCaptureType; |
| ImageCapture.Builder builder = new ImageCapture.Builder().setTargetName("ImageCapture"); |
| Button toggleButton = findViewById(R.id.PhotoToggle); |
| toggleButton.setText(mCurrentImageCaptureType.toString()); |
| |
| switch (imageCaptureType) { |
| case IMAGE_CAPTURE_TYPE_HDR: |
| HdrImageCaptureExtender hdrImageCaptureExtender = HdrImageCaptureExtender.create( |
| builder); |
| if (hdrImageCaptureExtender.isExtensionAvailable(CAMERA_SELECTOR)) { |
| hdrImageCaptureExtender.enableExtension(CAMERA_SELECTOR); |
| } |
| break; |
| case IMAGE_CAPTURE_TYPE_BOKEH: |
| BokehImageCaptureExtender bokehImageCapture = BokehImageCaptureExtender.create( |
| builder); |
| if (bokehImageCapture.isExtensionAvailable(CAMERA_SELECTOR)) { |
| bokehImageCapture.enableExtension(CAMERA_SELECTOR); |
| } |
| break; |
| case IMAGE_CAPTURE_TYPE_NIGHT: |
| NightImageCaptureExtender nightImageCapture = NightImageCaptureExtender.create( |
| builder); |
| if (nightImageCapture.isExtensionAvailable(CAMERA_SELECTOR)) { |
| nightImageCapture.enableExtension(CAMERA_SELECTOR); |
| } |
| break; |
| case IMAGE_CAPTURE_TYPE_BEAUTY: |
| BeautyImageCaptureExtender beautyImageCapture = BeautyImageCaptureExtender.create( |
| builder); |
| if (beautyImageCapture.isExtensionAvailable(CAMERA_SELECTOR)) { |
| beautyImageCapture.enableExtension(CAMERA_SELECTOR); |
| } |
| break; |
| case IMAGE_CAPTURE_TYPE_AUTO: |
| AutoImageCaptureExtender autoImageCapture = AutoImageCaptureExtender.create( |
| builder); |
| if (autoImageCapture.isExtensionAvailable(CAMERA_SELECTOR)) { |
| autoImageCapture.enableExtension(CAMERA_SELECTOR); |
| } |
| break; |
| case IMAGE_CAPTURE_TYPE_DEFAULT: |
| break; |
| case IMAGE_CAPTURE_TYPE_NONE: |
| return; |
| } |
| |
| mImageCapture = builder.build(); |
| |
| Button captureButton = findViewById(R.id.Picture); |
| |
| final Format formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US); |
| |
| final File dir = |
| new File( |
| Environment.getExternalStoragePublicDirectory( |
| Environment.DIRECTORY_PICTURES), |
| "ExtensionsPictures"); |
| dir.mkdirs(); |
| captureButton.setOnClickListener( |
| new View.OnClickListener() { |
| @Override |
| public void onClick(View view) { |
| mTakePictureIdlingResource.increment(); |
| mImageCapture.takePicture( |
| new File( |
| dir, |
| formatter.format(Calendar.getInstance().getTime()) |
| + mCurrentImageCaptureType.name() |
| + ".jpg"), |
| CameraXExecutors.mainThreadExecutor(), |
| new ImageCapture.OnImageSavedCallback() { |
| @Override |
| public void onImageSaved(@NonNull File file) { |
| Log.d(TAG, "Saved image to " + file); |
| |
| if (!mTakePictureIdlingResource.isIdleNow()) { |
| mTakePictureIdlingResource.decrement(); |
| } |
| |
| // Trigger MediaScanner to scan the file |
| Intent intent = new Intent( |
| Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); |
| intent.setData(Uri.fromFile(file)); |
| sendBroadcast(intent); |
| |
| Toast.makeText(getApplicationContext(), |
| "Saved image to " + file, |
| Toast.LENGTH_SHORT).show(); |
| } |
| |
| @Override |
| public void onError( |
| @ImageCapture.ImageCaptureError int error, |
| @NonNull String message, |
| Throwable cause) { |
| Log.e(TAG, "Failed to save image - " + message, cause); |
| } |
| }); |
| } |
| }); |
| } |
| |
| void disableImageCapture() { |
| if (mImageCapture != null) { |
| mCameraProvider.unbind(mImageCapture); |
| mImageCapture = null; |
| } |
| |
| Button button = findViewById(R.id.Picture); |
| button.setVisibility(View.INVISIBLE); |
| button.setOnClickListener(null); |
| } |
| |
| /** Creates all the use cases. */ |
| private void createUseCases() { |
| ExtensionsManager.setExtensionsErrorListener(new ExtensionsErrorListener() { |
| @Override |
| public void onError(@NonNull ExtensionsErrorCode errorCode) { |
| Log.d(TAG, "Extensions error in error code: " + errorCode); |
| } |
| }); |
| createImageCapture(); |
| createPreview(); |
| bindUseCases(); |
| showTakePictureButton(); |
| } |
| |
| private void bindUseCases() { |
| List<UseCase> useCases = new ArrayList(); |
| // When it is not IMAGE_CAPTURE_TYPE_NONE, mImageCapture won't be null. |
| if (mImageCapture != null) { |
| useCases.add(mImageCapture); |
| } |
| useCases.add(mPreview); |
| mCameraProvider.bindToLifecycle(this, CAMERA_SELECTOR, |
| useCases.toArray(new UseCase[useCases.size()])); |
| } |
| |
| private void showTakePictureButton() { |
| if (mImageCapture != null) { |
| // Set the TakePicture button visible after bindToLifeCycle. |
| Button captureButton = findViewById(R.id.Picture); |
| captureButton.setVisibility(View.VISIBLE); |
| } |
| } |
| |
| @SuppressWarnings("UnstableApiUsage") |
| @Override |
| protected void onCreate(Bundle savedInstanceState) { |
| super.onCreate(savedInstanceState); |
| setContentView(R.layout.activity_camera_extensions); |
| |
| StrictMode.VmPolicy policy = |
| new StrictMode.VmPolicy.Builder().detectAll().penaltyLog().build(); |
| StrictMode.setVmPolicy(policy); |
| mTextureView = findViewById(R.id.textureView); |
| |
| // Get params from adb extra string |
| Bundle bundle = getIntent().getExtras(); |
| if (bundle != null) { |
| String newCameraFacing = bundle.getString("cameraFacing"); |
| if (newCameraFacing != null) { |
| mCurrentCameraFacing = newCameraFacing; |
| } |
| } |
| |
| ListenableFuture<ProcessCameraProvider> cameraProviderFuture = |
| ProcessCameraProvider.getInstance(this); |
| Futures.addCallback(setupPermissions(), new FutureCallback<Boolean>() { |
| @Override |
| public void onSuccess(@Nullable Boolean result) { |
| mPermissionsGranted = Preconditions.checkNotNull(result); |
| Futures.addCallback(cameraProviderFuture, |
| new FutureCallback<ProcessCameraProvider>() { |
| @Override |
| public void onSuccess(@Nullable ProcessCameraProvider result) { |
| mCameraProvider = result; |
| setupCamera(); |
| } |
| |
| @Override |
| public void onFailure(Throwable t) { |
| throw new RuntimeException("Failed to get camera provider", t); |
| } |
| }, ContextCompat.getMainExecutor(CameraExtensionsActivity.this)); |
| } |
| |
| @Override |
| public void onFailure(Throwable t) { |
| throw new RuntimeException("Failed to get permissions", t); |
| } |
| }, ContextCompat.getMainExecutor(this)); |
| } |
| |
| void setupCamera() { |
| if (!mPermissionsGranted) { |
| Log.d(TAG, "Permissions denied."); |
| return; |
| } |
| |
| try { |
| Log.d(TAG, "Camera Facing: " + mCurrentCameraFacing); |
| @LensFacing int facing; |
| if (mCurrentCameraFacing.equalsIgnoreCase("BACK")) { |
| facing = LensFacing.BACK; |
| } else if (mCurrentCameraFacing.equalsIgnoreCase("FRONT")) { |
| facing = LensFacing.FRONT; |
| } else { |
| throw new RuntimeException("Invalid lens facing: " + mCurrentCameraFacing); |
| } |
| mCurrentCameraId = CameraX.getCameraWithLensFacing(facing); |
| } catch (Exception e) { |
| Log.e(TAG, "Unable to obtain camera with specified facing. " + e.getMessage()); |
| } |
| |
| Log.d(TAG, "Using cameraId: " + mCurrentCameraId); |
| |
| ListenableFuture<ExtensionsManager.ExtensionsAvailability> availability = |
| ExtensionsManager.init(); |
| |
| Futures.addCallback(availability, |
| new FutureCallback<ExtensionsManager.ExtensionsAvailability>() { |
| @Override |
| public void onSuccess( |
| @Nullable ExtensionsManager.ExtensionsAvailability availability) { |
| // Run this on the UI thread to manipulate the Textures & Views. |
| createUseCases(); |
| } |
| |
| @Override |
| public void onFailure(Throwable throwable) { |
| |
| } |
| }, |
| CameraXExecutors.mainThreadExecutor() |
| ); |
| } |
| |
| private ListenableFuture<Boolean> setupPermissions() { |
| return CallbackToFutureAdapter.getFuture(completer -> { |
| mPermissionCompleter = completer; |
| if (!allPermissionsGranted()) { |
| makePermissionRequest(); |
| } else { |
| mPermissionCompleter.set(true); |
| } |
| |
| return "get_permissions"; |
| }); |
| } |
| |
| private void makePermissionRequest() { |
| ActivityCompat.requestPermissions(this, getRequiredPermissions(), PERMISSIONS_REQUEST_CODE); |
| } |
| |
| /** Returns true if all the necessary permissions have been granted already. */ |
| private boolean allPermissionsGranted() { |
| for (String permission : getRequiredPermissions()) { |
| if (ContextCompat.checkSelfPermission(this, permission) |
| != PackageManager.PERMISSION_GRANTED) { |
| return false; |
| } |
| } |
| return true; |
| } |
| |
| /** Tries to acquire all the necessary permissions through a dialog. */ |
| private String[] getRequiredPermissions() { |
| PackageInfo info; |
| try { |
| info = getPackageManager().getPackageInfo(getPackageName(), |
| PackageManager.GET_PERMISSIONS); |
| } catch (NameNotFoundException exception) { |
| Log.e(TAG, "Failed to obtain all required permissions.", exception); |
| return new String[0]; |
| } |
| String[] permissions = info.requestedPermissions; |
| if (permissions != null && permissions.length > 0) { |
| return permissions; |
| } else { |
| return new String[0]; |
| } |
| } |
| |
| public Preview getPreview() { |
| return mPreview; |
| } |
| |
| public ImageCapture getImageCapture() { |
| return mImageCapture; |
| } |
| |
| public ImageCaptureType getCurrentImageCaptureType() { |
| return mCurrentImageCaptureType; |
| } |
| |
| @Override |
| public void onRequestPermissionsResult( |
| int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { |
| switch (requestCode) { |
| case PERMISSIONS_REQUEST_CODE: { |
| // If request is cancelled, the result arrays are empty. |
| if (grantResults.length > 0 |
| && grantResults[0] == PackageManager.PERMISSION_GRANTED) { |
| Log.d(TAG, "Permissions Granted."); |
| mPermissionCompleter.set(true); |
| } else { |
| Log.d(TAG, "Permissions Denied."); |
| mPermissionCompleter.set(false); |
| } |
| return; |
| } |
| default: |
| // No-op |
| } |
| } |
| |
| /** A {@link Callable} whose return value can be set. */ |
| private static final class SettableCallable<V> implements Callable<V> { |
| private final AtomicReference<V> mValue = new AtomicReference<>(); |
| |
| public void set(V value) { |
| mValue.set(value); |
| } |
| |
| @Override |
| public V call() { |
| return mValue.get(); |
| } |
| } |
| } |