proton-mail-android/app/src/main/java/ch/protonmail/android/activities/AddAttachmentsActivity.java

608 lines
24 KiB
Java

/*
* Copyright (c) 2022 Proton AG
*
* This file is part of Proton Mail.
*
* Proton Mail 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.
*
* Proton Mail 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 Proton Mail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.activities;
import static ch.protonmail.android.attachments.ImportAttachmentsWorkerKt.KEY_INPUT_DATA_DELETE_ORIGINAL_FILE_BOOLEAN;
import static ch.protonmail.android.attachments.ImportAttachmentsWorkerKt.KEY_INPUT_DATA_FILE_URIS_STRING_ARRAY;
import android.app.Activity;
import android.content.ClipData;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.ParcelFileDescriptor;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.appcompat.app.ActionBar;
import androidx.core.content.FileProvider;
import androidx.lifecycle.ViewModelProvider;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkManager;
import com.squareup.otto.Subscribe;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.atomic.AtomicLong;
import javax.inject.Inject;
import butterknife.BindView;
import ch.protonmail.android.R;
import ch.protonmail.android.adapters.AttachmentListAdapter;
import ch.protonmail.android.attachments.AttachmentsViewModel;
import ch.protonmail.android.attachments.AttachmentsViewState;
import ch.protonmail.android.attachments.ImportAttachmentsWorker;
import ch.protonmail.android.core.Constants;
import ch.protonmail.android.core.ProtonMailApplication;
import ch.protonmail.android.data.local.model.Attachment;
import ch.protonmail.android.data.local.model.LocalAttachment;
import ch.protonmail.android.events.DownloadedAttachmentEvent;
import ch.protonmail.android.events.PostImportAttachmentEvent;
import ch.protonmail.android.events.PostImportAttachmentFailureEvent;
import ch.protonmail.android.events.Status;
import ch.protonmail.android.utils.DateUtil;
import ch.protonmail.android.utils.DownloadUtils;
import ch.protonmail.android.utils.Logger;
import ch.protonmail.android.utils.extensions.TextExtensions;
import ch.protonmail.android.utils.ui.dialogs.DialogUtils;
import dagger.hilt.android.AndroidEntryPoint;
import kotlin.collections.ArraysKt;
import kotlin.collections.CollectionsKt;
import timber.log.Timber;
@AndroidEntryPoint
public class AddAttachmentsActivity extends BaseStoragePermissionActivity implements AttachmentListAdapter.IAttachmentListener {
private static final String TAG_ADD_ATTACHMENTS_ACTIVITY = "AddAttachmentsActivity";
public static final String EXTRA_ATTACHMENT_LIST = "EXTRA_ATTACHMENT_LIST";
public static final String EXTRA_DRAFT_ID = "EXTRA_DRAFT_ID";
public static final String EXTRA_DRAFT_CREATED = "EXTRA_DRAFT_CREATED";
private static final String ATTACHMENT_MIME_TYPE = "*/*";
private static final int REQUEST_CODE_ATTACH_FILE = 1;
private static final int REQUEST_CODE_TAKE_PHOTO = 2;
private static final String STATE_PHOTO_PATH = "STATE_PATH_TO_PHOTO";
private AttachmentListAdapter mAdapter;
@BindView(R.id.progress_layout)
View mProgressLayout;
@BindView(R.id.processing_attachment_layout)
View mProcessingAttachmentLayout;
@BindView(R.id.no_attachments)
View mNoAttachmentsView;
@BindView(R.id.num_attachments)
TextView mNumAttachmentsView;
@BindView(R.id.attachment_list)
ListView mListView;
@Inject
WorkManager workManager;
@Inject
DownloadUtils downloadUtils;
private String mPathToPhoto;
private String mDraftId;
private boolean mDraftCreated;
private List<Uri> mAttachFileWithoutPermission;
private String mAttachTakePhotoWithoutPermission;
private boolean openGallery = false;
private boolean openCamera = false;
@Override
protected int getLayoutId() {
return R.layout.activity_add_attachments;
}
@Override
public void onHasPermission(Constants.PermissionType type) {
if (type == Constants.PermissionType.STORAGE) {
super.onHasPermission(type);
mHasStoragePermission = true;
}
}
@Override
public void onPermissionDenied(Constants.PermissionType type) {
super.onPermissionDenied(type);
openCamera = false;
openGallery = false;
DialogUtils.Companion.showInfoDialog(AddAttachmentsActivity.this, getString(R.string.need_permissions_title),
getString(R.string.need_storage_permissions_add_attachment_text), unit -> unit);
}
@Override
public void onPermissionConfirmed(Constants.PermissionType type) {
super.onPermissionConfirmed(type);
if (openGallery) {
openGallery();
}
if (openCamera) {
openCamera();
}
}
@Override
protected void storagePermissionGranted() {
mHasStoragePermission = true;
if (mAttachFileWithoutPermission != null && mAttachFileWithoutPermission.size() > 0) {
mProcessingAttachmentLayout.setVisibility(View.VISIBLE);
String[] uriStrings = new String[mAttachFileWithoutPermission.size()];
for (int i = 0; i < mAttachFileWithoutPermission.size(); i++) {
uriStrings[i] = mAttachFileWithoutPermission.get(i).toString();
}
Data workerData = new Data.Builder()
.putStringArray(KEY_INPUT_DATA_FILE_URIS_STRING_ARRAY, uriStrings)
.build();
OneTimeWorkRequest importAttachmentsWork = new OneTimeWorkRequest.Builder(ImportAttachmentsWorker.class)
.setInputData(workerData)
.build();
workManager.enqueue(importAttachmentsWork);
mAttachFileWithoutPermission = null;
}
if (!TextUtils.isEmpty(mAttachTakePhotoWithoutPermission)) {
mProcessingAttachmentLayout.setVisibility(View.VISIBLE);
handleTakePhotoRequest(mAttachTakePhotoWithoutPermission);
mAttachTakePhotoWithoutPermission = null;
}
}
@Override
protected boolean checkForPermissionOnStartup() {
return true;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setTitle(R.string.add_attachment);
}
Intent intent = getIntent();
ArrayList<LocalAttachment> attachmentList = intent.getParcelableArrayListExtra(EXTRA_ATTACHMENT_LIST);
if (attachmentList == null) {
attachmentList = new ArrayList<>();
}
mDraftId = intent.getStringExtra(EXTRA_DRAFT_ID);
mDraftCreated = intent.getBooleanExtra(EXTRA_DRAFT_CREATED, true);
int attachmentsCount = attachmentList.size();
int totalEmbeddedImages = countEmbeddedImages(attachmentList);
updateAttachmentsCount(attachmentsCount, totalEmbeddedImages);
if (mDraftCreated) {
mProgressLayout.setVisibility(View.GONE);
} else {
mProgressLayout.setVisibility(View.VISIBLE);
}
mAdapter = new AttachmentListAdapter(this, attachmentList, totalEmbeddedImages,
workManager);
mListView.setAdapter(mAdapter);
AttachmentsViewModel viewModel = new ViewModelProvider(this).get(AttachmentsViewModel.class);
viewModel.init();
viewModel.getViewState().observe(this, this::viewStateChanged);
}
@Override
protected void onStart() {
super.onStart();
ProtonMailApplication.getApplication().getBus().register(this);
}
@Override
protected void onStop() {
super.onStop();
ProtonMailApplication.getApplication().getBus().unregister(this);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.menu_attachments, menu);
return true;
}
@Override
public boolean onPrepareOptionsMenu(Menu menu) {
menu.findItem(R.id.take_photo).setVisible(getPackageManager().hasSystemFeature(PackageManager.FEATURE_CAMERA));
menu.findItem(R.id.attach_file).setVisible(mDraftCreated);
menu.findItem(R.id.take_photo).setVisible(mDraftCreated);
return true;
}
@Override
public void onBackPressed() {
Intent intent = new Intent();
intent.putExtra(EXTRA_DRAFT_ID, mDraftId);
ArrayList<LocalAttachment> currentAttachments = mAdapter.getData();
intent.putParcelableArrayListExtra(EXTRA_ATTACHMENT_LIST, currentAttachments);
setResult(Activity.RESULT_OK, intent);
finish();
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
case R.id.attach_file: {
openGallery = true;
if (mHasStoragePermission != null && mHasStoragePermission) {
return openGallery();
} else {
storagePermissionHelper.checkPermission();
return false;
}
}
case R.id.take_photo: {
openCamera = true;
if (mHasStoragePermission != null && mHasStoragePermission) {
return openCamera();
} else {
storagePermissionHelper.checkPermission();
return false;
}
}
default:
return super.onOptionsItemSelected(item);
}
}
private boolean isAttachmentsSizeAllowed(long newAttachmentSie) {
if (mAdapter == null) return false;
long currentAttachmentsSize =
CollectionsKt.sumOfLong(CollectionsKt.map(mAdapter.getData(), LocalAttachment::getSize));
return currentAttachmentsSize + newAttachmentSie < Constants.MAX_ATTACHMENT_FILE_SIZE_IN_BYTES;
}
private boolean isAttachmentsCountAllowed() {
return mAdapter != null && mAdapter.getCount() < Constants.MAX_ATTACHMENTS;
}
@Subscribe
public void onPostImportAttachmentEvent(PostImportAttachmentEvent event) {
mProcessingAttachmentLayout.setVisibility(View.GONE);
mNoAttachmentsView.setVisibility(View.GONE);
LocalAttachment newAttachment = new LocalAttachment(Uri.parse(event.uri), event.displayName, event.size, event.mimeType);
ArrayList<LocalAttachment> currentAttachments = mAdapter.getData();
boolean alreadyExists = false;
for (LocalAttachment localAttachment : currentAttachments) {
String localAttachmentDisplayName = localAttachment.getDisplayName();
boolean isEmpty = TextUtils.isEmpty(localAttachmentDisplayName);
if (!isEmpty && localAttachmentDisplayName.equals(newAttachment.getDisplayName())) {
alreadyExists = true;
}
}
if (alreadyExists) {
TextExtensions.showToast(AddAttachmentsActivity.this, R.string.attachment_exists, Toast.LENGTH_SHORT);
return;
}
currentAttachments.add(newAttachment);
int totalEmbeddedImages = countEmbeddedImages(currentAttachments);
mAdapter.updateData(new ArrayList<>(currentAttachments), totalEmbeddedImages);
int attachments = currentAttachments.size();
updateAttachmentsCount(attachments, totalEmbeddedImages);
}
@Subscribe
public void onPostImportAttachmentFailureEvent(PostImportAttachmentFailureEvent event) {
mProcessingAttachmentLayout.setVisibility(View.GONE);
TextExtensions.showToast(this, R.string.problem_selecting_file);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode != RESULT_OK) {
super.onActivityResult(requestCode, resultCode, data);
return;
}
if (mHasStoragePermission == null || !mHasStoragePermission) {
if (requestCode == REQUEST_CODE_ATTACH_FILE) {
mAttachFileWithoutPermission = new ArrayList<>();
ClipData clipData = data.getClipData();
if (clipData != null) {
for (int i = 0; i < clipData.getItemCount(); i++) {
ClipData.Item item = clipData.getItemAt(i);
mAttachFileWithoutPermission.add(item.getUri());
}
} else {
mAttachFileWithoutPermission.add(data.getData());
}
} else if (requestCode == REQUEST_CODE_TAKE_PHOTO) {
mAttachTakePhotoWithoutPermission = mPathToPhoto;
}
storagePermissionHelper.checkPermission();
return;
}
if (requestCode == REQUEST_CODE_ATTACH_FILE) {
handleAttachFileRequest(data.getData(), data.getClipData());
} else if (requestCode == REQUEST_CODE_TAKE_PHOTO) {
handleTakePhotoRequest(mPathToPhoto);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putString(STATE_PHOTO_PATH, mPathToPhoto);
super.onSaveInstanceState(outState);
}
@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
mPathToPhoto = savedInstanceState.getString(STATE_PHOTO_PATH);
}
@Subscribe
public void onDownloadAttachmentEvent(DownloadedAttachmentEvent event) {
//once attachment has been downloaded
if (event.getStatus().equals(Status.SUCCESS)) {
downloadUtils.viewAttachment(event.getFilename(), event.getAttachmentUri());
TextExtensions.showToast(this, String.format(getString(R.string.attachment_download_success), event.getFilename()), Toast.LENGTH_SHORT);
} else {
TextExtensions.showToast(this, String.format(getString(R.string.attachment_download_failed), event.getFilename()), Toast.LENGTH_SHORT);
}
}
private void viewStateChanged(AttachmentsViewState viewState) {
if (viewState instanceof AttachmentsViewState.MissingConnectivity) {
onMessageReady();
}
if (viewState instanceof AttachmentsViewState.UpdateAttachments) {
onMessageReady();
updateDisplayedAttachments(
((AttachmentsViewState.UpdateAttachments) viewState).getAttachments()
);
}
}
private void onMessageReady() {
mDraftCreated = true;
mProgressLayout.setVisibility(View.GONE);
invalidateOptionsMenu();
}
private void updateDisplayedAttachments(List<Attachment> attachments) {
List<LocalAttachment> localAttachments = new ArrayList<>(
LocalAttachment.Companion.createLocalAttachmentList(attachments)
);
int totalEmbeddedImages = countEmbeddedImages(localAttachments);
mAdapter.updateData(new ArrayList(localAttachments), totalEmbeddedImages);
}
private void handleAttachFileRequest(Uri uri, ClipData clipData) {
String[] uris = null;
String uriString = uri != null ? uri.toString() : null;
if (uriString != null) {
uris = new String[]{uriString};
}
if (clipData != null) {
uris = new String[clipData.getItemCount()];
for (int i = 0; i < clipData.getItemCount(); i++) {
uris[i] = clipData.getItemAt(i).getUri().toString();
}
}
if (uris != null) {
// region Check whether the size of the attachments is within bounds
final AtomicLong incrementalSize = new AtomicLong(0);
List<String> sizeComplaintUrisList = ArraysKt.filter(uris, fileUriString -> {
Uri fileUri = Uri.parse(fileUriString);
try {
ParcelFileDescriptor fileDescriptor = getContentResolver().openFileDescriptor(fileUri, "r");
if (fileDescriptor == null) {
return false;
}
final long fileSize = fileDescriptor.getStatSize();
return isAttachmentsSizeAllowed(incrementalSize.addAndGet(fileSize));
} catch (FileNotFoundException e) {
return false;
}
});
if (sizeComplaintUrisList.size() < uris.length) {
TextExtensions.showToast(this, R.string.max_attachments_size_reached);
}
// endregion
if (sizeComplaintUrisList.size() > 0) {
mProcessingAttachmentLayout.setVisibility(View.VISIBLE);
String[] sizeComplaintUris = new String[sizeComplaintUrisList.size()];
sizeComplaintUrisList.toArray(sizeComplaintUris);
Data workerData = new Data.Builder()
.putStringArray(KEY_INPUT_DATA_FILE_URIS_STRING_ARRAY, sizeComplaintUris)
.build();
OneTimeWorkRequest importAttachmentsWork = new OneTimeWorkRequest.Builder(ImportAttachmentsWorker.class)
.setInputData(workerData)
.build();
workManager.enqueue(importAttachmentsWork);
}
}
}
private void handleTakePhotoRequest(String path) {
if (!TextUtils.isEmpty(path)) {
File file = new File(path);
// Check whether the size of the attachment is within bounds
if (!isAttachmentsSizeAllowed(file.length())) {
TextExtensions.showToast(this, R.string.max_attachments_size_reached);
return;
}
mProcessingAttachmentLayout.setVisibility(View.VISIBLE);
Uri uri = Uri.fromFile(file);
Data data = new Data.Builder()
.putStringArray(KEY_INPUT_DATA_FILE_URIS_STRING_ARRAY, new String[]{uri.toString()})
.putBoolean(KEY_INPUT_DATA_DELETE_ORIGINAL_FILE_BOOLEAN, true)
.build();
OneTimeWorkRequest importAttachmentsWork = new OneTimeWorkRequest.Builder(ImportAttachmentsWorker.class)
.setInputData(data)
.build();
workManager.enqueue(importAttachmentsWork);
workManager.getWorkInfoByIdLiveData(importAttachmentsWork.getId()).observe(this, workInfo -> {
if (workInfo != null) {
Timber.d("ImportAttachmentsWorker workInfo = " + workInfo.getState());
}
});
} else {
TextExtensions.showToast(this, R.string.attaching_photo_failed, Toast.LENGTH_LONG, Gravity.CENTER);
}
}
@Override
public void onAttachmentDeleted(int remainingAttachments, int embeddedImagesCount) {
updateAttachmentsCount(remainingAttachments, embeddedImagesCount);
}
@Override
public void askStoragePermission() {
storagePermissionHelper.checkPermission();
}
private void updateAttachmentsCount(int totalAttachmentsCount, int totalEmbeddedImagesCount) {
if (totalAttachmentsCount == 0 && totalEmbeddedImagesCount == 0) {
mNoAttachmentsView.postDelayed(() -> mNoAttachmentsView.setVisibility(View.VISIBLE), 350);
mNumAttachmentsView.setVisibility(View.GONE);
} else {
int normalAttachments = totalAttachmentsCount - totalEmbeddedImagesCount;
if (normalAttachments > 0) {
mNumAttachmentsView.setText(getResources().getQuantityString(R.plurals.attachments, normalAttachments, normalAttachments));
mNumAttachmentsView.setVisibility(View.VISIBLE);
} else {
mNumAttachmentsView.setText(getString(R.string.no_attachments));
mNumAttachmentsView.setVisibility(View.VISIBLE);
}
}
}
//TODO extract logic to separate class as it is not dependant on activity
private int countEmbeddedImages(List<LocalAttachment> attachments) {
int embeddedImages = 0;
for (LocalAttachment localAttachment : attachments) {
if (localAttachment.isEmbeddedImage()) {
embeddedImages++;
}
}
return embeddedImages;
}
private boolean openGallery() {
if (!isAttachmentsCountAllowed()) {
TextExtensions.showToast(this, R.string.max_attachments_reached);
return true;
}
Intent target = new Intent(Intent.ACTION_GET_CONTENT);
target.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true);
target.addCategory(Intent.CATEGORY_OPENABLE);
target.setType(ATTACHMENT_MIME_TYPE);
Intent intent = Intent.createChooser(target, getString(R.string.select_file));
if (intent.resolveActivity(getPackageManager()) != null) {
startActivityForResult(intent, REQUEST_CODE_ATTACH_FILE);
} else {
TextExtensions.showToast(this, R.string.no_application_found);
}
return true;
}
private boolean openCamera() {
if (!isAttachmentsCountAllowed()) {
TextExtensions.showToast(this, R.string.max_attachments_reached);
return true;
}
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
if (intent.resolveActivity(getPackageManager()) != null) {
String timestamp = DateUtil.generateTimestamp();
timestamp = timestamp.replace("-", "");
timestamp = timestamp.replaceAll("[^A-Za-z0-9]", "");
File directory = getExternalFilesDir(Environment.DIRECTORY_PICTURES);
try {
if (timestamp.length() < 3) {
Random random = new Random();
int number = random.nextInt(99999) + 100;
timestamp = timestamp + String.valueOf(number);
}
File file = File.createTempFile(timestamp, ".jpg", directory);
intent.putExtra(MediaStore.EXTRA_OUTPUT, FileProvider.getUriForFile(AddAttachmentsActivity.this, getApplicationContext().getPackageName() + ".provider", file));
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
mPathToPhoto = file.getAbsolutePath();
startActivityForResult(intent, REQUEST_CODE_TAKE_PHOTO);
} catch (IOException ioe) {
Logger.doLogException(TAG_ADD_ATTACHMENTS_ACTIVITY,
"Exception creating temporary file for photo", ioe);
TextExtensions.showToast(this, R.string.problem_taking_photo);
}
}
return true;
}
}