Android学习之路之数据存储(三)

写在前面

Android有五种主要存储方式的用法,包括

  1. SD卡文件
  2. 共享参数:SharedPreferences
  3. 数据库:SQLite
  4. 应用Application基础
  5. 内容提供器ContentProvider

下面,我们将要讲解一下Application与内容提供器ContentProvider


4. 应用Application基础

在App运行过程中有且仅有一个Application对象贯穿整个生命周期。

4.1 Application的生命周期

  1. onCreate:App启动时调用
  2. onTerminate:App退出时调用
    • 该方法是供模拟环境调用,在真机上不会被调用
  3. onLowMemory:低内存时调用
  4. onConfigurationChanged:配置改变时调用,例如从竖屏变为横屏

4.2 利用Application操作全局变量

4.2.1 适用场景

适合在Application中保存的全局变量主要有下面3类数据:

  1. 频繁读取的信息,如用户名、手机号等
  2. 从网络上获取的临时数据,为减少用户等待时间,暂时放在内存中供下次使用,如商品图片等Iogo
  3. 容易因频繁分配内存而导致内存泄漏的对象,如Handler对象等

4.2.2 实现全局内存的读写

要想通过Application实现全局内存的读写,得完成以下3项工作:

  1. 写一个继承自Application的类MainApplication。该类要采用单例模式,内部声明自身类的一个静态成员对象,在创建App时把自身赋值给这个静态对象,然后提供该静态对象的获取方法getlnstance.
package com.example.storage;

import java.util.HashMap;
import android.app.Application;
import android.graphics.Bitmap;
import android.util.Log;

public class MainApplication extends Application {
    private final static String TAG = "MainApplication";
    // 声明一个当前应用的静态实例
    private static MainApplication mApp;
    // 声明一个公共的信息映射对象,可当作全局变量使用
    public HashMap<String, String> mInfoMap = new HashMap<String, String>();
    // 声明一个公共的图标映射对象,
    public HashMap<Long, Bitmap> mIconMap = new HashMap<Long, Bitmap>();

    // 利用单例模式获取当前应用的唯一实例
    public static MainApplication getInstance() {
        return mApp;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        // 在打开应用时对静态的应用实例赋值
        mApp = this;
        Log.d(TAG, "onCreate");
    }

    @Override
    public void onTerminate() {
        Log.d(TAG, "onTerminate");
        super.onTerminate();
    }
}

  1. 在AndroidManifest.xml中注册新定义的Application类名,即在application节点中增加android:name属性,值为.MainApplication;表示application的入口代码是MainApplication.java.
<application
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
    android:name=".MainApplication">
    <activity android:name=".MainActivity">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    <provider
        android:name=".provider.UserInfoProvider"
        android:authorities="com.example.storage.provider.UserInfoProvider"
        android:enabled="true"
        android:exported="true" />
</application>
  1. 在Activity中调用MainApplication的getlnstance方法,获得MainApplication的一个静态对象,通过该对象访问MainApplication的公共变量和公共方法。
package com.example.storage;

import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.Spinner;
import android.widget.Toast;
import android.widget.AdapterView.OnItemSelectedListener;

import com.example.storage.util.DateUtil;

public class AppWriteActivity extends AppCompatActivity implements OnClickListener {
    private EditText et_name;
    private EditText et_age;
    private EditText et_height;
    private EditText et_weight;
    private boolean bMarried = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_app_write);
        et_name = findViewById(R.id.et_name);
        et_age = findViewById(R.id.et_age);
        et_height = findViewById(R.id.et_height);
        et_weight = findViewById(R.id.et_weight);
        findViewById(R.id.btn_save).setOnClickListener(this);
        initTypeSpinner();
    }

    // 初始化婚姻状况的下拉框
    private void initTypeSpinner() {
        ArrayAdapter<String> typeAdapter = new ArrayAdapter<String>(this,
                R.layout.item_select, typeArray);
        typeAdapter.setDropDownViewResource(R.layout.item_dropdown);
        Spinner sp_married = findViewById(R.id.sp_married);
        sp_married.setPrompt("请选择婚姻状况");
        sp_married.setAdapter(typeAdapter);
        sp_married.setSelection(0);
        sp_married.setOnItemSelectedListener(new TypeSelectedListener());
    }

    private String[] typeArray = {"未婚", "已婚"};

    class TypeSelectedListener implements OnItemSelectedListener {
        public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
            bMarried = (arg2 == 0) ? false : true;
        }

        public void onNothingSelected(AdapterView<?> arg0) {
        }
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_save) {
            String name = et_name.getText().toString();
            String age = et_age.getText().toString();
            String height = et_height.getText().toString();
            String weight = et_weight.getText().toString();
            if (TextUtils.isEmpty(name)) {
                showToast("请先填写姓名");
                return;
            } else if (TextUtils.isEmpty(age)) {
                showToast("请先填写年龄");
                return;
            } else if (TextUtils.isEmpty(height)) {
                showToast("请先填写身高");
                return;
            } else if (TextUtils.isEmpty(weight)) {
                showToast("请先填写体重");
                return;
            }
            // 获取当前应用的Application实例
            MainApplication app = MainApplication.getInstance();
            // 以下直接修改Application实例中保存的映射全局变量
            app.mInfoMap.put("name", name);
            app.mInfoMap.put("age", age);
            app.mInfoMap.put("height", height);
            app.mInfoMap.put("weight", weight);
            app.mInfoMap.put("married", typeArray[!bMarried ? 0 : 1]);
            app.mInfoMap.put("update_time", DateUtil.getNowDateTime("yyyy-MM-dd HH:mm:ss"));
            showToast("数据已写入全局内存");
        }
    }

    private void showToast(String desc) {
        Toast.makeText(this, desc, Toast.LENGTH_SHORT).show();
    }

}
package com.example.storage;

