Android学习笔记——内容提供器

来源:互联网 发布:kit scream软件 编辑:程序博客网 时间:2024/06/02 03:10

参考书籍:Android第一行代码(第二版).郭霖著

文件存储、SharedPreferences存储及数据库存储所保存的数据只能在当前应用程序中访问(虽然文件存储和SharedPreferences存储中提供了MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE操作模式供其它应用程序访问当前应用数据,但在Android4.2中已废弃)。
跨程序数据共享应使用更安全可靠的内容提供器(Android跨程序共享数据的标准方式)。内容提供器提供一套完整机制,允许一个程序访问另一个程序中数据(保证数据安全性),可选择对哪部分数据进行共享(保证隐私不会泄露)。

1、运行时权限

之前的Android权限机制在保护用户安全和隐私方面作用有限。在Android6.0中引入运行时权限功能,更好保护用户安全隐私。
(1)Android权限机制
项目中添加权限声明后,如果用户在低于6.0系统的设备上安装此程序,就会清楚知晓此程序申请了哪些权限从而决定是否安装,但容易造成”店大欺客”的情况。
在6.0系统中加入了运行时权限功能,用户不需在安装软件时一次性授予所有申请的权限,在软件使用过程中可再对某一权限进行授予。
Android将所有权限归成两类:普通权限(不会直接威胁用户安全隐私,自动授权)和危险权限(可能会触及隐私、影响设备安全性,必须用户手动点击授权)。
危险权限:
这里写图片描述

除以上9组24个权限外都是普通权限(在AndroidManifest.xml中添加权限声明即可)。注意:每个危险权限都属于一个权限组,在进行运行时权限处理时使用的是权限名(用户一旦同意授权,对应权限组中所有其他权限会同时被授权)。

(2)在程序运行时申请权限

新建一个RuntimePermissionTest项目。修改布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent">    <Button        android:id="@+id/make_call"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="Make Call" /></LinearLayout>

修改MainAcitivity:

