你的 App 還能在后臺啟動 Activity 嗎(非 AndroidQ 適配)

先說結(jié)論:在 Oppo熊榛、vivo舒帮、小米等機(jī)型上如果你沒有開啟后臺彈出界面權(quán)限,當(dāng)你的 App 處于后臺時(shí)澡罚,將無法通過 startActivity 方式啟動頁面

這一權(quán)限在不同型號的手機(jī)中的名稱不同肾请,以下我們統(tǒng)稱為后臺彈出界面權(quán)限留搔。對業(yè)務(wù)背景和問題定位不感興趣的話,可以直接拉到問題解決這一段落铛铁。

一民傻、業(yè)務(wù)背景

我們的 App 中有這樣一個(gè)場景:當(dāng)收到推送或者長連接消息的時(shí)候迫悠,需要啟動一個(gè) Activity 來展示相關(guān)的信息,在 Activity 展示后回復(fù)服務(wù)端 ACK 表示頁面正常展示。

統(tǒng)計(jì)數(shù)據(jù)顯示取具,ACK 與 消息發(fā)送總量的比例只有 80% 左右,產(chǎn)品經(jīng)理不干了:“你這不行啊悯仙,沒法開展業(yè)務(wù)啦囱修,巴拉巴拉...”。

二、問題定位

為了更詳細(xì)的定位問題当船,我們重新梳理了代碼流程题画,對一些關(guān)鍵節(jié)點(diǎn)(推測可能造成異常,數(shù)據(jù)丟失的地方)進(jìn)行埋點(diǎn)生年。結(jié)果:從線上埋點(diǎn)數(shù)據(jù)來看婴程,我們調(diào)用了 startActivity 方法,但是確沒有任何在目標(biāo)頁面 onCreate 方法中的埋點(diǎn)數(shù)據(jù)抱婉。

碰巧這時(shí)候產(chǎn)品同學(xué)找到我:“有一個(gè)新的業(yè)務(wù)也需要在收到長連接消息的時(shí)候展示頁面...”档叔,希望我給他展示一下已有的功能。

這個(gè)簡單啊蒸绩,我把測試機(jī)拿給產(chǎn)品:“你盯著屏幕衙四,我發(fā)一條消息,你就能看見展示的頁面了”患亿。之后我在云平臺上發(fā)了一條長連接消息传蹈,結(jié)果過了半天也沒見有頁面展示,真是尷尬步藕,不過也因此復(fù)現(xiàn)了收到長連接消息卻沒有頁面展示這一問題惦界。

反復(fù)試了幾次發(fā)現(xiàn),當(dāng) App 在前臺可見時(shí)是可以展示頁面的咙冗,但是當(dāng)按下 Home 鍵返回桌面沾歪,App 處于后臺時(shí),收到再多消息也沒有了反應(yīng)雾消。

三灾搏、問題分析

現(xiàn)在有一個(gè)可以明確的點(diǎn)是,在我的測試機(jī)上(vivo Z1)立润,App 處于后臺時(shí)狂窑,收到消息無法展示頁面,說白了就是在后臺無法通過 startActivity 的方式來啟動一個(gè)新頁面

這個(gè)時(shí)候我們需要考慮:

  1. 該問題和機(jī)型有沒有關(guān)系
  2. 該問題和 Android 系統(tǒng)版本有沒有關(guān)系
  3. 該問題是不是只在 App 處于后臺時(shí)發(fā)生

機(jī)型問題

后期通過我們更詳細(xì)的數(shù)據(jù)聚合分析桑腮,發(fā)現(xiàn)此類問題大量出現(xiàn)在 OPPO泉哈,vivo 手機(jī)上,也有少量的小米機(jī)型破讨。

我從測試那里拿了一些主流的機(jī)型和用戶使用比較多的機(jī)型進(jìn)行測試旨巷,發(fā)現(xiàn) OPPO,vivo 的手機(jī)確實(shí)有這個(gè)問題添忘,華為和三星倒沒這個(gè)問題采呐。

Android 系統(tǒng)版本問題

通過數(shù)據(jù)分析發(fā)現(xiàn),發(fā)生此問題的手機(jī) Android 系統(tǒng)版本分布很均勻搁骑,從 Android 6.0 到 Android 9.0 都有發(fā)生(當(dāng)時(shí) Android Q 還沒有推出)斧吐,因此和 Android 系統(tǒng)版本應(yīng)該沒有關(guān)系又固。

