Android scheme跳转和DeepLinkDispatch的使用

DeepLink 是什么?

Deep Link,又叫deep linking,中文译为深层链接。简单地从用户体验来讲,Deep Link,就是可以让你在手机的浏览器/Google Search上点击搜索的结果,便能直接跳转到已安装的应用中的某一个页面的技术。

例如我们收到淘宝店家发送的短信,我们点击短信中的短链接,然后在浏览器里面打开短链接,然后浏览器里面会弹出“网页请求打开淘宝”的对话框,然后我们点击打开,就可以打开淘宝并跳转到响应的界面。

我们先用Android原生的 scheme来实现一个类似的功能。关于scheme相关的知识可以参考 Scheme详解

首先新建一个Activity,然后在AndroidManifest.xml文件中,为Activity添加响应的scheme信息。

         <activity
                android:name=".SecondActivity"
                android:exported="true"
                android:label="SecondActivity">
            <intent-filter>
                <data
                        android:host="www.dmw.com"
                        android:scheme="com.dmw.android.wakeupappdemo"/>

                <!-- 必须加上该项,对一段数据执行的“标准”操作-->
                <action android:name="android.intent.action.VIEW"/>
                <!-- 必须加上该项 -->
                <category android:name="android.intent.category.DEFAULT"/>
                <!-- 如果希望该应用可以通过浏览器的连接启动,则添加该项 -->
                <category android:name="android.intent.category.BROWSABLE"/>
            </intent-filter>

            <intent-filter>
                <data
                        android:host="www.dmwdmw.com"
                        android:scheme="com.dmwdmw.android.wakeupappdemo"/>

                <!-- 必须加上该项,对一段数据执行的“标准”操作-->
                <action android:name="android.intent.action.VIEW"/>
                <!-- 必须加上该项 -->
                <category android:name="android.intent.category.DEFAULT"/>
                <!-- 如果希望该应用可以通过浏览器的连接启动,则添加该项 -->
                <category android:name="android.intent.category.BROWSABLE"/>
            </intent-filter>
        </activity>

注意:如果你有多个data标签的话,你需要使用多个 <intent-filter>标签,并确保每个 <intent-filter>标签里面只有一个data标签。不然会出现Error: Activity not started, unable to resolve Intent这样的问题。
参考 deep-linking-intent-does-not-work

然后在Activity中接收启动Activity的时候传递的参数。

class SecondActivity : AppCompatActivity() {

    companion object {

        val TYPE_INTENT = "type"
        val URL_INTENT = "url"
        val NAME_INTENT = "name"

        fun launch(context: Context) {
            val intent = Intent(context, SecondActivity::class.java)
            context.startActivity(intent)
        }
    }


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_second)

        if (intent.data != null) {
            val uri = intent.data
            tvScheme.text = uri?.scheme ?: "no scheme"
            tvHost.text = uri?.host ?: "no host"
            tvAuthority.text = uri?.authority ?: "no authority"

            val type = uri.getQueryParameter(TYPE_INTENT)
            val url = uri.getQueryParameter(URL_INTENT)
            val name = uri.getQueryParameter(NAME_INTENT)

            tvParams.text = "type=$type,url=$url,name=$name"

        }
    }
}

然后在Android studio自带的终端里面通过 am 命令启动 Activity。

adb shell
am start -d "com.dmw.android.wakeupappdemo://www.dmw.com:8080?type=green&url=123"
  • 匹配的intent-filter
<intent-filter>
                <data
                        android:host="www.dmw.com"
                        android:scheme="com.dmw.android.wakeupappdemo"/>
                <!-- 必须加上该项,对一段数据执行的“标准”操作-->
                <action android:name="android.intent.action.VIEW"/>
                <!-- 必须加上该项 -->
                <category android:name="android.intent.category.DEFAULT"/>
                <!-- 如果希望该应用可以通过浏览器的连接启动,则添加该项 -->
                <category android:name="android.intent.category.BROWSABLE"/>
            </intent-filter>

