美文网首页
Android开发如何将数据写入SD卡的非沙盒空间

Android开发如何将数据写入SD卡的非沙盒空间

作者: 量U移动广告归因 | 来源:发表于2021-05-29 23:07 被阅读0次
Tips:任何技术文章都有时效性,《从0到1手动打造移动广告归因系统》系列会尽力提供技术背景和环境,还请参考前务必看清文章发布时间,以免产生误导

量U的 Android SDK 在整个量U归因系统中处于前沿位置,扮演着数据采集的角色,负责为后端提供原始输入数据进行实时处理,为了精确测量App的安装量,SDK需要对设备标识符 IMEI 等数据进行客户端的持久化保存,而 Android SDK 的持久化逻辑如下

  • 首先写SD卡的非沙盒路径
    从Android Q(Android 10,对应 api 版本为29)开始,App被限制读写SD卡的公共空间(媒体文件除外),取而代之的是只能读写专属该App的沙盒空间,这个路径通常是 /sdcard/Android/data/包名/,你打开手机自带的文件管理器,一般都能看到这个路径,这一点与 /data/data/包名/ 这样的 internal 内部存储空间不同,/data/data/包名/ 通常需要 Root Explorer 这样的超级文件管理器才能访问到,同时与 internal 内部存储空间一样,SD卡的沙盒空间也会随App的卸载而删除,所以为了准确测量 App 的安装量,即便 App 被卸载重复安装多次,依然不会重复统计安装量——必须将设备信息写入SD卡的非沙盒路径

  • 如果SD卡的非沙盒路径不能写,退而求其次写 internal 内部存储,然后依靠 AndroidID 、MAC地址等其他唯一设备标识来对设备进行安装排重

虽然名义上从 Android Q 开始就不建议访问SD卡的公共空间,但是为了给开发者预留适配 Android Q 的时间,谷歌保留了一条豁免规则,那就是在 AndroidManifest.xml 的 application 节点添加一个 requestLegacyExternalStorage 属性(不要依赖这条规则豁免,下个版本可能会失效)

android:requestLegacyExternalStorage="true"

添加这个属性后,可以开启旧的文件访问方式,所以 targetSdkVersion >=29 和 <29 是两种完全不同的代码书写方式

  • targetSdkVersion < 29
    app 的 build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    defaultConfig {
        applicationId "com.example.root.test"
        minSdkVersion 26
        targetSdkVersion 26
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:26.+'
    implementation 'com.android.support.constraint:constraint-layout:1.1.0'
    implementation 'com.android.support:design:26.+'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.root.test">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:theme="@style/AppTheme.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
</manifest>

Tips: Android Q 访问沙盒文件不再需要声明权限,但是访问非沙盒文件(包括媒体文件)都需要声明权限并动态申请权限
单独写一个申请权限的类,可以与 MainActivity.java 放在同一个目录下面
CheckPermission.java

package com.example.root.test;

import android.app.Activity;
import android.content.pm.PackageManager;
import android.support.v4.app.ActivityCompat;
import android.util.Log;


public class CheckPermission {

    private static final int REQUEST_EXTERNAL_STORAGE = 1;
    private static final String[] PERMISSIONS_STORAGE = {"android.permission.READ_EXTERNAL_STORAGE", "android.permission.WRITE_EXTERNAL_STORAGE" };
    private static final String TAG = CheckPermission.class.getName();

    public static int verifyStoragePermissions(Activity activity) {
        try {
            //检测是否有写的权限
            int permission = ActivityCompat.checkSelfPermission(activity,PERMISSIONS_STORAGE[1]);
            if (permission != PackageManager.PERMISSION_GRANTED) {
                //Log.e(TAG, String.valueOf(permission));//询问or 拒绝 -1     使用中允许 or  始终允许  0
                // 没有写的权限,去申请写的权限,会弹出对话框
                ActivityCompat.requestPermissions(activity, PERMISSIONS_STORAGE,REQUEST_EXTERNAL_STORAGE);
            }
            return permission;
        } catch (Exception e) {
            return 1;
        }
    }
}

MainActivity.java

package com.example.root.test;

import android.content.pm.PackageManager;
import android.os.Bundle;
import android.os.Environment;
import android.support.annotation.NonNull;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

import java.io.File;
import java.io.FileOutputStream;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
                        .setAction("Action", null).show();
            }
        });
        int checkPermissionResult = CheckPermission.verifyStoragePermissions(MainActivity.this);
        if(checkPermissionResult == PackageManager.PERMISSION_GRANTED){
            write2SDCard("a.txt","hello");
        }
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_main, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    /**
     * 授权回调函数
     * @param requestCode
     * @param permissions
     * @param grantResults
     */
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        switch (requestCode){
            case 1:
                if(grantResults.length>0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    //权限已成功申请
                    //Toast.makeText(this,grantResults[0]+"",Toast.LENGTH_SHORT);
                    //Log.e(TAG, String.valueOf(grantResults[0]));
                    write2SDCard("a.txt","hello");
                } else {
                    //用户拒绝授权
                    Toast.makeText(this, "无法获取SD卡读写权限", Toast.LENGTH_SHORT).show();
                    //finish();
                }
                break;
            default:
                super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        }
    }

    /**
     * 写入SD卡私有方法
     * @param fileName
     * @param content
     */
    private void write2SDCard(String fileName,String content){
        //1、判断sd卡是否可用
        if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){
            //sd卡可用
            //2、获取sd卡路径
            try{
                File sdFile = Environment.getExternalStorageDirectory();
                //File sdFile = getExternalFilesDir(null);
                File packageSDDir = new File(sdFile,"/com.example.root.test/");
                if(!packageSDDir.exists() || !packageSDDir.isDirectory()){
                    boolean res = packageSDDir.mkdirs();//
                    //Log.e(TAG,String.valueOf(res));
                }
                File filePath = new File(packageSDDir,fileName);//sd卡下面的a.txt文件 参数 前面 是目录 后面是文件
                if(!filePath.exists() || !filePath.isFile()){
                    filePath.createNewFile();
                }

                FileOutputStream fileOutputStream = new FileOutputStream(filePath);
                fileOutputStream.write(content.getBytes());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

}

Tips: 使用新版的 Android Studio 4.2新建工程的时候,默认的 targetSdkVersion 就已经是30,如果强行手动调低,会使得编译无法通过,所以如果必要,可以使用低版本的 Android Studio,比如3.2版本,默认新建工程的targetSdkVersion是26

  • targetSdkVersion >= 29
    app.build
plugins {
    id 'com.android.application'
}

android {
    compileSdkVersion 30
    buildToolsVersion "28"

    defaultConfig {
        applicationId "com.example.test_library"
        minSdkVersion 26
        targetSdkVersion 30
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    lintOptions {
        abortOnError false
    }
}

dependencies {
    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'com.google.android.material:material:1.1.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    implementation 'androidx.navigation:navigation-fragment:2.2.2'
    implementation 'androidx.navigation:navigation-ui:2.2.2'
    testImplementation 'junit:junit:4.+'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
}

AndroidManifest.xml
必须加上 android:requestLegacyExternalStorage="true"

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.test_library">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Test_library"
        android:requestLegacyExternalStorage="true"> <!-- 此行必加-->
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:theme="@style/Theme.Test_library.NoActionBar">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

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

其他代码与 targetSdkVersion < 29 时一样
(完结)

相关文章

网友评论

      本文标题:Android开发如何将数据写入SD卡的非沙盒空间

      本文链接:https://www.haomeiwen.com/subject/kdoujltx.html