【Android】安卓四大元件之內容提供者

woodwhale發表於2022-01-26

【Android】安卓四大元件之內容提供者

1、關於內容提供者

1.1 什麼是內容提供者

內容提供者就是contentProvider,作用有如下:

  1. 給多個應用提供資料
  2. 類似一個介面
  3. 可以和多個應用分享資料

1.2 為什麼要有內容提供者

作為一個APP,自己的資料會在某些條件下提供給其他APP,但是,APP的資料是私有的。

例如,APP A的資料庫內容是不可以被APP B進行讀取的

這個時候,我們就需要一個內容提供者,將APP A中的資料資訊提供給APP B。

1.3 使用場景

就貼近生活一些吧,拿某寶、拼夕夕等購物軟體舉例子,下面幾種場景你肯定見過:

  • 獲取通訊錄中的聯絡人,申請好友。
  • 獲取其他軟體搜尋記錄,大資料計算,進行產品推送。
  • 預約直播,將預約資訊寫入手機備忘錄

2、如何自定義內容提供者

2.1 寫一個提供內容的APP

首先,在我們提供內容的APP中的manifest中,寫入provider:

  • authorities可以是包名
  • name就是自己定義的名字
  • exported=true可以讓其他的APP來訪問自己提供的內容
<provider
            android:authorities="top.woodwhale.picgo"
            android:name=".test.contentprovider.provider.UserProvider"
            android:exported="true"
            android:enabled="true"
            android:grantUriPermissions="true"/>

其次,我們操作的這個提供內容的APP,得有初始化後的資料庫

  • 關於資料庫,提幾句:
  • 相關的內容,在之前的SQLite學習章節中說過了
  • 在一個專案中,該有的架構還是有的,如下圖
  • image-20220125163457571
  • 其中dao是專門執行資料庫操作的,有相關介面和實現類
  • db資料夾是存放資料庫helper的,它的作用就是初始化資料庫,並且可以返回資料操作物件
  • pojo就是從資料中轉為物件的類
  • provider就是我們要寫的內容提供者的類
  • utils中就是常用的工具類

提完了資料庫,我們繼續說內容提供者

一個具有內容提供者的APP中必須得有如下的類:

  • 該類繼承ContentProvider,並且重寫其中的方法(增刪改查)
  • 賦予一個UriMatcher物件的成員變數
  • 進行一個Uri的匹配,authorities要和manifest中的一致,並且可以選擇表進行內容共提供。這些都在靜態程式碼塊中實現,使用addURI方法即可
  • 重寫增刪改查方法,前提是Uri匹配!
public class UserProvider extends ContentProvider {

    private static final String TAG = "UserProvider";
    private UserDatabaseHelper dbh;
    private static UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    private static final int USER_MATCH_CODE = 1;
    static {
        uriMatcher.addURI("top.woodwhale.picgo","user",USER_MATCH_CODE);
    }

    @Override
    public boolean onCreate() {
        dbh = new UserDatabaseHelper(getContext());
        return false;
    }

    @Nullable
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        int res = uriMatcher.match(uri);
        // 匹配規則
        if (res == USER_MATCH_CODE) {
            SQLiteDatabase db = dbh.getWritableDatabase();
            return db.query(Constants.TABLE_NAME, projection, selection, selectionArgs, null, null, sortOrder);
        } else {
            throw new IllegalArgumentException("引數錯誤!");
        }
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return null;
    }

    @Nullable
    @Override
    public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        int res = uriMatcher.match(uri);
        if (res == USER_MATCH_CODE) {
            SQLiteDatabase db = dbh.getWritableDatabase();
            long insertRes = db.insert(Constants.TABLE_NAME, null, values);
            Uri resUri = Uri.parse("content://top.woodwhale.picgo/user/"+insertRes);
            Log.d(TAG,"insertRes --> "+ insertRes);
            // 插入資料成功,資料變化了,需要通知其他地方
            getContext().getContentResolver().notifyChange(resUri,null);
            return resUri;
        } else {
            throw new IllegalArgumentException("引數錯誤!");
        }
    }

    @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;
    }
}