import java.util.Map;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.widget.TextView;

public class AppReadActivity extends AppCompatActivity {
    private TextView tv_app;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_app_read);
        tv_app = findViewById(R.id.tv_app);
        readAppMemory();
    }

    // 读取全局内存中保存的变量信息
    private void readAppMemory() {
        String desc = "全局内存中保存的信息如下:";
        // 获取当前应用的Application实例
        MainApplication app = MainApplication.getInstance();
        // 获取Application实例中保存的映射全局变量
        Map<String, String> mapParam = app.mInfoMap;
        // 遍历映射全局变量内部的键值对信息
        for (Map.Entry<String, String> item_map : mapParam.entrySet()) {
            desc = String.format("%s\n %s的取值为%s",
                    desc, item_map.getKey(), item_map.getValue());
        }
        if (mapParam.size() <= 0) {
            desc = "全局内存中保存的信息为空";
        }
        tv_app.setText(desc);
    }

}

5. 内容提供与处理

Android号称提供了4大组件,分别是:

  • 页面Activity
  • 广播Broadcast
  • 服务Service
  • 内容提供器ContentProvider
    • 跟数据存取有关的组件,完整的内容组件由内容提供器 ContentProvider内容解析器 ContentResolver内容观察器 ContentObserver 这三部分组成

5.1 内容提供器 ContentProvider

在实际编码中,ContentProvider只是一个服务端的数据存取接口,开发者需要在其基础上实现一个具体类,并重写以下相关数据库管理方法

  • onCreate:创建数据库并获得数据库连接
  • query:查询数据
  • insert:插入数据
  • update:更新数据
  • delete:删除数据
  • getType:获取数据类型

5.1.1 实践案例

既然内容提供器是四大组件之一,就得在AndroidManifest.xml中注册它的定义,并开放外部访问权限,注册代码如下:

<provider
    android:name=".provider.UserInfoProvider"
    android:authorities="com.example.storage.provider.UserInfoProvider"
    android:enabled="true"
    android:exported="true" />

注册完毕后就完成了服务端App的封装工作,接下来可由其他App进行数据存取

  • UserInfo类相关代码
package com.example.storage.bean;

public class UserInfo {
    public long rowid;
    public int xuhao;
    public String name;
    public int age;
    public long height;
    public float weight;
    public boolean married;
    public String update_time;
    public String phone;
    public String password;

    public UserInfo() {
        rowid = 0L;
        xuhao = 0;
        name = "";
        age = 0;
        height = 0L;
        weight = 0.0f;
        married = false;
        update_time = "";
        phone = "";
        password = "";
    }
}

  • UserDBHelper 类:数据库操作
package com.example.storage.database;

import java.util.ArrayList;

import com.example.storage.bean.UserInfo;

import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

@SuppressLint("DefaultLocale")
public class UserDBHelper extends SQLiteOpenHelper {
    private static final String TAG = "UserDBHelper";
    private static final String DB_NAME = "user.db"; // 数据库的名称
    private static final int DB_VERSION = 1; // 数据库的版本号
    private static UserDBHelper mHelper = null; // 数据库帮助器的实例
    private SQLiteDatabase mDB = null; // 数据库的实例
    public static final String TABLE_NAME = "user_info"; // 表的名称

    private UserDBHelper(Context context) {
        super(context, DB_NAME, null, DB_VERSION);
    }

    private UserDBHelper(Context context, int version) {
        super(context, DB_NAME, null, version);
    }

    // 利用单例模式获取数据库帮助器的唯一实例
    public static UserDBHelper getInstance(Context context, int version) {
        if (version > 0 && mHelper == null) {
            mHelper = new UserDBHelper(context, version);
        } else if (mHelper == null) {
            mHelper = new UserDBHelper(context);
        }
        return mHelper;
    }

    // 打开数据库的读连接
    public SQLiteDatabase openReadLink() {
        if (mDB == null || !mDB.isOpen()) {
            mDB = mHelper.getReadableDatabase();
        }
        return mDB;
    }

    // 打开数据库的写连接
    public SQLiteDatabase openWriteLink() {
        if (mDB == null || !mDB.isOpen()) {
            mDB = mHelper.getWritableDatabase();
        }
        return mDB;
    }

    // 关闭数据库连接
    public void closeLink() {
        if (mDB != null && mDB.isOpen()) {
            mDB.close();
            mDB = null;
        }
    }

