【Android】USB偏好设置-mtp 文件传输

USB偏好设置

在 Android 设备上,USB 偏好设置(USB Preferences)允许用户自定义设备通过 USB 连接电脑或其他主机时的行为,例如选择文件传输模式、充电模式或网络共享等。这个功能通常在连接 USB 数据线后,通过通知栏或系统设置进行配置。

USB偏好设置的主要选项

当 Android 设备通过 USB 连接电脑时,通常会弹出通知或提示,让用户选择 USB 用途。常见的选项包括:

文件传输(MTP)

用途:在电脑和手机之间传输文件(照片、视频、文档等)

适用场景:备份照片或音乐到电脑;从电脑导入文件到手机。

特点:手机存储以“媒体设备”形式显示在电脑上;不会影响手机正常使用(可同时使用其他应用)。

照片传输(PTP)

用途:仅传输照片和视频(适用于老式相机或某些软件)

适用场景:需要快速导出照片(如 Adobe Lightroom 导入);兼容性更好的旧设备连接。

特点:电脑仅识别 DCIM(相机照片)文件夹;比 MTP 更简单,但功能有限。

USB 网络共享(USB Tethering)

用途:将手机的移动网络共享给电脑使用(类似有线热点)。

适用场景:电脑没有 Wi-Fi 时(如台式机)通过手机上网;比 Wi-Fi 热点更稳定、耗电更低。

特点:需要手机开启移动数据;部分运营商可能限制此功能。

MIDI(音乐设备数字接口)

用途:连接 MIDI 设备(如电子琴、音乐控制器)。

适用场景:音乐制作(如使用 FL Studio、GarageBand);外接 MIDI 键盘或鼓机。

特点:低延迟音频传输;仅适用于专业音乐应用。

仅充电

用途:仅通过 USB 充电,不传输数据。

适用场景:在公共 USB 端口(如机场、网吧)充电时避免数据泄露;快速充电(某些电脑 USB 端口电流较低)。

特点:电脑无法访问手机文件,更安全。

默认 USB 配置(Android 10+)

在 开发者选项 中,可以设置默认的 USB 行为(如自动进入文件传输模式)。

路径:设置 > 系统 > 开发者选项 > 默认 USB 配置。

mtp 文件传输

MTP(Media Transfer Protocol,媒体传输协议)是一种由微软开发的通信协议,主要用于在计算机和便携设备(如智能手机、数码相机、媒体播放器等)之间传输媒体文件。它是传统USB大容量存储(USB Mass Storage, UMS)的替代方案,解决了UMS在文件系统兼容性和设备独占访问等方面的局限性。

典型应用场景

Android设备传输文件:连接电脑后通过MTP管理照片、音乐等。

数码相机/播放器:导出媒体内容而不影响设备正常运行。

云服务同步:部分应用通过MTP协议与本地设备交互。

源码分析

点击 “设置app” -> “已连接的设备” -> “USB” 即可进入到 USB 偏好设置界面。

设置app进入到一个界面后,可以用以下 adb 命令打印一下日志:

adb logcat -s SettingsActivity > SettingsActivity.txt

随后可以看到以下日志信息:

--------- beginning of main

05-24 17:40:07.273 5090 15496 D SettingsActivity: No enabled state changed, skipping updateCategory call

05-24 17:40:13.650 5090 5090 D SettingsActivity: Starting onCreate

05-24 17:40:13.654 5090 5090 D SettingsActivity: Starting to set activity title

05-24 17:40:13.654 5090 5090 D SettingsActivity: Done setting title

05-24 17:40:13.654 5090 5090 D SettingsActivity: Switching to fragment com.android.settings.connecteddevice.ConnectedDeviceDashboardFragment

05-24 17:40:13.662 5090 5090 D SettingsActivity: MetricsCategory is 747

05-24 17:40:13.667 5090 5090 D SettingsActivity: Executed frag manager pendingTransactions

05-24 17:40:13.668 5090 5090 D SettingsActivity: MetricsCategory is 747

05-24 17:40:13.693 5090 15496 D SettingsActivity: No enabled state changed, skipping updateCategory call

05-24 17:40:15.221 5090 5090 D SettingsActivity: Starting onCreate

05-24 17:40:15.224 5090 5090 D SettingsActivity: Starting to set activity title

05-24 17:40:15.224 5090 5090 D SettingsActivity: Done setting title

05-24 17:40:15.224 5090 5090 D SettingsActivity: Switching to fragment com.android.settings.connecteddevice.usb.UsbDetailsFragment

05-24 17:40:15.230 5090 5090 D SettingsActivity: MetricsCategory is 1291

05-24 17:40:15.236 5090 5090 D SettingsActivity: Executed frag manager pendingTransactions

