Skip to content

Commit

Permalink
enhacement: intents to directly add image to note and image occlusion
Browse files Browse the repository at this point in the history
  • Loading branch information
criticalAY authored and mikehardy committed Feb 15, 2024
1 parent 28ea0a3 commit 9310987
Show file tree
Hide file tree
Showing 9 changed files with 244 additions and 21 deletions.
29 changes: 29 additions & 0 deletions AnkiDroid/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,21 @@
<data android:mimeType="text/comma-separated-values"/>
</intent-filter>

<!-- Keep it separate as when sharing image AnkiDroid would be shown with Image Occlusion label -->
<intent-filter android:label="@string/image_occlusion">
<action android:name="android.intent.action.VIEW" />
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/jpg" />
<data android:mimeType="image/jpeg" />
<data android:mimeType="image/png" />
<data android:mimeType="image/ico" />
<data android:mimeType="image/webp" />
<data android:mimeType="image/tiff" />
<data android:mimeType="image/gif" />
<data android:mimeType="image/svg" />
</intent-filter>

<!-- Tasker DO_SYNC intent -->
<intent-filter>
<action android:name="com.ichi2.anki.DO_SYNC" />
Expand Down Expand Up @@ -374,6 +389,20 @@
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<!-- Keep it separate as when sharing image AnkiDroid would be shown with Add Image label -->
<intent-filter android:label="@string/multimedia_editor_popup_image">
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/jpg" />
<data android:mimeType="image/jpeg" />
<data android:mimeType="image/png" />
<data android:mimeType="image/ico" />
<data android:mimeType="image/webp" />
<data android:mimeType="image/tiff" />
<data android:mimeType="image/gif" />
<data android:mimeType="image/svg" />
</intent-filter>
</activity>
<activity
android:name="com.canhub.cropper.CropImageActivity"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright (c) 2024 Ashish Yadav <[email protected]>
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
* PARTICULAR PURPOSE. See the GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see <http://www.gnu.org/licenses/>.
*/

package com.ichi2.anki

