主頁 > 移動端開發 > 《第一行代碼:Android篇》學習筆記(七)

《第一行代碼:Android篇》學習筆記(七)

2022-05-12 08:28:47 移動端開發

本文和接下來的幾篇文章為閱讀郭霖先生所著《第一行代碼:Android(篇第2版)》的學習筆記,按照書中的內容順序進行記錄,書中的Demo本人全部都做過了,
每一章節本人都做了詳細的記錄,以下是我學習記錄(包含大量書中內容的整理和自己在學習中遇到的各種bug及解決方案),方便以后閱讀和查閱,最后,感激感激郭霖先生提供這么好的書籍,

第7章 跨程式共享資料——探究內容提供器

在上一章中我們學了Android資料持久化的技術,包括檔案存盤、SharedPreferences存盤以及資料庫存盤,使用這些持久化技術所保存的資料都只能在當前應用程式中訪問,

雖然檔案和SharedPreferences存盤中提供了MODE_WORLD_READABLE和MODE_WORLD_WRITEABLE這兩種操作模式,用于供給其他的應用程式訪問當前應用的資料,但這兩種模式在Android 4.2版本中都已被廢棄了,為什么呢?因為Android官方已經不再推薦使用這種方式來實作跨程式資料共享的功能,而是應該使用更加安全可靠的內容提供器技術,

為什么要將我們程式中的資料共享給其他程式呢?

當然,這個是要視情況而定的,比如說賬號和密碼這樣的隱私資料顯然是不能共享給其他程式的,不過一些可以讓其他程式進行二次開發的基礎性資料,我們還是可以選擇將其共享的,例如系統的電話簿程式,它的資料庫中保存了很多的聯系人資訊,如果這些資料都不允許第三方的程式進行訪問的話,恐怕很多應用的功能都要大打折扣了,除了電話簿之外,還有短信、媒體庫等程式都實作了跨程式資料共享的功能,而使用的技術當然就是內容提供器了,下面我們就來對這一技術進行深入的探討,

7.1 內容提供器簡介

內容提供器(Content Provider)主要用于在不同的應用程式之間實作資料共享的功能,它提供了一套完整的機制,允許一個程式訪問另一個程式中的資料,同時還能保證被訪資料的安全性,

目前,使用內容提供器是Android實作跨程式共享資料的標準方式,不同于檔案存盤和SharedPreferences存盤中的兩種全域可讀寫操作模式,內容提供器可以選擇只對哪一部分資料進行共享,從而保證我們程式中的隱私資料不會有泄漏的風險,

不過在正式開始學習內容提供器之前,我們需要先掌握另外一個非常重要的知識——Android運行時權限,因為待會的內容提供器示例中會使用到運行時權限的功能,當然不光是內容提供器,以后我們的開發程序中也會經常使用到運行時權限,因此你必須能夠牢牢掌握它才行,

7.2 運行時權限

Android的權限機制,從系統的第一個版本開始就已經存在了,但其實之前Android的權限機制在保護用戶安全和隱私等方面起到的作用比較有限,尤其是一些大家都離不開的常用軟體,非常容易“店大欺客”,為此,Android開發團隊在Android 6.0系統中參考了運行時權限這個功能,從而更好地保護了用戶的安全和隱私,那么本節我們就來詳細學習一下這個6.0系統中引入的新特性,

7.2.1 Android權限機制詳解

首先來回顧一下過去Android的權限機制是什么樣的,在第5章寫BroadcastTest專案的時候第一次接觸了Android權限相關的內容,當時為了要訪問系統的網路狀態以及監聽開機廣播,于是在AndroidManifest.xml檔案中添加了這樣兩句權限宣告:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.broadcasttest">
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
    ...
</manifest>

因為訪問系統的網路狀態以及監聽開機廣播涉及了用戶設備的安全性,因此必須在AndroidManifest.xml中加入權限宣告,否則我們的程式就會崩潰,

加入了這兩句權限宣告后,對于用戶來說到底有什么影響呢?為什么這樣就可以保護用戶設備的安全性了呢?

其實用戶主要在以下兩個方面得到了保護,一方面,如果用戶在低于6.0系統的設備上安裝該程式,會在安裝界面給出下圖所示的提醒,這樣用戶就可以清楚地知曉該程式一共申請了哪些權限,從而決定是否要安裝這個程式,

image

另一方面,用戶可以隨時在應用程式管理界面查看任意一個程式的權限申請情況,以此保證應用程式不會出現各種濫用權限的情況,

image

這種權限機制的設計思路其實非常簡單,就是用戶如果認可你所申請的權限,那么就會安裝你的程式,如果不認可你所申請的權限,那么拒絕安裝就可以了,

但是理想是美好的,現實卻很殘酷,因為很多我們所離不開的常用軟體普遍存在著濫用權限的情況,不管到呼叫不用得到,反正先把權限申請了再說,比如說微信所申請的權限串列如圖所示:

image

這只是微信所申請的一半左右的權限,因為權限太多一屏截不下來,其中有一些權限我并不認可,比如微信為什么要讀取我手機的短信和彩信?但是我不認可又能怎樣,難道我拒絕安裝微信?

Android開發團隊當然也意識到了這個問題,于是在6.0系統中加入了運行時權限功能,也就是說,用戶不需要在安裝軟體的時候一次性授權所有申請的權限,而是可以在軟體的使用程序中再對某一項權限申請進行授權,比如說一款相機應用在運行時申請了地理位置定位權限,就算我拒絕了這個權限,但是我應該仍然可以使用這個應用的其他功能,而不是像之前那樣直接無法安裝它,

當然,并不是所有權限都需要在運行時申請,對于用戶來說,不停地授權也很煩瑣

Android現在將所有的權限歸成了兩類,一類是普通權限,一類是危險權限,準確地講,其實還有第三類特殊權限,不過這種權限使用得很少,因此不在本書的討論范圍之內,

  • 普通權限:指的是那些不會直接威脅到用戶的安全和隱私的權限,對于這部分權限申請,系統會自動幫我們進行授權,而不需要用戶再去手動操作了,比如在BroadcastTest專案中申請的兩個權限就是普通權限,
  • 危險權限:則表示那些可能會觸及用戶隱私或者對設備安全性造成影響的權限,如獲取設備聯系人資訊、定位設備的地理位置等,對于這部分權限申請,必須要由用戶手動點擊授權才可以,否則程式就無法使用相應的功能,

但是,Android中有一共有上百種權限,我們怎么從中區分哪些是普通權限,哪些是危險權限呢?其實并沒有那么難,因為危險權限總共就那么幾個,除了危險權限之外,剩余的就都是普通權限了,下表列出了Android中所有的危險權限,一共是9組24個權限,

image

你并不需要了解表格中每個權限的作用,只要把它當成一個參照表來查看就行了,每當要使用一個權限時,可以先到這張表中來查一下,如果是屬于這張表中的權限,那么就需要進行運行時權限處理,如果不在這張表中,那么只需要在AndroidManifest.xml檔案中添加一下權限宣告就可以了,

