MTP,全称是 Media Transfer Protocol(媒体传输协议),它是微软的一个为计算机和便携式设备之间传输图像、音乐等所定制的协议。MTP 的应用分两种角色,一个是作为 Initiator ,另一个作为 Responder 。基于Android的存储访问框架SAF(Storage Access Framework),提供应用存储的访问接口。
下面介绍Android设备如平板作为 Initiator 端的方式。

权限

需要声明MANAGE_DOCUMENTS 权限,此为系统签名保护的权限。

    <uses-permission android:name="android.permission.MANAGE_DOCUMENTS" />

监听Mtp设备插入

通过监听Mtp设备的ContentProvider的回调,根据 获取的Mtp设备数量来判断是否有设备插入或拔出。

public static final String AUTHORITY = "com.android.mtp.documents";
private final Uri mMtpUri = DocumentsContract.buildRootsUri(AUTHORITY);
mContext.getContentResolver().registerContentObserver(mMtpUri, false, mMtpDeviceUriObserver);private final ContentObserver mMtpDeviceUriObserver = new ContentObserver(new Handler(mContext.getMainLooper())) {@Overridepublic void onChange(boolean b, Uri uri) {if (uri != null && uri.equals(mMtpUri) && mOnMtpDeviceChangeListener != null)mOnMtpDeviceChangeListener.OnMtpDeviceChange(getMtpDeviceInfoList());}};public interface OnMtpDeviceChangeListener {void OnMtpDeviceChange(List<MtpDeviceInfo> newMtpDevices);}

获取Mtp设备列表