如下图所示。

am-start.png
adb shell
 am start -d "com.dmwdmw.android.wakeupappdemo://www.dmwdmw.com:8080?type=green&url=123&name=hello"

匹配的intent-filter

<intent-filter>
                <data
                        android:host="www.dmwdmw.com"
                        android:scheme="com.dmwdmw.android.wakeupappdemo"/>
                <!-- 必须加上该项,对一段数据执行的“标准”操作-->
                <action android:name="android.intent.action.VIEW"/>
                <!-- 必须加上该项 -->
                <category android:name="android.intent.category.DEFAULT"/>
                <!-- 如果希望该应用可以通过浏览器的连接启动,则添加该项 -->
                <category android:name="android.intent.category.BROWSABLE"/>
            </intent-filter>

如下图所示

intent-start-another.png

如果你能用浏览器加载如下所示的HTML,或者使用HTML查看器类似的应用打开这段HTML。然后点击页面上的超链接,也是可以打开我们的App的。

<!DOCTYPE html>
<html>
<head>
    <title>Android短信测试</title>
</head>
<body>
    <a href="com.dmw.android.wakeupappdemo:">启动主界面</a>
    <a href="com.dmw.android.wakeupappdemo://www.dmw.com:8080?type=red&url=111&name=红色">启动红色程序/a>
    <a href="com.dmw.android.wakeupappdemo://www.dmw.com:8080?type=yellow&name=黄色">启动黄色程序,url为空</a>
    <a href="com.dmw.android.wakeupappdemo://www.dmw.com:8080?type=green&url=111">启动绿色程序,name为空</a>
    
    <a href="com.dmwdmw.android.wakeupappdemo://www.dmwdmw.com:8080?type=green&url=111">启动绿色程序,name为空</a>
    
</body>
</html>

上面例子中的完整代码请移步 github WakeUpAppDemo

DeepLinkDispatch的使用

DeepLinkDispatch是一个简单的基于注解的库,可以更好的处理让Android平台上的deep link 跳转问题。

使用 添加依赖

implementation 'com.airbnb:deeplinkdispatch:3.1.1'
annotationProcessor 'com.airbnb:deeplinkdispatch-processor:3.1.1'

创建你自己的link module(s)。每一个用DeepLinkModule注解的类,DeepLinkDispatch都会生成一个对应的Loader类,Loader类里面包含所有你使用@DeepLink注解注册的信息。

这个是在项目中其他library中创建的一个LibraryDeepLinkModule类,会生成一个LibraryDeepLinkModuleLoader 类

package com.airbnb.deeplinkdispatch.sample.library;

import com.airbnb.deeplinkdispatch.DeepLinkModule;

@DeepLinkModule
public class LibraryDeepLinkModule {
}

这是在主项目中创建的SampleModule类,会生成一个SampleModuleLoader类

package com.airbnb.deeplinkdispatch.sample;

import com.airbnb.deeplinkdispatch.DeepLinkModule;

@DeepLinkModule
public class SampleModule {
}

然后创建一个DeepLinkActivity 用来转发所有的 deep link 跳转信息。并在AndroidManifest.xml文件中声明你想要处理的scheme

//使用@DeepLinkHandler注解DeepLinkActivity ,并提供一个用@DeepLinkModule注解的类的列表。
@DeepLinkHandler({ SampleModule.class, LibraryDeepLinkModule.class })
public class DeepLinkActivity extends Activity {
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    //DeepLinkDelegate,LibraryDeepLinkModuleLoader 和SampleModuleLoader都是编译期间生成的。
    DeepLinkDelegate deepLinkDelegate = new DeepLinkDelegate(
        new SampleModuleLoader(), new LibraryDeepLinkModuleLoader());
   //把 deep link 处理转发个DeepLinkDispatch。DeepLinkDispatch会根据传入的Intent URI 启动正确的 Activity
    deepLinkDelegate.dispatchFrom(this);
    //结束当前Activity 因为已经启动了正确的Activity了。
    finish();
  }
}

