[貝聊科技]有關Android應用桌面角標(BadgeNumber)實現的探討

貝聊科技發表於2017-10-30

作者:小強 貝聊移動開發部 Android工程師 前言:本文主要講述了以下三方面:

  1. 怎麼在Android系統下讓自家的應用圖示像iOS系統那樣支援數字角標的顯示?
  2. 在網上找不到現成的解決方案的情況下,該如何去尋找問題的突破口?
  3. 一種簡潔性和擴充套件性都比較高的封裝思路

先放一個傳送門:GitHub傳送門

1.Android系統下如何支援應用桌面角標(BadgeNumber)的顯示

iOS系統下的應用桌面角標
其實本來Android原生系統是不支援應用桌面角標(BadgeNumber)顯示的。我們目前看到的能支援應用桌面角標顯示的Android系統,都是第三方廠商自己定製的。通過實現一套自己的launcher並且提供外部介面給第三方應用來呼叫即可。

我們公司的APP裡涉及到IM的功能。所以經常會有使用者向客服反饋,為什麼某Q、某信都支援應用桌面角標的顯示,但你們的APP卻不行......本著使用者就是上帝的原則,於是應用桌面角標顯示的優化就提上了日程。其實,測試部門在之前就已經跟我們提過這事了,只不過當時正忙於專案開發,沒時間優化。前段時間需求不多的時候,給公司的Android應用加上了桌面角標顯示的支援。現在將這個優化的過程總結一下。

目前已經存在的開源庫

如果大家有接觸過這方面的優化,應該很快就可以在搜尋引擎上找到某個被推薦次數較多的開源庫 ShortcutBadger

[貝聊科技]有關Android應用桌面角標(BadgeNumber)實現的探討

雖然這個庫適配的覆蓋機型貌似很多,但在實際的測試中發現,某些方法可能對於目前市面上的國產流行機型已經不奏效了。所以,不建議大家直接將這個開源專案用到專案中去。作為學習和參考倒是一個不錯的選擇。而且,在實際方案抉擇的過程中,我們發現,公司的APP主流機型排行榜中,前十的機型幾乎被OPPO、vivo、華為、小米這四個品牌屠榜了。所以,我們的優化目標暫時就先定下來了:先集中精力適配市面上的這四個主流品牌機型。其他的冷門機型,後面再慢慢完善。(其實實際上我們也找不來那麼多冷門的機型進行測試,所以對於沒自身確認過奏效的方案,即使網上已經有人給出,出於謹慎還是先不採納)

國產主流機型應用角標的適配(OPPO、vivo、華為、小米)

在開始之前,先宣告一下。第一,不是所有的國產手機都能找到支援角標顯示的方案(即使理論上可以,可能人家只對某Q某信等一些國民級的應用開放設定應用角標的白名單)。第二,本文中涉及到的方案都是經過實際測試且奏效的了(因為測試手機有限,所以不敢說針對這四個品牌的手機機型百分百支援,但支援大部分的機型應該是沒問題的)。而且,有些品牌的手機適配方案很容易找到,有些品牌的適配方案則很難找到,這部分我會放到後面的章節來說。下面直接上適配方案:

華為:

先在AndroidManifest檔案裡配置好下面的許可權:

 <!--華為手機更新應用桌面角標需要的許可權-->
    <uses-permission android:name="com.huawei.android.launcher.permission.CHANGE_BADGE"/>
複製程式碼

