PermissionX現在支持Java了!還有Android 11權限變更講解

本文同步發表於我的微信公衆號,掃一掃文章底部的二維碼或在微信搜索 郭霖 即可關注,每個工作日都有文章更新。

各位小夥伴們早上好,不知道你們有沒有驚訝於我的速度,因爲不久之前我才新發布的開源庫PermissionX今天又更新了。

是的,在不到一個月的時間裏,PermissionX又迎來了一次重大的版本更新。如果你覺得一個月還不算快的話,可別忘了,兩週之前我還發布了LitePal的新版本。對於我來說,這個速度已經是相當極限了。

不過,可能還有不少朋友不知道PermissionX是什麼,這裏我給出上一篇文章的鏈接,還沒看過的小夥伴先去補補課 Android運行時權限終極方案,用PermissionX吧

本來按照迭代計劃,下一個版本中,我是準備給PermissionX增加自定義權限提示對話框樣式的功能。然而隨着第一個版本的發佈,根據大家的反饋,我意識到了另一個更加緊急的需求,就是對Java語言的支持。

真的很遺憾看到,即使在今天,Kotlin在國內仍然還只是少部分開發者羣體使用的語言,然而這就是現實。因此,如果PermissionX只支持Kotlin語言的話,勢必將大部分的開發者都拒之了門外。

其實最初我讓PermissionX只支持Kotlin語言,是因爲我實在不想同時維護兩個版本,這樣修改任何功能都需要在兩個地方各改一遍,維護成本過高。

然而後面我又做了一些更全面的思考,發現只需要稍微付出一點點語法方面的代價,就可以讓一份代碼同時支持Java和Kotlin兩種語言,那麼本篇文章我們就來學習一下是如何實現的。


兼容Java和Kotlin

首先我們來回顧一下PermissionX的基本用法,這段代碼在上一篇文章中我們是見過的:

PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .onExplainRequestReason { deniedList ->
        showRequestReasonDialog(deniedList, "即將申請的權限是程序必須依賴的權限", "我已明白")
    }
    .onForwardToSettings { deniedList ->
        showForwardToSettingsDialog(deniedList, "您需要去應用程序設置當中手動開啓權限", "我已明白")
    }
    .request { allGranted, grantedList, deniedList ->
        if (allGranted) {
            Toast.makeText(this, "所有申請的權限都已通過", Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(this, "您拒絕瞭如下權限:$deniedList", Toast.LENGTH_SHORT).show()
        }
    }

是的,在Android程序的權限申請變得如此簡單。

首先調用init()方法進行初始化,調用permissions()方法來指定要申請哪些權限,在onExplainRequestReason()方法中針對那些被拒絕的權限向用戶解釋申請的原因並重新申請,在onForwardToSettings()方法中針對那些被永久拒絕的權限向用戶解釋爲什麼它們是必須的,並自動跳轉到應用設置當中提醒用戶手動開啓權限。最後調用request()方法開始請求權限,並接收申請的結果。

整段用法簡潔明瞭,而且PermissionX幫助開發者解決了權限申請過程中最痛苦的一些邏輯處理,比如權限被拒絕了怎麼辦?權限被永久拒絕了怎麼辦?

那麼之所以能將PermissionX的用法設計得這麼簡單明瞭,主要得感謝Kotlin的高階函數功能。上述代碼示例當中的onExplainRequestReason()方法、onForwardToSettings()方法、request()方法,實際上都是高階函數。對於高階函數中接收的函數類型參數,我們可以直接傳入一個Lambda表達式,然後在Lambda表達式當中處理回調邏輯即可。

然而問題也就出現在了這裏,由於Java是沒有高階函數這個概念的,因此這種便捷性的語法在Java語言當中並不適用,所以也就導致了PermissionX不支持Java的情況。

不過,這個問題是可以解決的!

事實上,在Kotlin語言當中,我們除了可以向高階函數傳遞Lambda表達式,還可以向另一種SAM函數傳遞Lambda表達式。SAM的全稱是Single Abstract Method,又叫做單抽象方法。具體來講,如果Java中定義的某個接口,裏面只有一個待實現方法(也就是所謂的單抽象方法),那麼此時我們也可以向其傳遞Lambda表達式。

舉一個具體的例子,所有Android開發者一定都調用過setOnClickListener()方法,這個方法可以用於給一個控件註冊點擊事件。

在Java當中我們會這樣寫:

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        
    }
});