import android.content.Context
import android.content.Intent
import android.net.Uri
/**
* Builder class for creating intents related to image occlusion in the [NoteEditor].
*/
class ImageOcclusionIntentBuilder(private val context: Context) {

fun buildIntent(imageUri: Uri?): Intent {
return Intent(context, NoteEditor::class.java).apply {
putExtra(NoteEditor.EXTRA_IMG_OCCLUSION, imageUri)
putExtra(NoteEditor.EXTRA_CALLER, NoteEditor.CALLER_IMG_OCCLUSION)
}
}
}
63 changes: 48 additions & 15 deletions AnkiDroid/src/main/java/com/ichi2/anki/IntentHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,11 @@ import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Message
import android.webkit.MimeTypeMap
import androidx.annotation.CheckResult
import androidx.annotation.VisibleForTesting
import androidx.core.app.TaskStackBuilder
import androidx.core.content.FileProvider
import androidx.core.content.IntentCompat
import com.ichi2.anki.UIUtils.showThemedToast
import com.ichi2.anki.dialogs.DialogHandler.Companion.storeMessage
import com.ichi2.anki.dialogs.DialogHandlerMessage
Expand All @@ -39,6 +40,7 @@ import com.ichi2.utils.FileUtil
import com.ichi2.utils.ImportUtils.handleFileImport
import com.ichi2.utils.ImportUtils.isInvalidViewIntent
import com.ichi2.utils.ImportUtils.showImportUnsuccessfulDialog
import com.ichi2.utils.IntentUtil.resolveMimeType
import com.ichi2.utils.NetworkUtils
import com.ichi2.utils.Permissions
import com.ichi2.utils.Permissions.hasStorageAccessPermission
Expand Down Expand Up @@ -71,8 +73,18 @@ class IntentHandler : Activity() {
// as this requires nothing
val runIfStoragePermissions = { runnable: () -> Unit -> performActionIfStorageAccessible(reloadIntent, action) { runnable() } }
when (getLaunchType(intent)) {
LaunchType.FILE_IMPORT -> runIfStoragePermissions { handleFileImport(intent, reloadIntent, action) }
LaunchType.TEXT_IMPORT -> runIfStoragePermissions { onSelectedCsvForImport(intent) }
LaunchType.FILE_IMPORT -> runIfStoragePermissions {
handleFileImport(fileIntent, reloadIntent, action)
finish()
}
LaunchType.TEXT_IMPORT -> runIfStoragePermissions {
onSelectedCsvForImport(fileIntent)
finish()
}
LaunchType.IMAGE_IMPORT -> runIfStoragePermissions {
handleImageImport(intent)
finish()
}
LaunchType.SYNC -> runIfStoragePermissions { handleSyncIntent(reloadIntent, action) }
LaunchType.REVIEW -> runIfStoragePermissions { handleReviewIntent(intent) }
LaunchType.DEFAULT_START_APP_IF_NEW -> {
Expand All @@ -97,6 +109,15 @@ class IntentHandler : Activity() {
showThemedToast(this, R.string.about_ankidroid_successfully_copied_debug_info, true)
}

private val fileIntent: Intent
get() {
return if (intent.action == Intent.ACTION_SEND) {
IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Intent::class.java) ?: intent
} else {
intent
}
}

/**
* Execute the runnable if one of the two following conditions are satisfied:
*
Expand Down Expand Up @@ -175,6 +196,22 @@ class IntentHandler : Activity() {
}
}

private fun handleImageImport(data: Intent) {
val imageUri = if (intent.action == Intent.ACTION_SEND) {
IntentCompat.getParcelableExtra(intent, Intent.EXTRA_STREAM, Uri::class.java)
} else {
data.data
}

val imageOcclusionIntentBuilder = ImageOcclusionIntentBuilder(this)
val intentImageOcclusion = imageOcclusionIntentBuilder.buildIntent(imageUri)

TaskStackBuilder.create(this)
.addNextIntentWithParentStack(Intent(this, DeckPicker::class.java))
.addNextIntent(intentImageOcclusion)
.startActivities()
}

private fun deleteDownloadedDeck(sharedDeckUri: Uri?) {
if (sharedDeckUri == null) {
Timber.i("onCreate: downloaded a shared deck but uri was null when trying to delete its file")
Expand Down Expand Up @@ -211,6 +248,10 @@ class IntentHandler : Activity() {

/** csv/tsv */
TEXT_IMPORT,

/** image */
IMAGE_IMPORT,

SYNC, REVIEW, COPY_DEBUG_INFO
}

Expand All @@ -228,10 +269,11 @@ class IntentHandler : Activity() {
@CheckResult
fun getLaunchType(intent: Intent): LaunchType {
val action = intent.action
return if (Intent.ACTION_VIEW == action && isValidViewIntent(intent)) {
return if (action == Intent.ACTION_SEND || Intent.ACTION_VIEW == action && isValidViewIntent(intent)) {
val mimeType = intent.resolveMimeType()
when (mimeType) {
"text/tab-separated-values", "text/comma-separated-values" -> LaunchType.TEXT_IMPORT
when {
mimeType?.startsWith("image/") == true -> LaunchType.IMAGE_IMPORT
mimeType == "text/tab-separated-values" || mimeType == "text/comma-separated-values" -> LaunchType.TEXT_IMPORT
else -> LaunchType.FILE_IMPORT
}
} else if ("com.ichi2.anki.DO_SYNC" == action) {
Expand All @@ -245,15 +287,6 @@ class IntentHandler : Activity() {
}
}

private fun Intent.resolveMimeType(): String? {
return if (type == null) {
val extension = MimeTypeMap.getFileExtensionFromUrl(data.toString())
MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
} else {
type
}
}

/**
* Send a Message to AnkiDroidApp so that the DialogMessageHandler forces a sync
*/
Expand Down
51 changes: 48 additions & 3 deletions AnkiDroid/src/main/java/com/ichi2/anki/NoteEditor.kt
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ import anki.notetypes.StockNotetype
import com.google.android.material.color.MaterialColors
import com.google.android.material.snackbar.Snackbar
import com.ichi2.anim.ActivityTransitionAnimation
import com.ichi2.anim.ActivityTransitionAnimation.Direction.*
import com.ichi2.anki.CollectionManager.TR
import com.ichi2.anki.CollectionManager.withCol
import com.ichi2.anki.dialogs.ConfirmationDialog
Expand Down Expand Up @@ -97,6 +96,7 @@ import com.ichi2.libanki.Decks.Companion.CURRENT_DECK
import com.ichi2.libanki.Note.ClozeUtils
import com.ichi2.libanki.Notetypes.Companion.NOT_FOUND_NOTE_TYPE
import com.ichi2.utils.*
import com.ichi2.utils.IntentUtil.resolveMimeType
import com.ichi2.widget.WidgetStatus
import org.json.JSONArray
import org.json.JSONException
Expand Down Expand Up @@ -365,6 +365,9 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
savedInstanceState.getSerializable("toggleSticky") as HashMap<Int, String?>
changed = savedInstanceState.getBoolean(NOTE_CHANGED_EXTRA_KEY)
} else {
if (intentLaunchedWithImage(intent)) {
intent.putExtra(EXTRA_CALLER, CALLER_ADD_IMAGE)
}
caller = intent.getIntExtra(EXTRA_CALLER, CALLER_NO_CALLER)
if (caller == CALLER_NO_CALLER) {
val action = intent.action
Expand Down Expand Up @@ -405,6 +408,29 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
setNavigationBarColor(R.attr.toolbarBackgroundColor)
}

private fun intentLaunchedWithImage(intent: Intent): Boolean {
val action = intent.action
return action == Intent.ACTION_SEND || (
Intent.ACTION_VIEW == action &&
!ImportUtils.isInvalidViewIntent(intent) &&
intent.resolveMimeType()?.startsWith("image/") == true
)
}

@NeedsTest("Test when the user directly passes image to the edit note field")
private fun handleImageIntent(data: Intent) {
val imageUri = if (data.action == Intent.ACTION_SEND) {
IntentCompat.getParcelableExtra(data, Intent.EXTRA_STREAM, Uri::class.java)
} else {
data.data
}
Timber.d("Image Uri : $imageUri")
// ImageIntentManager.saveImageUri(imageUri)
// the field won't exist so it will always be a new card
val note = getCurrentMultimediaEditableNote(getColUnsafe)
startMultimediaFieldEditor(0, note, imageUri)
}

override fun onSaveInstanceState(savedInstanceState: Bundle) {
addInstanceStateToBundle(savedInstanceState)
super.onSaveInstanceState(savedInstanceState)
Expand Down Expand Up @@ -500,6 +526,21 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
}
addNote = true
}
CALLER_IMG_OCCLUSION -> {
addNote = true
// val saveImageUri = ImageIntentManager.getImageUri()
val saveImageUri = IntentCompat.getParcelableExtra(intent, EXTRA_IMG_OCCLUSION, Uri::class.java)
if (saveImageUri != null) {
ImportUtils.getFileCachedCopy(this, saveImageUri)?.let { path ->
setupImageOcclusionEditor(path)
}
} else {
Timber.w("Image uri is null")
}
}
CALLER_ADD_IMAGE -> {
addNote = true
}
else -> {}
}

Expand Down Expand Up @@ -586,6 +627,7 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
}
contents?.let { setEditFieldTexts(it) }
tags?.let { setTags(it) }
if (caller == CALLER_ADD_IMAGE) handleImageIntent(intent)
} else {
mNoteTypeSpinner!!.onItemSelectedListener = EditNoteTypeListener()
setTitle(R.string.cardeditor_title_edit_card)
Expand Down Expand Up @@ -1596,9 +1638,10 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
}
}