另外注意一下,表格中每個危險權限都屬于一個權限組,我們在進行運行時權限處理時使用的是權限名,但是用戶一旦同意授權了,那么該權限所對應的權限組中所有的其他權限也會同時被授權,訪問:http://developer.android.google.cn/reference/android/Manifest.permission.html可以查看Android系統中完整的權限串列,

7.2.2 在程式運行時申請權限

新建一個RuntimePermissionTest專案,在這個專案的基礎上來學習運行時權限的使用方法,

簡單起見就使用CALL_PHONE這個權限來作為本小節中的示例,CALL_PHONE這個權限是撰寫撥打電話功能的時候需要宣告的,因為撥打電話會涉及用戶手機的資費問題,因而被列為了危險權限,

在Android 6.0系統出現之前,撥打電話功能的實作其實非常簡單,修改activity_main.xml布局檔案,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_
    android:layout_height="match_parent">

    <Button
        android:id="@+id/make_call"
        android:layout_
        android:layout_height="wrap_content"
        android:text="Make Call"/>
</LinearLayout>

在布局檔案中只是定義了一個按鈕,當點擊按鈕時就去觸發撥打電話的邏輯,接著修改MainActivity中的代碼,如下所示:

package com.zhouzhou.runtimepermissiontest;

import androidx.appcompat.app.AppCompatActivity;

import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;

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) {
                try {
                    Intent intent = new Intent(Intent.ACTION_CALL);
                    intent.setData(Uri.parse("tel:10086"));
                    startActivity(intent);
                } catch (SecurityException e) {
                    e.printStackTrace();
                }
            }
        });
    }
}

在按鈕的點擊事件中,構建了一個隱式Intent , Intent的action指定為Intent.ACTION_ CALL,這是一個系統內置的打電話的動作,然后在data部分指定了協議是tel,號碼是10086,

(在2.3.3小節中就已經見過了,當時指定的action是Intent.ACTION_DIAL,表示打開撥號界面,這個是不需要宣告權限的,而Intent.ACTION_ CALL則可以直接撥打電話,因此必須宣告權限,)

另外為了防止程式崩潰,我們將所有操作都放在了例外捕獲代碼塊當中,那么接下來修改AndroidManifest.xml檔案,在其中宣告如下權限:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.runtimepermissiontest">
    <uses-permission android:name="android.permission.CALL_PHONE"/>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.RuntimePermissionTest">
        ...
</manifest>

這樣我們就將撥打電話的功能成功實作了,并且在低于Android 6.0系統的手機上都是可以正常運行的,但是如果我們在6.0或者更高版本系統的手機上運行,點擊Make Call按鈕就沒有任何效果,這時觀察logcat中的列印日志,你會看到如圖:

image

錯誤資訊中提醒我們“Permission Denial”,可以看出,是由于權限被禁止所導致的,因為6.0及以上系統在使用危險權限時都必須進行運行時權限處理,那么下面我們就來嘗試修復這個問題,修改MainActivity中的代碼,如下所示:

package com.zhouzhou.runtimepermissiontest;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.content.Intent;

import android.content.pm.PackageManager;
import android.net.Uri;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;

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, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
                    ActivityCompat.requestPermissions(MainActivity.this,new String[]{ Manifest.permission.CALL_PHONE },1);
                } 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();
        }
    }

    @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) {
                    call();
                } else {
                    Toast.makeText(this,"You denied the permission",Toast.LENGTH_SHORT).show();
                }
                break;
            default:
        }
    }
}

上面的代碼將運行時權限的完整流程都覆寫了,下面我們來具體決議一下,

運行時權限的核心就是在程式運行程序中由用戶授權我們去執行某些危險操作,程式是不可以擅自做主去執行這些危險操作的,

因此,第一步就是要先判斷用戶是不是已經給過我們授權了,

借助的是ContextCompat.checkSelfPermission()方法,checkSelfPermission()方法接收兩個引數,第一個引數是Context,第二個引數是具體的權限名,比如打電話的權限名就是Manifest.permission.CALL_PHONE,然后我們使用方法的回傳值和PackageManager. PERMISSION_GRANTED做比較,相等就說明用戶已經授權,不等就表示用戶沒有授權,

  • 如果已經授權的話就簡單了,直接去執行撥打電話的邏輯操作就可以了,這里我們把撥打電話的邏輯封裝到了call()方法當中,
  • 如果沒有授權的話,則需要呼叫ActivityCompat. requestPermissions()方法來向用戶申請授權,requestPermissions()方法接收3個引數,第一個引數要求是Activity的實體,第二個引數是一個String陣列,我們把要申請的權限名放在陣列中即可,第三個引數是請求碼,只要是唯一值就可以了,這里傳入了1,

呼叫完了requestPermissions()方法之后,系統會彈出一個權限申請的對話框,然后用戶可以選擇同意或拒絕我們的權限申請,不論是哪種結果,最終都會回呼到onRequestPermissionsResult()方法中,而授權的結果則會封裝在grantResults引數當中,這里我們只需要判斷一下最后的授權結果,如果用戶同意的話就呼叫call()方法來撥打電話,如果用戶拒絕的話我們只能放棄操作,并且彈出一條失敗提示,

現在重新運行一下程式,并點擊Make Call按鈕,效果如圖:

image

由于用戶還沒有授權過我們撥打電話權限,因此第一次運行會彈出這樣一個權限申請的對話框,用戶可以選擇同意或者拒絕,比如說這里點擊了DENY,結果如圖:

image

由于用戶沒有同意授權,我們只能彈出一個操作失敗的提示,下面我們再次點擊Make Call按鈕,仍然會彈出權限申請的對話框,這次點擊ALLOW,結果如圖:

image

可以看到,這次我們就成功進入到撥打電話界面了,并且由于用戶已經完成了授權操作,之后再點擊Make Call按鈕就不會再彈出權限申請對話框了,而是可以直接撥打電話

用戶隨時都可以將授予程式的危險權限進行關閉,進入Settings → Apps→ RuntimePermissionTest → Permissions,在這里我們就可以對任何授予過的危險權限進行關閉了,界面如圖:

image

7.3 訪問其他程式中的資料

內容提供器的用法一般有兩種:

  • 一種是使用現有的內容提供器來讀取和操作相應程式中的資料,
  • 一種是創建自己的內容提供器給我們程式的資料提供外部訪問介面,

使用現有的內容提供器:如果一個應用程式通過內容提供器對其資料提供了外部訪問介面,那么任何其他的應用程式就都可以對這部分資料進行訪問,

Android系統中自帶的電話簿、短信、媒體庫等程式都提供了類似的訪問介面,這就使得第三方應用程式可以充分地利用這部分資料來實作更好的功能,下面我們就來看一看,內容提供器到底是如何使用的,

7.3.1 ContentResolver的基本用法

對于每一個應用程式來說,如果想要訪問內容提供器中共享的資料,就一定要借助ContentResolver類,可以通過Context中的getContentResolver()方法獲取到該類的實體,

