[Android速通]数据储存

/ 0评 / 0

之前全部应用都不会涉及到数据储存,这次终于开始了.最简单就是SharedPreferences,当然他储存起来相当简单,还是回到之前的登录框App里.

我做一个密码验证的类,通过SharedPreferences保存简单数据,这里保存的数据是明文的,也就是玩玩.

public class PasswordManager {
    private static final String PREF_NAME = "AppPasswordPrefs";
    private static final String PASSWORD_KEY = "user_password";
    private static final int PASSWORD_LENGTH = 4;

    private final SharedPreferences sharedPreferences;
    private final Context context;

    public PasswordManager(Context context) {
        this.context = context;
        this.sharedPreferences = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE);
    }

    // 检查是否是首次启动
    public boolean isFirstLaunch() {
        return !sharedPreferences.contains(PASSWORD_KEY);
    }

    // 生成随机4位数字密码
    private String generateRandomPassword() {
        // 自行实现
    }

    // 保存密码到SharedPreferences
    private void savePassword(String password) {
        sharedPreferences.edit()
                .putString(PASSWORD_KEY, password)
                .apply();
    }

    // 获取保存的密码
    public String getSavedPassword() {
        return sharedPreferences.getString(PASSWORD_KEY, null);
    }

    // 显示密码对话框
    public void showPasswordDialog() {
        String password = generateRandomPassword();
        savePassword(password);

        // 自行实现
    }

    // 验证密码
    public boolean verifyPassword(String inputPassword) {
        String savedPassword = getSavedPassword();
        return savedPassword != null && savedPassword.equals(inputPassword);
    }
}

然后在主程序中调用.

protected void onCreate(Bundle savedInstanceState) {
    btn_login = findViewById(R.id.btn_login);
    btn_login.setOnClickListener(this);

    passwordManager = new PasswordManager(this);
    if(passwordManager.isFirstLaunch()){
        passwordManager.showPasswordDialog();
    }
}

@Override
public void onClick(View view) {
    if (view.getId() == R.id.btn_login) {
        EditText et_password = findViewById(R.id.et_password);
        if(passwordManager.verifyPassword(et_password.getText().toString())) {
            Toast.makeText(this,"密码正确",Toast.LENGTH_LONG).show();
        }else{
            Toast.makeText(this,"正确密码应该是" + passwordManager.getSavedPassword(),Toast.LENGTH_LONG).show();
        }
    }
}

这里提一个之前的知识点,我们一般是验证密码OK后会跳转到另一个Activity,之后会记录用户曾经输入的密码到数据库里,只有下一次打开App就不要再验证密码了,这时候启动另一个Activity要清空活动堆栈.不然用户按返回又回到登录界面,岂不是感觉很奇怪.

清空堆栈的方法

intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)

另外SharedPreferences只能存简单小量数据,因为他是加载到内存中进行的,如果要储存更多数据,可以考虑用DataStore代替,因为他用起来也是差不多的,对于更多的数据,就应该存数据库了,大多数安卓App都会运行着SQLite数据库.因为我是速通Android,我假设你看我这个笔记时候也是有SQL基础的了,我不会再细分讲这个,直接说一下如何使用数据库.在Android中,通常用SQLiteOpenHelper来方便管理数据库.

在说数据库储存前,先要说一下文件储存,因为我打算做的应用和这个有关,文件储存是需要权限的.

要先在AndroidManifest写入需要的权限.

    <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
    <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
    <uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

然后动态申请权限

public static boolean requestFilePermission(Activity activity, String permission) {
    // 检查是否已经拥有权限
    if (ContextCompat.checkSelfPermission(activity, permission)
            == PackageManager.PERMISSION_GRANTED) {
        return true;
    }

    // 如果没有权限,则动态请求
    ActivityCompat.requestPermissions(activity, new String[]{permission},
            getRequestCodeForPermission(permission));
    return false;
}

发起一个意图来选择图片.

Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
pickImageLauncher.launch(intent);