public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        Button makeCall = (Button) findViewById(R.id.make_call);        makeCall.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                try {//直接拨打电话,必须声明权限,为防止程序崩溃,所有操作都放在异常捕获代码块中                    Intent intent = new Intent(Intent.ACTION_CALL);                    intent.setData(Uri.parse("tel:10086"));                    startActivity(intent);                }catch (SecurityException e){                    e.printStackTrace();                }            }        });    }}

修改AndroidManifest.xml文件,添加:

<uses-permission android:name="android.permission.CALL_PHONE" />

这样在低于Android6.0系统手机上可以运行,但在6.0及更高版本系统运行没有任何效果,会提示权限被禁止的错误信息(必须进行运行时处理)。
修改MainActivity:

public class MainActivity extends AppCompatActivity {    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        Button makeCall = (Button) findViewById(R.id.make_call);        makeCall.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED){                    //判断用户是否已给我们授权,第二个参数是具体权限名,使用checkSelfPermission方法的返回值与PackageManager.PERMISSION_GRANTED作比较,相等说明已授权                    ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE }, 1);                    //没授权则需调用ActivityCompat.requestPermissions方法向用户申请授权,第二个参数是String数组,把要申请的权限名放入即可,                    // 第三个参数是请求码,只要是唯一值即可                }else {                    //已授权则直接执行拨打电话逻辑操作                    call();                }            }        });    }    private void call(){        try {//直接拨打电话,必须声明权限,为防止程序崩溃,所有操作都放在异常捕获代码块中            Intent intent = new Intent(Intent.ACTION_CALL);            intent.setData(Uri.parse("tel:10086"));            startActivity(intent);        }catch (SecurityException e){            e.printStackTrace();        }    }    //调用requestPermissions()方法后,系统会弹出一个权限申请对话框(用户选择同意或拒绝),不论哪种结果,最终都会回调到onRequestPermissionsResult()方法中    //授权结果会封装在grantResults参数中    @Override    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {        switch (requestCode){            case 1:                //判断授权结果                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){                    call();                }else {                    Toast.makeText(this, "you denied the permission", Toast.LENGTH_SHORT).show();                }                break;            default:        }    }}

重新运行程序,点击按钮。
这里写图片描述
点击DENY:
这里写图片描述
再次点击按钮,选择ALLOW:这里写图片描述
如果再次点击Make Call按钮就不会再弹出权限申请对话框了。如想关闭授权,进入Settings->Apps->RuntimePermissionTest->Permissions,进行关闭即可。

2、访问其他程序中的数据

内容提供器有两种用法:使用现有的内容提供器读取和操作相应程序中数据,创建自己的内容提供器给程序数据提供外部访问接口。
如果应用程序通过内容提供器对其数据提供了外部访问接口,其他任何应用程序都可对这部分数据进行访问。(Android中自带的电话簿、短信、媒体库等都提供了类似的访问接口)

(1)ContentResolver的基本用法

如果要想访问内容提供器中共享的数据,就一定要借助ContentResolver类(通过Context中的getContentResolver方法获取实例)。此类提供了一系列CRUD方法。
不同于SQLiteDatabases,此类中的增删查改方法不接收表名,用Uri参数代替(内容URI)。内容URI由两部分构成:authority(区分不同应用程序,一般用程序包名)和path(区分不同表),如:
content://com.example.app.provider/table1
得到内容URI后,需解析成Uri对象(Uri.parse()方法)才可作为参数传入。可使用此Uri对象查询table1中的数据:
Cursor cursor = getContentResolver().query(uri, projection, selection, selectionArgs, sortOrder);
这里写图片描述
仍然返回Cursor对象。
增删改方法类似。

(2)读取系统联系人
需先手动添加几个联系人以便稍后读取。新建一个ContactsTest项目,编写布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:layout_width="match_parent"    android:layout_height="match_parent"    android:orientation="vertical">    <ListView        android:id="@+id/contacts_View"        android:layout_width="match_parent"        android:layout_height="match_parent" /></LinearLayout>

为了将关注重点放在读取系统联系人上,没有使用RecyclerView(代码偏多)。修改MainActivity:

public class MainActivity extends AppCompatActivity {    ArrayAdapter<String> adapter;    List<String> contactsList = new ArrayList<>();    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        ListView contactsView = (ListView) findViewById(R.id.contacts_View);        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, contactsList);        contactsView.setAdapter(adapter);        if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CONTACTS) != PackageManager.PERMISSION_GRANTED) {            ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.READ_CONTACTS}, 1);        }else {            readContacts();        }    }    private void readContacts(){        Cursor cursor = null;        try{            //查询联系人数据            cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,null,null,null,null);            //ContactsContract.CommonDataKinds.Phone.CONTENT_URI常量为Uri.parse()解析出来的结果            if (cursor != null){                while (cursor.moveToNext()){                    //获取联系人姓名                    String displayName = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));                    //获取联系人手机号                    String number = cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));                    contactsList.add(displayName + "\n" + number);                }                //通知刷新ListView                adapter.notifyDataSetChanged();            }        }catch (Exception e){            e.printStackTrace();        }finally {            if (cursor != null){                //关闭Cursor对象                cursor.close();            }        }    }    @Override    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {        switch (requestCode){            case 1:                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){                    readContacts();                }else {                    Toast.makeText(this, "You denied the permisson", Toast.LENGTH_SHORT).show();                }                break;            default:        }    }}

还需声明读取系统联系人权限:

运行程序测试。
这里写图片描述

3、创建自己的内容提供器

(1)创建步骤

新建类继承ContentProvider来创建内容提供器。ContentProvider类中有6个抽象方法,继承时需重写。新建MyProvider:

public class MyProvider extends ContentProvider {    @Override    //初始化内容提供器时调用,通常在此完成对数据库的创建和升级等操作,返回true表示初始化成功    //只有当存在ContentResolver尝试访问程序中数据时,内容提供器才会被初始化    public boolean onCreate() {        return false;    }    @Nullable    @Override    //从内容提供器中查询数据,参数:第一哪张表,第二哪些列,第三第四约束查询哪些行,第五对结果进行排序,结果存在Cursor对象中    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {        return null;    }    @Nullable    @Override    //根据传入的内容URI返回相应的MIME类型    public String getType(Uri uri) {        return null;    }    @Nullable    @Override    //向内容提供器添加一条数据,uri参数确定目标表,新数据保存在values参数中。返回用于表示这条新记录的URI    public Uri insert(Uri uri, ContentValues values) {        return null;    }    @Override    //删除内容提供器中数据。返回被删除的行数    public int delete(Uri uri, String selection, String[] selectionArgs) {        return 0;    }    @Override    //更新内容提供器中已有数据。返回受影响的行数    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {        return 0;    }}

几乎每个方法都有Uri参数(调用ContentResolver的增删查改方法时传递过来的)。
需要对传入的Uri进行解析,分析出调用方期望访问的表和数据。内容URI主要有两种格式,可通过通配符分别匹配:
匹配任意表(*表示匹配任意长度任意字符)
content://com.example.app.provider/*
匹配table1表中任意行数据(#表示匹配任意长度数字)
content://com.example.app.provider/table1/#
接着借助UriMatcher类实现匹配内容URI功能。此类提供了addURI()方法,接收三个参数:authority,path,自定义代码。当调用UriMatcher的match()方法时,将Uri对象闯入,返回值为能匹配这个Uri对象对应的自定义代码(可判断调用方期望访问哪张表中数据)。修改MyProvider:

public class MyProvider extends ContentProvider {    public static final int TABLE1_DIR = 0;    public static final int TABLE1_ITEM = 1;    public static final int TABLE2_DIR = 2;    public static final int TABLE2_ITEM = 3;    private static UriMatcher uriMatcher;    static {        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);        //将期望匹配的内容URI格式传入        uriMatcher.addURI("com.example.app.provider", "table1", TABLE1_DIR);        uriMatcher.addURI("com.example.app.provider", "table1/#", TABLE1_ITEM);        uriMatcher.addURI("com.example.app.provider", "table2", TABLE2_DIR);        uriMatcher.addURI("com.example.app.provider", "table2/#", TABLE2_ITEM);    }   ...    @Nullable    @Override    //从内容提供器中查询数据,参数:第一哪张表,第二哪些列,第三第四约束查询哪些行,第五对结果进行排序,结果存在Cursor对象中    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {        switch (uriMatcher.match(uri)){//匹配成功,返回相应自定义代码            case TABLE1_DIR:                //查询table1表中所有数据                break;            case TABLE1_ITEM:                //查询table1表中的单条数据                break;            case TABLE2_DIR:                //查询table2表中所有数据                break;            case TABLE2_ITEM:                //查询table2表中的单条数据                break;            default:                break;        }       ...    }    ...

以查询方法为例,其他方法类似。getType()用于获取Uri对象对应的MIME类型,MIME字符串主要由三部分组成:
a.以vnd开头
b.如内容URI以路径结尾,后接android.cursor.dir/,如以id结尾,则接android.cursor.item/.
c.最后接上vnd..。

继续实现getType()中逻辑:

...public String getType(Uri uri) {        switch (uriMatcher.match(uri)){            case TABLE1_DIR:                return "vnd.android.cursor.dir/vnd.com.example.app.provider.table1";            case TABLE1_ITEM:                return "vnd.android.cursor.item/vnd.com.example.app.provider.table1";            case TABLE2_DIR:                return "vnd.android.cursor.dir/vnd.com.example.app.provider.table2";            case TABLE2_ITEM:                return "vnd.android.cursor.item/vnd.com.example.app.provider.table2";            default:                break;        }        return null;    }    ...

(2)跨程序数据共享

在DatabaseTest项目上开发,通过内容提供器加入外部访问接口。首先将MyDatabaseHelper中的Toast去掉(跨程序访问不能使用Toast),然后创建内容提供器,右击包名->New->Other->Content Provider,命名为DatabaseProvider, authority指定为包名.provider.Exported是否允许外部程序访问,Enable是否启用,都勾选。
修改DatabaseProvider:

public class DatabaseProvider extends ContentProvider {    public static final int BOOK_DIR = 0;    public static final int BOOK_ITEM = 1;    public static final int CATEGORY_DIR = 2;    public static final int CATEGORY_ITEM = 3;    public static final String AUTHORITY = "com.example.jojo.databasetest.provider";    private static UriMatcher uriMatcher;    private MyDatabaseHelper dbHelper;    static {        uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);        uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);        uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);        uriMatcher.addURI(AUTHORITY, "category", CATEGORY_DIR);        uriMatcher.addURI(AUTHORITY, "category/#", CATEGORY_ITEM);    }    @Override    public int delete(Uri uri, String selection, String[] selectionArgs) {        // 删除数据        SQLiteDatabase db = dbHelper.getReadableDatabase();        int deletedRows = 0;        switch (uriMatcher.match(uri)){            case BOOK_DIR:                deletedRows = db.delete("Book", selection, selectionArgs);                break;            case BOOK_ITEM:                String bookId = uri.getPathSegments().get(1);                deletedRows = db.delete("Book", "id = ?", new String[]{ bookId });                break;            case CATEGORY_DIR:                deletedRows = db.delete("Category",selection, selectionArgs);                break;            case CATEGORY_ITEM:                String categoryId = uri.getPathSegments().get(1);                deletedRows = db.delete("Category", "id = ?", new String[]{ categoryId });                break;            default:                break;        }        return deletedRows;    }    @Override    public String getType(Uri uri) {        switch (uriMatcher.match(uri)){            case BOOK_DIR:                return "vnd.android.cursor.dir/vnd.com.example.jojo.databasetest.provider.book";            case BOOK_ITEM:                return "vnd.android.cursor.item/vnd.com.example.jojo.databasetest.provider.book";            case CATEGORY_DIR:                return "vnd.android.cursor.dir/vnd.com.example.jojo.databasetest.provider.category";            case CATEGORY_ITEM:                return "vnd.android.cursor.item/vnd.com.example.jojo.databasetest.provider.category";            default:                break;        }        return null;    }    @Override    public Uri insert(Uri uri, ContentValues values) {        //添加数据        SQLiteDatabase db = dbHelper.getReadableDatabase();        Uri uriReturn = null;        switch (uriMatcher.match(uri)){            case BOOK_DIR:            case BOOK_ITEM:                long newBookId = db.insert("Book", null, values);                uriReturn = Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);                break;            case CATEGORY_DIR:            case CATEGORY_ITEM:                long newCategoryId = db.insert("Category", null, values);                uriReturn = Uri.parse("content://" + AUTHORITY + "/Category/" + newCategoryId);                break;            default:                break;        }        return uriReturn;    }    @Override    public boolean onCreate() {        //创建MyDatabaseHelper实例,返回true表示内容提供器创建成功,这时数据库已经完成创建升级        dbHelper = new MyDatabaseHelper(getContext(),"BookStore.db", null, 2);        return true;    }    @Override    public Cursor query(Uri uri, String[] projection, String selection,                        String[] selectionArgs, String sortOrder) {        //查询数据        SQLiteDatabase db = dbHelper.getReadableDatabase();        Cursor cursor = null;        switch (uriMatcher.match(uri)){            case BOOK_DIR:                cursor = db.query("Book", projection, selection, selectionArgs, null, null, sortOrder);                break;            case BOOK_ITEM:                //getPathSegments将内容URI权限后的部分以“/”符号分割,把分割后的结果放入到一个字符串列表中,                // 此列表的第0个位置存放的就是路径,第1个位置存放的是id                String bookId = uri.getPathSegments().get(1);                cursor = db.query("Book", projection, "id = ?", new String[]{bookId},null, null, sortOrder);                break;            case CATEGORY_DIR:                cursor = db.query("Category", projection, selection, selectionArgs, null, null, sortOrder);                break;            case CATEGORY_ITEM:                String categoryId = uri.getPathSegments().get(1);                cursor = db.query("Category", projection, "id = ?", new String[]{categoryId},null, null, sortOrder);                break;            default:                break;        }        return cursor;    }    @Override    public int update(Uri uri, ContentValues values, String selection,                      String[] selectionArgs) {        //更新数据        SQLiteDatabase db = dbHelper.getReadableDatabase();        int updateRows = 0;        switch (uriMatcher.match(uri)){            case BOOK_DIR:                updateRows = db.update("Book", values, selection, selectionArgs);                break;            case BOOK_ITEM:                String bookId = uri.getPathSegments().get(1);                updateRows = db.update("Book", values, "id = ?", new String[]{bookId});                break;            case CATEGORY_DIR:                updateRows = db.update("Category", values, selection, selectionArgs);                break;            case CATEGORY_ITEM:                String categoryId = uri.getPathSegments().get(1);                updateRows = db.update("Category", values, "id = ?", new String[]{categoryId});                break;            default:                break;        }        return updateRows;    }}

内容提供器一定要注册才能使用(Android Studio快捷方式创建的已自动完成这一步)。

<manifest xmlns:android="http://schemas.android.com/apk/res/android"    package="com.example.jojo.databasetest">    <application        android:allowBackup="true"        android:icon="@mipmap/ic_launcher"        android:label="@string/app_name"        android:supportsRtl="true"        android:theme="@style/AppTheme">        ...        <provider            android:name=".DatabaseProvider"            android:authorities="com.example.jojo.databasetest.provider"            android:enabled="true"            android:exported="true"></provider>    </application></manifest>

现在先将模拟器中的DatabaseTest程序删掉(放置遗留数据造成干扰),然后重新运行安装。关闭掉此项目,新建一个ProviderTest项目(用于访问DatabaseTest中数据)。修改布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"    android:orientation="vertical"    android:layout_width="match_parent"    android:layout_height="match_parent">    <Button        android:id="@+id/add_data"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="Add To Book" />    <Button        android:id="@+id/query_data"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="Query From Book" />    <Button        android:id="@+id/update_data"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="Update Book" />    <Button        android:id="@+id/delete_data"        android:layout_width="match_parent"        android:layout_height="wrap_content"        android:text="Delete From Book" /></LinearLayout>

修改MainActivity:

public class MainActivity extends AppCompatActivity {    private String newId;    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_main);        Button addData = (Button) findViewById(R.id.add_data);        addData.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                //添加数据                Uri uri = Uri.parse("content://com.example.jojo.databasetest.provider/book");                ContentValues values = new ContentValues();                values.put("name", "A Clash of Kings");                values.put("author", "George Martin");                values.put("pages",1040);                values.put("price", 22.85);                Uri newUri = getContentResolver().insert(uri, values);                newId = newUri.getPathSegments().get(1);            }        });        Button queryData = (Button) findViewById(R.id.query_data);        queryData.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Uri uri = Uri.parse("content://com.example.jojo.databasetest.provider/book");                Cursor cursor = getContentResolver().query(uri, null, null, null, null);                if (cursor != null){                    while (cursor.moveToNext()){                        String name = cursor.getString(cursor.getColumnIndex("name") );                        String author = cursor.getString(cursor.getColumnIndex("author"));                        int pages = cursor.getInt(cursor.getColumnIndex("pages"));                        double price = cursor.getDouble(cursor.getColumnIndex("price"));                        Log.d("MainActivity", "book name is " + name);                        Log.d("MainActivity", "book author is " + author);                        Log.d("MainActivity", "book pages is " + pages);                        Log.d("MainActivity", "book price is " + price);                    }                    cursor.close();                }            }        });        Button updateData = (Button) findViewById(R.id.update_data);        updateData.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {            //更新刚添加的数据,表中其他行不受影响                Uri uri = Uri.parse("content://com.example.jojo.databasetest.provider/book/" + newId);                ContentValues values = new ContentValues();                values.put("name", "A Storm of Swords");                values.put("pages",1216);                values.put("price", 24.05);                getContentResolver().update(uri, values, null, null);            }        });        Button deleteData = (Button) findViewById(R.id.delete_data);        deleteData.setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Uri uri = Uri.parse("content://com.example.jojo.databasetest.provider/book/" + newId);                getContentResolver().delete(uri, null, null);            }        });    }}

运行程序,分别点击增删改按钮,点击查询按钮查看日志信息。
添加后查询
添加
更新后查询
更新

原创粉丝点击