private fun startMultimediaFieldEditor(index: Int, note: IMultimediaEditableNote?) {
private fun startMultimediaFieldEditor(index: Int, note: IMultimediaEditableNote?, imageUri: Uri? = null) {
val field = note!!.getField(index)!!
val editCard = Intent(this@NoteEditor, MultimediaEditFieldActivity::class.java)
editCard.putExtra(MultimediaEditFieldActivity.INTENT_IMAGE_URI, imageUri)
editCard.putExtra(MultimediaEditFieldActivity.EXTRA_MULTIMEDIA_EDIT_FIELD_ACTIVITY, MultimediaEditFieldActivityExtra(index, field, note))
requestMultiMediaEditLauncher.launch(editCard)
}
Expand Down Expand Up @@ -2323,6 +2366,7 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
private const val ACTION_CREATE_FLASHCARD_SEND = "android.intent.action.SEND"
const val NOTE_CHANGED_EXTRA_KEY = "noteChanged"
const val RELOAD_REQUIRED_EXTRA_KEY = "reloadRequired"
const val EXTRA_IMG_OCCLUSION = "image_uri"

// calling activity
const val CALLER_NO_CALLER = 0
Expand All @@ -2335,8 +2379,9 @@ class NoteEditor : AnkiActivity(), DeckSelectionListener, SubtitleListener, Tags
const val CALLER_NOTEEDITOR = 8
const val CALLER_PREVIEWER_EDIT = 9
const val CALLER_NOTEEDITOR_INTENT_ADD = 10

const val RESULT_UPDATED_IO_NOTE = 11
const val CALLER_IMG_OCCLUSION = 12
const val CALLER_ADD_IMAGE = 13

// preferences keys
const val PREF_NOTE_EDITOR_SCROLL_TOOLBAR = "noteEditorScrollToolbar"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package com.ichi2.anki.multimediacard.activity
import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
Expand All @@ -30,6 +31,7 @@ import androidx.annotation.VisibleForTesting
import androidx.appcompat.widget.Toolbar
import androidx.core.app.ActivityCompat
import androidx.core.app.ActivityCompat.OnRequestPermissionsResultCallback
import androidx.core.content.IntentCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import com.ichi2.anki.AnkiActivity
Expand Down Expand Up @@ -98,10 +100,23 @@ class MultimediaEditFieldActivity :
fieldIndex = extras.first
field = extras.second
note = extras.third

val savedImageUri = getIntentImageUri()
if (savedImageUri != null) {
intentEditImage()
}
onBack()
recreateEditingUi(ChangeUIRequest.init(field), controllerBundle)
}