private final ActivityResultLauncher<Intent> pickImageLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
    if (result.getResultCode() == RESULT_OK && result.getData() != null) {
        handleImageSelection(result.getData());
    }
});

图片如何处理我这里暂且不说,因为到这一步有了图片的路径和权限,和普通的Java读写文件就没有区别了,说一下数据库需要注意的.数据库最好使用单例模式.重写onCreate和onUpgrade,这两个会在App升级和数据库首次创建时候发生,之后就可以使用增删查改,我这里每次操作都会打开关闭数据库,如果高频访问,当然可以只打开一次,操作完成再慢慢关闭.

public class ImageDbHelper extends SQLiteOpenHelper {
    private static final String DATABASE_NAME = "ImageDatabase.db";
    private static final int DATABASE_VERSION = 1;

    // 表名和列名
    public static final String TABLE_IMAGES = "images";
    public static final String COLUMN_ID = "_id";
    public static final String COLUMN_MD5 = "md5";
    public static final String COLUMN_PATH = "path";
    public static final String COLUMN_SIZE = "size";
    public static final String COLUMN_WIDTH = "width";
    public static final String COLUMN_HEIGHT = "height";
    public static final String COLUMN_DATE_ADDED = "date_added";

    // 单例实例
    private static ImageDbHelper instance;

    // 创建表的SQL语句
    private static final String CREATE_TABLE =
            "CREATE TABLE " + TABLE_IMAGES + " (" +
                    COLUMN_ID + " INTEGER PRIMARY KEY AUTOINCREMENT, " +
                    COLUMN_MD5 + " TEXT UNIQUE, " +
                    COLUMN_PATH + " TEXT, " +
                    COLUMN_SIZE + " INTEGER, " +
                    COLUMN_WIDTH + " INTEGER, " +
                    COLUMN_HEIGHT + " INTEGER, " +
                    COLUMN_DATE_ADDED + " INTEGER" +
                    ")";

    // 私有构造函数
    private ImageDbHelper(Context context) {
        super(context.getApplicationContext(), DATABASE_NAME, null, DATABASE_VERSION);
    }

    // 获取单例
    public static synchronized ImageDbHelper getInstance(Context context) {
        if (instance == null) {
            instance = new ImageDbHelper(context);
        }
        return instance;
    }