設定角標的方法如下:

 public static void setBadgeNumber(Context context, int number) {
        try {
            if (number < 0) number = 0;
            Bundle bundle = new Bundle();
            bundle.putString("package", context.getPackageName());
            String launchClassName = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()).getComponent().getClassName();
            bundle.putString("class", launchClassName);
            bundle.putInt("badgenumber", number);
            context.getContentResolver().call(Uri.parse("content://com.huawei.android.launcher.settings/badge/"), "change_badge", null, bundle);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製程式碼

OPPO:

    public static void setBadgeNumber(Context context, int number) {
        try {
            if (number == 0) {
                number = -1;
            }
            Intent intent = new Intent("com.oppo.unsettledevent");
            intent.putExtra("pakeageName", context.getPackageName());
            intent.putExtra("number", number);
            intent.putExtra("upgradeNumber", number);
            if (canResolveBroadcast(context, intent)) {
                context.sendBroadcast(intent);
            } else {
                try {
                    Bundle extras = new Bundle();
                    extras.putInt("app_badge_count", number);
                    context.getContentResolver().call(Uri.parse("content://com.android.badge/badge"), "setAppBadgeCount", null, extras);
                } catch (Throwable t) {
                    t.printStackTrace();
                }
            }

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static boolean canResolveBroadcast(Context context, Intent intent) {
        PackageManager packageManager = context.getPackageManager();
        List<ResolveInfo> receivers = packageManager.queryBroadcastReceivers(intent, 0);
        return receivers != null && receivers.size() > 0;
    }
複製程式碼

vivo:

public static void setBadgeNumber(Context context, int number) {
        try {
            Intent intent = new Intent("launcher.action.CHANGE_APPLICATION_NOTIFICATION_NUM");
            intent.putExtra("packageName", context.getPackageName());
            String launchClassName = context.getPackageManager().getLaunchIntentForPackage(context.getPackageName()).getComponent().getClassName();
            intent.putExtra("className", launchClassName);
            intent.putExtra("notificationNum", number);
            context.sendBroadcast(intent);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製程式碼

小米:

小米的設定應用角標方式比較有個性,跟其他廠商的不太一樣,是跟Notification繫結在一起的。而且小米系統還有個比較特殊的地方,如果在應用內直接呼叫設定角標的方法,設定角標會不生效,所以只能在應用在後臺並且收到推送的情況下進行角標的設定。另外,即使你設定了角標的顯示,只要使用者點選應用圖示進入到應用內,應用的角標就會自動消失掉,即使應用內還存在新的未讀訊息。所以,針對小米機型,建議在收到推送後並且進行notification的時機更新應用角標

//在呼叫NotificationManager.notify(notifyID, notification)這個方法之前先設定角標顯示的數目

public static void setBadgeNumber(Notification notification, int number) {
        try {
            Field field = notification.getClass().getDeclaredField("extraNotification");
            Object extraNotification = field.get(notification);
            Method method = extraNotification.getClass().getDeclaredMethod("setMessageCount", int.class);
            method.invoke(extraNotification, number);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
複製程式碼

2.在網上找不到現成的解決方案的情況下,該如何去尋找問題的突破口?

在上面的適配方案中,最容易找到而且奏效的就是華為和小米的適配方案。而OPPO的適配方案,即使找到了,在現有的測試機型上卻不奏效;vivo的適配方案則是最難找的。既然在網上找不到,而在國內某Q和某信貌似又是適配得最好的,這就說明,某Q和某信的原始碼裡肯定有現成的解決方案。那麼,不如嘗試一下反編譯,看看能不能從這兩個APP中找到一些靈感?

在對某Q的apk進行反編譯後,在某個類下果然找到了設定應用角標的實現類:

某Q設定桌面角標的實現類

從上圖可以看出,某Q對於各種廠商的適配算是比較完善的了。除了小米、華為、OPPO、vivo,還適配了聯想、三星、索尼等。

不同機型的適配方法也都有具體的實現:(下面是對於OPPO和vivo的適配)

某Q對於OPPO和vivo的適配

但是,我們也不能直接拷貝過來就使用。因為說不定有些方法只針對某Q才生效呢是吧?

在對某信的apk進行反編譯後,也能找到關於應用角標適配的程式碼:

某信對於vivo手機桌面角標的適配

總之,對比了一下某Q和某信的原始碼,在某些機型的適配方式上,可能兩邊會有些出入。實現方式可能也不太一樣。但不得不說,不愧是大廠的APP,看了原始碼後,實在是學習了很多,特別是一些細節上的處理。

上面總結出的適配方案,其實就是在參考了網上各種資料以及某Q和某信的原始碼之後總結出來的可行的適配方案。如果還不滿足大家的需求,大家可以發揮一下自己的主觀能動性,找到自己想要的解決方案,並總結出一套屬於自己的適配方案。

3.一種擴充套件性比較高的簡潔的封裝思路

看完了某Q和某信的原始碼後,我發現兩邊都有一個共同點,那就是某個實現類裡塞了很多適配的方法。估計也是可能涉及到不同的人在不同時期維護的歷史原因。但一個類裡面的程式碼太多了,可能會對查閱和後續維護造成一些不便。

這裡,我參考了Android原始碼裡面NotificationManagerCompat這個類的實現方式。Android原始碼中本身就涉及到很多關於不同版本的適配的場景。某個方法,在不同的版本下,可能實現方式不太一樣。於是,怎麼在不斷往某個類增加不同的實現方式的情況下,保持程式碼的美觀以及擴充套件性易讀性變成了一個問題。NotificationManagerCompat這個類的實現就十分簡潔美觀。下面是一部分原始碼截圖,有興趣的可以直接去看一下完整的原始碼。

[貝聊科技]有關Android應用桌面角標(BadgeNumber)實現的探討

下面就是模仿後的實現:

public class BadgeNumberManager {

    private Context mContext;

    private BadgeNumberManager(Context context) {
        mContext = context;
    }

    public static BadgeNumberManager from(Context context) {
        return new BadgeNumberManager(context);
    }

    private static final BadgeNumberManager.Impl IMPL;

    /**
     * 設定應用在桌面上顯示的角標數字
     * @param number 顯示的數字
     */
    public void setBadgeNumber(int number) {
        IMPL.setBadgeNumber(mContext, number);
    }

    interface Impl {

        void setBadgeNumber(Context context, int number);

    }

    static class ImplHuaWei implements Impl {

        @Override
        public void setBadgeNumber(Context context, int number) {
            BadgeNumberManagerHuaWei.setBadgeNumber(context, number);
        }
    }

    static class ImplVIVO implements Impl {

        @Override
        public void setBadgeNumber(Context context, int number) {
            BadgeNumberManagerVIVO.setBadgeNumber(context, number);
        }
    }


    static class ImplBase implements Impl {

        @Override
        public void setBadgeNumber(Context context, int number) {
            //do nothing
        }
    }

    static {
        String manufacturer = Build.MANUFACTURER;
        if (manufacturer.equalsIgnoreCase("Huawei")) {
            IMPL = new ImplHuaWei();
        } else if (manufacturer.equalsIgnoreCase("vivo")) {
            IMPL = new ImplVIVO();
        } else if (manufacturer.equalsIgnoreCase("XXX")) {
            //其他品牌機型的實現類
            IMPL = new ImplXXX();
            ......
        } else {
            IMPL = new ImplBase();
        }
    }
}
複製程式碼

使用的時候,只需要呼叫BadgeNumberManager.from(context).setBadgeNumber(num)就行了。BadgeNumberManagerHuaWeiBadgeNumberManagerVIVO等都是針對某個手機品牌的具體實現類。

當然,這只是一種實現的思路而已。具體去實現的時候,請根據自己專案的實際情況,怎樣實現擴充套件性可讀性較高就選哪種。

4.原始碼GitHub地址

為了響應部分童鞋的要求,現已將BadgeNumberManager的實現原始碼上傳到了GitHub: beiliao-mobile:BadgeNumberManager

5.後記

如果有關於別的機型的適配方案,歡迎在評論下留言(最好是自己親自測試過並且有效的)。如果文章中有出現錯誤的地方,歡迎指正。如果對於文章中的某些部分有不同的理解和想法,或者有更好的想法, 也歡迎留言討論。

注:本文僅供技術學習探討使用。如果文中有不適宜的內容,請聯絡我們,我們會第一時間處理:)

6.填坑記錄(2017.12.11)

  1. 經過測試,目前暫時不支援的機型:華為榮耀6、OPPO A59、OPPO R9,OPPO R11、vivo X9i(截止至2017.12.11)

  2. 一開始以為某些機型不支援可能是少了某些跟角標設定相關的許可權,於是反編譯微信、QQ、支付寶,從這些App中收集AndroidManifest裡配置的可能跟角標設定相關的許可權,並新增到Demo中來測試,後來發現還是不行

  3. 針對華為手機,在某些機型上,例如華為 mate9,在manifest裡除了需要配置com.huawei.android.launcher.permission.CHANGE_BADGE許可權之外,還需要配置android.permission.INTERNET許可權才可以正常設定桌面角標(不過一般的App應該都會配置了android.permission.INTERNET許可權)

  4. 關於OPPO手機,在一些較舊的機型上可以正常設定桌面角標,但在一些比較新的機型上(例如OPPO R9,OPPO R11等),只有在通知許可權管理中,有“在桌面圖示上顯示角標”這個選項的App才可以正常設定角標。目前就只發現QQ,微信,釘釘有這個許可權,就連支付寶都沒有這個許可權。於是嘗試著寫了個Demo,將Demo的包名改成了微信的包名,然後在通知許可權管理中,就出現了“在桌面圖示上顯示圖示”這個選項。所以,在新的機型上,OPPO應該是根據包名來維護了一個白名單,只針對一些比較大型的IM型別的App開放桌面角標設定的許可權。所以,這個問題暫時還沒有解決方法

相關文章