TakePhotoActivity.java 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. /*
  2. * Nextcloud Talk application
  3. *
  4. * @author Andy Scherzinger
  5. * @author Stefan Niedermann
  6. * Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
  7. * Copyright (C) 2021 Stefan Niedermann <info@niedermann.it>
  8. *
  9. * This program is free software: you can redistribute it and/or modify
  10. * it under the terms of the GNU General Public License as published by
  11. * the Free Software Foundation, either version 3 of the License, or
  12. * at your option) any later version.
  13. *
  14. * This program is distributed in the hope that it will be useful,
  15. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  16. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  17. * GNU General Public License for more details.
  18. *
  19. * You should have received a copy of the GNU General Public License
  20. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  21. */
  22. package com.nextcloud.talk.activities;
  23. import android.content.Context;
  24. import android.content.Intent;
  25. import android.graphics.Bitmap;
  26. import android.graphics.BitmapFactory;
  27. import android.net.Uri;
  28. import android.os.Bundle;
  29. import android.util.DisplayMetrics;
  30. import android.util.Log;
  31. import android.util.Size;
  32. import android.view.OrientationEventListener;
  33. import android.view.Surface;
  34. import android.view.View;
  35. import android.widget.Toast;
  36. import com.google.common.util.concurrent.ListenableFuture;
  37. import com.nextcloud.talk.R;
  38. import com.nextcloud.talk.databinding.ActivityTakePictureBinding;
  39. import com.nextcloud.talk.models.TakePictureViewModel;
  40. import com.nextcloud.talk.utils.BitmapShrinker;
  41. import com.nextcloud.talk.utils.FileUtils;
  42. import java.io.File;
  43. import java.text.SimpleDateFormat;
  44. import java.util.Date;
  45. import java.util.Locale;
  46. import java.util.concurrent.ExecutionException;
  47. import androidx.annotation.NonNull;
  48. import androidx.annotation.Nullable;
  49. import androidx.appcompat.app.AppCompatActivity;
  50. import androidx.camera.core.Camera;
  51. import androidx.camera.core.ImageCapture;
  52. import androidx.camera.core.ImageCaptureException;
  53. import androidx.camera.core.Preview;
  54. import androidx.camera.lifecycle.ProcessCameraProvider;
  55. import androidx.core.content.ContextCompat;
  56. import androidx.exifinterface.media.ExifInterface;
  57. import androidx.lifecycle.ViewModelProvider;
  58. public class TakePhotoActivity extends AppCompatActivity {
  59. private static final String TAG = TakePhotoActivity.class.getSimpleName();
  60. private static final float MAX_SCALE = 6.0f;
  61. private static final float MEDIUM_SCALE = 2.45f;
  62. private ActivityTakePictureBinding binding;
  63. private TakePictureViewModel viewModel;
  64. private ListenableFuture<ProcessCameraProvider> cameraProviderFuture;
  65. private OrientationEventListener orientationEventListener;
  66. private final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss", Locale.ROOT);
  67. @Override
  68. protected void onCreate(@Nullable Bundle savedInstanceState) {
  69. super.onCreate(savedInstanceState);
  70. binding = ActivityTakePictureBinding.inflate(getLayoutInflater());
  71. viewModel = new ViewModelProvider(this).get(TakePictureViewModel.class);
  72. setContentView(binding.getRoot());
  73. cameraProviderFuture = ProcessCameraProvider.getInstance(this);
  74. cameraProviderFuture.addListener(() -> {
  75. try {
  76. final ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
  77. final Preview preview = getPreview();
  78. final ImageCapture imageCapture = getImageCapture();
  79. final Camera camera = cameraProvider.bindToLifecycle(
  80. this,
  81. viewModel.getCameraSelector(),
  82. imageCapture,
  83. preview);
  84. viewModel.getTorchToggleButtonImageResource()
  85. .observe(
  86. this,
  87. res -> binding.toggleTorch.setIcon(ContextCompat.getDrawable(this, res)));
  88. viewModel.isTorchEnabled()
  89. .observe(
  90. this,
  91. enabled -> camera.getCameraControl().enableTorch(enabled));
  92. binding.toggleTorch.setOnClickListener((v) -> viewModel.toggleTorchEnabled());
  93. binding.switchCamera.setOnClickListener((v) -> {
  94. viewModel.toggleCameraSelector();
  95. cameraProvider.unbindAll();
  96. cameraProvider.bindToLifecycle(
  97. this,
  98. viewModel.getCameraSelector(),
  99. imageCapture,
  100. preview);
  101. });
  102. binding.retake.setOnClickListener((v) -> {
  103. Uri uri = (Uri) binding.photoPreview.getTag();
  104. File photoFile = new File(uri.getPath());
  105. if (!photoFile.delete()) {
  106. Log.w(TAG, "Error deleting temp camera image");
  107. }
  108. binding.takePhoto.setEnabled(true);
  109. binding.photoPreview.setTag(null);
  110. showCameraElements();
  111. });
  112. binding.send.setOnClickListener((v) -> {
  113. Uri uri = (Uri) binding.photoPreview.getTag();
  114. setResult(RESULT_OK, new Intent().setDataAndType(uri, "image/jpeg"));
  115. binding.photoPreview.setTag(null);
  116. finish();
  117. });
  118. // Enable enlarging the image more than default 3x maximumScale.
  119. // Medium scale adapted to make double-tap behaviour more consistent.
  120. binding.photoPreview.setMaximumScale(MAX_SCALE);
  121. binding.photoPreview.setMediumScale(MEDIUM_SCALE);
  122. } catch (IllegalArgumentException | ExecutionException | InterruptedException e) {
  123. Log.e(TAG, "Error taking picture", e);
  124. Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
  125. finish();
  126. }
  127. }, ContextCompat.getMainExecutor(this));
  128. }
  129. @Override
  130. public void onBackPressed() {
  131. Uri uri = (Uri) binding.photoPreview.getTag();
  132. if (uri != null) {
  133. File photoFile = new File(uri.getPath());
  134. if (!photoFile.delete()) {
  135. Log.w(TAG, "Error deleting temp camera image");
  136. }
  137. binding.photoPreview.setTag(null);
  138. }
  139. super.onBackPressed();
  140. }
  141. private void showCameraElements() {
  142. binding.send.setVisibility(View.GONE);
  143. binding.retake.setVisibility(View.GONE);
  144. binding.photoPreview.setVisibility(View.INVISIBLE);
  145. binding.preview.setVisibility(View.VISIBLE);
  146. binding.takePhoto.setVisibility(View.VISIBLE);
  147. binding.switchCamera.setVisibility(View.VISIBLE);
  148. binding.toggleTorch.setVisibility(View.VISIBLE);
  149. }
  150. private void showPictureProcessingElements() {
  151. binding.preview.setVisibility(View.INVISIBLE);
  152. binding.takePhoto.setVisibility(View.GONE);
  153. binding.switchCamera.setVisibility(View.GONE);
  154. binding.toggleTorch.setVisibility(View.GONE);
  155. binding.send.setVisibility(View.VISIBLE);
  156. binding.retake.setVisibility(View.VISIBLE);
  157. binding.photoPreview.setVisibility(View.VISIBLE);
  158. }
  159. private ImageCapture getImageCapture() {
  160. final ImageCapture imageCapture = new ImageCapture.Builder().setTargetResolution(new Size(1080, 1920)).build();
  161. orientationEventListener = new OrientationEventListener(this) {
  162. @Override
  163. public void onOrientationChanged(int orientation) {
  164. int rotation;
  165. // Monitors orientation values to determine the target rotation value
  166. if (orientation >= 45 && orientation < 135) {
  167. rotation = Surface.ROTATION_270;
  168. } else if (orientation >= 135 && orientation < 225) {
  169. rotation = Surface.ROTATION_180;
  170. } else if (orientation >= 225 && orientation < 315) {
  171. rotation = Surface.ROTATION_90;
  172. } else {
  173. rotation = Surface.ROTATION_0;
  174. }
  175. imageCapture.setTargetRotation(rotation);
  176. }
  177. };
  178. orientationEventListener.enable();
  179. binding.takePhoto.setOnClickListener((v) -> {
  180. binding.takePhoto.setEnabled(false);
  181. final String photoFileName = dateFormat.format(new Date()) + ".jpg";
  182. try {
  183. final File photoFile = FileUtils.getTempCacheFile(this, "photos/" + photoFileName);
  184. final ImageCapture.OutputFileOptions options =
  185. new ImageCapture.OutputFileOptions.Builder(photoFile).build();
  186. imageCapture.takePicture(
  187. options,
  188. ContextCompat.getMainExecutor(this),
  189. new ImageCapture.OnImageSavedCallback() {
  190. @Override
  191. public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
  192. setPreviewImage(photoFile);
  193. showPictureProcessingElements();
  194. }
  195. @Override
  196. public void onError(@NonNull ImageCaptureException e) {
  197. Log.e(TAG, "Error", e);
  198. if (!photoFile.delete()) {
  199. Log.w(TAG, "Deleting picture failed");
  200. }
  201. binding.takePhoto.setEnabled(true);
  202. }
  203. });
  204. } catch (Exception e) {
  205. Toast.makeText(this, R.string.take_photo_error_deleting_picture, Toast.LENGTH_SHORT).show();
  206. }
  207. });
  208. return imageCapture;
  209. }
  210. private void setPreviewImage(File photoFile) {
  211. final Uri savedUri = Uri.fromFile(photoFile);
  212. BitmapFactory.Options options = new BitmapFactory.Options();
  213. options.inPreferredConfig = Bitmap.Config.ARGB_8888;
  214. DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
  215. int doubleScreenWidth = displayMetrics.widthPixels * 2;
  216. int doubleScreenHeight = displayMetrics.heightPixels * 2;
  217. Bitmap bitmap = BitmapShrinker.shrinkBitmap(photoFile.getAbsolutePath(),
  218. doubleScreenWidth,
  219. doubleScreenHeight);
  220. binding.photoPreview.setImageBitmap(bitmap);
  221. binding.photoPreview.setTag(savedUri);
  222. }
  223. public int getImageOrientation(File imageFile) {
  224. int rotate = 0;
  225. try {
  226. ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
  227. int orientation = exif.getAttributeInt(
  228. ExifInterface.TAG_ORIENTATION,
  229. ExifInterface.ORIENTATION_NORMAL);
  230. switch (orientation) {
  231. case ExifInterface.ORIENTATION_ROTATE_270:
  232. rotate = 270;
  233. break;
  234. case ExifInterface.ORIENTATION_ROTATE_180:
  235. rotate = 180;
  236. break;
  237. case ExifInterface.ORIENTATION_ROTATE_90:
  238. rotate = 90;
  239. break;
  240. default:
  241. rotate = 0;
  242. break;
  243. }
  244. Log.i(TAG, "ImageOrientation - Exif orientation: " + orientation + " - " + "Rotate value: " + rotate);
  245. } catch (Exception e) {
  246. Log.w(TAG, "Error calculation rotation value");
  247. }
  248. return rotate;
  249. }
  250. private Preview getPreview() {
  251. Preview preview = new Preview.Builder().build();
  252. preview.setSurfaceProvider(binding.preview.getSurfaceProvider());
  253. return preview;
  254. }
  255. @Override
  256. protected void onPause() {
  257. if (this.orientationEventListener != null) {
  258. this.orientationEventListener.disable();
  259. }
  260. super.onPause();
  261. }
  262. @Override
  263. protected void onResume() {
  264. super.onResume();
  265. if (this.orientationEventListener != null) {
  266. this.orientationEventListener.enable();
  267. }
  268. }
  269. @Override
  270. public void onSaveInstanceState(Bundle savedInstanceState) {
  271. if (binding.photoPreview.getTag() != null) {
  272. savedInstanceState.putString("Uri", ((Uri) binding.photoPreview.getTag()).getPath());
  273. }
  274. super.onSaveInstanceState(savedInstanceState);
  275. }
  276. @Override
  277. public void onRestoreInstanceState(Bundle savedInstanceState) {
  278. super.onRestoreInstanceState(savedInstanceState);
  279. String uri = savedInstanceState.getString("Uri", null);
  280. if (uri != null) {
  281. File photoFile = new File(uri);
  282. setPreviewImage(photoFile);
  283. showPictureProcessingElements();
  284. }
  285. }
  286. public static Intent createIntent(@NonNull Context context) {
  287. return new Intent(context, TakePhotoActivity.class).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
  288. }
  289. }