2.2 使用其他的APP來呼叫上述內容

我們寫其他的一個APP來呼叫上述的內容:

首先是查詢:

  • 注意Uri.parse("content://top.woodwhale.picgo/user")是我們在上面APP中寫的:
    • top.woodwhale.picgoauthorities
    • user是表名
    • 對應上面的uriMatcher.addURI("top.woodwhale.picgo","user",USER_MATCH_CODE)
/**
     * 測試通過內容提供者,獲取picgo專案中搞得資料庫內容
     * @param view this activity
     */
public void getContent(View view) {
    ContentResolver contentResolver = this.getContentResolver();
    Uri uri = Uri.parse("content://top.woodwhale.picgo/user");
    @SuppressLint("Recycle") Cursor cursor = contentResolver.query(uri, null, null, null, null);
    String[] columnNames = cursor.getColumnNames();
    Log.d(TAG, "columnNames.length --> "+String.valueOf(columnNames.length));
    Log.d(TAG,"=================================");
    while (cursor.moveToNext()) {
        for (String columnName : columnNames) {
            @SuppressLint("Range") String cursorString = cursor.getString(cursor.getColumnIndex(columnName));
            Log.d(TAG,"cursorString --> " + cursorString);
        }
    }
    Log.d(TAG,"=================================");
}

點選測試:

MessageCenterUI_gjIgBUEiWt

然後是插入:

還是非常簡單的:

/**
     * 新增資料
     * @param view this activity
     */
public void insertContent(View view) {
    ContentResolver contentResolver = this.getContentResolver();
    Uri uri = Uri.parse("content://top.woodwhale.picgo/user");
    ContentValues values = new ContentValues();
    values.put(Constants.FIELD_USERNAME,"wyh");
    values.put(Constants.FIELD_PASSWORD,"114514");
    values.put(Constants.FIELD_AGE,3);
    values.put(Constants.FIELD_SEX,"男");
    contentResolver.insert(uri,values);
}

我們可以在onCreate的時候就註冊一個內容觀察者,當我們內容提供者的資料發生改變的時候,就可以監聽到,也就是,我們插入成功就可以監聽到

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

    Uri uri = Uri.parse("content://top.woodwhale.picgo/user");
    ContentResolver contentResolver = getContentResolver();
    contentResolver.registerContentObserver(uri, true, new ContentObserver(new Handler()) {
        @Override
        public void onChange(boolean selfChange) {
            super.onChange(selfChange);
            Log.d(TAG,"使用者資料發生變化");
        }
    });
}

註冊成功之後,我們呼叫插入方法,可以發現已經被監聽了,並且我們查詢資料庫,確實新增了如上的資訊

MessageCenterUI_KoyYrWPhNU

2.3 內容提供者的小結

其實使用內容提供者非常的簡單和便捷,但是又有多少APP敢將自身的資料提供給他人呢?

所以自己寫的APP中的內容提供者少之又少,基本上都是同一廠商敢和自家APP聯動剽取使用者的資訊共享。

但是在安卓手機中,有很多自帶的APP,他們都具有內容提供者的對應介面,用來讓常用的APP進行內容的增刪改查,下面我們來進行常見內容提供者的學習!

3、使用“日曆”內容提供者

在很多的情況下,我們會將一些事情寫入到我們手機中的“日曆”中,當到了預定的時間就會提醒,那麼設定一個日曆提醒事件怎麼做到呢?——我們可以使用安卓開發給定的CalendarContract進行完成

CalendarContract是日曆內容提供者和APP之間的一個合同,當我們的APP獲取了讀、寫日曆的許可權之後,就可以對手機自帶的這個"日曆APP"進行新增事件的操作,我們通過下面的程式碼來認識一下!

3.1 獲取日曆許可權

在安卓6.0,也就是SDK>=23的版本後,我們的APP許可權需要動態申請,首先在manifest中申請日曆的讀寫許可權

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