05-24 17:40:15.237 5090 5090 D SettingsActivity: MetricsCategory is 1291

05-24 17:40:15.247 5090 15496 D SettingsActivity: No enabled state changed, skipping updateCategory call

于是,我们可以定位到 UsbDetailsFragment 就是 USB 偏好设置的 fragment:

USBDetailsFragment.java

public class UsbDetailsFragment extends DashboardFragment {

private static final String TAG = UsbDetailsFragment.class.getSimpleName();

private List mControllers;

private UsbBackend mUsbBackend;

private boolean mUserAuthenticated = false;

@VisibleForTesting

UsbConnectionBroadcastReceiver mUsbReceiver;

private UsbConnectionBroadcastReceiver.UsbConnectionListener mUsbConnectionListener =

(connected, functions, powerRole, dataRole, isUsbFigured) -> {

for (UsbDetailsController controller : mControllers) {

controller.refresh(connected, functions, powerRole, dataRole);

}

};

private UsbConnectionBroadcastReceiver.UsbConnectionListener mUsbConnectionListener =

(connected, functions, powerRole, dataRole, isUsbFigured) -> {

for (UsbDetailsController controller : mControllers) {

controller.refresh(connected, functions, powerRole, dataRole);

}

};

boolean isUserAuthenticated() {

return mUserAuthenticated;

}

void setUserAuthenticated(boolean userAuthenticated) {

mUserAuthenticated = userAuthenticated;

}

@Override

public void onStart() {

super.onStart();

mUserAuthenticated = false;

}

@Override

public void onViewCreated(View view, Bundle savedInstanceState) {

super.onViewCreated(view, savedInstanceState);

Utils.setActionBarShadowAnimation(getActivity(), getSettingsLifecycle(), getListView());

}

@Override

public int getMetricsCategory() {

return SettingsEnums.USB_DEVICE_DETAILS;

}

@Override

protected String getLogTag() {

return TAG;

}

@Override

protected int getPreferenceScreenResId() {

return R.xml.usb_details_fragment;

}

@Override

protected List createPreferenceControllers(Context context) {

mUsbBackend = new UsbBackend(context);

mControllers = createControllerList(context, mUsbBackend, this);

mUsbReceiver = new UsbConnectionBroadcastReceiver(context, mUsbConnectionListener,

mUsbBackend);

this.getSettingsLifecycle().addObserver(mUsbReceiver);

return new ArrayList<>(mControllers);

}

private static List createControllerList(Context context,

UsbBackend usbBackend, UsbDetailsFragment fragment) {

List ret = new ArrayList<>();

ret.add(new UsbDetailsHeaderController(context, fragment, usbBackend));

ret.add(new UsbDetailsDataRoleController(context, fragment, usbBackend));

ret.add(new UsbDetailsFunctionsController(context, fragment, usbBackend));

ret.add(new UsbDetailsPowerRoleController(context, fragment, usbBackend));

ret.add(new UsbDetailsTranscodeMtpController(context, fragment, usbBackend));

return ret;

}

/**

* For Search.

*/

public static final BaseSearchIndexProvider SEARCH_INDEX_DATA_PROVIDER =

new BaseSearchIndexProvider(R.xml.usb_details_fragment) {

@Override

protected boolean isPageSearchEnabled(Context context) {

return checkIfUsbDataSignalingIsDisabled(

context, UserHandle.myUserId()) == null;

}

@Override

public List createPreferenceControllers(

Context context) {

return new ArrayList<>(

createControllerList(context, new UsbBackend(context), null));

}

};

}

于是可以走到 R.xml.usb_details_fragment.xml 中看一下布局的代码(这个代码较为简单,此不列出),于是可以得知“USB 的用途”的列表项是通过 java 代码进行动态加载的。

定位到 USBDetailsFragment.java 中的 createPreferenceControllers(Context context)方法,然后再定位到 createControllerList(Context context, UsbBackend usbBackend, UsbDetailsFragment fragment) 方法,此时可知 UsbDetailsFunctionsController.java 便是与“USB 的用途”的列表项有关的 controller 类,其核心代码如下:

UsbDetailsFunctionsController.java

private SelectorWithWidgetPreference getProfilePreference(String key, int titleId) {

SelectorWithWidgetPreference pref = mProfilesContainer.findPreference(key);

if (pref == null) {

pref = new SelectorWithWidgetPreference(mProfilesContainer.getContext());

pref.setKey(key);

pref.setTitle(titleId);

pref.setSingleLineTitle(false);

pref.setOnClickListener(this);

mProfilesContainer.addPreference(pref);

}

return pref;

}

@Override