ContentResolver中提供了一系列的方法用于對資料進行CRUD操作,其中insert()方法用于添加資料,update()方法用于更新資料,delete()方法用于洗掉資料,query()方法用于查詢資料,

不同于SQLiteDatabase, ContentResolver中的增刪改查方法都是不接收表名引數的,而是使用一個Uri引數代替,這個引數被稱為內容URI,

內容URI給內容提供器中的資料建立了唯一識別符號,它主要由兩部分組成:authority和path,

  • authority是用于對不同的應用程式做區分的,一般為了避免沖突,都會采用程式包名的方式來進行命名,比如某個程式的包名是com.example.app,那么該程式對應的authority就可以命名為com.example.app.provider,
  • path則是用于對同一應用程式中不同的表做區分的,通常都會添加到authority的后面,比如某個程式的資料庫里存在兩張表:table1和table2,這時就可以將path分別命名為/table1和/table2,然后把authority和path進行組合,內容URI就變成了com.example.app.provider/table1和com.example.app.provider/table2,

不過,目前還很難辨認出這兩個字串就是兩個內容URI,我們還需要在字串的頭部加上協議宣告,因此,內容URI最標準的格式寫法如下:

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

內容URI可以非常清楚地表達出我們想要訪問哪個程式中哪張表里的資料,也正是因此,ContentResolver中的增刪改查方法才都接收Uri物件作為引數,因為如果使用表名的話,系統將無法得知我們期望訪問的是哪個應用程式里的表,在得到了內容URI字串之后,我們還需要將它決議成Uri物件才可以作為引數傳入,決議的方法也相當簡單,代碼如下所示:

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

只需要呼叫Uri.parse()方法,就可以將內容URI字串決議成Uri物件了,現在我們就可以使用這個Uri物件來查詢table1表中的資料了,代碼如下所示:

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

這些引數和SQLiteDatabase中query()方法里的引數很像,但總體來說要簡單一些,畢竟這是在訪問其他程式中的資料,沒必要構建過于復雜的查詢陳述句,下表對使用到的這部分引數進行了詳細的解釋,

image

查詢完成后回傳的仍然是一個Cursor物件,這時就可以將資料從Cursor物件中逐個讀取出來了,讀取的思路仍然是通過移動游標的位置來遍歷Cursor的所有行,然后再取出每一行中相應列的資料,代碼如下所示:

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

掌握了最難的查詢操作,剩下的增加、修改、洗掉操作就更不在話下了,我們先來看看如何向table1表中添加一條資料,代碼如下所示:

ContentValues values = new ContentValues();
values.put("column1","text");
values.put("column2",1);
getContentResolver().insert(uri,values);

可以看到,仍然是將待添加的資料組裝到ContentValues中,然后呼叫ContentResolver的insert()方法,將Uri和ContentValues作為引數傳入即可,現在如果我們想要更新這條新添加的資料,把column1的值清空,可以借助ContentResolver的update()方法實作,代碼如下所示:

ContentValues values = new ContentValues();
values.put("column1","");
getContentResolver().update(uri,values,"column1 = ? and column2 = ? ",new String[] {"text","1"});

注意上述代碼使用了selection和selectionArgs引數來對想要更新的資料進行約束,以防止所有的行都會受影響,最后,可以呼叫ContentResolver的delete()方法將這條資料洗掉掉,代碼如下所示:

getContentResolver().delete(uri,"column2 = ? ",new String[] {"1"});

到這里為止,我們就把ContentResolver中的增刪改查方法全部學完了,那么接下來,就利用目前所學,看一看如何讀取系統電話簿中的聯系人資訊,

7.3.2 讀取系統聯系人

由于我們之前一直使用的都是模擬器,電話簿里面并沒有聯系人存在,所以現在需要自己手動添加幾個,以便稍后進行讀取,打開電話簿程式,界面如圖:

image

目前電話簿里是沒有任何聯系人的,我們可以通過點擊ADD ACONTACT按鈕來對聯系人進行創建,這里就先創建兩個聯系人吧,分別填入他們的姓名和手機號:

image

這樣準備作業就做好了,現在新建一個ContactsTest專案,首先還是來撰寫一下布局檔案,這里我們希望讀取出來的聯系人資訊能夠在ListView中顯示,因此,修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">

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

LinearLayout里就只放置了一個ListView,這里使用ListView而不是RecyclerView,是因為我們要將關注的重點放在讀取系統聯系人上面,如果使用RecyclerView的話,代碼偏多,會容易讓我們找不著重點,接著修改MainActivity中的代碼,如下所示:

package com.zhouzhou.contactstest;

import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.os.Bundle;
import android.provider.ContactsContract;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Toast;

import java.util.ArrayList;
import java.util.List;

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);
            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, @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,"You denied the permission",Toast.LENGTH_SHORT).show();
                }
                break;
            default:
        }
    }
}

在onCreate()方法中,我們首先獲取了ListView控制元件的實體,并給它設定好了配接器,然后開始呼叫運行時權限的處理邏輯,因為READ_CONTACTS權限是屬于危險權限的,這里在用戶授權之后呼叫readContacts()方法來讀取系統聯系人資訊,

下面重點看一下readContacts()方法,這里使用了ContentResolver的query()方法來查詢系統的聯系人資料,

不過,傳入的Uri引數為什么沒有呼叫Uri.parse()方法去決議一個內容URI字串呢?

這是因為ContactsContract.CommonDataKinds.Phone類已經幫我們做好了封裝,提供了一個CONTENT_URI常量,而這個常量就是使用Uri.parse()方法決議出來的結果,接著我們對Cursor物件進行遍歷,將聯系人姓名和手機號這些資料逐個取出,聯系人姓名這一列對應的常量是ContactsContract.CommonDataKinds. Phone.DISPLAY_NAME,聯系人手機號這一列對應的常量是ContactsContract.CommonData-Kinds.Phone.NUMBER,兩個資料都取出之后,將它們進行拼接,并且在中間加上換行符,然后將拼接后的資料添加到ListView的資料源里,并通知重繪一下ListView,最后千萬不要忘記將Cursor物件關閉掉,

讀取系統聯系人的權限千萬不能忘記宣告,修改AndroidManifest.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.contactstest">
    <uses-permission android:name="android.permission.READ_CONTACTS"/>
    ...
</manifest>

加入了android.permission.READ_CONTACTS權限,這樣我們的程式就可以訪問到系統的聯系人資料了,來運行一下程式吧,效果如圖:

image

首先彈出了申請訪問聯系人權限的對話框,我們點擊DENY,然后結果如圖:

image

點擊ALLOW,剛剛添加的兩個聯系人的資料都成功讀取出來了!這說明跨程式訪問資料的功能確實是實作了,結果如圖:

image

7.4 創建自己的內容提供器

上一節當中,思路還是非常簡單的,只需要獲取到該應用程式的內容URI,然后借助ContentResolver進行CRUD操作就可以了,

那些提供外部訪問介面的應用程式都是如何實作這種功能的呢?它們又是怎樣保證資料的安全性,使得隱私資料不會泄漏出去?學習完本節的知識后,疑惑將會被一一解開,

