1 /*
2  * Copyright 2021 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package androidx.camera.integration.view;
18 
19 import static java.lang.Math.abs;
20 import static java.lang.Math.round;
21 
22 import android.content.ContentResolver;
23 import android.content.ContentValues;
24 import android.graphics.Bitmap;
25 import android.graphics.BitmapFactory;
26 import android.graphics.Canvas;
27 import android.graphics.Color;
28 import android.graphics.Matrix;
29 import android.graphics.Paint;
30 import android.graphics.Rect;
31 import android.graphics.RectF;
32 import android.net.Uri;
33 import android.os.Bundle;
34 import android.os.Environment;
35 import android.provider.MediaStore;
36 import android.util.Log;
37 import android.view.LayoutInflater;
38 import android.view.View;
39 import android.view.ViewGroup;
40 import android.widget.Toast;
41 import android.widget.ToggleButton;
42 
43 import androidx.annotation.MainThread;
44 import androidx.annotation.OptIn;
45 import androidx.camera.core.CameraSelector;
46 import androidx.camera.core.ImageAnalysis;
47 import androidx.camera.core.ImageCapture;
48 import androidx.camera.core.ImageCaptureException;
49 import androidx.camera.core.ImageProxy;
50 import androidx.camera.core.Logger;
51 import androidx.camera.view.LifecycleCameraController;
52 import androidx.camera.view.PreviewView;
53 import androidx.camera.view.TransformExperimental;
54 import androidx.camera.view.transform.CoordinateTransform;
55 import androidx.camera.view.transform.FileTransformFactory;
56 import androidx.camera.view.transform.ImageProxyTransformFactory;
57 import androidx.camera.view.transform.OutputTransform;
58 import androidx.exifinterface.media.ExifInterface;
59 import androidx.fragment.app.Fragment;
60 
61 import org.jspecify.annotations.NonNull;
62 import org.jspecify.annotations.Nullable;
63 
64 import java.io.File;
65 import java.io.FileInputStream;
66 import java.io.FileOutputStream;
67 import java.io.IOException;
68 import java.io.InputStream;
69 import java.io.OutputStream;
70 import java.util.concurrent.ExecutorService;
71 import java.util.concurrent.Executors;
72 
73 /**
74  * A fragment that demos transform utilities.
75  */
76 @OptIn(markerClass = TransformExperimental.class)
77 public final class TransformFragment extends Fragment {
78 
79     private static final String TAG = "TransformFragment";
80 
81     private static final int TILE_COUNT = 4;
82     public static final RectF NORMALIZED_RECT = new RectF(-1, -1, 1, 1);
83 
84     private LifecycleCameraController mCameraController;
85     private ExecutorService mExecutorService;
86     private ToggleButton mMirror;
87     private ToggleButton mCameraToggle;
88 
89     // Synthetic access
90     @SuppressWarnings("WeakerAccess")
91     PreviewView mPreviewView;
92     // Synthetic access
93     @SuppressWarnings("WeakerAccess")
94     OverlayView mOverlayView;
95 
96     // The following two variables should only be accessed from mExecutorService.
97     // Synthetic access
98     @SuppressWarnings("WeakerAccess")
99     @Nullable OutputTransform mImageProxyTransform;
100     // Synthetic access
101     @SuppressWarnings("WeakerAccess")
102     @Nullable RectF mBrightestTile;
103 
104     private FileTransformFactory mFileTransformFactoryWithoutExif;
105     private FileTransformFactory mFileTransformFactoryWithExif;
106 
107     private final ImageAnalysis.Analyzer mAnalyzer = new ImageAnalysis.Analyzer() {
108 
109         private final ImageProxyTransformFactory mImageProxyTransformFactory =
110                 new ImageProxyTransformFactory();
111 
112         @Override
113         @OptIn(markerClass = TransformExperimental.class)
114         public void analyze(@NonNull ImageProxy imageProxy) {
115             // Find the brightest tile to highlight.
116             mBrightestTile = findBrightestTile(imageProxy);
117             mImageProxyTransform =
118                     mImageProxyTransformFactory.getOutputTransform(imageProxy);
119             imageProxy.close();
120 
121             // Take a snapshot of the analyze result for thread safety.
122             final RectF brightestTile = new RectF(mBrightestTile);
123             final OutputTransform imageProxyTransform = mImageProxyTransform;
124 
125             // Calculate PreviewView transform on UI thread.
126             mOverlayView.post(() -> {
127                 // Calculate the transform.
128                 RectF brightestTileInPreviewView = getBrightestTileInPreviewView(
129                         imageProxyTransform, brightestTile);
130                 if (brightestTileInPreviewView == null) {
131                     // PreviewView transform info is not ready. No-op.
132                     return;
133                 }
134 
135                 // Draw the tile on top of PreviewView.
136                 mOverlayView.setTileRect(brightestTileInPreviewView);
137                 mOverlayView.postInvalidate();
138             });
139         }
140     };
141 
142     /**
143      * Gets the transform matrix based on exif orientation.
144      *
145      * <p> A forked version of {@code TransformUtil#getExifTransform} to make the test app
146      * self-contained.
147      */
getExifTransform(int exifOrientation, int width, int height)148     public static @NonNull Matrix getExifTransform(int exifOrientation, int width, int height) {
149         Matrix matrix = new Matrix();
150 
151         // Map the bitmap to a normalized space (-1, -1) - (1, 1) and perform transform in the
152         // normalized space.
153         RectF rect = new RectF(0, 0, width, height);
154         matrix.setRectToRect(rect, NORMALIZED_RECT, Matrix.ScaleToFit.FILL);
155 
156         // A flag that check if the image has been rotated 90/270.
157         boolean isWidthHeightSwapped = false;
158 
159         // Transform the normalized space based on exif orientation.
160         switch (exifOrientation) {
161             case android.media.ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
162                 matrix.postScale(-1f, 1f);
163                 break;
164             case android.media.ExifInterface.ORIENTATION_ROTATE_180:
165                 matrix.postRotate(180);
166                 break;
167             case android.media.ExifInterface.ORIENTATION_FLIP_VERTICAL:
168                 matrix.postScale(1f, -1f);
169                 break;
170             case android.media.ExifInterface.ORIENTATION_TRANSPOSE:
171                 // Flipped about top-left <--> bottom-right axis, it can also be represented by
172                 // flip horizontally and then rotate 270 degree clockwise.
173                 matrix.postScale(-1f, 1f);
174                 matrix.postRotate(270);
175                 isWidthHeightSwapped = true;
176                 break;
177             case android.media.ExifInterface.ORIENTATION_ROTATE_90:
178                 matrix.postRotate(90);
179                 isWidthHeightSwapped = true;
180                 break;
181             case android.media.ExifInterface.ORIENTATION_TRANSVERSE:
182                 // Flipped about top-right <--> bottom-left axis, it can also be
183                 // represented by flip horizontally and then rotate 90 degree clockwise.
184                 matrix.postScale(-1f, 1f);
185                 matrix.postRotate(90);
186                 isWidthHeightSwapped = true;
187                 break;
188             case android.media.ExifInterface.ORIENTATION_ROTATE_270:
189                 matrix.postRotate(270);
190                 isWidthHeightSwapped = true;
191                 break;
192             case android.media.ExifInterface.ORIENTATION_NORMAL:
193                 // Fall-through
194             case android.media.ExifInterface.ORIENTATION_UNDEFINED:
195                 // Fall-through
196             default:
197                 break;
198         }
199 
200         // Map the normalized space back to the bitmap coordinates.
201         RectF restoredRect = isWidthHeightSwapped ? new RectF(0, 0, height, width) : rect;
202         Matrix restore = new Matrix();
203         restore.setRectToRect(NORMALIZED_RECT, restoredRect, Matrix.ScaleToFit.FILL);
204         matrix.postConcat(restore);
205         return matrix;
206     }
207 
208     /**
209      * Finds the brightest tile in the given {@link ImageProxy}.
210      *
211      * <p> Divides the crop rect of the image into a 4x4 grid, and find the brightest tile
212      * among the 16 tiles.
213      *
214      * @return the box of the brightest tile.
215      */
216     // Synthetic access
217     @SuppressWarnings("WeakerAccess")
findBrightestTile(ImageProxy image)218     RectF findBrightestTile(ImageProxy image) {
219         // Divide the crop rect in to 4x4 tiles.
220         Rect cropRect = image.getCropRect();
221         int[][] tiles = new int[TILE_COUNT][TILE_COUNT];
222         int tileWidth = cropRect.width() / TILE_COUNT;
223         int tileHeight = cropRect.height() / TILE_COUNT;
224 
225         // Loop through the y plane and get the sum of the luminance for each tile.
226         byte[] bytes = new byte[image.getPlanes()[0].getBuffer().remaining()];
227         image.getPlanes()[0].getBuffer().get(bytes);
228         int tileX;
229         int tileY;
230         for (int x = 0; x < cropRect.width(); x++) {
231             for (int y = 0; y < cropRect.height(); y++) {
232                 tileX = Math.min(x / tileWidth, TILE_COUNT - 1);
233                 tileY = Math.min(y / tileHeight, TILE_COUNT - 1);
234                 tiles[tileX][tileY] +=
235                         bytes[(y + cropRect.top) * image.getWidth() + cropRect.left + x] & 0xFF;
236             }
237         }
238 
239         // Find the brightest tile among the 16 tiles.
240         float maxLuminance = 0;
241         int brightestTileX = 0;
242         int brightestTileY = 0;
243         for (int i = 0; i < TILE_COUNT; i++) {
244             for (int j = 0; j < TILE_COUNT; j++) {
245                 if (tiles[i][j] > maxLuminance) {
246                     maxLuminance = tiles[i][j];
247                     brightestTileX = i;
248                     brightestTileY = j;
249                 }
250             }
251         }
252 
253         // Return the rectangle of the tile.
254         return new RectF(brightestTileX * tileWidth + cropRect.left,
255                 brightestTileY * tileHeight + cropRect.top,
256                 (brightestTileX + 1) * tileWidth + cropRect.left,
257                 (brightestTileY + 1) * tileHeight + cropRect.top);
258     }
259 
260     @Override
261     @OptIn(markerClass = TransformExperimental.class)
onCreateView( @onNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState)262     public @NonNull View onCreateView(
263             @NonNull LayoutInflater inflater,
264             @Nullable ViewGroup container,
265             @Nullable Bundle savedInstanceState) {
266         mFileTransformFactoryWithoutExif = new FileTransformFactory();
267         mFileTransformFactoryWithExif = new FileTransformFactory();
268         mFileTransformFactoryWithExif.setUsingExifOrientation(true);
269         mExecutorService = Executors.newSingleThreadExecutor();
270         mCameraController = new LifecycleCameraController(requireContext());
271         mCameraController.bindToLifecycle(getViewLifecycleOwner());
272 
273         View view = inflater.inflate(R.layout.transform_view, container, false);
274 
275         mPreviewView = view.findViewById(R.id.preview_view);
276         // Set to compatible so the custom transform (e.g. mirroring) would work.
277         mPreviewView.setImplementationMode(PreviewView.ImplementationMode.COMPATIBLE);
278         mPreviewView.setController(mCameraController);
279 
280         mCameraController.setImageAnalysisAnalyzer(mExecutorService, mAnalyzer);
281 
282         mOverlayView = view.findViewById(R.id.overlay_view);
283         mMirror = view.findViewById(R.id.mirror_preview);
284         mMirror.setOnCheckedChangeListener((buttonView, isChecked) -> updateMirrorState());
285 
286         mCameraToggle = view.findViewById(R.id.toggle_camera);
287         mCameraToggle.setOnCheckedChangeListener(
288                 (buttonView, isChecked) -> updateCameraOrientation());
289 
290         view.findViewById(R.id.capture).setOnClickListener(
291                 v -> saveHighlightedFilePreservingExif());
292 
293         view.findViewById(R.id.capture_and_transform).setOnClickListener(
294                 v -> saveHighlightedUriWithoutExif());
295 
296         updateMirrorState();
297         updateCameraOrientation();
298         return view;
299     }
300 
301     // Synthetic access
302     @SuppressWarnings("WeakerAccess")
showToast(String message)303     void showToast(String message) {
304         requireActivity().runOnUiThread(
305                 () -> Toast.makeText(requireContext(), message, Toast.LENGTH_LONG).show());
306     }
307 
createDefaultPictureFolderIfNotExist()308     private void createDefaultPictureFolderIfNotExist() {
309         File pictureFolder = Environment.getExternalStoragePublicDirectory(
310                 Environment.DIRECTORY_PICTURES);
311         if (!pictureFolder.exists()) {
312             if (!pictureFolder.mkdir()) {
313                 Log.e(TAG, "Failed to create directory: " + pictureFolder);
314             }
315         }
316     }
317 
318     /**
319      * Takes a picture, applies the exif info, highlights the brightest tile and saves it to
320      * MediaStore without exif info.
321      */
saveHighlightedUriWithoutExif()322     private void saveHighlightedUriWithoutExif() {
323         // Take a picture and save to MediaStore
324         createDefaultPictureFolderIfNotExist();
325         ContentValues contentValues = new ContentValues();
326         contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
327         ImageCapture.OutputFileOptions outputFileOptions =
328                 new ImageCapture.OutputFileOptions.Builder(
329                         requireContext().getContentResolver(),
330                         MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
331                         contentValues).build();
332 
333         mCameraController.takePicture(outputFileOptions, mExecutorService,
334                 new ImageCapture.OnImageSavedCallback() {
335                     @Override
336                     public void onImageSaved(
337                             ImageCapture.@NonNull OutputFileResults outputFileResults) {
338                         if (mImageProxyTransform == null || mBrightestTile == null) {
339                             Logger.d(TAG, "ImageAnalysis result not ready.");
340                             return;
341                         }
342                         Uri uri = outputFileResults.getSavedUri();
343                         if (uri == null) {
344                             showToast("Saved URI should not be null.");
345                             return;
346                         }
347                         try {
348                             RectF tileInUri = getBrightestTileInUriWithExif(
349                                     uri,
350                                     mImageProxyTransform,
351                                     mBrightestTile);
352                             Bitmap bitmap = loadBitmapWithExifApplied(uri);
353                             drawRectOnBitmap(bitmap, tileInUri);
354                             saveBitmapToUri(bitmap, uri);
355                             showToast("Image saved.");
356                         } catch (IOException e) {
357                             showToast("Failed to draw on file. " + e);
358                         }
359                     }
360 
361                     @Override
362                     public void onError(@NonNull ImageCaptureException exception) {
363                         showToast("Failed to capture image. " + exception);
364                     }
365                 });
366     }
367 
368     /**
369      * Loads {@link Bitmap} from the given {@link Uri} and applies exif info to the {@link Bitmap}.
370      */
371     // Synthetic access
372     @SuppressWarnings("WeakerAccess")
loadBitmapWithExifApplied(Uri uri)373     @NonNull Bitmap loadBitmapWithExifApplied(Uri uri) throws IOException {
374         // Loads bitmap.
375         BitmapFactory.Options options = new BitmapFactory.Options();
376         options.inMutable = true;
377         Bitmap original;
378         try (InputStream inputStream = requireContext().getContentResolver().openInputStream(uri)) {
379             original = BitmapFactory.decodeStream(inputStream, /*outPadding*/ null, options);
380         }
381 
382         // Reads exif orientation.
383         int exifOrientation;
384         try (InputStream inputStream = requireContext().getContentResolver().openInputStream(uri)) {
385             ExifInterface exifInterface = new ExifInterface(inputStream);
386             exifOrientation = exifInterface.getAttributeInt(
387                     ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
388         }
389         Matrix matrix = getExifTransform(exifOrientation, original.getWidth(),
390                 original.getHeight());
391 
392         // Calculate the bitmap size with exif applied.
393         float[] sizeVector = new float[]{original.getWidth(), original.getHeight()};
394         matrix.mapVectors(sizeVector);
395 
396         // Create a new bitmap with exif applied.
397         Bitmap bitmapWithExif = Bitmap.createBitmap(
398                 round(abs(sizeVector[0])),
399                 round(abs(sizeVector[1])),
400                 Bitmap.Config.ARGB_8888);
401         Canvas canvas = new Canvas(bitmapWithExif);
402         Paint paint = new Paint();
403         paint.setAntiAlias(true);
404         canvas.drawBitmap(original, matrix, paint);
405         return bitmapWithExif;
406     }
407 
408     // Synthetic access
409     @SuppressWarnings("WeakerAccess")
saveBitmapToUri(@onNull Bitmap bitmap, @NonNull Uri uri)410     void saveBitmapToUri(@NonNull Bitmap bitmap, @NonNull Uri uri) throws IOException {
411         try (OutputStream outputStream =
412                      requireContext().getContentResolver().openOutputStream(uri)) {
413             bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
414         }
415     }
416 
417     /**
418      * Takes a picture, highlights the brightest tile and saves it to MediaStore preserving Exif
419      * info.
420      */
saveHighlightedFilePreservingExif()421     private void saveHighlightedFilePreservingExif() {
422         // Create an internal temp file for drawing an overlay.
423         File tempFile;
424         try {
425             tempFile = File.createTempFile("camerax-view-test_transform-test", ".jpg");
426             tempFile.deleteOnExit();
427         } catch (IOException e) {
428             showToast("Failed to create temp file. " + e);
429             return;
430         }
431 
432         // Take a picture.
433         ImageCapture.OutputFileOptions outputFileOptions =
434                 new ImageCapture.OutputFileOptions.Builder(tempFile).build();
435         mCameraController.takePicture(outputFileOptions, mExecutorService,
436                 new ImageCapture.OnImageSavedCallback() {
437                     @Override
438                     public void onImageSaved(
439                             ImageCapture.@NonNull OutputFileResults outputFileResults) {
440                         if (mImageProxyTransform == null || mBrightestTile == null) {
441                             Logger.d(TAG, "ImageAnalysis result not ready.");
442                             return;
443                         }
444                         try {
445                             RectF tileInFile = getBrightestTileInFileWithoutExif(
446                                     tempFile,
447                                     mImageProxyTransform,
448                                     mBrightestTile);
449                             // Load a mutable Bitmap.
450                             BitmapFactory.Options options = new BitmapFactory.Options();
451                             options.inMutable = true;
452                             Bitmap bitmap = BitmapFactory.decodeFile(tempFile.getAbsolutePath(),
453                                     options);
454                             drawRectOnBitmap(bitmap, tileInFile);
455                             saveBitmapToFilePreservingExif(tempFile, bitmap);
456                             insertFileToMediaStore(tempFile);
457                             showToast("Image saved.");
458                         } catch (IOException e) {
459                             showToast("Failed to draw on file. " + e);
460                         }
461                     }
462 
463                     @Override
464                     public void onError(@NonNull ImageCaptureException exception) {
465                         showToast("Failed to capture image. " + exception);
466                     }
467                 });
468     }
469 
updateMirrorState()470     private void updateMirrorState() {
471         if (mMirror.isChecked()) {
472             mPreviewView.setScaleX(-1);
473         } else {
474             mPreviewView.setScaleX(1);
475         }
476     }
477 
updateCameraOrientation()478     private void updateCameraOrientation() {
479         if (mCameraToggle.isChecked()) {
480             mCameraController.setCameraSelector(CameraSelector.DEFAULT_BACK_CAMERA);
481         } else {
482             mCameraController.setCameraSelector(CameraSelector.DEFAULT_FRONT_CAMERA);
483         }
484     }
485 
486     /**
487      * Saves the Bitmap to the given File while preserving the File's exif orientation.
488      */
saveBitmapToFilePreservingExif(@onNull File originalFile, @NonNull Bitmap bitmap)489     void saveBitmapToFilePreservingExif(@NonNull File originalFile, @NonNull Bitmap bitmap)
490             throws IOException {
491         ExifInterface exifInterface = new ExifInterface(originalFile);
492         int orientation = exifInterface.getAttributeInt(
493                 ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED);
494         try (OutputStream outputStream = new FileOutputStream(originalFile)) {
495             bitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream);
496         }
497         exifInterface = new ExifInterface(originalFile);
498         exifInterface.setAttribute(ExifInterface.TAG_ORIENTATION, String.valueOf(orientation));
499         exifInterface.saveAttributes();
500     }
501 
insertFileToMediaStore(@onNull File file)502     void insertFileToMediaStore(@NonNull File file) throws IOException {
503         ContentValues contentValues = new ContentValues();
504         contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg");
505         ContentResolver contentResolver = requireContext().getContentResolver();
506         Uri uri = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
507                 contentValues);
508         try (OutputStream outputStream = contentResolver.openOutputStream(uri)) {
509             try (InputStream inputStream = new FileInputStream(file)) {
510                 byte[] buf = new byte[1024];
511                 int len;
512                 while ((len = inputStream.read(buf)) > 0) {
513                     outputStream.write(buf, 0, len);
514                 }
515             }
516         }
517     }
518 
519     // Synthetic access
520     @SuppressWarnings("WeakerAccess")
drawRectOnBitmap(@onNull Bitmap bitmap, @NonNull RectF rectF)521     void drawRectOnBitmap(@NonNull Bitmap bitmap, @NonNull RectF rectF) {
522         Paint paint = new Paint();
523         paint.setStyle(Paint.Style.STROKE);
524 
525         Canvas canvas = new Canvas(bitmap);
526 
527         // Draw a rect with black stroke and white glow so it's always visible regardless of
528         // background.
529         paint.setStrokeWidth(20);
530         paint.setColor(Color.WHITE);
531         canvas.drawRect(rectF, paint);
532 
533         paint.setStrokeWidth(10);
534         paint.setColor(Color.BLACK);
535         canvas.drawRect(rectF, paint);
536     }
537 
538     // Synthetic access
539     @SuppressWarnings("WeakerAccess")
540     @OptIn(markerClass = TransformExperimental.class)
getBrightestTileInFileWithoutExif(@onNull File file, @NonNull OutputTransform imageProxyTransform, @NonNull RectF imageProxyTile)541     @NonNull RectF getBrightestTileInFileWithoutExif(@NonNull File file,
542             @NonNull OutputTransform imageProxyTransform,
543             @NonNull RectF imageProxyTile) throws IOException {
544         return getBrightestTile(
545                 imageProxyTransform,
546                 mFileTransformFactoryWithoutExif.getOutputTransform(file),
547                 imageProxyTile);
548     }
549 
550     // Synthetic access
551     @SuppressWarnings("WeakerAccess")
552     @OptIn(markerClass = TransformExperimental.class)
getBrightestTileInUriWithExif(@onNull Uri uri, @NonNull OutputTransform imageProxyTransform, @NonNull RectF imageProxyTile)553     @NonNull RectF getBrightestTileInUriWithExif(@NonNull Uri uri,
554             @NonNull OutputTransform imageProxyTransform,
555             @NonNull RectF imageProxyTile) throws IOException {
556         OutputTransform uriTransform = mFileTransformFactoryWithExif.getOutputTransform(
557                 requireContext().getContentResolver(), uri);
558         return getBrightestTile(imageProxyTransform, uriTransform, imageProxyTile);
559     }
560 
561     // Synthetic access
562     @SuppressWarnings("WeakerAccess")
563     @OptIn(markerClass = TransformExperimental.class)
564     @MainThread
getBrightestTileInPreviewView(@onNull OutputTransform imageProxyTransform, @NonNull RectF imageProxyTile)565     @Nullable RectF getBrightestTileInPreviewView(@NonNull OutputTransform imageProxyTransform,
566             @NonNull RectF imageProxyTile) {
567         OutputTransform previewViewTransform = mPreviewView.getOutputTransform();
568         if (previewViewTransform == null) {
569             // PreviewView transform info is not ready. No-op.
570             return null;
571         }
572         return getBrightestTile(imageProxyTransform, previewViewTransform, imageProxyTile);
573     }
574 
575     @OptIn(markerClass = TransformExperimental.class)
getBrightestTile(@onNull OutputTransform source, @NonNull OutputTransform target, @NonNull RectF imageProxyTile)576     private @NonNull RectF getBrightestTile(@NonNull OutputTransform source,
577             @NonNull OutputTransform target,
578             @NonNull RectF imageProxyTile) {
579         CoordinateTransform transform = new CoordinateTransform(source, target);
580         Matrix matrix = new Matrix();
581         transform.transform(matrix);
582         matrix.mapRect(imageProxyTile);
583         return imageProxyTile;
584     }
585 
586     @Override
onDestroyView()587     public void onDestroyView() {
588         super.onDestroyView();
589         if (mExecutorService != null) {
590             mExecutorService.shutdown();
591         }
592     }
593 }
594