然後我們在activity中寫一個方法來動態的獲取許可權

// 成員變數
private static final int PERMISSION_REQUEST_CODE = 1;

private void initPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        String[] reqPermissions = new String[]{Manifest.permission.READ_CALENDAR,Manifest.permission.WRITE_CALENDAR};
        for (String reqPermission : reqPermissions) {
            if (checkSelfPermission(reqPermission) != PackageManager.PERMISSION_GRANTED) {
                // 如果有沒有授權的,就去提醒授權
                requestPermissions(reqPermissions,PERMISSION_REQUEST_CODE);
                break;
            }
        }
    }
}

同時我們還可以重寫一個回撥方法onRequestPermissionsResult,也就是許可權獲取結果的回撥,如果拒絕了我們的許可權申請,那麼久finish()當前頁面

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    if (requestCode == PERMISSION_REQUEST_CODE) {
        for (int grantResult : grantResults) {
            if (grantResult != PackageManager.PERMISSION_GRANTED) {
                Log.d(TAG,"somePermissionsWereNotGranted");
                finish();
                break;
            }
        }
        Log.d(TAG,"allPermissionsHaveBeenGiven...");
        Toast.makeText(this, "allPermissionsHaveBeenGiven...", Toast.LENGTH_SHORT).show();
    }
}

3.2 有關日曆的各種class和屬性

下面我們正式開始,在開始之前,我們來看看有關日曆的class的各種作用:

20191104_184557.png

3.3 獲取一個日曆使用者

在獲取完許可權之後,我們可以獲取一個日曆使用者的ID(前提是,日曆程式中有這個使用者)

我們呼叫contentResolver的query()方法,獲得一個cursor,再查詢其中的CalendarContract.Calendars._ID,這個id就是我們的使用者ID

@SuppressLint("Range")
private int getCalendarID() {
    Log.d(TAG,"getCalendarUserId...");
    ContentResolver contentResolver = this.getContentResolver();
    Uri uri = CalendarContract.Calendars.CONTENT_URI;
    Cursor cursor = contentResolver.query(uri, null, null, null, null, null);
    cursor.moveToFirst();
    int id = cursor.getInt(cursor.getColumnIndex(CalendarContract.Calendars._ID));
    Log.d(TAG,"anInt --> " + id);
    cursor.close();
    return id;
}

3.4 將內容寫入日曆

最後一步就是寫入日曆內容

  • 我們通過ContentValues物件的put方法,將我們的鍵值對寫入其中

  • 有什麼常量可以寫入呢?

    20191105_184333.png

  • 寫入規則

    image-20220125160204152

我們在寫入成功之後,得到的Uri可以進行一個intent隱式意圖的跳轉,直接檢視我們寫入的事件

@RequiresApi(api = Build.VERSION_CODES.N)
public void writeCalendarEvent(View view) {
    long calID = getCalendarID();
    if (calID == -1) {
        // 如果沒有賬戶,那麼就終止這個方法
        return;
    }
    // 設定開始時間,注意 month、date 從 0 開始
    Calendar beginTime = Calendar.getInstance();
    beginTime.set(2022,0,30,0,0);
    long beginTimeTimeInMillis = beginTime.getTimeInMillis();
    // 設定結束時間
    Calendar endTime = Calendar.getInstance();
    endTime.set(2022,0,30,23,59);
    long endTimeTimeInMillis = endTime.getTimeInMillis();
    // 設定內容values
    String timeZone = TimeZone.getDefault().getID();
    ContentValues values = new ContentValues();
    values.put(CalendarContract.Events.DTSTART,beginTimeTimeInMillis);
    values.put(CalendarContract.Events.DTEND,endTimeTimeInMillis);
    values.put(CalendarContract.Events.CALENDAR_ID, calID);
    values.put(CalendarContract.Events.EVENT_TIMEZONE,timeZone);
    values.put(CalendarContract.Events.TITLE,"準備過年!");
    values.put(CalendarContract.Events.DESCRIPTION,"衝就完了!");
    values.put(CalendarContract.Events.EVENT_LOCATION,"九江");
    // 插入資料
    Uri uri = CalendarContract.Events.CONTENT_URI;
    ContentResolver contentResolver = getContentResolver();
    Uri res = contentResolver.insert(uri, values);
    Log.d(TAG,"uriRes --> " + res);
    gotoCalendar(res);
}