    // 创建数据库,执行建表语句
    public void onCreate(SQLiteDatabase db) {
        Log.d(TAG, "onCreate");
        String drop_sql = "DROP TABLE IF EXISTS " + TABLE_NAME + ";";
        Log.d(TAG, "drop_sql:" + drop_sql);
        db.execSQL(drop_sql);
        String create_sql = "CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " ("
                + "_id INTEGER PRIMARY KEY  AUTOINCREMENT NOT NULL,"
                + "name VARCHAR NOT NULL," + "age INTEGER NOT NULL,"
                + "height LONG NOT NULL," + "weight FLOAT NOT NULL,"
                + "married INTEGER NOT NULL," + "update_time VARCHAR NOT NULL"
                //演示数据库升级时要先把下面这行注释
                + ",phone VARCHAR" + ",password VARCHAR"
                + ");";
        Log.d(TAG, "create_sql:" + create_sql);
        db.execSQL(create_sql);
    }

    // 修改数据库,执行表结构变更语句
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
        Log.d(TAG, "onUpgrade oldVersion=" + oldVersion + ", newVersion=" + newVersion);
        if (newVersion > 1) {
            //Android的ALTER命令不支持一次添加多列,只能分多次添加
            String alter_sql = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + "phone VARCHAR;";
            Log.d(TAG, "alter_sql:" + alter_sql);
            db.execSQL(alter_sql);
            alter_sql = "ALTER TABLE " + TABLE_NAME + " ADD COLUMN " + "password VARCHAR;";
            Log.d(TAG, "alter_sql:" + alter_sql);
            db.execSQL(alter_sql);
        }
    }

    // 根据指定条件删除表记录
    public int delete(String condition) {
        // 执行删除记录动作,该语句返回删除记录的数目
        return mDB.delete(TABLE_NAME, condition, null);
    }

    // 删除该表的所有记录
    public int deleteAll() {
        // 执行删除记录动作,该语句返回删除记录的数目
        return mDB.delete(TABLE_NAME, "1=1", null);
    }

    // 往该表添加一条记录
    public long insert(UserInfo info) {
        ArrayList<UserInfo> infoArray = new ArrayList<UserInfo>();
        infoArray.add(info);
        return insert(infoArray);
    }

    // 往该表添加多条记录
    public long insert(ArrayList<UserInfo> infoArray) {
        long result = -1;
        for (int i = 0; i < infoArray.size(); i++) {
            UserInfo info = infoArray.get(i);
            ArrayList<UserInfo> tempArray = new ArrayList<UserInfo>();
            // 如果存在同名记录,则更新记录
            // 注意条件语句的等号后面要用单引号括起来
            if (info.name != null && info.name.length() > 0) {
                String condition = String.format("name='%s'", info.name);
                tempArray = query(condition);
                if (tempArray.size() > 0) {
                    update(info, condition);
                    result = tempArray.get(0).rowid;
                    continue;
                }
            }
            // 如果存在同样的手机号码,则更新记录
            if (info.phone != null && info.phone.length() > 0) {
                String condition = String.format("phone='%s'", info.phone);
                tempArray = query(condition);
                if (tempArray.size() > 0) {
                    update(info, condition);
                    result = tempArray.get(0).rowid;
                    continue;
                }
            }
            // 不存在唯一性重复的记录,则插入新记录
            ContentValues cv = new ContentValues();
            cv.put("name", info.name);
            cv.put("age", info.age);
            cv.put("height", info.height);
            cv.put("weight", info.weight);
            cv.put("married", info.married);
            cv.put("update_time", info.update_time);
            cv.put("phone", info.phone);
            cv.put("password", info.password);
            // 执行插入记录动作,该语句返回插入记录的行号
            result = mDB.insert(TABLE_NAME, "", cv);
            // 添加成功后返回行号,失败后返回-1
            if (result == -1) {
                return result;
            }
        }
        return result;
    }

    // 根据条件更新指定的表记录
    public int update(UserInfo info, String condition) {
        ContentValues cv = new ContentValues();
        cv.put("name", info.name);
        cv.put("age", info.age);
        cv.put("height", info.height);
        cv.put("weight", info.weight);
        cv.put("married", info.married);
        cv.put("update_time", info.update_time);
        cv.put("phone", info.phone);
        cv.put("password", info.password);
        // 执行更新记录动作,该语句返回记录更新的数目
        return mDB.update(TABLE_NAME, cv, condition, null);
    }

    public int update(UserInfo info) {
        // 执行更新记录动作,该语句返回记录更新的数目
        return update(info, "rowid=" + info.rowid);
    }

    // 根据指定条件查询记录,并返回结果数据队列
    public ArrayList<UserInfo> query(String condition) {
        String sql = String.format("select rowid,_id,name,age,height,weight,married,update_time," +
                "phone,password from %s where %s;", TABLE_NAME, condition);
        Log.d(TAG, "query sql: " + sql);
        ArrayList<UserInfo> infoArray = new ArrayList<UserInfo>();
        // 执行记录查询动作,该语句返回结果集的游标
        Cursor cursor = mDB.rawQuery(sql, null);
        // 循环取出游标指向的每条记录
        while (cursor.moveToNext()) {
            UserInfo info = new UserInfo();
            info.rowid = cursor.getLong(0); // 取出长整型数
            info.xuhao = cursor.getInt(1); // 取出整型数
            info.name = cursor.getString(2); // 取出字符串
            info.age = cursor.getInt(3);
            info.height = cursor.getLong(4);
            info.weight = cursor.getFloat(5); // 取出浮点数
            //SQLite没有布尔型,用0表示false,用1表示true
            info.married = (cursor.getInt(6) == 0) ? false : true;
            info.update_time = cursor.getString(7);
            info.phone = cursor.getString(8);
            info.password = cursor.getString(9);
            infoArray.add(info);
        }
        cursor.close(); // 查询完毕,关闭游标
        return infoArray;
    }

