一个APP拉起另一个APP
# 1. 需求场景
- A应用:在A应用中拉起B应用 (🎨A Demo地址 (opens new window))
- B应用:被拉起的应用 (🔗B Demo地址 (opens new window))
A应用拉起B应用的非主页面的某个页面,并且传值(一般是鉴权token值、type值以及其他参数值,本文仅仅以传递type值为例),B应用根据传递过来的不同的值启动不同的页面。前期说明(主要针对B应用):
Android开发一般页面分为启动页(SplashActivity)、引导页(GuideActivity)、活动闪屏页(ScreenActivity)、主页(MainActivity)、登录页(LoginActivity)以及其他页面。 Android开发主页(MainActivity)的启动模式一般设置为:android:launchMode=“singleTask”,只有设置了这种启动模式才能更好的避免重复的启动主页面以及退出页面顺序异常的问题。 我们需要一个Activity的管理工具类,启动时添加Activity,销毁时移除Activity,并且可以提供几个方法方便我们调用:是否包含主页(MainActivity)的方法、获取栈顶Activity的方法等等。
# 2. 实现过程
拉起第三方客户端有三种方式:
①包名 ②ACTION ③URL(推荐)。
如果简简单单拉起一个B应用的某个页面那么可以通过ACTION和URL的方法,但是如果这样做总让人感觉哪里是不合理的。试想一下,你从A应用拉起一个B应用的非主页面的某个页面,点击返回,一下子返回到A应用了,感觉好突然有木有。如果拉起第三方客户端仅仅只是为了一个页面,还不如我们自己写个原生的页面算了,或者搞成SDK,这样岂不是可以节省好多资源撒。
因此,真正的实现过程应该是:A应用拉起B应用的启动页(SplashActivity)并传值(token值和type值以及其他参数值),在启动页获取到值并且存储到SharedPreferences中,最好再存储一个Boolean值,代表这是从第三方应用拉起来的。然后有两种情况,通过Activity的管理工具类判断栈中是否含有B应用的主页面(即B应用是否已经运行在后台)①不含有:代表后台并没有运行B应用,那么我们正常启动主页面,在主页(MainActivity)从SharedPreferences中获取到这个Boolean值与A应用传递过来的值,由主页根据A应用传递过来的值打开相应的页面,这样用户点击返回顺序为:B应用相应页面-B应用主页面-A应用;②含有:代表B应用已经运行在后台,并且现在可能停留在某个页面,此时,我们不应该在启动页继续走启动主页面的逻辑了,如果继续启动主页面,由于我们设置了主页的启动模式为android:launchMode=“singleTask”,那么B应用栈中主页面以上页面都会出栈,用户将看不到刚刚浏览过的页面,这样太不友好了。因此此时的解决方案是我们要在启动页发个静态广播,在广播接收者中获取到SharedPreferences中的Boolean值与A应用传递过来的值,并且通过Activity的管理工具类获取到栈顶的Activity,然后在栈顶Activity的基础上启动相应的页面,(当然,这里也可以不发送广播,直接在启动页通过Activity的管理工具类获取到栈顶的Activity,然后在栈顶Activity的基础上启动相应的页面,这样效果是一样的)这样用户点击返回的顺序为:B应用相应页面-B应用用户拉起客户端之前浏览的页面-B应用主页面-A应用。 这样既能跳转到B应用中我们应该跳转的页面,还可以使用B应用其他的功能,也就是说可以正常使用B应用,并且返回的顺序也是合理的,这样才算真正的拉起第三方应用。
注意:我所描述的栈顶Activity在实际应用中其实并不是真正的栈顶Activity,因为目前栈顶Activity应该是启动页(SplashActivity),并不是用户在拉起客户端之前浏览的页面,我们的目的就是获取到用户在拉起客户端之前浏览的页面,所以Activity的管理工具类应该提供一个获取栈顶下方的第一个Activity方法,因为这个Activity才是用户在拉起客户端之前浏览的页面。切记!!!—其实经过本人亲测,由启动页(SplashActivity)而非用户在拉起客户端之前浏览的页面,打开相应页面时,效果看起来其实是相同的,只是感觉在逻辑上有点别扭,所以本人还是强烈建议最好还是获取到真正的用户在拉起客户端之前浏览的页面,在此页面基础上打开相应页面,这样逻辑上才是最合理的。
先说下基本过程,A应用拉起B应用,从上面的实现方法某一个都可以实现,但是如果是A应用拉起B应用中的某个界面,但不是主页面,这时点击返回,直接跳出B应用来到A应用,很突然,对用户体验会有一定的影响。
正确的实现过程,首先判断被拉起的B应用是否处于后台运行。1、没有处于后台运行,A应用正常拉起B应用的主界面,然后再通过A应用传递type值,再跳转到对应的B界面。这时返回的顺序是B(type对应的界面)—>B应用的主界面—>A应用的界面;2、处于后台运行,说明B应用已经被打开,处于后台运行模式,这时我们不可以走B应用启动主界面的逻辑了,如果执意要这样做,由于我们设置了主页的启动模式为android:launchMode=“singleTask”,那么在B应用栈中位于B应用主界面以上的界面全部都会出栈,我们设置主界面的启动模式为singleTask是为了更好的避免重复的启动主页面以及退出页面顺序异常的问题。所以我们可以直接启动Activity的管理工具类,在启动页获取栈顶的Activity,然后在栈顶的Activity的基础上启动type对应的界面,这时返回的顺序是B(type)对应的界面—>B应用之前打开的界面—>B应用的主界面—>A应用的界面。
# 3. 实现的方法
隐式启动拉起第三方APP有三种方式:
- Package;
- Action;
- Uri(推荐)
# 是否存在且跳转到应用商店
首先判断我们拉起(跳转)的第三方APP是否存在
/**
* 拉起(跳转)的第三方APP是否存在
*
* @param context
* @param packageName
* @return
*/
public boolean isApkInstalled(Context context, String packageName) {
if (TextUtils.isEmpty(packageName)) {
Toast.makeText(getApplicationContext(), "应用未安装", Toast.LENGTH_SHORT).show();
return false;
}
try {
ApplicationInfo info = context.getPackageManager().getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
return true;
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
return false;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
不存在APP则让它跳转到应用商店
/**
* 启动到应用商店app详情界面
*
* @param appPkg 目标App的包名
* @param marketPkg 应用商店包名
*/
public void launchAppDetail(String appPkg, String marketPkg) {
try {
if (TextUtils.isEmpty(appPkg)) return;
Uri uri = Uri.parse("market://details?id=" + appPkg);
Intent intent = new Intent(Intent.ACTION_VIEW, uri);
if (!TextUtils.isEmpty(marketPkg)) {
intent.setPackage(marketPkg);
}
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 3.1 Package
# 第一种:包名
Intent intent = getPackageManager().getLaunchIntentForPackage("top.iqqcode.next");
if (intent != null) {
intent.putExtra("type", "110");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
}
2
3
4
5
6
# 第二种:包名+启动页所在项目位置(清单文件Activity配置中android:name所声明的全路径)
Intent intent = new Intent();
Bundle bundle = new Bundle();
ComponentName componentName = new ComponentName(appPackageName, "top.iqqcode.app2.MainActivity");
bundle.putString("data", "你好,MainActivity2!来自Package");
intent.putExtras(bundle);
// intent.setClassName("B应用包名", "B应用包名.Activity");
intent.setComponent(componentName);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
2
3
4
5
6
7
8
9
# 3.2 Action
在启动页(SplashActivity)清单文件增加如下配置:
注意:不要在原有的intent-filter中增加代码,而是在原有intent-filter下方再增加一个intent-filter。
被拉起APP中Manifest配置:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!--ACTION启动配置-->
<intent-filter>
<action android:name="top.iqqcode.intent.action.IQQCODE" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
2
3
4
5
6
7
8
9
10
11
12
# 第一种:ACTION字符串
Intent intent = new Intent();
intent.setAction("CSD");
intent.putExtra("type", "110");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
2
3
4
5
# 第二种:ACTION字符串+包名+启动页所在项目位置(清单文件Activity配置中android:name所声明的全路径):
ComponentName componentName = new ComponentName("top.iqqcode.next", "top.iqqcode.next.SplashActivity");
Intent intent = new Intent();
intent.setComponent(componentName);
//这个值一定要和B应用的action一致,否则会报错
intent.setAction("top.iqqcode.intent.action.IQQCODE");
intent.putExtra("data", "你好,MainActivity2!来自Action");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
2
3
4
5
6
7
8
注意:A应用代码中的ACTION字符串必须与B应用清单文件配置的ACTION字符串完全匹配才会成功拉起。
# 3.3 Uri
这种方式同样适用于HTML中的a标签链接拉起B应用。
B应用清单文件需要配置:
在启动页(SplashActivity)清单文件增加如下配置:
注意:不要在原有的intent-filter中增加代码,而是在原有intent-filter下方再增加一个intent-filter。
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!--URI启动配置-->
<intent-filter>
<data
android:host="top.iqqcode.app2"
android:path="/iqqcode"
android:scheme="iqqcode" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
</intent-filter>
</activity>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
注意:这里scheme为必填,host、path为选填。选填内容可以不填,但是一旦填上了就必须全部完全匹配才会成功拉起。
A应用编码:
Uri uri = Uri.parse("iqqcode://top.iqqcode.app2/iqqcode");
//这里Intent当然也可传递参数,但是一般情况下都会放到上面的URI中进行传递也就是 "scheme://host/path?xx=xx"
Intent intent = new Intent();
intent.putExtra("data", "你好,MainActivity2!来自URI");
intent.setData(uri);
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(intent);
2
3
4
5
6
7
B应用启动页拉起后可以获取到Uri,你可以选择先存储到SharedPreferences中,在主页或者广播接收者中取出来,然后再对URI进行解析;或者在启动页立刻将Uri解析成bean对象,放到全局的Application中,在主页或者广播接收者中直接使用。
# B应用解析Uri
B应用的启动页(SplashActivity)中简单的解析:
//解析APP1中的Uri
Uri uri = intent.getData();
if (uri != null) {
String scheme = uri.getScheme();
String host = uri.getHost();
String path = uri.getPath();
String type = uri.getQueryParameter("type");
Log.d(TAG, "解析的Uri为: scheme=" + scheme + " host=" + host + " path=" + path + " type=" + type);
}
2
3
4
5
6
7
8
9
解析结果:
如果存储到SharedPreferences中那么一定是把Uri转换成String类型了
Uri uri = Uri.parse(url);
切记:A应用拉起B应用的编码千万不要忘记添加:
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
这种启动模式如果不添加会发现有时候返回顺序是混乱的
【举个例子】
如果你在浏览B应用的M页面(从B应用主页面进入的),点击HOME键退出,你打开了A应用,从A应用拉起了B应用的N页面。
此时我们需要的合理友好的的返回顺序应该是:B应用的N页面-B应用的M页面-B应用主页-A应用-桌面,但是你会发现你返回的顺序是:B应用的N页面-A应用-B应用的M页面-B应用主页-桌面
# 3.4 网页链接拉起APP
https://www.jianshu.com/p/45af72036e58
# 4. 小结
推荐拉起第三方APP的需求尽量采用URL拉起方式,原因有两个:
不必暴露第三方应用的包名与类名;
有一个问题问题,而这个问题只有URL启动方式可以避免:通过包名或者ACTION拉起时假如B应用已经运行在后台,然后你再次在A应用中将其拉起,此时你会发现的确拉起了B应用,但是页面还是刚刚点击HOME键退出前的页面,所有页面的所有生命周期都没有触发,也因此并不会走你准备好的跳转到相应页面的逻辑,而URL却是正常的会走相应页面的生命周期。
问题场景假设:A应用登陆成功后将鉴权token传递给B应用,然后点击Home键退出B应用再打开A应用,在A应用切换用户以后再次拉起B应用,此时你会发现B应用的所有数据信息还是上一个用户的数据信息。综上所述,强烈推荐拉起第三方的APP的需求尽量采用URL拉起方式
【文章参考】