private void gotoCalendar(Uri res) {
    Intent intent = new Intent(Intent.ACTION_VIEW)
        .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
        .setData(res);
    startActivity(intent);
}

3.5 設定日曆事件提醒

在上面完成了之後,我們發現其實並沒有開啟提示模式,也就是說,到了當前並沒有鬧鐘或者資訊的通知

我們可以通過CalendarContract.Reminders來設定提醒

需要注意需要如下的常量value設定:

20191105_223511.png

@RequiresApi(api = Build.VERSION_CODES.N)
public void writeCalendarEvent(View view) {
    long calID = getCalendarID();
    if (calID == -1) {
        // 如果沒有賬戶,那麼就終止這個方法
        return;
    }
    // 設定開始時間,注意 month、date 從 0 開始
    Calendar beginTime = Calendar.getInstance();
    beginTime.set(2022,0,30,0,0);
    long beginTimeTimeInMillis = beginTime.getTimeInMillis();
    // 設定結束時間
    Calendar endTime = Calendar.getInstance();
    endTime.set(2022,0,30,23,59);
    long endTimeTimeInMillis = endTime.getTimeInMillis();
    // 設定內容values
    String timeZone = TimeZone.getDefault().getID();
    ContentValues eventValues = new ContentValues();
    eventValues.put(CalendarContract.Events.DTSTART,beginTimeTimeInMillis);
    eventValues.put(CalendarContract.Events.DTEND,endTimeTimeInMillis);
    eventValues.put(CalendarContract.Events.CALENDAR_ID, calID);
    eventValues.put(CalendarContract.Events.EVENT_TIMEZONE,timeZone);
    eventValues.put(CalendarContract.Events.TITLE,"準備過年!");
    eventValues.put(CalendarContract.Events.DESCRIPTION,"衝就完了!");
    eventValues.put(CalendarContract.Events.EVENT_LOCATION,"九江");
    // 插入資料
    Uri eventUri = CalendarContract.Events.CONTENT_URI;
    ContentResolver contentResolver = getContentResolver();
    Uri eventRes = contentResolver.insert(eventUri, eventValues);
    Log.d(TAG,"eventRes --> " + eventRes);

    // METHOD_ALERT提醒
    String eventID = eventRes.getLastPathSegment();
    ContentValues reminderValues = new ContentValues();
    reminderValues.put(CalendarContract.Reminders.EVENT_ID,eventID);
    reminderValues.put(CalendarContract.Reminders.MINUTES,15);
    reminderValues.put(CalendarContract.Reminders.METHOD,CalendarContract.Reminders.METHOD_ALERT);
    Uri reminderUri = CalendarContract.Reminders.CONTENT_URI;
    Uri reminderRes = contentResolver.insert(reminderUri, reminderValues);
    Log.d(TAG,"reminderRes --> " + reminderRes);
    Toast.makeText(this, "資訊通知已開啟", Toast.LENGTH_SHORT).show();
}

3.6 測試是否插入成功

普通新增效果圖如下:

qemu-system-x86_64_1apbuq0Gyc

新增了提醒方法後的效果如下:

qemu-system-x86_64_ZEzUYMJuEX

可以發現,我們成功的將插入方法中的內容寫入到了系統自帶的“日曆APP”中

4、使用“通訊錄”內容提供者

某寶、某夕夕,經常會申請通訊錄許可權,然後幫你自動加好友。

有沒有思考過一個問題,那就是,他們是如何讀取你的通訊錄的?

其實這個問題非常的簡單,使用“通訊錄”內容提供者就完全可以做到,只需要使用者提供通訊錄的許可權即可。

