問題來源
由于我司的android app産品主要是面向海外,是以,app中的分享功能接入facebook分享是必不可少的。最近在接入facebook android sdk進行分享時,發現一個非常奇怪的現象,明明手機上已經安裝了facebook用戶端,但卻經常出現無法調起用戶端分享,而是調起了facebook sdk内置的網頁分享。
在網頁端分享時,使用者又需要重新輸入賬号密碼才能分享(用戶端不用,因為一般都已經登入過)。這樣使用者體驗就非常差,進而會導緻很多使用者會因為要輸入賬戶密碼而放棄分享。是以,這是一個嚴重影響體驗的問題,需要緊急修複。
本文主要就是介紹該問題的分析思路,然後給出一個比較完美的解決方案,給同樣遇到此坑的同行們提供一個思路。
問題分析
為了分析此問題,我們需要進入sdk代碼内部,借助android studio debug工具,debug每一個流程,觀察是在哪個流程出了問題。我這裡接入的facebook-android-sdk版本是目前最新的版本, 4.38.1,以下引用的sdk代碼都是基于該版本。
首先看看分享功能的簡單實作:
1. 分享功能的實作
這裡我們已分享一個連結為例,展示sdk的調用方式。
分享連結時,首先new一個ShareLinkContent(參考文檔),然後new一個ShareDialog,并調用其show方法,這樣就能啟動facebook分享了,具體代碼如下:
ShareDialog shareDialog = new ShareDialog(activity)
ShareLinkContent content = new ShareLinkContent.Builder()
.setContentUrl(Uri.parse("https://developers.facebook.com"))
.build();
shareDialog.show(content, ShareDialog.Mode.AUTOMATIC)
按照正常流程,使用者手機安裝了facebook app就應該啟動facebook app分享,未安裝app就啟動網頁,讓使用者在網頁裡登入後再分享。但實際情況是,facebook app被殺死時,無法啟動facebook app分享,這是為何呢?難道facebook sdk裡有嚴重的bug嗎?下面,來追溯源碼,分析問題本因。
2.源碼分析
上面的shareDialog的show方法最終調用到了showImpl方法中,其具體的實作為:
// Pass in BASE_AUTOMATIC_MODE when Automatic mode choice is desired
protected void showImpl(final CONTENT content, final Object mode) {
AppCall appCall = createAppCallForMode(content, mode);
if (appCall != null) {
if (fragmentWrapper != null) {
DialogPresenter.present(appCall, fragmentWrapper);
} else {
DialogPresenter.present(appCall, activity);
}
} else {
// If we got a null appCall, then the derived dialog code is doing something wrong
String errorMessage = "No code path should ever result in a null appCall";
Log.e(TAG, errorMessage);
if (FacebookSdk.isDebugEnabled()) {
throw new IllegalStateException(errorMessage);
}
}
}
在showImpl方法中構造了一個AppCall,AppCall是最終頁面跳轉的地方,也是intent的簡單包裝,是以,需要弄清楚AppCall是怎麼構造出來的。
先看上面的
createAppCallForMode
方法是如何實作的:
private AppCall createAppCallForMode(final CONTENT content, final Object mode) {
boolean anyModeAllowed = (mode == BASE_AUTOMATIC_MODE);
AppCall appCall = null;
for (ModeHandler handler : cachedModeHandlers()) {
if (!anyModeAllowed && !Utility.areObjectsEqual(handler.getMode(), mode)) {
continue;
}
if (!handler.canShow(content, true /*isBestEffort*/)) {
continue;
}
try {
appCall = handler.createAppCall(content);
} catch (FacebookException e) {
appCall = createBaseAppCall();
DialogPresenter.setupAppCallForValidationError(appCall, e);
}
break;
}
if (appCall == null) {
appCall = createBaseAppCall();
DialogPresenter.setupAppCallForCannotShowError(appCall);
}
return appCall;
}
通過上面代碼可知,AppCall是通過ModeHandler的createAppCall方法來建立的,前提是這個handler需要滿足上面的兩個if條件才行。那麼這裡的cachedModeHandlers()裡面緩存了哪些handler呢?
再看代碼:
private List<ModeHandler> cachedModeHandlers() {
if (modeHandlers == null) {
modeHandlers = getOrderedModeHandlers();
}
return modeHandlers;
}
getOrderedModeHandlers()是抽象方法,在ShareDialog中的實作是:
@Override
protected List<ModeHandler> getOrderedModeHandlers() {
ArrayList<ModeHandler> handlers = new ArrayList<>();
handlers.add(new NativeHandler());
handlers.add(new FeedHandler()); // Feed takes precedence for link-shares for Mode.AUTOMATIC
handlers.add(new WebShareHandler());
handlers.add(new CameraEffectHandler());
handlers.add(new ShareStoryHandler());//Share into story
return handlers;
}
原來handler有這麼多個,不過,通過名字可知我們需要的是第一個handler,即NativeHandler(),本地調用,也就是調用本地用戶端。問題應該是出在了這裡,本應使用NativeHandler構造一個AppCall,實際上使用了WebShareHander。
下面,再看NativeHandler的具體實作:
我們具體隻看它的canShow()方法,因為肯定是因為canShow方法傳回了false,導緻無法調用其createAppCall方法。
private class NativeHandler extends ModeHandler {
@Override
public Object getMode() {
return Mode.NATIVE;
}
@Override
public boolean canShow(final ShareContent content, boolean isBestEffort) {
if (content == null || (content instanceof ShareCameraEffectContent)
|| (content instanceof ShareStoryContent)) {
return false;
}
boolean canShowResult = true;
if (!isBestEffort) {
if (content.getShareHashtag() != null) {
canShowResult = DialogPresenter.canPresentNativeDialogWithFeature(
ShareDialogFeature.HASHTAG);
}
if ((content instanceof ShareLinkContent) &&
(!Utility.isNullOrEmpty(((ShareLinkContent)content).getQuote()))) {
canShowResult &= DialogPresenter.canPresentNativeDialogWithFeature(
ShareDialogFeature.LINK_SHARE_QUOTES);
}
}
return canShowResult && ShareDialog.canShowNative(content.getClass());
}
@Override
public AppCall createAppCall(final ShareContent content) {
// 代碼省略,這個就是構造跳轉到facebook用戶端的intent
}
}
上面的createAppCallForMode中調用canShow方法時,傳入的isBestEffort是true:
if (!handler.canShow(content, true /*isBestEffort*/)) {
continue;
}
是以
if (!isBestEffort)
中的代碼不會執行,隻需看
ShareDialog.canShowNative(content.getClass())
這個方法就傳回結果就行。
// ShareDialog.java
private static boolean canShowNative(Class<? extends ShareContent> contentType) {
DialogFeature feature = getFeature(contentType);
return feature != null && DialogPresenter.canPresentNativeDialogWithFeature(feature);
}
// DialogPresenter.java
public static boolean canPresentNativeDialogWithFeature(
DialogFeature feature) {
return getProtocolVersionForNativeDialog(feature).getProtocolVersion()
!= NativeProtocol.NO_PROTOCOL_AVAILABLE;
}
// DialogPresenter.java
public static NativeProtocol.ProtocolVersionQueryResult getProtocolVersionForNativeDialog(
DialogFeature feature) {
String applicationId = FacebookSdk.getApplicationId();
String action = feature.getAction();
int[] featureVersionSpec = getVersionSpecForFeature(applicationId, action, feature);
return NativeProtocol.getLatestAvailableProtocolVersionForAction(
action,
featureVersionSpec);
}
// NativeProtocol.java
public static ProtocolVersionQueryResult getLatestAvailableProtocolVersionForAction(
String action,
int[] versionSpec) {
List<NativeAppInfo> appInfoList = actionToAppInfoMap.get(action);
return getLatestAvailableProtocolVersionForAppInfoList(appInfoList, versionSpec);
}
// NativeProtocol.java
private static ProtocolVersionQueryResult getLatestAvailableProtocolVersionForAppInfoList(
List<NativeAppInfo> appInfoList,
int[] versionSpec) {
// Kick off an update
updateAllAvailableProtocolVersionsAsync();
if (appInfoList == null) {
return ProtocolVersionQueryResult.createEmpty();
}
// Could potentially cache the NativeAppInfo to latestProtocolVersion
for (NativeAppInfo appInfo : appInfoList) {
int protocolVersion =
computeLatestAvailableVersionFromVersionSpec(
appInfo.getAvailableVersions(),
getLatestKnownVersion(),
versionSpec);
if (protocolVersion != NO_PROTOCOL_AVAILABLE) {
return ProtocolVersionQueryResult.create(appInfo, protocolVersion);
}
}
return ProtocolVersionQueryResult.createEmpty();
}
通過上面一系列調用可知,最終是通過appInfo.getAvailableVersions()得到的版本号計算的到一個協定版本(protocolVersion),如果取到了protocolVersion值(不是NO_PROTOCOL_AVAILABLE),就是傳回一個結果,canShow方法就傳回true。顯然,問題就出在這裡,這裡擷取不到protocolVersion的值。
那麼,appInfo.getAvailableVersions()這裡是如何得到AvailableVersion的呢?
是通過
updateAllAvailableProtocolVersionsAsync()
方法嗎?
顯然不是,因為這個方法是在異步線程中執行的,是以本段代碼執行結束後才會執行線程裡面的異步方法,這裡,顯然是為了更新之前已經取到的AvailableVersion的值。
再看看
updateAllAvailableProtocolVersionsAsync()
方法被哪裡調用過,發現在sdkInitialize方法裡有調用:
public static synchronized void sdkInitialize(
final Context applicationContext,
final InitializeCallback callback) {
...省略代碼
// Fetch available protocol versions from the apps on the device
NativeProtocol.updateAllAvailableProtocolVersionsAsync();
...省略代碼
}
原來,是在應用啟動初始化SDk時異步擷取的。
這樣做應該是為了提高分享時界面響應速度,避免在主線程進行contentProvider跨程序調用耗時,導緻主線程阻塞。
下面在看看異步線程裡到底做了什麼耗時的操作:
public static void updateAllAvailableProtocolVersionsAsync() {
if (!protocolVersionsAsyncUpdating.compareAndSet(false, true)) {
return;
}
FacebookSdk.getExecutor().execute(new Runnable() {
@Override
public void run() {
try {
for (NativeAppInfo appInfo : facebookAppInfoList) {
appInfo.fetchAvailableVersions(true);
}
} finally {
protocolVersionsAsyncUpdating.set(false);
}
}
});
}
最終調用到NativeAppInfo的fetchAvailableVersions方法:
private synchronized void fetchAvailableVersions(boolean force) {
if (force || availableVersions == null) {
availableVersions = fetchAllAvailableProtocolVersionsForAppInfo(this);
}
}
fetchAllAvailableProtocolVersionsForAppInfo方法的實作為:
private static TreeSet<Integer> fetchAllAvailableProtocolVersionsForAppInfo(
NativeAppInfo appInfo) {
TreeSet<Integer> allAvailableVersions = new TreeSet<>();
Context appContext = FacebookSdk.getApplicationContext();
ContentResolver contentResolver = appContext.getContentResolver();
String [] projection = new String[]{ PLATFORM_PROVIDER_VERSION_COLUMN };
Uri uri = buildPlatformProviderVersionURI(appInfo);
Cursor c = null;
try {
// First see if the base provider exists as a check for whether the native app is
// installed. We do this prior to querying, to prevent errors from being output to
// logcat saying that the provider was not found.
PackageManager pm = FacebookSdk.getApplicationContext().getPackageManager();
String contentProviderName = appInfo.getPackage() + PLATFORM_PROVIDER;
ProviderInfo pInfo = null;
try {
pInfo = pm.resolveContentProvider(contentProviderName, 0);
} catch (RuntimeException e) {
Log.e(TAG, "Failed to query content resolver.", e);
}
if (pInfo != null) {
try {
c = contentResolver.query(uri, projection, null, null, null);
} catch (NullPointerException|SecurityException|IllegalArgumentException ex) {
Log.e(TAG, "Failed to query content resolver.");
c = null;
}
if (c != null) {
while (c.moveToNext()) {
int version = c.getInt(c.getColumnIndex(PLATFORM_PROVIDER_VERSION_COLUMN));
allAvailableVersions.add(version);
}
}
}
} finally {
if (c != null) {
c.close();
}
}
return allAvailableVersions;
}
這段代碼的意圖很明顯,就是通過contentProvider跨程序調用,擷取目前安裝的facebook app支援的sdk分享的版本号。
通過debug這段代碼,可知ConentProvider調用的主要參數為:
URI = "content://com.facebook.katana.provider.PlatformProvider/versions"
projection = {"version"}
packageName = "com.facebook.katana"
contentProviderName = "com.facebook.katana.provider.PlatformProvider"
ContentProvider傳回的結果為:
![](https://img.laitimes.com/img/_0nNw4CM6IyYiwiM6ICdiwiIwgzLcF3LcBTNz8CX39CXy8CX3VWaWV2Zh1Wa-cmbw5iNw4SMz4yMyAjMlUjMtETMtgTMwIDMyUyNBVSN4UyNFViQBViRCVSNFVSN5USOCVSNFViR4USMCVSNFVyLclTMyFGMwUWMjBnYzU2c5sWc2UTZ1RXd08CX5IzNk5WaX9CXt92Yu8WdsVnY5pnLjlGdhR3cvw1LcpDc0RHaiojIsJye.png)
這些日期資訊,猜測應該是目前的app版本支援的分享sdk版本的日期。
問題發現
再次通過斷點調試無法掉起facebook用戶端時,方法
fetchAllAvailableProtocolVersionsForAppInfo
的傳回結果,我們發現,這裡傳回的是cursor是空,這樣其傳回值
allAvailableVersions
(TreeSet),也是空的(内容是空,對象不為空)。
這樣:
public static ProtocolVersionQueryResult getLatestAvailableProtocolVersionForAction(
String action,
int[] versionSpec) {
List<NativeAppInfo> appInfoList = actionToAppInfoMap.get(action);
return getLatestAvailableProtocolVersionForAppInfoList(appInfoList, versionSpec);
}
這裡傳回的結果也就是
NO_PROTOCOL_AVAILABLE
canShowNative
傳回的也就是false,進而導緻無法使用
NativeHandler
來建立
AppCall
,進而無法掉起facebook用戶端分享。
問題原因
明明用戶端已經按安裝,為什麼通過contentProvider無法擷取facebook用戶端的存儲的内容呢?并且,隻是機率性問題。
再仔細測試問題,我們發現,在google的pixel 2手機上沒有此問題,在主流國産手機(魅族,小米等)上都有此問題。
然而,當打開手機設定裡(或者手機管家等系統app裡)facebook自啟動權限(或者應用間互相啟動權限)之後,就沒有問題了,這說明當app沒有自啟動權限時,該app程序被殺掉(沒有做保活)後,其他app是無法通過service, broadcast receiver, contentProvider啟動該app程序(通過activity可以)。為了驗證這一點,本人親自寫了個demo驗證了一番,結論的确是如此!!至此,我們發現了根本問題原因。
四大元件中三個跨程序調用時,很有可能會調用失敗,這樣也太不靠譜了。難怪國内的很多app要做保活的功能,這應該也是主要原因之一。
問題修複
既然知道問題原因,修複起來就比較Easy了。既然無法通過contentProvider啟動facebook程序,那我們通過startActivty先啟動facebook app再走分享的流程不就行了。
這樣又有一個不太友好的體驗,就是界面會跳轉兩次,先跳到facebook主界面,再跳到facebook分享界面。但是,這種問題确實也無法規避,隻能跳轉兩次。
既然無法規避,我們可以通過判斷facebook程序是否啟動,來決定是否需要主動啟動facebook app後再分享。這樣可以降低跳轉兩次的頻率,使用者體驗較好。
那問題又來了,如何判斷一個app程序是否啟動了呢?有沒有現成的api?
google一搜,發現還真有:
//ActivityManager.java
@Deprecated
public List<RunningTaskInfo> getRunningTasks(int maxNum)
throws SecurityException {
try {
return getService().getTasks(maxNum, 0);
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
但是很抱歉,此方法的注釋裡有一段很重要的話:
@deprecated As of {@link android.os.Build.VERSION_CODES#LOLLIPOP}, this method
* is no longer available to third party
* applications: the introduction of document-centric recents means
* it can leak person information to the caller. For backwards compatibility,
* it will still retu rn a small subset of its data: at least the caller's
* own tasks, and possibly some other tasks
* such as home that are known to not be sensitive.
此方法已經不對第三方應用開放,它會洩露使用者資訊給調用者,現在隻會傳回調用者自己的任務棧資訊了。OMG!此路不同!那有沒有其他辦法呢?
另辟蹊徑
我們是如何知道facebook程序無法被啟動的呢?
再回過頭看看上面的分析過程,是因為通過contentProvider無法擷取到facebook app裡的資料,我們得知facebook程序沒有被調起。
既然如此,那我們何不直接copy sdk中通過contentProvider擷取facebook資料的代碼,自己先擷取一遍,取到了
allAvailableVersions
,說明facebook程序已經啟動了可以走正常分享流程,沒有取到,先啟動facebook的主activity,再走正常的分享流程。
多說無益,直接看代碼吧:(Talk is cheap,show me the code!)
kolin代碼:
object FacebookShareWrapper {
private val FACEBOOK_PACKAGE_NAME = "com.facebook.katana"
private val TAG = "FacebookShareWrapper"
fun startFacebookShareChecking(activity: Activity, shareAction: (() -> Unit)?): Disposable? {
//這一行是從NativeProtocol中fetchAllAvailableProtocolVersionsForAppInfo方法複制過來的代碼
val versionSet = FacebookProtocolVersionHelper.fetchAllAvailableProtocolVersionsForAppInfo(activity, FACEBOOK_PACKAGE_NAME)
val am = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
var isFacebookStarted = false
//facebook未安裝,或者facebook程序無法啟動
if (versionSet.size <= 0) {
val intent = activity.packageManager.getLaunchIntentForPackage(FACEBOOK_PACKAGE_NAME)
if (intent != null) {
//Facebook程序無法啟動,啟動它吧!!
try {
isFacebookStarted = true
activity.startActivity(intent)
} catch (e: Exception) {
isFacebookStarted = false
Log.e(TAG, " start activity failed intent = $intent, error msg = ${e.message}", e)
}
}
}
//facebook沒有被啟動,走正常分享流程就行
if (!isFacebookStarted) {
shareAction?.invoke()
return null
}
//檢查facebook啟動情況的标志
var intervalCheckFacebookFlag = false
//一直輪詢,直到可以取到facebook的ProtocolVersion資料
return Flowable.intervalRange(0, 20, 0, 20, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
.subscribe {
if (!intervalCheckFacebookFlag) {
val versionSet = FacebookProtocolVersionHelper.fetchAllAvailableProtocolVersionsForAppInfo(activity, FACEBOOK_PACKAGE_NAME)
if (versionSet.size > 0) {
intervalCheckFacebookFlag = true
//将應用界面移到前台,并開始facebook分享
am.moveTaskToFront(activity.taskId, 0)
shareAction?.invoke()
}
}
}
}
}
使用的時候,隻用在原來的分享流程上面包一層就行了,非常easy:
FacebookShareWrapper.startFacebookShareChecking(activity!!) {
val shareDialog = ShareDialog(activity)
val content = ShareLinkContent.Builder()
.setContentUrl(Uri.parse("https://developers.facebook.com"))
.build()
shareDialog.show(content, ShareDialog.Mode.AUTOMATIC)
}
新的問題
經過多輪測試,上面的修改方式的确在能夠大多數情況下能夠調起用戶端分享。但是,還是有非常小的機率出現先啟動了facebook app,再打開facebook網頁分享,而不是用戶端分享!!這又是哪裡出了問題呢?
我們再回頭zai看看
getLatestAvailableProtocolVersionForAppInfoList
方法:
private static ProtocolVersionQueryResult getLatestAvailableProtocolVersionForAppInfoList(
List<NativeAppInfo> appInfoList,
int[] versionSpec) {
// 這裡使用線程池異步擷取ProtocolVersions資訊
// Kick off an update
updateAllAvailableProtocolVersionsAsync();
if (appInfoList == null) {
return ProtocolVersionQueryResult.createEmpty();
}
// Could potentially cache the NativeAppInfo to latestProtocolVersion
for (NativeAppInfo appInfo : appInfoList) {
int protocolVersion =
computeLatestAvailableVersionFromVersionSpec(
appInfo.getAvailableVersions(),
getLatestKnownVersion(),
versionSpec);
if (protocolVersion != NO_PROTOCOL_AVAILABLE) {
return ProtocolVersionQueryResult.create(appInfo, protocolVersion);
}
}
return ProtocolVersionQueryResult.createEmpty();
}
updateAllAvailableProtocolVersionsAsync
方法的具體實作為:
public static void updateAllAvailableProtocolVersionsAsync() {
if (!protocolVersionsAsyncUpdating.compareAndSet(false, true)) {
return;
}
FacebookSdk.getExecutor().execute(new Runnable() {
@Override
public void run() {
try {
for (NativeAppInfo appInfo : facebookAppInfoList) {
appInfo.fetchAvailableVersions(true);
}
} finally {
protocolVersionsAsyncUpdating.set(false);
}
}
});
}
這裡也是調用了
appInfo.fetchAvailableVersions(true);
getAvailableVersions
方法的具體實作為:
private static abstract class NativeAppInfo {
abstract protected String getPackage();
abstract protected String getLoginActivity();
private TreeSet<Integer> availableVersions;
public TreeSet<Integer> getAvailableVersions() {
if (availableVersions == null) {
fetchAvailableVersions(false);
}
return availableVersions;
}
private synchronized void fetchAvailableVersions(boolean force) {
if (force || availableVersions == null) {
availableVersions = fetchAllAvailableProtocolVersionsForAppInfo(this);
}
}
}
再看看
updateAllAvailableProtocolVersionsAsync
這個方法有哪些地方調用過了:
sdkInitialize
中的關鍵代碼為:
public static synchronized void sdkInitialize(
final Context applicationContext,
final InitializeCallback callback){
// 省略其他代碼
// Fetch available protocol versions from the apps on the device
NativeProtocol.updateAllAvailableProtocolVersionsAsync();
// 省略其他代碼
}
我們發現,在sdkInitialize方法中它也被調用過,而sdkInitialize是在Application啟動時被初始化的,是以這個方法在應用剛啟動時被調用了,但此時facebook是未啟動的,并且無法被contentProvider拉起,是以傳回的ProtocolVersions是空的,這裡請注意,contentProvider查到的cursor為空時,傳回一個size為0的TreeSet給availableVersions,此時,NativeAppInfo中的availableVersions是非空的**(not null, but size = 0)**,這樣,調用其
getAvailableVersions
傳回的是大小為空的非空對象,是以不會執行``fetchAvailableVersions
方法,無擷取到真實的
availableVersions`,進而無法facebook用戶端分享。
這個bug顯然是facebook share sdk自己的bug,顯然沒有考慮到app啟動時無法調起facebook,分享時才調起facebook的流程,在sdk代碼裡直接修改的話,是非常友善的, 隻需要在:NativeAppInfo的兩個方法裡加上
size=0
的判斷就行了:
private static abstract class NativeAppInfo {
abstract protected String getPackage();
abstract protected String getLoginActivity();
private TreeSet<Integer> availableVersions;
public TreeSet<Integer> getAvailableVersions() {
// 這裡加上size() = 0的條件
if (availableVersions == null || availableVersions.size() = 0) {
fetchAvailableVersions(false);
}
return availableVersions;
}
private synchronized void fetchAvailableVersions(boolean force) {
// 這裡加上size() = 0的條件
if (force || availableVersions == null || || availableVersions.size() = 0) {
availableVersions = fetchAllAvailableProtocolVersionsForAppInfo(this);
}
}
}
這裡,我給facebook share sdk的GitHub代碼庫送出了一個Pull Request:
https://github.com/facebook/facebook-android-sdk/pull/538
送出幾天之後,就被facebook的開發人員合并到主分支中。
完整修複
雖然送出了PR,但他們不會立即為了你出一個版本。
是以,還是要想辦法在我們自己app裡來修複。其實,我們隻需要通過反射将我們上面通過contentProvider擷取到的availableVersion個設到NativeAppInfo中就行了,非常容易實作。
完整的修改代碼如下(Kotlin):
object FacebookShareWrapper {
private val FACEBOOK_PACKAGE_NAME = "com.facebook.katana"
private val TAG = "FacebookShareWrapper"
fun startFacebookShareChecking(activity: Activity, shareAction: (() -> Unit)?): Disposable? {
//這一行是從NativeProtocol中fetchAllAvailableProtocolVersionsForAppInfo方法複制過來的代碼
val versionSet = FacebookProtocolVersionHelper.fetchAllAvailableProtocolVersionsForAppInfo(activity, FACEBOOK_PACKAGE_NAME)
val am = activity.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
var isFacebookStarted = false
//facebook未安裝,或者facebook程序無法啟動
if (versionSet.size <= 0) {
val intent = activity.packageManager.getLaunchIntentForPackage(FACEBOOK_PACKAGE_NAME)
if (intent != null) {
//Facebook程序無法啟動,啟動它吧!!
try {
isFacebookStarted = true
activity.startActivity(intent)
} catch (e: Exception) {
isFacebookStarted = false
Log.e(TAG, " start activity failed intent = $intent, error msg = ${e.message}", e)
}
}
}
//facebook沒有被啟動,走正常分享流程就行
if (!isFacebookStarted) {
hookNativeProtocalFacebookAppInfoList(versionSet, FACEBOOK_PACKAGE_NAME)
shareAction?.invoke()
return null
}
//檢查facebook啟動情況的标志
var intervalCheckFacebookFlag = false
//一直輪詢,直到可以取到facebook的ProtocolVersion資料
return Flowable.intervalRange(0, 20, 0, 20, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
.subscribe {
if (!intervalCheckFacebookFlag) {
val versionSet = FacebookProtocolVersionHelper.fetchAllAvailableProtocolVersionsForAppInfo(activity, FACEBOOK_PACKAGE_NAME)
if (versionSet.size > 0) {
hookNativeProtocalFacebookAppInfoList(versionSet, FACEBOOK_PACKAGE_NAME)
intervalCheckFacebookFlag = true
//将應用界面移到前台,并開始facebook分享
am.moveTaskToFront(activity.taskId, 0)
shareAction?.invoke()
}
}
}
}
private fun hookNativeProtocalFacebookAppInfoList(protocolVersionSet: TreeSet<Int>, packageName: String) {
try {
if (protocolVersionSet.isEmpty()) {
return
}
val facebookAppInfoList = ReflectUtils.getField("com.facebook.internal.NativeProtocol", "facebookAppInfoList") as? List<*>
if (facebookAppInfoList?.isEmpty() != false) {
return
}
facebookAppInfoList.forEach {
val thisPackageName = ReflectUtils.callMethod(it, "getPackage")
Log.d(TAG, " hookNativeProtocalFacebookAppInfoList thisPackageName = $thisPackageName")
if (thisPackageName == packageName) {
ReflectUtils.setField(it, "availableVersions", protocolVersionSet)
}
}
} catch (e: Exception) {
Log.d(TAG, " hookNativeProtocalFacebookAppInfoList failed ", e)
}
}
}
其中,兩個反射方法的封裝為:
//擷取類的靜态變量的值
public static Object getField(String className, String fieldName) {
return getField(className,null, fieldName);
}
private static Object getField(String className, Object receiver, String fieldName) {
Class<?> clazz = null;
Field field;
if (!TextUtils.isEmpty(className)) {
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} else {
if (receiver != null) {
clazz = receiver.getClass();
}
}
if (clazz == null) return null;
try {
field = findField(clazz, fieldName);
if (field == null) return null;
field.setAccessible(true);
return field.get(receiver);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (NullPointerException e) {
e.printStackTrace();
}
return null;
}
public static Object callMethod(Object receiver, String methodName, Object... params) {
return callMethod(null, receiver, methodName, params);
}
private static Object callMethod(String className, Object receiver, String methodName, Object... params) {
Class<?> clazz = null;
if (!TextUtils.isEmpty(className)) {
try {
clazz = Class.forName(className);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
} else {
if (receiver != null) {
clazz = receiver.getClass();
}
}
if (clazz == null) return null;
try {
Method method = findMethod(clazz, methodName, params);
if (method == null) {
return null;
}
method.setAccessible(true);
return method.invoke(receiver, params);
} catch (IllegalArgumentException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
至此,我們完美地解決了facebook分享無法調起facebook用戶端的問題。
總結
最後,對上面的問題原因以及修複方法作一個簡單的總結。
主要有兩個原因導緻facebook分享無法調起用戶端:
- facebook分享sdk在分享時,會先過contentProvider跨程序調用擷取facebook App儲存的protocolVersion資料,并根據該資料決定用戶端是否支援此次分享。由于在一些國産Rom上增加了禁止第三方自啟動(或者禁止應用間互相啟動)功能,導緻此次跨程序啟動facebook app失敗,facebook sdk擷取不到用戶端資料,是以,就無法調起用戶端分享,而是調起了網頁分享。
- facebook sdk本身存在一個TreeSet非空判斷的bug,導緻先起自己的app,再起facebook app時,擷取facebook app資料失敗,進而導緻分享失敗。
這兩個問題對應的修複方法為:
- 先判斷跨程序調用facebook app是否會失敗,失敗的話,先啟動facebook app,再跳回自己的應用走正常分享流程。
- 通過反射修複TreeSet非空判斷的bug。
End。