diff --git a/app-k9mail/badging/fossRelease-badging.txt b/app-k9mail/badging/fossRelease-badging.txt index cb9192fab86..a2f2128f7b7 100644 --- a/app-k9mail/badging/fossRelease-badging.txt +++ b/app-k9mail/badging/fossRelease-badging.txt @@ -14,6 +14,7 @@ uses-permission: name='android.permission.FOREGROUND_SERVICE' uses-permission: name='android.permission.FOREGROUND_SERVICE_DATA_SYNC' maxSdkVersion='33' uses-permission: name='android.permission.FOREGROUND_SERVICE_SPECIAL_USE' uses-permission: name='android.permission.SCHEDULE_EXACT_ALARM' +uses-permission: name='android.permission.CAMERA' uses-permission: name='android.permission.USE_BIOMETRIC' uses-permission: name='android.permission.USE_FINGERPRINT' uses-permission: name='com.fsck.k9.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION' @@ -84,6 +85,7 @@ property: name='android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE' value='This servic uses-library-not-required:'androidx.window.extensions' uses-library-not-required:'androidx.window.sidecar' feature-group: label='' + uses-feature-not-required: name='android.hardware.camera' uses-feature-not-required: name='android.hardware.touchscreen' provides-component:'app-widget' main diff --git a/app-k9mail/badging/fullRelease-badging.txt b/app-k9mail/badging/fullRelease-badging.txt index 999834cd6db..8d12d4f38de 100644 --- a/app-k9mail/badging/fullRelease-badging.txt +++ b/app-k9mail/badging/fullRelease-badging.txt @@ -14,6 +14,7 @@ uses-permission: name='android.permission.FOREGROUND_SERVICE' uses-permission: name='android.permission.FOREGROUND_SERVICE_DATA_SYNC' maxSdkVersion='33' uses-permission: name='android.permission.FOREGROUND_SERVICE_SPECIAL_USE' uses-permission: name='android.permission.SCHEDULE_EXACT_ALARM' +uses-permission: name='android.permission.CAMERA' uses-permission: name='android.permission.USE_BIOMETRIC' uses-permission: name='android.permission.USE_FINGERPRINT' uses-permission: name='com.android.vending.BILLING' @@ -85,6 +86,7 @@ property: name='android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE' value='This servic uses-library-not-required:'androidx.window.extensions' uses-library-not-required:'androidx.window.sidecar' feature-group: label='' + uses-feature-not-required: name='android.hardware.camera' uses-feature-not-required: name='android.hardware.touchscreen' provides-component:'app-widget' main diff --git a/core/android/common/src/main/AndroidManifest.xml b/core/android/common/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..bdf29cb63ef --- /dev/null +++ b/core/android/common/src/main/AndroidManifest.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + diff --git a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/CoreCommonAndroidModule.kt b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/CoreCommonAndroidModule.kt index 5a0b5994759..56bf2de5c47 100644 --- a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/CoreCommonAndroidModule.kt +++ b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/CoreCommonAndroidModule.kt @@ -1,5 +1,6 @@ package app.k9mail.core.android.common +import app.k9mail.core.android.common.camera.cameraModule import app.k9mail.core.android.common.contact.contactModule import app.k9mail.core.common.coreCommonModule import org.koin.core.module.Module @@ -9,4 +10,6 @@ val coreCommonAndroidModule: Module = module { includes(coreCommonModule) includes(contactModule) + + includes(cameraModule) } diff --git a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/camera/CameraCaptureHandler.kt b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/camera/CameraCaptureHandler.kt new file mode 100644 index 00000000000..257747aed78 --- /dev/null +++ b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/camera/CameraCaptureHandler.kt @@ -0,0 +1,57 @@ +package app.k9mail.core.android.common.camera + +import android.Manifest.permission +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.net.Uri +import android.provider.MediaStore +import androidx.core.app.ActivityCompat +import androidx.core.app.ActivityCompat.startActivityForResult +import androidx.core.content.ContextCompat +import app.k9mail.core.android.common.camera.io.CaptureImageFileWriter + +class CameraCaptureHandler( + private val captureImageFileWriter: CaptureImageFileWriter, +) { + + private lateinit var capturedImageUri: Uri + + companion object { + const val REQUEST_IMAGE_CAPTURE: Int = 6 + const val CAMERA_PERMISSION_REQUEST_CODE: Int = 100 + } + + fun getCapturedImageUri(): Uri { + if (::capturedImageUri.isInitialized) { + return capturedImageUri + } else { + throw UninitializedPropertyAccessException("Image Uri not initialized") + } + } + + fun canLaunchCamera(context: Context) = + context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY) + + fun openCamera(activity: Activity) { + val intent = Intent(MediaStore.ACTION_IMAGE_CAPTURE) + capturedImageUri = captureImageFileWriter.getFileUri() + intent.putExtra(MediaStore.EXTRA_OUTPUT, capturedImageUri) + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + startActivityForResult(activity, intent, REQUEST_IMAGE_CAPTURE, null) + } + + fun requestCameraPermission(activity: Activity) { + ActivityCompat.requestPermissions( + activity, + arrayOf(permission.CAMERA), + CAMERA_PERMISSION_REQUEST_CODE, + ) + } + + fun hasCameraPermission(context: Context): Boolean { + val hasPermission = ContextCompat.checkSelfPermission(context, permission.CAMERA) + return hasPermission == PackageManager.PERMISSION_GRANTED + } +} diff --git a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/camera/CameraKoinModule.kt b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/camera/CameraKoinModule.kt new file mode 100644 index 00000000000..8d4996618be --- /dev/null +++ b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/camera/CameraKoinModule.kt @@ -0,0 +1,13 @@ +package app.k9mail.core.android.common.camera + +import app.k9mail.core.android.common.camera.io.CaptureImageFileWriter +import org.koin.dsl.module + +internal val cameraModule = module { + single { CaptureImageFileWriter(context = get()) } + single { + CameraCaptureHandler( + captureImageFileWriter = get(), + ) + } +} diff --git a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/camera/io/CaptureImageFileWriter.kt b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/camera/io/CaptureImageFileWriter.kt new file mode 100644 index 00000000000..72ab8fa621e --- /dev/null +++ b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/camera/io/CaptureImageFileWriter.kt @@ -0,0 +1,31 @@ +package app.k9mail.core.android.common.camera.io + +import android.content.Context +import android.net.Uri +import androidx.core.content.FileProvider +import java.io.File + +class CaptureImageFileWriter(private val context: Context) { + + fun getFileUri(): Uri { + val file = getCaptureImageFile() + return FileProvider.getUriForFile(context, "${context.packageName}.activity", file) + } + + private fun getCaptureImageFile(): File { + val fileName = "IMG_${System.currentTimeMillis()}$FILE_EXT" + return File(getDirectory(), fileName) + } + + private fun getDirectory(): File { + val directory = File(context.cacheDir, DIRECTORY_NAME) + directory.mkdirs() + + return directory + } + + companion object { + private const val FILE_EXT = ".jpg" + private const val DIRECTORY_NAME = "captureImage" + } +} diff --git a/core/android/common/src/main/kotlin/app/k9mail/core/android/common/camera/provider/CaptureImageFileProvider.kt b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/camera/provider/CaptureImageFileProvider.kt new file mode 100644 index 00000000000..06e8e3ad612 --- /dev/null +++ b/core/android/common/src/main/kotlin/app/k9mail/core/android/common/camera/provider/CaptureImageFileProvider.kt @@ -0,0 +1,5 @@ +package app.k9mail.core.android.common.camera.provider + +import androidx.core.content.FileProvider + +class CaptureImageFileProvider : FileProvider() diff --git a/core/android/common/src/main/res/xml/capture_image_file_provider_paths.xml b/core/android/common/src/main/res/xml/capture_image_file_provider_paths.xml new file mode 100644 index 00000000000..aa70953d5f4 --- /dev/null +++ b/core/android/common/src/main/res/xml/capture_image_file_provider_paths.xml @@ -0,0 +1,6 @@ + + + diff --git a/legacy/ui/legacy/build.gradle.kts b/legacy/ui/legacy/build.gradle.kts index 955e71ff9b1..bf07b41df4a 100644 --- a/legacy/ui/legacy/build.gradle.kts +++ b/legacy/ui/legacy/build.gradle.kts @@ -17,6 +17,7 @@ dependencies { implementation(projects.core.featureflags) implementation(projects.core.ui.theme.api) implementation(projects.feature.launcher) + implementation(projects.core.common) implementation(projects.feature.navigation.drawer.api) implementation(projects.feature.navigation.drawer.dropdown) implementation(projects.feature.navigation.drawer.siderail) diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java index 60014129ca8..d6562344657 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/MessageCompose.java @@ -7,8 +7,8 @@ import java.util.Locale; import java.util.Map; import java.util.regex.Pattern; - import android.annotation.SuppressLint; +import android.app.Activity; import android.app.Dialog; import android.app.PendingIntent; import android.content.Context; @@ -17,6 +17,7 @@ import android.content.IntentSender; import android.content.IntentSender.SendIntentException; import android.content.pm.ActivityInfo; +import android.content.pm.PackageManager; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; @@ -27,6 +28,7 @@ import android.view.ContextThemeWrapper; import android.view.LayoutInflater; import android.view.Menu; +import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; @@ -36,6 +38,7 @@ import android.widget.EditText; import android.widget.ImageView; import android.widget.LinearLayout; +import android.widget.PopupMenu; import android.widget.Toast; import androidx.annotation.NonNull; @@ -57,6 +60,7 @@ import com.fsck.k9.activity.compose.AttachmentPresenter; import com.fsck.k9.activity.compose.AttachmentPresenter.AttachmentMvpView; import com.fsck.k9.activity.compose.AttachmentPresenter.WaitingAction; +import app.k9mail.core.android.common.camera.CameraCaptureHandler; import com.fsck.k9.activity.compose.ComposeCryptoStatus; import com.fsck.k9.activity.compose.ComposeCryptoStatus.SendErrorState; import com.fsck.k9.activity.compose.IdentityAdapter; @@ -120,6 +124,9 @@ import org.openintents.openpgp.OpenPgpApiManager; import org.openintents.openpgp.util.OpenPgpIntentStarter; import timber.log.Timber; +import static com.fsck.k9.activity.compose.AttachmentPresenter.REQUEST_CODE_ATTACHMENT_URI; +import static app.k9mail.core.android.common.camera.CameraCaptureHandler.CAMERA_PERMISSION_REQUEST_CODE; +import static app.k9mail.core.android.common.camera.CameraCaptureHandler.REQUEST_IMAGE_CAPTURE; @SuppressWarnings("deprecation") // TODO get rid of activity dialogs and indeterminate progress bars @@ -171,6 +178,8 @@ public class MessageCompose extends K9Activity implements OnClickListener, private static final int REQUEST_MASK_ATTACHMENT_PRESENTER = (1 << 10); private static final int REQUEST_MASK_MESSAGE_BUILDER = (1 << 11); + + /** * Regular expression to remove the first localized "Re:" prefix in subjects. * @@ -188,6 +197,8 @@ public class MessageCompose extends K9Activity implements OnClickListener, private final Contacts contacts = DI.get(Contacts.class); + private final CameraCaptureHandler cameraCaptureHandler = DI.get(CameraCaptureHandler.class); + private QuotedMessagePresenter quotedMessagePresenter; private MessageLoaderHelper messageLoaderHelper; private AttachmentPresenter attachmentPresenter; @@ -324,6 +335,7 @@ public void onCreate(Bundle savedInstanceState) { EditText upperSignature = findViewById(R.id.upper_signature); EditText lowerSignature = findViewById(R.id.lower_signature); + QuotedMessageMvpView quotedMessageMvpView = new QuotedMessageMvpView(this); quotedMessagePresenter = new QuotedMessagePresenter(this, quotedMessageMvpView, account); attachmentPresenter = new AttachmentPresenter(getApplicationContext(), attachmentMvpView, @@ -503,7 +515,6 @@ public void onTextChanged(CharSequence s, int start, int before, int count) { setProgressBarIndeterminateVisibility(true); currentMessageBuilder.reattachCallback(this); } - } @Override @@ -796,11 +807,31 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { attachmentPresenter.onActivityResult(resultCode, requestCode, data); return; } + + if (requestCode == REQUEST_IMAGE_CAPTURE && resultCode == Activity.RESULT_OK) { + Intent intent = new Intent(); + intent.setData(cameraCaptureHandler.getCapturedImageUri()); + attachmentPresenter.onActivityResult(resultCode, REQUEST_CODE_ATTACHMENT_URI, intent); + return; + } } super.onActivityResult(requestCode, resultCode, data); } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) { + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + cameraCaptureHandler.openCamera(this); + } else { + Toast.makeText(this,R.string.camera_permission_denied,Toast.LENGTH_LONG).show(); + } + } + } + + private void onAccountChosen(LegacyAccount account, Identity identity) { if (!this.account.equals(account)) { Timber.v("Switching account from %s to %s", this.account, account); @@ -846,6 +877,29 @@ private void onAccountChosen(LegacyAccount account, Identity identity) { switchToIdentity(identity); } + private void showPopupMenu(View view) { + PopupMenu popup = new PopupMenu(this, view); + MenuInflater inflater = popup.getMenuInflater(); + inflater.inflate(R.menu.message_attachment_option, popup.getMenu()); + popup.getMenu().findItem(R.id.open_camera).setVisible(cameraCaptureHandler.canLaunchCamera(this)); + popup.setOnMenuItemClickListener(item -> { + int itemId = item.getItemId(); + if (itemId == R.id.open_camera) { + if(!cameraCaptureHandler.hasCameraPermission(this)){ + cameraCaptureHandler.requestCameraPermission(this); + }else { + cameraCaptureHandler.openCamera(this); + } + return true; + } else if (itemId == R.id.attach_file) { + attachmentPresenter.onClickAddAttachment(recipientPresenter); + return true; + } + return false; + }); + popup.show(); + } + private void switchToIdentity(Identity identity) { this.identity = identity; identityChanged = true; @@ -952,7 +1006,8 @@ public boolean onOptionsItemSelected(MenuItem item) { } else if (id == R.id.openpgp_sign_only_disable) { recipientPresenter.onMenuSetSignOnly(false); } else if (id == R.id.add_attachment) { - attachmentPresenter.onClickAddAttachment(recipientPresenter); + View attachmentMenuAnchor = findViewById(com.fsck.k9.ui.base.R.id.toolbar).findViewById(R.id.add_attachment); + showPopupMenu(attachmentMenuAnchor); } else if (id == R.id.read_receipt) { onReadReceipt(); } else { diff --git a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/AttachmentPresenter.java b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/AttachmentPresenter.java index bef1a763b60..d429900317f 100644 --- a/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/AttachmentPresenter.java +++ b/legacy/ui/legacy/src/main/java/com/fsck/k9/activity/compose/AttachmentPresenter.java @@ -39,7 +39,7 @@ public class AttachmentPresenter { private static final String LOADER_ARG_ATTACHMENT = "attachment"; private static final int LOADER_ID_MASK = 1 << 6; private static final int MAX_TOTAL_LOADERS = LOADER_ID_MASK - 1; - private static final int REQUEST_CODE_ATTACHMENT_URI = 1; + public static final int REQUEST_CODE_ATTACHMENT_URI = 1; // injected state diff --git a/legacy/ui/legacy/src/main/res/menu/message_attachment_option.xml b/legacy/ui/legacy/src/main/res/menu/message_attachment_option.xml new file mode 100644 index 00000000000..50a3ff00b25 --- /dev/null +++ b/legacy/ui/legacy/src/main/res/menu/message_attachment_option.xml @@ -0,0 +1,16 @@ + + + + + + + diff --git a/legacy/ui/legacy/src/main/res/values-ar/strings.xml b/legacy/ui/legacy/src/main/res/values-ar/strings.xml index a37cc8389bc..ff8145c0d58 100644 --- a/legacy/ui/legacy/src/main/res/values-ar/strings.xml +++ b/legacy/ui/legacy/src/main/res/values-ar/strings.xml @@ -853,5 +853,5 @@ الاسم وعنوان البريد الإلكتروني -فريق ثَندَربِرْد للهواتف الذكيّة + فريق ثَندَربِرْد للهواتف الذكيّة diff --git a/legacy/ui/legacy/src/main/res/values-az/strings.xml b/legacy/ui/legacy/src/main/res/values-az/strings.xml index a43f465349b..274295a9432 100644 --- a/legacy/ui/legacy/src/main/res/values-az/strings.xml +++ b/legacy/ui/legacy/src/main/res/values-az/strings.xml @@ -164,4 +164,4 @@ Bitir Poçtu yoxla Mesajları göndər - \ No newline at end of file + diff --git a/legacy/ui/legacy/src/main/res/values-br/strings.xml b/legacy/ui/legacy/src/main/res/values-br/strings.xml index dd850e893a5..8575ae5fc91 100644 --- a/legacy/ui/legacy/src/main/res/values-br/strings.xml +++ b/legacy/ui/legacy/src/main/res/values-br/strings.xml @@ -756,7 +756,7 @@ Gallout a rit mirout ar gemennadenn-mañ hag implij anezhi evel un enrolladenn e -Skipailh Thunderbird evit ardivinkoù hezoug + Skipailh Thunderbird evit ardivinkoù hezoug Bezañ skoazellet N\'eus ket bet gallet kargañ marilh ar c\'hemmoù. Petra nevez diff --git a/legacy/ui/legacy/src/main/res/values-gd/strings.xml b/legacy/ui/legacy/src/main/res/values-gd/strings.xml index 6519fd99afc..6ea2ec29ccd 100644 --- a/legacy/ui/legacy/src/main/res/values-gd/strings.xml +++ b/legacy/ui/legacy/src/main/res/values-gd/strings.xml @@ -744,7 +744,7 @@ -Sgioba mobile Thunderbird + Sgioba mobile Thunderbird Faigh cobhair Sàbhail an ceanglachan Crìoch air an fho-sgrìobhadh diff --git a/legacy/ui/legacy/src/main/res/values-hi/strings.xml b/legacy/ui/legacy/src/main/res/values-hi/strings.xml index 41e393261e0..22e9eeaef3f 100644 --- a/legacy/ui/legacy/src/main/res/values-hi/strings.xml +++ b/legacy/ui/legacy/src/main/res/values-hi/strings.xml @@ -160,4 +160,4 @@ एक्सपोर्ट पूरा नहीं हुआ। सब मिटाएं Thunderbird Mobile Team - \ No newline at end of file + diff --git a/legacy/ui/legacy/src/main/res/values/strings.xml b/legacy/ui/legacy/src/main/res/values/strings.xml index ec990bc04b4..c8933d17071 100644 --- a/legacy/ui/legacy/src/main/res/values/strings.xml +++ b/legacy/ui/legacy/src/main/res/values/strings.xml @@ -5,6 +5,9 @@ The K-9 Dog Walkers Thunderbird Mobile Team + Open Camera + Attach File + Camera Permission Denied! Unable to take photo Source code Apache License, Version 2.0 Open Source Project