使用的方法和“日曆”非常類似,步驟都是一樣的,看看原始碼或者市面上的教程都可以瞭解噢

我這裡就直接放我寫的一個testDemo了,如果需要檢視更多的通訊錄細則,建議閱讀一下原始碼噢!

先寫一個User類,其中封裝了聯絡人的資料

package top.woodwhale.providertest.contactsResolver;

import androidx.annotation.NonNull;

public class User {
    private int id;
    private String phoneNumber;
    private String name;

    public User(int id, String phoneNumber, String name) {
        this.id = id;
        this.phoneNumber = phoneNumber;
        this.name = name;
    }

    @NonNull
    @Override
    public String toString() {
        return "User{" +
            "id=" + id +
            ", phoneNumber='" + phoneNumber + '\'' +
            ", name='" + name + '\'' +
            '}';
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getPhoneNumber() {
        return phoneNumber;
    }

    public void setPhoneNumber(String phoneNumber) {
        this.phoneNumber = phoneNumber;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

然後就是常規操作了:

  • 首先SDK>=23需要動態申請許可權
  • 其次就是使用ContactsContract.Contacts來進行各種查詢!
  • 如下程式碼是獲取聯絡人id、name、phoneNumber
public class ThirdActivity extends Activity {
    private static final String TAG = "ThirdActivity";
    private static final int PERMISSION_REQUEST_CODE = 1;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_third);
        // 獲取許可權
        initPermission();
    }

    // 初始化許可權
    public void initPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            String[] reqPermissions = new String[]{Manifest.permission.READ_CONTACTS,Manifest.permission.WRITE_CONTACTS};
            for (String reqPermission : reqPermissions) {
                if (checkSelfPermission(reqPermission) != PackageManager.PERMISSION_GRANTED) {
                    // 如果有沒有授權的,就去提醒授權
                    requestPermissions(reqPermissions,PERMISSION_REQUEST_CODE);
                    break;
                }
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == PERMISSION_REQUEST_CODE) {
            String res = "allPermissionsHaveBeenGiven";
            for (int grantResult : grantResults) {
                if (grantResult != PackageManager.PERMISSION_GRANTED) {
                    Log.d(TAG,"somePermissionsWereNotGranted");
                    res = "somePermissionsWereNotGranted";
                    finish();
                    break;
                }
            }
            Log.d(TAG,"allPermissionsHaveBeenGiven...");
            Toast.makeText(this, res, Toast.LENGTH_SHORT).show();
        }
    }

    @SuppressLint("Range")
    public void getContactData(View view) {
        ContentResolver contentResolver = getContentResolver();
        Uri rawContactUri = ContactsContract.Contacts.CONTENT_URI;
        Cursor cursor = contentResolver.query(rawContactUri, null, null, null, null, null);
        Log.d(TAG,"cursorGetCount --> "+cursor.getCount());
        Toast.makeText(this,"count == " + cursor.getCount(), Toast.LENGTH_SHORT).show();
        while (cursor.moveToNext()) {
            // 聯絡人ID
            int id = cursor.getInt(cursor.getColumnIndex(ContactsContract.Contacts._ID));
            // 聯絡人name
            String name = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
            // 聯絡人號碼個數
            int numCount=cursor.getInt(cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER));
            // 聯絡人phoneNumber
            String phoneNumber = null;
            if (numCount > 0){
                Cursor phoneCursor=contentResolver.query(
                    ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
                    null, ContactsContract.CommonDataKinds.Phone.CONTACT_ID+"=?",
                    new String[]{Integer.toString(id)}, null);
                if(phoneCursor.moveToFirst()){     //僅讀取第一個電話號碼
                    phoneNumber = phoneCursor.getString(phoneCursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER));
                }
                phoneCursor.close();
            }
            // 新建物件,進行封裝處理
            User user = new User(id,phoneNumber,name);
            Log.d(TAG,"userInfo --> " + user.toString());
        }
        cursor.close();
    }

}