    // 根据手机号码查询指定记录
    public UserInfo queryByPhone(String phone) {
        UserInfo info = null;
        ArrayList<UserInfo> infoArray = query(String.format("phone='%s'", phone));
        if (infoArray.size() > 0) {
            info = infoArray.get(0);
        }
        return info;
    }

}

  • UserInfoProvider类:UserInfo内容提供者
package com.example.storage.provider;

import com.example.storage.database.UserDBHelper;
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;

public class UserInfoProvider extends ContentProvider {
    private final static String TAG = "UserInfoProvider";
    private UserDBHelper userDB; // 声明一个用户数据库的帮助器对象
    public static final int USER_INFO = 1; // Uri匹配时的代号
    public static final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    static { // 往Uri匹配器中添加指定的数据路径
        uriMatcher.addURI(UserInfoContent.AUTHORITIES, "/user", USER_INFO);
    }

    // 根据指定条件删除数据
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        int count = 0;
        if (uriMatcher.match(uri) == USER_INFO) {
            // 获取SQLite数据库的写连接
            SQLiteDatabase db = userDB.getWritableDatabase();
            // 执行SQLite的删除操作,返回删除记录的数目
            count = db.delete(UserInfoContent.TABLE_NAME, selection, selectionArgs);
            db.close(); // 关闭SQLite数据库连接
        }
        return count;
    }

    // 插入数据
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        Uri newUri = uri;
        if (uriMatcher.match(uri) == USER_INFO) {
            // 获取SQLite数据库的写连接
            SQLiteDatabase db = userDB.getWritableDatabase();
            // 向指定的表插入数据,返回记录的行号
            long rowId = db.insert(UserInfoContent.TABLE_NAME, null, values);
            if (rowId > 0) { // 判断插入是否执行成功
                // 如果添加成功,就利用新记录的行号生成新的地址
                newUri = ContentUris.withAppendedId(UserInfoContent.CONTENT_URI, rowId);
                // 通知监听器,数据已经改变
                getContext().getContentResolver().notifyChange(newUri, null);
            }
            db.close(); // 关闭SQLite数据库连接
        }
        return uri;
    }

    // 创建ContentProvider时调用,可在此获取具体的数据库帮助器实例
    @Override
    public boolean onCreate() {
        userDB = UserDBHelper.getInstance(getContext(), 1);
        return true;
    }

    // 根据指定条件查询数据库
    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        Cursor cursor = null;
        if (uriMatcher.match(uri) == USER_INFO) {
            // 获取SQLite数据库的读连接
            SQLiteDatabase db = userDB.getReadableDatabase();
            // 执行SQLite的查询操作
            cursor = db.query(UserInfoContent.TABLE_NAME,
                    projection, selection, selectionArgs, null, null, sortOrder);
            // 设置内容解析器的监听
            cursor.setNotificationUri(getContext().getContentResolver(), uri);
        }
        return cursor;
    }

    // 获取Uri数据的访问类型,暂未实现
    @Override
    public String getType(Uri uri) {
        throw new UnsupportedOperationException("Not yet implemented");
    }

    // 更新数据,暂未实现
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        throw new UnsupportedOperationException("Not yet implemented");
    }
}

  • 页面Activity: ContentProviderActivity
package com.example.storage;

import java.util.ArrayList;

import com.example.storage.bean.UserInfo;
import com.example.storage.provider.UserInfoContent;
import com.example.storage.util.DateUtil;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.EditText;
import android.widget.TextView;

@SuppressLint("DefaultLocale")
public class ContentProviderActivity extends AppCompatActivity implements OnClickListener {
    private static final String TAG = "ContentProviderActivity";
    private EditText et_name;
    private EditText et_age;
    private EditText et_height;
    private EditText et_weight;
    private TextView tv_read_user;
    private String mUserCount = "";
    private String mUserResult = "";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_content_provider);
        et_name = findViewById(R.id.et_name);
        et_age = findViewById(R.id.et_age);
        et_height = findViewById(R.id.et_height);
        et_weight = findViewById(R.id.et_weight);
        tv_read_user = findViewById(R.id.tv_read_user);
        findViewById(R.id.btn_add_user).setOnClickListener(this);
        tv_read_user.setOnClickListener(this);
        showUserInfo();
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_add_user) {
            UserInfo user = new UserInfo();
            user.name = et_name.getText().toString().trim();
            user.age = Integer.parseInt(et_age.getText().toString().trim());
            user.height = Integer.parseInt(et_height.getText().toString().trim());
            user.weight = Float.parseFloat(et_weight.getText().toString().trim());
            addUser(getContentResolver(), user);
            showUserInfo();
        } else if (v.getId() == R.id.tv_read_user) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle(mUserCount);
            builder.setMessage(mUserResult);
            builder.setPositiveButton("确定", null);
            builder.create().show();
        }
    }

    // 显示所有用户信息
    private void showUserInfo() {
        mUserResult = readAllUser(getContentResolver());
        String[] split = mUserResult.split("\n");
        int count = (!mUserResult.contains("\n")) ? 0 : split.length;
        mUserCount = String.format("当前共找到%d位用户信息", count);
        tv_read_user.setText(mUserCount);
    }

    // 添加一条用户记录
    private void addUser(ContentResolver resolver, UserInfo user) {
        ContentValues name = new ContentValues();
        name.put("name", user.name);
        name.put("age", user.age);
        name.put("height", user.height);
        name.put("weight", user.weight);
        name.put("married", false);
        name.put("update_time", DateUtil.getNowDateTime(""));
        // 通过内容解析器往指定Uri中添加用户信息
        resolver.insert(UserInfoContent.CONTENT_URI, name);
    }

    // 读取所有的用户记录
    private String readAllUser(ContentResolver resolver) {
        ArrayList<UserInfo> userArray = new ArrayList<UserInfo>();
        // 通过内容解析器从指定Uri中获取用户记录的游标
        Cursor cursor = resolver.query(UserInfoContent.CONTENT_URI, null, null, null, null);
        // 循环取出游标指向的每条用户记录
        while (cursor.moveToNext()) {
            UserInfo user = new UserInfo();
            user.name = cursor.getString(cursor.getColumnIndex(UserInfoContent.USER_NAME));
            user.age = cursor.getInt(cursor.getColumnIndex(UserInfoContent.USER_AGE));
            user.height = cursor.getInt(cursor.getColumnIndex(UserInfoContent.USER_HEIGHT));
            user.weight = cursor.getFloat(cursor.getColumnIndex(UserInfoContent.USER_WEIGHT));
            userArray.add(user); // 添加到用户信息队列
        }
        cursor.close(); // 关闭数据库游标

        String result = "";
        for (UserInfo user : userArray) {
            // 遍历用户信息队列,逐个拼接到结果字符串
            result = String.format("%s%s	年龄%d	身高%d	体重%f\n", result,
                    user.name, user.age, user.height, user.weight);
        }
        Log.d(TAG, "result=" + result);
        return result;
    }

}

