桌面添加图标心碎二三事[摔]

虽然开发过程中经常会中奖踩坑,但是这次真的踩了很大的坑啊,感觉填了很久踩勉强填好。当然也只是对我来说,在大神眼里根本不算什么。事情是这样的,两天前我需要在 App 被安装的时候,向桌面添加一个快捷图标,来增强在用户眼里的存在感。。。由于在此之前我正好给 App 换了个图标,虽然是 png 格式,但是宽高病不同,分辨率大概是 620×610 的样子,基本是个正方形,而且用于 App 的桌面图标也没有什么问题,可以正常显示。但是在用于给 App 添加桌面快捷方式的时候,却死活也不显示,明明 log 显示确实已经添加了,代码执行良好,也没有报错和警告,但就是死活看不到。这是第一个坑。

于是重新创建了一个小工程,用了完全相同的代码,只不过 App 图标是自带的,我没有更换。一执行,居然成功了!我简直惊呆了。仔细对比觉得唯一的可能就是出在照片上了。于是把原来的图标裁剪了一下,改成完全的正方型 610×610 ,还是不显示。没办法,直接用了另外一张照片,标准的 MD 风格的 png,再一执行,果然创建成功了。看来 AS 对照片的检查还真不是一般的严格,之前在 Eclipse 里还能用的 .9 图片,在 AS 里会报错,这次看来也一样,只是 AS 却没有任何表示,以后在照片的使用上必须多加注意。第一个坑填好。

然后就要给快捷图标添加点击 Intent ,在这里尝试了很多次,才找到正确的方法。具体的常识过程太啰嗦,就不详细讲了,主要讲一下在这个过程得到的经验吧。

  1. 首先需要在点击 Intent 要开启的 Activity 的 manifest 里添加 IntentFilter 标签;
  2. manifest 的 IntentFilter 标签里必须包含一个 Action ,这个 Action 的名字可以随便命名;
  3. manifest 的 IntentFilter 添加 category 不是必须的,可以不加;
  4. 在代码里创建的 点击 Intent 可以通过构造方法添加隐式 Intent 来开启目标 Activity ,隐式 Intent 里就是在 manifest 的 IntentFilter 设置的 Action ;但是这种开启方法不能与自己的 App 绑定,也就是 App 卸载后图标不会自动被移除;
  5. 点击 Intent 通过 setClass() 设置源和目标 Activity 来开启,并且可以与自己的 App 绑定,App 卸载后图标会被移除;
  6. 点击 Intent 可以不用再设置 action 和 category;
  7. 使用快捷方式打开的 Activity 最好设为 Single Instance 启动模式,或者为点击 Intent 设置必要的 flag ,以免重复打开相同的 Activity ;
  8. 在sendBoradcast()中的 intent 设置 intent.putExtra(“duplicate”, false) 不能保证可以避免重复创建快捷方式,这是最后一个坑,大坑啊,见后文。

所以我的添加快捷方式的代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Intent intent = new Intent();
intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
//快捷方式名字
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, getString(R.string.app_name));
//快捷方式图标
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_shortcut);
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bitmap);
//不能保证可以避免重复创建
intent.putExtra("duplicate", false);

//快捷方式点击执行的意图
Intent actionIntent = new Intent();
actionIntent.setClass(SplashActivity.this, HomeActivity.class);
//actionIntent.setAction(Intent.ACTION_MAIN);
//actionIntent.addCategory(Intent.CATEGORY_LAUNCHER);

intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, actionIntent);
//以发送广播的形式创建快捷方式
sendBroadcast(intent);

因为 intent.putExtra("duplicate", false); 这一句代码不能保证可以避免重复创建快捷方式(在虚拟机测试有效,但在 htc 真机上无效),所以还需要自己来判断是否已经存在相同的快捷方式。这这里耗费了将近一天的时间来测试,真的无所不用其极,感觉已经翻遍 google 了。这里不得不吐槽了 Android 各种机型想要完全适配工作量真的是太大了,现在屏幕上的适配反而比较容易,但是对各大厂商自己定制的 ROM ,那才是最大的沟渠啊,就桌面 Launcher 来说,每种手机都有自己的定制版,那么对于相应的权限,提供认证的 provider 都可能不同,适配起来也就无比麻烦。同时 Android 不同的版本也可能存在不同的 Launcher ,就现在的 Android 原生来说,已经有 3 个版本的 Launcher 了:launcher、launcher2 和 launcher3 。

下面是碰到的几种错误 log 和解释:

Failed to find provider info for com.android.launcher.settings

这个是因为 Android SDK 版本的不同,系统 launcher 版本也有变化,可能回事 launcher2,或者 launcher3,或者其他 ROM 的自家 launcher;

getAuthorityFromPermission 返回 null

这个方法是现在网络上普遍使用的可以根据权限来识别认证的 provider,但是网络版本还不够完善,因为经过测试发现,传入的 permission 不一定能够匹配所有的机型的 permission ,也就不能保证能够传回想要的 provider ,在这里我做了一些改进,最终能够在 htc 真机和虚拟机测试通过;