效果如下:

MessageCenterUI_KZ1BNrtK7F

5、使用“簡訊”內容提供者

目前,很多APP都會自動監聽傳送來的驗證碼,我們可以實現這樣的效果嘛?

當然可以,而且還非常簡單,只需要一個監聽sms可以啦!

原理:

  • 記不記得在寫自定義的內容提供者的時候,我們使用了contentResolver.registerContentObserver的方法?
  • 這個方法就是註冊了一個內容觀察者,如果我們將這個觀察者來觀察我們的簡訊,獲取簡訊內容,再通過正則匹配獲取,最後setText一下,是不是就解決了這個問題呢?
  • 原理非常簡單,下面來寫一寫如何實現!

首先是佈局檔案xml

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

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="10dp">
        <EditText
            android:hint="請輸入手機號"
            android:inputType="number"
            android:layout_weight="1"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:id="@+id/et_phoneNumber"/>
        <Button
            android:layout_width="110dp"
            android:layout_height="50dp"
            android:text="獲取驗證碼"
            android:id="@+id/bt_getVerificationCode"
            android:onClick="getVerificationCode"/>
    </LinearLayout>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:layout_marginTop="10dp">
        <EditText
            android:hint="請輸入驗證碼"
            android:inputType="number"
            android:layout_weight="1"
            android:layout_width="match_parent"
            android:layout_height="50dp"
            android:id="@+id/et_verificationCode"/>
        <Button
            android:layout_width="110dp"
            android:layout_height="50dp"
            android:text="提交驗證碼"
            android:id="@+id/bt_submit"
            android:onClick="getVerificationCode"/>
    </LinearLayout>


</LinearLayout>

然後是最基本的動態獲取許可權,如果我們需要讀取簡訊,首先必須得有一個READ_SMS的許可權,仍然是通過動態申請獲取,步驟還是老樣子:

  • 現在manifest中宣告:

    <uses-permission android:name="android.permission.READ_SMS"/>
    
  • 然後在activity的onCreate()方法中呼叫如下的動態申請程式碼:

    // 初始化許可權
    public void initPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            String[] reqPermissions = new String[]{Manifest.permission.READ_SMS};
            for (String reqPermission : reqPermissions) {
                if (checkSelfPermission(reqPermission) != PackageManager.PERMISSION_GRANTED) {
                    // 如果有沒有授權的,就去提醒授權
                    requestPermissions(reqPermissions,PERMISSION_REQUEST_CODE);
                    break;
                }
            }
        }
    }
    
  • 當然,我們可以再使用許可權申請的回撥方法:

    // 內容觀察者
    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == PERMISSION_REQUEST_CODE) {
            String res = "allPermissionsHaveBeenGiven";
            for (int grantResult : grantResults) {
                if (grantResult != PackageManager.PERMISSION_GRANTED) {
                    Log.d(TAG,"somePermissionsWereNotGranted");
                    res = "somePermissionsWereNotGranted";
                    finish();
                    break;
                }
            }
            Log.d(TAG,"allPermissionsHaveBeenGiven...");
            Toast.makeText(this, res, Toast.LENGTH_SHORT).show();
        }
    }
    
  • 如此一來,我們的許可權就get到了

