【Android】 四大组件详解之广播接收器、内容提供器

目录

  • 前言
  • 广播机制
    • 简介
    • 系统广播
      • 动态注册实现监听网络变化
      • 静态注册实现开机自启动
    • 自定义广播
      • 发送标准广播
      • 发送有序广播
    • 本地广播
  • 内容提供器
    • 简介
    • 运行时权限
    • 访问其他程序中的数据
      • ContentResolver的基本用法
      • 读取系统联系人
    • 创建自己的内容提供器
      • 创建内容提供器的步骤
    • 跨程序数据共享实例
      • DatabaseProvider
      • ProviderTest
      • 注意注意

前言

本文用于介绍Android四大组件中的广播接收器以及内容提供器,希望对您有帮助。

广播机制

简介

首先我们要知道什么是广播:广播是Android操作系统中用于应用程序之间或应用程序内部进行通信的机制,它允许一个应用程序发送消息(广播事件),而其他应用程序可以接收并对这些消息做出响应

广播可以分为两种类型:标准广播有序广播

  • 标准广播:标准广播是一种完全异步执行的广播,在广播发出之后,所有广播接收器几乎同一时间接收到这条广播,它们之间没有任何先后顺序而言。这种广播的效率比较高,但同时也无法被截断
  • 有序广播:有序广播是一种同步执行的广播,在广播发出之后,同一时刻只有一个广播接收器能收到这条广播消息,只有当当前广播接收器将逻辑处理完之后广播才会继续传递,优先级高的广播先接到广播消息,并且前面的广播接收器可以截断正在传递的广播,后面的广播接收器就接不到消息了。

系统广播

系统广播是由Android操作系统自身发出的广播,用于通知应用程序有关设备状态和系统事件的变化。

注册广播有两种方式:动态注册->在代码中注册,静态注册->在AndroidManifest.xml中注册。
下面我们分别演示一下。

动态注册实现监听网络变化

动态注册只需新建一个类并让其继承自BroadcastReceiver并重写onReceive()方法即可。当有广播来时,onReceive()方法便会执行,执行这里面的具体逻辑。下面我们来演示动态监听网络状态发生改变。

public class MainActivity extends AppCompatActivity {

	//用于封装action
    private IntentFilter intentFilter;
    private NetworkChangeReceiver networkChangeReceiver;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        intentFilter=new IntentFilter();
        //当网络状态发生变化时,系统发出一条值为android.net.conn.CONNECTIVITY_CHANGE的广播
        intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");//要监听什么广播,就在这添加相应的action
        networkChangeReceiver=new NetworkChangeReceiver();
        registerReceiver(networkChangeReceiver,intentFilter);
    }

    @Override
    protected void onDestroy(){
        super.onDestroy();
        //一定要记得取消注册
        unregisterReceiver(networkChangeReceiver);
    }

    class NetworkChangeReceiver extends BroadcastReceiver{
        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context,"net work change",Toast.LENGTH_SHORT).show();
        }
    }
}

注意:动态注册的广播可以自由控制注册与注销,但有一个缺点是必须在程序启动之后才能接收到广播。要想实现让程序未启动的情况下就能接收到广播,那就必须使用静态注册的方法了。

静态注册实现开机自启动

创建一个广播接收器BootCompleteReceiver:

public class BootCompleteReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "Boot Complete", Toast.LENGTH_LONG).show();
    }
}

只要我们是使用快捷方式来创建的广播接收器,系统会帮我们自动完成其注册,如创建出的静态广播接收器BootCompleteReceiver是会在AndroidManifest.xml文件中完成自动注册的,如下所示:

在这里插入图片描述

接着我们在注册好的广播的<intent-filter>标签里添加相应的action即可:

<intent-filter>
     <action android:name="android.intent.action.BOOT_COMPLETED"/> 开机就会自动发送一个action为此值的广播
</intent-filter>

最后监听系统开机广播需要声明权限:

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

这样我们在开机之后就会弹出一个Toast。

自定义广播

前面我们已经在知道广播主要分为标准广播和有序广播,接下来具体介绍一下这两种广播的具体用法。

发送标准广播

发送广播之前,我们要先创建一个广播接收器来接收此广播,新建MyBroadcastReceiver:

public class MyBroadReceiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context, "received in MyBroadcastReceiver", Toast.LENGTH_LONG).show();
    }
}

接着在AndroidManifest.xml中对广播进行注册(com.example.broadcasttest.MY_BROADCAST标识只接收值为这个的广播):

		<receiver
            android:name=".MyBroadReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter>
                <action android:name="com.example.broadcasttest.MY_BROADCAST"/>
            </intent-filter>
        </receiver>