這段代碼因爲所有人都實在是太熟悉了,因此沒什麼解釋的必要。

但是可以看到,在setOnClickListener()方法中,我們創建了一個View.OnClickListener的匿名類,那麼View.OnClickListener的代碼是什麼樣的呢?點擊查看其源碼,如下所示:

public class View implements Callback, android.view.KeyEvent.Callback, AccessibilityEventSource {

    public interface OnClickListener {
        void onClick(View view);
    }

可以看到,OnClickListener是一個接口,並且這個接口當中只有一個onClick()方法,因此這就是一個單抽象方法接口。

那麼根據上面的規則,Kotlin允許我們向一個接收單抽象方法接口的函數傳遞Lambda表達式。因此,在Kotlin當中,我們給一個按鈕註冊點擊事件通常都是這麼寫的:

button.setOnClickListener {

}

看到這裏,有沒有受到點啓發呢?反正我是受到了。也就是說,如果PermissionX想要同時兼容Java和Kotlin語言的話,可以很好地利用單抽象方法接口這個特性。將原本的高階函數都改成這種SAM函數,那麼不就自然可以兼容兩種語言了嗎?

沒錯,我也確實是這樣做的,不過具體在實現的過程中還是遇到了一點問題。

因爲高階函數的功能是十分強大的,我們除了可以定義一個函數類型的參數列表以及它的返回值,還可以定義它的所屬類。來看PermissionX中的一段示例代碼:

fun onExplainRequestReason(callback: ExplainScope.(deniedList: MutableList<String>) -> Unit): PermissionBuilder {
	explainReasonCallback = callback
	return this
}

以上代碼對於沒接觸過Kotlin的朋友來說,可能會像天書一樣難以理解,然而如果你學過Kotlin的話,就知道這只是定義了一個簡單的函數類型參數。是的,這裏我又要推薦我寫的新書《第一行代碼 第3版》了,還沒有閱讀過的朋友可以認真考慮一下,能在很大程序上幫助你輕鬆上手Kotlin語言。

那麼上述代碼中,我們將函數類型的所屬類定義在了ExplainScope當中,這意味着什麼?意味着,在Lambda表達式當中,我們就自動擁有了ExplainScope的上下文,因此可以直接調用ExplainScope類中的任何方法。所以,你也已經猜到了,本篇文章第一段示例代碼中調用的showRequestReasonDialog()方法就是定義在ExplainScope類當中的。

然而Kotlin中這個非常棒的特性,很遺憾,在Java當中也沒有,而且即使通過SAM函數也無法實現。

所以,這裏我不得不付出一點語法特性的代價,將Kotlin這種定義所屬類上下文的特性改成了傳遞參數的方式。也因爲這個原因,新版PermissionX的語法無法做到和上一個版本百分百兼容,而是要稍微做出一點點修改。

那麼新版的PermissionX中實現和剛纔同樣的功能需要這樣寫:

PermissionX.init(this)
    .permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
    .onExplainRequestReason { scope, deniedList ->
        scope.showRequestReasonDialog(deniedList, "即將申請的權限是程序必須依賴的權限", "我已明白")
    }
    .onForwardToSettings { scope, deniedList ->
        scope.showForwardToSettingsDialog(deniedList, "您需要去應用程序設置當中手動開啓權限", "我已明白")
    }
    .request { allGranted, grantedList, deniedList ->
        if (allGranted) {
            Toast.makeText(this, "所有申請的權限都已通過", Toast.LENGTH_SHORT).show()
        } else {
            Toast.makeText(this, "您拒絕瞭如下權限:$deniedList", Toast.LENGTH_SHORT).show()
        }
    }

可以看到,只是在onExplainRequestReason()和onForwardToSettings()方法的Lambda表達式參數列表中增加了一個scope參數,然後調用解釋權限申請原因對話框的時候,前面也要加上scope對象,僅此一點點變化,其他用法部分和之前是完全一模一樣的。

而Kotlin在用法層面做出這一點點的犧牲,帶來的卻是Java語言的全面支持,使用Java實現同樣的功能只需要這樣寫:

PermissionX.init(this)
	.permissions(Manifest.permission.CAMERA, Manifest.permission.READ_CONTACTS, Manifest.permission.CALL_PHONE)
	.onExplainRequestReason(new ExplainReasonCallbackWithBeforeParam() {
		@Override
		public void onExplainReason(ExplainScope scope, List<String> deniedList, boolean beforeRequest) {
			scope.showRequestReasonDialog(deniedList, "即將申請的權限是程序必須依賴的權限", "我已明白");
		}
	})
	.onForwardToSettings(new ForwardToSettingsCallback() {
		@Override
		public void onForwardToSettings(ForwardScope scope, List<String> deniedList) {
			scope.showForwardToSettingsDialog(deniedList, "您需要去應用程序設置當中手動開啓權限", "我已明白");
		}
	})
	.request(new RequestCallback() {
		@Override
		public void onResult(boolean allGranted, List<String> grantedList, List<String> deniedList) {
			if (allGranted) {
				Toast.makeText(MainActivity.this, "所有申請的權限都已通過", Toast.LENGTH_SHORT).show();
			} else {
				Toast.makeText(MainActivity.this, "您拒絕瞭如下權限:" + deniedList, Toast.LENGTH_SHORT).show();
			}
		}
	});

單純從兩種語言上來對比,Kotlin版的代碼肯定是要遠比Java版的更簡潔,但是很多朋友或許就是更加習慣Java的這種語法結構吧,看起來可能也更加親切一些。


支持Android 11

目前Android 11的Beta版本已在上週四正式發佈了,我這次也算是走在了時代的前沿,第一時間研究了Android 11中的各種新特性。

其中,權限相關的部分有了較大的變化,不過大家也不用擔心,需要我們開發者進行適配的地方並不多,只是你應該瞭解這些變化。

首先,那個讓無數開發者極其討厭的“拒絕並不再詢問”選項沒有了。但是別高興的太早,Android 11只是將它換成了另外一種展現形式。假如應用程序申請的某個權限被用戶拒絕了兩次,那麼Android系統會自動將其視爲“拒絕並不再詢問”來處理。

另外權限申請對話框現在允許取消了,如果用戶取消了權限對話框,將會視爲一次拒絕。

Android 11中還引入了權限過期的機制,本來用戶授予了應用程序某個權限,該權限會一直有效,現在如果某應用程序很長時間沒有啓動,Android系統會自動收回用戶授予的權限,下次啓動需要重新請求授權。

另外,Android 11針對攝像機、麥克風、地理定位這3種權限提供了單次授權的選項。因爲這3種權限都是屬於隱私敏感的權限,如果像過去一樣用戶同意一次就代表永久授權,可能某些惡意應用會無節制地採集用戶信息。在Android 11中請求攝像機權限,界面如下圖所示。

可以看到,圖中多了一個“僅限這一次”的選項。如果用戶選擇了這個選項,那麼在整個應用程序的生命週期內,我們都是可以獲取到攝像機數據的。但是當下次啓動程序時,則需要再次請求權限。

以上部分就是Android 11中權限相關的主要變化,你會發現,這些變化其實並沒有影響到我們的代碼編寫,也不用做什麼額外的適配,所以只需要瞭解一下就行了。

不過接下來的部分,就是我們需要進行適配的地方了。

Android 10系統首次引入了android:foregroundServiceType屬性,如果你想要在前臺Service中獲取用戶的位置信息,那麼必須在AndroidManifest.xml中進行以下配置聲明:

<manifest>
    ...
    <service ... 
		android:foregroundServiceType="location" />
</manifest>

而在Android 11系統中,這個要求擴展到了攝像機和麥克風權限。也就是說,如果你想要在前臺Service中獲取設備的攝像機和麥克風數據,那麼也需要在AndroidManifest.xml中進行聲明:

<manifest>
    ...
    <service ...
		android:foregroundServiceType="location|camera|microphone" />
</manifest>

接下來再來看另外一個需要適配的地方。

Android 10系統中引入了一個新的權限:ACCESS_BACKGROUND_LOCATION,用於允許應用程序在後臺請求設備的位置信息。不過這個權限是不可以單獨申請的,而是要和ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION一起申請纔行。這個也很好理解,怎麼可能連前臺請求位置信息都沒同意呢,就允許在後臺請求位置信息了。

在Android 10系統中,如果我們同時申請前臺和後臺定位權限,那麼將會出現如下界面:

可以看到,界面上的選項有些不同,“始終允許”表示同時允許了前臺和後臺定位權限,“僅在使用此應用時允許”表示只允許前臺定位權限,“拒絕”表示都不允許。

但是如果我們在Android 11系統中同時申請前臺和後臺定位權限會怎麼樣呢?很遺憾地告訴你,會崩潰。

因爲Android 11系統要求,ACCESS_BACKGROUND_LOCATION權限必須單獨申請,並且在那之前,應用程序還必須已經獲得了ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION權限纔行。

這個規則其實PermissionX是可以不用考慮的,如果開發者在Android 11中同時申請前臺和後臺定位權限 ,那麼就讓系統直接拋出異常也是合理的,因爲這種請求方式違反了Android 11的規則。

然而爲了讓開發者更方便地使用PermissionX,減少這種差異化編程的的場景,我還是決定對Android 11的這個新規則進行適配。

具體思路也是比較簡單的,如果應用程序同時申請了前臺和後臺定位權限,那麼就先忽略後臺定位權限,只申請前臺定位以及其他權限,等所有權限都申請完畢後再單獨去申請後臺定位權限。

看上去很簡單是不是?可是當我具體去實現的時候差點沒把我累死,同時也暴露出了PermissionX的擴展性設計得非常糟糕的問題。

其實本來我一直覺得PermissionX的代碼寫得非常出色,還鼓勵大家去閱讀源碼,然而這次爲了兼容Android 11我才發現,有多個地方的耦合性太高,牽一髮而動全身,導致難以擴展功能。

PermissionX中有很多可以註冊回調監聽的地方,權限被拒絕時有回調,權限被永久拒絕時有回調,權限申請結束時有回調。而在代碼邏輯中去通知這些回調的地方就更多了,傳入一個空權限列表是不會進行權限請求的,直接回調結束。傳入的權限列表如果全部都已經授權了,也會直接回調結束。還有點擊解釋權限申請原因對話框上的取消按鈕,也要終止後續的權限請求。

以上還只是處理了一些邊界情況,都不是正式的權限請求流程,正式請求之後的回調邏輯就更多了。

那麼如此複雜的回調邏輯帶來了一個什麼問題?我很難找到一個切入點去判斷除了後臺定位權限之外的其他權限都處理完了(那麼多的回調點都需要處理),然後再單獨去申請後臺定位權限。另外,後臺定位權限還要複用之前的邏輯,這樣每個回調的地方我都要知道當前是在請求非後臺定位權限,還是後臺定位權限(否則將無法知道接下來應該是去請求後臺定位權限,還是結束請求回調給開發者)。

我大概嘗試了兩種不同的if else設計思路來實現兼容Android 11系統的功能,最終都失敗了。寫到後面邏輯越來越複雜,改了這個bug出現那個bug,實在無法繼續。

最終我決定將PermissionX的整體架構全部推翻重來。這是一個不容易的決定,但是既然已經知道PermissionX的擴展性設計得非常糟糕,早晚我都是要解決這個問題的。

新版PermissionX的整體架構改成了鏈式任務的執行模式,根據不同的權限類型將請求分成兩種任務,權限的請求以及結果的回調都是封裝在任務當中的。當一個任務執行結束之後會判斷是否還有下一個任務要執行,如果有的話就執行下一個任務,沒有的話就回調結束。示意圖如下所示:

部分鏈式任務的實現代碼如下:

/**
 * Maintain the task chain of permission request process.
 * @author guolin
 * @since 2020/6/10
 */
public class RequestChain {