5.2 内容解析器 ContentResolver

前面利用ContentProvider实现服务端App的数据封装。
如果客户端App想访问对方的内部数据,就要通过内容解析器ContentResolver访问

  • 内容解析器是客户端App操作服务端数据的工具,相对应的内容提供器是服务端的数据接口。
  • 要获取ContentResolver对象,在Activity代码中调用getContentResolver方法即可
  • ContentResolver提供的方法与ContentProvider是对应的

5.2.1 相关实例

  • 在实际开发中,普通App很少会开放数据接口给其他应用访问,作为服务端接口的ContentProvider基本用不到
  • 内容组件能够派上用场的情况往往是App想要访问系统应用的通信数据,比如查看联系人、短信、通话记录,以及对这些通信信息进行增、删、改、查。
package com.example.storage.util;

import java.util.ArrayList;

import com.example.storage.bean.CallRecord;
import com.example.storage.bean.Contact;
import com.example.storage.bean.SmsContent;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ContentProviderOperation;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.provider.CallLog;
import android.provider.ContactsContract;
import android.provider.Telephony;
import android.util.Log;

@SuppressLint("DefaultLocale")
public class CommunicationUtil {
    private final static String TAG = "CommunicationUtil";
    private static Uri mContactUri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
    private static String[] mContactColumn = new String[]{
            ContactsContract.CommonDataKinds.Phone.NUMBER,
            ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME};

    // 读取手机保存的联系人数量
    public static int readPhoneContacts(ContentResolver resolver) {
        ArrayList<Contact> contactArray = new ArrayList<Contact>();
        Cursor cursor = resolver.query(mContactUri, mContactColumn, null, null, null);
        while (cursor.moveToNext()) {
            Contact contact = new Contact();
            contact.phone = cursor.getString(0).replace("+86", "").replace(" ", "");
            contact.name = cursor.getString(1);
            Log.d(TAG, contact.name + " " + contact.phone);
            contactArray.add(contact);
        }
        cursor.close();
        return contactArray.size();
    }

    // 读取sim卡保存的联系人数量
    public static int readSimContacts(ContentResolver resolver) {
        Uri simUri = Uri.parse("content://icc/adn");
        ArrayList<Contact> contactArray = new ArrayList<Contact>();
        Cursor cursor = resolver.query(simUri, mContactColumn, null, null, null);
        while (cursor.moveToNext()) {
            Contact contact = new Contact();
            contact.phone = cursor.getString(0).replace("+86", "").replace(" ", "");
            contact.name = cursor.getString(1);
            Log.d(TAG, contact.name + " " + contact.phone);
            contactArray.add(contact);
        }
        cursor.close();
        return contactArray.size();
    }

    // 往手机中添加一个联系人信息(包括姓名、电话号码、电子邮箱)
    public static void addContacts(ContentResolver resolver, Contact contact) {
        // 构建一个指向系统联系人提供器的Uri对象
        Uri raw_uri = Uri.parse("content://com.android.contacts/raw_contacts");
        ContentValues values = new ContentValues(); // 创建新的配对
        // 往 raw_contacts 添加联系人记录,并获取添加后的联系人编号
        long contactId = ContentUris.parseId(resolver.insert(raw_uri, values));
        // 构建一个指向系统联系人数据的Uri对象
        Uri uri = Uri.parse("content://com.android.contacts/data");
        ContentValues name = new ContentValues(); // 创建新的配对
        name.put("raw_contact_id", contactId); // 往配对添加联系人编号
        // 往配对添加“姓名”的数据类型
        name.put("mimetype", "vnd.android.cursor.item/name");
        name.put("data2", contact.name); // 往配对添加联系人的姓名
        resolver.insert(uri, name); // 往提供器添加联系人的姓名记录
        ContentValues phone = new ContentValues(); // 创建新的配对
        phone.put("raw_contact_id", contactId); // 往配对添加联系人编号
        // 往配对添加“电话号码”的数据类型
        phone.put("mimetype", "vnd.android.cursor.item/phone_v2");
        phone.put("data1", contact.phone); // 往配对添加联系人的电话号码
        phone.put("data2", "2"); // 联系类型。1表示家庭,2表示工作
        resolver.insert(uri, phone); // 往提供器添加联系人的号码记录
        ContentValues email = new ContentValues(); // 创建新的配对
        email.put("raw_contact_id", contactId); // 往配对添加联系人编号
        // 往配对添加“电子邮箱”的数据类型
        email.put("mimetype", "vnd.android.cursor.item/email_v2");
        email.put("data1", contact.email); // 往配对添加联系人的电子邮箱
        email.put("data2", "2"); // 联系类型。1表示家庭,2表示工作
        resolver.insert(uri, email); // 往提供器添加联系人的邮箱记录
    }