最后点击按钮发送广播:

	Button button=(Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Log.d("MainActivity", String.valueOf(666));
                Intent intent=new Intent();
                intent.setAction("com.example.broadcasttest.MY_BROADCAST");
                intent.setPackage(getPackageName());//不可少,少了广播将无法被接收到
                sendBroadcast(intent);
            }
        });

启动项目点击发送即可发现屏幕上出现Toast。

发送有序广播

广播是一种可以跨进程的通信方式,因此在我们的应用程序内发出的广播,其他应用程序也可以接收到,只要其他应用程序的广播接收器的action与原本广播接收器的action一致。发送有序广播就允许我们随时截断这个广播

要想改发送标准广播为发送有序广播,只需将sendBroadcast(intent)改为sendOrderedBroadcast(intent,null)即可,另外我们需要去给广播接收器设置优先级,优先级越高,越先接收到广播,设置优先级如下所示:

		<receiver
            android:name=".MyBroadReceiver"
            android:enabled="true"
            android:exported="true">
            <intent-filter android:priority="100">
                <action android:name="com.example.broadcasttest.MY_BROADCAST"/>
            </intent-filter>
        </receiver>

最后,如果我们接收到广播后想截断广播,只需在广播接收器的onReceive()方法中加入abortBroadcast();即可让广播在此处截断。

本地广播

前面的我们发送和接收的广播全是属于系统全局广播,即发出的广播可以被其他应用程序接收到,并且我们也可以接收到其他应用程序的广播,这样容易引起安全性问题。

为此Android提供了一套本地广播机制,使用这个机制发出的广播只能在应用程序内部进行传递,并且广播接收器也只能接收来自本应用程序发出的广播。本地广播也是我们使用到的更多的广播。

下面提供一个使用本地广播的实例:

public class MainActivity extends AppCompatActivity {

    private IntentFilter intentFilter;
    private LocalReceiver localReceiver;
    private LocalBroadcastManager localBroadcastManager;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //获取实例
        localBroadcastManager=LocalBroadcastManager.getInstance(this);
        Button button=(Button) findViewById(R.id.button);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
               Intent intent=new Intent("com.example.broadcasttest.LOCAL_BROADCAST");
               //发送本地广播
               localBroadcastManager.sendBroadcast(intent);
            }
        });
        intentFilter=new IntentFilter();
        intentFilter.addAction("com.example.broadcasttest.LOCAL_BROADCAST");
        localReceiver=new LocalReceiver();
        //注册本地广播监听器,监听值为action的广播
        localBroadcastManager.registerReceiver(localReceiver,intentFilter);
    }

    @Override
    protected void onDestroy(){
        super.onDestroy();
        localBroadcastManager.unregisterReceiver(localReceiver);
    }

    class LocalReceiver extends BroadcastReceiver{

        @Override
        public void onReceive(Context context, Intent intent) {
            Toast.makeText(context,"receive local broadcast",Toast.LENGTH_SHORT).show();
        }
    }

}

要实现本地广播,我们需要用一个LocalBroadcastManager来对广播进行管理,用IntentFilter来封装我们的action。

当然如果这样你看的不是很懂的话,我们换一种方式。

  • 新建Local2Receiver:
public class Local2Receiver extends BroadcastReceiver {

    @Override
    public void onReceive(Context context, Intent intent) {
        Toast.makeText(context,"receive local broadcast666",Toast.LENGTH_SHORT).show();
    }
}
  • 在MainActivity中:
public class MainActivity extends AppCompatActivity {

    private IntentFilter intentFilter;
    private LocalBroadcastManager localBroadcastManager;


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //获取实例
        localBroadcastManager=LocalBroadcastManager.getInstance(this);

        Button button2=(Button) findViewById(R.id.button2);
        button2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Intent intent=new Intent("111");
                localBroadcastManager.sendBroadcast(intent);
            }
        });
        intentFilter=new IntentFilter();
        intentFilter.addAction("111");
        Local2Receiver local2Receiver=new Local2Receiver();
        localBroadcastManager.registerReceiver(local2Receiver,intentFilter);
    }
}

可以看到,在MainActivity中,我们首先获取了LocalBroadcastManager实例用来对广播进行管理,接着当我们点击按钮发送一条action为111的广播时,接下来我们注册本地广播监听,两个action一致,这样我们就可以让Local2Receiver广播接收器里的onReceive()方法得到执行了。

内容提供器

本章介绍的是Android四大组件之一内容提供器。在最后面提供了一个跨程序数据共享实例来参考学习。

简介

