【Android】探究ContentProvider、运行时申请权限的做法、访问其他程序中的数据、创建自己的ContentProvider、实现跨程序数据共享并且解决存在的权限问题导致无法访问。

一、危险权限表

image-20230727092835738

二、运行时申请权限

1、使用ContextCompat.checkSelfPerSelfPermission()检查权限状态

2、获取的权限状态和PackageManager.PERMISSION_GRANTEN比较

3、如果没有权限则向用户申请权限ActivityCompat.requesPermissions()

4、重写onRequestPermissionsResult

权限判断值:

  • PackageManager.PERMISSION_GRANTED:表示已授予权限。0
  • PackageManager.PERMISSION_DENIED:表示权限被拒绝。-1

运行时权限的核心在于从用户获取权限。

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        binding = ActivityMainBinding.inflate(getLayoutInflater());

        setContentView(binding.getRoot());

        binding.button1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //判断是否存在需要的权限,不存在则申请
                if (ContextCompat.checkSelfPermission(MainActivity.this,
                        Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
                    ActivityCompat.requestPermissions(MainActivity.this, new String[]{Manifest.permission.CALL_PHONE}, 1);
                } else {
                    call();
                }
            }
        });
    }

    protected void call() {
        try {
            Intent intent = new Intent(Intent.ACTION_CALL);
            intent.setData(Uri.parse("tel:10086"));
            startActivity(intent);
        } catch (SecurityException e) {
            e.printStackTrace();
        }

    }

    //判断是否具有权限
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            case 1:
                //grantResults储存了用户的授权结果
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    call();
                }else {
                    Toast.makeText(this, "No Permission", Toast.LENGTH_SHORT).show();
                }
        }
    }
}

ContextCompat.checkSelfPermission()是一个用于检查权限状态的方法,它是Android支持库(Support Library)中的一个实用工具类。该方法用于在应用中检查特定权限是否已经被授予。**返回一个int值。**该方法获取两个参数,第一个是context,第二个是具体的权限名。

ActivityCompat.requestPermissions()它会在运行时向用户请求权限。具有三个三个参数。

  • 第一个是context
  • 第二个是一个权限数组(保存需要获取的权限名)
  • 第三个参数是请求码,用于标识此特定权限请求(后续在onRequestPermissionsResult()方法中处理权限请求结果时,系统会提供这个请求码,帮助你识别是哪个权限请求的结果。)。

onRequestPermissionsResult该重写方法具有三个参数:

  • int requestCode: 是请求权限时传递的请求码,用于标识特定的权限请求。
  • String[] permissions: 是请求的权限数组,包含所请求的权限的名称。
  • int[] grantResults: 是对应权限数组的授权结果数组,它包含用户对权限请求的响应,如果授权,则值为PackageManager.PERMISSION_GRANTED,否则为PackageManager.PERMISSION_DENIED

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

1、ContentResolver的基本用法

1.1 标准格式的URI写法

例如:包名为 com.example.permission,存在一张表table1

它的标准URI为 content://com.example.permission.provider/table1

1.2 基本的增删改查用法

1.2.1 查询数据

得到标准URI以后需要解析为Uri对象来查询table表的数据。

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

使用这个Uri进行查询的代码如下:

image-20230727113418251

image-20230727113436186

和SQLite的查询方式相似,我们还是获得一个Cursor对象,然后通过这个对象获取数据。

if(cursor != null){
    do {
        String name = cursor.getString(cursor.getColumnIndex("name"));
    }while (cursor.moveToNext());
}
1.2.2 增加数据
ContentValues values = new ContentValues();
values.put("name","This is a book");
getContentResolver().insert(uri,values);
1.2.3 删除数据
ContentValues values1 = new ContentValues();
values.put("name","This is a book");
getContentResolver().delete(uri,"name = ?",new String[]{"This is a b"});
1.2.4 修改数据
ContentValues values1 = new ContentValues();
values.put("name","This is a book1");
getContentResolver().update(uri,values,"name = ?",new String[]{"This is a book"});