    // 往手机中一次性添加一个联系人信息(包括主记录、姓名、电话号码、电子邮箱)
    public static void addFullContacts(ContentResolver resolver, Contact contact) {
        // 构建一个指向系统联系人提供器的Uri对象
        Uri raw_uri = Uri.parse("content://com.android.contacts/raw_contacts");
        // 构建一个指向系统联系人数据的Uri对象
        Uri uri = Uri.parse("content://com.android.contacts/data");
        // 创建一个插入联系人主记录的内容操作器
        ContentProviderOperation op_main = ContentProviderOperation
                .newInsert(raw_uri).withValue("account_name", null).build();
        // 创建一个插入联系人姓名记录的内容操作器
        ContentProviderOperation op_name = ContentProviderOperation
                .newInsert(uri).withValueBackReference("raw_contact_id", 0)
                .withValue("mimetype", "vnd.android.cursor.item/name")
                .withValue("data2", contact.name).build();
        // 创建一个插入联系人电话号码记录的内容操作器
        ContentProviderOperation op_phone = ContentProviderOperation
                .newInsert(uri).withValueBackReference("raw_contact_id", 0)
                .withValue("mimetype", "vnd.android.cursor.item/phone_v2")
                .withValue("data2", "2").withValue("data1", contact.phone).build();
        // 创建一个插入联系人电子邮箱记录的内容操作器
        ContentProviderOperation op_email = ContentProviderOperation
                .newInsert(uri).withValueBackReference("raw_contact_id", 0)
                .withValue("mimetype", "vnd.android.cursor.item/email_v2")
                .withValue("data2", "2").withValue("data1", contact.email).build();
        // 声明一个内容操作器的队列,并将上面四个操作器添加到该队列中
        ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>();
        operations.add(op_main);
        operations.add(op_name);
        operations.add(op_phone);
        operations.add(op_email);
        try {
            // 批量提交四个内容操作器所做的修改
            resolver.applyBatch("com.android.contacts", operations);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //private static Uri mSmsUri = Uri.parse("content://sms"); //该地址表示所有短信,包括收件箱和发件箱
    //private static Uri mSmsUri = Uri.parse("content://sms/inbox"); //该地址为收件箱

    private static Uri mSmsUri;
    private static String[] mSmsColumn;

    // 读取指定号码发来的短信记录
    @TargetApi(Build.VERSION_CODES.KITKAT)
    public static int readSms(ContentResolver resolver, String phone, int gaps) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            mSmsUri = Telephony.Sms.Inbox.CONTENT_URI;
        } else {
            mSmsUri = Uri.parse("content://sms/inbox");
        }
        mSmsColumn = new String[]{
                Telephony.Sms.ADDRESS, Telephony.Sms.PERSON,
                Telephony.Sms.BODY, Telephony.Sms.DATE,
                Telephony.Sms.TYPE};
        ArrayList<SmsContent> smsArray = new ArrayList<SmsContent>();
        String selection = "";
        if (phone != null && phone.length() > 0) {
            selection = String.format("address='%s'", phone);
        }
        if (gaps > 0) {
            selection = String.format("%s%sdate>%d", selection,
                    (selection.length() > 0) ? " and " : "", System.currentTimeMillis() - gaps * 1000);
        }
        Cursor cursor = resolver.query(mSmsUri, mSmsColumn, selection, null, "date desc");
        while (cursor.moveToNext()) {
            SmsContent sms = new SmsContent();
            sms.address = cursor.getString(0);
            sms.person = cursor.getString(1);
            sms.body = cursor.getString(2);
            sms.date = DateUtil.formatDate(cursor.getLong(3));
            sms.type = cursor.getInt(4);  //type=1表示收到的短信,type=2表示发送的短信
            Log.d(TAG, sms.address + " " + sms.person + " " + sms.date + " " + sms.type + " " + sms.body);
            smsArray.add(sms);
        }
        cursor.close();
        return smsArray.size();
    }

    private static Uri mRecordUri = CallLog.Calls.CONTENT_URI;
    private static String[] mRecordColumn = new String[]{
            CallLog.Calls.CACHED_NAME, CallLog.Calls.NUMBER, CallLog.Calls.TYPE,
            CallLog.Calls.DATE, CallLog.Calls.DURATION, CallLog.Calls.NEW};