内容提供器的主要功能是用于在不同的应用程序之间实现数据的共享。它允许一个程序访问另一个程序中的数据,同时保持被访问数据的安全性。除此之外,内容提供器还能选择只对哪一部分数据进行共享,从而保证我们程序中的隐私数据不被泄露。

在详细讲解内容提供器的内容之前,我们需要先学习一下Android运行时权限。

运行时权限

我们常用的就两类权限,一类是普通权限,另一类是危险权限。普通权限不会直接威胁到用户的安全和隐私,只需去AndroidManifest.xml文件中注册一下即可;危险权限是可能会触及用户的隐私或对设备的安全性造成影响的权限,我们不仅需要去AndroidManifest.xml文件中注册一下,还必须要去手动申请这部分权限,否则程序无法使用相应的功能。

申请危险权限的实例如下所示:

  • AndroidManifest.xml
<!--    注册要申请的权限-->
<uses-permission android:name="android.permission.CAMERA"/>
  • 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 view) {
                //检查需要的权限是否被授权
                if(ContextCompat.checkSelfPermission(MainActivity.this, android.Manifest.permission.CAMERA)!= PackageManager.PERMISSION_GRANTED){
                    //未授权,请求权限
                    ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.CAMERA},1);
                }else {
                    call();
                }
            }
        });
    }
    //授权成功做的事
    private void call(){}
    
    @Override
    public void onRequestPermissionsResult(int requestCode,String[] permissions,int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, 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:
        }
    }
}

借助ContextCompat.checkSelfPermission()方法判断是否授权,在这里面我们要声明具体申请的权限名,用户未授权的话调用ActivityCompat.requestPermissions()方法申请权限。

访问其他程序中的数据

内容提供器的用法一般有两种:

  • 使用现有的内容提供器(系统提供的)来读取和操作相应程序中的数据
  • 创建自己的内容提供器给我们程序的数据提供外部访问接口

这里我们先来介绍第一种用法——使用现有的内容提供器。

如果一个应用程序通过内容提供器对其数据提供了外部访问接口,那其他的应用程序都可以对这部分数据进行数据访问。如电话簿就提供了类似的访问接口。我们先来学习一下ContentResolver的基本用法再去访问系统联系人的数据。

ContentResolver的基本用法

想访问内容提供器中的共享数据,必须使用ContentResolver类,我们可以通过Context中的getContentResolver()方法获取到这个类的实例。ContentResolver提供了一系列方法对数据进行CRUD操作,其中insert()用于添加数据、update()用于更新数据、delete()用于删除数据、query()用于查询数据。

说到这可能你会想到这个不是和SQLiteDatabase相似吗,但其实还是有些地方不同的。SQLiteDatabase是根据数据库名再根据表名来进行数据库操作。

ContentResolver不涉及到数据库名、表名,而是使用一个Uri参数代替,这个Uri参数被称为内容URI,内容URI分为两部分——authority和path,authority是用于对不同的应用程序进行区分的,如某程序包名是com.example.app,那么它的authority就可以命名为com.example.app.provider;path则是用于对应用程序中的表进行区分,并通常将其添加到authority后面,如该应用程序数据库中存在一张表table1,那么其path就可以命名为/table1,将authority与path组合,内容URI就变成了com.example.app.provider/table1,为了更容易地辨识出该字符串是内容URI,我们还需要在字符串头部加上协议声明content://,所以一个完整的内容URI就可以写成content://com.example.app.provider/table1

在这里插入图片描述

得到内容URI字符串后,我们需要将其解析为Uri对象,只需调用Uri的parse()方法即可:

Uri uri=Uri.parse("content://com.example.app.provider/table1");

接下来我们就可以使用这个Uri对象来查询table1中的数据了:

Cursor cursor=getContentResolver().query(
	uri,
    projection,
    selection,
    selectionArgs,
    sortOrder
);

下面对这些参数进行一些详解:

参数对应的SQL部分描述
urifrom tabel_name指定查询某应用程序下的某张表
projectionselect column1,column2指定查询的列名
selectionwhere column=value指定where的约束条件
selectionArgs-为where中的占位符提供具体的值
sortOrderorder by column1,column2指定查询结果的排列方式

查询结束后给我们返回的仍是一个Cursor对象,接下来就可以像查询数据库操作一样的,将数据依次从Cursor对象中逐个读取出来:

if(cursor!=null){
    while(cursor.moveToNext()){
        String column1=cursor.getString(cursor.getColumnIndex("column1"));
        int column2=cursor.getInt(cursor.getColumnIndex("column2"));
    }
    cursor.colse();
}
  • 添加操作