7.4.1 創建內容提供器的步驟

前面已經提到過,如果想要實作跨程式共享資料的功能,官方推薦的方式就是使用內容提供器,可以通過新建一個類去繼承ContentProvider的方式來創建一個自己的內容提供器,ContentProvider類中有6個抽象方法,我們在使用子類繼承它的時候,需要將這6個方法全部重寫,新建MyProvider繼承自ContentProvider,代碼如下所示:

package com.zhouzhou.contactstest;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class MyProvider extends ContentProvider {
    @Override
    public boolean onCreate() {
        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] strings, @Nullable String s, @Nullable String[] strings1, @Nullable String s1) {
        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;
    }
}
  1. onCreate()

初始化內容提供器的時候呼叫,通常會在這里完成對資料庫的創建和升級等操作,回傳true表示內容提供器初始化成功,回傳false則表示失敗,

  1. query()

從內容提供器中查詢資料,使用uri引數來確定查詢哪張表,projection引數用于確定查詢哪些列,selection和selectionArgs引數用于約束查詢哪些行,sortOrder引數用于對結果進行排序,查詢的結果存放在Cursor物件中回傳,

  1. insert()

向內容提供器中添加一條資料,使用uri引數來確定要添加到的表,待添加的資料保存在values引數中,添加完成后,回傳一個用于表示這條新記錄的URI,

  1. update()

更新內容提供器中已有的資料,使用uri引數來確定更新哪一張表中的資料,新資料保存在values引數中,selection和selectionArgs引數用于約束更新哪些行,受影響的行數將作為回傳值回傳,

  1. delete()

從內容提供器中洗掉資料,使用uri引數來確定洗掉哪一張表中的資料,selection和selectionArgs引數用于約束洗掉哪些行,被洗掉的行數將作為回傳值回傳,

  1. getType()

根據傳入的內容URI來回傳相應的MIME型別,

可以看到,幾乎每一個方法都會帶有Uri這個引數,這個引數也正是呼叫ContentResolver的增刪改查方法時傳遞過來的,

而現在,我們需要對傳入的Uri引數進行決議,從中分析出呼叫方期望訪問的表和資料,回顧一下,一個標準的內容URI寫法是這樣的:

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

這就表示呼叫方期望訪問的是com.example.app這個應用的table1表中的資料,除此之外,我們還可以在這個內容URI的后面加上一個id,如下所示:

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

內容URI的格式主要就只有以上兩種,以路徑結尾就表示期望訪問該表中所有的資料,以id結尾就表示期望訪問該表中擁有相應id的資料,我們可以使用通配符的方式來分別匹配這兩種格式的內容URI,規則如下,

  • 星號(*) 表示匹配任意長度的任意字符,
  • 井號(#) 表示匹配任意長度的數字,

所以,一個能夠匹配任意表的內容URI格式就可以寫成:

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

而一個能夠匹配table1表中任意一行資料的內容URI格式就可以寫成:

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

接著,再借助UriMatcher這個類就可以輕松地實作匹配內容URI的功能,UriMatcher中提供了一個addURI()方法,這個方法接收3個引數,可以分別把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);
        uriMatcher.addURI("com.zhouzhou.app.provider","table1",TABLE1_DIR);
        uriMatcher.addURI("com.zhouzhou.app.provider","table1/#",TABLE1_ITEM);
        uriMatcher.addURI("com.zhouzhou.app.provider","table2",TABLE2_DIR);
        uriMatcher.addURI("com.zhouzhou.app.provider","table2/#",TABLE2_ITEM);
    }
    ...
    @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:
                break;
        }
      ...
    }
    ...
}

可以看到,MyProvider中新增了4個整型常量,其中TABLE1_DIR表示訪問table1表中的所有資料,TABLE1_ITEM表示訪問table1表中的單條資料,TABLE2_DIR表示訪問table2表中的所有資料,TABLE2_ITEM表示訪問table2表中的單條資料,

接著在靜態代碼塊里我們創建了UriMatcher的實體,并呼叫addURI()方法,將期望匹配的內容URI格式傳遞進去,注意這里傳入的路徑引數是可以使用通配符的,然后,當query()方法被呼叫的時候,就會通過UriMatcher的match()方法對傳入的Uri物件進行匹配,如果發現UriMatcher中某個內容URI格式成功匹配了該Uri物件,則會回傳相應的自定義代碼,然后我們就可以判斷出呼叫方期望訪問的到底是什么資料了,

上述代碼只是以query()方法為例做了個示范,其實insert()、update()、delete()這幾個方法的實作也是差不多的,它們都會攜帶Uri這個引數,然后同樣利用UriMatcher的match()方法判斷出呼叫方期望訪問的是哪張表,再對該表中的資料進行相應的操作就可以了,

即getType()方法,它是所有的內容提供器都必須提供的一個方法,用于獲取Uri物件所對應的MIME型別,一個內容URI所對應的MIME字串主要由3部分組成,Android對這3個部分做了如下格式規定,

? 必須以vnd開頭,

? 如果內容URI以路徑結尾,則后接android.cursor.dir/,如果內容URI以id結尾,則后接android.cursor.item/,

? 最后接上vnd..

所以,對于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

可以繼續完善MyProvider中的內容了,這次來實作getType()方法中的邏輯,代碼如下所示:

...
    public class MyProvider extends ContentProvider {
    ...
    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        switch (uriMatcher.match(uri)) {
            case TABLE1_DIR:
                return "vnd.android.cursor.dir/vnd.com.zhouzhou.app.provider.table1";
            case TABLE1_ITEM:
                return "vnd.android.cursor.item/vnd.com.zhouzhou.app.provider.table1";
            case TABLE2_DIR:
                return "vnd.android.cursor.dir/vnd.com.zhouzhou.app.provider.table2";
            case TABLE2_ITEM:
                return "vnd.android.cursor.item/vnd.com.zhouzhou.app.provider.table2";
            default:
                break;
        }
        return null;
    }
}

到這里,一個完整的內容提供器就創建完成了,現在任何一個應用程式都可以使用ContentResolver來訪問我們程式中的資料,

那么,如何才能保證隱私資料不會泄漏出去呢?

內容提供器的良好機制使得這個問題在不知不覺中已經被解決了,因為所有的CRUD操作都一定要匹配到相應的內容URI格式才能進行的,而我們不可能向UriMatcher中添加隱私資料的URI,所以這部分資料根本無法被外部程式訪問到,安全問題也就不存在了,實戰一下,真正體驗一回跨程式資料共享的功能,

7.4.2 實作跨程式資料共享

  1. 在上一章中DatabaseTest專案的基礎上繼續開發,通過內容提供器來給它加入外部訪問介面,
  2. 打開DatabaseTest專案,首先將MyDatabaseHelper中使用Toast彈出創建資料庫成功的提示去除掉,因為跨程式訪問時我們不能直接使用Toast,
  3. 然后,創建一個內容提供器,右擊com.zhouzhou.databasetest包→New→Other→Content Provider,

會彈出如圖所示的視窗:

