Clean up after removing ImportAttachmentsWorker.kt

MAILAND-1669
This commit is contained in:
Davide Farella 2021-05-26 09:56:33 +02:00
parent eb5d9d9cd1
commit 5db8f021a9
12 changed files with 14 additions and 1574 deletions

View File

@ -212,11 +212,6 @@
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
<activity
android:name=".activities.AddAttachmentsActivity"
android:exported="false"
android:configChanges="orientation|screenSize"
android:screenOrientation="portrait" />
<activity
android:name=".activities.SearchActivity"
android:exported="false"

View File

@ -1,614 +0,0 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail 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.
*
* ProtonMail 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 ProtonMail. 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.MessageDao;
import ch.protonmail.android.data.local.MessageDatabase;
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 MessageDao messageDao;
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_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);
messageDao = MessageDatabase.Factory.getInstance(getApplicationContext(), mUserManager.requireCurrentUserId()).getDao();
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() {
saveLastInteraction();
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);
saveLastInteraction();
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(this, 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;
}
}

View File

@ -18,11 +18,6 @@
*/
package ch.protonmail.android.activities;
import static ch.protonmail.android.settings.pin.ValidatePinActivityKt.EXTRA_FRAGMENT_TITLE;
import static ch.protonmail.android.settings.pin.ValidatePinActivityKt.EXTRA_PIN_VALID;
import static ch.protonmail.android.worker.FetchUserWorkerKt.FETCH_USER_INFO_WORKER_NAME;
import static ch.protonmail.android.worker.FetchUserWorkerKt.FETCH_USER_INFO_WORKER_RESULT;
import android.app.AlertDialog;
import android.content.BroadcastReceiver;
import android.content.Context;
@ -79,6 +74,11 @@ import ch.protonmail.android.worker.FetchUserWorker;
import dagger.hilt.android.AndroidEntryPoint;
import timber.log.Timber;
import static ch.protonmail.android.settings.pin.ValidatePinActivityKt.EXTRA_FRAGMENT_TITLE;
import static ch.protonmail.android.settings.pin.ValidatePinActivityKt.EXTRA_PIN_VALID;
import static ch.protonmail.android.worker.FetchUserWorkerKt.FETCH_USER_INFO_WORKER_NAME;
import static ch.protonmail.android.worker.FetchUserWorkerKt.FETCH_USER_INFO_WORKER_RESULT;
@AndroidEntryPoint
public abstract class BaseActivity extends AppCompatActivity implements INetworkConfiguratorCallback {
@ -373,10 +373,6 @@ public abstract class BaseActivity extends AppCompatActivity implements INetwork
if (!validationCanceled && !(this instanceof ValidatePinActivity)) {
saveLastInteraction();
}
if (!(this instanceof AddAttachmentsActivity)) {
inApp = false;
activateScreenProtector();
}
}
private void deactivateScreenProtector() {

View File

@ -40,12 +40,10 @@ import android.text.TextUtils;
import android.text.TextWatcher;
import android.text.format.Formatter;
import android.text.util.Linkify;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewTreeObserver;
import android.webkit.WebSettings;
@ -68,10 +66,6 @@ import androidx.core.widget.NestedScrollView;
import androidx.lifecycle.Observer;
import androidx.loader.app.LoaderManager;
import androidx.loader.content.Loader;
import androidx.work.Data;
import androidx.work.OneTimeWorkRequest;
import androidx.work.WorkInfo;
import androidx.work.WorkManager;
import com.google.android.material.snackbar.Snackbar;
import com.squareup.otto.Subscribe;
@ -92,7 +86,6 @@ import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import javax.annotation.Nonnull;
import javax.inject.Inject;
@ -100,7 +93,6 @@ import javax.inject.Inject;
import butterknife.BindView;
import butterknife.OnClick;
import ch.protonmail.android.R;
import ch.protonmail.android.activities.AddAttachmentsActivity;
import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository;
import ch.protonmail.android.api.models.MessageRecipient;
import ch.protonmail.android.api.models.SendPreference;
@ -109,7 +101,6 @@ import ch.protonmail.android.api.models.address.Address;
import ch.protonmail.android.api.models.enumerations.MessageEncryption;
import ch.protonmail.android.api.segments.event.AlarmReceiver;
import ch.protonmail.android.attachments.DownloadEmbeddedAttachmentsWorker;
import ch.protonmail.android.attachments.ImportAttachmentsWorker;
import ch.protonmail.android.compose.ComposeMessageViewModel;
import ch.protonmail.android.compose.presentation.ui.MessageRecipientArrayAdapter;
import ch.protonmail.android.compose.presentation.ui.ComposeMessageKotlinActivity;
@ -130,7 +121,6 @@ import ch.protonmail.android.events.DownloadEmbeddedImagesEvent;
import ch.protonmail.android.events.FetchDraftDetailEvent;
import ch.protonmail.android.events.FetchMessageDetailEvent;
import ch.protonmail.android.events.MessageSavedEvent;
import ch.protonmail.android.events.PostImportAttachmentEvent;
import ch.protonmail.android.events.PostLoadContactsEvent;
import ch.protonmail.android.events.ResignContactEvent;
import ch.protonmail.android.events.Status;
@ -155,7 +145,6 @@ import ch.protonmail.android.utils.ServerTime;
import ch.protonmail.android.utils.UiUtil;
import ch.protonmail.android.utils.crypto.TextDecryptionResult;
import ch.protonmail.android.utils.extensions.CommonExtensionsKt;
import ch.protonmail.android.utils.extensions.SerializationUtils;
import ch.protonmail.android.utils.extensions.TextExtensions;
import ch.protonmail.android.utils.ui.dialogs.DialogUtils;
import ch.protonmail.android.utils.ui.locks.ComposerLockIcon;
@ -164,15 +153,9 @@ import ch.protonmail.android.views.MessagePasswordButton;
import ch.protonmail.android.views.MessageRecipientView;
import ch.protonmail.android.views.PMWebViewClient;
import dagger.hilt.android.AndroidEntryPoint;
import kotlin.Unit;
import kotlin.collections.CollectionsKt;
import kotlin.jvm.functions.Function0;
import me.proton.core.accountmanager.domain.AccountManager;
import timber.log.Timber;
import static ch.protonmail.android.attachments.ImportAttachmentsWorkerKt.KEY_INPUT_DATA_COMPOSER_INSTANCE_ID;
import static ch.protonmail.android.attachments.ImportAttachmentsWorkerKt.KEY_INPUT_DATA_FILE_URIS_STRING_ARRAY;
import static ch.protonmail.android.settings.pin.ValidatePinActivityKt.EXTRA_ATTACHMENT_IMPORT_EVENT;
import static ch.protonmail.android.settings.pin.ValidatePinActivityKt.EXTRA_DRAFT_DETAILS_EVENT;
import static ch.protonmail.android.settings.pin.ValidatePinActivityKt.EXTRA_MESSAGE_DETAIL_EVENT;
@ -207,7 +190,6 @@ public class ComposeMessageActivity
public static final String EXTRA_REPLY_FROM_GCM = "reply_from_gcm";
public static final String EXTRA_LOAD_IMAGES = "load_images";
public static final String EXTRA_LOAD_REMOTE_CONTENT = "load_remote_content";
private static final int REQUEST_CODE_ADD_ATTACHMENTS = 1;
private static final String STATE_ATTACHMENT_LIST = "attachment_list";
private static final String STATE_ADDITIONAL_ROWS_VISIBLE = "additional_rows_visible";
private static final String STATE_DIRTY = "dirty";
@ -260,8 +242,6 @@ public class ComposeMessageActivity
@Inject
DownloadEmbeddedAttachmentsWorker.Enqueuer attachmentsWorker;
String composerInstanceId;
Menu menu;
@Override
@ -680,15 +660,6 @@ public class ComposeMessageActivity
addRecipientsToView(recipients, recipient);
}
@NonNull
private Function0<Unit> onConnectivityCheckRetry() {
return () -> {
networkSnackBarUtil.getCheckingConnectionSnackBar(mSnackLayout, null).show();
composeMessageViewModel.checkConnectivityDelayed();
return null;
};
}
private void onConnectivityEvent(Constants.ConnectionState connectivity) {
Timber.v("onConnectivityEvent hasConnectivity:%s DoHOngoing:%s", connectivity.name(), isDohOngoing);
if (!isDohOngoing) {
@ -792,34 +763,7 @@ public class ComposeMessageActivity
private void handleSendFileUri(Uri uri) {
if (uri != null) {
composerInstanceId = UUID.randomUUID().toString();
Data data = new Data.Builder()
.putStringArray(KEY_INPUT_DATA_FILE_URIS_STRING_ARRAY, new String[]{uri.toString()})
.putString(KEY_INPUT_DATA_COMPOSER_INSTANCE_ID, composerInstanceId)
.build();
OneTimeWorkRequest importAttachmentsWork = new OneTimeWorkRequest.Builder(ImportAttachmentsWorker.class)
.setInputData(data)
.build();
WorkManager workManager = WorkManager.getInstance();
workManager.enqueue(importAttachmentsWork);
// Observe the Worker with a LiveData, because result will be received when the
// Activity will back in foreground, since an EventBut event would be lost while in
// Background
workManager.getWorkInfoByIdLiveData(importAttachmentsWork.getId())
.observe(this, workInfo -> {
if (workInfo != null && workInfo.getState() == WorkInfo.State.SUCCEEDED) {
// Get the Event from Worker
String json = workInfo.getOutputData().getString(composerInstanceId);
if (json != null) {
PostImportAttachmentEvent event = SerializationUtils.deserialize(
json, PostImportAttachmentEvent.class
);
onPostImportAttachmentEvent(event);
}
}
});
composeMessageViewModel.addAttachment(uri, false);
composeMessageViewModel.setIsDirty(true);
}
}
@ -827,42 +771,7 @@ public class ComposeMessageActivity
void handleSendMultipleFiles(Intent intent) {
ArrayList<Uri> fileUris = intent.getParcelableArrayListExtra(Intent.EXTRA_STREAM);
if (fileUris != null) {
String[] uriStrings = new String[fileUris.size()];
for (int i = 0; i < fileUris.size(); i++) {
uriStrings[i] = fileUris.get(i).toString();
}
composerInstanceId = UUID.randomUUID().toString();
Data data = new Data.Builder()
.putStringArray(KEY_INPUT_DATA_FILE_URIS_STRING_ARRAY, uriStrings)
.putString(KEY_INPUT_DATA_COMPOSER_INSTANCE_ID, composerInstanceId)
.build();
OneTimeWorkRequest importAttachmentsWork = new OneTimeWorkRequest.Builder(ImportAttachmentsWorker.class)
.setInputData(data)
.build();
WorkManager.getInstance().enqueue(importAttachmentsWork);
WorkManager.getInstance().getWorkInfoByIdLiveData(importAttachmentsWork.getId()).observe(this, workInfo -> {
if (workInfo != null) {
Log.d("PMTAG", "ImportAttachmentsWorker workInfo = " + workInfo.getState());
}
});
}
}
@Subscribe
public void onPostImportAttachmentEvent(PostImportAttachmentEvent event) {
if (event == null || event.composerInstanceId == null || !event.composerInstanceId.equals(composerInstanceId)) {
return;
}
List<LocalAttachment> attachmentsList = composeMessageViewModel.getMessageDataResult().getAttachmentList();
boolean alreadyAdded = CollectionsKt.firstOrNull(attachmentsList, localAttachment ->
localAttachment.getUri().toString().equals(event.uri)
) != null;
if (!alreadyAdded) {
composeMessageViewModel.setIsDirty(true);
attachmentsList.add(new LocalAttachment(Uri.parse(event.uri), event.displayName, event.size, event.mimeType));
renderViews();
composeMessageViewModel.addAttachments(fileUris, false);
}
}
@ -1286,25 +1195,9 @@ public class ComposeMessageActivity
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == REQUEST_CODE_ADD_ATTACHMENTS && resultCode == RESULT_OK) {
Timber.d("ComposeMessageAct.onActivityResult Received add attachment response with result OK");
askForPermission = false;
ArrayList<LocalAttachment> resultAttachmentList = data.getParcelableArrayListExtra(AddAttachmentsActivity.EXTRA_ATTACHMENT_LIST);
ArrayList<LocalAttachment> listToSet = resultAttachmentList != null ? resultAttachmentList : new ArrayList<>();
composeMessageViewModel.setAttachmentList(listToSet);
composeMessageViewModel.setIsDirty(true);
String oldDraftId = composeMessageViewModel.getDraftId();
afterAttachmentsAdded();
composeMessageViewModel.setIsDirty(true);
} else if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_VALIDATE_PIN) {
if (resultCode == RESULT_OK && requestCode == REQUEST_CODE_VALIDATE_PIN) {
// region pin results
if (data.hasExtra(EXTRA_ATTACHMENT_IMPORT_EVENT)) {
Object attachmentExtra = data.getSerializableExtra(EXTRA_ATTACHMENT_IMPORT_EVENT);
if (attachmentExtra instanceof PostImportAttachmentEvent) {
onPostImportAttachmentEvent((PostImportAttachmentEvent) attachmentExtra);
}
composeMessageViewModel.setBeforeSaveDraft(false, messageBodyEditText.getText().toString());
} else if (data.hasExtra(EXTRA_MESSAGE_DETAIL_EVENT) || data.hasExtra(EXTRA_DRAFT_DETAILS_EVENT)) {
if (data.hasExtra(EXTRA_MESSAGE_DETAIL_EVENT) || data.hasExtra(EXTRA_DRAFT_DETAILS_EVENT)) {
FetchMessageDetailEvent messageDetailEvent = (FetchMessageDetailEvent) data.getSerializableExtra(EXTRA_MESSAGE_DETAIL_EVENT);
FetchDraftDetailEvent draftDetailEvent = (FetchDraftDetailEvent) data.getSerializableExtra(EXTRA_DRAFT_DETAILS_EVENT);
if (messageDetailEvent != null) {
@ -1316,18 +1209,11 @@ public class ComposeMessageActivity
}
toRecipientView.requestFocus();
UiUtil.toggleKeyboard(this, toRecipientView);
super.onActivityResult(requestCode, resultCode, data);
// endregion
} else {
Timber.w("ComposeMessageAct.onActivityResult Received result not handled", requestCode, resultCode);
super.onActivityResult(requestCode, resultCode, data);
}
}
private void afterAttachmentsAdded() {
composeMessageViewModel.setBeforeSaveDraft(false, messageBodyEditText.getText().toString());
composeMessageViewModel.setIsDirty(true);
renderViews();
super.onActivityResult(requestCode, resultCode, data);
}
@Subscribe
@ -1813,16 +1699,6 @@ public class ComposeMessageActivity
}
}
private class ParentViewScrollListener implements View.OnTouchListener {
@Override
public boolean onTouch(View v, MotionEvent event) {
messageBodyEditText.setFocusableInTouchMode(true);
messageBodyEditText.requestFocus();
return false;
}
}
private class RespondInlineButtonClickListener implements View.OnClickListener {
@Override
public void onClick(View v) {

View File

@ -1,147 +0,0 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail 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.
*
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.adapters
import android.content.Context
import android.content.Intent
import android.text.format.Formatter
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ImageButton
import android.widget.TextView
import androidx.core.content.FileProvider
import androidx.work.WorkManager
import ch.protonmail.android.R
import ch.protonmail.android.data.local.model.LocalAttachment
import ch.protonmail.android.worker.DeleteAttachmentWorker
import java.io.File
import java.util.ArrayList
import java.util.Comparator
class AttachmentListAdapter(
context: Context,
attachmentsList: List<LocalAttachment>?,
private var numberOfEmbeddedImages: Int,
private val workManager: WorkManager
) : ArrayAdapter<LocalAttachment>(context, 0, attachmentsList ?: emptyList()) {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private val listener: IAttachmentListener
private val attachmentSortComparator = Comparator<LocalAttachment> { lhs, rhs ->
val embeddedImageCompare = java.lang.Boolean.valueOf(lhs.isEmbeddedImage).compareTo(rhs.isEmbeddedImage)
if (embeddedImageCompare != 0) {
embeddedImageCompare
} else lhs.displayName.compareTo(rhs.displayName, ignoreCase = true)
}
private var attachmentsList = attachmentsList?.sortedWith(attachmentSortComparator) ?: emptyList()
val data: ArrayList<LocalAttachment>
get() = ArrayList(attachmentsList)
init {
listener = context as IAttachmentListener
sort(attachmentSortComparator)
notifyDataSetChanged()
}
fun updateData(attachmentList: ArrayList<LocalAttachment>, numEmbeddedImages: Int) {
clear()
numberOfEmbeddedImages = numEmbeddedImages
attachmentsList = attachmentList.sortedWith(attachmentSortComparator)
addAll(attachmentsList)
sort(attachmentSortComparator)
notifyDataSetChanged()
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var view = convertView
val attachment = getItem(position)
var previousAttachment: LocalAttachment? = null
if (position > 0) {
previousAttachment = getItem(position - 1)
}
view = if (attachment!!.isEmbeddedImage && (position == 0 || !previousAttachment!!.isEmbeddedImage)) {
inflater.inflate(R.layout.attachment_inline_first_list_item, parent, false)
} else {
inflater.inflate(R.layout.attachment_list_item, parent, false)
}
val attachmentName = view.findViewById<TextView>(R.id.attachment_name)
val attachmentSize = view.findViewById<TextView>(R.id.attachment_size)
val embeddedImageHeader = view.findViewById<TextView>(R.id.num_embedded_images_attachments)
val embeddedImagePrefix = view.findViewById<TextView>(R.id.embedded_image_attachment)
val removeButton = view.findViewById<ImageButton>(R.id.remove)
if (embeddedImageHeader != null && attachment.isEmbeddedImage) {
embeddedImageHeader.visibility = View.VISIBLE
embeddedImageHeader.text = String.format(context.getString(R.string.inline_header), numberOfEmbeddedImages)
} else if (embeddedImageHeader != null) {
embeddedImageHeader.visibility = View.GONE
}
if (attachment.isEmbeddedImage) {
embeddedImagePrefix.visibility = View.VISIBLE
} else {
embeddedImagePrefix.visibility = View.GONE
}
attachmentName.text = attachment.displayName
attachmentSize.text = context.getString(R.string.attachment_size, Formatter.formatShortFileSize(context, attachment.size))
removeButton.setOnClickListener {
val isEmbedded = attachment.isEmbeddedImage
remove(attachment)
attachmentsList = attachmentsList.filterNot {
attachment.attachmentId.isNotEmpty() &&
it.attachmentId == attachment.attachmentId ||
attachment.attachmentId.isEmpty() &&
attachment.displayName == it.displayName
}
if (isEmbedded) {
numberOfEmbeddedImages -= 1
}
listener.onAttachmentDeleted(count, numberOfEmbeddedImages)
DeleteAttachmentWorker.Enqueuer(workManager).enqueue(attachment.attachmentId)
}
attachmentName.setOnClickListener {
if ("file" == attachment.uri.scheme) {
val localFileUri = FileProvider.getUriForFile(context, context.applicationContext.packageName + ".provider", File(attachment.uri.path))
val intent = Intent(Intent.ACTION_VIEW).setDataAndType(localFileUri, attachment.mimeType).addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
if (intent.resolveActivity(context.packageManager) != null) {
context.startActivity(intent)
}
}
}
return view
}
interface IAttachmentListener {
fun onAttachmentDeleted(remainingAttachments: Int, embeddedImagesCount: Int)
fun askStoragePermission()
}
}

View File

@ -1,84 +0,0 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail 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.
*
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.attachments
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import ch.protonmail.android.activities.AddAttachmentsActivity.EXTRA_DRAFT_ID
import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository
import ch.protonmail.android.core.NetworkConnectivityManager
import ch.protonmail.android.data.local.model.Message
import ch.protonmail.android.utils.MessageUtils
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch
import me.proton.core.util.kotlin.DispatcherProvider
import javax.inject.Inject
@HiltViewModel
class AttachmentsViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val dispatchers: DispatcherProvider,
private val messageDetailsRepository: MessageDetailsRepository,
private val networkConnectivityManager: NetworkConnectivityManager
) : ViewModel() {
val viewState: MutableLiveData<AttachmentsViewState> = MutableLiveData()
fun init() {
viewModelScope.launch(dispatchers.Io) {
val messageId = savedStateHandle.get<String>(EXTRA_DRAFT_ID) ?: return@launch
val message = messageDetailsRepository.findMessageById(messageId).first()
message?.let { existingMessage ->
val messageDbId = requireNotNull(existingMessage.dbId)
val messageFlow = messageDetailsRepository.findMessageByDbId(messageDbId)
if (!networkConnectivityManager.isInternetConnectionPossible()) {
viewState.postValue(AttachmentsViewState.MissingConnectivity)
}
messageFlow.collect { updatedMessage ->
if (updatedMessage == null) {
return@collect
}
if (!this.isActive) {
return@collect
}
if (draftCreationHappened(existingMessage, updatedMessage)) {
viewState.postValue(AttachmentsViewState.UpdateAttachments(updatedMessage.Attachments))
this.cancel()
}
}
}
}
}
private fun draftCreationHappened(existingMessage: Message, updatedMessage: Message) =
!isRemoteMessage(existingMessage) && isRemoteMessage(updatedMessage)
private fun isRemoteMessage(message: Message) = !MessageUtils.isLocalMessageId(message.messageId)
}

View File

@ -1,134 +0,0 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail 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.
*
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.attachments
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import android.provider.MediaStore
import android.provider.OpenableColumns
import android.webkit.MimeTypeMap
import androidx.work.Worker
import androidx.work.WorkerParameters
import androidx.work.workDataOf
import ch.protonmail.android.core.Constants
import ch.protonmail.android.events.PostImportAttachmentEvent
import ch.protonmail.android.events.PostImportAttachmentFailureEvent
import ch.protonmail.android.utils.AppUtil
import ch.protonmail.android.utils.Logger
import ch.protonmail.android.utils.extensions.serialize
import java.io.File
// region constants
const val KEY_INPUT_DATA_FILE_URIS_STRING_ARRAY = "KEY_INPUT_DATA_FILE_URIS_STRING_ARRAY"
const val KEY_INPUT_DATA_DELETE_ORIGINAL_FILE_BOOLEAN = "KEY_INPUT_DATA_DELETE_ORIGINAL_FILE_BOOLEAN"
const val KEY_INPUT_DATA_COMPOSER_INSTANCE_ID = "KEY_INPUT_DATA_COMPOSER_INSTANCE_ID"
// endregion
/**
* Represents one unit of work importing attachments to app's cache directory.
*
* InputData has to contain non-null values for:
* - fileUris
*
* Optionally:
* - deleteOriginalFile: if Uri's scheme is [ContentResolver.SCHEME_FILE], delete this file after import
*
* OutputData contains:
* TODO when we move from EventBus to observing Workers
*
* @see androidx.work.WorkManager
* @see androidx.work.Data
*/
class ImportAttachmentsWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
override fun doWork(): Result {
val fileUris = inputData.getStringArray(KEY_INPUT_DATA_FILE_URIS_STRING_ARRAY)?.mapNotNull { Uri.parse(it) } ?: return Result.failure()
val deleteOriginalFile = inputData.getBoolean(KEY_INPUT_DATA_DELETE_ORIGINAL_FILE_BOOLEAN, false)
val composerInstanceId = inputData.getString(KEY_INPUT_DATA_COMPOSER_INSTANCE_ID)
val postImportAttachmentEvents = mutableListOf<PostImportAttachmentEvent>()
val contentResolver = applicationContext.contentResolver
fileUris.filterNot { it.scheme == "file" && (it.path ?: "").contains(applicationContext.applicationInfo.dataDir) }.forEach { uri ->
try {
contentResolver.openInputStream(uri)?.let {
AppUtil.createTempFileFromInputStream(applicationContext, it)?.let { importedFile ->
var displayName = ""
var size: Long = 0
var mimeType: String? = ""
when (uri.scheme) {
ContentResolver.SCHEME_CONTENT -> {
mimeType = contentResolver.getType(uri)
val cursor = contentResolver.query(uri, null, null, null, null)
if (cursor != null && cursor.moveToFirst()) {
displayName = try {
cursor.getString(cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME))
} catch (e: java.lang.Exception) {
cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA)).split("/")[cursor.getString(cursor.getColumnIndex(MediaStore.Audio.Media.DATA)).split("/").size - 1]
}
size = cursor.getLong(cursor.getColumnIndex(OpenableColumns.SIZE))
size = if (size > 0) size else importedFile.length()
}
if (cursor != null && !cursor.isClosed) {
cursor.close()
}
}
ContentResolver.SCHEME_FILE -> {
val file = File(uri.path)
displayName = file.name
size = file.length()
val extension = MimeTypeMap.getFileExtensionFromUrl(file.name)
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
if (deleteOriginalFile) {
file.delete()
}
}
}
postImportAttachmentEvents.add(PostImportAttachmentEvent(Uri.fromFile(importedFile), displayName, size, mimeType ?: Constants.MIME_TYPE_UNKNOWN_FILE, composerInstanceId))
}
}
} catch (e: Exception) {
Logger.doLogException(e)
}
}
if (postImportAttachmentEvents.isEmpty()) {
AppUtil.postEventOnUi(PostImportAttachmentFailureEvent())
} else {
postImportAttachmentEvents.forEach {
AppUtil.postEventOnUi(it)
}
}
// Mapping the import events by their `composerInstanceId`
val outputEventPairs = postImportAttachmentEvents
.map { (it.composerInstanceId ?: "") to it.serialize() }
return Result.success(workDataOf(*outputEventPairs.toTypedArray()))
}
}

