Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentSender
import android.os.Build
import android.service.chooser.ChooserResult
import androidx.core.app.PendingIntentCompat
import androidx.core.content.IntentCompat
import com.ichi2.anki.AnkiDroidApp
Expand All @@ -21,8 +23,9 @@ class ExportSharedReceiver : BroadcastReceiver() {
intent: Intent,
) {
val target = IntentCompat.getParcelableExtra(intent, Intent.EXTRA_CHOSEN_COMPONENT, ComponentName::class.java)
Timber.i("Export shared with %s", target?.packageName)
if (target == null || target.packageName == context.packageName) return
val crossProfile = intent.isCrossProfileShare()
Timber.i("Export shared with %s, cross-profile: %b", target?.packageName, crossProfile)
if (!crossProfile && (target == null || target.packageName == context.packageName)) return
val path = intent.getStringExtra(EXTRA_EXPORT_PATH) ?: return
BackupPromptDialog.onFileExported(AnkiDroidApp.wrapForActiveProfile(context), File(path))
}
Expand All @@ -44,3 +47,9 @@ class ExportSharedReceiver : BroadcastReceiver() {
)?.intentSender
}
}

private fun Intent.isCrossProfileShare(): Boolean {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.VANILLA_ICE_CREAM) return false
val result = IntentCompat.getParcelableExtra(this, Intent.EXTRA_CHOOSER_RESULT, ChooserResult::class.java)
return result?.type == ChooserResult.CHOOSER_RESULT_UNKNOWN
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package com.ichi2.anki.receiver

import android.app.Activity
import android.content.ComponentName
import android.content.Intent
import android.content.IntentSender
import android.os.Build
import android.os.Parcel
import android.service.chooser.ChooserResult
import androidx.core.content.IntentCompat
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SdkSuppress
import com.ichi2.anki.RobolectricTest
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.annotation.Config
import java.io.File
import kotlin.test.assertEquals
import kotlin.test.assertNotNull
import kotlin.test.assertTrue

@RunWith(AndroidJUnit4::class)
@Config(sdk = [36])
@SdkSuppress(minSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
class ExportSharedReceiverRegressionTest : RobolectricTest(loadBackend = false) {
@Test
fun `Android 16 same profile chooser result control postpones the reminder`() {
val sender = assertNotNull(ExportSharedReceiver.getIntentSender(targetContext, File(tempFolder.root, "collection.colpkg")))
val chooser = Intent.createChooser(Intent(Intent.ACTION_SEND), "Share", sender)
val callback =
assertNotNull(IntentCompat.getParcelableExtra(chooser, Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER, IntentSender::class.java))
val parcel = Parcel.obtain()
val result =
try {
parcel.writeInt(0) // CHOOSER_RESULT_SELECTED_COMPONENT
ComponentName.writeToParcel(ComponentName("com.example.drive", "UploadActivity"), parcel)
parcel.writeBoolean(false)
parcel.setDataPosition(0)
ChooserResult.CREATOR.createFromParcel(parcel)
} finally {
parcel.recycle()
}

callback.sendIntent(
targetContext,
Activity.RESULT_OK,
Intent()
.putExtra(
Intent.EXTRA_CHOOSER_RESULT,
result,
).putExtra(Intent.EXTRA_CHOSEN_COMPONENT, ComponentName("com.example.drive", "UploadActivity")),
null,
null,
)
advanceRobolectricLooper()

assertTrue(
getPreferences().getLong("timeToShowBackupDialog", 0) > 0,
"Real Android 16 same profile callback must postpone the reminder",
)
}

@Test
fun `Android 16 work profile share postpones the reminder`() {
val sender = assertNotNull(ExportSharedReceiver.getIntentSender(targetContext, File(tempFolder.root, "collection.colpkg")))
val chooser = Intent.createChooser(Intent(Intent.ACTION_SEND), "Share", sender)
val callback =
assertNotNull(IntentCompat.getParcelableExtra(chooser, Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER, IntentSender::class.java))
val parcel = Parcel.obtain()
val result =
try {
parcel.writeInt(-1) // CHOOSER_RESULT_UNKNOWN: Android redacts cross profile targets.
ComponentName.writeToParcel(null, parcel)
parcel.writeBoolean(false)
parcel.setDataPosition(0)
ChooserResult.CREATOR.createFromParcel(parcel)
} finally {
parcel.recycle()
}

callback.sendIntent(targetContext, Activity.RESULT_OK, Intent().putExtra(Intent.EXTRA_CHOOSER_RESULT, result), null, null)
advanceRobolectricLooper()

assertTrue(getPreferences().getLong("timeToShowBackupDialog", 0) > 0, "Completed share to work profile must postpone the reminder")
}

@Test
fun `Android 16 copy action keeps the reminder`() {
val sender = assertNotNull(ExportSharedReceiver.getIntentSender(targetContext, File(tempFolder.root, "collection.colpkg")))
val chooser = Intent.createChooser(Intent(Intent.ACTION_SEND), "Share", sender)
val callback =
assertNotNull(IntentCompat.getParcelableExtra(chooser, Intent.EXTRA_CHOOSER_RESULT_INTENT_SENDER, IntentSender::class.java))
val parcel = Parcel.obtain()
val result =
try {
parcel.writeInt(ChooserResult.CHOOSER_RESULT_COPY)
ComponentName.writeToParcel(null, parcel)
parcel.writeBoolean(false)
parcel.setDataPosition(0)
ChooserResult.CREATOR.createFromParcel(parcel)
} finally {
parcel.recycle()
}

callback.sendIntent(targetContext, Activity.RESULT_OK, Intent().putExtra(Intent.EXTRA_CHOOSER_RESULT, result), null, null)
advanceRobolectricLooper()

assertEquals(0L, getPreferences().getLong("timeToShowBackupDialog", 0))
}

@Test
fun `legacy callback control postpones the reminder`() {
val sender = assertNotNull(ExportSharedReceiver.getIntentSender(targetContext, File(tempFolder.root, "collection.colpkg")))
val result = Intent().putExtra(Intent.EXTRA_CHOSEN_COMPONENT, ComponentName("com.example.drive", "UploadActivity"))

sender.sendIntent(targetContext, Activity.RESULT_OK, result, null, null)
advanceRobolectricLooper()

assertTrue(getPreferences().getLong("timeToShowBackupDialog", 0) > 0)
}
}
Loading