image

  1. 將內容提供器命名為DatabaseProvider,
  2. authority指定為com.zhouzhou. databasetest.provider,
  3. Exported屬性表示是否允許外部程式訪問我們的內容提供器,
  4. Enabled屬性表示是否啟用這個內容提供器,將兩個屬性都勾中,點擊Finish完成創建,

接著我們修改DatabaseProvider中的代碼,如下所示:

package com.zhouzhou.databasetest;

import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.net.Uri;

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.zhouzhou.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);
    }
    public DatabaseProvider() {
    }

    @Override
    public int delete(Uri uri, String selection, String[] selectionArgs) {
        // Implement this to handle requests to delete one or more rows.
        //throw new UnsupportedOperationException("Not yet implemented");
        //洗掉資料
        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;
            case CATEGORY_DIR:
                deleteRows = db.delete("Category",selection,selectionArgs);
                break;
            case CATEGORY_ITEM:
                String categoryId = uri.getPathSegments().get(1);
                deleteRows = db.delete("Category","id = ?",new String[] { categoryId });
                break;
            default:
                break;
        }
        return  deleteRows;
    }

    @Override
    public String getType(Uri uri) {
        // TODO: Implement this to handle requests for the MIME type of the data
        // at the given URI.
        //throw new UnsupportedOperationException("Not yet implemented");
        switch (uriMatcher.match(uri)) {
            case BOOK_DIR:
                return "vnd.android.cursor.dir/vnd.com.zhouzhou.databasetest.provider.book";
            case BOOK_ITEM:
                return "vnd.android.cursor.item/vnd.com.zhouzhou.database.provider.book";
            case CATEGORY_DIR:
                return "vnd.android.cursor.dir/vnd.com.zhouzhou.databasetest.provider.category";
            case CATEGORY_ITEM:
                return "vnd.android.cursor.item/vnd.com.zhouzhou.databasetest.provider.category";
        }
        return null;
    }

    @Override
    public Uri insert(Uri uri, ContentValues values) {
        // TODO: Implement this to handle requests to insert a new row.
        //throw new UnsupportedOperationException("Not yet implemented");
        //添加資料
        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;
            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() {
        // TODO: Implement this to initialize your content provider on startup.
        dbHelper = new MyDatabaseHelper(getContext(),"BookStore.db",null,2);
        return true;
    }

    @Override
    public Cursor query(Uri uri, String[] projection, String selection,
                        String[] selectionArgs, String sortOrder) {
        // TODO: Implement this to handle query requests from clients.
        //throw new UnsupportedOperationException("Not yet implemented");
        //查詢資料
        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:
                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) {
        // TODO: Implement this to handle requests to update one or more rows.
        //throw new UnsupportedOperationException("Not yet implemented");
        //更新資料
        SQLiteDatabase db = dbHelper.getWritableDatabase();
        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;
    }
}

首先在類的一開始,同樣是定義了4個常量,分別用于表示訪問Book表中的所有資料、訪問Book表中的單條資料訪問Category表中的所有資料訪問Category表中的單條資料

然后,在靜態代碼塊里對UriMatcher進行了初始化操作,將期望匹配的幾種URI格式添加了進去,

  • onCreate()方法,這個方法的代碼很短,就是創建了一個MyDatabaseHelper的實體,然后回傳true表示內容提供器初始化成功,這時資料庫就已經完成了創建或升級操作,
  • query()方法,在這個方法中先獲取到了SQLiteDatabase的實體,然后根據傳入的Uri引數判斷出用戶想要訪問哪張表,再呼叫SQLiteDatabase的query()進行查詢,并將Cursor物件回傳就好了,

注意,當訪問單條資料的時候有一個細節,這里呼叫了Uri物件的getPathSegments()方法,它會將內容URI權限之后的部分以“/”符號進行分割,并把分割后的結果放入到一個字串串列中,那這個串列的第0個位置存放的就是路徑,第1個位置存放的就是id了,得到了id之后,再通過selection和selectionArgs引數進行約束,就實作了查詢單條資料的功能,

  • insert()方法,同樣它也是先獲取到了SQLiteDatabase的實體,然后根據傳入的Uri引數判斷出用戶想要往哪張表里添加資料,再呼叫SQLiteDatabase的insert()方法進行添加就可以了,

注意insert()方法,要求回傳一個能夠表示這條新增資料的URI,所以我們還需要呼叫Uri.parse()方法來將一個內容URI決議成Uri物件,當然這個內容URI是以新增資料的id結尾的,

  • update()方法,也是先獲取SQLiteDatabase的實體,然后根據傳入的Uri引數判斷出用戶想要更新哪張表里的資料,再呼叫SQLiteDatabase的update()方法進行更新就好了,受影響的行數將作為回傳值回傳,
  • delete()方法,仍然是先獲取到SQLiteDatabase的實體,然后根據傳入的Uri引數判斷出用戶想要洗掉哪張表里的資料,再呼叫SQLiteDatabase的delete()方法進行洗掉就好了,被洗掉的行數將作為回傳值回傳,
  • getType()方法,這個方法中的代碼完全是按照上一節中介紹的格式規則撰寫的,這樣我們就將內容提供器中的代碼全部撰寫完了,

注意,內容提供器一定要在AndroidManifest.xml檔案中注冊才可以使用,不過幸運的是,由于我們是使用Android Studio的快捷方式創建的內容提供器,因此注冊這一步已經被自動完成了,打開AndroidManifest.xml檔案瞧一瞧,代碼如下所示:

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

image

可以看到,<application>標簽內出現了一個新的標簽<provider>,使用它來對DatabaseProvider這個內容提供器進行注冊,android:name屬性指定了DatabaseProvider的類名,android:authorities屬性指定了DatabaseProvider的authority,而enabled和exported屬性則是根據剛才勾選的狀態自動生成的,這里表示允許DatabaseProvider被其他應用程式進行訪問,

現在DatabaseTest這個專案就已經擁有了跨程式共享資料的功能了,

  • 首先,需要將DatabaseTest程式從模擬器中洗掉掉,以防止上一章中產生的遺留資料對我們造成干擾,
  • 然后,運行一下專案,將DatabaseTest程式重新安裝在模擬器上了,
  • 接著,關閉掉DatabaseTest這個專案,并創建一個新專案ProviderTest,我們就將通過這個程式去訪問DatabaseTest中的資料,

還是先來撰寫一下布局檔案吧,修改activity_main.xml中的代碼,如下所示:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_
    android:layout_height="match_parent">

    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/add_data"
        android:text="Add To Book"/>
    
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/query_data"
        android:text="Query From Book"/>
    
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/update_data"
        android:text="Update Book"/>
    
    <Button
        android:layout_
        android:layout_height="wrap_content"
        android:id="@+id/delete_data"
        android:text="Delete From Book"/>
</LinearLayout>

布局檔案很簡單,里面放置了4個按鈕,分別用于添加、查詢、修改和洗掉資料,然后修改MainActivity中的代碼,如下所示:

package com.zhouzhou.providertest;

import androidx.appcompat.app.AppCompatActivity;