    @Override
    public void onCreate(SQLiteDatabase db) {
        db.execSQL(CREATE_TABLE);
    }

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        db.execSQL("DROP TABLE IF EXISTS " + TABLE_IMAGES);
        onCreate(db);
    }

    // 插入图片信息
    public long insertImage(ImageInfo imageInfo) {
        SQLiteDatabase db = this.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put(COLUMN_MD5, imageInfo.getMd5());
        values.put(COLUMN_PATH, imageInfo.getPath());
        values.put(COLUMN_SIZE, imageInfo.getSize());
        values.put(COLUMN_WIDTH, imageInfo.getWidth());
        values.put(COLUMN_HEIGHT, imageInfo.getHeight());
        values.put(COLUMN_DATE_ADDED, System.currentTimeMillis());

        long id = db.insertWithOnConflict(TABLE_IMAGES, null, values,
                SQLiteDatabase.CONFLICT_IGNORE);
        db.close();
        return id;
    }

    // 根据MD5查询图片是否存在
    public boolean isImageExists(String md5) {
        SQLiteDatabase db = this.getReadableDatabase();
        Cursor cursor = db.query(TABLE_IMAGES,
                new String[]{COLUMN_ID},
                COLUMN_MD5 + "=?",
                new String[]{md5},
                null, null, null);

        boolean exists = cursor.getCount() > 0;
        cursor.close();
        db.close();
        return exists;
    }

    // 获取所有图片信息
    @SuppressLint("Range")
    public List<ImageInfo> getAllImages() {
        List<ImageInfo> imageList = new ArrayList<>();
        SQLiteDatabase db = this.getReadableDatabase();
        Cursor cursor = db.query(TABLE_IMAGES,
                null, null, null, null, null, COLUMN_DATE_ADDED + " DESC");

        if (cursor.moveToFirst()) {
            do {
                ImageInfo image = new ImageInfo();
                image.setId(cursor.getLong(cursor.getColumnIndex(COLUMN_ID)));
                image.setMd5(cursor.getString(cursor.getColumnIndex(COLUMN_MD5)));
                image.setPath(cursor.getString(cursor.getColumnIndex(COLUMN_PATH)));
                image.setSize(cursor.getLong(cursor.getColumnIndex(COLUMN_SIZE)));
                image.setWidth(cursor.getInt(cursor.getColumnIndex(COLUMN_WIDTH)));
                image.setHeight(cursor.getInt(cursor.getColumnIndex(COLUMN_HEIGHT)));
                image.setDateAdded(cursor.getLong(cursor.getColumnIndex(COLUMN_DATE_ADDED)));

                imageList.add(image);
            } while (cursor.moveToNext());
        }
        cursor.close();
        db.close();
        return imageList;
    }

    // 根据ID删除图片
    public void deleteImage(long id) {
        SQLiteDatabase db = this.getWritableDatabase();
        db.delete(TABLE_IMAGES, COLUMN_ID + "=?", new String[]{String.valueOf(id)});
        db.close();
    }

    // 更新图片信息
    public int updateImage(ImageInfo imageInfo) {
        SQLiteDatabase db = this.getWritableDatabase();
        ContentValues values = new ContentValues();
        values.put(COLUMN_PATH, imageInfo.getPath());
        values.put(COLUMN_SIZE, imageInfo.getSize());
        values.put(COLUMN_WIDTH, imageInfo.getWidth());
        values.put(COLUMN_HEIGHT, imageInfo.getHeight());

        return db.update(TABLE_IMAGES, values,
                COLUMN_ID + "=?",
                new String[]{String.valueOf(imageInfo.getId())});
    }
}

存了几个图后读出来.

另外数据可以使用ORM库继续简化.

具体代码可以参考 https://gist.github.com/nickfox-taterli/6ab1c3c04b6b5e8204485f88159cc09a

我看到网上有人推荐一个方法,重写应用组件的一些方法,对数据库统一打开,对高频全局关键数据进行储存,这也是一个办法.Activity是会随着不断访问生成和销毁,但是Application只要程序还在,他就还在.具体在AndroidMainfest.xml的Application增加上name节点.

<application
    android:allowBackup="true"
    android:dataExtractionRules="@xml/data_extraction_rules"
    android:fullBackupContent="@xml/backup_rules"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.MyApplication"
    android:name=".MainApplication"
    tools:targetApi="31">
    <activity
        android:name=".MainActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

写一个类来重写特定方法.

public class MainApplication extends Application {
    @Override
    public void onCreate(){
        super.onCreate();
    }
    
    @Override
    public void onTerminate(){
        super.onTerminate();
    }
}

再按照单例的方式,之后不同的Activity都可以获取Application实例,使用他的方法.

public class MainApplication extends Application {
    private static MainApplication app;
    public static MainApplication getInstance(){
        return app;
    }
    @Override
    public void onCreate(){
        super.onCreate();
        app = this;
    }
}

如果引入包过多,可能需要修改其继承为MultiDexApplication.

上一节说到广播时候,可以跨越App进行通信,但是如果要传递大量数据,还是使用ContentProvider更合适一些.其完整组件其实是包含ContentProvider,ContentResolver,ContentObserver三个部分,其中ContentProvider是抽象类,所以需要自行实现.

在菜单New->Other->Content Provider可以快速新建一个,比如我打算创建一个ImgInfoProvider.

他创建了几个需要我们进行实现的方法,这里和数据库上是一一对应的,稍微实现一下也不是很麻烦,这里给出调用的例子.