protected void refresh(boolean connected, long functions, int powerRole, int dataRole) {

if (DEBUG) {

Log.d(TAG, "refresh() connected : " + connected + ", functions : " + functions

+ ", powerRole : " + powerRole + ", dataRole : " + dataRole);

}

if (!connected || dataRole != DATA_ROLE_DEVICE) {

mProfilesContainer.setEnabled(false);

} else {

// Functions are only available in device mode

mProfilesContainer.setEnabled(true);

}

SelectorWithWidgetPreference pref;

for (long option : FUNCTIONS_MAP.keySet()) {

int title = FUNCTIONS_MAP.get(option);

pref = getProfilePreference(UsbBackend.usbFunctionsToString(option), title);

// Only show supported options

if (mUsbBackend.areFunctionsSupported(option)) {

if (isAccessoryMode(functions)) {

pref.setChecked(UsbManager.FUNCTION_MTP == option);

} else if (functions == UsbManager.FUNCTION_NCM) {

pref.setChecked(UsbManager.FUNCTION_RNDIS == option);

} else {

pref.setChecked(functions == option);

}

} else {

mProfilesContainer.removePreference(pref);

}

}

}

那么此时我们就可以得知“USB 的用途”的列表项显示与否,就取决于 mUsbBackend.areFunctionsSupported(option) 的回调值了。那么定位到 UsbBackend.java 的核心代码:

UsbBackend.java

public class UsbBackend {

private final boolean mFileTransferRestricted;

private final boolean mFileTransferRestrictedBySystem;

private final boolean mTetheringRestricted;

private final boolean mTetheringRestrictedBySystem;

private final boolean mMidiSupported;

private final boolean mTetheringSupported;

private final boolean mUVCEnabled;

private final boolean mIsAdminUser;

private UsbManager mUsbManager;

@Nullable

private UsbPort mPort;

@Nullable

private UsbPortStatus mPortStatus;

public UsbBackend(Context context) {

this(context, (UserManager) context.getSystemService(Context.USER_SERVICE));

}

@VisibleForTesting

public UsbBackend(Context context, UserManager userManager) {

mUsbManager = context.getSystemService(UsbManager.class);

mFileTransferRestricted = isUsbFileTransferRestricted(userManager);

mFileTransferRestrictedBySystem = isUsbFileTransferRestrictedBySystem(userManager);

mTetheringRestricted = isUsbTetheringRestricted(userManager);

mTetheringRestrictedBySystem = isUsbTetheringRestrictedBySystem(userManager);

mUVCEnabled = isUvcEnabled();

mIsAdminUser = userManager.isAdminUser();

mMidiSupported = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI);

final TetheringManager tm = context.getSystemService(TetheringManager.class);

mTetheringSupported = tm.isTetheringSupported();

updatePorts();

}

public boolean areFunctionsSupported(long functions) {

if ((!mMidiSupported && (functions & UsbManager.FUNCTION_MIDI) != 0)

|| (!mTetheringSupported && (functions & UsbManager.FUNCTION_RNDIS) != 0)) {

return false;

}

return !(areFunctionDisallowed(functions) || areFunctionsDisallowedBySystem(functions)

|| areFunctionsDisallowedByNonAdminUser(functions));

}

private boolean areFunctionDisallowed(long functions) {

return (mFileTransferRestricted && ((functions & UsbManager.FUNCTION_MTP) != 0

|| (functions & UsbManager.FUNCTION_PTP) != 0))

|| (mTetheringRestricted && ((functions & UsbManager.FUNCTION_RNDIS) != 0));

}

private boolean areFunctionsDisallowedBySystem(long functions) {

return (mFileTransferRestrictedBySystem && ((functions & UsbManager.FUNCTION_MTP) != 0

|| (functions & UsbManager.FUNCTION_PTP) != 0))

|| (mTetheringRestrictedBySystem && ((functions & UsbManager.FUNCTION_RNDIS) != 0))

|| (!mUVCEnabled && ((functions & UsbManager.FUNCTION_UVC) != 0));

}

}

设置新的 “USB 的用途”,定位到 UsbManager.java:

UsbManager.java

@SystemApi

@RequiresPermission(Manifest.permission.MANAGE_USB)

public void setCurrentFunctions(@UsbFunctionMode long functions) {

int operationId = sUsbOperationCount.incrementAndGet() + Binder.getCallingUid();

try {

mService.setCurrentFunctions(functions, operationId);

} catch (RemoteException e) {

Log.e(TAG, "setCurrentFunctions: failed to call setCurrentFunctions. functions:"

+ functions + ", opId:" + operationId, e);

throw e.rethrowFromSystemServer();

}

}

/**

* Sets the current USB functions when in device mode.

*

* @deprecated use setCurrentFunctions(long) instead.

* @param functions the USB function(s) to set.

* @param usbDataUnlocked unused

* @hide

*/

