diff --git a/app/build.gradle b/app/build.gradle
index 567567eb5..404d9de15 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -197,6 +197,10 @@ dependencies {
ktlint "com.pinterest:ktlint:0.42.1"
implementation 'org.conscrypt:conscrypt-android:2.5.2'
+ implementation 'androidx.camera:camera-camera2:1.0.1'
+ implementation 'androidx.camera:camera-lifecycle:1.0.1'
+ implementation 'androidx.camera:camera-view:1.0.0-alpha20'
+
implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
implementation 'androidx.biometric:biometric:1.0.1'
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index acca95c7a..557db667a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -142,6 +142,11 @@
android:configChanges="orientation|keyboardHidden|screenSize">
+
+
diff --git a/app/src/main/java/com/nextcloud/talk/activities/TakePhotoActivity.java b/app/src/main/java/com/nextcloud/talk/activities/TakePhotoActivity.java
new file mode 100644
index 000000000..db00dd054
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/activities/TakePhotoActivity.java
@@ -0,0 +1,185 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * @author Stefan Niedermann
+ * Copyright (C) 2021 Andy Scherzinger
+ * Copyright (C) 2021 Stefan Niedermann
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.activities;
+
+import android.content.Context;
+import android.content.Intent;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+import android.util.Size;
+import android.view.OrientationEventListener;
+import android.view.Surface;
+import android.widget.Toast;
+
+import com.google.common.util.concurrent.ListenableFuture;
+import com.nextcloud.talk.databinding.ActivityTakePictureBinding;
+import com.nextcloud.talk.models.TakePictureViewModel;
+import com.nextcloud.talk.utils.FileUtils;
+
+import java.io.File;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+import java.util.concurrent.ExecutionException;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.camera.core.Camera;
+import androidx.camera.core.ImageCapture;
+import androidx.camera.core.ImageCaptureException;
+import androidx.camera.core.Preview;
+import androidx.camera.lifecycle.ProcessCameraProvider;
+import androidx.core.content.ContextCompat;
+import androidx.lifecycle.ViewModelProvider;
+
+public class TakePhotoActivity extends AppCompatActivity {
+
+ private static final String TAG = TakePhotoActivity.class.getSimpleName();
+
+ private ActivityTakePictureBinding binding;
+ private TakePictureViewModel viewModel;
+
+ private ListenableFuture cameraProviderFuture;
+ private OrientationEventListener orientationEventListener;
+
+ private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH-mm-ss", Locale.ROOT);
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ binding = ActivityTakePictureBinding.inflate(getLayoutInflater());
+ viewModel = new ViewModelProvider(this).get(TakePictureViewModel.class);
+
+ setContentView(binding.getRoot());
+
+ cameraProviderFuture = ProcessCameraProvider.getInstance(this);
+ cameraProviderFuture.addListener(() -> {
+ try {
+ final ProcessCameraProvider cameraProvider = cameraProviderFuture.get();
+ final Preview preview = getPreview();
+ final ImageCapture imageCapture = getImageCapture();
+ final Camera camera = cameraProvider.bindToLifecycle(this, viewModel.getCameraSelector(), imageCapture, preview);
+
+ viewModel.getCameraSelectorToggleButtonImageResource().observe(this, res -> binding.switchCamera.setImageDrawable(ContextCompat.getDrawable(this, res)));
+ viewModel.getTorchToggleButtonImageResource().observe(this, res -> binding.toggleTorch.setImageDrawable(ContextCompat.getDrawable(this, res)));
+ viewModel.isTorchEnabled().observe(this, enabled -> camera.getCameraControl().enableTorch(enabled));
+
+ binding.toggleTorch.setOnClickListener((v) -> viewModel.toggleTorchEnabled());
+ binding.switchCamera.setOnClickListener((v) -> {
+ viewModel.toggleCameraSelector();
+ cameraProvider.unbindAll();
+ cameraProvider.bindToLifecycle(this, viewModel.getCameraSelector(), imageCapture, preview);
+ });
+ } catch (IllegalArgumentException | ExecutionException | InterruptedException e) {
+ Log.e(TAG, "Error taking picture", e);
+ Toast.makeText(this, e.getMessage(), Toast.LENGTH_LONG).show();
+ finish();
+ }
+ }, ContextCompat.getMainExecutor(this));
+ }
+
+ private ImageCapture getImageCapture() {
+ final ImageCapture imageCapture = new ImageCapture.Builder().setTargetResolution(new Size(720, 1280)).build();
+
+ orientationEventListener = new OrientationEventListener(this) {
+ @Override
+ public void onOrientationChanged(int orientation) {
+ int rotation;
+
+ // Monitors orientation values to determine the target rotation value
+ if (orientation >= 45 && orientation < 135) {
+ rotation = Surface.ROTATION_270;
+ } else if (orientation >= 135 && orientation < 225) {
+ rotation = Surface.ROTATION_180;
+ } else if (orientation >= 225 && orientation < 315) {
+ rotation = Surface.ROTATION_90;
+ } else {
+ rotation = Surface.ROTATION_0;
+ }
+
+ imageCapture.setTargetRotation(rotation);
+ }
+ };
+ orientationEventListener.enable();
+
+ binding.takePhoto.setOnClickListener((v) -> {
+ binding.takePhoto.setEnabled(false);
+ final String photoFileName = dateFormat.format(new Date())+ ".jpg";
+ try {
+ final File photoFile = FileUtils.getTempCacheFile(this, "photos/" + photoFileName);
+ final ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder(photoFile).build();
+ imageCapture.takePicture(options, ContextCompat.getMainExecutor(this), new ImageCapture.OnImageSavedCallback() {
+ @Override
+ public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
+ final Uri savedUri = Uri.fromFile(photoFile);
+ Log.i(TAG, "onImageSaved - savedUri:" + savedUri.toString());
+ setResult(RESULT_OK, new Intent().setDataAndType(savedUri, "image/jpeg"));
+ finish();
+ }
+
+ @Override
+ public void onError(@NonNull ImageCaptureException e) {
+ Log.e(TAG, "Error", e);
+ //noinspection ResultOfMethodCallIgnored
+ photoFile.delete();
+ binding.takePhoto.setEnabled(true);
+ }
+ });
+ } catch (Exception e) {
+ // TODO replace string with placeholder
+ Toast.makeText(this, "Error taking picture", Toast.LENGTH_SHORT).show();
+ }
+ });
+
+ return imageCapture;
+ }
+
+ private Preview getPreview() {
+ Preview preview = new Preview.Builder().build();
+ preview.setSurfaceProvider(binding.preview.getSurfaceProvider());
+ return preview;
+ }
+
+ @Override
+ protected void onPause() {
+ if (this.orientationEventListener != null) {
+ this.orientationEventListener.disable();
+ }
+ super.onPause();
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ if (this.orientationEventListener != null) {
+ this.orientationEventListener.enable();
+ }
+ }
+
+ public static Intent createIntent(@NonNull Context context) {
+ return new Intent(context, TakePhotoActivity.class).setFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ }
+}
diff --git a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
index dc80c46e4..294fd925c 100644
--- a/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
+++ b/app/src/main/java/com/nextcloud/talk/controllers/ChatController.kt
@@ -96,6 +96,7 @@ import com.google.android.flexbox.FlexboxLayout
import com.nextcloud.talk.R
import com.nextcloud.talk.activities.MagicCallActivity
import com.nextcloud.talk.activities.MainActivity
+import com.nextcloud.talk.activities.TakePhotoActivity
import com.nextcloud.talk.adapters.messages.IncomingLocationMessageViewHolder
import com.nextcloud.talk.adapters.messages.IncomingPreviewMessageViewHolder
import com.nextcloud.talk.adapters.messages.IncomingVoiceMessageViewHolder
@@ -1208,6 +1209,34 @@ class ChatController(args: Bundle) :
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
}
}
+ } else if (requestCode == REQUEST_CODE_PICK_CAMERA) {
+ if (resultCode == RESULT_OK) {
+ try {
+ checkNotNull(intent)
+ filesToUpload.clear()
+ run {
+ checkNotNull(intent.data)
+ intent.data.let {
+ filesToUpload.add(intent.data.toString())
+ }
+ }
+ require(filesToUpload.isNotEmpty())
+
+ if (UploadAndShareFilesWorker.isStoragePermissionGranted(context!!)) {
+ uploadFiles(filesToUpload, false)
+ } else {
+ UploadAndShareFilesWorker.requestStoragePermission(this)
+ }
+ } catch (e: IllegalStateException) {
+ Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG)
+ .show()
+ Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
+ } catch (e: IllegalArgumentException) {
+ Toast.makeText(context, context?.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG)
+ .show()
+ Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
+ }
+ }
}
}
@@ -2535,6 +2564,10 @@ class ChatController(args: Bundle) :
}
}
+ fun sendPictureFromCamIntent() {
+ startActivityForResult(TakePhotoActivity.createIntent(context!!), REQUEST_CODE_PICK_CAMERA)
+ }
+
companion object {
private const val TAG = "ChatController"
private const val CONTENT_TYPE_SYSTEM_MESSAGE: Byte = 1
@@ -2549,6 +2582,7 @@ class ChatController(args: Bundle) :
private const val AGE_THREHOLD_FOR_DELETE_MESSAGE: Int = 21600000 // (6 hours in millis = 6 * 3600 * 1000)
private const val REQUEST_CODE_CHOOSE_FILE: Int = 555
private const val REQUEST_RECORD_AUDIO_PERMISSION = 222
+ private const val REQUEST_CODE_PICK_CAMERA: Int = 333
private const val OBJECT_MESSAGE: String = "{object}"
private const val MINIMUM_VOICE_RECORD_DURATION: Int = 1000
private const val VOICE_RECORD_CANCEL_SLIDER_X: Int = -50
diff --git a/app/src/main/java/com/nextcloud/talk/models/TakePictureViewModel.java b/app/src/main/java/com/nextcloud/talk/models/TakePictureViewModel.java
new file mode 100644
index 000000000..e6874cd65
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/models/TakePictureViewModel.java
@@ -0,0 +1,80 @@
+/*
+ * Nextcloud Talk application
+ *
+ * @author Andy Scherzinger
+ * @author Stefan Niedermann
+ * Copyright (C) 2021 Andy Scherzinger
+ * Copyright (C) 2021 Stefan Niedermann
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package com.nextcloud.talk.models;
+
+import com.nextcloud.talk.R;
+
+import androidx.annotation.NonNull;
+import androidx.camera.core.CameraSelector;
+import androidx.lifecycle.LiveData;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Transformations;
+import androidx.lifecycle.ViewModel;
+
+import static androidx.camera.core.CameraSelector.DEFAULT_BACK_CAMERA;
+import static androidx.camera.core.CameraSelector.DEFAULT_FRONT_CAMERA;
+
+public class TakePictureViewModel extends ViewModel {
+
+ @NonNull
+ private CameraSelector cameraSelector = DEFAULT_BACK_CAMERA;
+ @NonNull
+ private final MutableLiveData cameraSelectorToggleButtonImageResource = new MutableLiveData<>(R.drawable.ic_baseline_camera_front_24);
+ @NonNull
+ private final MutableLiveData torchEnabled = new MutableLiveData<>(false);
+
+ @NonNull
+ public CameraSelector getCameraSelector() {
+ return this.cameraSelector;
+ }
+
+ public LiveData getCameraSelectorToggleButtonImageResource() {
+ return this.cameraSelectorToggleButtonImageResource;
+ }
+
+ public void toggleCameraSelector() {
+ if (this.cameraSelector == DEFAULT_BACK_CAMERA) {
+ this.cameraSelector = DEFAULT_FRONT_CAMERA;
+ this.cameraSelectorToggleButtonImageResource.postValue(R.drawable.ic_baseline_camera_rear_24);
+ } else {
+ this.cameraSelector = DEFAULT_BACK_CAMERA;
+ this.cameraSelectorToggleButtonImageResource.postValue(R.drawable.ic_baseline_camera_front_24);
+ }
+ }
+
+ public void toggleTorchEnabled() {
+ //noinspection ConstantConditions
+ this.torchEnabled.postValue(!this.torchEnabled.getValue());
+ }
+
+ public LiveData isTorchEnabled() {
+ return this.torchEnabled;
+ }
+
+ public LiveData getTorchToggleButtonImageResource() {
+ return Transformations.map(isTorchEnabled(), enabled -> enabled
+ ? R.drawable.ic_baseline_flash_off_24
+ : R.drawable.ic_baseline_flash_on_24);
+ }
+}
+
diff --git a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
index 51b2959fb..ed6185bbe 100644
--- a/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
+++ b/app/src/main/java/com/nextcloud/talk/ui/dialog/AttachmentDialog.kt
@@ -54,6 +54,10 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
@JvmField
var attachFromCloud: AppCompatTextView? = null
+ @BindView(R.id.menu_attach_picture_from_cam)
+ @JvmField
+ var pictureFromCamItem: LinearLayout? = null
+
private var unbinder: Unbinder? = null
override fun onCreate(savedInstanceState: Bundle?) {
@@ -88,6 +92,12 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
chatController.sendSelectLocalFileIntent()
dismiss()
}
+
+ pictureFromCamItem?.setOnClickListener {
+ chatController.sendPictureFromCamIntent()
+ dismiss()
+ }
+
attachFromCloud?.setOnClickListener {
chatController.showBrowserScreen(BrowserController.BrowserType.DAV_BROWSER)
dismiss()
diff --git a/app/src/main/java/com/nextcloud/talk/utils/FileUtils.java b/app/src/main/java/com/nextcloud/talk/utils/FileUtils.java
new file mode 100644
index 000000000..e18e0147b
--- /dev/null
+++ b/app/src/main/java/com/nextcloud/talk/utils/FileUtils.java
@@ -0,0 +1,46 @@
+package com.nextcloud.talk.utils;
+
+import android.content.Context;
+import android.util.Log;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+import androidx.annotation.NonNull;
+
+public class FileUtils {
+ private static final String TAG = FileUtils.class.getSimpleName();
+
+ /**
+ * Creates a new {@link File}
+ */
+ public static File getTempCacheFile(@NonNull Context context, String fileName) throws IOException {
+ File cacheFile = new File(context.getApplicationContext().getFilesDir().getAbsolutePath() + "/" + fileName);
+
+ Log.v(TAG, "Full path for new cache file:" + cacheFile.getAbsolutePath());
+
+ final File tempDir = cacheFile.getParentFile();
+ if (tempDir == null) {
+ throw new FileNotFoundException("could not cacheFile.getParentFile()");
+ }
+ if (!tempDir.exists()) {
+ Log.v(TAG,
+ "The folder in which the new file should be created does not exist yet. Trying to create it…");
+ if (tempDir.mkdirs()) {
+ Log.v(TAG, "Creation successful");
+ } else {
+ throw new IOException("Directory for temporary file does not exist and could not be created.");
+ }
+ }
+
+ Log.v(TAG, "- Try to create actual cache file");
+ if (cacheFile.createNewFile()) {
+ Log.v(TAG, "Successfully created cache file");
+ } else {
+ throw new IOException("Failed to create cacheFile");
+ }
+
+ return cacheFile;
+ }
+}
diff --git a/app/src/main/res/drawable/ic_baseline_camera_front_24.xml b/app/src/main/res/drawable/ic_baseline_camera_front_24.xml
new file mode 100644
index 000000000..25c1a79b8
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_camera_front_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_camera_rear_24.xml b/app/src/main/res/drawable/ic_baseline_camera_rear_24.xml
new file mode 100644
index 000000000..51cea2177
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_camera_rear_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_flash_off_24.xml b/app/src/main/res/drawable/ic_baseline_flash_off_24.xml
new file mode 100644
index 000000000..2a3b0ff5d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_flash_off_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_flash_on_24.xml b/app/src/main/res/drawable/ic_baseline_flash_on_24.xml
new file mode 100644
index 000000000..4574d0e20
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_flash_on_24.xml
@@ -0,0 +1,5 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml b/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml
new file mode 100644
index 000000000..497db8383
--- /dev/null
+++ b/app/src/main/res/drawable/ic_baseline_photo_camera_24.xml
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/app/src/main/res/layout/activity_take_picture.xml b/app/src/main/res/layout/activity_take_picture.xml
new file mode 100644
index 000000000..d5daa77cf
--- /dev/null
+++ b/app/src/main/res/layout/activity_take_picture.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/dialog_attachment.xml b/app/src/main/res/layout/dialog_attachment.xml
index 719393a94..dbc3332ba 100644
--- a/app/src/main/res/layout/dialog_attachment.xml
+++ b/app/src/main/res/layout/dialog_attachment.xml
@@ -105,6 +105,39 @@
+
+
+
+
+
+
+
+
#606060
+
+ #7f000000
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 807345106..cb881b959 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -374,6 +374,7 @@
Add to conversation
Upload local file
+ Upload from camera
Share from %1$s
Sorry, upload failed
Choose files
@@ -457,4 +458,8 @@
%1$s (%2$d)
Invalid password
Do you want to reauthorize or delete this account?
+
+ Take a photo
+ Switch camera
+ Toggle torch
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
index 7b09c2017..029d893be 100644
--- a/app/src/main/res/values/styles.xml
+++ b/app/src/main/res/values/styles.xml
@@ -55,6 +55,18 @@
- @color/grey950
+
+
+
+