// 插入图片信息
ContentValues values = new ContentValues();
values.put(ImageDbHelper.COLUMN_MD5, "****************");
values.put(ImageDbHelper.COLUMN_PATH, "/*****/*****/*****/*****.png");
values.put(ImageDbHelper.COLUMN_SIZE, 1024);
values.put(ImageDbHelper.COLUMN_WIDTH, 1920);
values.put(ImageDbHelper.COLUMN_HEIGHT, 1080);
getContentResolver().insert(ImgInfoProvider.CONTENT_URI, values);

// 查询所有图片
Cursor c = getContentResolver().query(
    ImgInfoProvider.CONTENT_URI,
    null, null, null, 
    ImageDbHelper.COLUMN_DATE_ADDED + " DESC");

不过实际开发中,很少App开放接口给别人读取,所以我们做更实际的例子,读取短信,监听短信,另外在我实现的过程中,发现小米通知类短信的权限是额外需要打开的,不然可能读不到验证码之类的,并且App必须在前台.下面是一个简单例子.

public class MainActivity extends AppCompatActivity {
    private static final int PERMISSION_REQUEST_CODE = 100;
    private TextView tvTrafficInfo;
    private Button btnQuery;
    private SmsObserver smsObserver;
    private boolean checkPermissions() {
        return ActivityCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) == PackageManager.PERMISSION_GRANTED
                && ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_GRANTED;
    }
    private void requestPermissions() {
        ActivityCompat.requestPermissions(
                this,
                new String[]{Manifest.permission.SEND_SMS, Manifest.permission.READ_SMS},
                PERMISSION_REQUEST_CODE
        );
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        if (requestCode == PERMISSION_REQUEST_CODE) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                sendTrafficQuerySms();
            } else {
                Toast.makeText(this, "需要短信权限才能查询流量", Toast.LENGTH_SHORT).show();
            }
        }
    }

    private void sendTrafficQuerySms() {
        try {
            SmsManager smsManager = SmsManager.getDefault();
            smsManager.sendTextMessage("10010", null, "10010", null, null); // 电信查询流量指令
            Toast.makeText(this, "已发送查询短信", Toast.LENGTH_SHORT).show();

            // 注册短信观察者
            registerSmsObserver();
        } catch (Exception e) {
            Toast.makeText(this, "发送短信失败: " + e.getMessage(), Toast.LENGTH_SHORT).show();
        }
    }

    private void registerSmsObserver() {
        if (smsObserver == null) {
            ContentResolver resolver = getContentResolver();
            smsObserver = new SmsObserver(new Handler());
            resolver.registerContentObserver(
                    Uri.parse("content://sms/inbox"),
                    true,
                    smsObserver
            );
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        EdgeToEdge.enable(this);
        setContentView(R.layout.activity_main);
        ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
            Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
            return insets;
        });

        tvTrafficInfo = findViewById(R.id.tvTrafficInfo);
        btnQuery = findViewById(R.id.btnQuery);

        btnQuery.setOnClickListener(v -> {
            if (checkPermissions()) {
                sendTrafficQuerySms();
            } else {
                requestPermissions();
            }
        });
    }

    private class SmsObserver extends ContentObserver {
        public SmsObserver(Handler handler) {
            super(handler);
        }

        @Override
        public void onChange(boolean selfChange) {
            super.onChange(selfChange);
            checkForTrafficSms();
        }

        private void checkForTrafficSms() {
            Cursor cursor = null;
            try {
                ContentResolver resolver = getContentResolver();
                cursor = resolver.query(
                        Uri.parse("content://sms/inbox"),
                        new String[]{"body", "address"},
                        "address = ?",
                        new String[]{"10010"}, // 电信回复号码
                        "date DESC LIMIT 5"
                );

                if (cursor != null && cursor.moveToFirst()) {
                    String smsBody = cursor.getString(0);
                    if (smsBody.contains("剩余流量")) { // 根据实际短信内容调整
                        runOnUiThread(() -> tvTrafficInfo.setText("流量信息: " + smsBody));
                        // 已经获取到自己的信息了,取消监听.
                        getContentResolver().unregisterContentObserver(smsObserver);
                        smsObserver = null;
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                if (cursor != null) {
                    cursor.close();
                }
            }
        }
    }
}

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注