Android 分区存储

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

1.Android存储

Android存储分为内部存储和外部存储外部存储并不是指SD存储卡或外部硬盘。

①内部存储

用于Android系统本身和应用程序的存储区域比如手机的/system/、/data/等目录。 如果没有这一块存储区域是无法运行Android系统和应用程序的。

其中data/data/包名/XXX是Android系统提供给app存储数据的内部存储空间由app创建的SharedPreferences、Sqlite数据库、缓存文件等都保存在该文件夹中。该目录只能由该app自身访问其他应用程序和用户无法访问存储在此空间中的文件并且该目录会随着应用的卸载而被移除。

获取手机内部存储绝对路径/data的方法

Environment.getDataDirectory().getAbsolutePath();

注意如果手机获取到了root权限就可以访问内部存储空间中的文件数据。

②外部存储

手机的内部存储空间通常不会很大一旦手机的内部存储容量用完可能会出现手机无法使用的情形。因此需要将一些比较大的媒体文件放到机身外部存储中。以前的手机中具备一个SD存储卡槽插入SD存储卡系统会将SD储存卡的存储空间挂载成外部存储空间。但是现在的手机已经不支持SD存储卡扩展功能了取而代之的是手机自带了一个机身内置存储空间这个机身存储和SD存储卡的功能是完全一样的Android系统会将内置存储空间的一部分区域划分为内部存储另一部分划分为外部存储然后通过Linux文件挂载的方式将这些存储空间进行挂载。

获取手机外部存储的路径/storage/emulated/0的方法
Environment.getExternalStorageDirectory().getAbsolutePath();

③现在很多手机自带的内置存储空间很大 因此Android4.4系统及以上的手机将机身内置存储分为了内部存储internal和外部存储external两个不同的储存区域用来存储不同的数据。

如果Android4.4系统及以上的手机还支持单独外接SD卡那么外接的SD卡一定是外部存储此时手机上是有两个外部存储空间的。为了区分这两个外部存储google提供了getExternalFilesDirs方法这个API可以获取多个外部存储空间它返回一个File数组File数组中就包含了手机自身所带的外部存储和外接SD卡所定义的外部存储了。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {

    File[] files = getExternalFilesDirs( Environment.MEDIA_MOUNTED);

    for(File file:files){

        Log.e("Environment", file.getAbsolutePath())

    }

}

注意Android手机支持外接多个外部存储介质空间如果用户在设备上移除了介质则外部存储可能变为不可用状态。 所以在使用外部存储执行任何工作之前 最好调用getExternalStorageState()来检查介质是否可用。