App 前后臺問題

在對少量的異常數(shù)據(jù)和重復(fù)消息進(jìn)行過濾后發(fā)現(xiàn),在調(diào)用 startActivity 方法的時(shí)候煤率,App 確實(shí)都處于后臺仰冠。

在測試的過程中發(fā)現(xiàn)有一臺 OPPO 手機(jī)可以正常展示,我們通過對比這臺 OPPO 手機(jī)和其他 OPPO 手機(jī)的各種開關(guān)蝶糯、配置后發(fā)現(xiàn)洋只,在這臺 OPPO 手機(jī)設(shè)置中打開了一個(gè)叫做 xxx 的權(quán)限開關(guān),我們又去查看 vivo 和 小米手機(jī)發(fā)現(xiàn)都有類似的權(quán)限開關(guān)昼捍。

四识虚、問題解決

我對網(wǎng)上提到的一些方法和自己的一些想法進(jìn)行一一驗(yàn)證,測試機(jī)型為:

  • OPPO R17
  • vivo Z1
  • 小米 6

測試代碼

5s 后將啟動 StartFromBackActivity 這個(gè) Activity妒茬,測試的時(shí)候需要手動將 App 切換到后臺担锤。

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
    }

    fun onDelayStartClick(view: View) {
        val intent = Intent(this@MainActivity, StartFromBackActivity::class.java)

        view.postDelayed({
            Log.d("realxz","startActivity")
            startActivity(intent)
        }, 5000)
    }
}

class StartFromBackActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_start_from_back)
        Log.d("realxz", "StartFromBackActivity onCreate")
    }
}

前臺 Service 啟動

我試想構(gòu)建一個(gè)前臺 Service,能否繞過這個(gè)限制乍钻,在這個(gè) Service 的 onStartCommand 方法中延遲啟動 Activity:

class ForegroundService : Service() {
    @RequiresApi(Build.VERSION_CODES.O)
    override fun onCreate() {
        super.onCreate()
        Log.d("realxz", "onCreate()")
        createNotificationChannel(this, "Test", "Test", NotificationManager.IMPORTANCE_HIGH)
        val builder =
            Notification.Builder(this, "Test").setSmallIcon(R.drawable.ic_launcher_background)
        startForeground(1, builder.build())
    }
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d("realxz", "onStartCommand")
        Handler().postDelayed({
            startActivity(Intent(this, StartFromBackActivity::class.java))
        }, 5000)
        return super.onStartCommand(intent, flags, startId)
    }
}

通過 adb shell dumpsys activity services com.example.realxz 命令查看當(dāng)我們的 Service 確實(shí)是一個(gè)前臺 Service



但是頁面沒有啟動成功肛循,這個(gè)方案 pass 掉,通過 logcat 日志可以發(fā)現(xiàn)我們啟動頁面的行為被系統(tǒng)攔截了:


PendingIntent 啟動

方案來源

通過 PendingIntent 的 send() 方法來執(zhí)行相關(guān)操作

/**
     * Perform the operation associated with this PendingIntent.
     *
     * @see #send(Context, int, Intent, android.app.PendingIntent.OnFinished, Handler)
     *
     * @throws CanceledException Throws CanceledException if the PendingIntent
     * is no longer allowing more intents to be sent through it.
     */
    public void send() throws CanceledException {
        send(null, 0, null, null, null, null, null);
    }

簡單的通過一個(gè)點(diǎn)擊事件來延遲發(fā)送一個(gè) PendingIntent:

    fun onPendingClick(view: View) {
        view.postDelayed({
            val intent = Intent(this, StartFromBackActivity::class.java)
            val pendingIntent = PendingIntent.getActivity(this, 0, intent, 0)
            pendingIntent.send()
            Log.e("realxz","pendingIntent.send()")
        }, 10 * 1000)
    }

此方案在 OPPO R17 上可行银择,但是在 vivo Z1 上失敗了多糠,logcat 日志顯示啟動 StartFromBackActivity 是不允許的,原因是 App 在 fobid 這個(gè)列表中浩考,這應(yīng)該是禁止后臺啟動的應(yīng)用列表夹孔。


播放音頻

以前做保活的時(shí)候怀挠,我們嘗試通過播放一段無聲的音頻析蝴,期望能提高 App 進(jìn)程的優(yōu)先級害捕,我們來嘗試一下這么操作對于啟動 Activity 有沒有幫助