Permission Denial: opening provider com.android.launcher3.LauncherProvider from ProcessRecord{f264d45 13880:com.example.alpha.mobilesafe/u0a68} (pid=13880, uid=10068) requires com.google.android.launcher.permission.READ_SETTINGS or com.google.android.launcher.permission.WRITE_SETTINGS

很明显,因为虚拟机是 Nexus4,还需要添加 google 的权限:
com.google.android.launcher.permission.READ_SETTINGS

接下来主要说说适配的一点小技巧,也是最后一个坑:

网络版本的 getAuthorityFromPermission 是直接传入一个完整的 permission ,而我只是传入了半个,比如:.launcher.permission.READ_SETTINGS ,因为我发现不管是虚拟机,还是真机,在权限上都包含这一部分,既然如此,就通过是否包含来判断对应烦人 authority ,但是在 htc 真机上又有个问题, authority 总是传回 com.htc.launcher.ExternalApp而不是我想要的com.htc.launcher.settings,所以在回传结果的再加一次判断:

1
2
3
4
if (provider.authority.contains("launcher.settings")
|| provider.authority.contains("launcher2.settings")
|| provider.authority.contains("launcher3.settings")) {
return provider.authority;

不过这样虽然可以达到我们想要的结果,但是这个方法的适用范围就仅限于此了。这个就需要大家自己斟酌了。

最后贴上完整的代码:

manifest:

1
2
3
4
5
6
7
8
9
10
11
<uses-permission android:name="com.android.launcher.permission.READ_SETTINGS" />
<uses-permission android:name="com.htc.launcher.permission.READ_SETTINGS" />
<uses-permission android:name="com.android.launcher2.permission.READ_SETTINGS" />
<uses-permission android:name="com.android.launcher3.permission.READ_SETTINGS" />
<uses-permission android:name="com.google.android.launcher.permission.READ_SETTINGS"/>

<activity android:name=".Activity.HomeActivity" android:launchMode="singleInstance">
<intent-filter>
<action android:name="com.example.alpha.mobilesafe.home"/>
</intent-filter>
</activity>

添加 shortcut

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 创建桌面快捷方式
*/
private void addShortCut() {
if (AppInfoUtils.hasShortCut(this)) {
return;
}

Intent intent = new Intent();
intent.setAction("com.android.launcher.action.INSTALL_SHORTCUT");
//快捷方式名字
intent.putExtra(Intent.EXTRA_SHORTCUT_NAME, getString(R.string.app_name));
//快捷方式图标
Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_shortcut);
intent.putExtra(Intent.EXTRA_SHORTCUT_ICON, bitmap);
//不要重复创建
intent.putExtra("duplicate", false);

//快捷方式点击执行的意图
Intent actionIntent = new Intent();
actionIntent.setClass(SplashActivity.this, HomeActivity.class);
//不是必须的
//actionIntent.setAction(Intent.ACTION_MAIN);
//actionIntent.addCategory(Intent.CATEGORY_LAUNCHER);

intent.putExtra(Intent.EXTRA_SHORTCUT_INTENT, actionIntent);
//以发送广播的形式创建快捷方式
sendBroadcast(intent);
Log.d(TAG, "addShortCut: 桌面图标添加完成");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/**
* 判断当前应用是否已经创建快捷图标
*
* @param context 上下文
* @return 是否创建
*/
public static boolean hasShortCut(Context context) {
boolean result = false;
String authority = getAuthorityFromPermission(context,
".launcher.permission.READ_SETTINGS");
Log.d(TAG, "hasShortCut: authority是什么呢?"+authority);

String uriStr = "content://" + authority + "/favorites?notify=true";

try {
Cursor cursor = context.getContentResolver().query(
Uri.parse(uriStr),
null,
"title=?",
new String[]{context.getString(R.string.app_name)},
null);
result = cursor != null && cursor.getCount() > 0;
Log.d(TAG, String.valueOf("hasShortCut: 有没有图标啊?" + result));
if (cursor != null) {
cursor.close();
}
} catch (Exception e) {
e.printStackTrace();
}
return result;
}

/**
* 根据传入的权限名查找目标 authority
* @param context 上下文
* @param permission 部分权限名
* @return 目标 authority
*/
private static String getAuthorityFromPermission(Context context, String permission) {
if (permission == null) return null;
List<PackageInfo> installedPackages = context.getPackageManager()
.getInstalledPackages(PackageManager.GET_PROVIDERS);
if (installedPackages != null) {
for (PackageInfo pack : installedPackages) {
ProviderInfo[] providers = pack.providers;
if (providers != null) {
for (ProviderInfo provider : providers) {
Log.d(TAG, "getAuthorityFromPermission: " + provider.readPermission);
//Log.d(TAG, "getAuthorityFromPermission: " + provider.writePermission);
if (provider.readPermission != null && provider.readPermission.contains(permission)) {
if (provider.authority.contains("launcher.settings")
|| provider.authority.contains("launcher2.settings")
|| provider.authority.contains("launcher3.settings")) {
return provider.authority;
}
}
}
}
}
}
return null;
}

这样无论是真机还是虚拟机,都可以判断是否已经存在快捷方式,而且根据我的方法,我想还是存在一定的广泛型,可以匹配大部分机型,当然仅限这一功能了。