//判断外部存储是否可用
if(Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {
    ...
}

b17fe52e74ea47bdb2fcdcd7f7ef1c07.png

可见内部存储和外部存储可以是在同一块存储介质上面的只是概念上做了区分即内部存储和外部存储是一块存储介质上的不同区域。

 

2.Android应用存储目录

①内部存储空间中的应用私有目录

对于每一个安装的App系统都会在内部存储空间的data/data目录下以应用包名为名字自动创建与之对应的文件夹。这个文件夹包含SharedPreferences和SQLiteDatabase持久化应用相关数据等。该文件夹是应用的私有文件夹其他应用和用户不能访问文件夹里面的内容的(Root用户除外)。每个应用访问自己的内部存储是不需要权限的。当用户卸载该应用时这些文件也会被移除。

1获取当前app包名文件夹下的files文件夹路径

context.getFilesDir().getAbsolutePath();

返回结果:data/data/packagename/files

2获取当前app包名文件夹下cache文件夹路径

context.getCacheDir().getAbsolutePath();

返回结果:data/data/packagename/cache

3在内部存储空间内创建或打开现有的目录

context.getDir("setting", MODE_PRIVATE).getAbsolutePath();

返回结果:data/data/packagename/setting

4获取内部存储files路径下的所有文件名

context.fileList();

5删除内部存储files路径下的文件

//返回true则表示删除成功

context.deleteFile(filename);

6要在files或cache目录中创建新文件可以使用File()构造函数传递给File上述方法提供的指定内部存储目录的方法

//保存内容到私有的files目录

public void save(String filename, String content) throws IOException{

    File file= new File(context.getFilesDir(), filename);

    FileOutputStream myfos= new FileOutputStream(file);

    myfos.write(content.getBytes());

    myfos.close();

}

或者读取文件

//通过文件名来获取私有的files目录中的文件

public String get(String filename) throws IOException {

    File file= new File(context.getFilesDir(), filename);

    FileInputStream fis = new FileInputStream(file);

    ByteArrayOutputStream baos = new ByteArrayOutputStream();

    byte[] data = new byte[1024];

    int len = -1;

    while ((len = fis.read(data)) != -1) {

        baos.write(data, 0, len);

    }

    return new String(baos.toByteArray());

}

Android系统可以调用Context类中提供的openFileOutput()方法获取FileOutputStream来对文件进行操作。openFileOutput()方法接受两个参数第一个参数是文件名在文本创建的时候使用的就是这个名称注意这里指定的文件名不可以包含路径因为默认存储到/data/data//files/目录下。第二个参数是文件的操作模式主要有两种MODE_PRIVATE默认的操作模式表示当指定同样文件名的时候当该文件名有内容时再次调用会覆盖原内容MODE_APPEND表示该文件如果已存在就往文件里面追加内容。

调用openFileOutput()获取FileOutputStream对象后就可以使用Java流的方式将数据写入到文件中了。

写入文件

//保存内容到私有的files目录

public void save(String filename, String content) throws IOException{

    FileoutputStream myfos=context.openFileoutput(filename,Context.MODE_PRIVATE);

    myfos.write(content.getBytes());

    myfos.close();

}

读取文件:

//通过文件名来获取私有的files目录中的文件

public String get(String filename) throws IOException {

    FileInputStream fis = context.openFileInput( filename);

    ByteArrayOutputStream baos = new ByteArrayOutputStream();

    byte[] data = new byte[1024];

    int len = -1;

    while ((len = fis.read(data)) != -1) {

        baos.write(data, 0, len);

    }

    return new String(baos.toByteArray());

}

如果要将文件保存在data/data/packagename/cache目录中同样可以使用File进行保存需要注意的是此目录适用于临时文件应定期清理。如果磁盘空间不足系统可能会删除cache中的文件因此要确保在读取之前检查缓存文件是否存在。

②外部存储空间中的应用私有目录

外部存储可以是外置SD卡也可以是内置存储卡的部分分区。 外部存储可分为公共目录和私有目录操作外部存储的私有目录不需要READ_EXTERNAL_STORAGE或WRITE_EXTERNAL_STORAGE权限。

Android SDK中提供了API供开发人员直接操作外部存储空间下的应用私有目录

1获取某个应用在外部存储中的files路径

context.getExternalFilesDir(type).getAbsolutePath();

返回结果 /storage/emulated/0/Android/data/packagename/files

2获取某个应用在外部存储中的cache路径

context.getExternalCacheDir().getAbsolutePath()

返回结果 /storage/emulated/0/Android/data/packagename/cache

当用户卸载应用时此目录及其内容将被删除。此外系统媒体扫描程序不会读取这些目录中的文件因此不能从MediaStore内容提供程序访问这些文件。因此如果你的媒体文件不需要其他应用使用则应该存储在外部存储上的私有存储目录。

可以通过调用getExternalFilesDir()并向其传递一个名称来获取一个外部存储的私有目录。

public void save(String filename, String content) throws IOException{

    boolean canUse = Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED);

    if(canUse){

      File file= new File( context.getExternalFilesDir(Environment.DIRECTORY_PICTURES), "c.txt");

      FileOutputStream myfos= new FileOutputStream(file);

      myfos.write(content.getBytes());

      myfos.close();

    }

}

如果没有提前定义好子文件目录名则可以调用 getExternalFilesDir()并传递null这将返回外部存储上应用程序私有目录的根目录。 type根据文件类型可传系统预定义的子目录常量 如图片Environment.DIRECTORY_PICTURES此时返回/storage/emulated/0/Android/data/包名/files/Pictures或者传null直接返回/storage/emulated/0/Android/data/包名/files。

注意外部存储空间中的应用私有目录在用户卸载应用程序时会删除该目录。如果您保存的文件需要在用户卸载应用程序后仍然可用例如当您的应用程序下载照片并且用户应保留这些照片时应该将文件保存到外部存储的公共目录中。