fun onMusicClick(view: View) {
        val mediaPlayer = MediaPlayer.create(this, R.raw.meglive_mouth_open)
        mediaPlayer.isLooping = true
        mediaPlayer.start()

        view.postDelayed({
            val intent = Intent(this, StartFromBackActivity::class.java)
            startActivity(intent)
            Log.e("realxz", "onMusicClick startActivity")
        }, 10 * 1000)
    }

此方案同樣在 OPPO R17 上可行绿淋,vivo Z1 上仍然不行,logcat 日志與之前相同尝盼⊥讨停看上去 OPPO 的限制要小一點(diǎn),而 vivo 的限制更嚴(yán)格一點(diǎn)盾沫。

嘗試獲取系統(tǒng)權(quán)限

我們換了一種想法裁赠,能否通過 Hack 的方式來修改手機(jī)的權(quán)限設(shè)置(vivo、小米等廠商并沒有提供獲取相關(guān)權(quán)限的 API)赴精,上網(wǎng)搜了一下佩捞,發(fā)現(xiàn)有人研究過這個(gè)問題,以 vivo Z1 為例:

Android 破解vivo手機(jī)權(quán)限管理

獲取 vivo 系統(tǒng)權(quán)限設(shè)置的 APK

打開手機(jī)到具體的權(quán)限設(shè)置頁面蕾哟,通過 adb 命令一忱,adb shell dumpsys activity top 來獲取當(dāng)前棧頂 Activity 的包名相關(guān)信息莲蜘,如圖可知 vivo Z1 這款手機(jī)的權(quán)限管理的包名為 PermissionManager

然后通過 Android Studio 的 Device File Explorer 工具來打開 PermissionManager 路徑帘营,將需要的 apk票渠、vdexodex 文件拷貝出來芬迄。

通過 jadx-gui 打開 apk 文件

按照文章中所說问顷,打開 apk 的清單文件,可以找到如下的權(quán)限定義和 Provider 聲明禀梳。

<permission android:label="provider write pomission"
android:name="com.vivo.permissionmanager.provider.write"
android:protectionLevel="signatureOrSystem"/>

<provider android:name=".provider.PermissionProvider"
android:writePermission="com.vivo.permissionmanager.provider.write"
android:exported="true"
android:authorities="com.vivo.permissionmanager.provider.permission"/>

可以看見杜窄,只有系統(tǒng)應(yīng)用或者和系統(tǒng)應(yīng)用有相同簽名的應(yīng)用,才能夠有寫入數(shù)據(jù)的權(quán)限出皇,到這里基本上可以確定這個(gè)方案 GG 了羞芍。

通過 jadx-gui 打開 dex 文件

Android 破解vivo手機(jī)權(quán)限管理 這篇文章的作者在 Github 上提供了相關(guān)代碼來進(jìn)行測試,我對代碼進(jìn)行簡單的修改郊艘,來測試我們需要讀取的權(quán)限

public static int getVivoApplistPermissionStatus(Context context) {
        Uri uri2 = Uri.parse("content://com.vivo.permissionmanager.provider.permission/start_bg_activity");
        try {
            Cursor cursor = context.getContentResolver().query(uri2, null, null, null, null);
            if (cursor != null) {
                cursor.moveToFirst();
                while (cursor.moveToNext()) {
                    String pkgName = cursor.getString(cursor.getColumnIndex("pkgname"));
                    String currentState = cursor.getString(cursor.getColumnIndex("currentstate"));
                    Log.e("realxz", "----------------" + "\n");
                    Log.e("realxz", "pkg name is  " + pkgName);
                    Log.e("realxz", "current state is " + currentState);
                }
            }
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return -1;
    }

通過日志可以看到荷科,我們 App 的 state 為 1,這個(gè)時(shí)候后臺啟動 App 權(quán)限為關(guān)閉狀態(tài)纱注,手動打開權(quán)限后畏浆,這個(gè) state 會變?yōu)?0。

如果我們嘗試去修改 Provider 的內(nèi)容時(shí):

 Uri uri2 = Uri.parse("content://com.vivo.permissionmanager.provider.permission/start_bg_activity");
 ContentValues contentValues = new ContentValues();
 contentValues.put("currentstate", 0);
 context.getContentResolver().update(uri2, contentValues, "pkgname=?", new String[]{"com.example.realxz.startfromback"});

可以在 logcat 中看到以下崩潰信息:

