Android-关于读写U盘文件(USB)-操作外置sd卡

一、背景

很久以前做过这个功能,一直没有总结。碰巧最近有网友问到,就总结一下。
项目要求实现两个功能:
1、读取U盘里的apk文件并安装
2、导出数据,生成Excel表格到U盘

二、可行性分析

关于问题1

第一个问题很简单,读取U盘里的内容并安装是不需要系统权限的。动态获取到读权限就可以了

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

关于问题2

我们需要获取到

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

但是即使我们获取了写入权限,写入的时候,依旧会失败,会提示permission denied。
原因是:
Android5.0以上系统已经不支持开发者随便写手机的外部存储(包含tf卡、otg外接u盘等),必须用特定的方法去读写。即5.0以下我们还是可以直接读写的(直接拿到U盘文件路径也可以操作)。

关于问题2,我们可以采取四种方式操作:

1、使用google提供的特定方式,SAF框架操作外置sd卡,关键字DocumentFile.
2、只操作固定的目录:Android/data/包名/…,这里面你可以随便做操作,不过app删了,这些数据也会自动删掉。
3、使用第三方的框架libaums
4、系统签名,成为系统应用

//目前最新版本为0.7.0,但是该版本的兼容库已迁移至androidx
implementation 'com.github.mjdev:libaums:0.6.0'

我最后采取的是第四种方式。
但是这个文章主要是为第三种方式提供demo。
注意:当时调研的时候发现libaums这个框架对于nfts格式的U盘读取是有问题的,貌似只支持FAT32。
我没有去研究libaums的源码,并不知道它是如何在没有权限的情况下写入成功的。观测情况是,一旦授予权限,文件管理器是看不到这个U盘的。所以我估计,它可能是把U盘挂载到了其他地方。
以上都是废话,下面是经测可用的demo。

实现效果

把挂载到Android设备的U盘,遍历文件并输出。进行简单的读写操作。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

依赖build.gradle

compile 'com.github.mjdev:libaums:0.5.5'

布局文件activity_u.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
    android:layout_height="match_parent">
    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <TextView
            android:textStyle="bold"
            android:textColor="#000000"
            android:textSize="16sp"
            android:id="@+id/log_tv"
            android:text="content \ndisplay"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" />
    </ScrollView>
</LinearLayout>

权限AndroidManifest.xml

 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <!--手机必须支持USB主机特性(OTG)-->
    <uses-feature android:name="android.hardware.usb.host" />

一个类UsbActivity

package com.example.pc.testeverything.Activity;

import android.Manifest;
import android.app.PendingIntent;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.hardware.usb.UsbDevice;
import android.hardware.usb.UsbManager;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.widget.TextView;

import com.example.pc.testeverything.R;
import com.github.mjdev.libaums.UsbMassStorageDevice;
import com.github.mjdev.libaums.fs.FileSystem;
import com.github.mjdev.libaums.fs.UsbFile;
import com.github.mjdev.libaums.fs.UsbFileStreamFactory;
import com.github.mjdev.libaums.partition.Partition;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Locale;

import static android.hardware.usb.UsbManager.ACTION_USB_DEVICE_ATTACHED;
import static android.hardware.usb.UsbManager.ACTION_USB_DEVICE_DETACHED;


/**
 * 写了一个简单测试
 *
 * @author Rachel
 */
