天天看點

Android App接入Facebook分享SDK,機率性無法啟動Facebook用戶端的問題分析

問題來源

由于我司的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傳回的結果為:

Android App接入Facebook分享SDK,機率性無法啟動Facebook用戶端的問題分析

這些日期資訊,猜測應該是目前的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

這個方法有哪些地方調用過了:

Android App接入Facebook分享SDK,機率性無法啟動Facebook用戶端的問題分析

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分享無法調起用戶端:

  1. facebook分享sdk在分享時,會先過contentProvider跨程序調用擷取facebook App儲存的protocolVersion資料,并根據該資料決定用戶端是否支援此次分享。由于在一些國産Rom上增加了禁止第三方自啟動(或者禁止應用間互相啟動)功能,導緻此次跨程序啟動facebook app失敗,facebook sdk擷取不到用戶端資料,是以,就無法調起用戶端分享,而是調起了網頁分享。
  2. facebook sdk本身存在一個TreeSet非空判斷的bug,導緻先起自己的app,再起facebook app時,擷取facebook app資料失敗,進而導緻分享失敗。

這兩個問題對應的修複方法為:

  1. 先判斷跨程序調用facebook app是否會失敗,失敗的話,先啟動facebook app,再跳回自己的應用走正常分享流程。
  2. 通過反射修複TreeSet非空判斷的bug。

End。

繼續閱讀