AndroidManifest.xml文件中声明你想要处理的scheme信息。

<activity
    android:name="com.example.DeepLinkActivity"
    android:theme="@android:style/Theme.NoDisplay">
    <intent-filter>
        <action android:name="android.intent.action.VIEW" />
        <category android:name="android.intent.category.DEFAULT" />
        <category android:name="android.intent.category.BROWSABLE" />
        <data android:scheme="foo" />
    </intent-filter>
</activity>

注意:

  1. 从v3版本开始,你需要自己声明一个用@DeepLinkHandler注解的类。(就是上面我们声明的DeepLinkActivity )
  2. AndroidManifest.xml文件中每个<intent-filter>下面只能包含一个data标签,如果是多个data标签的话,你需要声明多个<intent-filter>标签。如下所示。
<intent-filter>
    ...
    <data android:scheme="http"
          android:host="example.com"/>
</intent-filter>

<intent-filter>
    ...
    <data android:scheme="http"
          android:host="example.com"
          android:pathPrefix="/gizmos"/>
</intent-filter>


@DeepLink({ "dld://classDeepLink", "http://example.com/foo{arg}", "dld://example.com/deepLink" })
public class MainActivity extends AppCompatActivity {

  ...
 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        tvParams = findViewById(R.id.tvParams);
        Intent intent = getIntent();
        if (intent.getBooleanExtra(DeepLink.IS_DEEP_LINK, false)) {
            String message;
            Bundle parameters = intent.getExtras();
            Log.d(TAG, "Deeplink params: " + parameters);

            if (ACTION_DEEP_LINK_METHOD.equals(intent.getAction())) {
                message = "method with param1:" + parameters.getString("param1");
            } else if (ACTION_DEEP_LINK_COMPLEX.equals(intent.getAction())) {
                message = parameters.getString("arbitraryNumber");
            } else if (parameters.containsKey("arg")) {
                message = "class and found arg:" + parameters.getString("arg");
            } else {
                message = "class";
            }

            // You can pass a query parameter with the URI, and it's also in parameters, like
            // dld://classDeepLink?qp=123
            if (parameters.containsKey("qp")) {
                message += " with query parameter " + parameters.getString("qp");
            }
            Uri referrer = ActivityCompat.getReferrer(this);
            if (referrer != null) {
                message += " and referrer: " + referrer.toString();
            }
            tvParams.setText(message);
        }
    }
}

MainActivity 使用@DeepLink注解,表明只要scheme是注解中的任意一个,都可以打开MainActivity 。

然后我们使用下面的命令在Android studio 的终端启动App,能正确打开MainActivity ,并且我们可以在onCreate()方法中,获取Uri中的信息。

adb shell
//对应注解的第一个值
am start -W -a android.intent.action.VIEW -d "dld:classDeepLink"

//对应注解第二个值
am start -W -a android.intent.action.VIEW -d "http://example.com/foodumingwei"

//对应注解第三个值,我们启动的时候也可以在后面加上要启动的app的包名。
am start -W -a android.intent.action.VIEW -d "dld://example.com/deepLink" com.airbnb.deeplinkdispatch.sample


注意上面第二种注解

http://example.com/foo{arg}

用花括号括起来的是参数名,如果我们传递了相应的参数,就会添加到intent的extra中,我们就可以在Activity中获取我们传递的参数。

am start -W -a android.intent.action.VIEW -d "http://example.com/foodumingwei"

用上面的启动方式启动Activity以后,我们打印intent的 extras。

 if (intent.getBooleanExtra(DeepLink.IS_DEEP_LINK, false)) {
            String message;
            Bundle parameters = intent.getExtras();
            Log.d(TAG, "Deeplink params: " + parameters);
}