获取Mtp设备列表,DocumentsContract.Root 代表根目录:

    public List<MtpDeviceInfo> getMtpDeviceInfoList() {List<MtpDeviceInfo> list = new ArrayList<>();ContentProviderClient providerClient = mContext.getContentResolver().acquireUnstableContentProviderClient(mMtpUri);if (providerClient != null) {Cursor cursor = null;try {cursor = providerClient.query(mMtpUri, null, null, null, null);if (cursor != null) {while (cursor.moveToNext()) {MtpDeviceInfo deviceInfo = new MtpDeviceInfo();int flags = CursorUtils.getCursorInt(cursor, DocumentsContract.Root.COLUMN_FLAGS);int icon = CursorUtils.getCursorInt(cursor, DocumentsContract.Root.COLUMN_ICON);String title = CursorUtils.getCursorString(cursor, DocumentsContract.Root.COLUMN_TITLE);String summary = CursorUtils.getCursorString(cursor, DocumentsContract.Root.COLUMN_SUMMARY);String documentId = CursorUtils.getCursorString(cursor, DocumentsContract.Root.COLUMN_DOCUMENT_ID);long availableBytes = CursorUtils.getCursorLong(cursor, DocumentsContract.Root.COLUMN_AVAILABLE_BYTES);long capacityBytes = CursorUtils.getCursorLong(cursor, DocumentsContract.Root.COLUMN_CAPACITY_BYTES);deviceInfo.setTitle(title);deviceInfo.setSummary(summary);deviceInfo.setDocumentId(documentId);deviceInfo.setAvailableBytes(availableBytes);deviceInfo.setCapacityBytes(capacityBytes);deviceInfo.setIcon(icon);// deviceInfo.setUri(DocumentsContract.buildChildDocumentsUri(AUTHORITY, documentId));// 判断是否有数据(非仅充电模式),由于无法直接当前mtp的模式,只能通过获取的大小和标志进行判断if (availableBytes != -1) {list.add(deviceInfo);}}}} catch (RemoteException e) {e.printStackTrace();} finally {if (cursor != null) {cursor.close();}providerClient.close();}}return list;}

MtpDeviceInfo 为自定义的Mtp设备信息类,保存Mtp设备名称、概要、DocumentId(用以访问根目录的顶层目录,可构建DocumentsUri,遍历得到子目录)、可用大小、总容量(可能获取不到)、图标,可以按具体需要定义。具体代码如下:

    public class MtpDeviceInfo {private String title;private String summary;private String documentId;private long availableBytes;private long capacityBytes;private int icon;public String getTitle() {return title;}public void setTitle(String title) {this.title = title;}public String getSummary() {return summary;}public void setSummary(String summary) {this.summary = summary;}public String getDocumentId() {return documentId;}public void setDocumentId(String documentId) {this.documentId = documentId;}public long getAvailableBytes() {return availableBytes;}public void setAvailableBytes(long availableBytes) {this.availableBytes = availableBytes;}public long getCapacityBytes() {return capacityBytes;}public void setCapacityBytes(long capacityBytes) {this.capacityBytes = capacityBytes;}public int getIcon() {return icon;}public void setIcon(int icon) {this.icon = icon;}}

下面为Android系统的文档/文件浏览器 DocumentsUI 界面,显示通过数据线连接的手机储存:

访问Mtp设备目录

浏览Mtp手机存储设备,需要通过 Root根目录的 documentId 即上述获取的根目录的 **documentId **

    public static final String MTP_AUTHORITY = "com.android.mtp.documents";Uri childDocumentsUri = DocumentsContract.buildChildDocumentsUri(MTP_AUTHORITY, rootDocumentId);

之后根据成员变量 childDocumentsUri 得到对应目录的 Cursor :

  ContentProviderClient client = context.getContentResolver().acquireUnstableContentProviderClient(childDocumentsUri);Cursor cursor = null;if (client != null) {try {cursor = client.query(dirChildUri, null, null, null, null);if (cursor != null) {cursor = new SortingCursorWrapper(cursor, ComparatorUtils.getIns().getSortWay());updateDocumentData(cursor);}} catch (RemoteException e) {e.printStackTrace();} finally {client.close();}}

根据 Cursor 遍历得到各个文件的文件名称、mimeType文件类型、documentId、上次修改事件、 文件大小等:

        String fileName = getCursorString(cursor, DocumentsContract.Document.COLUMN_DISPLAY_NAME);String mimeType = getCursorString(cursor, DocumentsContract.Document.COLUMN_MIME_TYPE);String documentId = getCursorString(cursor, DocumentsContract.Document.COLUMN_DOCUMENT_ID);long lastModified = getCursorLong(cursor, DocumentsContract.Document.COLUMN_LAST_MODIFIED);long size = getCursorLong(cursor, DocumentsContract.Document.COLUMN_SIZE);
    public static long getCursorLong(Cursor cursor, String columnName) {final int index = cursor.getColumnIndex(columnName);if (index == -1) return -1;final String value = cursor.getString(index);if (value == null) return -1;try {return Long.parseLong(value);} catch (NumberFormatException e) {return -1;}}public static String getCursorString(Cursor cursor, String columnName) {final int index = cursor.getColumnIndex(columnName);return (index != -1) ? cursor.getString(index) : null;}

其中如果 mimeType文件类型 为DocumentsContract.Document.MIME_TYPE_DIR 则代表为文件夹,代表可以继续遍历浏览。接下来构建文件夹的 DocumentsUri 进行遍历:

Uri contentUri = DocumentsContract.buildChildDocumentsUri(MtpDeviceManager.AUTHORITY, documentId);

得到子目录文件夹的 DocumentsUri 可以继续按上面获取 Cursor 的步骤遍历文件夹。

可以访问手机存储的文件目录:

与上述遍历文件目录DocumentsContract.buildChildDocumentsUri()不同,要想创建文件、删除文件、修改文件需要通过DocumentsContract.buildDocumentUri()方式构建文件Uri才能进行文件操作。

    public static final String MTP_AUTHORITY = "com.android.mtp.documents";Uri mtpFileUri = DocumentsContract.buildDocumentUri(MTP_AUTHORITY, documentId);

注意:buildChildDocumentsUri用于构建文件夹的Uri来遍历文件夹目录,buildDocumentUri用于构建文件的Uri来进行文件操作。

mimeType 介绍

mimeType文件类型 为DocumentsContract.Document.MIME_TYPE_DIR代表文件夹,但文件种类很多,以"audio/"开头为音频类型、以"image/"开头为图片类型、以"video/"开头为视频类型,还有如下很多类型:

    public static String getFileType(String mimeType) {if (mimeType.startsWith("audio/")) {return TYPE_AUDIO;} else if (mimeType.startsWith("image/")) {return TYPE_IMAGE;} else if (mimeType.startsWith("video/")) {return TYPE_VIDEO;} else if (mFileTypeMap.containsKey(mimeType)) {return mFileTypeMap.get(mimeType);}return TYPE_UNKNOW;}static {// Compress file types 压缩文件类型mFileTypeMap.put("application/rar", TYPE_COMPRESS);mFileTypeMap.put("application/zip", TYPE_COMPRESS);mFileTypeMap.put("application/x-tar", TYPE_COMPRESS);mFileTypeMap.put("application/gzip", TYPE_COMPRESS);mFileTypeMap.put("application/x-7z-compressed", TYPE_COMPRESS);mFileTypeMap.put("application/x-rar-compressed", TYPE_COMPRESS);// Common file types 文本类型mFileTypeMap.put("text/plain", TYPE_DOCUMENT);mFileTypeMap.put("text/html", TYPE_DOCUMENT);mFileTypeMap.put("application/xhtml+xml", TYPE_DOCUMENT);mFileTypeMap.put("application/pdf", TYPE_DOCUMENT);//Microsoft typ, TYPE_DOCUMENT); office文档类型mFileTypeMap.put("application/msword", TYPE_DOCUMENT);mFileTypeMap.put("application/vnd.openxmlformats-officedocument.wordprocessingml.document", TYPE_DOCUMENT);mFileTypeMap.put("application/vnd.ms-powerpoint", TYPE_DOCUMENT);mFileTypeMap.put("application/vnd.openxmlformats-officedocument.presentationml.presentation", TYPE_DOCUMENT);mFileTypeMap.put("application/vnd.ms-excel", TYPE_DOCUMENT);mFileTypeMap.put("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", TYPE_DOCUMENT);// Google doc typ, TYPE_DOCUMENT);  Google文档类型mFileTypeMap.put("application/vnd.google-apps.document", TYPE_DOCUMENT);mFileTypeMap.put("application/vnd.google-apps.spreadsheet", TYPE_DOCUMENT);mFileTypeMap.put("application/vnd.google-apps.presentation", TYPE_DOCUMENT);mFileTypeMap.put("application/vnd.google-apps.drawing", TYPE_DOCUMENT);mFileTypeMap.put("application/vnd.google-apps.fusiontable", TYPE_DOCUMENT);mFileTypeMap.put("application/vnd.google-apps.form", TYPE_DOCUMENT);mFileTypeMap.put("application/vnd.google-apps.map", TYPE_DOCUMENT);mFileTypeMap.put("application/vnd.google-apps.sites", TYPE_DOCUMENT);// 文件夹类型mFileTypeMap.put("vnd.android.document/directory", TYPE_DIR);// Apk类型mFileTypeMap.put("application/vnd.android.package-archive", TYPE_APK);// Special media mime types 特殊媒体类型mFileTypeMap.put("application/ogg", TYPE_AUDIO);mFileTypeMap.put("application/x-flac", TYPE_AUDIO);}

可以以下面的方式请求打开不同 mimeType 的文件的打开方式:

            Intent intent = new Intent(Intent.ACTION_VIEW);intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);intent.setDataAndType(uri, mimeType);Intent chooserIntent = Intent.createChooser(intent, null);intent.putExtra(Intent.EXTRA_AUTO_LAUNCH_SINGLE_CHOICE, false);startActivity(chooserIntent);

其中uri以 DocumentsContract.buildDocumentUri(authority, documentId) 的形式构建。

Mtp文件的操作(创建、删除、拷贝)

创建文件

    private void createDirectory(String name) {ContentResolver resolver = mContext.getContentResolver();ContentProviderClient client = resolver.acquireUnstableContentProviderClient(MTP_AUTHORITY);if (client != null) {try {Uri childUri = DocumentsContract.createDocument(resolver, mContentUri, DocumentsContract.Document.MIME_TYPE_DIR, name);if (mDirectoryListener != null) {mDirectoryListener.onCreate(childUri);}} catch (Exception e) {e.printStackTrace();} finally {client.close();}}}

删除文件

    public static void deleteDocument(DocumentInfo doc, DocumentInfo parent) {try {Context context = AppUtils.getApplicationContext();if (parent != null && doc.isRemoveSupported()) {DocumentsContract.removeDocument(context.getContentResolver(), doc.getUri(), parent.getUri());} else if (doc.isDeleteSupported()) {DocumentsContract.deleteDocument(context.getContentResolver(), doc.getUri());}} catch (FileNotFoundException | RuntimeException e) {e.printStackTrace();}}

复制文件

在同一 Mtp 文件或者 File 文件 的 Provider 进行复制时,尝试使用下面的方式优化复制。目前 Mtp 文件目录移动拷贝到 File 文件目录或是 File 文件移动拷贝到Mtp 文件目录 需要进行逐字节复制移动,具体参考下一小节:移动本地 File 文件和 Mtp 文件之间的拷贝移动。

  if ((src.flags & Document.FLAG_SUPPORTS_COPY) != 0) {try {if (DocumentsContract.copyDocument(getClient(src), src.derivedUri,dstDirInfo.derivedUri) != null) {Metrics.logFileOperated(appContext, operationType, Metrics.OPMODE_PROVIDER);return;}} catch (RemoteException | RuntimeException e) {e.printStackTrace();}}// 如果不能做一个优化复制,则进行字节副本的复制。byteCopyDocument(src, dstDirInfo);

本地 File 文件和 Mtp 文件之间的拷贝移动

需要将本地的 File 文件路径 转换为能创建新文件的 DocumentUri ,获取的方式如下:

    private static Uri getDocumentUri(String path) {final ContentProviderClient storageClient = AppUtils.getApplicationContext().getContentResolver().acquireContentProviderClient(DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY);Bundle bundle = null;try {bundle = storageClient.call("getDocIdForFileCreateNewDir", path, null);} catch (RemoteException e) {e.printStackTrace();} finally {storageClient.close();}final String docId = bundle == null ? null : bundle.getString("DOC_ID");return DocumentsContract.buildDocumentUri(DocumentsContract.EXTERNAL_STORAGE_PROVIDER_AUTHORITY, docId);}

文件的复制粘贴需要通过 DocumentInfo 的方式进行,下面方式得到 目标目录的 DocumentInfo :

            Uri destPathUri = getDocumentUri(destPath);DocumentInfo destFileInfo = null;try {destFileInfo = DocumentInfo.fromUri(AppUtils.getApplicationContext().getContentResolver(), destPathUri);} catch (FileNotFoundException e) {e.printStackTrace();}

首先需要创建目标文件,根据其 Uri 获取创建的新文件的 DocumentInfo:

            Uri dstUri = null;try {dstUri = DocumentsContract.createDocument(mResolver, destFileInfo.getUri(),  destFileMimeType, destFileName);} catch (FileNotFoundException | RuntimeException e) {return false;}DocumentInfo dstInfo = null;try {dstInfo = DocumentInfo.fromUri(mResolver, dstUri);} catch (FileNotFoundException | RuntimeException e) {return false;}

拷贝文件还需要考虑文件 mimeType 类型是否为文件夹,如果为文件夹则还需要遍历子目录进行拷贝,如果为单个文件类型则进行单独的文件拷贝。
单独拷贝文件方式如下:

        private void copyFile(DocumentInfo src, DocumentInfo dest, String mimeType) throws ResourceException {AssetFileDescriptor srcFileAsAsset = null;ParcelFileDescriptor srcFile = null;ParcelFileDescriptor dstFile = null;InputStream in = null;ParcelFileDescriptor.AutoCloseOutputStream out = null;boolean success = false;try {if (src.isVirtual()) {try {srcFileAsAsset = mResolver.acquireContentProviderClient(src.getUri().getAuthority()).openTypedAssetFileDescriptor(src.getUri(), mimeType, null, mSignal);} catch (FileNotFoundException | RemoteException | RuntimeException e) {throw new ResourceException("Failed to open a file as asset for %s due to an "+ "exception.", src.getUri(), e);}if (srcFileAsAsset != null) {srcFile = srcFileAsAsset.getParcelFileDescriptor();}try {in = new AssetFileDescriptor.AutoCloseInputStream(srcFileAsAsset);} catch (IOException e) {throw new ResourceException("Failed to open a file input stream for %s due "+ "an exception.", src.getUri(), e);}} else {try {srcFile = mResolver.acquireContentProviderClient(src.getUri().getAuthority()).openFile(src.getUri(), "r", mSignal);} catch (FileNotFoundException | RemoteException | RuntimeException e) {throw new ResourceException("Failed to open a file for %s due to an exception.", src.getUri(), e);}in = new ParcelFileDescriptor.AutoCloseInputStream(srcFile);}try {dstFile = mResolver.acquireContentProviderClient(dest.getUri().getAuthority()).openFile(dest.getUri(), "w", mSignal);} catch (FileNotFoundException | RemoteException | RuntimeException e) {throw new ResourceException("Failed to open the destination file %s for writing "+ "due to an exception.", dest.getUri(), e);}out = new ParcelFileDescriptor.AutoCloseOutputStream(dstFile);try {// If we know the source size, and the destination supports disk// space allocation, then allocate the space we'll need. This// uses fallocate() under the hood to optimize on-disk layout// and prevent us from running out of space during large copies.final StorageManager sm = AppUtils.getApplicationContext().getSystemService(StorageManager.class);final long srcSize = srcFile.getStatSize();final FileDescriptor dstFd = dstFile.getFileDescriptor();if (srcSize > 0 && sm.isAllocationSupported(dstFd)) {sm.allocateBytes(dstFd, srcSize);}try {final Int64Ref last = new Int64Ref(0);FileUtils.copy(in, out, new FileUtils.ProgressListener() {@Overridepublic void onProgress(long progress) {if (isCancelled()) {mSignal.cancel();}final long delta = progress - last.value;last.value = progress;publishProgress(null, String.valueOf(delta));}}, mSignal);} catch (OperationCanceledException e) {return;}// Need to invoke Os#fsync to ensure the file is written to the storage device.try {Os.fsync(dstFile.getFileDescriptor());} catch (ErrnoException error) {// fsync will fail with fd of pipes and return EROFS or EINVAL.if (error.errno != OsConstants.EROFS && error.errno != OsConstants.EINVAL) {throw new SyncFailedException("Failed to sync bytes after copying a file.");}}// Need to invoke IoUtils.close explicitly to avoid from ignoring errors at flush.dstFile.close();srcFile.checkError();} catch (IOException e) {throw new ResourceException("Failed to copy bytes from %s to %s due to an IO exception.",src.getUri(), dest.getUri(), e);}success = true;} finally {if (!success) {if (dstFile != null) {try {dstFile.closeWithError("Error copying bytes.");} catch (IOException closeError) {closeError.printStackTrace();}}mSignal.cancel();deleteDocument(dest.getUri(), null);}try {if (in != null) {in.close();}if (out != null) {out.close();}} catch (IOException e) {e.printStackTrace();}}}

如果进行移动操作,则需要增加删除操作。具体粘贴的细节可参考 DocumentsUI 源码中 CopyJob.java 部分。

相关系统源码目录

Identifier端

frameworks/base/core/java/android/provider/DocumentsContract.java
framework/base/packages/MtpDucumentsProvider.java
framework/base/media/java/android/mtp/
frameworks/base/core/java/com/android/internal/content/FileSystemProvider.java

Responser端

packages/providers/MediaProvider/
/frameworks/base/packages/ExternalStorageProvider/ 外部存储的provider
frameworks/base/services/usb/java/com/android/server/usb/MtpNotificationManager.java 负责android的通知显示 frameworks/base/services/usb/java/com/android/server/usb/UsbProfileGroupSettingsManager.java
frameworks/base/packages/SystemUI/src/com/android/systemui/usb/UsbResolverActivity.java 显示USB弹框

总结

以上是 Initiator 端实现Mtp访问浏览手机存储的方式,一般PC、平板或者手机都可作为Initiator 端访问另一台Android设备。
Android设备作为 Responder 端的相关介绍参考博客:Android之 MTP框架和流程分析。
想要实现插入USB时响应系统启动文件管理器,参考下篇博客: Android实现Mtp访问浏览手机存储(二) 禁止DocumentsUI文件直接弹出。

Android实现Mtp访问浏览手机存储(一)访问Mtp目录相关推荐

  1. Android实现Mtp访问浏览手机存储(二) 禁止DocumentsUI文件直接弹出

    Android实现Mtp访问浏览手机存储(一)访问Mtp目录 Android实现Mtp访问浏览手机存储(二) 禁止DocumentsUI文件直接弹出 当usb接入时,默认打开系统的DocumentUI ...

  2. DCloud之Android平台应用启动时读写手机存储、访问设备信息(如IMEI)等权限策略控制

    目录 一.控制缘由 二.说明 三.云端打包配置 1.读写手机存储权限 (1)源码视图配置 2.访问设备信息权限 (1)源码视图配置 四.离线打包提示语配置及弹窗配置 1.提示语配置 2.弹窗配置 五. ...

  3. wgt文件怎么安装到手机_uni-app开发经验分享十二: Android平台应用启动时读写手机存储、访问设备信息(如IMEI)等权限策略及提示信息...

    Android平台从6.0(API23)开始系统对权限的管理更加严格,所有涉及敏感权限都需要用户授权允许才能获取. 因此一些应用基础业务逻辑需要的权限会在应用启动时申请,并引导用户允许. 读写手机存储 ...

  4. MTP模式与USB存储模式(MTP in Android)

    转载:http://bbs.meizu.cn/thread-4747416-1-1.html MTP in Android MTP的全称是Media Transfer Protocol(媒体传输协议) ...

  5. uniapp 安卓平台应用启动时读写手机存储、访问设备信息(如IMEI)等权限

    今天接到一个项目 uniapp写的app 客户要求 在app加载页不可以向用户申请读取权限 要使用到这个权限的时候再申请 打开manifest.json文件,切换到"源码视图"项 ...

  6. Android 11 高版本 出现外部存储无法访问的问题

    最近在做Android 应用开发,IDE是android studio , 使用的版本配置如下:compileSdk 32 buildToolsVersion '32.0.0' defaultConf ...

  7. android 手机存储 目录,android 62 手机存储目录的划分

    android下应用程序的路径和javase不同,应用程序的数据要保存自己的文件夹里面 > > getFileDir(); 获取自己的文件夹 /data/data/包名(应用程序的名字)/ ...

  8. Android 手机存储相关内容

    应用操作的文件存储位置分为三个部分: 1.应用内部存储私有文件目录 2.应用外部存储私有文件目录 3.公有目录 Android手机存储分为两个部分:内部存储和外部存储,内部存储一般是手机自带的存储空间 ...

  9. linux电脑访问android手机存储

    系统软件确认: 手机:android 系统 电脑:linux 系统 (测试手机 android7.1.2,电脑 debian9.2,其他没测试过) 手机端设置: 手机usb线连接电脑,usb 使用方式 ...

最新文章

  1. 说说如何搭建 Nginx 反向代理 Tomcat
  2. 高可用集群技术之RHCS应用详解(一)
  3. python多元线性回归模型案例_Python 实战多元线性回归模型,附带原理+代码
  4. fseek/ftell/rewind/fgetpos/fsetpos函数使用-linux
  5. 机器人 蓝buff 钩_lol:机器人史诗级加强,从河道钩蓝buff,对面打野要骂人
  6. Oracle案例:index range scan真的不会多块读吗?
  7. XenServer 6.5实战系列之十三:图形界面安装Linux Redhat系统
  8. 给开发者的9个安全建议:既能保护供应链安全,也不会拖慢开发进程
  9. (一)Quartz2.2.1 简单例子
  10. 130242014049-魏俊斌-《电商系统分类模块》
  11. 通过ajax获取经纬度,通过百度地图获取经纬度
  12. 3D STL文件解析
  13. Mac电脑没声音的解决方法
  14. emqx配置ssl/tsl实现双向认证
  15. Nginx反向代理有什么用?
  16. 紫罗兰永恒花园rust简谱_Sincerely钢琴谱_TRUE_紫罗兰永恒花园OP
  17. MP3歌词的同步与拖拽设计
  18. SSD1306-7针脚OLED的使用心得
  19. 世界各国电源插头插座形式
  20. 【前端面试必读】实现图片16:9

热门文章

  1. 自学Mplus的记录与回顾
  2. php 按照相同键值分组合并数组
  3. HADOOP集群优化——CPU、内存、磁盘IO、YARN监控
  4. 国培 计算机远程培训心得,国培网络研修心得体会(通用4篇)
  5. 平安智慧企业_赋能业务 快乐平安APP荣获“2019中国智能办公年度优秀产品奖”...
  6. CCF-CSP 第23次认证考试 202109-3脉冲神经网络(模拟)(100分)
  7. 创业公司打造顶级团队的七个方法
  8. 基于微信电子书阅读小程序毕业设计毕设作品(4)开题报告
  9. MATLAB 对含有nan值矩阵的处理
  10. 5G网络优化工程师简历怎么才能脱颖而出?