fix(share): 认下字符串形态的 EXTRA_STREAM,并在一条内容都没读进来时如实告知
为什么字符串形态必须认 真实世界里 EXTRA_STREAM 有三种形态:规范推荐的 Uri(多选时 ArrayList<Uri>)、String[]、 ArrayList<String>。Android 上确实有一部分发送方(历史遗留 / 跨平台壳应用)在用后两种,而本端 此前完全认不出:getParcelableExtra 遇到 String[] 只往 logcat 打一句 ClassCastException 再回 null,getParcelableArrayListExtra 同理。实测 am start ... --esa android.intent.extra.STREAM uri1,uri2 投两张图进来就是这个样子。 为什么静默是 bug 上面那次分享的结局是:后端零请求、库里零新增、界面连一句软告知都没有。用户点了分享,没有任何 东西告诉他"没读进来"还是"在路上"——这是最糟的那种失败形态,所以本次一并消灭:带着内容迹象却 一条可用内容都没提取出来时,走既有的柔和通道说一句"这次分享的内容没能读进来,没有发送任何内容"。 EXTRA_TEXT 与 EXTRA_STREAM 都为空(用户确实没分享任何东西)时仍然保持静默,那条边界有测试钉住。 做了什么 - ShareIntake 改成读 EXTRA_STREAM 的原始值再按形态分派(Bundle.get 不做类型检查),Uri / Uri 数组 / String / CharSequence / 数组 / 列表一并摊平;顺带不再往 logcat 丢那两行 ClassCastException。 - 字符串形态只认 content://:那是本端新开的一条读入口,送进来的只是一段文本,没有任何框架级担保。 file:// 等于让发送方替本端指定一个本地路径、由本端拿自己的权限去读它;http(s):// 是网络地址, 收下它等于让第三方的一行文本指定一个 URL 由本应用代取。被拒的条目逐条跳过,不让整次分享失败。 - 优先顺序与去重不变:EXTRA_STREAM 的规范形态 -> 字符串兼容形态 -> ClipData(仅前者全为空时兜底), trim / 去空 / 去重,先来的先算;重复项既不算截断,也不把真图挤到上限之外。 - 「带着内容迹象」的判据取 EXTRA_STREAM 的原始值(由调用方读了原始值判出来,含形态本端认不出的那种), 不是「认下来的地址」——否则「有值但一条都认不出」恰恰会被判成「什么都没分享」而继续静默。 - SharedContent 新增 contentUnreadable 与 worthDelivering:后者是唯一判据,ShareIntake 用它决定 要不要往下接,shareNoticeFor 用它守卫"什么都没有就一个字都不说"那条边界,两处不会走岔。 - 分类(图片 vs 文档)、落 cache、截断与拷贝失败的既有语义一概不动。 测试 - ShareExtractionTest:字符串形态的顺序与逐条跳过、两种形态并存去重、scheme 白名单表、 读不进来 / 形态认不出 / 静默三条边界。 - ShareStreamExtrasTest(新):按真实对象验形态分派(String[] / ArrayList<String> / String / CharSequence / 认不出的形态不猜但照样算带着内容),并接到提取层验一次端到端。 - ShareNoticeTest(新):"没能读进来"成句且不说成发送失败、正常落地一个字不说、空白分享仍然静默。 assembleDebug 与 testDebugUnitTest 全绿(1084 tests / 0 failures / 0 errors)。
This commit is contained in:
1 parent
ef257c3167
commit
6cdf111419
6 files changed
+663
-29
No files matched your search
@@ -0,0 +1,136 @@
|
||||
package top.yeij.cyrene.app
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import top.yeij.cyrene.domain.share.ShareRules
|
||||
|
||||
/**
|
||||
* `EXTRA_STREAM` 原始值的**形态分派**([shareStreamExtrasOf])。
|
||||
*
|
||||
* 真实世界里这个 extra 有三种形态:规范推荐的 `Uri`(多选时 `ArrayList<Uri>`)、`String[]`、
|
||||
* `ArrayList<String>`。后两种此前**完全认不出**——`getParcelableExtra` 遇到 `String[]` 只往 logcat
|
||||
* 打一行 `ClassCastException` 再回 `null`,于是整次分享静默落空。这里按**真实对象**把新认下的形态、
|
||||
* 「认不出的形态不猜」以及「接到 [ShareRules.extract] 之后真的能落地」一起钉住。
|
||||
*
|
||||
* `Uri` 形态在这里分不出单独的用例:JVM 单测造不出 `android.net.Uri`(android.jar 的桩方法一调就抛
|
||||
* `Stub!`)。那一路由 `ShareRules.extract` 的既有用例与真机验证兜住;本文件管的是新开的两条路。
|
||||
*/
|
||||
class ShareStreamExtrasTest {
|
||||
|
||||
private val imageMime: (String) -> String? = { "image/png" }
|
||||
|
||||
@Test
|
||||
fun `a string array stream yields every entry in order`() {
|
||||
// `am start … --esa android.intent.extra.STREAM <uri1>,<uri2>` 打出来的就是这个形态。
|
||||
val extras = shareStreamExtrasOf(
|
||||
arrayOf("content://media/external/images/media/1", "content://media/external/images/media/2"),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf("content://media/external/images/media/1", "content://media/external/images/media/2"),
|
||||
extras.stringForms,
|
||||
)
|
||||
assertTrue(extras.uriForms.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a string list stream yields every entry in order`() {
|
||||
val extras = shareStreamExtrasOf(
|
||||
arrayListOf("content://media/external/images/media/1", "content://media/external/images/media/2"),
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf("content://media/external/images/media/1", "content://media/external/images/media/2"),
|
||||
extras.stringForms,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a bare string stream is a string form`() {
|
||||
assertEquals(listOf("content://m/1"), shareStreamExtrasOf("content://m/1").stringForms)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a char sequence stream is read through`() {
|
||||
// `Spanned` 之类也是 `CharSequence`:它照样要过 scheme 白名单,不是「因为它是 CharSequence
|
||||
// 就信」。按 String 读会直接拿到 null。
|
||||
val raw: Any = StringBuilder("content://m/1")
|
||||
|
||||
assertEquals(listOf("content://m/1"), shareStreamExtrasOf(raw).stringForms)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an absent stream yields nothing`() {
|
||||
val extras = shareStreamExtrasOf(null)
|
||||
|
||||
assertTrue(extras.uriForms.isEmpty())
|
||||
assertTrue(extras.stringForms.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a shape we do not know is not guessed`() {
|
||||
val extras = shareStreamExtrasOf(42)
|
||||
|
||||
assertTrue(extras.uriForms.isEmpty())
|
||||
assertTrue(extras.stringForms.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an entry of a shape we do not know is skipped and the rest survives`() {
|
||||
val raw: Any = arrayListOf<Any?>("content://m/1", 42, null, "content://m/2")
|
||||
|
||||
assertEquals(listOf("content://m/1", "content://m/2"), shareStreamExtrasOf(raw).stringForms)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a string array stream ends up as two images`() {
|
||||
// 端到端那一步:事故现场那两张图现在真的进到待发送内容里了,顺序不变。
|
||||
val extras = shareStreamExtrasOf(
|
||||
arrayOf("content://media/external/images/media/1", "content://media/external/images/media/2"),
|
||||
)
|
||||
|
||||
val share = ShareRules.extract(
|
||||
action = ShareRules.ACTION_SEND_MULTIPLE,
|
||||
stream = extras.uriForms.firstOrNull(),
|
||||
streams = extras.uriForms,
|
||||
streamStrings = extras.stringForms,
|
||||
mimeOf = imageMime,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf("content://media/external/images/media/1", "content://media/external/images/media/2"),
|
||||
share.images,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a string array stream of unusable uris ends up as a read failure we can talk about`() {
|
||||
val extras = shareStreamExtrasOf(arrayOf("javascript:alert(1)", "这只是一段纯文本"))
|
||||
|
||||
val share = ShareRules.extract(
|
||||
action = ShareRules.ACTION_SEND_MULTIPLE,
|
||||
streamStrings = extras.stringForms,
|
||||
mimeOf = imageMime,
|
||||
)
|
||||
|
||||
assertTrue(share.isEmpty)
|
||||
assertTrue(share.contentUnreadable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a shape we cannot decode still counts as a share that carried something`() {
|
||||
// 形态认不出时本端既不猜、也不该因此闭嘴:`streamPresent` 由读了原始值的调用方判(见
|
||||
// `ShareIntake.accept`),否则「分享点下去什么都没发生」会从那条缝里回来。
|
||||
val raw: Any? = 42
|
||||
|
||||
val share = ShareRules.extract(
|
||||
action = ShareRules.ACTION_SEND,
|
||||
streamPresent = raw != null,
|
||||
mimeOf = imageMime,
|
||||
)
|
||||
|
||||
assertTrue(share.isEmpty)
|
||||
assertTrue(share.contentUnreadable)
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package top.yeij.cyrene.domain.share
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import top.yeij.cyrene.domain.chat.ChatSendRouting
|
||||
@@ -212,6 +213,137 @@ class ShareExtractionTest {
|
||||
assertEquals(listOf("content://media/external/images/media/1"), share.images)
|
||||
}
|
||||
|
||||
// —— 字符串形态的 EXTRA_STREAM(兼容读取)——
|
||||
|
||||
// 形态分派(`String` / `String[]` / `ArrayList<String>` 怎么变成下面这些字符串)在
|
||||
// `app/ShareStreamExtrasTest`——那一步要按**真实对象**判形态,放不进这个纯函数。
|
||||
// 这里管的是「拿到这些字符串之后怎么用」。
|
||||
|
||||
@Test
|
||||
fun `string form content uris are delivered in the order the sender gave them`() {
|
||||
// 实测事故:`am start … --esa android.intent.extra.STREAM uri1,uri2` 投两张图,
|
||||
// EXTRA_STREAM 是 `String[]`,getParcelableExtra / getParcelableArrayListExtra 双双回 null,
|
||||
// 于是整次分享什么都没发生(后端零请求、库里零新增、界面一句话都没有)。
|
||||
val share = ShareRules.extract(
|
||||
action = ShareRules.ACTION_SEND_MULTIPLE,
|
||||
streamStrings = listOf(
|
||||
"content://media/external/images/media/1",
|
||||
"content://media/external/images/media/2",
|
||||
),
|
||||
mimeOf = imageMime,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
"content://media/external/images/media/1",
|
||||
"content://media/external/images/media/2",
|
||||
),
|
||||
share.images,
|
||||
)
|
||||
assertEquals(0, share.truncatedImages)
|
||||
assertFalse(share.contentUnreadable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `string form items with an unusable scheme are skipped one by one`() {
|
||||
val share = ShareRules.extract(
|
||||
action = ShareRules.ACTION_SEND_MULTIPLE,
|
||||
streamStrings = listOf(
|
||||
"content://media/external/images/media/1",
|
||||
"",
|
||||
" ",
|
||||
"javascript:alert(1)",
|
||||
"intent://scan/#Intent;scheme=zxing;end",
|
||||
"这只是一段纯文本",
|
||||
"file:///sdcard/DCIM/Camera/IMG_0001.jpg",
|
||||
"https://example.com/a.png",
|
||||
"/sdcard/DCIM/Camera/IMG_0001.jpg",
|
||||
"content://media/external/images/media/2",
|
||||
),
|
||||
mimeOf = imageMime,
|
||||
)
|
||||
|
||||
// 一条坏的不能把整次分享废掉。被拒的也不计进丢件计数:那说的是「对方给的东西读不到」,
|
||||
// 与「本端不收这个 scheme」不是一回事(后者连读都没试过)。
|
||||
assertEquals(
|
||||
listOf(
|
||||
"content://media/external/images/media/1",
|
||||
"content://media/external/images/media/2",
|
||||
),
|
||||
share.images,
|
||||
)
|
||||
assertEquals(0, share.truncatedImages)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `string form files go to the document path`() {
|
||||
val share = ShareRules.extract(
|
||||
action = ShareRules.ACTION_SEND,
|
||||
streamStrings = listOf("content://com.android.providers.downloads.documents/document/42"),
|
||||
mimeOf = textMime,
|
||||
)
|
||||
|
||||
assertTrue(share.images.isEmpty())
|
||||
assertEquals(
|
||||
listOf("content://com.android.providers.downloads.documents/document/42"),
|
||||
share.documents.map { it.uri },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a uri given in both forms is counted once and the uri form comes first`() {
|
||||
val share = ShareRules.extract(
|
||||
action = ShareRules.ACTION_SEND,
|
||||
stream = "content://media/external/images/media/1",
|
||||
streamStrings = listOf(
|
||||
"content://media/external/images/media/1",
|
||||
"content://media/external/images/media/2",
|
||||
),
|
||||
mimeOf = imageMime,
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
"content://media/external/images/media/1",
|
||||
"content://media/external/images/media/2",
|
||||
),
|
||||
share.images,
|
||||
)
|
||||
// 去重发生在截断之前:重复项不该把真图挤掉,也不该被算成「截掉了 1 张」。
|
||||
assertEquals(0, share.truncatedImages)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the string form only accepts content uris`() {
|
||||
assertEquals(
|
||||
"content://media/external/images/media/1",
|
||||
ShareRules.stringStreamUri("content://media/external/images/media/1"),
|
||||
)
|
||||
// 两侧空白是传输噪声,trim 掉再用(认它比因为一个空格而静默安全)。
|
||||
assertEquals("content://m/1", ShareRules.stringStreamUri(" content://m/1 "))
|
||||
// scheme 大小写不敏感(`Uri.parse` 也会把它归一成小写)。
|
||||
assertEquals("CONTENT://m/1", ShareRules.stringStreamUri("CONTENT://m/1"))
|
||||
// 这里只判 scheme 形态,不解析路径:这条确实读不到,会由落盘那一步如实计入丢件。
|
||||
assertEquals("content:not//a/uri", ShareRules.stringStreamUri("content:not//a/uri"))
|
||||
|
||||
// 下面这些**不能**当地址读。`file://` 是别人指定的本地路径(本端会拿自己的权限去读它,
|
||||
// 本应用私有目录也在里面);`http(s)://` 是网络地址(收下等于让第三方指定一个 URL、
|
||||
// 由本应用代它去取);其余是任意 scheme / 没有 scheme 的裸路径 / 纯文本。
|
||||
assertNull(ShareRules.stringStreamUri("file:///sdcard/DCIM/Camera/IMG_0001.jpg"))
|
||||
assertNull(ShareRules.stringStreamUri("http://example.com/a.png"))
|
||||
assertNull(ShareRules.stringStreamUri("https://example.com/a.png"))
|
||||
assertNull(ShareRules.stringStreamUri("javascript:alert(1)"))
|
||||
assertNull(ShareRules.stringStreamUri("intent://x/#Intent;end"))
|
||||
assertNull(ShareRules.stringStreamUri("android.resource://com.example/drawable/icon"))
|
||||
assertNull(ShareRules.stringStreamUri("/sdcard/DCIM/Camera/IMG_0001.jpg"))
|
||||
assertNull(ShareRules.stringStreamUri("这只是一段纯文本"))
|
||||
// scheme 的形态按 RFC 3986:首字符必须是字母,其后不能出现 `/`。
|
||||
assertNull(ShareRules.stringStreamUri("1content://m/1"))
|
||||
assertNull(ShareRules.stringStreamUri("a/b:c"))
|
||||
assertNull(ShareRules.stringStreamUri(""))
|
||||
assertNull(ShareRules.stringStreamUri(" "))
|
||||
}
|
||||
|
||||
// —— 文件(非图片)分流 ——
|
||||
|
||||
@Test
|
||||
@@ -373,4 +505,96 @@ class ShareExtractionTest {
|
||||
assertFalse(name, name.contains(".."))
|
||||
assertTrue(name, name.startsWith("share-"))
|
||||
}
|
||||
|
||||
// —— 读不进来 / 静默那条边界 ——
|
||||
|
||||
@Test
|
||||
fun `stream values that are all unusable are reported as unreadable instead of vanishing`() {
|
||||
val share = ShareRules.extract(
|
||||
action = ShareRules.ACTION_SEND_MULTIPLE,
|
||||
streamStrings = listOf("javascript:alert(1)", "这只是一段纯文本"),
|
||||
mimeOf = imageMime,
|
||||
)
|
||||
|
||||
assertTrue(share.isEmpty)
|
||||
assertTrue("有内容迹象却一条都没读进来,必须能说出口", share.contentUnreadable)
|
||||
assertTrue(share.worthDelivering)
|
||||
// 它连丢件计数都留不下(本端一条都没接收),所以这件事不能靠 droppedImages 来说。
|
||||
assertEquals(0, share.droppedImages)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clip items without a uri are an indication too`() {
|
||||
// ClipData 里有条目、但只有纯文本条目(uri == null):本端**不读** ClipData 里的文本,
|
||||
// 所以这也是一次「读不进来」,而不是「什么都没分享」。
|
||||
val share = ShareRules.extract(
|
||||
action = ShareRules.ACTION_SEND,
|
||||
clipUris = listOf(null),
|
||||
)
|
||||
|
||||
assertTrue(share.isEmpty)
|
||||
assertTrue(share.contentUnreadable)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a share that carries nothing at all stays silent`() {
|
||||
// 边界:EXTRA_TEXT 没有值、EXTRA_STREAM 也没有值、ClipData 也没有条目——用户确实没分享
|
||||
// 任何东西,这时候说一句才是噪声。(`ShareIntake` 靠 worthDelivering 判要不要往下走,
|
||||
// `shareNoticeFor` 靠同一条判要不要说。)
|
||||
val share = ShareRules.extract(action = ShareRules.ACTION_SEND)
|
||||
|
||||
assertTrue(share.isEmpty)
|
||||
assertFalse(share.contentUnreadable)
|
||||
assertFalse(share.worthDelivering)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a blank text with blank stream entries is still nothing`() {
|
||||
val share = ShareRules.extract(
|
||||
action = ShareRules.ACTION_SEND,
|
||||
text = " \n\t ",
|
||||
streamStrings = listOf(" ", ""),
|
||||
)
|
||||
|
||||
assertTrue(share.isEmpty)
|
||||
assertFalse("占位空白不是内容迹象", share.contentUnreadable)
|
||||
assertFalse(share.worthDelivering)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unusable streams next to a usable text are not a whole read failure`() {
|
||||
val share = ShareRules.extract(
|
||||
action = ShareRules.ACTION_SEND,
|
||||
text = "看这个",
|
||||
streamStrings = listOf("javascript:alert(1)"),
|
||||
)
|
||||
|
||||
assertEquals("看这个", share.text)
|
||||
// 正文照发:这次分享不是「什么都没读进来」,就不能说成那一句。
|
||||
assertFalse(share.contentUnreadable)
|
||||
assertTrue(share.worthDelivering)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a stream value whose shape we cannot decode is still an indication`() {
|
||||
// 调用方读了 EXTRA_STREAM 的原始值:里面确实躺着东西,只是形态本端认不出(本端**不猜**)。
|
||||
// 这也不能闭嘴——它是「分享点下去什么都没发生」的另一条缝。默认 false 保住上面那条静默边界。
|
||||
val share = ShareRules.extract(action = ShareRules.ACTION_SEND, streamPresent = true)
|
||||
|
||||
assertTrue(share.isEmpty)
|
||||
assertTrue(share.contentUnreadable)
|
||||
assertTrue(share.worthDelivering)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unrelated intent with a stream in it still extracts to nothing`() {
|
||||
// 不是分享 action 就不是分享:哪怕里面照样躺着地址,也不该走这条路(更不该出一条告知)。
|
||||
val share = ShareRules.extract(
|
||||
action = "android.intent.action.VIEW",
|
||||
streamStrings = listOf("content://media/external/images/media/1"),
|
||||
)
|
||||
|
||||
assertFalse(share.contentUnreadable)
|
||||
assertFalse(share.worthDelivering)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package top.yeij.cyrene.ui.chat
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import top.yeij.cyrene.domain.share.ShareRules
|
||||
import top.yeij.cyrene.domain.share.SharedContent
|
||||
import top.yeij.cyrene.domain.share.SharedDocument
|
||||
|
||||
/**
|
||||
* 分享落地那条**柔和的告知**([shareNoticeFor],画面上走 `Banner` 的 `onSurfaceVariantSummary`,
|
||||
* 不是错误横幅)。
|
||||
*
|
||||
* 背景:主人实机分享两张图进来「什么都没发生」——没有消息发出去,界面也没有任何告知。第一处来处是
|
||||
* 提取层(`EXTRA_STREAM` 的字符串形态认不出,见 `ShareExtractionTest` / `ShareStreamExtrasTest`);
|
||||
* 第二处就在这里:一条内容谁都没读进来的分享,此前既不落盘、也不说一句话。本测试钉住三件事:
|
||||
*
|
||||
* 1. 「内容没能读进来」要成句,且**不说成发送失败**——失败的是读取,不是发送;
|
||||
* 2. 正常落地(一条文本 / 一张图 / 一个文件)一个字都不说;
|
||||
* 3. 边界:整条分享里什么都没有时**仍然静默**(那种情况没什么可说的,说了才是噪声)。
|
||||
*/
|
||||
class ShareNoticeTest {
|
||||
|
||||
@Test
|
||||
fun `a share nothing could be read from says so`() {
|
||||
assertEquals(SHARE_UNREADABLE_NOTICE, shareNoticeFor(SharedContent(contentUnreadable = true)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the unreadable notice never says the send failed`() {
|
||||
assertTrue(SHARE_UNREADABLE_NOTICE.contains("没能读进来"))
|
||||
assertFalse(SHARE_UNREADABLE_NOTICE.contains("失败"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unreadable share never also claims to be an empty share`() {
|
||||
val said = shareNoticeFor(SharedContent(contentUnreadable = true))!!
|
||||
|
||||
assertFalse("两句混在一起会让人以为东西还在路上", said.contains("分享的内容是空的"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a normal share says nothing`() {
|
||||
assertNull(shareNoticeFor(SharedContent(text = "看这个")))
|
||||
assertNull(shareNoticeFor(SharedContent(images = listOf("file:///cache/share-1.png"))))
|
||||
assertNull(
|
||||
shareNoticeFor(SharedContent(documents = listOf(SharedDocument(uri = "file:///cache/share-0.txt")))),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a share that carries nothing at all stays silent`() {
|
||||
// 边界:既没有文本、`EXTRA_STREAM` 也没有值、`ClipData` 也没有条目。
|
||||
assertNull(shareNoticeFor(SharedContent()))
|
||||
assertNull(shareNoticeFor(ShareRules.extract(action = ShareRules.ACTION_SEND)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the existing wording for a truncated share is unchanged`() {
|
||||
assertEquals(
|
||||
"分享的图片超过 ${ShareRules.MAX_IMAGES} 张,只发送前 1 张",
|
||||
shareNoticeFor(SharedContent(images = listOf("file:///cache/share-1.png"), truncatedImages = 3)),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `dropped content next to an empty body says both things`() {
|
||||
val said = shareNoticeFor(SharedContent(failedImages = 2))!!
|
||||
|
||||
assertTrue(said.contains("有 2 张图片读不到,未发送"))
|
||||
assertTrue(said.contains("分享的内容是空的,没有可发送的内容"))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user