③外部存储空间中的公共目录

通常应用涉及到的持久化数据分为两类应用相关数据和应用无关数据。应用相关数据指专供宿主App使用的数据信息比如一些应用的配置信息、数据库信息、缓存文件等。当应用被卸载这些信息也应该被随之删除避免存储空间产生不必要的占用。而应用无关的公共文件可被其他程序自由访问当应用被卸载之后文件仍然保留。比如相机类应用被卸载后照片仍然存在。在Android外部存储中的公共目录有9大类均为系统创建的文件夹如果操作外部存储的公共目录需要申请READ_EXTERNAL_STORAGE或WRITE_EXTERNAL_STORAGE等权限。开发人员可以通过Environment类提供的方法直接获取相应目录的绝对路径传递不同的type参数类型即可比如Environment.getExternalStorageDirectory().getAbsolutePath()方法是获取外部存储的根路径Environment.getExternalStoragePublicDirectory(String type); 方法是获取外部存储的共享目录。

Environment类提供了很多type参数的常量比如

Environment.DIRECTORY_DCIM 数字相机拍摄的照片

Environment.DIRECTORY_MUSIC用户音乐

Environment.DIRECTORY_PODCASTS音频 / 视频的剪辑片段

Environment.DIRECTORY_RINGTONES铃声

Environment.DIRECTORY_ALARMS闹钟的声音

Environment.DIRECTORY_PICTURES所有的图片 (不包括那些用照相机拍摄的照片)

Environment.DIRECTORY_MOVIES所有的电影 (不包括那些用摄像机拍摄的视频) 和 Download / 其他下载的内容。

注意在Android10版本也就是api29以后Android官方开始推荐分区存储分区存储主要是对外部存储空间中的公共目录的文件操作做了相关的限制

 

3.分区存储Scoped Storage

由于外部存储空间分为私有目录和公共目录在Android10以前应用程序可以通过申请READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限获得外部存储空间的权限以后直接通过file path读取和修改外部存储空间中任意的文件当然也包括其他应用的外部私有目录文件这样极易造成泄露用户隐私。而且很多应用会在外部存储根目录新建一个自己应用的文件夹用于存放应用的数据。这样会导致用户卸载后应用数据不会随之删除导致手机文件特别混乱长期占用空间。

为了解决这个问题Google从Android 10开始增加了一个新特性Scoped Storage即分区存储或者称为沙盒。增加分区存储意在限制程序对外部存储中公有目录的使用而对内部存储私有目录和外部存储私有目录都没有影响。简单来说就是在Android10中对于私有目录的读写没有变化仍然可以使用File那一套且不需要任何权限对于公有目录的读写必须使用MediaStore提供的API或是SAF存储访问框架。

分区存储的修改主要有两点访问外部储存的文件方式变更、访问外部储存的文件权限变更。

1访问外部储存的文件方式变更

在Android 10.0之前访问外部存储访问目录/文件可通过两个方法一是通过File路径访问File路径可以直接构造文件路径也可以通过MediaStore获取文件路径二是通过Uri访问Uri可以通过MediaStore或者SAF获取。

Android 10.0之后访问外部存储的公共目录中的媒体文件即/storage/emulated/0目录下的文件例如DCIM、Pictures、Alarms、Music、Notification、Podcasts、Ringtones、Movies、Download等必须通过MediaStore或者Storage Access FrameworkSAF获取到Uri然后通过Uri进行访问其中媒体文件如图片、音频、视频等能通过MediaStore和SAF两种方式访问非媒体文件只能通过SAF访问。App无法通过路径File(filePath)直接访问、新建、删除、修改目录/文件等。

2访问外部储存的文件权限变更

限制了应用程序对外部存储空间的操作权限。即使申请了READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限后也不能读写整个外部存储空间中的目录了。应用程序只能访问外部存储中的公共媒体目录即android系统创建的几个大类的文件夹如DCIM、Pictures、Alarms、Music、Notifications、Podcasts、Ringtones、Movies、Download等而对于外部存储的非公共目录例如外部存储的根目录其他应用的外部存储中的私有目录等是无法进行访问和操作的。

