gossip

The company took over the project to develop an online upgrade function, in which I needed to realize the communication between mobile and PC. The company chose to use MTP to implement this requirement, so I analyzed a lot of MTP code, from the Frameworks layer to the app to the JNI layer. In view of the lack of online articles about this, and my development process is relatively long, so I decided to write down the framework, APP, JNI layer analysis, hoping to help develop similar functions of partners.

Deng Fanping, a teacher who wrote a series of in-depth understanding of Android books, some of which are no longer published, but who posted all the content of his articles on his blog, said knowledge needs to be passed on. This, I deeply admire.

Service is open

UsbService is a system service that is created and registered in the system_server process.

private static final String USB_SERVICE_CLASS =
        "com.android.server.usb.UsbService$Lifecycle";
            
private void startOtherServices(a) {
    // ...
    
    mSystemServiceManager.startService(USB_SERVICE_CLASS);
    
    // ...   
}
Copy the code

SystemServiceManager creates the UsbService$Lifecycle object (Lifecycle is an internal class of the UsbService) through reflection, adds it to the List collection, and finally calls the onStart method of the Lifcycle object.

SystemServiceManager stores the various services and tells the service about the various phases of system startup. We can look at the various Lifecycle callbacks of UsbService$Lifecycle

public class UsbService extends IUsbManager.Stub {

    public static class Lifecycle extends SystemService {
    
        // Service creation phase
        @Override
        public void onStart(a) {
            
            mUsbService = new UsbService(getContext());
            publishBinderService(Context.USB_SERVICE, mUsbService);
        }
        
