Android--Recovery模組之恢復出廠設定
(一)、 在進行詳細流程分析之前,先看一下幾個重要概念:
一、Recovery的工作需要整個軟體平臺的配合,從架構角度看,有三個部分:
1、Main system:用boot.img啟動的Linux系統,Android的正常工作模式。
2、Recovery:用recovery.img啟動的Linux系統,只要是執行Recovery程式。
3、Bootloader:除了載入、啟動系統,還會通過讀取flash的MISC分割槽獲取來自Main system和Recovery的訊息,並以此覺得做何種操作。
在Recovery的工作流程中,上述三個實體的通訊是必不可少的。
二、通訊介面
1、BCB(bootloader control block)
Bootloader和Recovery模組以及主系統之間的通訊是通過系統的misc分割槽來完成的,描述misc分割槽的資料結構是bootloader_message,定義如下:
command:command欄位中儲存的是命令,當想要重啟進入Recovery模式,或升級radio/bootloader firmware時,會更新這個欄位(如果它的值是“boot-recovery”,系統進入Recovery模式;如果它的值是“update-radio”或“update-hboot”,系統進入更新firmware模式,這個更新過程由bootloader完成;如果command中的值為null,則進入主系統,正常啟動);當firmware更新完畢,為了啟動後進入Recovery做最終的清除,bootloader還會修改它。struct bootloader_message { char command[32]; char status[32]; char recovery[768]; // The 'recovery' field used to be 1024 bytes. It has only ever // been used to store the recovery command line, so 768 bytes // should be plenty. We carve off the last 256 bytes to store the // stage string (for multistage packages) and possible future // expansion. char stage[32]; char reserved[224]; };
status:status欄位儲存的是更新的結果。update-radio或update-hboot完成後,由Recovery或Bootloader將更新結果寫入到這個欄位中。
recovery:recovery欄位存放的是recovery模組的啟動引數。僅被Main system寫入,用於向Recovery傳送訊息,必須以“recovery\n”開頭,否則這個欄位中的所有內容會被忽略。這一項的內容中“recovery/\n”以後的部分,是/cache/recovery/command支援的命令,可以認為這是在Recovery操作過程中,對命令操作的備份。Recovery也會更新這個域的資訊,執行某操作前把該操作命令寫到recovery域,並更新command域,操作完成後再清空recovery域及command域,這樣在進入Main system之前,就能確保操作被執行。
(二)詳細流程分析
一、入口流程分析
1、在MasterClearConfirm.java(/packages/apps/settings/src/com/android/settings/MasterClearConfirm.java)中顯示恢復出廠提示和對應button,點選button後呼叫button的click方法。如果選中了mEraseSdCard(格式化SD卡),則啟動ExternalStorageFormatter的服務;否則傳送Intent.ACTION_MASTER_CLEAR廣播。
private void doMasterClear() {
if (mEraseSdCard) {
Intent intent = new Intent(ExternalStorageFormatter.FORMAT_AND_FACTORY_RESET);
intent.putExtra(Intent.EXTRA_REASON, "MasterClearConfirm");
intent.setComponent(ExternalStorageFormatter.COMPONENT_NAME);
getActivity().startService(intent);
} else {
Intent intent = new Intent(Intent.ACTION_MASTER_CLEAR);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
intent.putExtra(Intent.EXTRA_REASON, "MasterClearConfirm");
getActivity().sendBroadcast(intent);
// Intent handling is asynchronous -- assume it will happen soon.
}
}
2、定位到註冊改廣播的地方,/frameworks/base/core/res/AndroidMenifest.xml中註冊了改廣播接收器。
<receiver android:name="com.android.server.MasterClearReceiver"
android:permission="android.permission.MASTER_CLEAR">
<intent-filter
android:priority="100" >
<!-- For Checkin, Settings, etc.: action=MASTER_CLEAR -->
<action android:name="android.intent.action.MASTER_CLEAR" />
<!-- MCS always uses REMOTE_INTENT: category=MASTER_CLEAR -->
<action android:name="com.google.android.c2dm.intent.RECEIVE" />
<category android:name="android.intent.category.MASTER_CLEAR" />
</intent-filter>
</receiver>
3、對應的在MasterClearReceiver(\frameworks\base\services\core\java\com\android\server)會接受此廣播,在onReceive()方法中會呼叫RecoverySystem.rebootWipeUserData()方法,並傳遞三個引數:context;shutdown=false;reason=“MasterClearConfirm”。
@Override
public void onReceive(final Context context, final Intent intent) {
if (intent.getAction().equals(Intent.ACTION_REMOTE_INTENT)) {
if (!"google.com".equals(intent.getStringExtra("from"))) {
Slog.w(TAG, "Ignoring master clear request -- not from trusted server.");
return;
}
}
final boolean shutdown = intent.getBooleanExtra("shutdown", false);
final String reason = intent.getStringExtra(Intent.EXTRA_REASON);//"MasterClearConfirm"
Slog.w(TAG, "!!! FACTORY RESET !!!");
// The reboot call is blocking, so we need to do it on another thread.
Thread thr = new Thread("Reboot") {
@Override
public void run() {
try {
RecoverySystem.rebootWipeUserData(context, shutdown, reason);//shutdown=false;reason="MasterClearConfirm"
Log.wtf(TAG, "Still running after master clear?!");
} catch (IOException e) {
Slog.e(TAG, "Can't perform master clear/factory reset", e);
} catch (SecurityException e) {
Slog.e(TAG, "Can't perform master clear/factory reset", e);
}
}
};
thr.start();
}
二、應用層流程分析
設定模組中恢復出廠設定,不管是否刪除SD卡,最終都會執行如下兩步:
1、 往/cache/recovery/command檔案中寫入命令欄位
2、 重啟系統,進入recovery模式
具體可參考framework/base/core/java/android/os/RecoverySystem.java檔案,程式碼片段如下:
/**
* Reboots the device and wipes the user data and cache
* partitions. This is sometimes called a "factory reset", which
* is something of a misnomer because the system partition is not
* restored to its factory state. Requires the
* {@link android.Manifest.permission#REBOOT} permission.
*重啟裝置並且擦除使用者data和cache分割槽;難免有些用詞不當,因為system分割槽沒有回覆到出廠狀態。
* @param context the Context to use
* @param shutdown if true, the device will be powered down after
* the wipe completes, rather than being rebooted
* back to the regular system.
* true,擦除完成後關閉裝置;否則重新引導回正常系統;
* @throws IOException if writing the recovery command file
* fails, or if the reboot itself fails.
* @throws SecurityException if the current user is not allowed to wipe data.
* 如果當前使用者不允許擦除資料,丟擲SecurityException。
* @hide
*/
public static void rebootWipeUserData(Context context, boolean shutdown, String reason)
throws IOException {
UserManager um = (UserManager) context.getSystemService(Context.USER_SERVICE);
if (um.hasUserRestriction(UserManager.DISALLOW_FACTORY_RESET)) {
throw new SecurityException("Wiping data is not allowed for this user.");
}
final ConditionVariable condition = new ConditionVariable();
Intent intent = new Intent("android.intent.action.MASTER_CLEAR_NOTIFICATION");
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
context.sendOrderedBroadcastAsUser(intent, UserHandle.OWNER,
android.Manifest.permission.MASTER_CLEAR,
new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
condition.open();
}
}, null, 0, null, null);//傳送廣播:android.intent.action.MASTER_CLEAR_NOTIFICATION,通知所有接收端處理相關行為
// Block until the ordered broadcast has completed.
condition.block();//等待所有接收完成動作
String shutdownArg = null;
if (shutdown) {
shutdownArg = "--shutdown_after";
}//從上面傳遞引數,此處shutdown為false
String reasonArg = null;
if (!TextUtils.isEmpty(reason)) {
reasonArg = "--reason=" + sanitizeArg(reason);
}//reason為“MasterClearConfirm”
final String localeArg = "--locale=" + Locale.getDefault().toString();
bootCommand(context, shutdownArg, "--wipe_data", reasonArg, localeArg);
}
<pre name="code" class="java"> /** Used to communicate with recovery. See bootable/recovery/recovery.c. */
private static File RECOVERY_DIR = new File("/cache/recovery");
private static File COMMAND_FILE = new File(RECOVERY_DIR, "command");
private static File LOG_FILE = new File(RECOVERY_DIR, "log");
/**
* Reboot into the recovery system with the supplied argument.
* @param arg to pass to the recovery utility.
* @throws IOException if something goes wrong.
*/
private static void bootCommand(Context context, String arg) throws IOException {
RECOVERY_DIR.mkdirs(); // In case we need it
COMMAND_FILE.delete(); // In case it's not writable
LOG_FILE.delete();
FileWriter command = new FileWriter(COMMAND_FILE);
try {
command.write(arg);//將arg引數寫入/cache/recovery/command檔案中
command.write("\n");
} finally {
command.close();
}
// Having written the command file, go ahead and reboot
PowerManager pm = (PowerManager) context.getSystemService(Context.POWER_SERVICE);
pm.reboot("recovery");//重啟,進入recovery模式。
throw new IOException("Reboot failed (no permissions?)");
}
在rebootWipeUserData方法中,會呼叫bootCommand方法,並傳入引數--wipe_data命令欄位(命令欄位:--wipe_data--reason="MasterClearConfirm"--locale="zh_CN");在bootCommand方法中將命令欄位寫入/cache/recovery/command檔案中,並重啟進入recovery模式,recovery服務會通過讀取此引數來擦除data和cache分割槽。詳細流程如下。
三、重啟流程分析
1、在bootCommand中將命令欄位寫入/cache/recovery/command檔案後,呼叫PowerManager的reboot方法,程式碼如下:
public void reboot(String reason) {
try {
mService.reboot(false, reason, true);
} catch (RemoteException e) {
}
}
進而呼叫PowerManagerService物件的reboot方法,傳遞三個引數:confirm=false,不會顯示重啟確認對話方塊;reason=“recovery”,導致重啟的原因是進行恢復出廠設定,重啟後進入recovery模式;wait=true,等待重啟完成。程式碼如下:
/**
* Reboots the device.
*
* @param confirm If true, shows a reboot confirmation dialog.
* @param reason The reason for the reboot, or null if none.
* @param wait If true, this call waits for the reboot to complete and does not return.
*/
@Override // Binder call
public void reboot(boolean confirm, String reason, boolean wait) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.REBOOT, null);
if (PowerManager.REBOOT_RECOVERY.equals(reason)) {
mContext.enforceCallingOrSelfPermission(android.Manifest.permission.RECOVERY, null);
}
final long ident = Binder.clearCallingIdentity();
try {
shutdownOrRebootInternal(false, confirm, reason, wait);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
private void shutdownOrRebootInternal(final boolean shutdown, final boolean confirm,
final String reason, boolean wait) {//shutdown=false;confirm=false;reason=“recovery”;wait=true;
if (mHandler == null || !mSystemReady) {
throw new IllegalStateException("Too early to call shutdown() or reboot()");
}
Runnable runnable = new Runnable() {
@Override
public void run() {
synchronized (this) {
if (shutdown) {
ShutdownThread.shutdown(mContext, confirm);
} else {
ShutdownThread.reboot(mContext, reason, confirm);//恢復出廠設定時執行reboot
}
}
}
};
// ShutdownThread must run on a looper capable of displaying the UI.執行在顯示使用者介面介面
Message msg = Message.obtain(mHandler, runnable);
msg.setAsynchronous(true);//設定為非同步訊息
mHandler.sendMessage(msg);
// PowerManager.reboot() is documented not to return so just wait for the inevitable.//reboot沒有返回值
if (wait) {
synchronized (runnable) {
while (true) {
try {
runnable.wait();//掛起,其他執行緒呼叫notify時才會重新啟用
} catch (InterruptedException e) {
}
}
}
}
}
由於傳遞的shutdown引數為false,此處執行ShutdownThread類的reboot方法,並且傳遞引數reason為“recovery”;confirm為false。ShutdownThread類位於/frameworks/base/services/core/java/com/android/server/power中。
/**
* Request a clean shutdown, waiting for subsystems to clean up their
* state etc. Must be called from a Looper thread in which its UI
* is shown.
*
* @param context Context used to display the shutdown progress dialog.
* @param reason code to pass to the kernel (e.g. "recovery"), or null.
* @param confirm true if user confirmation is needed before shutting down.
*/
public static void reboot(final Context context, String reason, boolean confirm) {
mReboot = true;
mRebootSafeMode = false;
mRebootReason = reason;
shutdownInner(context, confirm);
}
在reboot方法中分別為變數賦值:mReboot=true;mRebootSafeMode=false;mRebootReason=reason=“recovery”;之後呼叫shutdownInner方法。
static void shutdownInner(final Context context, boolean confirm) {
// ensure that only one thread is trying to power down.
// any additional calls are just returned
synchronized (sIsStartedGuard) {
if (sIsStarted) {
Log.d(TAG, "Request to shutdown already running, returning.");
return;
}
}
boolean showRebootOption = false;
final int longPressBehavior = context.getResources().getInteger(
com.android.internal.R.integer.config_longPressOnPowerBehavior);
int resourceId = mRebootSafeMode
? com.android.internal.R.string.reboot_safemode_confirm
: (longPressBehavior == 2
? com.android.internal.R.string.shutdown_confirm_question
: com.android.internal.R.string.shutdown_confirm);
if (showRebootOption && !mRebootSafeMode) {
resourceId = com.android.internal.R.string.reboot_confirm;
}
Log.d(TAG, "Notifying thread to start shutdown longPressBehavior=" + longPressBehavior);
if (confirm) {
final CloseDialogReceiver closer = new CloseDialogReceiver(context);
if (sConfirmDialog != null) {
sConfirmDialog.dismiss();
}
closer.dialog = sConfirmDialog;
sConfirmDialog.setOnDismissListener(closer);
sConfirmDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_KEYGUARD_DIALOG);
sConfirmDialog.show();
} else {
beginShutdownSequence(context);
}
}
上面傳遞的confirm為false,因此這裡直接執行beginShutdownSequence方法。
private static void beginShutdownSequence(Context context) {
synchronized (sIsStartedGuard) {
if (sIsStarted) {
Log.d(TAG, "Shutdown sequence already running, returning.");
return;
}
sIsStarted = true;
}
。。。。。。
sInstance.start();
}
beginShutdownSequence方法中進行關機的一些準備操作,包括停止播放音樂,顯示關機對話方塊,設定CPU鎖避免cpu休眠,screen鎖防止螢幕變暗(防止機器在關機過程中休眠);最後執行sInstance.start方法。sInstance是ShutdownThread的物件,ShutdownThread整合Thread,因此當執行sInstance.start方法的時候就是允許Thread的run方法。
/**
* Makes sure we handle the shutdown gracefully.
* Shuts off power regardless of radio and bluetooth state if the alloted time has passed.
*/
public void run() {
BroadcastReceiver br = new BroadcastReceiver() {
@Override public void onReceive(Context context, Intent intent) {
// We don't allow apps to cancel this, so ignore the result.
actionDone();
}
};
/*
* Write a system property in case the system_server reboots before we
* get to the actual hardware restart. If that happens, we'll retry at
* the beginning of the SystemServer startup.這裡設定重啟的原因為recovery
*/
{
String reason = (mReboot ? "1" : "0") + (mRebootReason != null ? mRebootReason : "");
SystemProperties.set(SHUTDOWN_ACTION_PROPERTY, reason);
}
/*
* If we are rebooting into safe mode, write a system property
* indicating so.
*/
if (mRebootSafeMode) {
SystemProperties.set(REBOOT_SAFEMODE_PROPERTY, "1");
}
Log.i(TAG, "Sending shutdown broadcast...");
// First send the high-level shut down broadcast.
mActionDone = false;
Intent intent = new Intent(Intent.ACTION_SHUTDOWN);
intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
mContext.sendOrderedBroadcastAsUser(intent,//傳送關機廣播,通知各app儲存資料;
UserHandle.ALL, null, br, mHandler, 0, null, null);
final long endTime = SystemClock.elapsedRealtime() + MAX_BROADCAST_TIME;
synchronized (mActionDoneSync) {
while (!mActionDone) {
long delay = endTime - SystemClock.elapsedRealtime();
if (delay <= 0) {
Log.w(TAG, "Shutdown broadcast timed out");
break;
}
try {
mActionDoneSync.wait(delay);
} catch (InterruptedException e) {
}
}
}
Log.i(TAG, "Shutting down activity manager...");
final IActivityManager am =
ActivityManagerNative.asInterface(ServiceManager.checkService("activity"));
if (am != null) {
try {
am.shutdown(MAX_BROADCAST_TIME);//關閉activity manager,即關閉AppOpsService,UsageStatsService,BatteryStatsService
} catch (RemoteException e) {
}
}
Log.i(TAG, "Shutting down package manager...");
final PackageManagerService pm = (PackageManagerService)
ServiceManager.getService("package");
if (pm != null) {
pm.shutdown();//關閉package manager,儲存app使用時間到 disk裡
}
// Shutdown radios.
shutdownRadios(MAX_RADIO_WAIT_TIME);//shutdown radio[NFC,BT,MODEM]
// Shutdown MountService to ensure media is in a safe state
IMountShutdownObserver observer = new IMountShutdownObserver.Stub() {
public void onShutDownComplete(int statusCode) throws RemoteException {
Log.w(TAG, "Result code " + statusCode + " from MountService.shutdown");
actionDone();
}
};
Log.i(TAG, "Shutting down MountService");
// Set initial variables and time out time.特別要注意這裡,很可能會導致關機失敗
mActionDone = false;
final long endShutTime = SystemClock.elapsedRealtime() + MAX_SHUTDOWN_WAIT_TIME;
synchronized (mActionDoneSync) {
try {
final IMountService mount = IMountService.Stub.asInterface(
ServiceManager.checkService("mount"));
if (mount != null) {
mount.shutdown(observer);
} else {
Log.w(TAG, "MountService unavailable for shutdown");
}
} catch (Exception e) {
Log.e(TAG, "Exception during MountService shutdown", e);
}
while (!mActionDone) {
long delay = endShutTime - SystemClock.elapsedRealtime();
if (delay <= 0) {
Log.w(TAG, "Shutdown wait timed out");
break;
}
try {
mActionDoneSync.wait(delay);
} catch (InterruptedException e) {
}
}
}
rebootOrShutdown(mReboot, mRebootReason);
}
在run方法中設定系統屬性“sys.shutdown.requested”為“recovery”,之後關閉一些系統服務,最後呼叫rebootOrShutdown方法,並傳遞引數mReboot=true,mRebootReason=“recovery”。
/**
* Do not call this directly. Use {@link #reboot(Context, String, boolean)}
* or {@link #shutdown(Context, boolean)} instead.
*
* @param reboot true to reboot or false to shutdown
* @param reason reason for reboot
*/
public static void rebootOrShutdown(boolean reboot, String reason) {
if (reboot) {
Log.i(TAG, "Rebooting, reason: " + reason);
PowerManagerService.lowLevelReboot(reason);//重啟
Log.e(TAG, "Reboot failed, will attempt shutdown instead");
} else if (SHUTDOWN_VIBRATE_MS > 0) {//如果是執行關機操作,執行震動
// vibrate before shutting down
Vibrator vibrator = new SystemVibrator();
try {
vibrator.vibrate(SHUTDOWN_VIBRATE_MS, VIBRATION_ATTRIBUTES);
} catch (Exception e) {
// Failure to vibrate shouldn't interrupt shutdown. Just log it.
Log.w(TAG, "Failed to vibrate during shutdown.", e);
}
// vibrator is asynchronous so we need to wait to avoid shutting down too soon.
try {
Thread.sleep(SHUTDOWN_VIBRATE_MS);
} catch (InterruptedException unused) {
}
}
// Shutdown power
Log.i(TAG, "Performing low-level shutdown...");
PowerManagerService.lowLevelShutdown();//關機
}
在rebootOrShutdown方法中根據引數reboot為true,主要是呼叫了PowerManagerService.lowLevelReboot方法。
/**
* Low-level function to reboot the device. On success, this
* function doesn't return. If more than 20 seconds passes from
* the time a reboot is requested (120 seconds for reboot to
* recovery), this method returns.
*
* @param reason code to pass to the kernel (e.g. "recovery"), or null.
*/
public static void lowLevelReboot(String reason) {
if (reason == null) {
reason = "";
}
long duration;
if (reason.equals(PowerManager.REBOOT_RECOVERY)) {//recovery
// If we are rebooting to go into recovery, instead of
// setting sys.powerctl directly we'll start the
// pre-recovery service which will do some preparation for
// recovery and then reboot for us.
//
// This preparation can take more than 20 seconds if
// there's a very large update package, so lengthen the
// timeout.
SystemProperties.set("ctl.start", "pre-recovery");
duration = 120 * 1000L;
} else {
SystemProperties.set("sys.powerctl", "reboot," + reason);
duration = 20 * 1000L;
}
try {
Thread.sleep(duration);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
在lowLevelReboot方法中,主要是呼叫SystemProperties.set("ctl.start", "pre-recovery");,啟動“pre-recovery”服務。正是這個動作觸發關機流程繼續往下走,“pre-recovery”服務定義在init.rc(/system/core/rootdir)中。如下所示:
service pre-recovery /system/bin/uncrypt
class main
disabled //不自動啟動
oneshot
底層的內容不再分析,執行關機流程,之後開機,底層判斷節點後進入recovery模式,recovery.img釋放完成後,進入開機流程。
四、恢復模式流程分析
重啟後,從recovery模式的init.rc檔案中可以看到啟動recovery服務,具體可參考bootable/recovery/etc/init.rc檔案,程式碼片段如下:
service recovery /sbin/recovery
seclabel u:r:recovery:s0
1、recovery服務的主函式在bootable/recovery/recovery.c檔案中,main函式結構清晰,主要流程分析如下:
int main(int argc, char **argv) {
time_t start = time(NULL);
redirect_stdio(TEMPORARY_LOG_FILE);// 將標準輸出和標準錯誤輸出重定向到/tmp/recovery.log。
// If this binary is started with the single argument "--adbd",
// instead of being the normal recovery binary, it turns into kind
// of a stripped-down version of adbd that only supports the
// 'sideload' command. Note this must be a real argument, not
// anything in the command file or bootloader control block; the
// only way recovery should be run with this argument is when it
// starts a copy of itself from the apply_from_adb() function.
if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
adb_main();//如果啟動引數帶有adbd,則作為adbd的demon程序執行
return 0;
}
printf("Starting recovery (pid %d) on %s", getpid(), ctime(&start));
load_volume_table();//讀取/etc/recovery.fstab檔案中的分割槽內容
ensure_path_mounted(LAST_LOG_FILE);// "/cache/recovery/last_log"
rotate_last_logs(KEEP_LOG_COUNT);//將last_log檔案重新命名,Rename last_log -> last_log.1 -> last_log.2 -> ... -> last_log.$max
get_args(&argc, &argv);//獲得啟動引數;將"boot-recovery"和“--wipe_data”寫入BCB(bootloader control block)
const char *send_intent = NULL;
const char *update_package = NULL;
int wipe_data = 0, wipe_cache = 0, show_text = 0;
bool just_exit = false;
bool shutdown_after = false;
int arg;
while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {//解析命令列選項引數,
switch (arg) {
case 's': send_intent = optarg; break;
case 'u': update_package = optarg; break;//升級系統
case 'w': wipe_data = wipe_cache = 1; break;//擦除資料
case 'c': wipe_cache = 1; break;//擦除CACHE分割槽
case 't': show_text = 1; break;//指示升級時是否顯示UI
case 'x': just_exit = true; break;//退出
case 'l': locale = optarg; break;//指定Local
case 'g': {
if (stage == NULL || *stage == '\0') {
char buffer[20] = "1/";
strncat(buffer, optarg, sizeof(buffer)-3);
stage = strdup(buffer);
}
break;
}
case 'p': shutdown_after = true; break;//退出recovery後關閉系統
case 'r': reason = optarg; break;
case '?':
LOGE("Invalid command argument\n");
continue;
}
}
if (locale == NULL) {//如果沒有指定local
load_locale_from_cache();//從檔案/cache/recovery/last_local中讀取
}
printf("locale is [%s]\n", locale);
printf("stage is [%s]\n", stage);
printf("reason is [%s]\n", reason);
Device* device = make_device();//建立DefaultDevice類物件,<span style="font-size: 11.8181819915771px; font-family: Arial, Helvetica, sans-serif;">http://blog.csdn.net/u010223349/article/details/40392789</span>
ui = device->GetUI();// 呼叫Device類的GetUI函式返回RecoveryUI物件,recovery中共有三個關於UI的類:DefaultUI,ScreenRecoveryUI,RecoveryUI
gCurrentUI = ui;//初始化RecoveryUI物件
ui->SetLocale(locale);//為UI設定local,這樣就能以合適的語音顯示
ui->Init();
int st_cur, st_max;
if (stage != NULL && sscanf(stage, "%d/%d", &st_cur, &st_max) == 2) {
ui->SetStage(st_cur, st_max);
}
ui->SetBackground(RecoveryUI::NONE);//設定初始狀態的背景圖
if (show_text) ui->ShowText(true);//如果啟動引數指定了顯示UI,則設定在UI物件中
struct selinux_opt seopts[] = {
{ SELABEL_OPT_PATH, "/file_contexts" }
};
sehandle = selabel_open(SELABEL_CTX_FILE, seopts, 1);
if (!sehandle) {
ui->Print("Warning: No file_contexts\n");
}
device->StartRecovery();//空函式
printf("Command:");
for (arg = 0; arg < argc; arg++) {
printf(" \"%s\"", argv[arg]);
}
printf("\n");
if (update_package) {//預處理更新命令</span>
// For backwards compatibility on the cache partition only, if
// we're given an old 'root' path "CACHE:foo", change it to
// "/cache/foo".如果更新命令以“CACHE:”開頭,則把它換成實際的cache目錄路徑
if (strncmp(update_package, "CACHE:", 6) == 0) {
int len = strlen(update_package) + 10;
char* modified_path = (char*)malloc(len);
strlcpy(modified_path, "/cache/", len);
strlcat(modified_path, update_package+6, len);
printf("(replacing path \"%s\" with \"%s\")\n",
update_package, modified_path);
update_package = modified_path;
}
}
printf("\n");
//獲取一些property的值
property_list(print_property, NULL);
property_get("ro.build.display.id", recovery_version, "");
printf("\n");
int status = INSTALL_SUCCESS;
if (update_package != NULL) {//如果命令是更新系統
status = install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE, true);///tmp/last_install
if (status == INSTALL_SUCCESS && wipe_cache) {//如果更新成功,並且啟動引數要求擦除cache分割槽
if (erase_volume("/cache")) {//擦除CACHE分割槽
LOGE("Cache wipe (requested by package) failed.");
}
}
if (status != INSTALL_SUCCESS) {//安裝失敗
ui->Print("Installation aborted.\n");//列印提示
// If this is an eng or userdebug build, then automatically
// turn the text display on if the script fails so the error
// message is visible.
char buffer[PROPERTY_VALUE_MAX+1];
property_get("ro.build.fingerprint", buffer, "");
if (strstr(buffer, ":userdebug/") || strstr(buffer, ":eng/")) {
ui->ShowText(true);//在螢幕上列印失敗的版本資訊
}
}
} else if (wipe_data) {//如果是擦除資料區的命令
if (device->WipeData()) status = INSTALL_ERROR;//成功返回0
if (erase_volume("/data")) status = INSTALL_ERROR;//格式化(擦除)DATA分割槽
if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;//格式化(擦除)CACHE分割槽
if (erase_persistent_partition() == -1 ) status = INSTALL_ERROR;
if (status != INSTALL_SUCCESS) ui->Print("Data wipe failed.\n");
} else if (wipe_cache) {//擦除cache分割槽的命令
if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
if (status != INSTALL_SUCCESS) ui->Print("Cache wipe failed.\n");
} else if (!just_exit) {//只是回退命令
status = INSTALL_NONE; // No command specified
ui->SetBackground(RecoveryUI::NO_COMMAND);
}
if (status == INSTALL_ERROR || status == INSTALL_CORRUPT) {
copy_logs();
ui->SetBackground(RecoveryUI::ERROR);//執行命令錯誤,在螢幕上顯示標誌
}
Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;
if (status != INSTALL_SUCCESS || ui->IsTextVisible()) {
Device::BuiltinAction temp = prompt_and_wait(device, status);//進入選單模式
if (temp != Device::NO_ACTION) after = temp;
}
// Save logs and clean up before rebooting or shutting down.
finish_recovery(send_intent);//擦除BCB
switch (after) {//結束recovery模式,關機或重啟系統
case Device::SHUTDOWN:
ui->Print("Shutting down...\n");
property_set(ANDROID_RB_PROPERTY, "shutdown,");
break;
case Device::REBOOT_BOOTLOADER:
ui->Print("Rebooting to bootloader...\n");
property_set(ANDROID_RB_PROPERTY, "reboot,bootloader");
break;
default:
ui->Print("Rebooting...\n");
property_set(ANDROID_RB_PROPERTY, "reboot,");
break;
}
sleep(5); // should reboot before this finishes
return EXIT_SUCCESS;
}
我們分析一下main函式的主要流程。
(1)、呼叫load_volume_table()函式,讀取/etc/recovery.fstab檔案的內容,這個檔案中儲存的是進入recovery模式後系統的分割槽情況,包括分割槽名稱,引數等。
(2)、呼叫get_args()函式的主要作用就是建立recovery的啟動引數,如果命令列中有recovery命令,則優先執行命令列的recovery命令,否則讀取misc分割槽中的命令。如果misc分割槽中不存在命令,則執行/cache/recovery/command檔案中的命令。get_args()函式則通過get_bootloader_message()來讀取misc分割槽中的命令。參考:http://book.51cto.com/art/201504/474440.htm
// command line args come from, in decreasing precedence:
// - the actual command line
// - the bootloader control block (one per line, after "recovery")
// - the contents of COMMAND_FILE (one per line)
static void
get_args(int *argc, char ***argv) {
struct bootloader_message boot;
memset(&boot, 0, sizeof(boot));//分配儲存空間
get_bootloader_message(&boot); // this may fail, leaving a zeroed structure讀取/misc分割槽的命令,儲存到boot變數中
stage = strndup(boot.stage, sizeof(boot.stage));//將boot.stage拷貝到新建的位置處,有stage指向該位置,使用完後要使用free刪除動態申請的記憶體空間
if (boot.command[0] != 0 && boot.command[0] != 255) {
LOGI("Boot command: %.*s\n", (int)sizeof(boot.command), boot.command);
}
if (boot.status[0] != 0 && boot.status[0] != 255) {
LOGI("Boot status: %.*s\n", (int)sizeof(boot.status), boot.status);
}
// --- if arguments weren't supplied, look in the bootloader control block
if (*argc <= 1) {//如果命令列沒有傳遞引數,
boot.recovery[sizeof(boot.recovery) - 1] = '\0'; // Ensure termination
const char *arg = strtok(boot.recovery, "\n");//將boot.recovery以"\n"為分隔符進行分割,arg指向boot變數的recovery
if (arg != NULL && !strcmp(arg, "recovery")) {//如果字串以recovery開頭,則使用從/misc分割槽讀取的命令串建立啟動引數
*argv = (char **) malloc(sizeof(char *) * MAX_ARGS);
(*argv)[0] = strdup(arg);//拷貝arg的副本,有(*argv)[0]指向該副本
for (*argc = 1; *argc < MAX_ARGS; ++*argc) {
if ((arg = strtok(NULL, "\n")) == NULL) break;
(*argv)[*argc] = strdup(arg);
}
LOGI("Got arguments from boot message\n");
} else if (boot.recovery[0] != 0 && boot.recovery[0] != 255) {
LOGE("Bad boot message\n\"%.20s\"\n", boot.recovery);
}
}
// --- if that doesn't work, try the command file
if (*argc <= 1) {//如果從/misc分割槽也沒有讀到命令
FILE *fp = fopen_path(COMMAND_FILE, "r");//開啟/cache/recovery/command檔案
if (fp != NULL) {
char *token;
char *argv0 = (*argv)[0];
*argv = (char **) malloc(sizeof(char *) * MAX_ARGS);
(*argv)[0] = argv0; // use the same program name
char buf[MAX_ARG_LENGTH];
for (*argc = 1; *argc < MAX_ARGS; ++*argc) {//使用讀取的檔案內容建立啟動引數
if (!fgets(buf, sizeof(buf), fp)) break;
token = strtok(buf, "\r\n");
if (token != NULL) {
(*argv)[*argc] = strdup(token); // Strip newline.
} else {
--*argc;
}
}
check_and_fclose(fp, COMMAND_FILE);
LOGI("Got arguments from %s\n", COMMAND_FILE);
}
}
// --> write the arguments we have back into the bootloader control block
// always boot into recovery after this (until finish_recovery() is called)把啟動引數也放到boot物件中
strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));
int i;
for (i = 1; i < *argc; ++i) {
strlcat(boot.recovery, (*argv)[i], sizeof(boot.recovery));
strlcat(boot.recovery, "\n", sizeof(boot.recovery));//將兩個char型別連線
}
set_bootloader_message(&boot);
}
A、bootloader_message這個結構體在文章開始部分做了介紹。
B、get_bootloader_message函式,主要工作是根據分割槽的檔案格式型別(mtd或emmc)從MISC分割槽中讀取BCB資料塊到一個臨時的變數中,程式碼如下:
int get_bootloader_message(struct bootloader_message *out) {
Volume* v = volume_for_path("/misc");
if (v == NULL) {
LOGE("Cannot load volume /misc!\n");
return -1;
}
if (strcmp(v->fs_type, "mtd") == 0) {
return get_bootloader_message_mtd(out, v);
} else if (strcmp(v->fs_type, "emmc") == 0) {
return get_bootloader_message_block(out, v);
}
LOGE("unknown misc partition fs_type \"%s\"\n", v->fs_type);
return -1;
}
C、之後開始判斷Recovery服務是否有帶命令列的引數(/sbin/recovery,根據現有的邏輯是沒有的),若沒有就從BCB這讀取recovery域;如果讀取失敗則從/cache/recovery/command中繼續讀取。這樣這個BCB的臨時變數值得recovery域就被更新了。在將這個BCB的臨時變數寫回真實的BCB之前,還會更新這個BCB臨時變數的command域為“boot-recovery”。這樣做的目的是如果在升級失敗(比如在升級過程中斷電)時,系統在重啟之後還會進入Recovery模式,知道升級完成。D、在這個BCB的臨時變數的各個域更新完成後使用set_bootloader_message()寫回到真實的BCB塊中。
用一個簡單的圖來概括一下這個過程,如下:
(3)、呼叫make_device()建立Device物件,這裡實際上建立的是DefaultDevice物件,它繼承自Device類,DefaultDevice物件又建立了ScreenRecoveryUI物件,程式碼如下:
public:
DefaultDevice() :
ui(new ScreenRecoveryUI) {
}
我們會看到Device的很多函式都是空函式,並沒有實現。這是為什麼呢?因為Android提供的Recovery模組可以由廠商進行個性化開發,廠商通過過載Device類來加入自己特有的功能。這些空函式就是留著廠商實現的。
(4)、執行DefaultUI物件的初始化,Recovery的UI就是簡單的控制檯文字輸出介面。
(5)、執行更新系統的命令。如果從引數中得到的是更新系統的命令,則呼叫install_package函式來更新。
(6)、執行刪除DATA分割槽、CACHE分割槽的命令。
(7)、執行完命令後,如果命令執行不成功,或者呼叫DefaultUI的IsTextVisible函式返回true,呼叫promp_and_wait函式顯示選單。
(8)、finish_recovery(),
// clear the recovery command and prepare to boot a (hopefully working) system,
// copy our log file to cache as well (for the system to read), and
// record any intent we were asked to communicate back to the system.
// this function is idempotent: call it as many times as you like.
static void
finish_recovery(const char *send_intent) {
// By this point, we're ready to return to the main system...
if (send_intent != NULL) {
FILE *fp = fopen_path(INTENT_FILE, "w");// /cache/recovery/intent
if (fp == NULL) {
LOGE("Can't open %s\n", INTENT_FILE);
} else {
fputs(send_intent, fp);//向指定的檔案寫入一個字串:將send_intent寫入到intent檔案中
check_and_fclose(fp, INTENT_FILE);
}
}
// Save the locale to cache, so if recovery is next started up
// without a --locale argument (eg, directly from the bootloader)
// it will use the last-known locale.
if (locale != NULL) {
LOGI("Saving locale \"%s\"\n", locale);
FILE* fp = fopen_path(LOCALE_FILE, "w");// /cache/recovery/last_local
fwrite(locale, 1, strlen(locale), fp);
fflush(fp);
fsync(fileno(fp));
check_and_fclose(fp, LOCALE_FILE);
}
copy_logs();
// Reset to normal system boot so recovery won't cycle indefinitely.
struct bootloader_message boot;
memset(&boot, 0, sizeof(boot));//將boot變數清0
set_bootloader_message(&boot);
// Remove the command file, so recovery won't repeat indefinitely.
if (ensure_path_mounted(COMMAND_FILE) != 0 ||
(unlink(COMMAND_FILE) && errno != ENOENT)) {
LOGW("Can't unlink %s\n", COMMAND_FILE);
}
ensure_path_unmounted(CACHE_ROOT);
sync(); // For good measure.
}
A、將intent的內容作為引數傳進finish_recovery中,如果有intent需要告知Main system,則將其寫入/cache/recovery/intent檔案中。
B、將local引數寫入到/cache/recovery/last_local檔案中。
C、copy_logs():將記憶體檔案系統中的Recovery服務的日誌/tmp/recovery.log追加到/cache/recovery/log中,並且拷貝到/cache/recovery/last_log中,將/tmp/last_install拷貝到/cache/recovery/last_install中;儲存核心日誌到/cache/recovery/last_kmsg檔案中。
D、擦除MISC分割槽中的BCB資料塊的內容,以便系統重啟後不再進入Recovery模式,而是進入Main system。
E、刪除/cache/recovery/command檔案。因為重啟後Bootloader會自動檢索這個檔案,如果沒有刪除又會進入Recovery模式。
finish_recovery()函式的處理流程如下:
(8)、退出Recovery。根據引數,選擇關閉或重啟系統。
2、執行選單命令
我們看下Recovery是如何執行選單命令的。prompt_and_wait()函式用來列印螢幕選單並接收使用者輸入,當在main函式中執行命令失敗並且IsTextVisible顯示選單時,呼叫prompt_and_wait函式。程式碼如下:
// Return REBOOT, SHUTDOWN, or REBOOT_BOOTLOADER. Returning NO_ACTION
// means to take the default, which is to reboot or shutdown depending
// on if the --shutdown_after flag was passed to recovery.如果返回NO_ACTION表示採取由傳遞進來的--shutdown_after標記決定的預設動作
static Device::BuiltinAction
prompt_and_wait(Device* device, int status) {
const char* const* headers = prepend_title(device->GetMenuHeaders());
for (;;) {
finish_recovery(NULL);
switch (status) {//根據命令的執行情況,改變UI的背景
case INSTALL_SUCCESS:
case INSTALL_NONE:
ui->SetBackground(RecoveryUI::NO_COMMAND);
break;
case INSTALL_ERROR:
case INSTALL_CORRUPT:
ui->SetBackground(RecoveryUI::ERROR);
break;
}
ui->SetProgressType(RecoveryUI::EMPTY);//設定升級進度
int chosen_item = get_menu_selection(headers, device->GetMenuItems(), 0, 0, device);//系統將在get_menu_selection函式中等待使用者輸入
// device-specific code may take some action here. It may
// return one of the core actions handled in the switch
// statement below.
Device::BuiltinAction chosen_action = device->InvokeMenuItem(chosen_item);//將使用者的選擇先交給Device物件處理
int wipe_cache = 0;
switch (chosen_action) {
case Device::NO_ACTION:
break;
case Device::REBOOT:
case Device::SHUTDOWN:
case Device::REBOOT_BOOTLOADER:
return chosen_action;
case Device::WIPE_DATA://擦除資料區
wipe_data(ui->IsTextVisible(), device);
if (!ui->IsTextVisible()) return Device::NO_ACTION;
break;
case Device::WIPE_CACHE://擦除cache分割槽
ui->Print("\n-- Wiping cache...\n");
erase_volume("/cache");
ui->Print("Cache wipe complete.\n");
if (!ui->IsTextVisible()) return Device::NO_ACTION;
break;
case Device::APPLY_EXT: {//從SD卡上更新
ensure_path_mounted(SDCARD_ROOT);//"sdcard"
char* path = browse_directory(SDCARD_ROOT, device);
if (path == NULL) {
ui->Print("\n-- No package file selected.\n", path);
break;
}
ui->Print("\n-- Install %s ...\n", path);
set_sdcard_update_bootloader_message();
void* token = start_sdcard_fuse(path);
int status = install_package(FUSE_SIDELOAD_HOST_PATHNAME, &wipe_cache,
TEMPORARY_INSTALL_FILE, false);
finish_sdcard_fuse(token);
ensure_path_unmounted(SDCARD_ROOT);
if (status == INSTALL_SUCCESS && wipe_cache) {
ui->Print("\n-- Wiping cache (at package request)...\n");
if (erase_volume("/cache")) {
ui->Print("Cache wipe failed.\n");
} else {
ui->Print("Cache wipe complete.\n");
}
}
if (status >= 0) {
if (status != INSTALL_SUCCESS) {
ui->SetBackground(RecoveryUI::ERROR);
ui->Print("Installation aborted.\n");
} else if (!ui->IsTextVisible()) {
return Device::NO_ACTION; // reboot if logs aren't visible
} else {
ui->Print("\nInstall from sdcard complete.\n");
}
}
break;
}
case Device::APPLY_CACHE://從cache中更新,已經被否決
ui->Print("\nAPPLY_CACHE is deprecated.\n");
break;
case Device::READ_RECOVERY_LASTLOG:
choose_recovery_file(device);
break;
case Device::APPLY_ADB_SIDELOAD://啟動adbd,主要是讓使用者通過adb連線來執行sideload命令上傳,更新檔案到/tmp/update.zip,執行更新操作。
status = apply_from_adb(ui, &wipe_cache, TEMPORARY_INSTALL_FILE);
if (status >= 0) {
if (status != INSTALL_SUCCESS) {
ui->SetBackground(RecoveryUI::ERROR);
ui->Print("Installation aborted.\n");
copy_logs();
} else if (!ui->IsTextVisible()) {
return Device::NO_ACTION; // reboot if logs aren't visible
} else {
ui->Print("\nInstall from ADB complete.\n");
}
}
break;
}
}
}
prompt_and_wait()函式的邏輯是,在螢幕上打印出選單,然後等待使用者按鍵輸入,根據輸入命令進行處理。