@Deprecated

@UnsupportedAppUsage

public void setCurrentFunction(String functions, boolean usbDataUnlocked) {

int operationId = sUsbOperationCount.incrementAndGet() + Binder.getCallingUid();

try {

mService.setCurrentFunction(functions, usbDataUnlocked, operationId);

} catch (RemoteException e) {

Log.e(TAG, "setCurrentFunction: failed to call setCurrentFunction. functions:"

+ functions + ", opId:" + operationId, e);

throw e.rethrowFromSystemServer();

}

}

因此,若想选择“USB 用途”,例如选择开启/关闭mtp,可以直接调用以下代码:

usbManager.setCurrentFunction(UsbManager.USB_FUNCTION_MTP, true);// 选择“文件传输mtp”列表项

usbManager.setCurrentFunction(UsbManager.USB_FUNCTION_NONE, true);// 选择“不用于文件传输”列表项

有时候,我们会有禁用掉一些“USB 用途”的需求,例如启用/禁用mtp,可以调用以下代码:

mUserManager.setUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER, true);// 启用 mtp

mUserManager.setUserRestriction(UserManager.DISALLOW_USB_FILE_TRANSFER, false);// 禁用 mtp

从 UsbDetailsFragment.java 的 createPreferenceControllers(Context context) 函数中,可以看到已经用观察者模式注册了 mUsbConnectionListener 的回调,而该回调通过打日志,也不难发现确实每次我们在设置一个“USB 用途”的时候都会回调 Controller 类的 refresh() 方法,也就是刷新界面。但是当我们调用 mUserManager.setUserRestriction() 时,会发现界面并不能按我们预料的那般刷新界面。

既然在 refresh() 函数中,会判断 mUsbBackend.areFunctionsSupported(option) 来刷新界面,那大概率问题也出在这里面。以开启/关闭 mtp 为例,对于上面列出的 UsbBackend.java 源码,我们易发现变量 mFileTransferRestricted 和变量 mFileTransferRestrictedBySystem 都是 final 类型的,亦即进入“USB 偏好设置”后,mUsbBackend.areFunctionsSupported(option) 获取到的结果其实是只会获取一次的,因此无法做到在调用 mUserManager.setUserRestriction()后动态刷新界面。想要实现在开启/禁用 mtp 后动态刷新“USB 偏好设置”界面,只需要作出如下修改:

--- a/src/com/android/settings/connecteddevice/usb/UsbBackend.java

+++ b/src/com/android/settings/connecteddevice/usb/UsbBackend.java

@@ -47,15 +47,16 @@ public class UsbBackend {

static final int PD_ROLE_SWAP_TIMEOUT_MS = 4000;

static final int NONPD_ROLE_SWAP_TIMEOUT_MS = 15000;

- private final boolean mFileTransferRestricted;

- private final boolean mFileTransferRestrictedBySystem;

+ private boolean mFileTransferRestricted;

+ private boolean mFileTransferRestrictedBySystem;

private final boolean mTetheringRestricted;

private final boolean mTetheringRestrictedBySystem;

private final boolean mMidiSupported;

private final boolean mTetheringSupported;

private final boolean mUVCEnabled;

private final boolean mIsAdminUser;

-

+

+ private UserManager mUserManager;

private UsbManager mUsbManager;

@Nullable

@@ -77,6 +78,7 @@ public class UsbBackend {

mTetheringRestrictedBySystem = isUsbTetheringRestrictedBySystem(userManager);

mUVCEnabled = isUvcEnabled();

mIsAdminUser = userManager.isAdminUser();

+ mUserManager = userManager;

mMidiSupported = context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_MIDI);

final TetheringManager tm = context.getSystemService(TetheringManager.class);

@@ -206,12 +208,14 @@ public class UsbBackend {

}

private boolean areFunctionDisallowed(long functions) {

+ mFileTransferRestricted = isUsbFileTransferRestricted(mUserManager);

return (mFileTransferRestricted && ((functions & UsbManager.FUNCTION_MTP) != 0

|| (functions & UsbManager.FUNCTION_PTP) != 0))

|| (mTetheringRestricted && ((functions & UsbManager.FUNCTION_RNDIS) != 0));

}

private boolean areFunctionsDisallowedBySystem(long functions) {

+ mFileTransferRestrictedBySystem = isUsbFileTransferRestrictedBySystem(mUserManager);

return (mFileTransferRestrictedBySystem && ((functions & UsbManager.FUNCTION_MTP) != 0

|| (functions & UsbManager.FUNCTION_PTP) != 0))

|| (mTetheringRestrictedBySystem && ((functions & UsbManager.FUNCTION_RNDIS) != 0))