    // 读取规定时间内的通话记录
    public static int readCallRecord(ContentResolver resolver, int gaps) {
        ArrayList<CallRecord> recordArray = new ArrayList<CallRecord>();
        String selection = String.format("date>%d", System.currentTimeMillis() - gaps * 1000);
        Cursor cursor = resolver.query(mRecordUri, mRecordColumn, selection, null, "date desc");
        while (cursor.moveToNext()) {
            CallRecord record = new CallRecord();
            record.name = cursor.getString(0);
            record.phone = cursor.getString(1);
            record.type = cursor.getInt(2);  //type=1表示接听,2表示拨出,3表示未接
            record.date = DateUtil.formatDate(cursor.getLong(3));
            record.duration = cursor.getLong(4);
            record._new = cursor.getInt(5);
            Log.d(TAG, record.name + " " + record.phone + " " + record.type + " " + record.date + " " + record.duration);
            recordArray.add(record);
        }
        cursor.close();
        return recordArray.size();
    }

    // 读取所有的联系人信息
    public static String readAllContacts(ContentResolver resolver) {
        ArrayList<Contact> contactArray = new ArrayList<Contact>();
        Cursor cursor = resolver.query(
                ContactsContract.Contacts.CONTENT_URI, null, null, null, null);
        int contactIdIndex = 0;
        int nameIndex = 0;

        if (cursor.getCount() > 0) {
            contactIdIndex = cursor.getColumnIndex(ContactsContract.Contacts._ID);
            nameIndex = cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME);
        }
        while (cursor.moveToNext()) {
            Contact contact = new Contact();
            contact.contactId = cursor.getString(contactIdIndex);
            contact.name = cursor.getString(nameIndex);
            contactArray.add(contact);
        }
        cursor.close();

        for (int i = 0; i < contactArray.size(); i++) {
            Contact contact = contactArray.get(i);
            contact.phone = getColumn(resolver, contact.contactId,
                    ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                    ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
                    ContactsContract.CommonDataKinds.Phone.NUMBER);
            contact.email = getColumn(resolver, contact.contactId,
                    ContactsContract.CommonDataKinds.Email.CONTENT_URI,
                    ContactsContract.CommonDataKinds.Email.CONTACT_ID,
                    ContactsContract.CommonDataKinds.Email.DATA);
            contactArray.set(i, contact);
            Log.d(TAG, contact.contactId + " " + contact.name + " " + contact.phone + " " + contact.email);
        }
        String result = "";
        for (Contact contact : contactArray) {
            result = String.format("%s%s	%s	%s\n", result, contact.name, contact.phone, contact.email);
        }
        return result;
    }

    private static String getColumn(ContentResolver resolver, String contactId,
                                    Uri uri, String selection, String column) {
        Cursor cursor = resolver.query(uri, null, selection + "=" + contactId, null, null);
        int index = 0;
        if (cursor.getCount() > 0) {
            index = cursor.getColumnIndex(column);
        }
        String value = "";
        while (cursor.moveToNext()) {
            value = cursor.getString(index);
        }
        cursor.close();
        return value;
    }

}

5.3 内容观察器 ContentObserver

5.3.1 ContentObserver使用场景

  • ContentResolver获取数据采用的是主动查询方式,有查询就有数据,没查询就没数据。

有时我们不但要获取以往的数据,还要实时获取新增的数据,最常见的业务场景是短信验证码。电商App经常在用户注册或付款时发送验证码短信,为了给用户省事,App通常会监控手机刚收到的验证码数字,并自动填入验证码输入框

这时就用到了内容观察器ContentObserver,给目标内容注册一个观察器,目标内容的数据一旦发生变化,观察器规定好的动作马上触发,从而执行开发者预先定义的代码。

5.3.2 ContentObserver相关方法

内容观察器的用法与内容提供器类似,也要从ContentObserver派生一个观察器类,然后通过ContentResolver对象调用相应的方法注册或注销观察器。

相关方法如下:

  • registerContentObserver:注册内容观察器
  • unregisterContentObserver:注销内容观察器。
  • notifyChange:通知内容观察器发生了数据变化。

5.3.3 ContentObserver相关实例

  • 手机App监控10086发送的短信内容,自动获取手机号码的月流量额度,实现流量校准
package com.example.storage;

import android.Manifest;
import android.annotation.SuppressLint;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.database.ContentObserver;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.os.Handler;
import android.support.v4.content.ContextCompat;
import android.support.v7.app.AppCompatActivity;
import android.telephony.SmsManager;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.TextView;
import android.widget.Toast;

/**
 * Created by ouyangshen on 2017/12/4.
 */