在Android10之前的系统中无论是通过MediaStore API或者File Path方式向外部存储中读写数据都需要申请READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限。

Android 10.0之后分不同的情况

1应用通过MediaStore API在指定的共享媒体目录下创建媒体文件或对该应用所创建的媒体文件集进行查询、修改、删除的操作时不需要申请READ_EXTERNAL_STORAGE和WRITE_EXTERNAL_STORAGE权限了。这是因为在创建的时候系统会将我们应用的packageName 写入到系统文件数据库中的owner_package_name字段从而在后续的使用中判断这个文件是哪个应用创建的。如果该应用卸载后重装之前保存的文件将不属于该应用创建的文件。

2通过MediaStore API对其他应用在共享媒体目录下贡献的媒体文件(图片、音频、视频)进行查询时需要申请READ_EXTERNAL_STORAGE权限如果未申请该权限通过ContentResolver查询不到其他应用贡献的文件Uri仅能查询到自己应用贡献的文件。使用MediaStore API时就算申请特READ_EXTERNAL_STORAGE权限 也仅允许读取其他应用共享的音频、视频和图片等媒体文件并不允许访问其他应用创建的下载数据和非媒体文件(pdf、office、doc、txt等)。 在Android 10里唯一一种访问其他应用贡献的非媒体文件的途径是使用存储访问框架 (Storage Access Framework) 提供的文档选择器向用户申请操作指定的文件。

3如果需要修改或删除其他应用贡献的媒体文件则需要申请WRITE_EXTERNAL_STORAGE权限。如果应用没有WRITE_EXTERNAL_STORAGE权限时去修改其它app的文件时就会throw java.lang.SecurityException: xxxx has no access to content://media/external/images/media/52 的异常。当应用申请了WRITE_EXTERNAL_STORAGE权限后去修改其它app贡献的媒体文件时会throw另一个Exception android.app.RecoverableSecurityException: xxxxxx has no access to content://media/external/images/media/52。此时将这个RecoverableSecurityException给Catch 住并向用户申请修改该文件的权限用户操作同意后就可以在onActivityResult回调中拿到Uri结果进行操作了。

try {

    editFile(editFileUri)

}catch (rse : RecoverableSecurityException){

    requestForOtherAppFiles( REQUEST_CODE_FOR_EDIT_IMAGE,rse)

}

private fun requestForOtherAppFiles( requestCode: Int, rse: RecoverableSecurityException){

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

        startIntentSenderForResult( rse.userAction.actionIntent.intentSender, requestCode, null, 0, 0, 0, null)

    }

}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {

    super.onActivityResult(requestCode, resultCode, data)

    if (resultCode == Activity.RESULT_OK){

        when(requestCode){

            REQUEST_CODE_FOR_EDIT_IMAGE ->{

                editImage(editImageUri)

            }

            REQUEST_CODE_FOR_DELETE_IMAGE ->{

                contentResolver.delete( deleteImageUri,null,null)

            }

        }

    }

}

注意

①应用私有目录包含内部存储空间中的应用私有目录和外部存储空间中的应用私有目录这两块区域没有做相应的变更还是和以前一样。这两个私有目录并不需要添加任何权限就可以访问并且可以通过路径File(filePath)直接访问、新建、删除、修改目录/文件等。 并且当对应的应用程序卸载以后这两个私有目录也会被移除。

②作为一个过度阶段在Android 10仍然可以通过以下两种手段避开分区存储targetSdkVersion设成29以下或在manifest中设置 android:requestLegacyExternalStorage=“true”。但是到了Android 11requestLegacyExternalStorage就失效了。此时增加了preserveLegacyExternalStorage属性对于覆盖安装的应用还能继续用而新应用不能用。

 

4.分区存储后门MANAGE_EXTERNAL_STORAGE

分区存储也是留有后门的在Android 11里新增了一个权限MANAGE_EXTERNAL_STORAGE该权限将授权读写所有共享存储内容这也将同时包含非媒体类型的文件。但是获得这个权限的应用还是无法访问其他应用的应用专属目录 (app-specific directory)无论是外部存储还是内部存储。

