Skip to content

Commit 1ea01b5

Browse files
committed
enhacement: add drawing option to multimedia fragment
* refactor: compress camera clicked pictures * refactor: add camera permission check * refactor: audio controller * chore: add test annotations #This is the commit message #2:
1 parent a8816ef commit 1ea01b5

11 files changed

Lines changed: 349 additions & 91 deletions

AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ import com.ichi2.anki.multimedia.MultimediaBottomSheet
9797
import com.ichi2.anki.multimedia.MultimediaImageFragment
9898
import com.ichi2.anki.multimedia.MultimediaUtils.createImageFile
9999
import com.ichi2.anki.multimedia.MultimediaViewModel
100+
import com.ichi2.anki.multimediacard.IMultimediaEditableNote
100101
import com.ichi2.anki.multimediacard.fields.AudioRecordingField
101102
import com.ichi2.anki.multimediacard.fields.EFieldType
102103
import com.ichi2.anki.multimediacard.fields.IField
@@ -503,7 +504,12 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su
503504
Timber.w("Note is null, returning")
504505
return
505506
}
506-
// TODO: start the MultimediaImageFragment with the image intent
507+
openMultimediaImageFragment(
508+
fieldIndex = 0,
509+
field = ImageField(),
510+
multimediaNote = note,
511+
imageUri = imageUri
512+
)
507513
}
508514

509515
override fun onSaveInstanceState(outState: Bundle) {
@@ -1695,13 +1701,7 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su
16951701
Timber.i("Selected Image option")
16961702
val field = ImageField()
16971703
note.setField(fieldIndex, field)
1698-
val imageIntent = MultimediaImageFragment.getIntent(
1699-
requireContext(),
1700-
MultimediaActivityExtra(fieldIndex, field, note),
1701-
MultimediaImageFragment.ImageOptions.GALLERY
1702-
)
1703-
1704-
multimediaFragmentLauncher.launch(imageIntent)
1704+
openMultimediaImageFragment(fieldIndex = fieldIndex, field, note)
17051705
}
17061706

17071707
MultimediaBottomSheet.MultimediaAction.SELECT_AUDIO_FILE -> {
@@ -1718,7 +1718,17 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su
17181718
}
17191719

17201720
MultimediaBottomSheet.MultimediaAction.OPEN_DRAWING -> {
1721-
// TODO("Not yet implemented")
1721+
Timber.i("Selected Drawing option")
1722+
val field = ImageField()
1723+
note.setField(fieldIndex, field)
1724+
1725+
val drawingIntent = MultimediaImageFragment.getIntent(
1726+
requireContext(),
1727+
MultimediaActivityExtra(fieldIndex, field, note),
1728+
MultimediaImageFragment.ImageOptions.DRAWING
1729+
)
1730+
1731+
multimediaFragmentLauncher.launch(drawingIntent)
17221732
}
17231733

17241734
MultimediaBottomSheet.MultimediaAction.SELECT_AUDIO_RECORDING -> {
@@ -1765,6 +1775,27 @@ class NoteEditor : AnkiFragment(R.layout.note_editor), DeckSelectionListener, Su
17651775
}
17661776
}
17671777