ContentValues values=new ContentValues();
values.put("column1","text");
values.put("column2",1);
getContentResolver().insert(uri,values);
  • 更新数据
ContentValues values=new ContentValues();
getContentResolver().update(uri,values,"column1=? and column2=?",new String[]{"text","1"});
  • 删除数据
//删除column2=1的那条数据
getContentResolver().delete(uri,"column2=?",new String[]{"1"});

读取系统联系人

我们首先先去联系人里添加几个数据。

在这里插入图片描述

  • 使用一个ListView用来展示查询的联系人数据
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <ListView
        android:id="@+id/contacts_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</LinearLayout>
  • 在AndroidManifest.xml里声明添加联系人权限
<uses-permission android:name="android.permission.READ_CONTACTS"/>
  • 在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, android.Manifest.permission.CAMERA)!= PackageManager.PERMISSION_GRANTED){
            //未授权,请求权限
            ActivityCompat.requestPermissions(this,new String[]{android.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);
            if(cursor!=null){
                while(cursor.moveToNext()){
                    //获取联系人姓名
                    @SuppressLint("Range") String displayName=cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME));
                    //获取联系人手机号
                    @SuppressLint("Range") String number=cursor.getString(cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                    contactsList.add(displayName+"\n"+number);
                }
                adapter.notifyDataSetChanged();
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if(cursor!=null){
                cursor.close();
            }
        }
    }
    @Override
    public void onRequestPermissionsResult(int requestCode,String[] permissions,int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode){
            case 1:
                if(grantResults.length>0&& grantResults[0]==PackageManager.PERMISSION_GRANTED){
                    //授权成功,执行操作
                    readContacts();
                }else {
                    //拒绝授权
                    Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
                }
                break;
            default:
        }
    }
}

我们主要来看readContacts()方法,其他都是申请权限相关的。在readContacts()方法中,我们的query()查询方法不是需要传入一个Uri参数吗,这里其实是ContactsContract.CommonDataKinds.Phone这个类替我们做好了封装,它的CONTENT_URI常量就是我们使用Uri.parse()方法解析出来的Uri参数,这就表示我们要使用内容提供器读取系统联系人的数据,另外联系人姓名和手机号也做好了封装,分别对应DISPLAY_NAME、NUMBER。最终我们就可以将从系统联系人中读到的数据展示在ListView上了。

创建自己的内容提供器

创建内容提供器的步骤

要创建自己的内容提供器(在这个内容提供器中,我们要完成执行数据库逻辑的操作,在别的应用程序中就可以通过这个内容提供器提供的方法来执行逻辑),只需创建出一个类继承自ContentProvider即可,新建MyProvider,其中我们要重写六个方法:

  • onCreate():在这完成数据库的创建和升级等操作,返回true表示内容提供器初始化成功,返回false表示失败
  • query():从内容提供器查询数据
  • insert():向内容提供器插入一条数据
  • update():更新内容提供器中已有的数据
  • delete():从内容提供器中删除数据
  • getType():根据传入的内容URI返回相应的MIME类型

上面的方法中,除了onCreate()方法,每个方法都需要传入一个Uri,这里我们对Uri进行一下解析,分析一下调用方期望访问的表和数据。标准的Uri写法如下:

content://com.example.app.provider/table1

这就表示我们期望访问的是com.example.app这个应用的table1表中的数据,我们还可以在后面加上一个id:

content://com.example.app.provider/table1/1

表示我们要访问的是com.example.app这个应用的table1表中的id为1的数据。内容URI的格式主要就以上两种,以路径结尾表示访问表中的所有数据,以id结尾表示访问表中相应id的数据。

另外我,还可以使用通配符来匹配上面两种格式的URI:

  • *:表示匹配任意长度的任意字符
  • #:表示匹配任意长度的数字

所以一个能匹配任意表的内容URI可以写成:

content://com.example.app.provider/*

一个能够匹配table1表中任意一行数据的内容URI可以写成

content://com.example.app.provider/table1/#