然後就是註冊一個內容觀察者,來監聽sms

  • 首先,我們需要寫一個UriMatcher來匹配簡訊的Uri

    public static UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    private static final int MATCH_CODE = 11;
    // Uri匹配器
    static {
        uriMatcher.addURI("sms","inbox/#",MATCH_CODE);
    }
    
  • 之所以這麼寫,是因為傳送來的簡訊Uri提示如下uri --> content://sms/inbox/20,所以我們可以來匹配inbox表中的所有數字,可以用#來完成

  • 然後我們可以寫一個方法來註冊內容觀察者,然後在匹配接收簡訊的uri中,查詢body欄位,就可以得到簡訊內容

    // 內容觀察者
    private void initContentObserver() {
        ContentResolver contentResolver = getContentResolver();
        // 匹配簡訊的內容
        contentResolver.registerContentObserver(Uri.parse("content://sms/"), true, new ContentObserver(new Handler()) {
            @SuppressLint("Range")
            @Override
            public void onChange(boolean selfChange, @Nullable Uri uri) {
                int match = uriMatcher.match(uri);
                Log.d(TAG, "uri --> " + uri + " match --> " + match);
                if (match == MATCH_CODE) {
                    Cursor cursor = contentResolver.query(uri, null, null, null, null, null);
                    String body = null;
                    if (cursor.moveToNext()) {
                        body = cursor.getString(cursor.getColumnIndex("body"));
                    }
                    cursor.close();
                    Log.d(TAG,"body --> " + body);
                    handleBody(body);
                }
            }
        });
    }
    
  • 而handleBody()這個方法就是最簡單的正則匹配一個簡訊的驗證碼:

    // 處理驗證碼匹配,並且自動填充
    private void handleBody(String body) {
        if(body != null && body.startsWith("【picgoTest】")) {
            // 擷取4位數字
            Pattern p = Pattern.compile("(?<![0-9])([0-9]{4})(?![0-9])");
            Matcher matcher = p.matcher(body);
            boolean contain = matcher.find();
            if (contain) {
                Log.d(TAG,"verifyCode -- > " + matcher.group());
                verificationCodeEt.setText(matcher.group());
            }
        }
    }
    

那麼到此,其實就差不多構建完了,完整程式碼如下:(有一個小細節就是驗證碼傳送之後,按鈕會進行倒數計時)

public class FourthActivity extends Activity {
    private static final String TAG = "FourthActivity";
    private static final int PERMISSION_REQUEST_CODE = 1;

    public static UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    private static final int MATCH_CODE = 11;
    // Uri匹配器
    static {
        uriMatcher.addURI("sms","inbox/#",MATCH_CODE);
    }

    private Button verificationCodeBtn;
    private EditText verificationCodeEt;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_fourth);
        // 獲取元件
        initView();
        // 獲取許可權
        initPermission();
        // 建立時候就註冊一個Observer
        initContentObserver();
    }

    // 初始化元件
    private void initView() {
        verificationCodeBtn = findViewById(R.id.bt_getVerificationCode);
        verificationCodeEt = findViewById(R.id.et_verificationCode);
    }

    // 內容觀察者
    private void initContentObserver() {
        ContentResolver contentResolver = getContentResolver();
        // 匹配簡訊的內容
        contentResolver.registerContentObserver(Uri.parse("content://sms/"), true, new ContentObserver(new Handler()) {
            @SuppressLint("Range")
            @Override
            public void onChange(boolean selfChange, @Nullable Uri uri) {
                int match = uriMatcher.match(uri);
                Log.d(TAG, "uri --> " + uri + " match --> " + match);
                if (match == MATCH_CODE) {
                    Cursor cursor = contentResolver.query(uri, null, null, null, null, null);
                    String body = null;
                    if (cursor.moveToNext()) {
                        body = cursor.getString(cursor.getColumnIndex("body"));
                    }
                    cursor.close();
                    Log.d(TAG,"body --> " + body);
                    handleBody(body);
                }
            }
        });
    }

    // 處理驗證碼匹配,並且自動填充
    private void handleBody(String body) {
        if(body != null && body.startsWith("【picgoTest】")) {
            // 擷取4位數字
            Pattern p = Pattern.compile("(?<![0-9])([0-9]{4})(?![0-9])");
            Matcher matcher = p.matcher(body);
            boolean contain = matcher.find();
            if (contain) {
                Log.d(TAG,"verifyCode -- > " + matcher.group());
                verificationCodeEt.setText(matcher.group());
            }
        }
    }

    // 初始化許可權
    public void initPermission() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
            String[] reqPermissions = new String[]{Manifest.permission.READ_SMS};
            for (String reqPermission : reqPermissions) {
                if (checkSelfPermission(reqPermission) != PackageManager.PERMISSION_GRANTED) {
                    // 如果有沒有授權的,就去提醒授權
                    requestPermissions(reqPermissions,PERMISSION_REQUEST_CODE);
                    break;
                }
            }
        }
    }

    @Override
    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
        if (requestCode == PERMISSION_REQUEST_CODE) {
            String res = "allPermissionsHaveBeenGiven";
            for (int grantResult : grantResults) {
                if (grantResult != PackageManager.PERMISSION_GRANTED) {
                    Log.d(TAG,"somePermissionsWereNotGranted");
                    res = "somePermissionsWereNotGranted";
                    finish();
                    break;
                }
            }
            Log.d(TAG,"allPermissionsHaveBeenGiven...");
            Toast.makeText(this, res, Toast.LENGTH_SHORT).show();
        }
    }

    // 計時器(帶有每秒呼叫和最終回撥)
    private final CountDownTimer countDownTimer = new CountDownTimer(60*1000,1000) {
        @SuppressLint("SetTextI18n")
        @Override
        public void onTick(long millisUntilFinished) {
            verificationCodeBtn.setText("重新獲取("+millisUntilFinished/1000+")");
            verificationCodeBtn.setEnabled(false);
        }

        @Override
        public void onFinish() {
            verificationCodeBtn.setText("獲取驗證碼");
            verificationCodeBtn.setEnabled(true);
        }
    };

    // 點選獲取驗證碼
    public void getVerificationCode(View view) {
        countDownTimer.start();
    }
}

