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())) {
...
}
可见,内部存储和外部存储可以是在同一块存储介质上面的,只是概念上做了区分,即内部存储和外部存储是一块存储介质上的不同区域。
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);
}