接着我们可以借助UriMatcher这个类可以轻松实现匹配内容URI,UriMatcher提供了一个addURI()方法,这个方法接收三个参数,可以把authority、path和一个自定义代码传进去,这样当我们调用UriMatcher的match()方法时就可以将一个Uri对象传入,返回值是某个能够匹配这个Uri对象所对应的自定义代码,利用这个代码,我们就可以判断出调用方期望访问的是哪张表中的数据了(传过来的Uri匹配的是哪个,就去哪个case执行对应的逻辑),MyProivder中的代码如下所示:

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;
    //期望匹配的URI
    static{
        uriMatcher=new UriMatcher(UriMatcher.NO_MATCH);
        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);
    }
    @Override
    public boolean onCreate() {
        //完成对数据库的创建和升级等操作,返回true表示内容提供器初始化成功,返回false表示失败
        return false;
    }
    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
        //从内容提供器查询数据
        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:
        }
        return null;
    }
    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }
    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues contentValues) {
        //向内容提供器插入一条数据
        return null;
    }
    @Override
    public int delete(@NonNull Uri uri, @Nullable String s, @Nullable String[] strings) {
        //从内容提供器删除一条数据
        return 0;
    }
    @Override
    public int update(@NonNull Uri uri, @Nullable ContentValues contentValues, @Nullable String s, @Nullable String[] strings) {
        //从内容提供器更新一条数据
        return 0;
    }
}

当query()方法被调用时,就会通过UriMatcher的match()方法对传入的Uri对象进行匹配,如果发现UriMatcher中某个内容URI格式成功匹配了该Uri对象,就会返回相应的自定义代码,然后我们就可以判断出调用方期望访问的到底是什么数据了,接着只需要去对应的case下执行具体的逻辑。

最后就是一个getType()方法,它是所有内容提供器都必须提供的方法,用于获取Uri对象的MIME类型,一个内容URI的MIME字符串主要有三部分组成,Android对这三部分做了如下格式规定:

  • 必须以vnd开头
  • 如果内容URI以路径结尾,则vnd后接android.cursor.dir/;如果内容以id结尾,则后接android.cursor.item/
  • 最后接上vnd.<authority>.<path>

所以对于content://com.example.app.provider/table1这个内容URI,它对于的MIME类型可以写成:

vnd.android.cursor.dir/vnd.com.example.app.provider.table1

对于content://com.example.app.provider/table1/1这个内容URI,它对应的MIME类型可以写成:

vnd.android.cursor.item/vnd.com.example.app.provider.table1

接着完善getType()方法:

	@Override
    public String getType(@NonNull Uri uri) {
        //根据传入的内容URI来返回相应的MIME类型
        switch (uriMatcher.match(uri)){
            case TABLE1_DIR:
                return "vnd.android.cursor.dir/com.example.app.provider.table1";
            case TABLE1_ITEM:
                return "vnd.android.cursor.item/com.example.app.provider.table1";
            case TABLE2_DIR:
                return "vnd.android.cursor.dir/com.example.app.provider.table2";
            case TABLE2_ITEM:
                return "vnd.android.cursor.item/com.example.app.provider.table2";
            default:
                break;
        }
        return null;
    }

到这一个完整的内容提供器就完成了,现在任何一个应用程序都可以使用ContentResolver来访问我们程序中的数据了。另外在我们创建内容提供器的过程中,就完成了保证隐私数据不被泄露的操作,我们在向UriMatcher中添加URI的时候,只要不去添加含隐私数据的URI即可,隐私数据无法被访问到,安全问题不久解决了。UriMatcher就是用来完成确定内容URI是否匹配的

小结:ContentResolver想进行增删改查操作,就必须传个Uri对象到方法中。而在我们自定义的内容提供器中可以使用UriMatcher来先定义好几个Uri对象,也就是说当在别处调用了增删改查方法时,传进去的Uri对象会与自定义内容提供器中通过UriMatcher定义好的Uri进行对比,只有能够匹配时我们才去在内容提供器中进行增删改查操作。

跨程序数据共享实例

大致过程:首先我们需要一个应用程序来充当内容提供器的数据供应方,这里我新建了一个项目DatabaseProvider,并在此应用程序的数据库中创建出一个Book表(数据库名为BookStore.db),同时在这个项目中实现了一个内容提供器用于共享其数据,具体在下面演示。接着我新建了一个项目ProviderTest,用来去访问DatabaseProvider这个程序,并对其Book表进行增删改查操作。

DatabaseProvider

  • 首先创建一个MyDatabaseHelper用于创建数据库和Book表
public class MyDatabaseHelper extends SQLiteOpenHelper{
    public static final String CREATE_BOOK="create table Book("
            +"id integer primary key autoincrement,"
            +"author text,"
            +"price real,"
            +"pages integer,"
            +"image blob,"
            +"name text)";

    private Context mContext;

    public MyDatabaseHelper(@Nullable Context context, @Nullable String name, @Nullable SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
        mContext=context;
    }

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

    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {}
}

注意:在这里我们不能使用Toast,跨程序访问不允许我们使用Toast。

  • 最重要的一步,实现一个内容提供器DatabaseProvider(这个项目的名字也叫DatabaseProvider,记得区分开),通过该内容提供器就可以给外部提供一个访问接口,对本应用程序的数据库进行操作