public class UsbActivity extends AppCompatActivity {
    private static final String TAG = "Rachel_test";
    private static final String ACTION_USB_PERMISSION = "com.demo.otgusb.USB_PERMISSION";
    private UsbMassStorageDevice[] storageDevices;
    private TextView logTv;
    /**
     * 监听USB设备的广播
     */
    BroadcastReceiver mUsbReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            String intentAction = intent.getAction();
            UsbDevice device = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE);
            switch (intentAction) {
                case ACTION_USB_PERMISSION:
                    //自定义Action
                    if (intent.getBooleanExtra(UsbManager.EXTRA_PERMISSION_GRANTED, false)) {
                        logShow("用户同意USB设备访问权限");
                        readDevice(getUsbMass(device));
                    } else {
                        logShow("用户拒绝USB设备访问权限");
                    }
                    break;
                case ACTION_USB_DEVICE_ATTACHED:
                    logShow("U盘设备插入");
                    if (device != null) {
                        redUDiskDeviceList();
                    }
                    break;
                case ACTION_USB_DEVICE_DETACHED:
                    logShow("U盘设备移除");
                    break;
                default:
                    Log.i(TAG, "----------------------------------");
            }
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_u);
        logTv = (TextView) findViewById(R.id.log_tv);
        init();
    }

    /**
     * 动态申请读写权限
     */
    private void init() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            requestPermissions(new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,
                    Manifest.permission.READ_EXTERNAL_STORAGE}, 111);
        }
        registerUsbReceiver();
        redUDiskDeviceList();
    }

    /**
     * 权限申请回调
     *
     * @param requestCode 标识码
     */
    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (requestCode == 111) {
            registerUsbReceiver();
            redUDiskDeviceList();
        }
    }

    /**
     * 动态注册监听USB设备的广播。
     * 由于耗电等原因,8.0不能对大部分的广播进行静态注册
     */
    private void registerUsbReceiver() {
        IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
        filter.addAction("android.hardware.usb.action.USB_STATE");
        filter.addAction("android.hardware.usb.action.USB_DEVICE_ATTACHED");
        filter.addAction("android.hardware.usb.action.USB_DEVICE_DETACHED");
        registerReceiver(mUsbReceiver, filter);
    }

    /**
     * 获取存储设备
     *
     * @param usbDevice 与android设备连接在一起的USB设备
     * @return Usb设备的一个包装类
     */
    private UsbMassStorageDevice getUsbMass(UsbDevice usbDevice) {
        for (UsbMassStorageDevice device : storageDevices) {
            if (usbDevice.equals(device.getUsbDevice())) {
                return device;
            }
        }
        return null;
    }

    /**
     * 初始化USB设备
     *
     * @param device USB设备
     */
    private void readDevice(UsbMassStorageDevice device) {
        try {
            device.init();//初始化
            //设备分区
            Partition partition = device.getPartitions().get(0);
            //文件系统
            FileSystem currentFs = partition.getFileSystem();
            //可以获取到设备的标识
            currentFs.getVolumeLabel();
            //通过FileSystem可以获取当前U盘的一些存储信息,包括剩余空间大小,容量等等
            Log.e("Volume Label: ", currentFs.getVolumeLabel());
            Log.e("Capacity: ", fSize(currentFs.getCapacity()));
            Log.e("Occupied Space: ", fSize(currentFs.getOccupiedSpace()));
            Log.e("Free Space: ", fSize(currentFs.getFreeSpace()));
            Log.e("Chunk size: ", fSize(currentFs.getChunkSize()));
            readAndWriteTest(currentFs);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * USB设备读取
     */
    private void redUDiskDeviceList() {
        Context context = this;
        //设备管理器
        UsbManager usbManager = (UsbManager) context.getSystemService(Context.USB_SERVICE);
        //获取U盘存储设备
        storageDevices = UsbMassStorageDevice.getMassStorageDevices(context);
        PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, new Intent(ACTION_USB_PERMISSION), 0);
        //一般手机只有1个OTG插口
        if (storageDevices.length <= 0) {
            return;
        }
        UsbMassStorageDevice device = storageDevices[0];
        //读取设备是否有权限
        if (usbManager.hasPermission(device.getUsbDevice())) {
            readDevice(device);
        } else {
            //没有权限,进行申请
            usbManager.requestPermission(device.getUsbDevice(), pendingIntent);
        }
    }

    private void logShow(final String s) {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                logTv.setText(logTv.getText() + "\n" + s);
            }
        });
    }


    /**
     * 读写文件尝试
     *
     * @param currentFs
     */
    private void readAndWriteTest(FileSystem currentFs) {
        try {
            //设置当前文件对象为根目录
            UsbFile usbFile = currentFs.getRootDirectory();
            UsbFile[] files = usbFile.listFiles();
            for (UsbFile file : files) {
                logShow("文件: " + file.getName());
            }
            // 新建文件
            UsbFile newFile = usbFile.createFile("hello_" + System.currentTimeMillis() + ".txt");
            logShow("新建文件: " + newFile.getName());

            // 写文件
            OutputStream os = UsbFileStreamFactory.createBufferedOutputStream(newFile, currentFs);
            os.write(("hi_" + System.currentTimeMillis()).getBytes());
            os.close();
            logShow("写文件: " + newFile.getName());
            // 读文件
            // InputStream is = new UsbFileInputStream(newFile);
            InputStream is = UsbFileStreamFactory.createBufferedInputStream(newFile, currentFs);
            byte[] buffer = new byte[currentFs.getChunkSize()];
            int len;
            File sdFile = new File("/sdcard/111");
            sdFile.mkdirs();
            FileOutputStream sdOut = new FileOutputStream(sdFile.getAbsolutePath() + "/" + newFile.getName());
            while ((len = is.read(buffer)) != -1) {
                sdOut.write(buffer, 0, len);
            }
            is.close();
            sdOut.close();
            logShow("读文件: " + newFile.getName() + " ->复制到/sdcard/111/");
        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    public static String fSize(long sizeInByte) {
        if (sizeInByte < 1024) {
            return String.format("%s", sizeInByte);
        } else if (sizeInByte < 1024 * 1024) {
            return String.format(Locale.CANADA, "%.2fKB", sizeInByte / 1024.);
        } else if (sizeInByte < 1024 * 1024 * 1024) {
            return String.format(Locale.CANADA, "%.2fMB", sizeInByte / 1024. / 1024);
        } else {
            return String.format(Locale.CANADA, "%.2fGB", sizeInByte / 1024. / 1024 / 1024);
        }
    }
}

三、参考文章

libaums
android OTG (USB读写,U盘读写)最全使用相关总结
Android-USB-OTG-读写U盘文件

SAF
android 使用SAF框架操作外置sd卡
android官网

四、DEMO

TestEverything
注:一个测试demo,里面都是乱七八糟的东西,不建议下载。第15个,是本文的测试。
在这里插入图片描述

Java基础不好的小水怪,正在学习。有错请指出,一起加油。

  • 2
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值