    /**
     * Holds the first task of request process. Permissions request begins here.
     */
    private BaseTask headTask;

    /**
     * Holds the last task of request process. Permissions request ends here.
     */
    private BaseTask tailTask;

    /**
     * Add a task into task chain.
     * @param task  task to add.
     */
    public void addTaskToChain(BaseTask task) {
        if (headTask == null) {
            headTask = task;
        }
        // add task to the tail
        if (tailTask != null) {
            tailTask.next = task;
        }
        tailTask = task;
    }

    /**
     * Run this task chain from the first task.
     */
    public void runTask() {
        headTask.request();
    }

}

這裏我使用了鏈表這種數據結構來實現,每當新增一個任務的時候,就將它添加到鏈表的尾部。執行任務的時候則從第一個任務開始執行,然後依次向後,直到所有任務執行結束纔回調給開發者。

然後在請求權限的request()方法中,我構建了這樣一條任務鏈:

/**
 * Request permissions at once, and handle request result in the callback.
 *
 * @param callback Callback with 3 params. allGranted, grantedList, deniedList.
 */
public void request(RequestCallback callback) {
    requestCallback = callback;
    // Build the request chain.
    // RequestNormalPermissions runs first.
    // Then RequestBackgroundLocationPermission runs.
    RequestChain requestChain = new RequestChain();
    requestChain.addTaskToChain(new RequestNormalPermissions(this));
    requestChain.addTaskToChain(new RequestBackgroundLocationPermission(this));
    requestChain.runTask();
}

可以看到,這裏先是創建了RequestChain的實例,然後向鏈表中添加一個RequestNormalPermissions任務用於請求普通的權限,又添加了一個RequestBackgroundLocationPermission任務用於請求後臺定位權限,接着調用runTask()方法就可以從鏈表頭部依次向後執行任務了。

現在,當你使用PermissionX來進行權限處理,可以完全不用理會Android 11上的權限機制差異,所有判斷邏輯PermissionX都會在內部幫你處理好。假如你同時請求了前臺和後臺定位權限,在Android 10系統中會將它們一起申請,在Android 11系統中會將它們分開申請,在Android 9或以下系統,則不會去申請後臺定位權限,因爲那個時候還沒有這個權限。

另外,使用這種鏈式任務的執行模式之後,PermissionX未來的擴展性會變得非常好。因爲除了上述我們討論的權限之外,Android系統還有一些更加特殊的權限,比如懸浮窗權限。這種權限是不可以調用代碼來進行申請的,而是要跳轉到一個專門的設置界面,提醒用戶手動開啓。而現在的PermissionX,想要支持這種權限,其實只需要再添加一個新的任務就行了。當然,這個功能是相對比較靠後的版本計劃,下一個版本的重點還是自定義權限提示對話框樣式的功能。


如何升級

關於PermissionX新版本的內容變化就介紹到這裏,升級的方式非常簡單,改一下dependencies當中的版本號即可:

dependencies {
    ...
    implementation 'com.permissionx.guolindev:permissionx:1.2.2'
}

尤其是還在使用Java語言的開發者們,這次的版本更新是非常值得一試的。

另外,如果你的項目還沒有升級到AndroidX,那麼可以使用Permission-Support這個版本,用法都是一模一樣的,只是dependencies中的依賴聲明需要改成:

dependencies {
    ...
    implementation 'com.permissionx.guolindev:permission-support:1.2.2'
}

最後附上PermissionX的開源庫地址,歡迎大家star和fork。

https://github.com/guolindev/PermissionX

本篇文章的內容還是比較充實的,既講了PermissionX的新版用法,又講了一些Kotlin的知識,還講了Android 11的權限變更,當然最後還有新版PermissionX的架構設計思路,希望大家都有學到一些知識吧。


關注我的技術公衆號,每天都有優質技術文章推送。

微信掃一掃下方二維碼即可關注:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章