import android.annotation.SuppressLint;
import android.content.ContentValues;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;

public class MainActivity extends AppCompatActivity {
    private String newId;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button addData = https://www.cnblogs.com/1693977889zz/p/(Button) findViewById(R.id.add_data);
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //添加資料
                Uri uri = Uri.parse("content://com.zhouzhou.databasetest.provider/book");
                ContentValues values = new ContentValues();
                values.put("name","A Clash of Kings");
                values.put("author","George Martin");
                values.put("pages","1024");
                values.put("price","22.85");
                Uri newUri = getContentResolver().insert(uri,values);
                newId = newUri.getPathSegments().get(1);
            }
        });
        Button queryData = https://www.cnblogs.com/1693977889zz/p/(Button) findViewById(R.id.query_data);
        queryData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //查詢資料
                Uri uri = Uri.parse("content://com.zhouzhou.databasetest.provider/book");
                Cursor cursor = getContentResolver().query(uri,null,null,null,null);
                if (cursor != null) {
                    while (cursor.moveToNext()) {
                        @SuppressLint("Range") String name = cursor.getString(cursor.getColumnIndex("name"));
                        @SuppressLint("Range") String author = cursor.getString(cursor.getColumnIndex("author"));
                        @SuppressLint("Range") int pages = cursor.getInt(cursor.getColumnIndex("pages"));
                        @SuppressLint("Range") 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 = https://www.cnblogs.com/1693977889zz/p/(Button) findViewById(R.id.update_data);
        updateData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //更新資料
                Uri uri = Uri.parse("content://com.zhouzhou.databasetest.provider./book" + newId);
                ContentValues values = new ContentValues();
                values.put("name","A Storm of Swords");
                values.put("pages",1216);
                values.put("price","77.77");
                getContentResolver().update(uri,values,null,null);
            }
        });
        Button deleteData = https://www.cnblogs.com/1693977889zz/p/(Button) findViewById(R.id.delete_data);
        deleteData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //洗掉資料
                Uri uri = Uri.parse("content://com.zhouzhou.databasetest.book" + newId);
                getContentResolver().delete(uri,null,null);
            }
        });
    }
}

分別在這4個按鈕的點擊事件里面處理了增刪改查的邏輯,

添加資料的時候,首先呼叫了Uri.parse()方法將一個內容URI決議成Uri物件,然后把要添加的資料都存放到ContentValues物件中,接著呼叫ContentResolver的insert()方法執行添加操作就可以了,注意insert()方法會回傳一個Uri物件,這個物件中包含了新增資料的id,我們通過getPathSegments()方法將這個id取出,稍后會用到它,

查詢資料的時候,同樣是呼叫了Uri.parse()方法將一個內容URI決議成Uri物件,然后呼叫ContentResolver的query()方法去查詢資料,查詢的結果當然還是存放在Cursor物件中的,之后對Cursor進行遍歷,從中取出查詢結果,并一一列印出來,

更新資料的時候,也是先將內容URI決議成Uri物件,然后把想要更新的資料存放到ContentValues物件中,再呼叫ContentResolver的update()方法執行更新操作就可以了,注意這里我們為了不想讓Book表中的其他行受到影響,在呼叫Uri.parse()方法時,給內容URI的尾部增加了一個id,而這個id正是添加資料時所回傳的,這就表示我們只希望更新剛剛添加的那條資料,Book表中的其他行都不會受影響,

洗掉資料的時候,也是使用同樣的方法決議了一個以id結尾的內容URI,然后呼叫ContentResolver的delete()方法執行洗掉操作就可以了,由于我們在內容URI里指定了一個id,因此只會刪掉擁有相應id的那行資料,Book表中的其他資料都不會受影響,

現在,運行一下ProviderTest專案,出現幾大問題:

  1. 日志報錯:Failed to find provider info for com.zhouzhou.databasetest.provider,Unknown URL content:...

    原因是:因為測驗用的模擬器的SDK是API 30的,該版本(Android 11)的更新中,改變了當前應用于本機其他應用進行互動的方式,由此若按照一些教程的例程去學習,會出現以上的一些訪問權限問題,

  2. 啟動兩個App后,出現螢屏閃爍/黑屏

解決:參考博客:https://blog.csdn.net/qq_34727886/article/details/110951082,

洗掉所有 device 模擬器,關閉AS再重新來過(解決閃爍/黑屏/螢屏有點呆/無法卸載應用的問題)),并且分別在DatabaseTest專案的AndroidManifest.xml檔案的manifest中加入如下代碼:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.databasetest">
    <uses-permission android:name="android.permission.INTERNET" />
    <permission
        android:name="DatabaseProvider._READ_PERMISSION"
        android:protectionLevel="normal" />
    <permission
        android:name="DatabaseProvider._WRITE_PERMISSION"
        android:protectionLevel="normal" />

    <application
        ...
    </application>

</manifest>

在ProviderTest專案的AndroidManifest.xml檔案中的manifest中加入如下代碼:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.zhouzhou.providertest">
    <uses-permission android:name="DatabaseProvider._READ_PERMISSION" />
    <uses-permission android:name="DatabaseProvider._WRITE_PERMISSION" />

    <queries>
        <package android:name="com.zhouzhou.databasetest" />

        <!-- 也可以單獨指定provider -->
        <!--<provider android:authorities="com.zhouzhou.databasetest.provider" />-->
    </queries>
    <application
        ...
    </application>
</manifest>

修改之后再次運行,完全OK,點擊一下Add To Book按鈕,此時資料就應該已經添加到DatabaseTest程式的資料庫中了,我們可以通過點擊Query From Book按鈕來檢查一下,列印日志如圖:

image

然后,點擊一下Update Book按鈕來更新資料,再點擊一下Query From Book按鈕進行檢查,結果如圖:

image

最后,點擊Delete From Book按鈕洗掉資料,測驗Ok,

由此可以看出,我們的跨程式共享資料功能已經成功實作了!經過測驗,單開ProviderTest程式取資料完全OK, 現在不僅是ProviderTest程式,任何一個程式都可以輕松訪問DatabaseTest中的資料,而且還絲毫不用擔心隱私資料泄漏的問題,

7.5 Git時間——版本控制工具進階

在上一次學習了關于Git最基本的用法,包括安裝Git、創建代碼倉庫,以及提交本地代碼,

本節中將要學習更多的使用技巧,準備作業,給一個專案創建代碼倉庫,這里就選擇在ProviderTest專案中創建吧,打開Git Bash,進入到這個專案的根目錄下面,然后執行git init命令,如圖:

image

這樣準備作業就已經完成了,讓我們繼續開始Git之旅吧,

7.5.1 忽略檔案

代碼倉庫現在已經創建好了,接下來應該去提交ProviderTest專案中的代碼,在提交之前你也許應該思考一下,是不是所有的檔案都需要加入到版本控制當中呢?

在第1章介紹Android專案結構的時候有提到過,build目錄下的檔案都是編譯專案時自動生成的,我們不應該將這部分檔案添加到版本控制當中,那么如何才能實作這樣的效果呢?