public class DatabaseProvider extends ContentProvider {
    public static final int BOOK_DIR=0;
    public static final int BOOK_ITEM=1;
    public static final String AUTHORITY="com.example.databaseprovider.provider";
    private static UriMatcher uriMatcher;
    private MyDatabaseHelper dbHelper;
    //期望匹配的URI
    static {
        uriMatcher=new UriMatcher(UriMatcher.NO_MATCH);
        uriMatcher.addURI(AUTHORITY,"book",BOOK_DIR);
        uriMatcher.addURI(AUTHORITY,"book/#",BOOK_ITEM);
    }
    @Override
    public boolean onCreate() {
        dbHelper=new MyDatabaseHelper(getContext(),"BookStore.db",null,1);
        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:
                //匹配的是根据id查询表,设置条件
                String bookId=uri.getPathSegments().get(1);
                cursor=db.query("Book",projection,"id=?",new String[]{bookId},null,null,sortOrder);
                break;
            default:
                break;
        }
        return cursor;
    }
    @Override
    public Uri insert(Uri uri, ContentValues values) {
        //添加数据
        SQLiteDatabase db=dbHelper.getWritableDatabase();
        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;
            default:
                break;
        }
        return uriReturn;
    }
    @Override
    public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
        //更新数据
        SQLiteDatabase db=dbHelper.getWritableDatabase();
        int updateRow=0;
        switch (uriMatcher.match(uri)){
            case BOOK_DIR:
                updateRow= db.update("Book",values,selection,selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId=uri.getPathSegments().get(1);
                updateRow=db.update("Book",values,"id=?",new String[]{bookId});
                break;
            default:
                break;
        }
        return updateRow;
    }
    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        //删除数据
        SQLiteDatabase db=dbHelper.getWritableDatabase();
        int deleteRows=0;
        switch (uriMatcher.match(uri)){
            case BOOK_DIR:
                deleteRows=db.delete("Book",selection,selectionArgs);
                break;
            case BOOK_ITEM:
                String bookId=uri.getPathSegments().get(1);
                deleteRows=db.delete("Book","id=?",new String[]{bookId});
                break;
            default:
                break;
        }
        return deleteRows;
    }
    @Override
    public String getType(Uri uri) {
        switch (uriMatcher.match(uri)){
            case BOOK_DIR:
                return "vnd.android.cursor.dir/vnd.com.example.databaseprovider.provider.book";
            case BOOK_ITEM:
                return "vnd.android.cursor.item/vnd.com.example.databaseprovider.provider.book";
        }
        return null;
    }
}

在该内容提供器中,我们定义了两个常量分别用于表示访问Book表中的所有数据访问Book表中的单条数据。接着在静态代码块中对UriMatcher进行了初始化操作并添加了两个期望匹配的URI格式。在onCreate()方法中创建了一个MyDatabaseHelper实例,用于下面的增删改查操作。接下来我们对这增删改查方法做一下基本讲解:

  • query():我们调用了uriMatcher.match()方法用于对我们想进行访问的数据进行匹配,通过这个方法我们可以得知期望访问的是哪张表中的哪些数据,接着根据不同需求完成数据的查询操作,最后将我们查询到的Cursor对象返回,在数据调用方遍历这个Cursor对象即可得到我们访问数据库得到的数据。
  • insert():在插入操作我们将数据插入进数据库的同时返回一个id,并将这个根据id组成一个URI返回给数据调用方,我们可以根据这个id执行增删改操作。
  • update()、delete():这两个方法没什么好说的,执行完操作后返回一个整型数据表示受影响的数据的行数。

至此,我们就可以将本项目启动一下再关闭了。接下来创建一个ProviderTest项目用于对DatabaseProvider进行访问及增删改查操作。

ProviderTest

  • 首先我们在布局中定义四个按钮进行增删改查操作
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".MainActivity">
    <Button
        android:id="@+id/add_data"
        android:text="Add To Book"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <Button
        android:id="@+id/query_data"
        android:text="Query From Book"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <Button
        android:id="@+id/update_data"
        android:text="Update Book"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
    <Button
        android:id="@+id/delete_data"
        android:text="Delete From Book"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"/>