app可以申请MANAGE_EXTERNAL_STORAGE权限。这是针对那些文件管理App的比如es explore它们必须有这样的权限要不然文件列表都无法列出来了尤其是非媒体类型。但是这个权限在上架google play时需要申请的。

在Android 11中通过Intent申请MANAGE_EXTERNAL_STORAGE权限可以将用户引导至系统设置页面让用户选择是否允许该应用 “访问所有文件” (All Files Access)。

manifest中声明该权限

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

申请权限代码

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {

     if(!Environment.isExternalStorageManager()) { //判断是否拥有MANAGE_EXTERNAL_STORAGE权限

        Intent intent = new Intent();

        intent.setAction( Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION);

        ActivityCompat.startActivity( v.getContext(), intent, null);

        return;

    }

}

 

5.MediaStore

MediaStore访问应用自身存放到公共目录下的文件不需要申请权限但是如果应用卸载后重装之前保存的文件将不属于本应用创建的文件而如果要访问其他应用保存到公共目录下的文件则需要申请权限。

MediaStore通过Uri操作文件。各个目录的Uri如下

①Image类型对应的Uri为content://media/external/images/media MediaStore.Images.Media.EXTERNAL_CONTENT_URI默认路径为Pictures。

②Video类型对应的Uri为content://media/external/video/media MediaStore.Video.Media.EXTERNAL_CONTENT_URI默认路径为Movies。

③Audio类型对应的Uri为content://media/external/audio/media MediaStore.Audio.Media.EXTERNAL_CONTENT_URI默认路径为Music。

④Download类型对应的Uri为 content://media/external/downloads MediaStore.Downloads.EXTERNAL_CONTENT_URI默认路径为Download。

⑤File类型对应的Uri为content://media/external/ MediaStore.Files.getContentUri(“external”)默认路径为Documents。

MediaStore使用举例

①写文件

//从Assets读取Bitmap

Bitmap bitmap = null;

try {

    bitmap = BitmapFactory.decodeStream( getAssets().open("test.jpg"));

} catch (IOException e) {

    e.printStackTrace();

}

if (bitmap == null) return;

// 获取保存文件的 Uri

ContentResolver contentResolver = getContentResolver();

ContentValues values = new ContentValues();

Uri insertUri = contentResolver.insert( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);

// 保存图片到 Pictures 目录下

if (insertUri != null) {

    OutputStream os = null;

    try {

        os = contentResolver.openOutputStream( insertUri);

        bitmap.compress( Bitmap.CompressFormat.PNG, 100, os);

    } catch (FileNotFoundException e) {

        e.printStackTrace();

    } finally {

        try {

            if (os != null) {

                os.close();

            }

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

该例子直接把图片保存到Pictures根目录如果要在 Pictures下创建子目录需要用到 RELATIVE_PATHAndroid 版本 >= 10。

比如想把图片保存到在Pictures/test目录下只需要把子目录添加进ContentValues

ContentValues values = new ContentValues();

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

    // 指定子目录否则保存到对应媒体类型文件夹根目录

    values.put( MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES +"/test");

}

还可以向ContentValues中添加其他信息如文件名、MIME等。继续修改上面的例子

ContentValues values = new ContentValues();

// 获取保存文件的 Uri

values.put(MediaStore.Images.Media.MIME_TYPE, "image/png");

// 指定保存的文件名如果不设置则系统会取当前的时间戳作为文件名

values.put(MediaStore.Images.Media.DISPLAY_NAME, "test_" + System.currentTimeMillis() + ".png");

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

    // 指定子目录否则保存到对应媒体类型文件夹根目录  values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES + "/test");

}

②删除自己应用创建的文件

获取到对应的Uri之后 contentResolver.delete(uri,null,null) 即可。

③查询自己应用创建的文件

// 查询

ContentResolver contentResolver = getContentResolver();

Cursor cursor = contentResolver.query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, new String[]{MediaStore.Images.Media._ID, MediaStore.Images.Media.WIDTH, MediaStore.Images.Media.HEIGHT}, MediaStore.Images.Media._ID + " > ? ", new String[]{"100"}, MediaStore.Images.Media._ID + " DESC");

// 得到所有的 Uri

List<Uri> filesUris = new ArrayList<>();