Git提供了一種可配性很強的機制來允許用戶將指定的檔案或目錄排除在版本控制之外,它會檢查代碼倉庫的目錄下是否存在一個名為.gitignore的檔案,如果存在的話,就去一行行讀取這個檔案中的內容,并把每一行指定的檔案或目錄排除在版本控制之外,注意.gitignore中指定的檔案或目錄是可以使用“*”通配符的,

我們并不需要自己去創建.gitignore檔案,Android Studio在創建專案的時候會自動幫我們創建出兩個.gitignore檔案,一個在根目錄下面,一個在app模塊下面,首先看一下根目錄下面的.gitignore檔案,如圖:

image

這是Android Studio自動生成的一些默認配置,通常情況下,這部分內容都是不用添加到版本控制當中的,除了*.iml表示指定任意以.iml結尾的檔案,其他都是指定的具體的檔案名或者目錄名,上面配置中的所有內容都不會被添加到版本控制當中,因為基本都是一些由IDE自動生成的配置,

再來看一下app模塊下面的.gitignore檔案,這個就簡單多了,如圖:

image

由于app模塊下面基本都是我們撰寫的代碼,因此默認情況下只有其中的build目錄不會被添加到版本控制當中,當然,我們完全可以對以上兩個檔案進行任意地修改,來滿足特定的需求,比如說,app模塊下面的所有測驗檔案都只是給我自己使用的,我并不想把它們添加到版本控制中,那么就可以這樣修改app/.gitignore檔案中的內容:

/build
/src/test
/src/androidTest

只需添加這樣兩行配置,因為所有的測驗檔案都是放在這兩個目錄下的,現在我們可以提交代碼了,先使用add命令將所有的檔案進行添加,如下所示:

git add .

然后執行commit命令完成提交,如下所示:

git commit -m "First commit."

7.5.2 查看修改內容

在進行了第一次代碼提交之后,我們后面還可能會對專案不斷地進行維護或添加新功能等,比較理想的情況是每當完成了一小塊功能,就執行一次提交,

但是如果某個功能牽扯到的代碼比較多,有可能寫到后面的時候我們就已經忘記前面修改了什么東西了,遇到這種情況時不用擔心,Git全都幫你記著呢!如何使用Git來查看自上次提交后檔案修改的內容,只需要使用status命令就可以了,在專案的根目錄下輸入如下命令:

git status

然后,Git會提示目前專案中沒有任何可提交的檔案,因為我們剛剛才提交過嘛,現在對ProviderTest專案中的代碼稍做一下改動,修改MainActivity中的代碼,如下所示:

public class MainActivity extends AppCompatActivity {
    ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        addData.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                //添加資料
                ...
                values.put("price","11.11");
                ...
            }
        });
        ...
    }
}

這里僅僅是在添加資料的時候,將書的價格由22.85改成了11.11,然后重新輸入git status命令,這次結果如圖:

image

可以看到,Git提醒我們MainActivity.java這個檔案已經發生了更改,那么如何才能看到更改的內容呢?這就需要借助diff命令了,用法如下所示:

git diff

image

這樣可以查看到所有檔案的更改內容,如果你只想查看MainActivity.java這個檔案的更改內容,可以使用如下命令:

git diff app/src/main/java/com/zhouzhou/providertest/MainActivity.java

圖片和上圖一樣,因為剛剛測驗,只改了MainActivity.java這個檔案的內容,

其中,減號代表洗掉的部分,加號代表添加的部分,從圖中我們就可以明顯地看出,書的價格由22.85被修改成了11.11,

7.5.3 撤銷未提交的修改

有時候我們的代碼可能會寫得過于草率,以至于原本正常的功能,結果反倒被我們改出了問題,遇到這種情況時也不用著急,因為只要代碼還未提交,所有修改的內容都是可以撤銷的

比如,在上一小節中我們修改了MainActivity里一本書的價格,現在如果想要撤銷這個修改就可以使用checkout命令,用法如下所示:

git checkout app/src/main/java/com/zhouzhou/providertest/MainActivity.java

執行了這個命令之后,我們對MainActivity.java這個檔案所做的一切修改就應該都被撤銷了,

這種撤銷方式只適用于那些還沒有執行過add命令的檔案,如果某個檔案已經被添加過了,這種方式就無法撤銷其更改的內容,我們來做個試驗瞧一瞧,首先,仍然是將MainActivity中那本書的價格改成11.11,然后輸入如下命令:

git add .

這樣就把所有修改的檔案都進行了添加,可以輸入git status來檢查一下,結果如圖:

image

現在我們再執行一遍checkout命令,你會發現MainActivity仍然是處于已添加狀態,所修改的內容無法撤銷掉,

這種情況應該怎么辦?難道我們還沒法后悔了?當然不是,只不過對于已添加的檔案我們應該先對其取消添加,然后才可以撤回提交,取消添加使用的是reset命令,用法如下所示:

git reset HEAD app/src/main/java/com/zhouzhou/providertest/MainActivity.java

然后再運行一遍git status命令,你就會發現MainActivity.java這個檔案重新變回了未添加狀態,此時就可以使用checkout命令來將修改的內容進行撤銷了,

7.5.4 查看提交記錄

當ProviderTest這個專案開發了幾個月之后,可能已經執行過上百次的提交操作了,這個時候估計你早就已經忘記每次提交都修改了哪些內容,忠實的Git一直都幫我們清清楚楚地記錄著,可以使用log命令查看歷史提交資訊,用法如下所示:

git log

由于目前我們只執行過一次提交,所以能看到的資訊很少,如圖:

image

可以看到,每次提交記錄都會包含提交id、提交人、提交日期以及提交描述這4個資訊,那么,我們再次將書價修改成11.11,然后執行一次提交操作,如下所示:

git add .
git commit -m "Change price"

現在,重新執行git log命令,結果如圖:

image

當提交記錄非常多的時候,如果我們只想查看其中一條記錄,可以在命令中指定該記錄的id,并加上-1引數表示只想看到一行記錄,如下所示:

git log ca9f912bXXXXXXXXXXXXXXXXXXXXXXXXXXe20a77f -1

image

而如果想要查看這條提交記錄具體修改了什么內容,可以在命令中加入-p引數,命令如下:

git log ca9f912bXXXXXXXXXXXXXXXXXXXXXXXXXXe20a77f -1 -p

查詢出的結果如下圖所示,其中減號代表洗掉的部分,加號代表添加的部分,

image

在本章中,先了解了Android的權限機制,并且學會了如何在6.0以上的系統中使用運行時權限,然后又重點學習了內容提供器的相關內容,以實作跨程式資料共享的功能,還學習了怎樣創建自己的內容提供器來共享資料,

不過,只有真正需要將資料共享出去的時候我們才應該創建內容提供器,僅僅是用于程式內部訪問的資料就沒有必要這么做,所以千萬別對它進行濫用,

個人學習筆記,針對本人在自學中遇到的問題,

轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/472407.html

標籤:Android

上一篇:《第一行代碼:Android篇》學習筆記(六)