View File

@ -131,7 +131,6 @@ class ComposeMessageViewModel @Inject constructor(
private val _deleteResult: MutableLiveData<Event<PostResult>> = MutableLiveData()
private val _loadingDraftResult: MutableLiveData<Message> = MutableLiveData()
private val _messageResultError: MutableLiveData<Event<PostResult>> = MutableLiveData()
private val _openAttachmentsScreenResult: MutableLiveData<List<LocalAttachment>> = MutableLiveData()
private val _buildingMessageCompleted: MutableLiveData<Event<Message>> = MutableLiveData()
private val _dbIdWatcher: MutableLiveData<Long> = MutableLiveData()
private val _fetchMessageDetailsEvent: MutableLiveData<Event<MessageBuilderData>> = MutableLiveData()
@ -674,35 +673,6 @@ class ComposeMessageViewModel @Inject constructor(
)
}
fun openAttachmentsScreen() {
val oldList = _messageDataResult.attachmentList
viewModelScope.launch {
if (draftId.isNotEmpty()) {
val message = composeMessageRepository.findMessage(draftId)
if (message != null) {
val messageAttachments =
composeMessageRepository.getAttachments(message, dispatchers.Io)
if (oldList.size <= messageAttachments.size) {
val attachments = LocalAttachment.createLocalAttachmentList(messageAttachments)
_messageDataResult = MessageBuilderData.Builder()
.fromOld(_messageDataResult)
.attachmentList(ArrayList(attachments))
.build()
_openAttachmentsScreenResult.postValue(attachments)
return@launch
}
}
}
_messageDataResult = MessageBuilderData.Builder()
.fromOld(_messageDataResult)
.attachmentList(ArrayList(oldList))
.build()
_openAttachmentsScreenResult.postValue(oldList)
}
}
fun deleteDraft() {
viewModelScope.launch {
if (_draftId.get().isNotEmpty()) {
@ -1304,6 +1274,10 @@ class ComposeMessageViewModel @Inject constructor(
}
}
fun addAttachment(uri: Uri, deleteOriginalFile: Boolean) {
addAttachments(listOf(uri), deleteOriginalFile)
}
fun removeAttachment(uri: Uri) {
viewModelScope.launch {
imporedAttachments.removeIf { it.originalFileUri == uri }

View File

@ -1,43 +0,0 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail 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.
*
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.events;
import android.net.Uri;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.Serializable;
public class PostImportAttachmentEvent implements Serializable {
public final String uri;
public final String displayName;
public final long size;
public final String mimeType;
@Nullable public final String composerInstanceId;
public PostImportAttachmentEvent(@NonNull Uri uri, @NonNull String displayName, long size, @NonNull String mimeType, String composerInstanceId) {
this.uri = uri.toString();
this.displayName = displayName;
this.size = size;
this.mimeType = mimeType;
this.composerInstanceId = composerInstanceId;
}
}

View File

@ -31,7 +31,6 @@ import ch.protonmail.android.core.ProtonMailApplication
import ch.protonmail.android.events.FetchDraftDetailEvent
import ch.protonmail.android.events.FetchMessageDetailEvent
import ch.protonmail.android.events.MessageCountsEvent
import ch.protonmail.android.events.PostImportAttachmentEvent
import ch.protonmail.android.settings.pin.viewmodel.PinFragmentViewModel
import ch.protonmail.android.utils.AppUtil
import ch.protonmail.android.utils.extensions.showToast
@ -43,22 +42,16 @@ import java.util.concurrent.Executors
// region constants
const val EXTRA_PIN_VALID = "extra_pin_valid"
const val EXTRA_FRAGMENT_TITLE = "extra_title"
const val EXTRA_ATTACHMENT_IMPORT_EVENT = "extra_attachment_import_event"
const val EXTRA_TOTAL_COUNT_EVENT = "extra_total_count_event"
const val EXTRA_MESSAGE_DETAIL_EVENT = "extra_message_details_event"
const val EXTRA_DRAFT_DETAILS_EVENT = "extra_draft_details_event"
// endregion
/*
* Created by dkadrikj on 3/27/16.
*/
class ValidatePinActivity : BaseActivity(),
PinFragmentViewModel.IPinCreationListener,
SecureEditText.ISecurePINListener,
PinFragmentViewModel.ReopenFingerprintDialogListener {
private var importAttachmentEvent: PostImportAttachmentEvent? = null
private var messageCountsEvent: MessageCountsEvent? = null
private var messageDetailEvent: FetchMessageDetailEvent? = null
private var draftDetailEvent: FetchDraftDetailEvent? = null
@ -101,11 +94,6 @@ class ValidatePinActivity : BaseActivity(),
}
// region subscription events
@Subscribe
fun onPostImportAttachmentEvent(event: PostImportAttachmentEvent) {
importAttachmentEvent = event
}
@Subscribe
fun onMessageCountsEvent(event: MessageCountsEvent) {
messageCountsEvent = event
@ -217,9 +205,6 @@ class ValidatePinActivity : BaseActivity(),
private fun buildIntent(): Intent {
return Intent().apply {
putExtra(EXTRA_PIN_VALID, true)
if (importAttachmentEvent != null) {
putExtra(EXTRA_ATTACHMENT_IMPORT_EVENT, importAttachmentEvent)
}
if (messageCountsEvent != null) {
putExtra(EXTRA_TOTAL_COUNT_EVENT, messageCountsEvent)
}

View File

@ -1,141 +0,0 @@
<!--
Copyright (c) 2020 Proton Technologies AG
This file is part of ProtonMail.
ProtonMail 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.
ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:fitsSystemWindows="true"
tools:context=".activities.AddAttachmentsActivity">
<include
android:id="@+id/toolbar"
layout="@layout/toolbar" />
<FrameLayout
android:id="@+id/layout_no_connectivity_info"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<View
android:layout_width="match_parent"
android:layout_height="@dimen/shadow_height"
android:layout_below="@id/toolbar"
android:background="@drawable/actionbar_shadow" />
<ch.protonmail.android.views.CustomFontTextView
android:id="@+id/no_attachments"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_gravity="center"
android:layout_marginBottom="@dimen/spacing"
android:gravity="center"
android:text="@string/no_attachments"
android:textColor="@color/new_purple"
android:textSize="@dimen/h2"
android:visibility="gone"
app:fontName="Roboto-Thin.ttf" />
<ch.protonmail.android.views.CustomFontTextView
android:id="@+id/num_attachments"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/toolbar"
android:layout_centerHorizontal="true"
android:layout_gravity="center"
android:layout_marginTop="@dimen/fields_default_space"
android:gravity="center"
android:textColor="@color/new_purple"
android:textSize="@dimen/h2"
android:visibility="gone"
app:fontName="Roboto-Thin.ttf" />
<ListView
android:id="@+id/attachment_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/num_attachments"
android:divider="@color/fog_gray"
android:dividerHeight="@dimen/divider_height" />
<RelativeLayout
android:id="@+id/progress_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar"
android:background="@color/white"
android:visibility="gone">
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_gravity="center" />
<ch.protonmail.android.views.CustomFontTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="@dimen/fields_default_space"
android:gravity="center"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/fields_default_space_small"
android:textColor="@color/new_purple"
android:textSize="@dimen/h2"
android:layout_below="@id/progress_bar"
android:text="@string/sync_attachments"
app:fontName="Roboto-Thin.ttf" />
</RelativeLayout>
<RelativeLayout
android:id="@+id/processing_attachment_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar"
android:background="@color/white"
android:visibility="gone">
<ProgressBar
android:id="@+id/progress_bar_processing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layout_gravity="center" />
<ch.protonmail.android.views.CustomFontTextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginBottom="@dimen/fields_default_space"
android:gravity="center"
android:layout_centerHorizontal="true"
android:layout_marginTop="@dimen/fields_default_space_small"
android:textColor="@color/new_purple"
android:textSize="@dimen/h2"
android:layout_below="@id/progress_bar_processing"
android:text="@string/processing_attachment"
app:fontName="Roboto-Thin.ttf" />
</RelativeLayout>
</RelativeLayout>

View File

@ -1,223 +0,0 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail 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.
*
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.attachments
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.lifecycle.Observer
import androidx.lifecycle.SavedStateHandle
import ch.protonmail.android.activities.AddAttachmentsActivity
import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository
import ch.protonmail.android.attachments.AttachmentsViewState.MissingConnectivity
import ch.protonmail.android.attachments.AttachmentsViewState.UpdateAttachments
import ch.protonmail.android.core.NetworkConnectivityManager
import ch.protonmail.android.data.local.model.Attachment
import ch.protonmail.android.data.local.model.Message
import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.coVerifySequence
import io.mockk.every
import io.mockk.impl.annotations.RelaxedMockK
import io.mockk.mockk
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runBlockingTest
import me.proton.core.test.kotlin.CoroutinesTest
import org.junit.Before
import org.junit.Rule
import kotlin.test.Test
class AttachmentsViewModelTest : CoroutinesTest {
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
@RelaxedMockK
lateinit var messageRepository: MessageDetailsRepository
@RelaxedMockK
lateinit var networkConnectivityManager: NetworkConnectivityManager
@RelaxedMockK
private lateinit var mockObserver: Observer<AttachmentsViewState>
@RelaxedMockK
private lateinit var savedState: SavedStateHandle
private lateinit var viewModel: AttachmentsViewModel
@Before
fun setUp() {
MockKAnnotations.init(this)
viewModel = AttachmentsViewModel(
savedState,
dispatchers,
messageRepository,
networkConnectivityManager
)
viewModel.viewState.observeForever(mockObserver)
every { networkConnectivityManager.isInternetConnectionPossible() } returns true
}
@Test
fun initFindsMessageInDatabase() = runBlockingTest {
val messageId = "draftId3214"
coEvery { messageRepository.findMessageById(messageId) } returns mockk(relaxed = true)
every { savedState.get<String>(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId
viewModel.init()
coVerify { messageRepository.findMessageById(messageId) }
}
@Test
fun initObservesMessageRepositoryByMessageDbIdWhenGivenMessageIdIsFound() = runBlockingTest {
val messageId = "draftId234"
val messageDbId = 124L
val message = Message(messageId = messageId).apply { dbId = messageDbId }
coEvery { messageRepository.findMessageById(messageId) } returns flowOf(message)
every { savedState.get<String>(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId
viewModel.init()
coVerify { messageRepository.findMessageByDbId(messageDbId) }
}
@Test
fun initUpdatesViewStateWhenMessageIsUpdatedInDbAsAResultOfDraftCreationCompleting() = runBlockingTest {
val messageId = "91bbb263-2bf2-43dd-a079-233a305e69df"
val messageDbId = 124L
val message = Message(messageId = messageId).apply { dbId = messageDbId }
val updatedMessageAttachments = listOf(Attachment(attachmentId = "updatedAttId"))
val remoteMessage = message.copy(messageId = "Remote message id").apply {
Attachments = updatedMessageAttachments
}
coEvery { messageRepository.findMessageById(messageId) } returns flowOf(message)
coEvery { messageRepository.findMessageByDbId(messageDbId) } returns flowOf(remoteMessage)
every { savedState.get<String>(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId
viewModel.init()
val expectedState = UpdateAttachments(updatedMessageAttachments)
coVerify { mockObserver.onChanged(expectedState) }
}
@Test
fun initStopsListeningForMessageUpdatesWhenDraftCreationCompletedEventWasReceived() = runBlockingTest {
val messageId = "91bbb263-2bf2-43dd-a079-233a305e69df"
val messageDbId = 124L
val message = Message(messageId = messageId).apply { dbId = messageDbId }
val updatedMessageAttachments = listOf(Attachment(attachmentId = "updatedAttId"))
val remoteMessage = message.copy(messageId = "Remote message id").apply {
Attachments = updatedMessageAttachments
}
val updatedDraftMessage = remoteMessage.copy(messageId = "Updated remote messageID")
coEvery { messageRepository.findMessageById(messageId) } returns flowOf(message)
coEvery { messageRepository.findMessageByDbId(messageDbId) } returns flowOf(
remoteMessage, updatedDraftMessage
)
every { savedState.get<String>(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId
viewModel.init()
val expectedState = UpdateAttachments(updatedMessageAttachments)
coVerifySequence { mockObserver.onChanged(expectedState) }
}
@Test
fun initDoesNotUpdateViewStateWhenMessageIsUpdatedInDbAsAResultOfDraftUpdateCompleting() = runBlockingTest {
val messageId = "remote-draft-message ID"
val messageDbId = 2384L
val message = Message().apply { dbId = messageDbId }
val updatedMessageAttachments = listOf(Attachment(attachmentId = "updatedAttId"))
val remoteMessage = message.copy(messageId = "Updated Draft Remote message id").apply {
Attachments = updatedMessageAttachments
}
coEvery { messageRepository.findMessageById(messageId) } returns flowOf(message)
coEvery { messageRepository.findMessageByDbId(messageDbId) } returns flowOf(remoteMessage)
every { savedState.get<String>(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId
viewModel.init()
coVerify(exactly = 0) { mockObserver.onChanged(any()) }
}
@Test
fun initDoesNotUpdateViewStateWhenMessageThatWasUpdatedInDbIsNotARemoteMessage() = runBlockingTest {
val messageId = "91bbb263-2bf2-43dd-a079-233a305e69df"
val messageDbId = 124L
val message = Message(messageId = messageId).apply { dbId = messageDbId }
val updatedLocalMessage = message.copy(messageId = "82ccc723-2bf2-43dd-f834-233a305e69df")
coEvery { messageRepository.findMessageById(messageId) } returns flowOf(message)
coEvery { messageRepository.findMessageByDbId(messageDbId) } returns flowOf(updatedLocalMessage)
every { savedState.get<String>(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId
viewModel.init()
coVerify(exactly = 0) { mockObserver.onChanged(any()) }
}
@Test
fun initPostsOfflineViewStateWhenThereIsNoConnection() = runBlockingTest {
val messageId = "91bbb263-2bf2-43dd-a079-233a305e69df"
val messageDbId = 124L
val message = Message(messageId = messageId).apply { dbId = messageDbId }
coEvery { messageRepository.findMessageById(messageId) } returns flowOf(message)
coEvery { messageRepository.findMessageByDbId(messageDbId) } returns flowOf()
every { networkConnectivityManager.isInternetConnectionPossible() } returns false
every { savedState.get<String>(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId
viewModel.init()
coVerifySequence { mockObserver.onChanged(MissingConnectivity) }
}
@Test
fun initLogsWarningAndStopsExecutionIfDraftIdWasNotPassed() = runBlockingTest {
every { savedState.get<String>(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns null
viewModel.init()
// test warning is logged here
coVerify(exactly = 0) { messageRepository.findMessageByDbId(any()) }
coVerify(exactly = 0) { mockObserver.onChanged(any()) }
}
@Test
fun initIgnoresAnyNullMessagesReturnedByDatabaseFlow() = runBlockingTest {
val messageId = "91bbb263-2bf2-43dd-a079-233a305e79df"
val messageDbId = 113L
val message = Message(messageId = messageId).apply { dbId = messageDbId }
val updatedMessageAttachments = listOf(Attachment(attachmentId = "updatedAttId"))
val remoteMessage = message.copy(messageId = "Remote message id").apply {
Attachments = updatedMessageAttachments
}
coEvery { messageRepository.findMessageById(messageId) } returns flowOf(message)
coEvery { messageRepository.findMessageByDbId(messageDbId) } returns flowOf(
remoteMessage, null
)
every { savedState.get<String>(AddAttachmentsActivity.EXTRA_DRAFT_ID) } returns messageId
viewModel.init()
val expectedState = UpdateAttachments(updatedMessageAttachments)
coVerifySequence { mockObserver.onChanged(expectedState) }
}
}