fun getIntentImageUri(): Uri? {
return IntentCompat.getParcelableExtra(intent, INTENT_IMAGE_URI, Uri::class.java)
}

private fun intentEditImage() {
recreateEditingUi(ChangeUIRequest.uiChange(ImageField()), intentImgEdit = true)
}

// in case media is saved by view button then allows it to be inserted into the filed
private fun onBack() {
findViewById<Toolbar>(R.id.toolbar).setNavigationOnClickListener {
Expand Down Expand Up @@ -148,7 +163,7 @@ class MultimediaEditFieldActivity :
if (isAudioUIInitialized) audioRecordingController.onViewFocusChanged()
}

private fun recreateEditingUi(newUI: ChangeUIRequest, savedInstanceState: Bundle? = null) {
private fun recreateEditingUi(newUI: ChangeUIRequest, savedInstanceState: Bundle? = null, intentImgEdit: Boolean = false) {
Timber.d("recreateEditingUi()")

// Permissions are checked async, save our current state to allow continuation
Expand All @@ -163,6 +178,9 @@ class MultimediaEditFieldActivity :
return
}
val fieldController = createControllerForField(newUI.field)
if (intentImgEdit) {
(fieldController as? BasicImageFieldController)?.directImageEdit = true
}
UIRecreationHandler.onPreFieldControllerReplacement(this.fieldController)
this.fieldController = fieldController
field = newUI.field
Expand Down Expand Up @@ -462,6 +480,7 @@ class MultimediaEditFieldActivity :
const val EXTRA_RESULT_FIELD_INDEX = "edit.field.result.field.index"
const val EXTRA_MULTIMEDIA_EDIT_FIELD_ACTIVITY = "multim.card.ed.extra"
private const val BUNDLE_KEY_SHUT_OFF = "key.edit.field.shut.off"
const val INTENT_IMAGE_URI = "intent_img_uri"
private const val REQUEST_AUDIO_PERMISSION = 0
private const val REQUEST_CAMERA_PERMISSION = 1
const val IMAGE_LIMIT = 1024 * 1024 // 1MB in bytes
Expand Down
Loading

0 comments on commit 9310987

Please sign in to comment.