        // Respond to the system startup phase
        @Override
        public void onBootPhase(int phase) {
            if (phase == SystemService.PHASE_ACTIVITY_MANAGER_READY) {
				// System ready
                mUsbService.systemReady();
            } else if (phase == SystemService.PHASE_BOOT_COMPLETED) {
            	// The system is startedmUsbService.bootCompleted(); }}}}Copy the code

As you can see, a service goes through creation phases, system ready phases, and system startup phases. Next, the UsbService startup process is analyzed in three parts.

Service creation phase

In the service creation phase, a UsbService object is first created, and since UsbService is a Binder object, the service is then published to the ServiceManager. Once the service is published, clients can access it.

Now look at the UsbService constructor

    public UsbService(Context context) {
        // ...
        
        if (new File("/sys/class/android_usb").exists()) {
            mDeviceManager = new UsbDeviceManager(context, mAlsaManager, mSettingsManager);
        }

        // ...
    }
Copy the code

In the constructor, the main MTP-related code is to create the UsbDeviceManager object.

In MTP mode, the Android Device functions as the Device side and UsbDeviceManager is used to process transactions on the Device side.

Now what does the constructor of UsbDeviceManager do

    public UsbDeviceManager(Context context, UsbAlsaManager alsaManager, UsbSettingsManager settingsManager) {
        // ...
        
        // My project does not support Hal layer of MTP
        if (halNotPresent) {
            1. Initialize mHandler
            mHandler = new UsbHandlerLegacy(FgThread.get().getLooper(), mContext, this,
                    alsaManager, settingsManager);
        } else {
            // ...
        }


        // 2. Register various broadcasts
        / /... There are actually a number of broadcast receivers registered (but with the code omitted)
        BroadcastReceiver chargingReceiver = new BroadcastReceiver() {
            @Override
            public void onReceive(Context context, Intent intent) {
                int chargePlug = intent.getIntExtra(BatteryManager.EXTRA_PLUGGED, -1);
                booleanusbCharging = chargePlug == BatteryManager.BATTERY_PLUGGED_USB; mHandler.sendMessage(MSG_UPDATE_CHARGING_STATE, usbCharging); }}; mContext.registerReceiver(chargingReceiver,new IntentFilter(Intent.ACTION_BATTERY_CHANGED));
        
        
        // 3. Listen for USB status changes
        mUEventObserver = new UsbUEventObserver();
        mUEventObserver.startObserving(USB_STATE_MATCH);
        mUEventObserver.startObserving(USB_STATE_MATCH_SEC);
        mUEventObserver.startObserving(ACCESSORY_START_MATCH);
    }
Copy the code

The constructor for UsbDeviceManager does three things.

First thing, initialize the mHandler object. Since my project does not support the Hal layer of MTP, mHandler is initialized using the UsbHandlerLegacy object.

Second, register various broadcast receivers, such as port changes, language changes, etc. Here I’ve shown the radio receiver code for charging. When we connect the phone to the computer through the USB cable, the phone will charge and a notification about USB charging will appear on the phone. By opening this notification about USB, we can switch USB functions, such as MTP, PTP, etc.

Third, listen for USB state changes through the Linux Uevent mechanism. When your phone connects to your computer over a USB cable, the USB state changes from DISCONNECTED to CONNECTED to CONFIGURED. The USB status update operation is handled when the state changes, which will be analyzed later.

Now look at the creation of the UsbHandlerLegacy object.

        UsbHandlerLegacy(Looper looper, Context context, UsbDeviceManager deviceManager,
                UsbAlsaManager alsaManager, UsbSettingsManager settingsManager) {
            // The superclass constructor initializes some parameters, which you can analyze for yourself as needed
            super(looper, context, deviceManager, alsaManager, settingsManager);
            try {
                // 1. Read the OEM coverage configuration
                readOemUsbOverrideConfig(context);
                
                // 2. Read the values of various attributes
                // 2.1 In normal mode, the value of the persist.sys.usb.config property is read
                mCurrentOemFunctions = getSystemProperty(getPersistProp(false),
                        UsbManager.USB_FUNCTION_NONE);
                        
                // If the value of ro.bootmode is normal or unknown, it indicates normal startup
                if (isNormalBoot()) {
                    // 2.2 Read the value of the sys.usb.config property, which represents the currently set USB function
                    mCurrentFunctionsStr = getSystemProperty(USB_CONFIG_PROPERTY,
                            UsbManager.USB_FUNCTION_NONE);
                    // 2.3 Compare the value of the sys.usb.config property with that of the sys.usb.state property
                    // The sys.usb.state property represents the actual function of the USB
                    // If the two properties are equal, all usb Settings are in effect
                    mCurrentFunctionsApplied = mCurrentFunctionsStr.equals(
                            getSystemProperty(USB_STATE_PROPERTY, UsbManager.USB_FUNCTION_NONE));
                } else{}// mCurrentFunctions represents the CURRENT USB function to be set. The initial value is 0
                mCurrentFunctions = UsbManager.FUNCTION_NONE;
                mCurrentUsbFunctionsReceived = true;
                
                // 3. Read the USB status once and then update it
                String state = FileUtils.readTextFile(new File(STATE_PATH), 0.null).trim();
                updateState(state);
            } catch (Exception e) {
                Slog.e(TAG, "Error initializing UsbHandler", e); }}Copy the code

The constructors for UsbHandlerLegacy are roughly divided into three steps. First look at the first step, read the OEM’s coverage configuration for USB functionality.

        private void readOemUsbOverrideConfig(Context context) {
            Bootmode :[original USB mode]:[USB mode used]
            String[] configList = context.getResources().getStringArray(
                    com.android.internal.R.array.config_oemUsbModeOverride);

            if(configList ! =null) {
                for (String config : configList) {
                    String[] items = config.split(":");
                    if (items.length == 3 || items.length == 4) {
                        if (mOemModeMap == null) {
                            mOemModeMap = new HashMap<>();
                        }
                        HashMap<String, Pair<String, String>> overrideMap =
                                mOemModeMap.get(items[0]);
                        if (overrideMap == null) {
                            overrideMap = new HashMap<>();
                            mOemModeMap.put(items[0], overrideMap);
                        }

                        // Favoring the first combination if duplicate exists
                        if(! overrideMap.containsKey(items[1]) {if (items.length == 3) {
                                overrideMap.put(items[1].new Pair<>(items[2].""));
                            } else {
                                overrideMap.put(items[1].new Pair<>(items[2], items[3]));
                            }
                        }
                    }
                }
            }
        }
Copy the code

The config_oemUsbModeOverride array is read and saved to mOemModeMap. The format of each item is [bootmode]:[original USB mode]:[USB mode used], The saved format can be roughly described as HashMap

. In my project, this array is empty.
,>

Then the second step reads the various property values (considering only normal startup mode) as follows.

  1. MCurrentOemFunctions value ispersist.sys.usb.configProperty value. According to the source code comments, this property value stores the status of ADB enabled (if ADB is enabled, the value will contain the ADB string). In addition, the source code notes that this property can also be used for carrier custom functions, but only for testing purposes.
  2. MCurrentFunctionsStr value issys.usb.configAttribute values. This property represents the value of the usb function currently set. On a daily basis, we can set this property value via adb shell commands to switch USB functions, for exampleadb shell setprop sys.usb.config mtp,adbYou can switch to MTP.
  3. If throughsys.usb.configProperty switch succeeded, thensys.usb.stateThe property value is equal tosys.usb.configProperty values are the same. That is to say,sys.usb.stateA value that represents the actual function of the USB. Therefore, you can compare the two values to determine whether all usb functions have been successfully switched. If so, mCurrentFunctionsApplied is 1, otherwise it is 0.

In the third step, the current USB state is read and an update operation is performed. The update operation sends notifications, as well as broadcasts, but this operation cannot be performed now that the service creation phase is being processed, so it is not analyzed here. However, when dealing with the system readiness phase or the system startup phase, you can do something about it, as you’ll see in the analysis below.

System readiness stage

According to the previous code, during the system readiness phase, the systemRead() method of UsbService is called and then the systemRead() method of UsbDeviceManager is passed on

    public void systemReady(a) {
        // Register a callback about the screen state. There are two methods
        LocalServices.getService(ActivityTaskManagerInternal.class).registerScreenObserver(this);

        mHandler.sendEmptyMessage(MSG_SYSTEM_READY);
    }
Copy the code

First, a screen callback is registered, which is used to set up usb functions under the security lock screen. However, this feature appears to be in development and can only be operated with the ADB shell command. You can see the help by typing adb shell SVC USB.

Next, a message MSG_SYSTEM_READY is sent. How is this message handled

        case MSG_SYSTEM_READY:
            // The Notification service interface is obtained
            mNotificationManager = (NotificationManager)
                    mContext.getSystemService(Context.NOTIFICATION_SERVICE);

            // Register a callback with adb Service to state the ADB related state
            LocalServices.getService(
                    AdbManagerInternal.class).registerTransport(new AdbTransport(this));

            // Ensure that the notification channels are set up
            if (isTv()) {
                // ...
            }
            // Set the system ready flag bit
            mSystemReady = true;
            // The system has not been started yet
            // This is probably due to historical code redundancy
            finishBoot();
            break;
Copy the code

As you can see, the interface of the notification service was obtained during the system readiness phase, which also proves that notifications cannot be sent during the UsbService creation phase. However, I am a little confused. The notification service is in the same process as the UsbService, and the notification service also has an internal interface for the system service to call. Why do we need to send a broadcast through NotificationManager? Just for the convenience of writing code, but in doing so, an unnecessary Binder communication (maybe I’m lying!) was created. ?

Once you get the notification service interface, you register a callback with the ADB service, through which you can receive messages about ADB opening/closing.

System startup phase

Now let’s look at the final stage, the system boot up phase. Based on the previous code, the bootCompleted () method of UsbService is called, followed by the bootCompleted () method of UsbDeviceManager

    public void bootCompleted(a) {
        mHandler.sendEmptyMessage(MSG_BOOT_COMPLETED);
    }
Copy the code

I’m just sending a message. How is the message handled

        case MSG_BOOT_COMPLETED:
			// Set a flag indicating that the system has started
            mBootCompleted = true;
            finishBoot();
            break;
Copy the code

Quite simply, a start flag is set and the finishBoot() method is called to complete the final task

        protected void finishBoot(a) {
            if (mBootCompleted && mCurrentUsbFunctionsReceived && mSystemReady) {
                // mPendingBootBroadcast is set during service creation
                if (mPendingBootBroadcast) {
                    // 1. Send/update a broadcast of the USB status change
                    updateUsbStateBroadcastIfNeeded(getAppliedFunctions(mCurrentFunctions));
                    mPendingBootBroadcast = false;
                }
                if(! mScreenLocked && mScreenUnlockedFunctions ! = UsbManager.FUNCTION_NONE) {// This function is still in the debugging stage, not analyzed
                    setScreenUnlockedFunctions();
                } else {
                    // 2. Set USB function to NONE
                    setEnabledFunctions(UsbManager.FUNCTION_NONE, false);
                }
                // About Accessory function
                if(mCurrentAccessory ! =null) {
                    mUsbDeviceManager.getCurrentSettings().accessoryAttached(mCurrentAccessory);
                }
				// 3. Send usb notification if the phone is connected to the computer, through which you can select USB mode
                updateUsbNotification(false);
				// 4. If ADB is enabled and the phone is connected to the computer, send adb notification
                updateAdbNotification(false);
                // About MIDI functionalityupdateUsbFunctions(); }}Copy the code

Step 1 sending USB status broadcast, step 3 USB notification, and step 4 ADB notification cannot be performed if the phone is not currently connected to the computer via USB cable. The only thing that can be done is the second step, setting the USB function to NONE.

OK, now finally comes the crucial step, setting up the USB function, which calls the setEnabledFunctions() method. The method itself is an abstract method, and in my project, the implementation class is UsbHandlerLegacy

        protected void setEnabledFunctions(long usbFunctions, boolean forceRestart) {
            // Check whether data is unlocked. Only MTP and PTP data are unlocked
            boolean usbDataUnlocked = isUsbDataTransferActive(usbFunctions);
            
            // Handle data unlock status changes
            if(usbDataUnlocked ! = mUsbDataUnlocked) {// Update the data unlock status
                mUsbDataUnlocked = usbDataUnlocked;
                // Update USB notifications
                updateUsbNotification(false);
                // If forceRestart is set to true, the USB function needs to be forcibly restarted
                forceRestart = true;
            }
            
            // Before setting the new USB function, save the old state to avoid the failure of setting the new function and can be restored
            final long oldFunctions = mCurrentFunctions;
            final boolean oldFunctionsApplied = mCurrentFunctionsApplied;
            
            // Try setting usb new features
            if (trySetEnabledFunctions(usbFunctions, forceRestart)) {
                return;
            }

            // The new function failed to be set
            if(oldFunctionsApplied && oldFunctions ! = usbFunctions) { Slog.e(TAG,"Failsafe 1: Restoring previous USB functions.");
                if (trySetEnabledFunctions(oldFunctions, false)) {
                    return; }}// If the callback fails, set the USB function to NONE
            if (trySetEnabledFunctions(UsbManager.FUNCTION_NONE, false)) {
                return;
            }

            // If setting NONE still fails, try setting NONE again
            if (trySetEnabledFunctions(UsbManager.FUNCTION_NONE, false)) {
                return;
            }
            
            // If this is the case, it is not the case.
            Slog.e(TAG, "Unable to set any USB functions!");
        }
Copy the code

First, check whether the data of the new USB function to be set is unlocked. Only the data of MTP and PTP mode is unlocked. This is why you can see the file in the phone on the PC after setting MTP or PTP mode.

If the data unlock status changes, then the status is updated, the USB broadcast is updated, and most importantly, the forceRestart variable is set to true, which means the USB function is forced to restart.

Finally, set up the new USB function. If you fail, fall back. Now how does the trySetEnabledFunctions() method set the new function

        private boolean trySetEnabledFunctions(long usbFunctions, boolean forceRestart) {
            // 1. Convert the new USB function to a string
			String functions = null;
			
			// If the new function is not NONE, convert
            if(usbFunctions ! = UsbManager.FUNCTION_NONE) { functions = UsbManager.usbFunctionsToString(usbFunctions); }// Save the new USB function to be set
            mCurrentFunctions = usbFunctions;
            
            // If the converted function is empty, fetch it from somewhere else
            if (functions == null || applyAdbFunction(functions)
                    .equals(UsbManager.USB_FUNCTION_NONE)) {
                // Get the value of the persist.sys.usb.config property
                functions = getSystemProperty(getPersistProp(true),
                            UsbManager.USB_FUNCTION_NONE);
                
                // If persist. Sys.usb. config is still NONE
                if (functions.equals(UsbManager.USB_FUNCTION_NONE))
                If adb is enabled, adb is returned, otherwise MTP is returned
                functions = UsbManager.usbFunctionsToString(getChargingFunctions());
            }

            // if adb is enabled, append ADB, otherwise remove ADB
            functions = applyAdbFunction(functions);

            // 2. Obtain the USB function covered by OEM
            String oemFunctions = applyOemOverrideFunction(functions);
            
            // Handle abnormal startup mode, ignore
            if(! isNormalBoot() && ! mCurrentFunctionsStr.equals(functions)) { setSystemProperty(getPersistProp(true), functions);
            }
            
            // 3. Set the new function
            if((! functions.equals(oemFunctions) && ! mCurrentOemFunctions.equals(oemFunctions)) || ! mCurrentFunctionsStr.equals(functions) || ! mCurrentFunctionsApplied || forceRestart) { Slog.i(TAG,"Setting USB config to " + functions);
                // Save the string value corresponding to the new function to be set
                mCurrentFunctionsStr = functions;
                // Save the string value of the OEM override function
                mCurrentOemFunctions = oemFunctions;
                mCurrentFunctionsApplied = false;

                // Disconnect the existing USB connection
                setUsbConfig(UsbManager.USB_FUNCTION_NONE);
                // Check whether it succeeded
                if(! waitForState(UsbManager.USB_FUNCTION_NONE)) { Slog.e(TAG,"Failed to kick USB config");
                    return false;
                }

                // Set the new feature. Note that OEM covered features are used here
                setUsbConfig(oemFunctions);
            
                // If the new feature includes MTP or PTP, update the USB status change broadcast
                // Broadcast receiver will map files in main memory to PC
                if (mBootCompleted
                        && (containsFunction(functions, UsbManager.USB_FUNCTION_MTP)
                        || containsFunction(functions, UsbManager.USB_FUNCTION_PTP))) {
                    updateUsbStateBroadcastIfNeeded(getAppliedFunctions(mCurrentFunctions));
                }
                
                // Wait for the new function to complete
                if(! waitForState(oemFunctions)) { Slog.e(TAG,"Failed to switch USB config to " + functions);
                    return false;
                }

                mCurrentFunctionsApplied = true;
            }
            return true;
        }
Copy the code

I’ve broken down the logic here into three steps.

First step, the USB function to be set into a string, there are two cases

  1. If new work is equal toFUNCTION_NONE, then the converted value changes frompersist.sys.usb.configIf NONE, it checks whether ADB is enabled. If it is enabled, the converted value is ADB. If not, the converted value is MTP. As the previous analysis said,persist.sys.usb.configIt mainly contains functions to determine whether ADB is enabled, and then some vendor-specific functions for testing purposes. For example, for the Qualcomm project, this value might beadb,diag, this diAG is Qualcomm’s own function.
  2. If the new feature is notFUNCTION_NONETo convert directly. New energy, for exampleFUNCTION_MTP, then the converted string ismtp.

After converting the string, decide whether to add or remove adb attributes from the converted string, depending on whether ADB is enabled.

Step 2: Obtain OEM covered features. As mentioned earlier, the default system does not use the override function, so the override function obtained here is the same as the string converted from the new function.

As I analyzed the code, I kept thinking about how to use this override feature. Based on my analysis of the code, the only rule is that major functionality cannot be covered. For example, if the string of the newly set function is MTP, then one of the elements in the override array should have the value normal: MTP: MTP,diag, where nomral means normal startup, MTP means original function, and MTP,diag means overwritten function. Note that After overwriting the function must save MTP this main function. Of course, this is just my personal analysis of the code and has not been verified. Here I have to poke fun at the designers of this feature. Is it so hard to write an example and precautions?

Step 3: Set up new features. However, before setting up a new feature, first disconnect the existing connection and then set up the new feature. The new functionality is set using the setUsbConfig() method

        private void setUsbConfig(String config) {
            / / set the sys. Usb. Config
            setSystemProperty(USB_CONFIG_PROPERTY, config);
        }
Copy the code

Shock!!! The original setting is the property value of sys.usb.config. Remember that this property value was explained in the previous analysis, which represents the usb function currently set, as evidenced here.

This also indicates that you can set this property with the ADB shell setprop command to control usb switching. In the actual work, it is always successful.

How do you determine success after setting this property? That’s what waitForState() does

        private boolean waitForState(String state) {
            String value = null;
            for (int i = 0; i < 20; i++) {
                // Get the sys.usb.stat value
                value = getSystemProperty(USB_STATE_PROPERTY, "");
                // Compare this to the value of the sys.usb.config property you just set
                if (state.equals(value)) return true;
                SystemClock.sleep(50);
            }
            return false;
        }
Copy the code

To tell you the truth, I saw this code, did eat a whale! This code is executed 20 times in one second to get the value of the sys.usb.state property and compare it to the value of the set sys.usb.config property. If the value is equal, the function is set successfully.

Remember? In the previous analysis, I also explained the use of the sys.USb. state property, which represents the actual functionality of USB and can be verified here.

So now there is a question, how to achieve the bottom of the USB function switch? Of course it is in response to the sys.usb.config property change, such as the underlying echo code of my project

on property:sys.usb.config=mtp,adb && property:sys.usb.configfs=0
	# write 0
    write /sys/class/android_usb/android0/enable 0
    # write serial number
    write /sys/class/android_usb/android0/iSerial ${ro.serialno}
    # write vid and pid
    write /sys/class/android_usb/android0/idVendor 05C6
    write /sys/class/android_usb/android0/idProduct 9039
    Set USB function to MTP, ADB
    write /sys/class/android_usb/android0/functions mtp,adb
    # Rewrite 1 enables the function
    write /sys/class/android_usb/android0/enable 1
    # start the adb
    start adbd
    Set sys.usb.state to sys.usb.config
    setprop sys.usb.state ${sys.usb.config}
Copy the code

According to the comments, you should have a good idea of the process.

So, you think that’s the end of it? Not yet, if the new Settings function is MTP or PTP, then update the broadcast.

        protected void updateUsbStateBroadcastIfNeeded(long functions) {
            // send a sticky broadcast containing current USB state
            Intent intent = new Intent(UsbManager.ACTION_USB_STATE);
            intent.addFlags(Intent.FLAG_RECEIVER_REPLACE_PENDING
                    | Intent.FLAG_RECEIVER_INCLUDE_BACKGROUND
                    | Intent.FLAG_RECEIVER_FOREGROUND);
            // Save the USB status value
            intent.putExtra(UsbManager.USB_CONNECTED, mConnected);
            intent.putExtra(UsbManager.USB_HOST_CONNECTED, mHostConnected);
            intent.putExtra(UsbManager.USB_CONFIGURED, mConfigured);
            intent.putExtra(UsbManager.USB_DATA_UNLOCKED,
                    isUsbTransferAllowed() && isUsbDataTransferActive(mCurrentFunctions));
            
            // Save the value of the new function to be set. For example, if MTP is set, the key is MTP and the value is true
            long remainingFunctions = functions;
            while(remainingFunctions ! =0) {
                intent.putExtra(UsbManager.usbFunctionsToString(
                        Long.highestOneBit(remainingFunctions)), true);
                remainingFunctions -= Long.highestOneBit(remainingFunctions);
            }

            // If the status does not change, no broadcast is sent
            if(! isUsbStateChanged(intent)) {return;
            }

            // Note that a sticky broadcast is sent
            sendStickyBroadcast(intent);
            mBroadcastedIntent = intent;
        
Copy the code

Note that a sticky broadcast is sent here. So who was the receiver of this broadcast? What did you do to receive this broadcast? That’s what the next article will be about.

The end of the

UsbService is an implementation of the USB protocol, such as MTP. PTP is built on the USB protocol, and UsbService is implemented. While this article is an MTP analysis, it’s not a small article to miss if you’re developing or customizing usb functionality.