</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 view) {
                //添加数据,表示我们想查询的是com.example.databaseprovider这个应用程序的Book表中的数据
                Uri uri=Uri.parse("content://com.example.databaseprovider.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 view) {
                //查询数据
                Uri uri=Uri.parse("content://com.example.databaseprovider.provider/book");
                Cursor cursor=getContentResolver().query(uri,null,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 are "+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 view) {
                //更新数据
                Uri uri=Uri.parse("content://com.example.databaseprovider.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 view) {
                //删除数据
                Uri uri=Uri.parse("content://com.example.databaseprovider.provider/book/"+newId);
                getContentResolver().delete(uri,null,null);
            }
        });
    }
}
  • 在插入数据中,我们先调用Uri.parse()方法将内容URI解析成Uri对象,这就表示我们想对com.example.databaseprovider这个应用程序的Book表进行访问,然后封装好一个ContentValues对象,接着调用ContentResolver的insert方法将数据插入即可,这个方法会返回一个id,在下面我们可以根据这id执行更新和删除数据的逻辑。
  • 在查询数据时,当我们调用query()方法时,上面我们讲到了这个方法会返回一个Cursor对象,这个对象里面有我们在DatabaseProvider里查询到的数据,这样我们就可以在ProviderTest这个程序中将数据取出来。

至此,当我们启动ProviderTest这个项目并点击添加数据的按钮时,我们可以打开DatabaseProvider的数据库进行查询,如下所示:

在这里插入图片描述

我们再点击查询数据的按钮时,控制台输出如下:

在这里插入图片描述

可以看到在ProviderTest中查询出来的数据和DatabaseProvider数据库中的数据一样,这样我们就实现了跨程序数据共享的功能。

注意注意

当你点击添加数据的按钮时,你可能会遇到UnKnown URL content://com.example…报错,这是因为在Android11之后,Android更改了程序间访问数据的方式,我们需要在AndroidManifest.xml文件中加入<queries>标签,如下所示:

  • 在DatabaseProvider中:

在这里插入图片描述

  • 在ProviderTest中

在这里插入图片描述
分享到此结束,希望对您有帮助!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/579783.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

数据仓库是什么

写在前面 刚接触大数据的新手小白可能会对数据仓库这个词比较陌生&#xff0c;本文将介绍数据仓库的主要特征及OLTP&OLAP的区别&#xff0c;帮助读者更好理解数据仓库。 一、什么是数据仓库 数据仓库&#xff0c;简称数仓&#xff0c;是一个对数据进行加工&#xff0c;集…

【go零基础】go-zero从零基础学习到实战教程 - 0环境配置

是个前端&#xff0c;最近开始学习go&#xff0c;后端除node外基本0基础&#xff0c;所以学习曲线有点绕&#xff0c;目标是个基础的服务端demo&#xff0c;搞个api服务后台&#xff0c;包含基础的用户登录、文章发布和写文章、权限控制&#xff0c;差不多就是个完整博客系统。…

CentOS 9 (stream) 安装 nginx

1.我们直接使用安装命令 dnf install nginx 2.安装完成后启动nginx服务 # 启动 systemctl start nginx # 设置开机自启动 systemctl enable nginx# 重启 systemctl restart nginx# 查看状态 systemctl status nginx# 停止服务 systemctl stop nginx 3.查看版本确认安装成功…

Apollo 7周年大会自动驾驶生态利剑出鞘

前言 4月22日&#xff0c;百度Apollo在北京车展前夕举办了以“破晓•拥抱智变时刻”为主题的智能汽车产品发布会&#xff0c;围绕汽车智能化&#xff0c;发布了智驾、智舱、智图等全新升级的“驾舱图”系列产品。 1、7周年大会 自2013年百度开始布局自动驾驶&#xff0c;201…

【leetcode】数组和相关题目总结

1. 两数之和 直接利用hashmap存储值和对于索引&#xff0c;利用target-nums[i]去哈希表里找对应数值。返回下标。 class Solution { public:vector<int> twoSum(vector<int>& nums, int target) {unordered_map<int, int> mp;vector<int> res;fo…

【Leetcode每日一题】 分治 - 面试题 17.14. 最小K个数(难度⭐⭐)(66)

1. 题目解析 题目链接&#xff1a;面试题 17.14. 最小K个数 这个问题的理解其实相当简单&#xff0c;只需看一下示例&#xff0c;基本就能明白其含义了。 2.算法原理 在快速排序算法中&#xff0c;我们通常会通过选择一个基准元素&#xff0c;然后将数组划分为三个部分&…

基于Spring Boot的火车订票管理系统设计与实现

基于Spring Boot的火车订票管理系统设计与实现 开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/idea 系统部分展示 前台首页功能界面图&#xff0c;在系统首页可以查看…

数据结构——插入排序

基本思想&#xff1a; 直接插入排序是一种简单的插入排序法&#xff0c;其基本思想是&#xff1a;把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中&#xff0c;直到所有的记录插入完为止&#xff0c;得到一个新的有序序列 。 实际中我们玩扑克牌时&…