1778+
private fun openMultimediaImageFragment(
1779+
fieldIndex: Int,
1780+
field: IField,
1781+
multimediaNote: IMultimediaEditableNote,
1782+
imageUri: Uri? = null
1783+
) {
1784+
val multimediaExtra = if (imageUri != null) {
1785+
MultimediaActivityExtra(fieldIndex, field, multimediaNote, imageUri.toString())
1786+
} else {
1787+
MultimediaActivityExtra(fieldIndex, field, multimediaNote)
1788+
}
1789+
1790+
val imageIntent = MultimediaImageFragment.getIntent(
1791+
requireContext(),
1792+
multimediaExtra,
1793+
MultimediaImageFragment.ImageOptions.GALLERY
1794+
)
1795+
1796+
multimediaFragmentLauncher.launch(imageIntent)
1797+
}
1798+
17681799
private fun handleMultimediaResult(extras: Bundle) {
17691800
val index = extras.getInt(MULTIMEDIA_RESULT_FIELD_INDEX)
17701801
val field = extras.getSerializableCompat<IField>(MULTIMEDIA_RESULT)

AnkiDroid/src/main/java/com/ichi2/anki/Reviewer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -637,7 +637,7 @@ open class Reviewer :
637637
setEditorStatus(false)
638638
if (!isAudioUIInitialized) {
639639
try {
640-
audioRecordingController = AudioRecordingController(this)
640+
audioRecordingController = AudioRecordingController(context = this)
641641
audioRecordingController?.createUI(
642642
this,
643643
micToolBarLayer,

AnkiDroid/src/main/java/com/ichi2/anki/multimedia/AudioRecordingFragment.kt

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import android.content.Context
2222
import android.content.Intent
2323
import android.os.Bundle
2424
import android.view.View
25+
import androidx.activity.result.contract.ActivityResultContracts
2526
import androidx.appcompat.app.AppCompatActivity
2627
import androidx.fragment.app.viewModels
2728
import androidx.lifecycle.lifecycleScope
@@ -31,7 +32,9 @@ import com.ichi2.anki.R
3132
import com.ichi2.anki.multimedia.MultimediaActivity.Companion.MULTIMEDIA_RESULT
3233
import com.ichi2.anki.multimedia.MultimediaActivity.Companion.MULTIMEDIA_RESULT_FIELD_INDEX
3334
import com.ichi2.anki.multimedia.audio.AudioRecordingController
35+
import com.ichi2.annotations.NeedsTest
3436
import com.ichi2.utils.FileUtil
37+
import com.ichi2.utils.Permissions
3538
import kotlinx.coroutines.launch
3639
import timber.log.Timber
3740

@@ -56,29 +59,37 @@ class AudioRecordingFragment : MultimediaFragment(R.layout.fragment_audio_record
5659
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
5760
super.onViewCreated(view, savedInstanceState)
5861

59-
if (!handleAudioPermission()) {
62+
if (!hasMicPermission()) {
6063
return
6164
}
6265

6366
initializeAudioRecorder()
6467
setupDoneButton()
6568
}
6669

67-
private fun handleAudioPermission(): Boolean {
68-
return hasPerformedPermissionRequest(
69-
permission = Manifest.permission.RECORD_AUDIO,
70-
onGranted = {
71-
Timber.d("Audio permission granted")
72-
initializeAudioRecorder()
73-
setupDoneButton()
74-
},
75-
onRejected = {
76-
Timber.d("Audio permission denied")
77-
showErrorDialog(resources.getString(R.string.multimedia_editor_audio_permission_refused))
78-
}
79-
)
70+
private val requestPermissionLauncher = registerForActivityResult(
71+
ActivityResultContracts.RequestPermission()
72+
) { isGranted ->
73+
if (isGranted) {
74+
Timber.d("Audio permission granted")
75+
initializeAudioRecorder()
76+
setupDoneButton()
77+
} else {
78+
Timber.d("Audio permission denied")
79+
showErrorDialog(resources.getString(R.string.multimedia_editor_audio_permission_refused))
80+
}
81+
}
82+
83+
private fun hasMicPermission(): Boolean {
84+
if (!Permissions.canRecordAudio(requireContext())) {
85+
Timber.i("Requesting Audio Permissions")
86+
requestPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
87+
return false
88+
}
89+
return true
8090
}
8191

92+
@NeedsTest("Done button is enabled only when the length is not null")
8293
private fun setupDoneButton() {
8394
lifecycleScope.launch {
8495
viewModel.currentMultimediaPath.collect { path ->
@@ -104,11 +115,12 @@ class AudioRecordingFragment : MultimediaFragment(R.layout.fragment_audio_record
104115
}
105116
}
106117

118+
@NeedsTest("AudioRecordingController is correctly initialized")
107119
private fun initializeAudioRecorder() {
108120
try {
109121
audioRecordingController = AudioRecordingController(
110122
context = requireActivity(),
111-
layout = view?.findViewById(R.id.audio_recorder_layout)!!,
123+
linearLayout = view?.findViewById(R.id.audio_recorder_layout)!!,
112124
viewModel = viewModel,
113125
note = note
114126
)

AnkiDroid/src/main/java/com/ichi2/anki/multimedia/AudioVideoFragment.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import com.ichi2.anki.multimedia.MultimediaActivity.Companion.MULTIMEDIA_RESULT
4444
import com.ichi2.anki.multimedia.MultimediaActivity.Companion.MULTIMEDIA_RESULT_FIELD_INDEX
4545
import com.ichi2.anki.multimedia.MultimediaUtils.createCachedFile
4646
import com.ichi2.anki.utils.ext.sharedPrefs
47+
import com.ichi2.annotations.NeedsTest
4748
import com.ichi2.compat.CompatHelper
4849
import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat
4950
import com.ichi2.utils.ExceptionUtil.executeSafe
@@ -87,6 +88,7 @@ class AudioVideoFragment : MultimediaFragment(R.layout.fragment_audio_video) {
8788
* Lazily initialized instance of MultimediaMenu.
8889
* The instance is created only when first accessed.
8990
*/
91+
@NeedsTest("The menu drawable icon shoule be correctly set")
9092
private val multimediaMenu by lazy {
9193
MultimediaMenuProvider(
9294
menuResId = R.menu.multimedia_menu,

AnkiDroid/src/main/java/com/ichi2/anki/multimedia/MultimediaActivity.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ import kotlin.reflect.jvm.jvmName
4949
data class MultimediaActivityExtra(
5050
val index: Int,
5151
val field: IField,
52-
val note: IMultimediaEditableNote
52+
val note: IMultimediaEditableNote,
53+
val imageUri: String? = null
5354
) : Serializable
5455

5556
/**

AnkiDroid/src/main/java/com/ichi2/anki/multimedia/MultimediaFragment.kt

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,28 @@
1717

1818
package com.ichi2.anki.multimedia
1919

20+
import android.content.Context
21+
import android.net.Uri
22+
import android.os.Build
2023
import android.os.Bundle
2124
import android.text.format.Formatter
2225
import android.view.MenuItem
2326
import android.view.View
24-
import androidx.activity.result.contract.ActivityResultContracts
2527
import androidx.annotation.DrawableRes
2628
import androidx.annotation.LayoutRes
2729
import androidx.appcompat.app.AlertDialog
2830
import androidx.core.content.ContextCompat
31+
import androidx.core.content.FileProvider
2932
import androidx.core.view.MenuHost
3033
import androidx.core.view.MenuProvider
3134
import androidx.fragment.app.Fragment
3235
import com.ichi2.anki.AnkiActivity
36+
import com.ichi2.anki.CrashReportService
3337
import com.ichi2.anki.R
3438
import com.ichi2.anki.multimediacard.IMultimediaEditableNote
3539
import com.ichi2.anki.multimediacard.fields.IField
3640
import com.ichi2.anki.snackbar.showSnackbar
3741
import com.ichi2.compat.CompatHelper.Companion.getSerializableCompat
38-
import com.ichi2.utils.Permissions
3942
import com.ichi2.utils.show
4043
import timber.log.Timber
4144
import java.io.File
@@ -58,6 +61,7 @@ abstract class MultimediaFragment(@LayoutRes layout: Int) : Fragment(layout) {
5861
protected var indexValue: Int = 0
5962
protected lateinit var field: IField
6063
protected lateinit var note: IMultimediaEditableNote
64+
protected var imageUri: Uri? = null
6165

6266
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
6367
super.onViewCreated(view, savedInstanceState)
@@ -72,27 +76,35 @@ abstract class MultimediaFragment(@LayoutRes layout: Int) : Fragment(layout) {
7276
indexValue = multimediaActivityExtra.index
7377
field = multimediaActivityExtra.field
7478
note = multimediaActivityExtra.note
79+
if (multimediaActivityExtra.imageUri != null) {
80+
imageUri = Uri.parse(multimediaActivityExtra.imageUri)
81+
}
7582
}
7683
}
7784
}
7885

79-
private fun requestPermissionLauncher(onDone: () -> Unit, onRejected: () -> Unit) = registerForActivityResult(
80-
ActivityResultContracts.RequestPermission()
81-
) { isGranted: Boolean ->
82-
if (isGranted) {
83-
onDone.invoke()
84-
} else {
85-
onRejected.invoke()
86-
}
87-
}
88-
89-
fun hasPerformedPermissionRequest(permission: String, onGranted: () -> Unit, onRejected: () -> Unit): Boolean {
90-
if (!Permissions.canRecordAudio(requireContext())) {
91-
Timber.d("Requesting Audio Permissions")
92-
requestPermissionLauncher(onGranted, onRejected).launch(permission)
93-
return false
86+
/**
87+
* Get Uri based on current image path
88+
*
89+
* @param file the file to get URI for
90+
* @return current image path's uri
91+
*/
92+
fun getUriForFile(file: File, activity: Context): Uri {
93+
Timber.d("getUriForFile() %s", file)
94+
try {
95+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
96+
return FileProvider.getUriForFile(
97+
activity,
98+
activity.applicationContext.packageName + ".apkgfileprovider",
99+
file
100+
)
101+
}
102+
} catch (e: Exception) {
103+
// #6628 - What would cause this? Is the fallback is effective? Telemetry to diagnose more:
104+
Timber.w(e, "getUriForFile failed on %s - attempting fallback", file)
105+
CrashReportService.sendExceptionReport(e, "MultimediaFragment", "Unexpected getUriForFile failure on $file", true)
94106
}
95-
return true
107+
return Uri.fromFile(file)
96108
}
97109

98110
fun setMenuItemIcon(menuItem: MenuItem, @DrawableRes icon: Int) {
@@ -125,9 +137,8 @@ abstract class MultimediaFragment(@LayoutRes layout: Int) : Fragment(layout) {
125137
* when clicked, finishes the current activity.
126138
*/
127139
fun showErrorDialog(errorMessage: String? = null) {
128-
val message = errorMessage ?: resources.getString(R.string.something_wrong)
129140
AlertDialog.Builder(requireContext()).show {
130-
setMessage(message)
141+
setMessage(errorMessage ?: resources.getString(R.string.something_wrong))
131142
setPositiveButton(getString(R.string.dialog_ok)) { _, _ ->
132143
requireActivity().finish()
133144
}

0 commit comments

Comments
 (0)