安卓Settings模块浅析

一.Settings的启动流程

(Settings部分源码在packages/Settings下)

1.入口

Settings入口位于Settings.java.进入Settings会发现:它没有重写任何SettingsActiviy的方法,也没有增加任何自己的方法,唯独增加了许多静态内部类,如:

public  class Settings extends SettingsActivity {

public   static class BluetoothSettingsActivity extends SettingsActivity { /* empty */ }

public static class WirelessSettingsActivity extends SettingsActivity { /* empty */ }

public  static class SimSettingsActivity extends SettingsActivity { /*  empty */ }

.....

这些子类是为了启动特定独立的Settings选项而创建的,例如在某个应用里需要设置无线那么只需要启动WirelessSettingsActivity就可以了。因此Settings模块启动流程主要看SettingsActivity类.

2.SettingsActivity

这边只看主要方法

protected  void onCreate(Bundle savedState) {

super.onCreate(savedState);

long  startTime = System.currentTimeMillis();     //Should happen before any call to getIntent()

getMetaData();//获得Activity的额外数据mFragmentClass,如果可以获得这个数据,那么下面会去显示mFragmentClass对应的Activity。直接启动Settings模块不会获得这个数据

…...

mIsShortcut = isShortCutIntent(intent) || isLikeShortCutIntent(intent) || intent.getBooleanExtra(EXTRA_SHOW_FRAGMENT_AS_SHORTCUT,

false);//判断是否为shortcut(桌面小部件)进入

...

...

mIsShowingDashboard=className.equals(Settings.class.getName())||

className.equals(Settings.WirelessSettings.class.getName())||

className.equals(Settings.DeviceSettings.class.getName())||

className.equals(Settings.PersonalSettings.class.getName())

||className.equals(Settings.WirelessSettings.class.getName());//判断是否为主界面

//This is a "Sub Settings" when:

//- this is a real SubSetting

//- or:settings:show_fragment_as_subsetting is passed to the Intent

final   boolean isSubSettings = this instanceof SubSettings ||intent.getBooleanExtra(EXTRA_SHOW_FRAGMENT_AS_SUBSETTING,false);//判断是否为子界面

//If this is a sub settings, then apply the SubSettings Theme for  the ActionBar content insets

if (isSubSettings) {

//Check also that we are not a Theme Dialog as we don't want to override them

final int themeResId = getThemeResId();

if  (themeResId != R.style.Theme_DialogWhenLarge && themeResId!= R.style.Theme_SubSettingsDialogWhenLarge) {

           setTheme(R.style.Theme_SubSettings);

}

}

setContentView(mIsShowingDashboard?R.layout.settings_main_dashboard: R.layout.settings_main_prefs);

if (savedState != null) {

//We are restarting from a previous saved state; used that to initialize, instead

//of starting fresh.

mSearchMenuItemExpanded=savedState.getBoolean(SAVE_KEY_SEARCH_MENU_EXPANDED);

mSearchQuery= savedState.getString(SAVE_KEY_SEARCH_QUERY);

setTitleFromIntent(intent); //从Intent设置标题

ArrayList  categories =

savedState.getParcelableArrayList(SAVE_KEY_CATEGORIES);//设置列表项

if

(categories != null) {

mCategories.clear();

mCategories.addAll(categories);

setTitleFromBackStack();//从返回栈设置标题

}

mDisplayHomeAsUpEnabled

= savedState.getBoolean(SAVE_KEY_SHOW_HOME_AS_UP);//是否显示返回键

mDisplaySearch

= savedState.getBoolean(SAVE_KEY_SHOW_SEARCH);//是否显示搜索键

}

else {

if(!mIsShowingDashboard) {mDisplaySearch= false;

//UP will be shown only if it is a sub settings

if (mIsShortcut) {mDisplayHomeAsUpEnabled= isSubSettings;标题栏的显示

}

else if (isSubSettings) {

子界面    mDisplayHomeAsUpEnabled= true;

}

else {   mDisplayHomeAsUpEnabled= false; }

setTitleFromIntent(intent);

Bundle  initialArguments =

intent.getBundleExtra(EXTRA_SHOW_FRAGMENT_ARGUMENTS);

switchToFragment(initialFragmentName, initialArguments, true, false,

mInitialTitleResId, mInitialTitle, false);//跳转到指定fragment}

else {

// No UP affordance if we are displaying the main Dashboard

mDisplayHomeAsUpEnabled = false;

// Show Search affordance

mDisplaySearch = true;

mInitialTitleResId = R.string.dashboard_title;

switchToFragment(DashboardSummary.class.getName(),null, false, false,mInitialTitleResId,mInitialTitle, false);


}



DashBoardSummary.class

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

List categories =

((SettingsActivity)getActivity()).getDashboardCategories();//主界面列表项

mSummaryLoader= new SummaryLoader(getActivity(), categories);

..

… ...

@Override

public

View onCreateView(LayoutInflater inflater, ViewGroup container,

Bundle

savedInstanceState) {

return

inflater.inflate(R.layout.dashboard,

container, false);    //这个xml文件主要是一个类似ListView用来显示主界面,把category传进去

}

总结一下:ActionBar(标题栏)

继承SettingsPreferenceFragment

(各子界面).......

点击”日期与时间”

进入日期设置界面Setting.java

DashBoardSummary(主列表界面)

继承

从Setting进入

SettingActivity

DateTimeSettingsActivity

DateTimeSettings

WirelessSettings

WirenessActivity

.......

二.Settings的界面显示

继承关系:

SettingsDrawerActivity

PreferenceFragment

SettingsActivity

SettingsPreferenceFragment

Settings部分一般使用Preference组件相信大家对Perference都比较熟悉了,也就是我们常说的偏好设置,首选项设置,可以保存一些数据,例如我们在上一次使用的时候的一些内容,希望在

下一次启动后依然生效,而不需要再进行配置那么麻烦。一般这个时候我们便会使用perference键值对的方式来处理,使用碎片的首选项配置方法,即使用PreferenceFragement来实现。

public

abstract class PreferenceFragment extends Fragment

以一个列表来展示首选项对象的层级关系,这些首选项将自动地保存为SharedPreferences,数据保存到data/data/包名/shared_prefs目录下的包名_preferences.xml中

此外,所展示的首选项将会遵循系统首选项的视觉风格,通过使用XML文件来创建各个首选项的视图层级(可以被显示在许多页面)会非常简单

addPreferencesFromResource(R.xml.preferences);//加载XML布局文件

常见Preference组件:

PreferenceScreen:可以用作显示设置界面,还可以启动Activity

PreferenceCategory:类似于LinearLayout,用于组合一组可设置标题的Preference,使布局更具备层次感

SwitchPreference:类似常见控件的Switch,一个item,右侧有一个Switch控件,用于通过SharePreferences存储操作的设置值

ListPreference:类似常见控件的ListView,一个item,点击弹出一个ListView的Dialog,用于通过SharePreferences存储操作的设置值

常用监听方法有:onPreferencechanged()onPreferenceClick()

onPreferenceTreeClick等

具体属性用法参考http://blog.csdn.net/yanbober/article/details/47954653

三、SettingsProvider

代码位置

frameworks/base/packages/SettingsProvider/src/com/Android/providers/settings/DatabaseHelper.Java

frameworks/base/packages/SettingsProvider/res/values/defaults.xml

在Android启动之后,我们通常需要根据自己的一些需要来设置一些符合我们使用习惯的属性。例如:来电铃声、锁屏时间、日期格式等等。而这些属性的设置通常是有Settings为入口,通过SettingsProvider来进行的。在第一次启动Android手机的时候会在默认的文件中读取设定的值,比如

在frameworks/base/packages/SettingsProvider/res/values/defaults.xml中,需要添加相应的项

name="def_dongle_name"

translatable="false">00:00:00:00:00

600000设置关屏超时时间的默认值

102设置亮度的默认值

false设置是否允许安装非Market应用程序的默认值

这些数据主要是存储在数据库中,对应的URI为:content://settings/system和content://settings/secure,这两个是主要的,目前也只是涉及到这两个数据库表的使用。例如:

当需要获得当前wifi状态的值,调用已封装的方法如下:

Settings.Secure.getInt(getContentResolver()

, Settings.Secure.WIFI_ON);

当需要获得当前时间日期自动获取,调用如下:

Settings.System.getInt(getContentResolver()

, "auto_time");

修改调用对应的setInt方法。

在安卓6.0之前,系统修改的数据最后是存储在data/data/com.android.providers.settings/databases目录下的Settings.db中,在6.0之后该目录下存放了一个backup数据库,里面的数据不是当前系统设置的全部数据,只有一部分内容,另一个是journal数据库,无数据。现在的数据库真正的数据存储目录在data/system/users/userId(我们没开启多用户,userid为0)。

CreateShortcut

CreateShortCut.Java继承ListActivity,以ListView的形式显示可以创建桌面快捷方式的Settings子项,主要方法有:

getTargetIntent():获取特定Intent

Intent

targetIntent = new Intent(Intent.ACTION_MAIN, null);

targetIntent.addCategory("com.android.settings.SHORTCUT");

targetIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

OnListItemClick():列表点击事件

onQueryPackageManager():queryIntentActivities()方法获取特定Activity的ResolveInfo信息List

因此,创建shortcut所需的intent对象将会由CreateShortcut和其父类LuancherActivity共同构建(详见CreateShortcut的onListItemClick),在创建LuancherActivity的ActivityAdapter对象时,执行了makeListItems()方法,该方法将应用PackageManager的queryIntentActivities来按照intent对象查询符合条件的activity。intent是从getTargetIntent函数返回的。不难发明,要想在CreateShortcut中显示,Activity在必要有

android:name="com.android.settings.SHORTCUT" />

Battery模块

一、使用类

BatteryStatsService在内部创建BatteryStatsImpl实例,并传入耗电量记录文件batterystats.bin;

ActivityManagerService创建并初始化BatteryStatsService,并传入耗电量记录文件batterystats.bin;

BatteryStatsHelper

--计算所有应用的耗电(A

helper class for retrieving the power usage information for all

applications and services.)

BatteryStats(abstract)实际用的是BatteryStatsImpl(providing

access to battery usage statistics, including information on

wakelocks, processes, packages, and services.)

BatteryStatsImpl:提供App各部件运行时间。

BatterySipper表示一个应用或服务的耗电量信息,包括

包名,图标,耗电量,使用时间,cpu时间,GPS、WIFI、等时间;而此类主要的方法加载名字和图标loadNameAndIcon(),从PackageManager中加载icon,name,packageName,并发送message到mHandler以便更新列表的显示。

PowerProfile

--实际是从xml(power_profile.xml)中读出里各个硬件cpu,屏幕蓝牙wifi之类的耗电基值(记录每种硬件1秒钟耗多少电)。这样,

根据各个应用的运行时间就可以算出耗电了。(Reports

power consumption values for various device activities. Reads values

from an XML file.)

BatteryEntry

--对应的包名和icon,作为UI的数据来源。(Wraps

the power usage data of a BatterySipper with information about

package name and icon image)

二、界面显示

(

packages/apps/Settings/src/com/android/settings/fuelgauge下)

我们从Settings中Battery的入口处开始看起,先看PowerUsageSummary.java类,这个类的抬头描述是:将应用自从上次拔下USB线(或者交流电等其它充电方式)的耗电量排成列表。这个类的具体的作用是显示当前电量以及筛选耗电量最多的前10个应用,并且展示在ListView列表中。n8976代码中PowerUsageSummary继承PowerUsageBase,但大体过程如下:

1、实例化广播mBatteryInfoReceiver:用来在收到电量变化的广播后更新耗电量列表refreshStats()

.

private

BatteryStatsHelper mStatsHelper;

private

BroadcastReceiver mBatteryInfoReceiver = new BroadcastReceiver() {

@Override

public

void onReceive(Context context, Intent intent) {

String

action = intent.getAction();

if

(Intent.ACTION_BATTERY_CHANGED.equals(action)

&&

updateBatteryStatus(intent)) {

if

(!mHandler.hasMessages(MSG_REFRESH_STATS)) {

mHandler.sendEmptyMessageDelayed(MSG_REFRESH_STATS,

500);

}

}

}

};

2、实例化mHandler:用来传给mStatsHelper,

Handler

mHandler = new Handler() {

@Override

public

void handleMessage(Message msg) {

switch

(msg.what) {

case

BatteryEntry.MSG_UPDATE_NAME_ICON:

BatteryEntry

entry = (BatteryEntry) msg.obj;

…..........................

case

MSG_REFRESH_STATS:

mStatsHelper.clearStats();

refreshStats();

}

super.handleMessage(msg);

}

};

3、onAttach()实例化BatteryStatsHelper,即mStatsHelper

@Override

public

void onAttach(Activity activity) {

super.onAttach(activity);

mUm

= (UserManager) activity.getSystemService(Context.USER_SERVICE);

mStatsHelper

= new BatteryStatsHelper(activity, true);

}

4、onCreate()初始化mStatsHelper;

加载R.xml.power_usage_summary,并定位preference

@Override

public

void onCreate(Bundle icicle) {

super.onCreate(icicle);

mStatsHelper.create(icicle);

addPreferencesFromResource(R.xml.power_usage_summary);

mAppListGroup

= (PreferenceGroup) findPreference(KEY_APP_LIST);

setHasOptionsMenu(true);

}

5、onResume()注册广播mBatteryInfoReceiver;

刷新耗电量列表refreshStats()

@Override

public

void onResume() {

super.onResume();

BatteryStatsHelper.dropFile(getActivity(),

BATTERY_HISTORY_FILE);

updateBatteryStatus(getActivity().registerReceiver(mBatteryInfoReceiver,

new

IntentFilter(Intent.ACTION_BATTERY_CHANGED)));

if

(mHandler.hasMessages(MSG_REFRESH_STATS)) {

mHandler.removeMessages(MSG_REFRESH_STATS);

mStatsHelper.clearStats();

}

refreshStats();

}

6、onPause()注销广播mBatteryInfoReceiver;mStatsHelper.pause();移除mHandler的message

@Override

public

void onPause() {

BatteryEntry.stopRequestQueue();

mHandler.removeMessages(BatteryEntry.MSG_UPDATE_NAME_ICON);

getActivity().unregisterReceiver(mBatteryInfoReceiver);

super.onPause();

}

7、onDestory()

mStatsHelper.destroy()

@Override

public

void onDestroy() {

super.onDestroy();

if

(getActivity().isChangingConfigurations()) {

mStatsHelper.storeState();

BatteryEntry.clearUidCache();

}

}

三.preference显示(以8976为例)

1.电量百分比SwitchPreference:

监听开关,若开关打开则在子线程中发送广播:

Intent

intent = new

Intent("android.intent.action.BATTERY_SHOW_PERCENTAGE");

ActivityManagerNative.broadcastStickyIntent(intent,

null,UserHandle.USER_ALL);

//

UserHandle.USER_ALL指对所有用户

2.历史用量BatteryHistoryPreference:

主要通过historyPref.setStats(mStatsHelper)将BatteryStatsHelper实例传入,BatteryStatsHelper可以获取所有应用的耗电信息;

3.耗电列表AppListGroup:

主要在refreshStats()方法里,先清空mAppListGroup列表,并设置其排序不按照添加顺序显示,接着添加耗电量的总信息mHistPref,通过mStatsHelper获取耗电量的列表List

usageList,最后依次遍历usageList,生成对应的preference(在8976中是PowerGaugePreference,把每个BatteryEntry实例传入),添加到mAppListGroup中;在遍历过程中可以通过

contine,筛选出符合特定条件的BatterySipper,例如:

if

(((int) (percentOfTotal + .5)) < 1) {

continue;

}

//电量低于0.5%就不显示

四、耗电量的计算

耗电量的计算在BatteryStatsHelper.java类中,计算耗电量的是processAppUsage方法。计算了手机上的每种硬件没秒钟耗费了多少电量,每个应用运行时使用了哪几种硬件,每个硬件使用了所长时间。

private

void processAppUsage(SparseArray  asUsers) {

//代表一个用户对象,可以理解为这个类里面存储了用户的相关信息.

final

boolean forAllUsers = (asUsers.get(UserHandle.USER_ALL) != null);

//判断该次计算是否针对所有用户,通过UserHandle的USER_ALL值来判断,该值为-1

mStatsPeriod

= mTypeBatteryRealtimeUs;//此次统计电量的时间间隔.

BatterySipper

osSipper = null;

final

SparseArray uidStats = mStats.getUidStats();

final

int NU = uidStats.size();      // osSipper里面可以存储一些后续我们要计算的值,然后通过BatteryStats类对象mStats来得到一个包含Uid的对象的SparseArray组数,然后计算了一下这个数组的大小。

/**

*计算每个Uid代表的App的耗电量,因为BatterySipper可计算的类型有三种:应用,系统服务,硬件类型,所以这个地方传入的是DrainType.APP.还有其他类型如下:(定义在BatterySipper.java中)

/*public

enum DrainType {

IDLE,

CELL,

PHONE,

WIFI,

BLUETOOTH,

FLASHLIGHT,

SCREEN,

APP,

USER,

UNACCOUNTED,

OVERCOUNTED,

CAMERA

}*/

*/

for  (int iu = 0; iu < NU; iu++) {

final Uid u = uidStats.valueAt(iu);

final  BatterySipper app = new BatterySipper(BatterySipper.DrainType.APP,u, 0);

/*

*6.0的对各个模块的消耗都交给了单独的类去计算,这些类都继承于PowerCalculator抽象类:

*蓝牙耗电:BluetoothPowerCalculator.java

*摄像头耗电:CameraPowerCalculator.java

*CPU耗电:mCpuPowerCalculator.java

*手电筒耗电: FlashlightPowerCalculator.java

*无线电耗电: MobileRadioPowerCalculator.java

*传感器耗电: SensorPowerCalculatormSensorPowerCalculator.java

*Wakelock耗电: WakelockPowerCalculator.java

*Wifi耗电: WifiPowerCalculator.java

*

*其中mStatsType的值为BatteryStats.STATS_SINCE_CHARGED,代表了我们的计算规则是从上次充满电后数据,还有一种规则是*STATS_SINCE_UNPLUGGED是拔掉USB线后的数据。而mRawRealtimUs是当前时间,mRawUptimeUs是运行时间。

*

*/

mCpuPowerCalculator.calculateApp(app,u, mRawRealtimeUs, mRawUptimeUs,mStatsType);

mWakelockPowerCalculator.calculateApp(app,u, mRawRealtimeUs, mRawUptimeUs, mStatsType);

mMobileRadioPowerCalculator.calculateApp(app,u, mRawRealtimeUs, mRawUptimeUs, mStatsType);

mWifiPowerCalculator.calculateApp(app,u, mRawRealtimeUs, mRawUptimeUs, mStatsType);

mBluetoothPowerCalculator.calculateApp(app,u, mRawRealtimeUs, mRawUptimeUs, mStatsType);

mSensorPowerCalculator.calculateApp(app,u, mRawRealtimeUs, mRawUptimeUs, mStatsType);

mCameraPowerCalculator.calculateApp(app,u, mRawRealtimeUs, mRawUptimeUs, mStatsType);

mFlashlightPowerCalculator.calculateApp(app,u, mRawRealtimeUs, mRawUptimeUs, mStatsType);

final  double totalPower = app.sumPower();// sumPower()计算总耗电量

if (DEBUG && totalPower != 0) {

Log.d(TAG, String.format("UID %d: total power=%s", u.getUid(),makemAh(totalPower)));

}

//  Add the app to the list if it is consuming power.

//添加进mUsageList

if  (totalPower != 0 || u.getUid() == 0) {

//

// Add the app to the app list, WiFi, Bluetooth, etc, or into "Other

Users" list.

//

final int uid = app.getUid();

final  int userId = UserHandle.getUserId(uid);

//如果是wifi和蓝牙就添加到mWifiSippers和mBluetoothSippers

if (uid == Process.WIFI_UID) {

  mWifiSippers.add(app);

}

else if (uid == Process.BLUETOOTH_UID) {

mBluetoothSippers.add(app);

}

else if (!forAllUsers && asUsers.get(userId) == null

&&

UserHandle.getAppId(uid) >= Process.FIRST_APPLICATION_UID) {

//

We are told to just report this user's apps as one large entry.

//如果我们的系统是单用户系统,且当前的userId号不在我们的统计范围内,且其进程id号是大于Process.FIRST_APPLICATION_UID(10000,系统分配给普通应用的其实id号),我们就要将其存放到mUserSippers数组中,

List  list = mUserSippers.get(userId);

if (list == null) {

list = new ArrayList<>();

mUserSippers.put(userId, list);

}

list.add(app);

} else {

mUsageList.add(app);

}

if  (uid == 0) {

    osSipper = app;//存入

  }

}

}  if (osSipper != null) {

// The device has probably been awake for longer than the screen on

// time and application wake lock time would account for.  Assign

// this remainder to the OS, if possible.

mWakelockPowerCalculator.calculateRemaining(osSipper,mStats, mRawRealtimeUs,

mRawUptimeUs,mStatsType);

osSipper.sumPower();//最终电量

}

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容