2、读取系统联系人

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;

    ArrayAdapter<String> adapter;

    //保存联系人
    List<String> contactsList = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        binding = ActivityMainBinding.inflate(getLayoutInflater());

        setContentView(binding.getRoot());
        

        adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1,contactsList);
        binding.contactsView.setAdapter(adapter);

        if(ContextCompat.checkSelfPermission(this,Manifest.permission.READ_CONTACTS)
                != PackageManager.PERMISSION_GRANTED){
            ActivityCompat.requestPermissions(this,new String[]{Manifest.permission.READ_CONTACTS},2);
        }else {
            ReadContacts();
        }
    }

    private void ReadContacts(){
        Cursor cursor = null;
        try {
            cursor = getContentResolver().query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                    null,null,null);
            if (cursor != null && cursor.moveToFirst()) {
                int nameColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME);
                int numberColumnIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER);
                do {

                    String name = cursor.getString(nameColumnIndex);
                    String number = cursor.getString(numberColumnIndex);
                    contactsList.add(name + " " +number);
                } while (cursor.moveToNext());
                adapter.notifyDataSetChanged();
            }

        }catch (Exception e){
            e.printStackTrace();
        }finally {
            if (cursor != null){
                cursor.close();
            }
        }
    }
    

    //判断是否具有权限
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        switch (requestCode) {
            case 2:
                if(grantResults.length >0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                    ReadContacts();
                }else {
                    Toast.makeText(this, "No Contacks Permission", Toast.LENGTH_SHORT).show();
                }
        }
    }
}

注意在AndroidManifest中加入这句话获取系统权限。

四、创建自己的ContentProvider

通过新建一个自己的类,去继承ContentProvider类。重写以下6个方法:

  • OnCreate ()
  • query()
  • insert()
  • update()
  • delete()
  • getType(): 根据传人的内容URI来返回相应的MIME类型。
    可以看到,几乎每一个方法都会带有Uri这个参数,这个参数也正是调用ContentResolver的增删改查方法时传递过来的。而现在,我们需要对传入的Uri参数进行解析,从中分析出调用方期望访问的表和数据。

1、使用UriMatcher实现匹配URi

UriMatcher提供了一个addURI()方法,这个方法接收三个参数,第一个参数是authority,第二个参数是path,第三个参数是指定id

image-20230727163319692

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

所以,一个能够匹配任意表的内容URI格式就可以写成:

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

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

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

    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);
        UriMatcher.addURI("com.example.app.runtimepermissiontest","table1",TABLE1_DIR);
        UriMatcher.addURI("com.example.app.runtimepermissiontest","table1/#",TABLE1_ITEM);
        UriMatcher.addURI("com.example.app.runtimepermissiontest","table2",TABLE2_DIR);
        UriMatcher.addURI("com.example.app.runtimepermissiontest","table1/#",TABLE2_ITEM);

    }

将URI封装为对应的int参数。

为什么要放在static中使用??

  • 1、让整个类都共享这个UriMatcher实例。
  • 2、static代码块在类加载时期执行,只会执行一次,避免了多次重复初始化的开销。

UriMatcher.NO_MATCHUriMatcher 类的一个常量,它的值为 -1。在 UriMatcher 的构造函数中,如果传递了 UriMatcher.NO_MATCH,表示创建一个空的 UriMatcher 对象,即不会匹配任何 URI。

使用方法样例如下:

    @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:
                //查询表1的所有数据
                break;
            case TABLE1_ITEM:
                //查询表1的单条数据
                break;
        }
        return null;
    }

2、getType()方法

它是所有的内容提供器都必须提供的一个方法,用于获取URI对象所对应的MIME类型。一个内容URI所对应的MIME字符串主要由3部分组成

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

例如:content://com.example.app.provider/table1这个内容的URI,对应的MIME为:

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

@Nullable
@Override
public String getType(@NonNull 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.tablel"
    }

    return null;
}

这就是创建一个contentProvier的基本步骤。

五、实现跨程序数据共享

重写继承ContenProvider的方法:

    public static final int BOOK_DIR = 0;
    public static final int BOOK_ITEM = 1;
    public static final int GATEGORY_DIR = 2;
    public static final int GATEGORY_ITEM = 3;

    public static final String AUTHORITY = "com.example.sqlite.provider";

    private static UriMatcher uriMatcher;

    private MyDatabaseHelper dbhelper;

    //使用urimather封装URI
    static {
        uriMatcher.addURI(AUTHORITY, "book", BOOK_DIR);
        uriMatcher.addURI(AUTHORITY, "book/#", BOOK_ITEM);
        uriMatcher.addURI(AUTHORITY, "category", GATEGORY_DIR);
        uriMatcher.addURI(AUTHORITY, "category/#", GATEGORY_ITEM);
    }

    @Override
    public boolean onCreate() {
        //创建一个数据库帮助类
        dbhelper = new MyDatabaseHelper(getContext(), "BookStore.db", null, 2);
        return true;
    }

1、重写query方法

@Override
public Cursor query(Uri uri, String[] projection, String selection,
                    String[] selectionArgs, String sortOrder) {

    SQLiteDatabase db = dbhelper.getWritableDatabase();
    Cursor cursor = null;
    
    switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
            cursor = db.query("BooK", projection, selection,
                              selectionArgs, null, null, sortOrder);
            break;
        case BOOK_ITEM:
            String BookID = uri.getPathSegments().get(1);
            cursor = db.query("Book", projection, "id = ?",
                              new String[]{BookID}, null, null, sortOrder);
            break;
        case GATEGORY_DIR:
            cursor = db.query("Category", projection, selection,
                              selectionArgs, null, null, sortOrder);
            break;

        case GATEGORY_ITEM:
            String GatId = uri.getPathSegments().get(1);
            cursor = db.query("Category", projection, "id = ?", new String[]{GatId}, null, null, sortOrder);
            break;
    }
    return cursor;
}