Deeplink params: Bundle[{is_deep_link_flag=true, android.intent.extra.REFERRER=http://example.com/foodumingwei, arg=dumingwei, deep_link_uri=http://example.com/foodumingwei}]

可以看到传递的 arg的实参是dumingwei

方法注解

你可以使用@DeepLink注解任何public static的方法。DeepLinkDispatch 会调用这些方法来创建intent,使用它来启动你的Activity

    @DeepLink("dld://methodDeepLink/{param1}")
    public static Intent intentForDeepLinkMethod(Context context) {
        return new Intent(context, MainActivity.class).setAction(ACTION_DEEP_LINK_METHOD);
    }

am start -W -a android.intent.action.VIEW -d "dld://methodDeepLink/hello world"

当我们在终端输入上面的命令后,可以成功启动MainActivity

如果你需要获取Intent中的extras,只需要为你的方法添加一个Bundle参数。例如:

@DeepLink("dld://host/somePathOne/{arbitraryNumber}/otherPath")
    public static Intent intentForComplexMethod(Context context, Bundle bundle) {
        if (bundle != null && bundle.containsKey("qp")) {
            Log.d(TAG, "found new parameter :with query parameter :" + bundle.getString("qp"));
        }
        return new Intent(context, MainActivity.class).setAction(ACTION_DEEP_LINK_COMPLEX);
    }


 am start -W -a android.intent.action.VIEW -d dld://host/somePathOne/123/otherPath?qp=dmw

当我们在终端输入上面的命令后,可以成功启动MainActivity。并且打印出了qp的实参dmw

MainActivity: found new parameter :with query parameter :dmw

如果你需要自定义Activity返回栈,那么方法可以返回一个TaskStackBuilder 。

@DeepLink("http://example.com/deepLink/{id}/{name}/{place}")
    public static TaskStackBuilder intentForTaskStackBuilderMethods(Context context, Bundle bundle) {
        Log.d(TAG, "without query parameter :");
        if (bundle != null && bundle.containsKey("qp")) {
            Log.d(TAG, "found new parameter :with query parameter :" + bundle.getString("qp"));
        }
        Intent detailsIntent =
                new Intent(context, SecondActivity.class).setAction(ACTION_DEEP_LINK_COMPLEX);
        Intent parentIntent =
                new Intent(context, MainActivity.class).setAction(ACTION_DEEP_LINK_COMPLEX);
        TaskStackBuilder taskStackBuilder = TaskStackBuilder.create(context);
        taskStackBuilder.addNextIntent(parentIntent);
        taskStackBuilder.addNextIntent(detailsIntent);
        return taskStackBuilder;
    }

am start -W -a android.intent.action.VIEW -d http://example.com/deepLink/2018/dumingwei/Shanghai?qp=dmw

当我们在终端输入上面的命令后,会先启动SecondActivity。点击返回键,会返回MainActivity

查询参数

查询参数会被自动解析和传递并且像其他参数一样可以被获取。例如我们可以获取在URIfoo://example.com/deepLink?qp=123中传递的查询参数。

@DeepLink("foo://example.com/deepLink")
public class MainActivity extends Activity {
  @Override protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Intent intent = getIntent();
    if (intent.getBooleanExtra(DeepLink.IS_DEEP_LINK, false)) {
      Bundle parameters = intent.getExtras();
      if (parameters != null && parameters.getString("qp") != null) {
        String queryParameter = parameters.getString("qp");
        // Do something with the query parameter...
      }
    }
  }
}

传递实参123

am start -W -a android.intent.action.VIEW -d foo://example.com/deepLink?qp=123

回调

你可以选择性的注册一个广播接收器,每当有deep link 请求打开你的App的时候,广播接收器都会被调用。无论deep link 请求成功还是失败,DeepLinkDispatch 会使用LocalBroadcastManager来广播一个IntentIntent会携带一些信息:

  • DeepLinkHandler.EXTRA_URI:deep link的URI
  • DeepLinkHandler.EXTRA_SUCCESSFUL:deep link 请求是否被成功处理。
  • DeepLinkHandler.EXTRA_ERROR_MESSAGE:如果 deep link 处理失败时候的错误信息。
public class DeepLinkReceiver extends BroadcastReceiver {
    private static final String TAG = DeepLinkReceiver.class.getSimpleName();

    @Override
    public void onReceive(Context context, Intent intent) {
        String deepLinkUri = intent.getStringExtra(DeepLinkHandler.EXTRA_URI);

        if (intent.getBooleanExtra(DeepLinkHandler.EXTRA_SUCCESSFUL, false)) {
            Log.i(TAG, "Success deep linking: " + deepLinkUri);
        } else {
            String errorMessage = intent.getStringExtra(DeepLinkHandler.EXTRA_ERROR_MESSAGE);
            Log.e(TAG, "Error deep linking: " + deepLinkUri + " with error message +" + errorMessage);
        }
    }
}

然后在Application中注册广播接收器。

public class SampleApplication extends Application {
  @Override public void onCreate() {
    super.onCreate();
    IntentFilter intentFilter = new IntentFilter(DeepLinkHandler.ACTION);
    LocalBroadcastManager.getInstance(this).registerReceiver(new DeepLinkReceiver(), intentFilter);
  }
}

自定义注解

你可以创建自定义注解提供通用的前缀,可以减少 deep links的重复。这些前缀会自动应用到每个使用自定义注解注解的类和方法。一个流行的使用场景是web和app的之间的deep link 请求。

// 为每一个deep link URI 添加一个 "app://airbnb"前缀。
@DeepLinkSpec(prefix = { "app://airbnb" })
public @interface AppDeepLink {
  String[] value();
}

//  为所有的web deep links 添加 "http://airbnb.com" and "https://airbnb.com"前缀
@DeepLinkSpec(prefix = { "http://airbnb.com", "https://airbnb.com" })
@Retention(RetentionPolicy.CLASS)
public @interface WebDeepLink {
  String[] value();
}

@AppDeepLink({"/view_users"})
@WebDeepLink({"/users", "/user/{id}"})
public class CustomPrefixesActivity extends AppCompatActivity {

    private static final String TAG = CustomPrefixesActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        if (getIntent().getBooleanExtra(DeepLink.IS_DEEP_LINK, false)) {
            Bundle parameters = getIntent().getExtras();
            Log.d(TAG, "Deeplink params: " + parameters);
    }
}

如上所示,上面的Activity能够处理的deep link:

// "app://airbnb/view_users"
// "http://airbnb.com/users"
// "http://airbnb.com/user/{id}"
// "https://airbnb.com/users"
// "https://airbnb.com/user/{id}"

我们在控制台试一试。

am start -W -a android.intent.action.VIEW -d app://airbnb/view_users

 am start -W -a android.intent.action.VIEW -d http://airbnb.com/users

am start -W -a android.intent.action.VIEW -d https://airbnb.com/users

 am start -W -a android.intent.action.VIEW -d http://airbnb.com/user/123

 am start -W -a android.intent.action.VIEW -d https://airbnb.com/user/123

都可以成功启动App。

我们在方法上试一试自定义注解

@AppDeepLink("/method/view_users")
    public static Intent intentForCustomDeepLinkMethod(Context context) {
        return new Intent(context, SecondActivity.class).setAction(ACTION_DEEP_LINK_METHOD);
}

在控制台启动,可以正常启动Activity

am start -W -a android.intent.action.VIEW -d app://airbnb/method/view_users

结束。

参考链接:

  1. DeepLinkDispatch

  2. Android 短信链接跳浏览器打开APP

  3. Deep Link是什么

  4. Android 跨应用间调用: URL Scheme

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342