zoukankan      html  css  js  c++  java
  • 桌面快捷方式的问题-创建-删除-判断

     
     

    原文来自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增加权限

    [html] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. <uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />  

    同时,根据Intent是隐式还是显示在相关的Activity声明相关的intent-filter。
    相关代码:

    删除快捷方式

    跟增加快捷方式一样,也是需要增加权限的。加上

    [html] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. <uses-permission android:name="com.android.launcher.permission.UNINSTALL_SHORTCUT" />  

    相关代码:

    快捷方式修改

    需要增加权限

    [html] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. <uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" />  
    2. <uses-permission android:name="com.android.launcher.permission.WRITE_SETTINGS" />  
    如果适配所有桌面,请添加附录中第二条所列出的权限。
    系统并没有提供API去更改桌面快捷方式。只能通过其他猥琐的办法了,可行的的办法之一就是通过ContentProvider去更改数据库相关的信息。当然有人会说了,先删掉快捷方式,再重新创建不就行了?这是个办法。但是有些系统是无法删除快捷方式的;另外,删除快捷方式与创建快捷方式都是通过广播实现的,这个地方需要控制两者的时间间隔。权衡之后,选用第一种办法相对稳妥。
    废话不多少,上代码。
    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. /** 
    2.   * 更新桌面快捷方式图标,不一定所有图标都有效<br/> 
    3.   * 如果快捷方式不存在,则不更新<br/>. 
    4.   */  
    5.  public static void updateShortcutIcon(Context context, String title, Intent intent,Bitmap bitmap) {  
    6.   if(bitmap==null){  
    7.    XLog.i(TAG, "update shortcut icon,bitmap empty");  
    8.    return;  
    9.   }  
    10.   try{  
    11.    final ContentResolver cr = context.getContentResolver();  
    12.    StringBuilder uriStr = new StringBuilder();  
    13.    String urlTemp="";  
    14.    String authority = LauncherUtil.getAuthorityFromPermissionDefault(context);  
    15.    if(authority==null||authority.trim().equals("")){  
    16.     authority = LauncherUtil.getAuthorityFromPermission(context,LauncherUtil.getCurrentLauncherPackageName(context)+".permission.READ_SETTINGS");  
    17.    }  
    18.    uriStr.append("content://");  
    19.    if (TextUtils.isEmpty(authority)) {  
    20.     int sdkInt = android.os.Build.VERSION.SDK_INT;  
    21.     if (sdkInt < 8) { // Android 2.1.x(API 7)以及以下的  
    22.      uriStr.append("com.android.launcher.settings");  
    23.     } else if (sdkInt < 19) {// Android 4.4以下  
    24.      uriStr.append("com.android.launcher2.settings");  
    25.     } else {// 4.4以及以上  
    26.      uriStr.append("com.android.launcher3.settings");  
    27.     }  
    28.    } else {  
    29.     uriStr.append(authority);  
    30.    }  
    31.    urlTemp=uriStr.toString();  
    32.    uriStr.append("/favorites?notify=true");  
    33.    Uri uri = Uri.parse(uriStr.toString());  
    34.    Cursor c = cr.query(uri, new String[] {"_id", "title", "intent" },  
    35.      "title=?  and intent=? ",  
    36.      new String[] { title, intent.toUri(0) }, null);  
    37.    int index=-1;  
    38.    if (c != null && c.getCount() > 0) {  
    39.     c.moveToFirst();  
    40.     index=c.getInt(0);//获得图标索引  
    41.     ContentValues cv=new ContentValues();  
    42.     cv.put("icon", flattenBitmap(bitmap));  
    43.     Uri uri2=Uri.parse(urlTemp+"/favorites/"+index+"?notify=true");  
    44.     int i=context.getContentResolver().update(uri2, cv, null,null);  
    45.     context.getContentResolver().notifyChange(uri,null);//此处不能用uri2,是个坑  
    46.     XLog.i(TAG, "update ok: affected "+i+" rows,index is"+index);  
    47.    }else{  
    48.     XLog.i(TAG, "update result failed");  
    49.    }  
    50.    if (c != null && !c.isClosed()) {  
    51.     c.close();  
    52.    }  
    53.   }catch(Exception ex){  
    54.    ex.printStackTrace();  
    55.    XLog.i(TAG, "update shortcut icon,get errors:"+ex.getMessage());  
    56.   }  
    57.  }  
    58.  private static byte[] flattenBitmap(Bitmap bitmap) {  
    59.   // Try go guesstimate how much space the icon will take when serialized  
    60.   // to avoid unnecessary allocations/copies during the write.  
    61.   int size = bitmap.getWidth() * bitmap.getHeight() * 4;  
    62.   ByteArrayOutputStream out = new ByteArrayOutputStream(size);  
    63.   try {  
    64.    bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);  
    65.    out.flush();  
    66.    out.close();  
    67.    return out.toByteArray();  
    68.   } catch (IOException e) {  
    69.    XLog.w(TAG, "Could not write icon");  
    70.    return null;  
    71.   }  
    72.  }  

    快捷方式存在判断

    需要增加的权限同修改快捷方式
    虽然说通过SharePreference来保证快捷方式不会重复创建,以及通过shortcutIntent.putExtra(“duplicate”, false)也可以确保,但是为了万无一失,还是可以通过去查询数据判断快捷方式是否存在,来避免重复创建。 代码如下:

    [java] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. /** 
    2.   * 检查快捷方式是否存在 <br/> 
    3.   * <font color=red>注意:</font> 有些手机无法判断是否已经创建过快捷方式<br/> 
    4.   * 因此,在创建快捷方式时,请添加<br/> 
    5.   * shortcutIntent.putExtra("duplicate", false);// 不允许重复创建<br/> 
    6.   * 最好使用{@link #isShortCutExist(Context, String, Intent)} 
    7.   * 进行判断,因为可能有些应用生成的快捷方式名称是一样的的<br/> 
    8.   * 此处需要在AndroidManifest.xml中配置相关的桌面权限信息<br/> 
    9.   * 错误信息已捕获<br/> 
    10.   */  
    11.  public static boolean isShortCutExist(Context context, String title) {  
    12.   boolean result = false;  
    13.   try {  
    14.    final ContentResolver cr = context.getContentResolver();  
    15.    StringBuilder uriStr = new StringBuilder();  
    16.    String authority = LauncherUtil.getAuthorityFromPermissionDefault(context);  
    17.    if(authority==null||authority.trim().equals("")){  
    18.     authority = LauncherUtil.getAuthorityFromPermission(context,LauncherUtil.getCurrentLauncherPackageName(context)+".permission.READ_SETTINGS");  
    19.    }  
    20.    uriStr.append("content://");  
    21.    if (TextUtils.isEmpty(authority)) {  
    22.     int sdkInt = android.os.Build.VERSION.SDK_INT;  
    23.     if (sdkInt < 8) { // Android 2.1.x(API 7)以及以下的  
    24.      uriStr.append("com.android.launcher.settings");  
    25.     } else if (sdkInt < 19) {// Android 4.4以下  
    26.      uriStr.append("com.android.launcher2.settings");  
    27.     } else {// 4.4以及以上  
    28.      uriStr.append("com.android.launcher3.settings");  
    29.     }  
    30.    } else {  
    31.     uriStr.append(authority);  
    32.    }  
    33.    uriStr.append("/favorites?notify=true");  
    34.    Uri uri = Uri.parse(uriStr.toString());  
    35.    Cursor c = cr.query(uri, new String[] { "title" },  
    36.      "title=? ",  
    37.      new String[] { title }, null);  
    38.    if (c != null && c.getCount() > 0) {  
    39.     result = true;  
    40.    }  
    41.    if (c != null && !c.isClosed()) {  
    42.     c.close();  
    43.    }  
    44.   } catch (Exception e) {  
    45.    e.printStackTrace();  
    46.    result=false;  
    47.   }  
    48.   return result;  
    49.  }  
    50.  /** 
    51.   * 不一定所有的手机都有效,因为国内大部分手机的桌面不是系统原生的<br/> 
    52.   * 更多请参考{@link #isShortCutExist(Context, String)}<br/> 
    53.   * 桌面有两种,系统桌面(ROM自带)与第三方桌面,一般只考虑系统自带<br/> 
    54.   * 第三方桌面如果没有实现系统响应的方法是无法判断的,比如GO桌面<br/> 
    55.   * 此处需要在AndroidManifest.xml中配置相关的桌面权限信息<br/> 
    56.   * 错误信息已捕获<br/> 
    57.   */  
    58.  public static boolean isShortCutExist(Context context, String title, Intent intent) {  
    59.   boolean result = false;  
    60.   try{  
    61.    final ContentResolver cr = context.getContentResolver();  
    62.    StringBuilder uriStr = new StringBuilder();  
    63.    String authority = LauncherUtil.getAuthorityFromPermissionDefault(context);  
    64.    if(authority==null||authority.trim().equals("")){  
    65.     authority = LauncherUtil.getAuthorityFromPermission(context,LauncherUtil.getCurrentLauncherPackageName(context)+".permission.READ_SETTINGS");  
    66.    }  
    67.    uriStr.append("content://");  
    68.    if (TextUtils.isEmpty(authority)) {  
    69.     int sdkInt = android.os.Build.VERSION.SDK_INT;  
    70.     if (sdkInt < 8) { // Android 2.1.x(API 7)以及以下的  
    71.      uriStr.append("com.android.launcher.settings");  
    72.     } else if (sdkInt < 19) {// Android 4.4以下  
    73.      uriStr.append("com.android.launcher2.settings");  
    74.     } else {// 4.4以及以上  
    75.      uriStr.append("com.android.launcher3.settings");  
    76.     }  
    77.    } else {  
    78.     uriStr.append(authority);  
    79.    }  
    80.    uriStr.append("/favorites?notify=true");  
    81.    Uri uri = Uri.parse(uriStr.toString());  
    82.    Cursor c = cr.query(uri, new String[] { "title", "intent" },  
    83.      "title=?  and intent=?",  
    84.      new String[] { title, intent.toUri(0) }, null);  
    85.    if (c != null && c.getCount() > 0) {  
    86.     result = true;  
    87.    }  
    88.    if (c != null && !c.isClosed()) {  
    89.     c.close();  
    90.    }  
    91.   }catch(Exception ex){  
    92.    result=false;  
    93.    ex.printStackTrace();  
    94.   }  
    95.   return result;  
    96.  }  

    兼容与注意事项

    兼容

    所有的快捷方式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进行刷新的。
      以上【所有】【部分】,分别表示必定出现,部分出现。

    参考

    1. http://grepcode.com/search/?query=InstallShortcutReceiver
    2. http://developer.android.com/index.html

    附录

    1. 完整代码可参考https://gist.github.com/waylife/437a3d98a84f245b9582
    2. 通用更新快捷方式权限列表
    [html] view plain copy
     在CODE上查看代码片派生到我的代码片
    1. <uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" />  
    2. <uses-permission android:name="com.android.launcher.permission.WRITE_SETTINGS" />  
    3. <uses-permission android:name="com.android.launcher2.permission.READ_SETTINGS" />  
    4. <uses-permission android:name="com.android.launcher2.permission.WRITE_SETTINGS" />  
    5. <uses-permission android:name="com.android.launcher3.permission.READ_SETTINGS" />  
    6. <uses-permission android:name="com.android.launcher3.permission.WRITE_SETTINGS" />  
    7. <uses-permission android:name="org.adw.launcher.permission.READ_SETTINGS" />  
    8. <uses-permission android:name="org.adw.launcher.permission.WRITE_SETTINGS" />  
    9. <uses-permission android:name="com.htc.launcher.permission.READ_SETTINGS" />  
    10. <uses-permission android:name="com.htc.launcher.permission.WRITE_SETTINGS" />  
    11. <uses-permission android:name="com.qihoo360.launcher.permission.READ_SETTINGS" />  
    12. <uses-permission android:name="com.qihoo360.launcher.permission.WRITE_SETTINGS" />  
    13. <uses-permission android:name="com.lge.launcher.permission.READ_SETTINGS" />  
    14. <uses-permission android:name="com.lge.launcher.permission.WRITE_SETTINGS" />  
    15. <uses-permission android:name="net.qihoo.launcher.permission.READ_SETTINGS" />  
    16. <uses-permission android:name="net.qihoo.launcher.permission.WRITE_SETTINGS" />  
    17. <uses-permission android:name="org.adwfreak.launcher.permission.READ_SETTINGS" />  
    18. <uses-permission android:name="org.adwfreak.launcher.permission.WRITE_SETTINGS" />  
    19. <uses-permission android:name="org.adw.launcher_donut.permission.READ_SETTINGS" />  
    20. <uses-permission android:name="org.adw.launcher_donut.permission.WRITE_SETTINGS" />  
    21. <uses-permission android:name="com.huawei.launcher3.permission.READ_SETTINGS" />  
    22. <uses-permission android:name="com.huawei.launcher3.permission.WRITE_SETTINGS" />  
    23. <uses-permission android:name="com.fede.launcher.permission.READ_SETTINGS" />  
    24. <uses-permission android:name="com.fede.launcher.permission.WRITE_SETTINGS" />  
    25. <uses-permission android:name="com.sec.android.app.twlauncher.settings.READ_SETTINGS" />  
    26. <uses-permission android:name="com.sec.android.app.twlauncher.settings.WRITE_SETTINGS" />  
    27. <uses-permission android:name="com.anddoes.launcher.permission.READ_SETTINGS" />  
    28. <uses-permission android:name="com.anddoes.launcher.permission.WRITE_SETTINGS" />  
    29. <uses-permission android:name="com.tencent.qqlauncher.permission.READ_SETTINGS" />  
    30. <uses-permission android:name="com.tencent.qqlauncher.permission.WRITE_SETTINGS" />  
    31. <uses-permission android:name="com.huawei.launcher2.permission.READ_SETTINGS" />  
    32. <uses-permission android:name="com.huawei.launcher2.permission.WRITE_SETTINGS" />  
    33. <uses-permission android:name="com.android.mylauncher.permission.READ_SETTINGS" />  
    34. <uses-permission android:name="com.android.mylauncher.permission.WRITE_SETTINGS" />  
    35. <uses-permission android:name="com.ebproductions.android.launcher.permission.READ_SETTINGS" />  
    36. <uses-permission android:name="com.ebproductions.android.launcher.permission.WRITE_SETTINGS" />  
    37. <uses-permission android:name="com.oppo.launcher.permission.READ_SETTINGS" />  
    38. <uses-permission android:name="com.oppo.launcher.permission.WRITE_SETTINGS" />  
    39. <uses-permission android:name="com.huawei.android.launcher.permission.READ_SETTINGS" />  
    40. <uses-permission android:name="com.huawei.android.launcher.permission.WRITE_SETTINGS" />  
    41. <uses-permission android:name="telecom.mdesk.permission.READ_SETTINGS" />  
    42. <uses-permission android:name="telecom.mdesk.permission.WRITE_SETTINGS" />  
    43. <uses-permission android:name="dianxin.permission.ACCESS_LAUNCHER_DATA" />  
    import java.util.List;
     
    import android.content.ContentResolver;
    import android.content.Context;
    import android.content.Intent;
    import android.content.pm.PackageInfo;
    import android.content.pm.PackageManager;
    import android.content.pm.ProviderInfo;
    import android.content.pm.ResolveInfo;
    import android.database.Cursor;
    import android.net.Uri;
    import android.text.TextUtils;
    import android.util.Log;
     
    public class launcherUtil {
     
        private static String AUTHORITY = null;
     
        public static boolean isShortCutExist(Context context, String title) {
     
            boolean isInstallShortcut = false;
     
            if (null == context || TextUtils.isEmpty(title))
                return isInstallShortcut;
     
            if (TextUtils.isEmpty(AUTHORITY))
                AUTHORITY = getAuthorityFromPermission(context);
     
            final ContentResolver cr = context.getContentResolver();
     
            if (!TextUtils.isEmpty(AUTHORITY)) {
                try {
                    final Uri CONTENT_URI = Uri.parse(AUTHORITY);
     
                    Cursor c = cr.query(CONTENT_URI, new String[] { "title",
                            "iconResource" }, "title=?", new String[] { title },
                            null);
     
                    // XXX表示应用名称。
                    if (c != null && c.getCount() > 0) {
                        isInstallShortcut = true;
                    }
                    if (null != c && !c.isClosed())
                        c.close();
                } catch (Exception e) {
                    // TODO: handle exception
                    Log.e("isShortCutExist", e.getMessage());
                }
     
            }
            return isInstallShortcut;
     
        }
     
        public static String getCurrentLauncherPackageName(Context context) {
     
            Intent intent = new Intent(Intent.ACTION_MAIN);
            intent.addCategory(Intent.CATEGORY_HOME);
            ResolveInfo res = context.getPackageManager()
                    .resolveActivity(intent, 0);
            if (res == null || res.activityInfo == null) {
                // should not happen. A home is always installed, isn't it?
                return "";
            }
            if (res.activityInfo.packageName.equals("android")) {
                return "";
            } else {
                return res.activityInfo.packageName;
            }
        }
     
        public static String getAuthorityFromPermissionDefault(Context context) {
     
            return getThirdAuthorityFromPermission(context,
                    "com.android.launcher.permission.READ_SETTINGS");
        }
     
        public static String getThirdAuthorityFromPermission(Context context,
                String permission) {
            if (TextUtils.isEmpty(permission)) {
                return "";
            }
     
            try {
                List packs = context.getPackageManager()
                        .getInstalledPackages(PackageManager.GET_PROVIDERS);
                if (packs == null) {
                    return "";
                }
                for (PackageInfo pack : packs) {
                    ProviderInfo[] providers = pack.providers;
                    if (providers != null) {
                        for (ProviderInfo provider : providers) {
                            if (permission.equals(provider.readPermission)
                                    || permission.equals(provider.writePermission)) {
                                if (!TextUtils.isEmpty(provider.authority)// 精准匹配launcher.settings,再一次验证
                                        && (provider.authority)
                                                .contains(".launcher.settings"))
                                    return provider.authority;
                            }
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            return "";
        }
     
        public static String getAuthorityFromPermission(Context context) {
            // 获取默认
            String authority = getAuthorityFromPermissionDefault(context);
            // 获取特殊第三方
            if (authority == null || authority.trim().equals("")) {
                String packageName = getCurrentLauncherPackageName(context);
                packageName += ".permission.READ_SETTINGS";
                authority = getThirdAuthorityFromPermission(context, packageName);
            }
            // 还是获取不到,直接写死
            if (TextUtils.isEmpty(authority)) {
                int sdkInt = android.os.Build.VERSION.SDK_INT;
                if (sdkInt < 8) { // Android 2.1.x(API 7)以及以下的
                    authority = "com.android.launcher.settings";
                } else if (sdkInt < 19) {// Android 4.4以下
                    authority = "com.android.launcher2.settings";
                } else {// 4.4以及以上
                    authority = "com.android.launcher3.settings";
                }
            }
            authority = "content://" + authority + "/favorites?notify=true";
            return authority;
     
        }
    }
  • 相关阅读:
    关于连接connection reset的问题
    Redis应用场景及缓存问题
    zookeeper集群及kafka集群搭建
    使用自定义注解和切面AOP实现Java程序增强
    Shell脚本控制docker容器启动顺序
    正则表达式匹配${key}并在Java中使用
    Redis基本数据结构之ZSet
    Redis基本数据结构之Set
    Redis基本数据结构之Hash
    Redis基本数据结构之List
  • 原文地址:https://www.cnblogs.com/qianyukun/p/5509835.html
Copyright © 2011-2022 走看看