• Android07——ContentProvider


    跨程序共享数据——ContentProvider


    可以让其他程序进行二次开发的数据都是可以共享的。包括通讯录、短信、媒体库等程序都实现了跨程序数据共享的功能,而使用的技术当然就是contentprovider。

    contentprovider主要用于不同的应用程序之间实现数据共享的功能,它提供了一套完整的机制,允许一个程序访问另一个程序中的数据,同时还能保证被访问数据的安全性。

    • 文件存储、sharedpreferences存储中是两种全局可读写模式,
    • contentprovider可以选择只对那一部分数据进行共享,从而保证我们程序中的隐私数据不会有泄漏的风险。

    运行时权限

    Android权限机制详解

    比如,当为了要监听开机广播,我们要在AndroidManifest.xml文件中添加了这样一句权限声明:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.ssozh.broadcastbestpractice">
        <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    </manifest>
    

    因为监听开机广播涉及了用户设备的安全,因此必须在AndroidManifest.xml中加入权限声明,否则我们的程序就会崩溃。

    用户主要在两个方面得到了保护:

    • 安装界面会给出该程序一共申请了哪些权限。从而决定是否要安装这个程序。
    • 用户可以随时在应用管理界面查看任意一个程序的权限申请情况。

    但是这样会存在”店大欺客“的问题,比如微信申请查看短信权。因为,Android开发团队在Android中加入了运行时权限功能。当然,并不是所有权限都需要在运行时申请,对于用户来说,不停地授权也很繁琐。

    Android现在将常的权限大致分为两类:

    • 普通权限:指的是哪些不会直接威胁到用户的安全和隐私的权限。对于这部分权限申请,系统会自动帮我们进行授权。比如开机广播
    • 危险权限:表示哪些可能会触及用户隐私劶对设备安全性造成影响的权限,比如获取联系人信息等。必须由用户手动授权才可以。
    • (特殊权限)

    因为权限很多,所以除了那么危险权限,剩下的大多数为危险权限:

    权限组名 权限名
    CALENDAR(日历) READ_CALENDAR 、WRITE_CALENDAR
    CALL_LOG READ_CALL_LOG、WRITE_CALL_LOG、PROCESS_OUTGOING_CALLS
    CAMERA CAMERA
    CONTACTS READ_CONTACTS、WRITE_CONTACTS、GET_ACCOUNTS
    LOCATIONS ACCESS_FINE_LOCATION、ACCESS_CORASE_LOCATION、ACCESS_BACKGROUND_LOCATION
    MICROPHONE RECORD_AUDIO
    PHONE
    SENSORS BODY_SENSORS
    ACTIVITY_RECOGNITION ACTIVITY_RECOGNITION
    SMS
    STORAGE

    如果是上面这张表的权限,就需要进行运行时权限处理,否则,只需要子啊AndroidManifest.xml文件中添加一下权限声明就可以了。另外注意、表格的每一个危险权限都属于同一个权限组。原则上,用户一旦同意了某个权限申请后,同组的其他权限也会被系统自动授权。

    在程序运行时申请权限

    首先常见一个RuntimePermissionTest项目:

    java代码:

        private final static String TAG = "MainActivity";    
    	@Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            makeCall = (Button) findViewById(R.id.make_call);
            makeCall.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // 这个try catch 是为了防止程序崩溃
                    try {
                        Log.d(TAG,"我要开始打电话了!");
                        Intent intent = new Intent(Intent.ACTION_CALL);  // 系统内置的打电话动作。而打开拨号界面是不需要声明权限的。ACTION_DIAL
                        intent.setData(Uri.parse("tel:10086"));  // data部分指定了协议是tel,号码是10086
                        startActivity(intent);
                    }catch (Exception e){
                        e.printStackTrace();
                    }
                }
            });
        }
    
    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.ssozh.runtimepermissiontest">
    
        <uses-permission android:name="android.permission.CALL_PHONE"/>
        
    

    但是这个是一个危险权限,所以其实会出现err:

    2020-12-04 19:56:40.595 18900-18900/com.ssozh.runtimepermissiontest D/MainActivity: 我要开始打电话了!
    2020-12-04 19:56:40.616 18900-18900/com.ssozh.runtimepermissiontest W/System.err:     at com.ssozh.runtimepermissiontest.MainActivity$1.onClick(MainActivity.java:29)
    

    因此,危险权限不仅要像普通权限一样在manifest里面声明,而且还要判断用户是否授权我们。

    具体流程如下:

    1. ContextCompat.checkSelfPermission()方法判断用户是否授权。第一个参数是上下文,第二个参数是具体权限名,Manifest.permission.CALL_PHONE

    2. 如果没有,需要调用ActivityCompat.requestPermissions()方法来向用户申请授权。三个参数

      1. Activity实例
      2. String数组,我们要把申请的权限名放入数组即可
      3. 请求码
      ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.CALL_PHONE}, 1);
      
    3. 调用完ActivityCompat.requestPermissions()方法后,系统会弹出一个权限申请框。无论用户是同意还是拒绝都会回调到onRequestPermission()方法中,而授权则会封装在grantResults参数当中,允许是[0]

    4. 注意在对话框中选择允许后,会默认永久允许这个操作,下次就不会再找你要权限了!想要关闭就需要:应用->应用管理->appName->关闭权限

    private Button makeCall;
    private final static String TAG = "MainActivity";
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        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) {
                    Log.d(TAG,"requestPermissions");
                    ActivityCompat.requestPermissions(MainActivity.this,new String[]{Manifest.permission.CALL_PHONE}, 1);
                }else {
                    Log.d(TAG,"beginCall");
    
                    call();
                }
            }
        });
    }
    private void call() {
        try{
            Intent intent = new Intent(Intent.ACTION_CALL);
            intent.setData(Uri.parse( "tel:10086"));
            startActivity(intent);
        }catch (Exception e){
            e.printStackTrace();
        }
    }
    
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
        Log.d(TAG, Arrays.toString(grantResults));  // [0]始终允许是0,如何添加仅一次允许
        switch (requestCode) {
            case 1:
                if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                    call();
                } else {
                    Toast.makeText(this, "你不让我打电话5555", Toast.LENGTH_SHORT).show();
                }
                break;
            default:
        }
    }
    

    访问其他程序中的数据

    contentprovider的用法一般有两种,一种是使用现有的contentprovider来读取和操作相应程序中的数据,另一种是创造自己的contentprovider给我们程序的数据提供外部访问的接口。

    ContentResolver的基本用法

    对于一个应用程序来说,如果想要访问contentprovider中共享的数据,就一定要借助contentresolver类,可以通过context中的getcontentresolver()方法获取该类的实例。可以通过context中的getcontentresolver()方法获取该类的实例。contentResolver中提供了RUCD操作的相关方法。

    不同于SQLiteDatabase,contentResolver中的CRUD是不接受表名参数的,而是使用了一个Uri参数代替,这个参数称为内容URI。内容URI给contentProvider中的数据简历了唯一标识符,它由两部分组成:authority和path。

    • authority:是用于对不同的应用程序做区别的,为了避免冲突,会采用应用包名的方式进行命名:包名.provider
    • path则是用于对统一应用程序不同的表做区分的。如果某个数据库中存在两张表table1和table2,path就是/table1和/table2。

    标准格式如下:

    content://com.ssozh.app.provider/table1
    content://com.ssozh.app.provider/table2
    

    另外通配符有:

    * 表示匹配任意长度的任意字符
    # 表示匹配任意长度的数字
    
    另外需要借助UriMatcher这个类实现匹配内容URI的功能。
    

    在得到了内容URI字符串之后,可以直接使用Uri.parse()进行解析:

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

    最后使用这个Uri对象查询table表中的数据了:

    Cursor cursor = getContentResovler().query(
    	uri,
        projection,
        seletion,
        seletionArgs,
        sortOrder);
    
    /**
    这些参数和SQLiteDatabase中query方法的参数很像。
    参数解释:
    query()的参数        对应的SQL部分          描述
    	uri             from table_name   
    	projection      select column1,column2
    	selection       where column=value
    	selectionArgs                       为where提供具体的值
    	orderBy         order by column1, column2
    	
    

    查询完成后返回的依然是一个Cursor对象,这时我们就可以将数据从Cursor对象中逐个读取出来即可(按行遍历)。读取代码如下:

    if(cursor !=null ){
        while (cursor.moveNext()) {
            String column1 = cursor.getString(cursor.getColumnIndex("column1"));
            int column2 = cursor.getInt(cursor.getColumnIndex("column1"));
        }
        cursor.close();
    }
    

    其他的增删改这里就不一一介绍了。大概代码如下;

    // 增
    ContentValues values = new ContentValues(); 
    value.put("colunm1", ""); 
    
    // 改
    getContentResolver().update(uri,values, "column1 = ? and column2 = ?", new String[]{"text", "1"});
    
    // 删
    getContentResolver().delete(uri, "column2 = ?", new String[]{"1"});
    

    读取系统联系人

    使用ListView展示从通讯录读取的信息:

    1. 创建一个List<String>数组存放通讯录读取的信息。
    2. 创建adapter用于ListView展示。
    3. 申请用户授权危险权限。【记得同时在manifest中添加声明】
    4. 判断是否授权后读取通讯录
      1. Uri为ContactsContract.CommonDataKinds.Phone.CONTENT_URI固定值。
      2. 因为是全部读取,所以其他都为null
      3. 读取后存入List
    5. 当List内容发生变化,通知ListView
        private ListView contactView;
        private ArrayAdapter<String> adapter;
        private List<String> contactList =new ArrayList<>();
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            contactView = (ListView) findViewById(R.id.contact_view);
            adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, contactList);
            contactView.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 {
                Uri uri = ContactsContract.CommonDataKinds.Phone.CONTENT_URI;
                cursor = getContentResolver().query(
                        uri,
                        null,
                        null,
                        null,
                        null
                );
                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));
                        contactList.add(displayName + "
    " + number);
                    }
                    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 1:
                    if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED){
                        readContacts();
                    }else {
                        Toast.makeText(this, "你拒绝了查看授权我查看通讯录的权利5555",Toast.LENGTH_SHORT).show();
                    }
                    break;
                default:
            }
        }
    

    从上面可以看出,自己能改的地方很少,自己就是对着模板抄就行了。还是很简单的!

    补充URI

    通用资源标志符(Universal Resource Identifier, 简称"URI")。Uri代表要操作的数据,Android上可用的每种资源 (图像、视频片段、网页等) 都可以用Uri来表示。从概念上来讲,URI包括URL。

    ​ Uri的通用格式为:scheme: scheme-specific-part #fragment

    通常有下面三种形式

    1. scheme://authority path ?query #fragment
    2. scheme://host:port path ?query #fragment
    3. scheme:scheme-specific-part #fragment

    第一种用于访问本地资源,这里的scheme为content或者file,resource

    Uri uri = Uri.parse("android.resource://"+context.getPackageName()+"/"+ R.raw.xxx);
    

    第二种用于访问网络资源,这里的scheme通常为http

    第三种用于打电话等服务,这里的scheme通常为smsto和tel等。

    创建自己的contentprovider

    创建contentprovider的步骤

    如果想要实现跨程序共享数据的功能,可以通过粗行间一个类去继承ContentProvider的方式来实现。ContentProvider类中有6个抽象方法,我们子啊使用子类继承他的时候,需要重写。

    public class MyProvider extends ContentProvider {
    
        @Override
        public boolean onCreate() {
            return false;
        }
    
        @Nullable
        @Override
        public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
            return null;
        }
    
        @Nullable
        @Override
        public String getType(@NonNull Uri uri) {
            return null;
        }
    
        @Nullable
        @Override
        public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
            return null;
        }
    
        @Override
        public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
            return 0;
        }
    
        @Override
        public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
            return 0;
        }
    }
    
    • onCreate():初始化contentprovider的时候调用。通常这里完成对数据库的创建和升级等操作。true表示初始化成功.

      a/**
       * onCreate 就是创建一个dbHelper~
       * @return 创建成功返回true
       */
      @Override
      public boolean onCreate() {
          dbHelper = new MydatabaseHelper(getContext(), "BookStore.db",null, 3);
          return true;
      }
      
    • query():是从contentprovider中查询数据

      • 首先获取SQLiteDatabase的实例
      • 根据传入的Uri判断用户想要访问那张表。uriMatcher.match(uri)
      • 返回Cursor即可。
          @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:
                      // 查看BOOK表中的所有数据
                      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;
                      //...
              }
      
    • insert():添加数据

      • 也是根据Uri判断是在那张表添加数据

      • 在对应表中inser数据

      • 由于insert()方法要求返回一个能表示这条新增数据的URI,因此需要调用Uri.parse()将内容URI解析成URI对象。

        uriReturn =  Uri.parse("content://" + AUTHORITY + "/book/" + newBookId);
        
    • update():更新:基本同上

    • delete():删除:基本同上

    • getType():根据传入的内容URI返回响应的MIME类型。(多用途互联网邮件扩展类型)

      • 他是所有contentprovider必须提供的一个方法,用于获取uri对象所对应的MIME类型。一个内容URI对应你的MIME字符串由三部分组成
      • 必须以vnd开头
      • 如果内容URI以路径结尾,则后面接android.cursor.dir/
      • 如果内容URI以id结尾,则后面解android.cursor.item/
      • 最后接上vnd.<authority>.<path>
        @Override
        public String getType(Uri uri) {
            // at the given URI.
    //        throw new UnsupportedOperationException("Not yet implemented");
            //     public static final String AUTHORITY = "com.ssozh.databasetest.provider";
            switch (uriMatcher.match(uri)){
                case BOOK_DIR:
                    return "vnd.android.cursor.dir/vnd." + AUTHORITY + ".Book";
                case BOOK_ITEM:
                    return "vnd.android.cursor.item/vnd." + AUTHORITY + ".Book";
                case CATEGORY_DIR:
                    return "vnd.android.cursor.dir/vnd." + AUTHORITY + ".Category";
                case CATEGORY_ITEM:
                    return "vnd.android.cursor.item/vnd." + AUTHORITY + ".Category";
                default:
                    break;
            }
            return null;
        }
    

    创建自己的contentprovider还是通过反键,other里面的content provider创建,他不仅给你创建文件,还帮你注册manifest。

    另外,contentprovider一定要在manifest中注册了才可以使用。

    实现跨程序数据共享

    最后再回调函数中使用contentprovider:

    // 以query为例 useUri是可以访问别的程序的db,而db则只能访问自己的数据
    @Override
    public void onClick(View v) {
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        Cursor cursor = null;
        if(useUri){
            // 使用CUR获取cursor
            Uri uri = Uri.parse("content://com.ssozh.databasetest.provider/book");
            cursor = getContentResolver().query(uri,null,null,null,null,null);
        }else {
            // 查询db表中的所有数据
            cursor = db.query("Book", null, null, null, null, null, null);
        }
    
        if(cursor!=null) {
            while (cursor.moveToNext()){
                String[] columns = new String[]{"name", "author", "pages", "price"};
                for(String column : columns){
                    printOnLog(cursor,column);
                }
    
            }
            cursor.close();
        }
    }
    
  • 相关阅读:
    ABP理论学习之Swagger UI集成
    最佳加法表达式
    洛谷 P1736 创意吃鱼法
    洛谷P1387 最大正方形
    1078 最小生成树
    判断元素是否存在
    1531 山峰 【栈的应用】
    洛谷 P2335 [SDOI2005]位图
    矿藏估价
    二分法小结
  • 原文地址:https://www.cnblogs.com/SsoZhNO-1/p/14092674.html
Copyright © 2020-2023  润新知