美文网首页
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