先获取SQLiteDatanase的实例,然后通过实例查询数据。

当访问单条数据时,使用了getPathSegments,它会将内容URI权限只后的部分以**/**分开,将分割的结果放到一个字符串列表中,列表的0位置就是存放的路径,列表1位置就是存放的id了。然后通过selectionselectionArgs进行约束。

为什么返回 Cursor 对象而不是直接返回查询结果数据呢?

这是因为查询结果可能非常大,直接返回数据会导致内存占用过高,而 Cursor 使用了懒加载的机制,只在需要数据时才去获取,有效地减少了内存消耗。同时,使用 Cursor 还可以进行异步查询,避免阻塞主线程

2、重写insert方法

@Override
public Uri insert(Uri uri, ContentValues values) {

    SQLiteDatabase db = dbhelper.getWritableDatabase();
    Uri uri1 = null;

    switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
        case BOOK_ITEM:
            long newBookId = db.insert("Book", null, values);
            uri1 = Uri.parse("Content://" + AUTHORITY + "/book/" + newBookId);
            break;
        case GATEGORY_DIR:
        case GATEGORY_ITEM:
            long newCategory = db.insert("Category", null, values);
            uri1 = Uri.parse("Content://" + AUTHORITY + "/category/" + newCategory);
            break;
    }
    return uri1;
}

向名为 “Book” 的数据库表中插入一条新的数据,并将插入后的新行的主键值或行号拼接到一个 Uri 中,以便返回给调用者。这个 Uri 包含了内容提供器的授权信息、表名和新行的主键值或行号等信息

在一些情况下,客户端可能不需要获取到新插入数据的Uri,或者内容提供器的设计不需要返回新插入数据的Uri,那么这个步骤可以省略。但是,如果需要引用到新插入的数据,将其添加到Uri是一个很常见的做法。

3、重写update方法

    @Override
    public int update(Uri uri, ContentValues values, String selection,
                      String[] selectionArgs) {

        SQLiteDatabase dp = dbhelper.getWritableDatabase();
        int updatarows = 0;
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                updatarows = dp.update("Book",values,selection,selectionArgs);
                break;
            case BOOK_ITEM:
                String BookId = uri.getPathSegments().get(1);
                updatarows = dp.update("Book",values,"id = ?",new String[]{BookId});

                break;
            case GATEGORY_DIR:
                updatarows = dp.update("Category",values,selection,selectionArgs);
                break;
            case GATEGORY_ITEM:
                String CategoryId = uri.getPathSegments().get(1);
                updatarows = dp.update("Category",values,"id = ?",new String[]{CategoryId});
                break;
        }
        return updatarows;
    }

返回受到更新的行数位置。

4、重写delete方法

@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {

    SQLiteDatabase dp = dbhelper.getWritableDatabase();
    int deletedRows = 0;

    switch (uriMatcher.match(uri)) {
        case BOOK_DIR:
            deletedRows = dp.delete("Book", selection, selectionArgs);
            break;
        case BOOK_ITEM:
            String BookId = uri.getPathSegments().get(1);
            deletedRows = dp.delete("Book", "id = ?", new String[]{BookId});
            break;
        case GATEGORY_DIR:
            deletedRows = dp.delete("Categroy", selection, selectionArgs);
            break;

        case GATEGORY_ITEM:
            String CategoryId = uri.getPathSegments().get(1);
            deletedRows = dp.delete("Category", "id = ?", new String[]{CategoryId});
            break;
    }
    return deletedRows;
}

5、重写getType方法

@Override
public String getType(Uri uri) {
    switch (uriMatcher.match(uri)){
        case BOOK_DIR:
            return "vnd.android.cursor.dir/vnd.com.example.sqlite.provider.book";
        case BOOK_ITEM:
            return "vnd.android.cursor.item/vnd.com.example.sqlite.provider.book";
        case GATEGORY_DIR:
            return "vnd.android.cursor.dir/vnd.com.example.sqlite.provider.category";
        case GATEGORY_ITEM:
            return "vnd.android.cursor.item/vnd.com.example.sqlite.provider.category";
    }
    return null;
}

6、新建一个应用访问具有ContenProvider的应用

新建一个项目ententprovidertest用于访问刚才的

public class MainActivity extends AppCompatActivity {
    ActivityMainBinding binding;

    private String newId;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        binding = ActivityMainBinding.inflate(getLayoutInflater());
        setContentView(binding.getRoot());

        //添加数据
        binding.add.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Uri uri = Uri.parse("content://com.example.sqlite.provider/book");
                ContentValues values =new ContentValues();

                //将数据保存起来,第二条数据
                values.put("name","two book");
                values.put("author","dow1");
                values.put("pages",2321);
                values.put("price",1212);
                Uri newUri = getContentResolver().insert(uri,values);
                newId = newUri.getPathSegments().get(1);
            }
        });

        //查询数据
        binding.query.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Uri uri = Uri.parse("content://com.example.sqlite.provider/book");
                Cursor cursor = getContentResolver().query(uri,null,null,null);
                if(cursor != null && cursor.moveToFirst()){
                    do {
                        Log.d("ACTIVITY1",cursor.getString((int)cursor.getColumnIndex("name")));
                        Log.d("ACTIVITY1",cursor.getString((int)cursor.getColumnIndex("author")));
                        Log.d("ACTIVITY1",cursor.getString((int)cursor.getColumnIndex("pages")));
                        Log.d("ACTIVITY1",cursor.getString((int)cursor.getColumnIndex("price")));
                    }while (cursor.moveToNext());
                    cursor.close();
                }
            }
        });


        //更新数据
        binding.Update.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Uri uri = Uri.parse("content://com.example.sqlite.provider/book/"+newId);
                ContentValues values = new ContentValues();

                values.put("name","three book");
                values.put("pages",123);
                values.put("price",20.5);

                getContentResolver().update(uri,values,null,null);

            }
        });

        //删除数据
        binding.Delete.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                Uri uri = Uri.parse("content://com.example.sqlite.provider/book/"+newId);
                getContentResolver().delete(uri,null,null);
            }
        });
    }
}

7、权限问题

Android 11(API级别30)以及更高版本, 更改了应用查询用户已在设备上安装的其他应用以及与之交互的方式。使用 <queries> 元素,应用可以定义一组自身可访问的其他软件包。

如果您的应用以 Android 11 或更高版本为目标平台,您可能需要在应用的清单文件中添加 <queries> 元素。在 <queries> 元素中,您可以按软件包名称、intent 签名或提供程序授权指定软件包。

7.1 指定软件包名进行交互

所以现在新的应用ententprovidertest是无法访问刚刚实现ContenProvider应用名为sqlite的应用,我们需要指定sqlite的软件包名:

<queries>
    <package android:name="com.example.sqlite" />
</queries>

这样我们使用ententprovidertest,就可以操作刚刚实现ContenProvider的应用的数据库。

image-20230728100521341

7.2 在给定提供程序授权的情况下查询应用及与之交互

如果您需要查询 Content Provider 但不知道具体的软件包名称,您可以在 <provider> 元素中声明该提供程序授权,假设我们不知道sqlite应用的包名,我们只知道它包含一个内容提供者

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

我们需要在ententprovidertest应用中查询是否存在这个sqlite应用的authorities。在ententprovidertest应用的AndroidManifest中加入这段代码:

<queries>
    <provider android:authorities="com.example.sqlite.provider"/>
</queries>

这样,ententprovidertest 应用就可以在运行时查询设备上是否安装了名为 sqlite 的应用,并可以访问其内容提供者 com.example.sqlite.provider 提供的数据。

请注意,如果在单个 <queries> 元素中需要声明多个提供者授权,您可以使用以英文分号分隔的方式来列出它们,如下所示:

<queries>
    <provider android:authorities="com.example.sqlite.provider; com.example.otherprovider" />
</queries>

或者,您也可以使用多个 <provider> 元素,将它们全部放在同一个 <queries> 元素中,如下所示:

<queries>
    <provider android:authorities="com.example.sqlite.provider" />
    <provider android:authorities="com.example.otherprovider" />
</queries>

7.3 查询所有应用及与之交互

在极少数情况下,您的应用可能需要查询设备上的所有已安装应用或与之交互,不管这些应用包含哪些组件。为了允许您的应用看到其他所有已安装应用,系统会提供 QUERY_ALL_PACKAGES 权限。它允许应用查询设备上所有已安装应用的信息,包括它们的包名、版本。

下面列出了适合添加 QUERY_ALL_PACKAGES 权限的用例的一些示例:

  • 启动器应用
  • 无障碍应用
  • 浏览器
  • 点对点 (P2P) 共享应用
  • 设备管理应用
  • 安全应用

ententprovidertest应用的AndroidManifest中加上:

    <uses-permission android:name="android.permission.QUERY_ALL_PACKAGES"
        tools:ignore="QueryAllPackagesPermission" />

QUERY_ALL_PACKAGES 权限:

允许查询设备上的任何正常应用程序,无论清单声明如何。

防护级别:普通

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

摸鱼小小虫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值