1. 程式人生 > >Android--Recovery模組之恢復出廠設定

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,定義如下:

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];
};
    command:command欄位中儲存的是命令,當想要重啟進入Recovery模式,或升級radio/bootloader firmware時,會更新這個欄位(如果它的值是“boot-recovery”,系統進入Recovery模式;如果它的值是“update-radio”或“update-hboot”,系統進入更新firmware模式,這個更新過程由bootloader完成;如果command中的值為null,則進入主系統,正常啟動);當firmware更新完畢,為了啟動後進入Recovery做最終的清除,bootloader還會修改它。

    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()函式的邏輯是,在螢幕上打印出選單,然後等待使用者按鍵輸入,根據輸入命令進行處理。