2019-12-01 17:20:11.641 5050-5068/? E/DatabaseUtils: Writing exception to parcel
    java.lang.SecurityException: Permission Denial: writing com.vivo.permissionmanager.provider.PermissionProvider uri content://com.vivo.permissionmanager.provider.permission/start_bg_activity from pid=20117, uid=10299 requires com.vivo.permissionmanager.provider.write, or grantUriPermission()
        at android.content.ContentProvider.enforceWritePermissionInner(ContentProvider.java:851)
        at android.content.ContentProvider$Transport.enforceWritePermission(ContentProvider.java:593)
        at android.content.ContentProvider$Transport.update(ContentProvider.java:390)
        at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:211)
        at android.os.Binder.execTransact(Binder.java:708)

那么這個(gè)方案到這也就被 Pass 了狞贱。

五刻获、臨時(shí)方案

以上的方案全部以失敗告終,這時(shí)我們已經(jīng)準(zhǔn)備和產(chǎn)品商量改變業(yè)務(wù)模式來避免這個(gè)問題瞎嬉,這個(gè)時(shí)候我們有了一個(gè)新的想法蝎毡,既然在后臺無法啟動 App,那有沒有辦法將 App 移動或者說切換到前臺呢氧枣?

Android 將后臺應(yīng)用切換到前臺

判斷應(yīng)用是否在前臺

private fun isAppRunningForeground(context: Context): Boolean {
        val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val runningAppProcessList = activityManager.runningAppProcesses ?: return false

        Log.e("realxz", "running app process list size is ${runningAppProcessList.size}")
        runningAppProcessList.forEach {
            Log.e(
                "realxz",
                "running app process name is ${it.processName} and importance is ${it.importance}"
            )
            if (it.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
                && it.processName == context.applicationInfo.processName
            ) {
                return true
            }
        }
        return false
    }
  1. ActivityManager 的 getRunningAppProcesses 方法沐兵,會返回一個(gè)在當(dāng)前設(shè)備上運(yùn)行的應(yīng)用進(jìn)程列表,或者返回 null 而不會返回一個(gè) Empty List便监,經(jīng)測試發(fā)現(xiàn)扎谎,此方法僅能獲取自己的 App 信息

  2. 通過比對進(jìn)程的優(yōu)先級,來判斷 App 是否運(yùn)行在前臺烧董,importance 是一個(gè)枚舉值毁靶,定義了我們 App 是在前臺運(yùn)行,或是在后臺運(yùn)行逊移,又或是有前臺 Service 在運(yùn)行:

    @IntDef(prefix = { "IMPORTANCE_" }, value = {
                 IMPORTANCE_FOREGROUND,
                 IMPORTANCE_FOREGROUND_SERVICE,
                 IMPORTANCE_TOP_SLEEPING,
                 IMPORTANCE_VISIBLE,
                 IMPORTANCE_PERCEPTIBLE,
                 IMPORTANCE_CANT_SAVE_STATE,
                 IMPORTANCE_SERVICE,
                 IMPORTANCE_CACHED,
                 IMPORTANCE_GONE,
         })
         @Retention(RetentionPolicy.SOURCE)
         public @interface Importance {}
    

將應(yīng)用切換至前臺

private fun moveAppToFront(context: Context) {
        val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
        val runningTasks = activityManager.getRunningTasks(100)
        for (taskInfo in runningTasks) {
            if (taskInfo.topActivity!!.packageName == context.packageName) {
                activityManager.moveTaskToFront(taskInfo.id, 0)
                break
            }
        }
    }
  1. ActivityManager 的 getRunningTasks 方法雖然已標(biāo)注為 Deprecated预吆,但是仍能返回調(diào)用者自己,也就是我們自己 App 的 Task 信息
  2. 然后調(diào)用 moveTaskToFront 方法胳泉,將我們 Task 移動到棧頂拐叉,按照方法的注釋所說 “Ask that the task associated with a given task ID be moved to the front of the stack, so it is now visible to the user.” 這樣做我們的 App 就可以對用戶可見了

檢查消息

經(jīng)過上面的操作:如果應(yīng)用在前臺觅够,那么我們可以直接啟動 Activity,如果應(yīng)用不再前臺巷嚣,我們可以通過 ActivityManager 提供的方法將 App 移動到前臺喘先。

在這之后我們有兩種方式來啟動頁面

  1. 在基類的 onResume 方法中,來編寫讀取有效消息廷粒,并啟動頁面的邏輯
  2. 輪詢檢測窘拯,在收到消息后采用輪訓(xùn)的方式來將 App 切換到前臺,并啟動頁面

