com.github.chenxiaolong.dualbootpatcher.socket.MbtoolSocket.java Source code

Java tutorial

Introduction

Here is the source code for com.github.chenxiaolong.dualbootpatcher.socket.MbtoolSocket.java

Source

/*
 * Copyright (C) 2014  Andrew Gunnerson <andrewgunnerson@gmail.com>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.github.chenxiaolong.dualbootpatcher.socket;

import android.content.Context;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import android.net.LocalSocketAddress.Namespace;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;

import com.github.chenxiaolong.dualbootpatcher.CommandUtils;
import com.github.chenxiaolong.dualbootpatcher.RomUtils.RomInformation;
import com.github.chenxiaolong.dualbootpatcher.ThreadUtils;
import com.github.chenxiaolong.dualbootpatcher.Version;
import com.github.chenxiaolong.dualbootpatcher.Version.VersionParseException;
import com.github.chenxiaolong.dualbootpatcher.patcher.PatcherUtils;
import com.github.chenxiaolong.dualbootpatcher.socket.MbtoolUtils.Feature;
import com.github.chenxiaolong.dualbootpatcher.switcher.SwitcherUtils;
import com.google.flatbuffers.FlatBufferBuilder;
import com.google.flatbuffers.Table;

import org.apache.commons.io.IOUtils;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;

import mbtool.daemon.v3.FileChmodRequest;
import mbtool.daemon.v3.FileChmodResponse;
import mbtool.daemon.v3.FileCloseRequest;
import mbtool.daemon.v3.FileCloseResponse;
import mbtool.daemon.v3.FileOpenRequest;
import mbtool.daemon.v3.FileOpenResponse;
import mbtool.daemon.v3.FileReadRequest;
import mbtool.daemon.v3.FileReadResponse;
import mbtool.daemon.v3.FileSELinuxGetLabelRequest;
import mbtool.daemon.v3.FileSELinuxGetLabelResponse;
import mbtool.daemon.v3.FileSELinuxSetLabelRequest;
import mbtool.daemon.v3.FileSELinuxSetLabelResponse;
import mbtool.daemon.v3.FileSeekRequest;
import mbtool.daemon.v3.FileSeekResponse;
import mbtool.daemon.v3.FileStatRequest;
import mbtool.daemon.v3.FileStatResponse;
import mbtool.daemon.v3.FileWriteRequest;
import mbtool.daemon.v3.FileWriteResponse;
import mbtool.daemon.v3.MbGetBootedRomIdRequest;
import mbtool.daemon.v3.MbGetBootedRomIdResponse;
import mbtool.daemon.v3.MbGetInstalledRomsRequest;
import mbtool.daemon.v3.MbGetInstalledRomsResponse;
import mbtool.daemon.v3.MbGetPackagesCountRequest;
import mbtool.daemon.v3.MbGetPackagesCountResponse;
import mbtool.daemon.v3.MbGetVersionRequest;
import mbtool.daemon.v3.MbGetVersionResponse;
import mbtool.daemon.v3.MbRom;
import mbtool.daemon.v3.MbSetKernelRequest;
import mbtool.daemon.v3.MbSetKernelResponse;
import mbtool.daemon.v3.MbSwitchRomRequest;
import mbtool.daemon.v3.MbSwitchRomResponse;
import mbtool.daemon.v3.MbSwitchRomResult;
import mbtool.daemon.v3.MbWipeRomRequest;
import mbtool.daemon.v3.MbWipeRomResponse;
import mbtool.daemon.v3.PathChmodRequest;
import mbtool.daemon.v3.PathChmodResponse;
import mbtool.daemon.v3.PathCopyRequest;
import mbtool.daemon.v3.PathCopyResponse;
import mbtool.daemon.v3.PathGetDirectorySizeRequest;
import mbtool.daemon.v3.PathGetDirectorySizeResponse;
import mbtool.daemon.v3.PathSELinuxGetLabelRequest;
import mbtool.daemon.v3.PathSELinuxGetLabelResponse;
import mbtool.daemon.v3.PathSELinuxSetLabelRequest;
import mbtool.daemon.v3.PathSELinuxSetLabelResponse;
import mbtool.daemon.v3.RebootRequest;
import mbtool.daemon.v3.RebootResponse;
import mbtool.daemon.v3.Request;
import mbtool.daemon.v3.RequestType;
import mbtool.daemon.v3.Response;
import mbtool.daemon.v3.ResponseType;
import mbtool.daemon.v3.StructStat;

public class MbtoolSocket {
    private static final String TAG = MbtoolSocket.class.getSimpleName();

    private static final String SOCKET_ADDRESS = "mbtool.daemon";

    // Same as the C++ default
    private static final int FBB_SIZE = 1024;

    private static final String RESPONSE_ALLOW = "ALLOW";
    private static final String RESPONSE_DENY = "DENY";
    private static final String RESPONSE_OK = "OK";
    private static final String RESPONSE_UNSUPPORTED = "UNSUPPORTED";

    private static MbtoolSocket sInstance;

    private LocalSocket mSocket;
    private InputStream mSocketIS;
    private OutputStream mSocketOS;
    private int mInterfaceVersion;
    private String mMbtoolVersion;

    // Keep this as a singleton class for now
    private MbtoolSocket() {
        mInterfaceVersion = 3;
    }

    public synchronized static MbtoolSocket getInstance() {
        if (sInstance == null) {
            sInstance = new MbtoolSocket();
        }
        return sInstance;
    }

    /**
     * Check if mbtool rejected the connection due to the signature check failing
     *
     * @throws IOException Signature check failed or unexpected response
     */
    private synchronized void verifyCredentials() throws IOException {
        String response = SocketUtils.readString(mSocketIS);
        if (RESPONSE_DENY.equals(response)) {
            throw new IOException("mbtool explicitly denied access to the daemon. "
                    + "WARNING: This app is probably not officially signed!");
        } else if (!RESPONSE_ALLOW.equals(response)) {
            throw new IOException("Unexpected reply: " + response);
        }
    }

    /**
     * Request protocol version from mbtool
     *
     * @throws IOException Protocol version not supported or unexpected reply
     */
    private synchronized void requestInterfaceVersion() throws IOException {
        SocketUtils.writeInt32(mSocketOS, mInterfaceVersion);
        String response = SocketUtils.readString(mSocketIS);
        if (RESPONSE_UNSUPPORTED.equals(response)) {
            throw new IOException("Daemon does not support interface " + mInterfaceVersion);
        } else if (!RESPONSE_OK.equals(response)) {
            throw new IOException("Unexpected reply: " + response);
        }
    }

    /**
     * Check that the minimum mbtool version is satisfied
     *
     * @throws IOException Could not determine mbtool version, invalid mbtool version, or mbtool
     *                     version is too old
     */
    private synchronized void verifyMbtoolVersion() throws IOException {
        // Get mbtool version
        FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
        MbGetVersionRequest.startMbGetVersionRequest(builder);
        int fbRequest = MbGetVersionRequest.endMbGetVersionRequest(builder);
        MbGetVersionResponse response = (MbGetVersionResponse) sendRequest(builder, fbRequest,
                RequestType.MbGetVersionRequest, ResponseType.MbGetVersionResponse);
        mMbtoolVersion = response.version();
        if (mMbtoolVersion == null) {
            throw new IOException("Could not determine mbtool version");
        }

        Version v1;
        Version v2 = MbtoolUtils.getMinimumRequiredVersion(Feature.DAEMON);

        try {
            v1 = new Version(mMbtoolVersion);
        } catch (VersionParseException e) {
            throw new IOException("Invalid version number: " + mMbtoolVersion);
        }

        Log.v(TAG, "mbtool version: " + v1);
        Log.v(TAG, "minimum version: " + v2);

        // Ensure that the version is newer than the minimum required version
        if (v1.compareTo(v2) < 0) {
            throw new IOException("mbtool version is: " + v1 + ", " + "minimum needed is: " + v2);
        }
    }

    /**
     * Initializes the mbtool connection.
     *
     * 1. Setup input and output streams
     * 2. Check to make sure mbtool authorized our connection
     * 3. Request interface version and check if the daemon supports it
     * 4. Get mbtool version from daemon
     *
     * @throws IOException
     */
    private synchronized void initializeConnection() throws IOException {
        mSocketIS = mSocket.getInputStream();
        mSocketOS = mSocket.getOutputStream();

        verifyCredentials();
        requestInterfaceVersion();
        verifyMbtoolVersion();
    }

    /**
     * Connects to the mbtool socket
     *
     * 1. Try connecting to the socket
     * 2. If that fails or if the version is too old, launch bundled mbtool and connect again
     * 3. If that fails, then return false
     *
     * @param context Application context
     */
    public synchronized void connect(Context context) throws IOException {
        // If we're already connected, then we're good
        if (mSocket != null) {
            return;
        }

        // Try connecting to the socket
        try {
            mSocket = new LocalSocket();
            mSocket.connect(new LocalSocketAddress(SOCKET_ADDRESS, Namespace.ABSTRACT));
            initializeConnection();
            return;
        } catch (IOException e) {
            Log.e(TAG, "Could not connect to mbtool socket", e);
            disconnect();
        }

        Log.v(TAG, "Launching bundled mbtool");

        if (!executeMbtool(context)) {
            throw new IOException("Failed to execute mbtool");
        }

        // Give mbtool a little bit of time to start listening on the socket
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        try {
            mSocket = new LocalSocket();
            mSocket.connect(new LocalSocketAddress(SOCKET_ADDRESS, Namespace.ABSTRACT));
            initializeConnection();
        } catch (IOException e) {
            disconnect();
            throw new IOException("Could not connect to mbtool socket", e);
        }
    }

    @SuppressWarnings("deprecation")
    private synchronized boolean executeMbtool(Context context) {
        PatcherUtils.extractPatcher(context);
        String abi;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            abi = Build.SUPPORTED_ABIS[0];
        } else {
            abi = Build.CPU_ABI;
        }

        String mbtool = PatcherUtils.getTargetDirectory(context) + "/binaries/android/" + abi + "/mbtool";

        return CommandUtils.runRootCommand("mount -o remount,rw /") == 0
                && CommandUtils.runRootCommand("mv /mbtool /mbtool.bak || :") == 0
                && CommandUtils.runRootCommand("cp " + mbtool + " /mbtool") == 0
                && CommandUtils.runRootCommand("chmod 755 /mbtool") == 0
                && CommandUtils.runRootCommand("mount -o remount,ro / || :") == 0
                && CommandUtils.runRootCommand("/mbtool daemon --replace --daemonize") == 0;
    }

    public synchronized void disconnect() {
        Log.i(TAG, "Disconnecting from mbtool");
        IOUtils.closeQuietly(mSocket);
        IOUtils.closeQuietly(mSocketIS);
        IOUtils.closeQuietly(mSocketOS);
        mSocket = null;
        mSocketIS = null;
        mSocketOS = null;

        mMbtoolVersion = null;
    }

    // RPC calls

    public synchronized boolean fileChmod(Context context, int id, int mode) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            FileChmodRequest.startFileChmodRequest(builder);
            FileChmodRequest.addId(builder, id);
            FileChmodRequest.addMode(builder, mode);
            int fbRequest = FileChmodRequest.endFileChmodRequest(builder);

            // Send request
            FileChmodResponse response = (FileChmodResponse) sendRequest(builder, fbRequest,
                    RequestType.FileChmodRequest, ResponseType.FileChmodResponse);

            if (!response.success()) {
                Log.e(TAG, "[" + id + "]: chmod failed: " + response.errorMsg());
                return false;
            }
            return true;
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    public synchronized boolean fileClose(Context context, int id) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            FileCloseRequest.startFileCloseRequest(builder);
            FileCloseRequest.addId(builder, id);
            int fbRequest = FileCloseRequest.endFileCloseRequest(builder);

            // Send request
            FileCloseResponse response = (FileCloseResponse) sendRequest(builder, fbRequest,
                    RequestType.FileCloseRequest, ResponseType.FileCloseResponse);

            if (!response.success()) {
                Log.e(TAG, "[" + id + "]: close failed: " + response.errorMsg());
                return false;
            }
            return true;
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    public synchronized int fileOpen(Context context, String path, short[] flags, int perms) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);

            int fbPath = builder.createString(path);
            int fbFlags = FileOpenRequest.createFlagsVector(builder, flags);

            FileOpenRequest.startFileOpenRequest(builder);
            FileOpenRequest.addPath(builder, fbPath);
            FileOpenRequest.addFlags(builder, fbFlags);
            FileOpenRequest.addPerms(builder, perms);
            int fbRequest = FileOpenRequest.endFileOpenRequest(builder);

            // Send request
            FileOpenResponse response = (FileOpenResponse) sendRequest(builder, fbRequest,
                    RequestType.FileOpenRequest, ResponseType.FileOpenResponse);

            if (!response.success()) {
                Log.e(TAG, "[" + path + "]: open failed: " + response.errorMsg());
                return -1;
            }
            return response.id();
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    @Nullable
    public synchronized ByteBuffer fileRead(Context context, int id, long size) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            FileReadRequest.startFileReadRequest(builder);
            FileReadRequest.addId(builder, id);
            FileReadRequest.addCount(builder, size);
            int fbRequest = FileReadRequest.endFileReadRequest(builder);

            // Send request
            FileReadResponse response = (FileReadResponse) sendRequest(builder, fbRequest,
                    RequestType.FileReadRequest, ResponseType.FileReadResponse);

            if (!response.success()) {
                Log.e(TAG, "[" + id + "]: read failed: " + response.errorMsg());
                return null;
            }
            return response.dataAsByteBuffer();
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    public synchronized long fileSeek(Context context, int id, long offset, short whence) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            FileSeekRequest.startFileSeekRequest(builder);
            FileSeekRequest.addId(builder, id);
            FileSeekRequest.addOffset(builder, offset);
            FileSeekRequest.addWhence(builder, whence);
            int fbRequest = FileSeekRequest.endFileSeekRequest(builder);

            // Send request
            FileSeekResponse response = (FileSeekResponse) sendRequest(builder, fbRequest,
                    RequestType.FileSeekRequest, ResponseType.FileSeekResponse);

            if (!response.success()) {
                Log.e(TAG, "[" + id + "]: seek failed: " + response.errorMsg());
                return -1;
            }
            return response.offset();
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    public static class StatBuf {
        public long st_dev;
        public long st_ino;
        public int st_mode;
        public long st_nlink;
        public int st_uid;
        public int st_gid;
        public long st_rdev;
        public long st_size;
        public long st_blksize;
        public long st_blocks;
        public long st_atime;
        public long st_mtime;
        public long st_ctime;
    }

    @Nullable
    public synchronized StatBuf fileStat(Context context, int id) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            FileStatRequest.startFileStatRequest(builder);
            FileStatRequest.addId(builder, id);
            int fbRequest = FileStatRequest.endFileStatRequest(builder);

            // Send request
            FileStatResponse response = (FileStatResponse) sendRequest(builder, fbRequest,
                    RequestType.FileStatRequest, ResponseType.FileStatResponse);

            if (!response.success()) {
                Log.e(TAG, "[" + id + "]: stat failed: " + response.errorMsg());
                return null;
            }

            StructStat ss = response.stat();
            StatBuf sb = new StatBuf();
            sb.st_dev = ss.stDev();
            sb.st_ino = ss.stIno();
            sb.st_mode = (int) ss.stMode();
            sb.st_nlink = ss.stNlink();
            sb.st_uid = (int) ss.stUid();
            sb.st_gid = (int) ss.stGid();
            sb.st_rdev = ss.stRdev();
            sb.st_size = ss.stSize();
            sb.st_blksize = ss.stBlksize();
            sb.st_blocks = ss.stBlocks();
            sb.st_atime = ss.stAtime();
            sb.st_mtime = ss.stMtime();
            sb.st_ctime = ss.stCtime();
            return sb;
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    public synchronized long fileWrite(Context context, int id, byte[] data) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            int fbData = FileWriteRequest.createDataVector(builder, data);
            FileWriteRequest.startFileWriteRequest(builder);
            FileWriteRequest.addId(builder, id);
            FileWriteRequest.addData(builder, fbData);
            int fbRequest = FileWriteRequest.endFileWriteRequest(builder);

            // Send request
            FileWriteResponse response = (FileWriteResponse) sendRequest(builder, fbRequest,
                    RequestType.FileWriteRequest, ResponseType.FileWriteResponse);

            if (!response.success()) {
                Log.e(TAG, "[" + id + "]: write failed: " + response.errorMsg());
                return -1;
            }
            return response.bytesWritten();
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    @Nullable
    public synchronized String fileSelinuxGetLabel(Context context, int id) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            FileSELinuxGetLabelRequest.startFileSELinuxGetLabelRequest(builder);
            FileSELinuxGetLabelRequest.addId(builder, id);
            int fbRequest = FileSELinuxGetLabelRequest.endFileSELinuxGetLabelRequest(builder);

            // Send request
            FileSELinuxGetLabelResponse response = (FileSELinuxGetLabelResponse) sendRequest(builder, fbRequest,
                    RequestType.FileSELinuxGetLabelRequest, ResponseType.FileSELinuxGetLabelResponse);

            if (!response.success()) {
                Log.e(TAG, "[" + id + "]: SELinux get label failed: " + response.errorMsg());
                return null;
            }
            return response.label();
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    public synchronized boolean fileSelinuxSetLabel(Context context, int id, String label) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            FileSELinuxSetLabelRequest.startFileSELinuxSetLabelRequest(builder);
            FileSELinuxSetLabelRequest.addId(builder, id);
            int fbRequest = FileSELinuxSetLabelRequest.endFileSELinuxSetLabelRequest(builder);

            // Send request
            FileSELinuxSetLabelResponse response = (FileSELinuxSetLabelResponse) sendRequest(builder, fbRequest,
                    RequestType.FileSELinuxSetLabelRequest, ResponseType.FileSELinuxSetLabelResponse);

            if (!response.success()) {
                Log.e(TAG, "[" + id + "]: SELinux set label failed: " + response.errorMsg());
                return false;
            }
            return true;
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    /**
     * Get version of the mbtool daemon.
     *
     * @param context Application context
     * @return String containing the mbtool version
     * @throws IOException When any socket communication error occurs
     */
    @NonNull
    public synchronized String version(Context context) throws IOException {
        connect(context);

        return mMbtoolVersion;
    }

    /**
     * Get list of installed ROMs.
     *
     * NOTE: The list of ROMs will be re-queried and a new array will be returned every time this
     *       method is called. It may be a good idea to cache the results after the initial call.
     *
     * @param context Application context
     * @return Array of {@link RomInformation} objects representing the list of installed ROMs
     * @throws IOException When any socket communication error occurs
     */
    @NonNull
    public synchronized RomInformation[] getInstalledRoms(Context context) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            MbGetInstalledRomsRequest.startMbGetInstalledRomsRequest(builder);
            // No parameters
            int fbRequest = MbGetInstalledRomsRequest.endMbGetInstalledRomsRequest(builder);

            // Send request
            MbGetInstalledRomsResponse response = (MbGetInstalledRomsResponse) sendRequest(builder, fbRequest,
                    RequestType.MbGetInstalledRomsRequest, ResponseType.MbGetInstalledRomsResponse);

            RomInformation[] roms = new RomInformation[response.romsLength()];

            for (int i = 0; i < response.romsLength(); i++) {
                RomInformation rom = roms[i] = new RomInformation();
                MbRom fbrom = response.roms(i);

                rom.setId(fbrom.id());
                rom.setSystemPath(fbrom.systemPath());
                rom.setCachePath(fbrom.cachePath());
                rom.setDataPath(fbrom.dataPath());
                rom.setVersion(fbrom.version());
                rom.setBuild(fbrom.build());
            }

            return roms;
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    /**
     * Get the current ROM ID.
     *
     * @param context Application context
     * @return String containing the current ROM ID
     * @throws IOException When any socket communication error occurs
     */
    @NonNull
    public synchronized String getBootedRomId(Context context) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            MbGetBootedRomIdRequest.startMbGetBootedRomIdRequest(builder);
            // No parameters
            int fbRequest = MbGetBootedRomIdRequest.endMbGetBootedRomIdRequest(builder);

            // Send request
            MbGetBootedRomIdResponse response = (MbGetBootedRomIdResponse) sendRequest(builder, fbRequest,
                    RequestType.MbGetBootedRomIdRequest, ResponseType.MbGetBootedRomIdResponse);

            return response.romId();
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    public enum SwitchRomResult {
        UNKNOWN_BOOT_PARTITION, SUCCEEDED, FAILED, CHECKSUM_INVALID, CHECKSUM_NOT_FOUND
    }

    /**
     * Switch to another ROM.
     *
     * NOTE: If {@link SwitchRomResult#FAILED} is returned, there is no way of determining the cause
     *       of failure programmatically. However, mbtool will likely print debugging information
     *       (errno, etc.) to the logcat for manual reviewing.
     *
     * @param context Application context
     * @param id ID of ROM to switch to
     * @return {@link SwitchRomResult#UNKNOWN_BOOT_PARTITION} if the boot partition could not be determined
     *         {@link SwitchRomResult#SUCCEEDED} if the ROM was successfully switched
     *         {@link SwitchRomResult#FAILED} if the ROM failed to switch
     * @throws IOException When any socket communication error occurs
     */
    @NonNull
    public synchronized SwitchRomResult switchRom(Context context, String id, boolean forceChecksumsUpdate)
            throws IOException {
        connect(context);

        try {
            String bootBlockDev = SwitcherUtils.getBootPartition(context);
            if (bootBlockDev == null) {
                Log.e(TAG, "Failed to determine boot partition");
                return SwitchRomResult.UNKNOWN_BOOT_PARTITION;
            }

            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            int fbRomId = builder.createString(id);
            int fbBootBlockDev = builder.createString(bootBlockDev);

            // Blockdev search dirs
            String[] searchDirs = SwitcherUtils.getBlockDevSearchDirs(context);
            int fbSearchDirs = 0;
            if (searchDirs != null) {
                int[] searchDirsOffsets = new int[searchDirs.length];
                for (int i = 0; i < searchDirs.length; i++) {
                    searchDirsOffsets[i] = builder.createString(searchDirs[i]);
                }

                fbSearchDirs = MbSwitchRomRequest.createBlockdevBaseDirsVector(builder, searchDirsOffsets);
            }

            MbSwitchRomRequest.startMbSwitchRomRequest(builder);
            MbSwitchRomRequest.addRomId(builder, fbRomId);
            MbSwitchRomRequest.addBootBlockdev(builder, fbBootBlockDev);
            MbSwitchRomRequest.addBlockdevBaseDirs(builder, fbSearchDirs);
            MbSwitchRomRequest.addForceUpdateChecksums(builder, forceChecksumsUpdate);
            int fbRequest = MbSwitchRomRequest.endMbSwitchRomRequest(builder);

            // Send request
            MbSwitchRomResponse response = (MbSwitchRomResponse) sendRequest(builder, fbRequest,
                    RequestType.MbSwitchRomRequest, ResponseType.MbSwitchRomResponse);

            SwitchRomResult result;
            switch (response.result()) {
            case MbSwitchRomResult.SUCCEEDED:
                result = SwitchRomResult.SUCCEEDED;
                break;
            case MbSwitchRomResult.FAILED:
                result = SwitchRomResult.FAILED;
                break;
            case MbSwitchRomResult.CHECKSUM_INVALID:
                result = SwitchRomResult.CHECKSUM_INVALID;
                break;
            case MbSwitchRomResult.CHECKSUM_NOT_FOUND:
                result = SwitchRomResult.CHECKSUM_NOT_FOUND;
                break;
            default:
                throw new IOException("Invalid SwitchRomResult: " + response.result());
            }

            return result;
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    public enum SetKernelResult {
        UNKNOWN_BOOT_PARTITION, SUCCEEDED, FAILED
    }

    /**
     * Set the kernel for a ROM.
     *
     * NOTE: If {@link SetKernelResult#FAILED} is returned, there is no way of determining the cause
     *       of failure programmatically. However, mbtool will likely print debugging information
     *       (errno, etc.) to the logcat for manual reviewing.
     *
     * @param context Application context
     * @param id ID of ROM to set the kernel for
     * @return {@link SetKernelResult#UNKNOWN_BOOT_PARTITION} if the boot partition could not be determined
     *         {@link SetKernelResult#SUCCEEDED} if setting the kernel was successful
     *         {@link SetKernelResult#FAILED} if setting the kernel failed
     * @throws IOException When any socket communication error occurs
     */
    @NonNull
    public synchronized SetKernelResult setKernel(Context context, String id) throws IOException {
        connect(context);

        try {
            String bootBlockDev = SwitcherUtils.getBootPartition(context);
            if (bootBlockDev == null) {
                Log.e(TAG, "Failed to determine boot partition");
                return SetKernelResult.UNKNOWN_BOOT_PARTITION;
            }

            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            int fbRomId = builder.createString(id);
            int fbBootBlockDev = builder.createString(bootBlockDev);
            MbSetKernelRequest.startMbSetKernelRequest(builder);
            MbSetKernelRequest.addRomId(builder, fbRomId);
            MbSetKernelRequest.addBootBlockdev(builder, fbBootBlockDev);
            int fbRequest = MbSetKernelRequest.endMbSetKernelRequest(builder);

            // Send request
            MbSetKernelResponse response = (MbSetKernelResponse) sendRequest(builder, fbRequest,
                    RequestType.MbSetKernelRequest, ResponseType.MbSetKernelResponse);

            return response.success() ? SetKernelResult.SUCCEEDED : SetKernelResult.FAILED;
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    /**
     * Reboots the device.
     *
     * @param context Application context
     * @param arg Reboot argument (eg. "recovery", "download", "bootloader"). Pass "" for a regular
     *            reboot.
     * @return True if the call to init succeeded and a reboot is pending. False, otherwise.
     * @throws IOException When any socket communication error occurs
     */
    public synchronized boolean restart(Context context, String arg) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            int fbArg = builder.createString(arg != null ? arg : "");
            RebootRequest.startRebootRequest(builder);
            RebootRequest.addArg(builder, fbArg);
            int fbRequest = RebootRequest.endRebootRequest(builder);

            // Send request
            RebootResponse response = (RebootResponse) sendRequest(builder, fbRequest, RequestType.RebootRequest,
                    ResponseType.RebootResponse);

            return response.success();
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    /**
     * Copy a file using mbtool.
     *
     * NOTE: If false is returned, there is no way of determining the cause of failure
     *       programmatically. However, mbtool will likely print debugging information (errno, etc.)
     *       to the logcat for manual reviewing.
     *
     * @param context Application context
     * @param source Absolute source path
     * @param target Absolute target path
     * @return True if the operation was successful. False, otherwise.
     * @throws IOException When any socket communication error occurs
     */
    public synchronized boolean pathCopy(Context context, String source, String target) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            int fbSource = builder.createString(source);
            int fbTarget = builder.createString(target);
            PathCopyRequest.startPathCopyRequest(builder);
            PathCopyRequest.addSource(builder, fbSource);
            PathCopyRequest.addTarget(builder, fbTarget);
            int fbRequest = PathCopyRequest.endPathCopyRequest(builder);

            // Send request
            PathCopyResponse response = (PathCopyResponse) sendRequest(builder, fbRequest,
                    RequestType.PathCopyRequest, ResponseType.PathCopyResponse);

            if (!response.success()) {
                Log.e(TAG, "Failed to copy from " + source + " to " + target + ": " + response.errorMsg());
                return false;
            }

            return true;
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    /**
     * Chmod a file using mbtool.
     *
     * NOTE: If false is returned, there is no way of determining the cause of failure
     *       programmatically. However, mbtool will likely print debugging information (errno, etc.)
     *       to the logcat for manual reviewing.
     *
     * @param context Application context
     * @param filename Absolute path
     * @param mode Unix permissions number (will be AND'ed with 0777 by mbtool for security reasons)
     * @return True if the operation was successful. False, otherwise.
     * @throws IOException When any socket communication error occurs
     */
    public synchronized boolean pathChmod(Context context, String filename, int mode) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            int fbFilename = builder.createString(filename);
            PathChmodRequest.startPathChmodRequest(builder);
            PathChmodRequest.addPath(builder, fbFilename);
            PathChmodRequest.addMode(builder, mode);
            int fbRequest = PathChmodRequest.endPathChmodRequest(builder);

            // Send request
            PathChmodResponse response = (PathChmodResponse) sendRequest(builder, fbRequest,
                    RequestType.PathChmodRequest, ResponseType.PathChmodResponse);

            if (!response.success()) {
                Log.e(TAG, "Failed to chmod " + filename + ": " + response.errorMsg());
                return false;
            }

            return true;
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    public static class WipeResult {
        // Targets as listed in WipeTarget
        public short[] succeeded;
        public short[] failed;
    }

    /**
     * Wipe a ROM.
     *
     * @param context Application context
     * @param romId ROM ID to wipe
     * @param targets List of {@link mbtool.daemon.v3.MbWipeTarget}s indicating the wipe targets
     * @return {@link WipeResult} containing the list of succeeded and failed wipe targets
     * @throws IOException When any socket communication error occurs
     */
    @NonNull
    public synchronized WipeResult wipeRom(Context context, String romId, short[] targets) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            int fbRomId = builder.createString(romId);
            int fbTargets = MbWipeRomRequest.createTargetsVector(builder, targets);
            MbWipeRomRequest.startMbWipeRomRequest(builder);
            MbWipeRomRequest.addRomId(builder, fbRomId);
            MbWipeRomRequest.addTargets(builder, fbTargets);
            int fbRequest = MbWipeRomRequest.endMbWipeRomRequest(builder);

            // Send request
            MbWipeRomResponse response = (MbWipeRomResponse) sendRequest(builder, fbRequest,
                    RequestType.MbWipeRomRequest, ResponseType.MbWipeRomResponse);

            WipeResult result = new WipeResult();
            result.succeeded = new short[response.succeededLength()];
            result.failed = new short[response.failedLength()];

            for (int i = 0; i < response.succeededLength(); i++) {
                result.succeeded[i] = response.succeeded(i);
            }
            for (int i = 0; i < response.failedLength(); i++) {
                result.failed[i] = response.failed(i);
            }

            return result;
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    public static class PackageCounts {
        public int systemPackages;
        public int systemUpdatePackages;
        public int nonSystemPackages;
    }

    @Nullable
    public synchronized PackageCounts getPackagesCounts(Context context, String romId) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            int fbRomId = builder.createString(romId);
            MbGetPackagesCountRequest.startMbGetPackagesCountRequest(builder);
            MbGetPackagesCountRequest.addRomId(builder, fbRomId);
            int fbRequest = MbGetPackagesCountRequest.endMbGetPackagesCountRequest(builder);

            // Send request
            MbGetPackagesCountResponse response = (MbGetPackagesCountResponse) sendRequest(builder, fbRequest,
                    RequestType.MbGetPackagesCountRequest, ResponseType.MbGetPackagesCountResponse);

            if (!response.success()) {
                return null;
            }

            PackageCounts pc = new PackageCounts();
            pc.systemPackages = (int) response.systemPackages();
            pc.systemUpdatePackages = (int) response.systemUpdatePackages();
            pc.nonSystemPackages = (int) response.nonSystemPackages();
            return pc;
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    /**
     * Get the SELinux label of a path.
     *
     * NOTE: If false is returned, there is no way of determining the cause of failure
     *       programmatically. However, mbtool will likely print debugging information (errno, etc.)
     *       to the logcat for manual reviewing.
     *
     * @param context Application context
     * @param path Absolute path
     * @param followSymlinks Whether to follow symlinks
     * @return SELinux label if it was successfully retrieved. False, otherwise.
     * @throws IOException When any socket communication error occurs
     */
    public synchronized String pathSelinuxGetLabel(Context context, String path, boolean followSymlinks)
            throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            int fbPath = builder.createString(path);
            PathSELinuxGetLabelRequest.startPathSELinuxGetLabelRequest(builder);
            PathSELinuxGetLabelRequest.addPath(builder, fbPath);
            PathSELinuxGetLabelRequest.addFollowSymlinks(builder, followSymlinks);
            int fbRequest = PathSELinuxGetLabelRequest.endPathSELinuxGetLabelRequest(builder);

            // Send request
            PathSELinuxGetLabelResponse response = (PathSELinuxGetLabelResponse) sendRequest(builder, fbRequest,
                    RequestType.PathSELinuxGetLabelRequest, ResponseType.PathSELinuxGetLabelResponse);

            if (!response.success()) {
                Log.e(TAG, "Failed to get SELinux label for " + path + ": " + response.errorMsg());
                return null;
            }

            return response.label();
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    /**
     * Set the SELinux label for a path.
     *
     * NOTE: If false is returned, there is no way of determining the cause of failure
     *       programmatically. However, mbtool will likely print debugging information (errno, etc.)
     *       to the logcat for manual reviewing.
     *
     * @param context Application context
     * @param path Absolute path
     * @param label SELinux label
     * @param followSymlinks Whether to follow symlinks
     * @return True if the SELinux label was successfully set. False, otherwise.
     * @throws IOException When any socket communication error occurs
     */
    public synchronized boolean pathSelinuxSetLabel(Context context, String path, String label,
            boolean followSymlinks) throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            int fbPath = builder.createString(path);
            int fbLabel = builder.createString(label);
            PathSELinuxSetLabelRequest.startPathSELinuxSetLabelRequest(builder);
            PathSELinuxSetLabelRequest.addPath(builder, fbPath);
            PathSELinuxSetLabelRequest.addLabel(builder, fbLabel);
            PathSELinuxSetLabelRequest.addFollowSymlinks(builder, followSymlinks);
            int fbRequest = PathSELinuxSetLabelRequest.endPathSELinuxSetLabelRequest(builder);

            // Send request
            PathSELinuxSetLabelResponse response = (PathSELinuxSetLabelResponse) sendRequest(builder, fbRequest,
                    RequestType.PathSELinuxSetLabelRequest, ResponseType.PathSELinuxSetLabelResponse);

            if (!response.success()) {
                Log.e(TAG, "Failed to set SELinux label for " + path + ": " + response.errorMsg());
                return false;
            }

            return true;
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    public synchronized long pathGetDirectorySize(Context context, String path, String[] exclusions)
            throws IOException {
        connect(context);

        try {
            // Create request
            FlatBufferBuilder builder = new FlatBufferBuilder(FBB_SIZE);
            int fbPath = builder.createString(path);
            int fbExclusions = 0;
            if (exclusions != null) {
                int[] exclusionOffsets = new int[exclusions.length];
                for (int i = 0; i < exclusions.length; i++) {
                    exclusionOffsets[i] = builder.createString(exclusions[i]);
                }

                fbExclusions = PathGetDirectorySizeRequest.createExclusionsVector(builder, exclusionOffsets);
            }
            PathGetDirectorySizeRequest.startPathGetDirectorySizeRequest(builder);
            PathGetDirectorySizeRequest.addPath(builder, fbPath);
            PathGetDirectorySizeRequest.addExclusions(builder, fbExclusions);
            int fbRequest = PathGetDirectorySizeRequest.endPathGetDirectorySizeRequest(builder);

            // Send request
            PathGetDirectorySizeResponse response = (PathGetDirectorySizeResponse) sendRequest(builder, fbRequest,
                    RequestType.PathGetDirectorySizeRequest, ResponseType.PathGetDirectorySizeResponse);

            if (!response.success()) {
                Log.e(TAG, "Failed to set directory size for " + path + ": " + response.errorMsg());
                return -1;
            }

            return response.size();
        } catch (IOException e) {
            disconnect();
            throw e;
        }
    }

    // Private helper functions

    @NonNull
    private synchronized Table sendRequest(FlatBufferBuilder builder, int fbRequest, byte fbRequestType,
            byte expected) throws IOException {
        ThreadUtils.enforceExecutionOnNonMainThread();

        Request.startRequest(builder);
        Request.addRequestType(builder, fbRequestType);
        Request.addRequest(builder, fbRequest);
        builder.finish(Request.endRequest(builder));

        SocketUtils.writeBytes(mSocketOS, builder.sizedByteArray());

        byte[] responseBytes = SocketUtils.readBytes(mSocketIS);
        ByteBuffer bb = ByteBuffer.wrap(responseBytes);
        Response response = Response.getRootAsResponse(bb);

        if (response.responseType() == ResponseType.Unsupported) {
            throw new IOException("Unsupported command");
        } else if (response.responseType() == ResponseType.Invalid) {
            throw new IOException("Invalid command request");
        } else if (response.responseType() != expected) {
            throw new IOException("Unexpected response type (response=" + response.responseType() + ", expected="
                    + expected + ")");
        }

        Table table;

        switch (response.responseType()) {
        case ResponseType.FileChmodResponse:
            table = new FileChmodResponse();
            break;
        case ResponseType.FileCloseResponse:
            table = new FileCloseResponse();
            break;
        case ResponseType.FileOpenResponse:
            table = new FileOpenResponse();
            break;
        case ResponseType.FileReadResponse:
            table = new FileReadResponse();
            break;
        case ResponseType.FileSeekResponse:
            table = new FileSeekResponse();
            break;
        case ResponseType.FileStatResponse:
            table = new FileStatResponse();
            break;
        case ResponseType.FileWriteResponse:
            table = new FileWriteResponse();
            break;
        case ResponseType.FileSELinuxGetLabelResponse:
            table = new FileSELinuxGetLabelResponse();
            break;
        case ResponseType.FileSELinuxSetLabelResponse:
            table = new FileSELinuxSetLabelResponse();
            break;
        case ResponseType.PathChmodResponse:
            table = new PathChmodResponse();
            break;
        case ResponseType.PathCopyResponse:
            table = new PathCopyResponse();
            break;
        case ResponseType.PathSELinuxGetLabelResponse:
            table = new PathSELinuxGetLabelResponse();
            break;
        case ResponseType.PathSELinuxSetLabelResponse:
            table = new PathSELinuxSetLabelResponse();
            break;
        case ResponseType.PathGetDirectorySizeResponse:
            table = new PathGetDirectorySizeResponse();
            break;
        case ResponseType.MbGetVersionResponse:
            table = new MbGetVersionResponse();
            break;
        case ResponseType.MbGetInstalledRomsResponse:
            table = new MbGetInstalledRomsResponse();
            break;
        case ResponseType.MbGetBootedRomIdResponse:
            table = new MbGetBootedRomIdResponse();
            break;
        case ResponseType.MbSwitchRomResponse:
            table = new MbSwitchRomResponse();
            break;
        case ResponseType.MbSetKernelResponse:
            table = new MbSetKernelResponse();
            break;
        case ResponseType.MbWipeRomResponse:
            table = new MbWipeRomResponse();
            break;
        case ResponseType.MbGetPackagesCountResponse:
            table = new MbGetPackagesCountResponse();
            break;
        case ResponseType.RebootResponse:
            table = new RebootResponse();
            break;
        default:
            throw new IOException("Invalid response type");
        }

        Table ret = response.response(table);

        if (ret == null) {
            throw new IOException("Invalid union data");
        }

        return ret;
    }
}