Android 分区存储

news2024/11/24 20:42:27

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 Framework(SAF)获取到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 11,requestLegacyExternalStorage就失效了。此时增加了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_PATH(Android 版本 >= 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);

}              

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/188874.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Windows Server 2022 中文版、英文版下载 (updated Jan 2023)

Windows Server 2022 正式版&#xff0c;2023 年 1 月更新&#xff0c;持续更新中… 请访问原文链接&#xff1a;https://sysin.org/blog/windows-server-2022/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作者主页&#xff1a;www.sysin.org 此次发布更新…

如何设置 Excel 的行标题

Excel的行标题 打开或关闭Excel标题行Excel中的标题行格式选项Microsoft Excel工作表可以容纳一百万行,其中包含数字或文本数据集。行标题是位于工作表第1列左侧的灰色列,其中包含数字(1、2、3等),有助于识别工作表中的每一行。 尽管列标题是灰色的行,但它通常是字母(A、…

java泛型4

通配符之设定类型通配符的上限-------什么时候需要设置上限&#xff1f;&#xff1f;&#xff1f;&#xff1f;协变 还差一个Canvas类 这样定义行不行&#xff1f;不行&#xff01;&#xff01;&#xff01; 测试一下&#xff1a; 注意上面的drawAll()方法的形参类型是List …

好的直线导轨应该具备哪些要求?

直线导轨运用于各行各业&#xff0c;范围非常之广&#xff0c;但是对于直线导轨的要求都是大同小异的&#xff0c;下面我们来看下良好的直线导轨都应具备哪些要求&#xff1f; 1>运动灵敏度与定位精度高&#xff1a;定位精度是指运动构件能按要求停止在指定位置的能力。运动…

musl pwn 入门 (4)

在前面的介绍中&#xff0c;我们学习了musl pwn的基本原理&#xff0c;下面我们就通过一道经典例题进一步巩固。 这是DefCon Quals 2021中的一道题mooosl&#xff0c;直接在github上搜这道题的名字就可以找到作者发布的附件&#xff0c;内含说明、作者的exp、源码以及二进制程…

Linux系统之openEuler安装部署

Linux系统之openEuler安装部署一、openEuler介绍1.openEuler简介2.openEuler的硬件要求①物理机的安装要求②虚拟机的安装要求二、下载openEuler系统镜像1.官方网址2.下载openEuler系统镜像三、虚拟机配置工作1.设置虚拟机名称2.处理器配置3.设置虚拟机内存4.设置网络类型5.磁盘…

电脑桌面壁纸不清晰?壁纸模糊怎么修复高清?

我们在入手新电脑之后&#xff0c;首先就是会想要设置一个好看的壁纸&#xff0c;虽然系统会自带一些壁纸&#xff0c;但大多数用户都不喜欢这样一成不变的壁纸。于是在网上找了很好好看的壁纸换上&#xff0c;结果发现在更换电脑壁纸之后却发现壁纸显示非常的模糊不清。为什么…

git中gitignore忽略文件规则配置

我们在日常开发中会遇见项目打包的情况&#xff0c;然后这时候我们想要打包完成后提交一次代码&#xff0c;会忘记删除dist文件或者打包文件&#xff0c;会跟着提交上去&#xff0c;这样就造成了协同开发的麻烦&#xff0c;也会造成codeReview的障碍&#xff0c;让别人在拉取代…

CNN平移不变性

目录 .1 简介&#xff1a; 1.1什么是平移不变性 1.2 平移不变性/平移同变性 1.3 为什么卷积神经网络具有平移不变性 总结 1.4 证伪&#xff1a;CNN中的图片平移不变性 .2 实例 references&#xff1a; .1 简介&#xff1a; 1.1什么是平移不变性 不变性 不变性意味着即…

跟风试试ChatGPT

文章目录前言什么是ChatGPTChatGPT怎么玩注册验证使用设计型开发型强人所难型Python调用ChatGPT总结前言 其实现在也不算是跟风了&#xff0c;从 ChatGPT 出现至今已经有几个月的时间&#xff0c;这股风似乎已经刮过去了&#xff0c;虽然各种新闻铺天盖地&#xff0c;但因为懒…

RHCE(远程连接服务器)

文章目录一、远程连接服务器简介1、什么是远程连接服务器2、远程连接服务器的功能3、远程连接服务器的类型4、文字接口连接服务器二、连接加密技术简介1、版本协商阶段2、密钥和算法协商阶段会话密钥的生成3、认证阶段SSH提供两种认证方法&#xff1a;三、SSH远程连接服务简介1…

高阶数据结构之红黑树

文章目录红黑树红黑树的性质红黑树的定义红黑树的插入情况一&#xff1a;插入节点的父节点为红&#xff0c;祖父节点为黑&#xff0c;叔叔节点存在且为红情况二&#xff1a;当前节点的父节点为红&#xff0c;祖父节点为黑&#xff0c;叔叔节点不存在或者为黑红黑树的验证验证是…

Linux常用命令——read命令

在线Linux命令查询工具(http://www.lzltool.com/LinuxCommand) read 从键盘读取变量值 补充说明 read命令从键盘读取变量的值&#xff0c;通常用在shell脚本中与用户进行交互的场合。该命令可以一次读取多个变量的值&#xff0c;变量和输入的值都需要使用空格隔开。在read命…

这些预测性维护的专业术语你都了解吗?

一、前言 随着时代的发展&#xff0c;越来越多的企业希望能够在对设备和系统无损的前提下&#xff0c;通过一系列的测试和分析来实现维护。这种维护工作是基于设备和系统本身的运行状态来安排实施的&#xff0c;被称为CBM&#xff08;Condition Based Maintenance&#xff09;…

Apache自带压力测试工具—ab

ab压力测试工具&#xff1a; ab全称为&#xff1a;apache bench 我们先来了解一下压力测试的概念&#xff1a; 吞吐率&#xff08;Requests per second&#xff09; 概念&#xff1a;服务器并发处理能力的量化描述&#xff0c;单位是reqs/s&#xff0c;指的是某个并发用户数…

【c++】模拟实现vector

一.vector的成员变量与迭代器vector里面可以存各种数据类型&#xff0c;所以必定会用到模板&#xff0c;假设vector的模板参数为T&#xff0c;那么T*这个指针封装后就是vector的迭代器和string的迭代器很像&#xff0c;只不过string确定存的是字符&#xff0c;所以迭代器直接是…

ubuntu20.04网络配置

安装net-toolssudo apt-get install net-tools2、ifconfig查看网卡设备其中flags表中&#xff1a;running表示正在使用中。查看设备核心网络路由表&#xff1a;route -nDestination目标网段或者主机Gateway网关地址&#xff0c;”*” 表示目标是本主机所属的网络&#xff0c;不…

【Vue2+Element ui通用后台】用户列表

文章目录新增用户用户列表新增用户 首先增加一个 ‘新增’ 按钮&#xff0c;点击弹出对话框来新增用户。弹出框可以使用 Element UI 的 Dialog对话框&#xff0c;其中 visible 表示是否显示 Dialog&#xff0c;支持 .sync 修饰符。我们点击新增按钮把这个标识置为 true&#x…

金山系不惧微软,前有WPS力扛Office,后有eversheet接力再战

金山软件作为国产互联网元老企业&#xff0c;这里出来的IT大佬不计其数&#xff0c;小米的雷军、逸趣网络的吴裔敏、甜瓜在线的朱勇......金山软件不屈不挠的企业文化&#xff0c;沉淀已久&#xff0c;造就一大批人才&#xff0c;甚至追溯到40年前。 1981年&#xff0c;张旋龙在…

IntelliJ IDEA 闪退的解决办法

场景 最近这idea闪退频率又多了不少 以前 几天一闪退 现在 一天N多次闪退 如下图 看这崩溃日志 这怎么顶 解决办法 查看崩溃日志 日志 1 日志2 日志3 可以看出现在生效的参数 Command Line: -Xms128m -Xmx750m -XX:ReservedCodeCacheSize512m -XX:IgnoreUnrecognized…