我采用的是輪訓(xùn)的方式:

    @SuppressLint("CheckResult")
    fun onForegroundClick(view: View) {
        Observable.intervalRange(1, 3, 3, 3, TimeUnit.SECONDS)
            .subscribe(object : Observer<Long> {
                lateinit var disposable: Disposable
                override fun onSubscribe(d: Disposable) {
                    disposable = d
                }

                override fun onNext(t: Long) {
                    Log.e("realxz", "interval long value is $t")
                    val isRunningForeground = isAppRunningForeground(this@MainActivity)
                    if (isRunningForeground) {
                        disposable.dispose()
                        // todo 讀取緩存數(shù)據(jù)坝茎,并啟動頁面
                    } else {
                        moveAppToFront(this@MainActivity)
                    }
                }

                override fun onComplete() {
                }

                override fun onError(e: Throwable) {
                }

            })
    }

這種方式通過了我手中所有的測試機(jī)的測試涤姊,有個(gè)小問題是,Vivo 手機(jī)調(diào)用一次 moveAppToFront 方法就可以切換到前臺嗤放,Oppo R17 的表現(xiàn)不太固定思喊,有時(shí)候可能需要調(diào)用三次。

在我們的項(xiàng)目中次酌,我配置的啟動次數(shù)是 3 次恨课。文章開頭所說 “ACK 與 消息發(fā)送總量的比例只有 80% 左右”,這一比例在采用這種方式后上升到了 97% 左右岳服。

六剂公、Android Q

以上所有方式在 Android Q 均失效,Google 在 Android Q 中增加了從后臺啟動 Activity 的限制吊宋,無法訪問的話纲辽,可能需要科學(xué)上網(wǎng)”

目前我們的后臺統(tǒng)計(jì)還沒有發(fā)現(xiàn)使用 Android Q 設(shè)備的用戶(用戶群體比較特殊)璃搜,但不可避免的隨著時(shí)間的推移拖吼,越來越多的用戶更新自己的設(shè)備,這一問題會徹底暴露这吻,看樣子只能通過其他的表現(xiàn)形式來實(shí)現(xiàn)這一功能了吊档。

不知道大家是否用這種強(qiáng)制提醒的業(yè)務(wù)需求,在 Android Q 下又是怎么適配或?qū)崿F(xiàn)的呢橘原?

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末籍铁,一起剝皮案震驚了整個(gè)濱河市涡上,隨后出現(xiàn)的幾起案子趾断,更是在濱河造成了極大的恐慌,老刑警劉巖吩愧,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件芋酌,死亡現(xiàn)場離奇詭異,居然都是意外死亡雁佳,警方通過查閱死者的電腦和手機(jī)脐帝,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門同云,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人堵腹,你說我怎么就攤上這事炸站。” “怎么了疚顷?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵旱易,是天一觀的道長。 經(jīng)常有香客問我腿堤,道長阀坏,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任笆檀,我火速辦了婚禮忌堂,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘酗洒。我一直安慰自己士修,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布樱衷。 她就那樣靜靜地躺著李命,像睡著了一般。 火紅的嫁衣襯著肌膚如雪箫老。 梳的紋絲不亂的頭發(fā)上封字,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天,我揣著相機(jī)與錄音耍鬓,去河邊找鬼阔籽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛牲蜀,可吹牛的內(nèi)容都是我干的笆制。 我是一名探鬼主播,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼涣达,長吁一口氣:“原來是場噩夢啊……” “哼在辆!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起度苔,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤匆篓,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后寇窑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體鸦概,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年甩骏,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了窗市。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片先慷。...
    茶點(diǎn)故事閱讀 38,018評論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖咨察,靈堂內(nèi)的尸體忽然破棺而出论熙,到底是詐尸還是另有隱情,我是刑警寧澤摄狱,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布赴肚,位于F島的核電站,受9級特大地震影響二蓝,放射性物質(zhì)發(fā)生泄漏誉券。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一刊愚、第九天 我趴在偏房一處隱蔽的房頂上張望踊跟。 院中可真熱鬧,春花似錦鸥诽、人聲如沸商玫。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拳昌。三九已至,卻和暖如春钠龙,著一層夾襖步出監(jiān)牢的瞬間炬藤,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工碴里, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留沈矿,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓咬腋,卻偏偏與公主長得像羹膳,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子根竿,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評論 2 345