@SuppressLint("DefaultLocale")
public class ContentObserverActivity extends AppCompatActivity implements OnClickListener {
    private static final String TAG = "ContentObserverActivity";
    private static TextView tv_check_flow;
    private static String mCheckResult;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_content_observer);
        tv_check_flow = findViewById(R.id.tv_check_flow);
        tv_check_flow.setOnClickListener(this);
        findViewById(R.id.btn_check_flow).setOnClickListener(this);
        initSmsObserver();
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_check_flow) {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M &&
                    ContextCompat.checkSelfPermission(this, Manifest.permission.SEND_SMS) != PackageManager.PERMISSION_GRANTED) {
                Toast.makeText(this, "请先给当前APP开启短信权限", Toast.LENGTH_LONG).show();
            } else {
                //查询数据流量,移动号码的查询方式为发送短信内容“18”给“10086”
                //电信和联通号码的短信查询方式请咨询当地运营商客服热线
                //跳到系统的短信发送页面,由用户手工发短信
                //sendSmsManual("10086", "18");
                //无需用户操作,自动发送短信
                sendSmsAuto("10086", "18");
            }
        } else if (v.getId() == R.id.tv_check_flow) {
            AlertDialog.Builder builder = new AlertDialog.Builder(this);
            builder.setTitle("收到流量校准短信");
            builder.setMessage(mCheckResult);
            builder.setPositiveButton("确定", null);
            builder.create().show();
        }
    }

    // 跳到系统的短信发送页面,由用户手工编辑与发送短信
    public void sendSmsManual(String phoneNumber, String message) {
        Intent intent = new Intent(Intent.ACTION_SENDTO, Uri.parse("smsto:" + phoneNumber));
        intent.putExtra("sms_body", message);
        startActivity(intent);
    }

    // 短信发送事件
    private String SENT_SMS_ACTION = "com.example.storage.SENT_SMS_ACTION";
    // 短信接收事件
    private String DELIVERED_SMS_ACTION = "com.example.storage.DELIVERED_SMS_ACTION";

    // 无需用户操作,由App自动发送短信
    public void sendSmsAuto(String phoneNumber, String message) {
        // 以下指定短信发送事件的详细信息
        Intent sentIntent = new Intent(SENT_SMS_ACTION);
        sentIntent.putExtra("phone", phoneNumber);
        sentIntent.putExtra("message", message);
        PendingIntent sentPI = PendingIntent.getBroadcast(this, 0, sentIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        // 以下指定短信接收事件的详细信息
        Intent deliverIntent = new Intent(DELIVERED_SMS_ACTION);
        deliverIntent.putExtra("phone", phoneNumber);
        deliverIntent.putExtra("message", message);
        PendingIntent deliverPI = PendingIntent.getBroadcast(this, 1, deliverIntent, PendingIntent.FLAG_UPDATE_CURRENT);
        // 获取默认的短信管理器
        SmsManager smsManager = SmsManager.getDefault();
        // 开始发送短信内容。要确保打开发送短信的完全权限,不是那种还需提示的不完整权限
        smsManager.sendTextMessage(phoneNumber, null, message, sentPI, deliverPI);
    }

    private Handler mHandler = new Handler(); // 声明一个处理器对象
    private SmsGetObserver mObserver; // 声明一个短信获取的观察器对象
    private static Uri mSmsUri; // 声明一个系统短信提供器的Uri对象
    private static String[] mSmsColumn; // 声明一个短信记录的字段数组

    // 初始化短信观察器
    private void initSmsObserver() {
        //mSmsUri = Uri.parse("content://sms/inbox");
        //Android5.0之后似乎无法单独观察某个信箱,只能监控整个短信
        mSmsUri = Uri.parse("content://sms");
        mSmsColumn = new String[]{"address", "body", "date"};
        // 创建一个短信观察器对象
        mObserver = new SmsGetObserver(this, mHandler);
        // 给指定Uri注册内容观察器,一旦Uri内部发生数据变化,就触发观察器的onChange方法
        getContentResolver().registerContentObserver(mSmsUri, true, mObserver);
    }

    // 在页面销毁时触发
    protected void onDestroy() {
        // 注销内容观察器
        getContentResolver().unregisterContentObserver(mObserver);
        super.onDestroy();
    }

    // 定义一个短信获取的观察器
    private static class SmsGetObserver extends ContentObserver {
        private Context mContext; // 声明一个上下文对象
        public SmsGetObserver(Context context, Handler handler) {
            super(handler);
            mContext = context;
        }

        // 观察到短信的内容提供器发生变化时触发
        public void onChange(boolean selfChange) {
            String sender = "", content = "";
            // 构建一个查询短信的条件语句,这里使用移动号码测试,故而查找10086发来的短信
            String selection = String.format("address='10086' and date>%d", System.currentTimeMillis() - 1000 * 60 * 60);
            // 通过内容解析器获取符合条件的结果集游标
            Cursor cursor = mContext.getContentResolver().query(
                    mSmsUri, mSmsColumn, selection, null, " date desc");
            // 循环取出游标所指向的所有短信记录
            while (cursor.moveToNext()) {
                sender = cursor.getString(0);
                content = cursor.getString(1);
                break;
            }
            cursor.close(); // 关闭数据库游标
            mCheckResult = String.format("发送号码:%s\n短信内容:%s", sender, content);
            // 依次解析流量校准短信里面的各项流量数值,并拼接流量校准的结果字符串
            String flow = String.format("流量校准结果如下:\n\t总流量为:%s\n\t已使用:%s" +
                            "\n\t剩余:%s", findFlow(content, "总流量为", ","),
                    findFlow(content, "已使用", "B"), findFlow(content, "剩余", "B"));
            if (tv_check_flow != null) { // 离开该页面时就不再显示流量信息
                // 把流量校准结果显示到文本视图tv_check_flow上面
                tv_check_flow.setText(flow);
            }
            super.onChange(selfChange);
        }
    }

    // 解析流量校准短信里面的流量数值
    private static String findFlow(String sms, String begin, String end) {
        int begin_pos = sms.indexOf(begin);
        if (begin_pos < 0) {
            return "未获取";
        }
        String sub_sms = sms.substring(begin_pos);
        int end_pos = sub_sms.indexOf(end);
        if (end_pos < 0) {
            return "未获取";
        }
        if (end.equals(",")) {
            return sub_sms.substring(begin.length(), end_pos);
        } else {
            return sub_sms.substring(begin.length(), end_pos + end.length());
        }
    }

}


6. 补充:Content组件经常使用的系统URI

在这里插入图片描述

  • 15
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值