下一篇:《第一行代碼:Android篇》學習筆記(八)

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【從零開始擼一個App】Dagger2

    Dagger2是一個IOC框架,一般用于Android平臺,第一次接觸的朋友,一定會被搞得暈頭轉向。它延續了Java平臺Spring框架代碼碎片化,注解滿天飛的傳統。嘗試將各處代碼片段串聯起來,理清思緒,真不是件容易的事。更不用說還有各版本細微的差別。 與Spring不同的是,Spring是通過反射 ......

    uj5u.com 2020-09-10 06:57:59 more
  • Flutter Weekly Issue 66

    新聞 Flutter 季度調研結果分享 教程 Flutter+FaaS一體化任務編排的思考與設計 詳解Dart中如何通過注解生成代碼 GitHub 用對了嗎?Flutter 團隊分享如何管理大型開源專案 插件 flutter-bubble-tab-indicator A Flutter librar ......

    uj5u.com 2020-09-10 06:58:52 more
  • Proguard 常用規則

    介紹 Proguard 入口,如何查看輸出,如何使用 keep 設定入口以及使用實體,如何配置壓縮,混淆,校驗等規則。

    ......

    uj5u.com 2020-09-10 06:59:00 more
  • Android 開發技術周報 Issue#292

    新聞 Android即將獲得類AirDrop功能:可向附近設備快速分享檔案 谷歌為安卓檔案管理應用引入可安全隱藏資料的Safe Folder功能 Android TV新主界面將顯示電影、電視節目和應用推薦內容 泄露的Android檔案暗示了傳說中的谷歌Pixel 5a與折疊屏新機 谷歌發布Andro ......

    uj5u.com 2020-09-10 07:00:37 more
  • AutoFitTextureView Error inflating class

    報錯: Binary XML file line #0: Binary XML file line #0: Error inflating class xxx.AutoFitTextureView 解決: <com.example.testy2.AutoFitTextureView android: ......

    uj5u.com 2020-09-10 07:00:41 more
  • 根據Uri,Cursor沒有獲取到對應的屬性

    Android: 背景:呼叫攝像頭,拍攝視頻,指定保存的地址,但是回傳的Cursor檔案,只有名稱和大小的屬性,沒有其他諸如時長,連ID屬性都沒有 使用 cursor.getInt(cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATIO ......

    uj5u.com 2020-09-10 07:00:44 more
  • Android連載29-持久化技術

    一、持久化技術 我們平時所使用的APP產生的資料,在記憶體中都是瞬時的,會隨著斷電、關機等丟失資料,因此android系統采用了持久化技術,用于存盤這些“瞬時”資料 持久化技術包括:檔案存盤、SharedPreference存盤以及資料庫存盤,還有更復雜的SD卡記憶體儲。 二、檔案存盤 最基本存盤方式, ......

    uj5u.com 2020-09-10 07:00:47 more
  • Android Camera2Video整合到自己專案里

    背景: Android專案里呼叫攝像頭拍攝視頻,原本使用的 MediaStore.ACTION_VIDEO_CAPTURE, 后來因專案需要,改成了camera2 1.Camera2Video 官方demo有點問題,下載后,不能直接整合到專案 問題1.多次拍攝視頻崩潰 問題2.雙擊record按鈕, ......

    uj5u.com 2020-09-10 07:00:50 more
  • Android 開發技術周報 Issue#293

    新聞 谷歌為Android TV開發者提供多種新功能 Android 11將自動填表功能整合到鍵盤輸入建議中 谷歌宣布Android Auto即將支持更多的導航和數字停車應用 谷歌Pixel 5只有XL版本 搭載驍龍765G且將比Pixel 4更便宜 [圖]Wear OS將迎來重磅更新:應用啟動時間 ......

    uj5u.com 2020-09-10 07:01:38 more
  • 海豚星空掃碼投屏 Android 接收端 SDK 集成 六步驟

    掃碼投屏,開放網路,獨占設備,不需要額外下載軟體,微信掃碼,發現設備。支持標準DLNA協議,支持倍速播放。視頻,音頻,圖片投屏。好點意思。還支持自定義基于 DLNA 擴展的操作動作。好像要收費,沒體驗。 這里簡單記錄一下集成程序。 一 跟目錄的build.gradle添加私有mevan倉庫 mave ......

    uj5u.com 2020-09-10 07:01:43 more
最新发布
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:40:31 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:40:11 more
  • 歡迎頁輪播影片

    如圖,引導開始,球從上落下,同時淡入文字,然后文字開始輪播,最后一頁時停止,點擊進入首頁。 在來看看效果圖。 重力球先不講,主要歡迎輪播簡單實作 首先新建一個類 TextTranslationXGuideView,用于影片展示 文本是類似的,最后會有個圖片箭頭影片,布局很簡單,就是一個 TextVi ......

    uj5u.com 2023-04-20 08:39:36 more
  • 【FAQ】關于華為推送服務因營銷訊息頻次管控導致服務通訊類訊息

    一. 問題描述 使用華為推送服務下發IM訊息時,下發訊息請求成功且code碼為80000000,但是手機總是收不到訊息; 在華為推送自助分析(Beta)平臺查看發現,訊息發送觸發了頻控。 二. 問題原因及背景 2023年1月05日起,華為推送服務對咨詢營銷類訊息做了單個設備每日推送數量上限管理,具體 ......

    uj5u.com 2023-04-20 08:39:13 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:16:23 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:16:15 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:15:46 more
  • iOS從UI記憶體地址到讀取成員變數(oc/swift)

    開發除錯時,我們發現bug時常首先是從UI顯示發現例外,下一步才會去定位UI相關連的資料的。XCode有給我們提供一系列debug工具,但是很多人可能還沒有形成一套穩定的除錯流程,因此本文嘗試解決這個問題,順便提出一個暴論:UI顯示例外問題只需要兩個步驟就能完成定位作業的80%: 定位例外 UI 組 ......

    uj5u.com 2023-04-19 09:14:53 more
  • FIDE重磅更新!性能飛躍!體驗有禮!

    FIDE 開發者工具重構升級啦!實作500%性能提升,誠邀體驗! 一直以來不少開發者朋友在社區反饋,在使用 FIDE 工具的程序中,時常會遇到諸如加載不及時、代碼預覽/渲染性能不如意的情況,十分影響開發體驗。 作為技術團隊,我們深知一件趁手的開發工具對開發者的重要性,因此,在2023年開年,FinC ......

    uj5u.com 2023-04-19 09:14:08 more
  • 游戲內嵌社區服務開放,助力開發者提升玩家互動與留存

    華為 HMS Core 游戲內嵌社區服務提供快速訪問華為游戲中心論壇能力,支持玩家直接在游戲內瀏覽帖子和交流互動,助力開發者擴展內容生產和觸達的場景。 一、為什么要游戲內嵌社區? 二、游戲內嵌社區的典型使用場景 1、游戲內打開論壇 您可以在游戲內繪制論壇入口,為玩家提供沉浸式發帖、瀏覽、點贊、回帖、 ......

    uj5u.com 2023-04-19 09:08:34 more