TakePhotoActivity.java 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. /*
  2. * Nextcloud Talk - Android Client
  3. *
  4. * SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
  5. * SPDX-FileCopyrightText: 2021 Stefan Niedermann <info@niedermann.it>
  6. * SPDX-License-Identifier: GPL-3.0-or-later
  7. */
  8. package com.nextcloud.talk.activities;
  9. import android.content.Context;
  10. import android.content.Intent;
  11. import android.graphics.Bitmap;
  12. import android.graphics.BitmapFactory;
  13. import android.hardware.camera2.CameraMetadata;
  14. import android.hardware.camera2.CaptureRequest;
  15. import android.net.Uri;
  16. import android.os.Bundle;
  17. import android.util.DisplayMetrics;
  18. import android.util.Log;
  19. import android.util.Size;
  20. import android.view.OrientationEventListener;
  21. import android.view.ScaleGestureDetector;
  22. import android.view.Surface;
  23. import android.view.View;
  24. import com.google.android.material.snackbar.Snackbar;
  25. import com.google.common.util.concurrent.ListenableFuture;
  26. import com.nextcloud.talk.R;
  27. import com.nextcloud.talk.application.NextcloudTalkApplication;
  28. import com.nextcloud.talk.databinding.ActivityTakePictureBinding;
  29. import com.nextcloud.talk.models.TakePictureViewModel;
  30. import com.nextcloud.talk.ui.theme.ViewThemeUtils;
  31. import com.nextcloud.talk.utils.BitmapShrinker;
  32. import com.nextcloud.talk.utils.FileUtils;
  33. import java.io.File;
  34. import java.text.SimpleDateFormat;
  35. import java.util.Date;
  36. import java.util.Locale;
  37. import java.util.concurrent.ExecutionException;
  38. import javax.inject.Inject;
  39. import androidx.activity.OnBackPressedCallback;
  40. import androidx.annotation.NonNull;
  41. import androidx.annotation.Nullable;
  42. import androidx.annotation.OptIn;
  43. import androidx.appcompat.app.AppCompatActivity;
  44. import androidx.camera.camera2.interop.Camera2Interop;
  45. import androidx.camera.core.AspectRatio;
  46. import androidx.camera.core.Camera;
  47. import androidx.camera.core.ImageCapture;
  48. import androidx.camera.core.ImageCaptureException;
  49. import androidx.camera.core.Preview;
  50. import androidx.camera.lifecycle.ProcessCameraProvider;
  51. import androidx.core.content.ContextCompat;
  52. import androidx.exifinterface.media.ExifInterface;
  53. import androidx.lifecycle.ViewModelProvider;
  54. import autodagger.AutoInjector;
  55. import static com.nextcloud.talk.utils.Mimetype.IMAGE_JPEG;
  56. @AutoInjector(NextcloudTalkApplication.class)
  57. public class TakePhotoActivity extends AppCompatActivity {
  58. private static final String TAG = TakePhotoActivity.class.getSimpleName();
  59. private static final float MAX_SCALE = 6.0f;
  60. private static final float MEDIUM_SCALE = 2.45f;
  61. private ActivityTakePictureBinding binding;
  62. private TakePictureViewModel viewModel;
  63. private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
  64. private OrientationEventListener orientationEventListener;
  65. private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss", Locale.ROOT);
  66. private Camera camera;
  67. @Inject
  68. ViewThemeUtils viewThemeUtils;
  69. private OnBackPressedCallback onBackPressedCallback = new OnBackPressedCallback(true) {
  70. @Override
  71. public void handleOnBackPressed() {
  72. Uri uri = (Uri) binding.photoPreview.getTag();
  73. if (uri != null) {
  74. File photoFile = new File(uri.getPath());
  75. if (!photoFile.delete()) {
  76. Log.w(TAG, "Error deleting temp camera image");
  77. }
  78. binding.photoPreview.setTag(null);
  79. }
  80. finish();
  81. }
  82. };
  83. @Override
  84. protected void onCreate(@Nullable Bundle savedInstanceState) {
  85. super.onCreate(savedInstanceState);
  86. NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
  87. binding = ActivityTakePictureBinding.inflate(getLayoutInflater());
  88. viewModel = new ViewModelProvider(this).get(TakePictureViewModel.class);
  89. setContentView(binding.getRoot());
  90. viewThemeUtils.material.themeFAB(binding.takePhoto);
  91. viewThemeUtils.material.colorMaterialButtonPrimaryFilled(binding.send);
  92. cameraProviderFuture = ProcessCameraProvider.getInstance(this);
  93. cameraProviderFuture.addListener(() -> {
  94. try {
  95. final ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
  96. camera = cameraProvider.bindToLifecycle(
  97. this,
  98. viewModel.getCameraSelector(),
  99. getImageCapture(
  100. viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()),
  101. getPreview(viewModel.isCropEnabled().getValue()));
  102. viewModel.getTorchToggleButtonImageResource()
  103. .observe(
  104. this,
  105. res -> binding.toggleTorch.setIcon(ContextCompat.getDrawable(this, res)));
  106. viewModel.isTorchEnabled()
  107. .observe(
  108. this,
  109. enabled -> camera.getCameraControl().enableTorch(viewModel.isTorchEnabled().getValue()));
  110. binding.toggleTorch.setOnClickListener((v) -> viewModel.toggleTorchEnabled());
  111. viewModel.getCropToggleButtonImageResource()
  112. .observe(
  113. this,
  114. res -> binding.toggleCrop.setIcon(ContextCompat.getDrawable(this, res)));
  115. viewModel.isCropEnabled()
  116. .observe(
  117. this,
  118. enabled -> {
  119. cameraProvider.unbindAll();
  120. camera = cameraProvider.bindToLifecycle(
  121. this,
  122. viewModel.getCameraSelector(),
  123. getImageCapture(
  124. viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()),
  125. getPreview(viewModel.isCropEnabled().getValue()));
  126. camera.getCameraControl().enableTorch(viewModel.isTorchEnabled().getValue());
  127. });
  128. binding.toggleCrop.setOnClickListener((v) -> viewModel.toggleCropEnabled());
  129. viewModel.getLowResolutionToggleButtonImageResource()
  130. .observe(
  131. this,
  132. res -> binding.toggleLowres.setIcon(ContextCompat.getDrawable(this, res)));
  133. viewModel.isLowResolutionEnabled()
  134. .observe(
  135. this,
  136. enabled -> {
  137. cameraProvider.unbindAll();
  138. camera = cameraProvider.bindToLifecycle(
  139. this,
  140. viewModel.getCameraSelector(),
  141. getImageCapture(
  142. viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()),
  143. getPreview(viewModel.isCropEnabled().getValue()));
  144. camera.getCameraControl().enableTorch(viewModel.isTorchEnabled().getValue());
  145. });
  146. binding.toggleLowres.setOnClickListener((v) -> viewModel.toggleLowResolutionEnabled());
  147. binding.switchCamera.setOnClickListener((v) -> {
  148. viewModel.toggleCameraSelector();
  149. cameraProvider.unbindAll();
  150. camera = cameraProvider.bindToLifecycle(
  151. this,
  152. viewModel.getCameraSelector(),
  153. getImageCapture(
  154. viewModel.isCropEnabled().getValue(), viewModel.isLowResolutionEnabled().getValue()),
  155. getPreview(viewModel.isCropEnabled().getValue()));
  156. });
  157. binding.retake.setOnClickListener((v) -> {
  158. Uri uri = (Uri) binding.photoPreview.getTag();
  159. File photoFile = new File(uri.getPath());
  160. if (!photoFile.delete()) {
  161. Log.w(TAG, "Error deleting temp camera image");
  162. }
  163. binding.takePhoto.setEnabled(true);
  164. binding.photoPreview.setTag(null);
  165. showCameraElements();
  166. });
  167. binding.send.setOnClickListener((v) -> {
  168. Uri uri = (Uri) binding.photoPreview.getTag();
  169. setResult(RESULT_OK, new Intent().setDataAndType(uri, IMAGE_JPEG));
  170. binding.photoPreview.setTag(null);
  171. finish();
  172. });
  173. ScaleGestureDetector mDetector =
  174. new ScaleGestureDetector(this, new ScaleGestureDetector.SimpleOnScaleGestureListener(){
  175. @Override
  176. public boolean onScale(ScaleGestureDetector detector){
  177. float ratio = camera.getCameraInfo().getZoomState().getValue().getZoomRatio();
  178. float delta = detector.getScaleFactor();
  179. camera.getCameraControl().setZoomRatio(ratio * delta);
  180. return true;
  181. }
  182. });
  183. binding.preview.setOnTouchListener((v, event) -> {
  184. v.performClick();
  185. mDetector.onTouchEvent(event);
  186. return true;
  187. });
  188. // Enable enlarging the image more than default 3x maximumScale.
  189. // Medium scale adapted to make double-tap behaviour more consistent.
  190. binding.photoPreview.setMaximumScale(MAX_SCALE);
  191. binding.photoPreview.setMediumScale(MEDIUM_SCALE);
  192. } catch (IllegalArgumentException | ExecutionException | InterruptedException e) {
  193. Log.e(TAG, "Error taking picture", e);
  194. Snackbar.make(binding.getRoot(), e.getMessage(), Snackbar.LENGTH_LONG).show();
  195. finish();
  196. }
  197. }, ContextCompat.getMainExecutor(this));
  198. getOnBackPressedDispatcher().addCallback(this, onBackPressedCallback);
  199. }
  200. private void showCameraElements() {
  201. binding.send.setVisibility(View.GONE);
  202. binding.retake.setVisibility(View.GONE);
  203. binding.photoPreview.setVisibility(View.INVISIBLE);
  204. binding.preview.setVisibility(View.VISIBLE);
  205. binding.takePhoto.setVisibility(View.VISIBLE);
  206. binding.switchCamera.setVisibility(View.VISIBLE);
  207. binding.toggleTorch.setVisibility(View.VISIBLE);
  208. binding.toggleCrop.setVisibility(View.VISIBLE);
  209. binding.toggleLowres.setVisibility(View.VISIBLE);
  210. }
  211. private void showPictureProcessingElements() {
  212. binding.preview.setVisibility(View.INVISIBLE);
  213. binding.takePhoto.setVisibility(View.GONE);
  214. binding.switchCamera.setVisibility(View.GONE);
  215. binding.toggleTorch.setVisibility(View.GONE);
  216. binding.toggleCrop.setVisibility(View.GONE);
  217. binding.toggleLowres.setVisibility(View.GONE);
  218. binding.send.setVisibility(View.VISIBLE);
  219. binding.retake.setVisibility(View.VISIBLE);
  220. binding.photoPreview.setVisibility(View.VISIBLE);
  221. }
  222. private ImageCapture getImageCapture(Boolean crop, Boolean lowres) {
  223. final ImageCapture imageCapture;
  224. if (lowres) imageCapture = new ImageCapture.Builder()
  225. .setTargetResolution(new Size(crop ? 1080 : 1440, 1920)).build();
  226. else imageCapture = new ImageCapture.Builder()
  227. .setTargetAspectRatio(crop ? AspectRatio.RATIO_16_9 : AspectRatio.RATIO_4_3).build();
  228. orientationEventListener = new OrientationEventListener(this) {
  229. @Override
  230. public void onOrientationChanged(int orientation) {
  231. int rotation;
  232. // Monitors orientation values to determine the target rotation value
  233. if (orientation >= 45 && orientation < 135) {
  234. rotation = Surface.ROTATION_270;
  235. } else if (orientation >= 135 && orientation < 225) {
  236. rotation = Surface.ROTATION_180;
  237. } else if (orientation >= 225 && orientation < 315) {
  238. rotation = Surface.ROTATION_90;
  239. } else {
  240. rotation = Surface.ROTATION_0;
  241. }
  242. imageCapture.setTargetRotation(rotation);
  243. }
  244. };
  245. orientationEventListener.enable();
  246. binding.takePhoto.setOnClickListener((v) -> {
  247. binding.takePhoto.setEnabled(false);
  248. final String photoFileName = dateFormat.format(new Date()) + ".jpg";
  249. try {
  250. final File photoFile = FileUtils.getTempCacheFile(this, "photos/" + photoFileName);
  251. final ImageCapture.OutputFileOptions options =
  252. new ImageCapture.OutputFileOptions.Builder(photoFile).build();
  253. imageCapture.takePicture(
  254. options,
  255. ContextCompat.getMainExecutor(this),
  256. new ImageCapture.OnImageSavedCallback() {
  257. @Override
  258. public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
  259. setPreviewImage(photoFile);
  260. showPictureProcessingElements();
  261. }
  262. @Override
  263. public void onError(@NonNull ImageCaptureException e) {
  264. Log.e(TAG, "Error", e);
  265. if (!photoFile.delete()) {
  266. Log.w(TAG, "Deleting picture failed");
  267. }
  268. binding.takePhoto.setEnabled(true);
  269. }
  270. });
  271. } catch (Exception e) {
  272. Log.e(TAG, "error while taking picture", e);
  273. Snackbar.make(binding.getRoot(), R.string.take_photo_error_deleting_picture, Snackbar.LENGTH_SHORT).show();
  274. }
  275. });
  276. return imageCapture;
  277. }
  278. private void setPreviewImage(File photoFile) {
  279. final Uri savedUri = Uri.fromFile(photoFile);
  280. BitmapFactory.Options options = new BitmapFactory.Options();
  281. options.inPreferredConfig = Bitmap.Config.ARGB_8888;
  282. DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
  283. int doubleScreenWidth = displayMetrics.widthPixels * 2;
  284. int doubleScreenHeight = displayMetrics.heightPixels * 2;
  285. Bitmap bitmap = BitmapShrinker.shrinkBitmap(photoFile.getAbsolutePath(),
  286. doubleScreenWidth,
  287. doubleScreenHeight);
  288. binding.photoPreview.setImageBitmap(bitmap);
  289. binding.photoPreview.setTag(savedUri);
  290. viewModel.disableTorchIfEnabled();
  291. }
  292. public int getImageOrientation(File imageFile) {
  293. int rotate = 0;
  294. try {
  295. ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
  296. int orientation = exif.getAttributeInt(
  297. ExifInterface.TAG_ORIENTATION,
  298. ExifInterface.ORIENTATION_NORMAL);
  299. switch (orientation) {
  300. case ExifInterface.ORIENTATION_ROTATE_270:
  301. rotate = 270;
  302. break;
  303. case ExifInterface.ORIENTATION_ROTATE_180:
  304. rotate = 180;
  305. break;
  306. case ExifInterface.ORIENTATION_ROTATE_90:
  307. rotate = 90;
  308. break;
  309. default:
  310. rotate = 0;
  311. break;
  312. }
  313. Log.i(TAG, "ImageOrientation - Exif orientation: " + orientation + " - " + "Rotate value: " + rotate);
  314. } catch (Exception e) {
  315. Log.w(TAG, "Error calculation rotation value");
  316. }
  317. return rotate;
  318. }
  319. @OptIn(markerClass = androidx.camera.camera2.interop.ExperimentalCamera2Interop.class)
  320. private Preview getPreview(boolean crop) {
  321. Preview.Builder previewBuilder = new Preview.Builder()
  322. .setTargetAspectRatio(crop ? AspectRatio.RATIO_16_9 : AspectRatio.RATIO_4_3);
  323. new Camera2Interop.Extender<>(previewBuilder)
  324. .setCaptureRequestOption(CaptureRequest.CONTROL_VIDEO_STABILIZATION_MODE,
  325. CameraMetadata.CONTROL_VIDEO_STABILIZATION_MODE_OFF
  326. );
  327. Preview preview = previewBuilder.build();
  328. preview.setSurfaceProvider(binding.preview.getSurfaceProvider());
  329. return preview;
  330. }
  331. @Override
  332. protected void onPause() {
  333. if (this.orientationEventListener != null) {
  334. this.orientationEventListener.disable();
  335. }
  336. super.onPause();
  337. }
  338. @Override
  339. protected void onResume() {
  340. super.onResume();
  341. if (this.orientationEventListener != null) {
  342. this.orientationEventListener.enable();
  343. }
  344. }
  345. @Override
  346. public void onSaveInstanceState(Bundle savedInstanceState) {
  347. if (binding.photoPreview.getTag() != null) {
  348. savedInstanceState.putString("Uri", ((Uri) binding.photoPreview.getTag()).getPath());
  349. }
  350. super.onSaveInstanceState(savedInstanceState);
  351. }
  352. @Override
  353. public void onRestoreInstanceState(Bundle savedInstanceState) {
  354. super.onRestoreInstanceState(savedInstanceState);
  355. String uri = savedInstanceState.getString("Uri", null);
  356. if (uri != null) {
  357. File photoFile = new File(uri);
  358. setPreviewImage(photoFile);
  359. showPictureProcessingElements();
  360. }
  361. }
  362. public static Intent createIntent(@NonNull Context context) {
  363. return new Intent(context, TakePhotoActivity.class).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
  364. }
  365. }