排序算法(1)

一、基础概念 稳定性&#xff1a;假定在待排序的记录序列中&#xff0c;存在多个具有相同的关键字的记录&#xff0c;若经过排序&#xff0c;这些记录的相对次序保持 不变&#xff0c;即在原序列中&#xff0c;r[i]r[j]&#xff0c;且r[i]在r[j]之前&#xff0c;而在排序后的序…

TCP/IP协议族中的TCP(一):解析其关键特性与机制

⭐小白苦学IT的博客主页⭐ ⭐初学者必看&#xff1a;Linux操作系统入门⭐ ⭐代码仓库&#xff1a;Linux代码仓库⭐ ❤关注我一起讨论和学习Linux系统 前言 TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;是一种面向连接的、可靠的、基于字…

Java基础_集合类_List

List Collection、List接口1、继承结构2、方法 Collection实现类1、继承结构2、相关类&#xff08;1&#xff09;AbstractCollection&#xff08;2&#xff09;AbstractListAbstractSequentialList&#xff08;子类&#xff09; 其它接口RandomAccess【java.util】Cloneable【j…

一键PDF水印添加工具

一键PDF水印添加工具 引言优点1. 精准定位与灵活布局2. 自由旋转与透明度调控3. 精细化页码选择4. 全方位自定义水印内容5. 无缝整合工作流程 功能详解结语工具示意图【工具链接】 引言 PDF作为最常用的文档格式之一&#xff0c;其安全性和版权保护显得尤为重要。今天&#xff…

MyBatis面试题总结,详细(2024最新)

面试必须要看看 1、MyBatis 中的一级缓存和二级缓存是什么&#xff1f;它们的区别是什么&#xff1f; MyBatis 中的一级缓存是指 SqlSession 对象内部的缓存&#xff0c;它是默认开启的。一级缓存的生命周期是与 SqlSession 对象绑定的&#xff0c;当 SqlSession 关闭时&#…

vue3 ——笔记 (条件渲染,列表渲染,事件处理)

条件渲染 v-if v-if 指令用于条件性地渲染一块内容&#xff0c;只有v-if的表达式返回值为真才会渲染 v-else v-else 为 v-if 添加一个 else 区块 v-else 必须在v-if或v-else-if后 v-else-if v-else-if 是v-if 的区块 可以连续多次重复使用 v-show 按条件显示元素 v-sh…

8 Dubbo 应用案例(动手实操一波)

概述 案例相关配置可参考 GitHub:https://github.com/apache/dubbo-spring-boot-project/tree/master/dubbo-spring-boot-samples 创建服务接口项目 创建一个名为 hello-dubbo-service-user-api 的项目,该项目只负责定义接口 POM <?xml version="1.0" enco…

28.Gateway-网关过滤器

GatewayFilter是网关中提供的一种过滤器&#xff0c;可以多进入网关的请求和微服务返回的响应做处理。 GatewayFilter(当前路由过滤器&#xff0c;DefaultFilter) spring中提供了31种不同的路由过滤器工厂。 filters针对部分路由的过滤器。 default-filters针对所有路由的默认…

OpenCV如何实现背投

返回:OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 上一篇&#xff1a;OpenCV直方图比较 下一篇 :OpenCV系列文章目录&#xff08;持续更新中......&#xff09; 目标 在本教程中&#xff0c;您将学习&#xff1a; 什么是背投以及它为什么有用如何使用 Ope…

GraspNet-1Billion 论文阅读

文章目录 GraspNet-1Billion总体数据集评价指标网络pointnet&#xff1a;Approach Network:Operation Network&#xff1a;Tolerance Network 摘要相关工作基于深度学习的抓取预测算法抓取数据集点云深度学习 GraspNet-1Billion CVPR2020 上海交大 论文和数据集地址&#xff1…

【漏洞复现】艺创科技智能营销路由器后台命令执行漏洞

漏洞描述&#xff1a; 成都艺创科技有限公司是一家专注于新型网络设备研发、生产、销售和服务的企业&#xff0c;在大数据和云时代&#xff0c;致力于为企业提供能够提升业绩的新型网络设备。 智能营销路由器存在后台命令执行漏洞&#xff0c;攻击者可利用漏洞获取路由器控制…

Android 开发工具使用

c调试 在NDK调试的时候&#xff0c;如果找不到 符号的话&#xff0c;我们可以在调试配置中添加符号地址的全路径一直到根目录&#xff1a;&#xff0c;xxx/armeabi-v7a&#xff1a; You must point the symbol search paths at the obj/local/ directory. This is also not a …