while (cursor.moveToNext()) {

    int index = cursor.getColumnIndex( MediaStore.Images.Media._ID);

    Uri uri = ContentUris.withAppendedId( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, cursor.getLong(index));

    filesUris.add(uri);

}

cursor.close();

// 通过Uri获取具体内容并显示到界面上

ParcelFileDescriptor pfd = null;

try {

    pfd = contentResolver.openFileDescriptor( filesUris.get(0), "r");

    if (pfd != null) {

        Bitmap bitmap = BitmapFactory.decodeF ileDescriptor(pfd.getFileDescriptor());

        ((ImageView) findViewById(R.id.image)).se tImageBitmap(bitmap);

    }

} catch (FileNotFoundException e) {

    e.printStackTrace();

} finally {

    if (pfd != null) {

        try {

            pfd.close();

        } catch (IOException e) {

            e.printStackTrace();

        }

    }

}

④查询其他应用创建的文件

访问自己应用创建的文件不需要 READ_EXTERNAL_STORAGE权限。以上代码获取到的filesUris只包含本应用之前创建的文件。

如果需要连其他应用的文件一起获取则申请下 READ_EXTERNAL_STORAGE 权限即可。

⑤修改其他应用创建的文件

同理需要申请WRITE_EXTERNAL_STORAGE权限。但是即便申请了WRITE_EXTERNAL_STORAGE权限之后还是会报如下异常

android.app.RecoverableSecurityException: xxx has no access to content://media/external/images/media/100

这是因为还需要向用户申请修改的权限。

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {

    try {

        delete();

    } catch (RecoverableSecurityException e) {

        e.printStackTrace();

        // 弹出对话框向用户申请修改其他应用文件的权限

        requestConfirmDialog(e);

    }

}

private void delete() {

    Uri uri = Uri.parse( "content://media/external/images/media/100");

    getContentResolver().delete(uri, null, null);

}

@RequiresApi(api = Build.VERSION_CODES.Q)

private void requestConfirmDialog( RecoverableSecurityException e) {

    try {

        startIntentSenderForResult( e.getUserAction().getActionIntent().getIntentSender(), 0, null, 0, 0, 0, null);

    } catch (IntentSender.SendIntentException ex){

        ex.printStackTrace();

    }

}

@Override

protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {

    super.onActivityResult(requestCode, resultCode, data);

    if (resultCode == RESULT_OK){

        delete();

    }

}

⑥将文件下载到Download目录

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

private void downloadApkAndInstall(String downloadUrl, String apkName) {

    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {

        // 使用原始方式

    } else {

        new Thread(() -> {

            BufferedInputStream bis = null;

            BufferedOutputStream bos = null;

            try {

                URL url = new URL(downloadUrl);

                URLConnection urlConnection = url.openConnection();

                InputStream is = urlConnection.getInputStream();

                bis = new BufferedInputStream(is);

                ContentValues values = new ContentValues();

                values.put( MediaStore.MediaColumns.DISPLAY_NAME, apkName);

                values.put( MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS);

                ContentResolver contentResolver = getContentResolver();

                Uri uri = contentResolver.insert( MediaStore.Downloads.EXTERNAL_CONTENT_URI, values);

                OutputStream os = contentResolver.openOutputStream(uri);

                bos = new BufferedOutputStream(os);

                byte[] buffer = new byte[1024];

                int bytes = bis.read(buffer);

                while (bytes >= 0) {

                    bos.write(buffer, 0, bytes);

                    bos.flush();

                    bytes = bis.read(buffer);

                }

                runOnUiThread(() -> installAPK(uri));

            } catch (IOException e) {

                e.printStackTrace();

            } finally {

                try {

                    if (bis != null) bis.close();

                } catch (IOException e) {

                    e.printStackTrace();

                }

                try {

                    if (bos != null) bos.close();

                } catch (IOException e) {

                    e.printStackTrace();

                }

            }

        }).start();

    }

}

private void installAPK(Uri uri) {

    Intent intent = new Intent( Intent.ACTION_VIEW);

    intent.addFlags( Intent.FLAG_GRANT_READ_URI_PERMISSION);

    intent.setDataAndType(uri, "application/vnd.android.package-archive");

    startActivity(intent);

}              

 

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: android