最終效果如下:

MessageCenterUI_AT7CyHXIFy

6、使用“媒體庫”內容提供者

首先我們需要知道Uri

  • 圖片的Url
MediaStore.Images.Media.EXTERNAL_CONTENT_URI
  • 視訊的Url
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
  • 音訊的
MediaStore.Audio.Media.EXTERNAL_CONTENT_URI

有了Uri之後,我們就可以通過contentResolver.query去查詢資料啦

但是在query之前,我們還得動態申請許可權:

// 動態申請許可權
private void initPermission() {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
        String[] permissions = new String[] {Manifest.permission.READ_EXTERNAL_STORAGE};
        for (String permission : permissions) {
            if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
                requestPermissions(permissions,REQUEST_PERMISSION_CODE);
            }
        }
    }
}

// 許可權申請回撥方法
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    if (requestCode == REQUEST_PERMISSION_CODE) {
        String info = "授予許可權成功!";
        for (int res : grantResults) {
            if (res != PackageManager.PERMISSION_GRANTED) {
                info = "授予許可權失敗,退出程式!";
                finish();
                break;
            }
        }
        Toast.makeText(this, info, Toast.LENGTH_SHORT).show();
    }
}

申請完了之後,我們去查詢試試:

ContentResolver contentResolver = getContentResolver();
Uri imageUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
Cursor cursor = contentResolver.query(imageUri, null, null, null, null);
String[] columnNames = cursor.getColumnNames();
Log.d(TAG,"count --> " + cursor.getCount());
while (cursor.moveToNext()) {
    Log.d(TAG,"====================================");
    for (String columnName : columnNames) {
        String info = cursor.getString(cursor.getColumnIndex(columnName));
        Log.d(TAG,columnName + " --> " + info);
        Log.d(TAG,"====================================");
    }
}
cursor.close();

我的相簿中一共兩張圖片,部分logcat輸出如下:

image-20220126112707138

重要的資料有:

  • _data,這個是存放圖片的位置資訊
  • _size,這個是圖片的大小
  • _display_name,就是圖片的名稱

有了上面的使用內容提供者的前提知識,我們們可以實現從媒體庫選擇圖片,效果如下:

qemu-system-x86_64_XuBI3s6H6P

需要的程式碼量很多,所以就不在這裡細說了,主要實現的就是從媒體庫中讀取圖片檔案,然後通過陣列形式返回路徑,再在前臺渲染選擇到的圖片。

相關文章