原文來自http://blog.zanlabs.com/2015/03/14/android-shortcut-summary/
將近二個多月沒寫博客了。
之前一段時間一直在搞紅包助手,就沒抽時間寫博客,但寫這個真的是很好玩。沒想到居然在Android上實現模擬點擊,從而實現自動搶紅包,有興趣的同學可以參考https://github.com/waylife/RedEnvelopeAssistant ,代碼已經開源。
紅包助手還有一些問題,但是現在基本的搶紅包基本沒問題了。目前正在對它進行優化以及較低版本的一些適配,還有項目的國際化工作。
廢話不多說了,下面是Andrioid開發過程中快捷方式相關的事與坑。
數據來源於上次組內自己的CodeReview總結。
背景
一般情況下,為了讓用戶更方便的打開應用,程序會在桌面上生成一些快捷方式。
本來呢,如果是原生的桌面,其實是十分簡單,直接調用系統相關的API就行了。但是眾多的系統廠商以及眾多第三方自己定制的桌面(Launcher),導致在適配、兼容方面存在很多問題。
比如,有些桌面無法刪除快捷方式(比如小米),有些桌面無法生成快捷方式(比如錘子),有些系統無法更新桌面圖標(比如華為榮耀6)。
在升級、降級的時候快捷方式發生變化;比如,全部變成應用的主圖標,升級、降級后點擊快捷方式沒有反應,刪除應用后無法刪除快捷方式。
很多問題都是需要解決的,雖然有些由於系統限制,沒有辦法搞定所有的,但是仍然需要尋求一個最優的方案。這也就是本文需要討論的問題。
本文說指的快捷方式是指應用桌面快捷方式,不包含長按彈出的生成快捷方式。
快捷方式所有信息都是存在於launcher的favorite表。一般需要用到的字段為_id,title,intent,iconResource,icon,分別表示 快捷方式名稱,快捷方式intent,快捷方式圖標(本地),快捷方式圖標(data二進制壓縮數據)。
兩個intent數據如下
數據可以通過SQLite Editor查看,需要已經ROOT的手機
實現
增加快捷方式
在AndroidManifest.xml增加權限
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
同時,根據Intent是隱式還是顯示在相關的Activity聲明相關的intent-filter。
相關代碼:
刪除快捷方式
跟增加快捷方式一樣,也是需要增加權限的。加上
<uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT" />
快捷方式修改
需要增加權限
<uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.android.launcher.permission.WRITE_SETTINGS" />
如果適配所有桌面,請添加附錄中第二條所列出的權限。
系統並沒有提供API去更改桌面快捷方式。只能通過其他猥瑣的辦法了,可行的的辦法之一就是通過ContentProvider去更改數據庫相關的信息。當然有人會說了,先刪掉快捷方式,再重新創建不就行了?這是個辦法。但是有些系統是無法刪除快捷方式的;另外,刪除快捷方式與創建快捷方式都是通過廣播實現的,這個地方需要控制兩者的時間間隔。權衡之后,選用第一種辦法相對穩妥。
廢話不多少,上代碼。
/** * 更新桌面快捷方式圖標,不一定所有圖標都有效<br/> * 如果快捷方式不存在,則不更新<br/>. */ public static void updateShortcutIcon(Context context, String title, Intent intent,Bitmap bitmap) { if(bitmap==null){ XLog.i(TAG, "update shortcut icon,bitmap empty"); return; } try{ final ContentResolver cr = context.getContentResolver(); StringBuilder uriStr = new StringBuilder(); String urlTemp=""; String authority = LauncherUtil.getAuthorityFromPermissionDefault(context); if(authority==null||authority.trim().equals("")){ authority = LauncherUtil.getAuthorityFromPermission(context,LauncherUtil.getCurrentLauncherPackageName(context)+".permission.READ_SETTINGS"); } uriStr.append("content://"); if (TextUtils.isEmpty(authority)) { int sdkInt = android.os.Build.VERSION.SDK_INT; if (sdkInt < 8) { // Android 2.1.x(API 7)以及以下的 uriStr.append("com.android.launcher.settings"); } else if (sdkInt < 19) {// Android 4.4以下 uriStr.append("com.android.launcher2.settings"); } else {// 4.4以及以上 uriStr.append("com.android.launcher3.settings"); } } else { uriStr.append(authority); } urlTemp=uriStr.toString(); uriStr.append("/favorites?notify=true"); Uri uri = Uri.parse(uriStr.toString()); Cursor c = cr.query(uri, new String[] {"_id", "title", "intent" }, "title=? and intent=? ", new String[] { title, intent.toUri(0) }, null); int index=-1; if (c != null && c.getCount() > 0) { c.moveToFirst(); index=c.getInt(0);//獲得圖標索引 ContentValues cv=new ContentValues(); cv.put("icon", flattenBitmap(bitmap)); Uri uri2=Uri.parse(urlTemp+"/favorites/"+index+"?notify=true"); int i=context.getContentResolver().update(uri2, cv, null,null); context.getContentResolver().notifyChange(uri,null);//此處不能用uri2,是個坑 XLog.i(TAG, "update ok: affected "+i+" rows,index is"+index); }else{ XLog.i(TAG, "update result failed"); } if (c != null && !c.isClosed()) { c.close(); } }catch(Exception ex){ ex.printStackTrace(); XLog.i(TAG, "update shortcut icon,get errors:"+ex.getMessage()); } } private static byte[] flattenBitmap(Bitmap bitmap) { // Try go guesstimate how much space the icon will take when serialized // to avoid unnecessary allocations/copies during the write. int size = bitmap.getWidth() * bitmap.getHeight() * 4; ByteArrayOutputStream out = new ByteArrayOutputStream(size); try { bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); out.flush(); out.close(); return out.toByteArray(); } catch (IOException e) { XLog.w(TAG, "Could not write icon"); return null; } }
快捷方式存在判斷
需要增加的權限同修改快捷方式
雖然說通過SharePreference來保證快捷方式不會重復創建,以及通過shortcutIntent.putExtra(“duplicate”, false)也可以確保,但是為了萬無一失,還是可以通過去查詢數據判斷快捷方式是否存在,來避免重復創建。 代碼如下:
/** * 檢查快捷方式是否存在 <br/> * <font color=red>注意:</font> 有些手機無法判斷是否已經創建過快捷方式<br/> * 因此,在創建快捷方式時,請添加<br/> * shortcutIntent.putExtra("duplicate", false);// 不允許重復創建<br/> * 最好使用{@link #isShortCutExist(Context, String, Intent)} * 進行判斷,因為可能有些應用生成的快捷方式名稱是一樣的的<br/> * 此處需要在AndroidManifest.xml中配置相關的桌面權限信息<br/> * 錯誤信息已捕獲<br/> */ public static boolean isShortCutExist(Context context, String title) { boolean result = false; try { final ContentResolver cr = context.getContentResolver(); StringBuilder uriStr = new StringBuilder(); String authority = LauncherUtil.getAuthorityFromPermissionDefault(context); if(authority==null||authority.trim().equals("")){ authority = LauncherUtil.getAuthorityFromPermission(context,LauncherUtil.getCurrentLauncherPackageName(context)+".permission.READ_SETTINGS"); } uriStr.append("content://"); if (TextUtils.isEmpty(authority)) { int sdkInt = android.os.Build.VERSION.SDK_INT; if (sdkInt < 8) { // Android 2.1.x(API 7)以及以下的 uriStr.append("com.android.launcher.settings"); } else if (sdkInt < 19) {// Android 4.4以下 uriStr.append("com.android.launcher2.settings"); } else {// 4.4以及以上 uriStr.append("com.android.launcher3.settings"); } } else { uriStr.append(authority); } uriStr.append("/favorites?notify=true"); Uri uri = Uri.parse(uriStr.toString()); Cursor c = cr.query(uri, new String[] { "title" }, "title=? ", new String[] { title }, null); if (c != null && c.getCount() > 0) { result = true; } if (c != null && !c.isClosed()) { c.close(); } } catch (Exception e) { e.printStackTrace(); result=false; } return result; } /** * 不一定所有的手機都有效,因為國內大部分手機的桌面不是系統原生的<br/> * 更多請參考{@link #isShortCutExist(Context, String)}<br/> * 桌面有兩種,系統桌面(ROM自帶)與第三方桌面,一般只考慮系統自帶<br/> * 第三方桌面如果沒有實現系統響應的方法是無法判斷的,比如GO桌面<br/> * 此處需要在AndroidManifest.xml中配置相關的桌面權限信息<br/> * 錯誤信息已捕獲<br/> */ public static boolean isShortCutExist(Context context, String title, Intent intent) { boolean result = false; try{ final ContentResolver cr = context.getContentResolver(); StringBuilder uriStr = new StringBuilder(); String authority = LauncherUtil.getAuthorityFromPermissionDefault(context); if(authority==null||authority.trim().equals("")){ authority = LauncherUtil.getAuthorityFromPermission(context,LauncherUtil.getCurrentLauncherPackageName(context)+".permission.READ_SETTINGS"); } uriStr.append("content://"); if (TextUtils.isEmpty(authority)) { int sdkInt = android.os.Build.VERSION.SDK_INT; if (sdkInt < 8) { // Android 2.1.x(API 7)以及以下的 uriStr.append("com.android.launcher.settings"); } else if (sdkInt < 19) {// Android 4.4以下 uriStr.append("com.android.launcher2.settings"); } else {// 4.4以及以上 uriStr.append("com.android.launcher3.settings"); } } else { uriStr.append(authority); } uriStr.append("/favorites?notify=true"); Uri uri = Uri.parse(uriStr.toString()); Cursor c = cr.query(uri, new String[] { "title", "intent" }, "title=? and intent=?", new String[] { title, intent.toUri(0) }, null); if (c != null && c.getCount() > 0) { result = true; } if (c != null && !c.isClosed()) { c.close(); } }catch(Exception ex){ result=false; ex.printStackTrace(); } return result; }
兼容與注意事項
兼容
所有的快捷方式Intent如果不是之前版本的存在很大問題,絕對不要改變參數,否則升級或者降級時快捷方式會出現問題;
同時,盡可能的采用隱式調用,自定義CATEGORY,而不是自定義ACTION,ACTION參數一定要為ACTION_MAIN,否則有些手機在卸載時無法刪除快捷方式(WTF)。
注意事項
- 【所有】activity路徑的變更導致老版本升級之后快捷方式無法使用
—> 1.一旦使用確定了activity的包路徑,之后就不要再變更;
—> 2.盡可能使用隱式調用,但是如果之前已經發出去的版本,為了兼容性,就必須一直使用老的方式,新版本的盡可能的不要更改方式,如果用戶降級,老版本快捷方式會無法使用。 - 【部分】多個快捷方式指向一個activity導致部分手機(三星SII)升級時圖標變成應用圖標
—> 盡可能的避免多個快捷方式指向同一個activity,可能通過多個activity再跳轉過去 - 【部分】應用刪除時無法刪除快捷方式。與系統桌面Launcher實現有關。
—> 為了適配所有Launcher,Intent Action使用Intent.ACTION_MAIN。如果是隱式調用,盡可能自定義CATEGORY,而不是自定義ACTION。 - 【部分】應用升級時需要刪除老版本部分快捷方式,但是部分手機無法刪除
—> 無解 - 【部分】第三方桌面無法生成、刪除、更新快捷方式
—> 呵呵,一般來說生成沒有問題,但是刪除,更新大部分桌面會有問題。盡可能避免這些操作。或者專門適配該桌面,成本較高。 - 【部分】部分桌面無法實時更新圖標,需要重啟
—> 無解,嘗試過重啟Launcher,但是結果是之前的快捷方式也消失了。只有重啟手機,按理來說應該是有方式觸發Launcher進行刷新的。
以上【所有】【部分】,分別表示必定出現,部分出現。
參考
附錄
- 完整代碼可參考
https://gist.github.com/waylife/437a3d98a84f245b9582 - 通用更新快捷方式權限列表
<uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.android.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.android.launcher2.permission.READ_SETTINGS" /> <uses-permission android:name="com.android.launcher2.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.android.launcher3.permission.READ_SETTINGS" /> <uses-permission android:name="com.android.launcher3.permission.WRITE_SETTINGS" /> <uses-permission android:name="org.adw.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="org.adw.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.htc.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.htc.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.qihoo360.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.qihoo360.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.lge.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.lge.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="net.qihoo.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="net.qihoo.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="org.adwfreak.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="org.adwfreak.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="org.adw.launcher_donut.permission.READ_SETTINGS" /> <uses-permission android:name="org.adw.launcher_donut.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.huawei.launcher3.permission.READ_SETTINGS" /> <uses-permission android:name="com.huawei.launcher3.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.fede.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.fede.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.sec.android.app.twlauncher.settings.READ_SETTINGS" /> <uses-permission android:name="com.sec.android.app.twlauncher.settings.WRITE_SETTINGS" /> <uses-permission android:name="com.anddoes.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.anddoes.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.tencent.qqlauncher.permission.READ_SETTINGS" /> <uses-permission android:name="com.tencent.qqlauncher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.huawei.launcher2.permission.READ_SETTINGS" /> <uses-permission android:name="com.huawei.launcher2.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.android.mylauncher.permission.READ_SETTINGS" /> <uses-permission android:name="com.android.mylauncher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.ebproductions.android.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.ebproductions.android.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.oppo.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.oppo.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="com.huawei.android.launcher.permission.READ_SETTINGS" /> <uses-permission android:name="com.huawei.android.launcher.permission.WRITE_SETTINGS" /> <uses-permission android:name="telecom.mdesk.permission.READ_SETTINGS" /> <uses-permission android:name="telecom.mdesk.permission.WRITE_SETTINGS" /> <uses-permission android:name="dianxin.permission.ACCESS_LAUNCHER_DATA" />