Android 推送消息开放接口 OpenPush
我们知道, 在中国不能使用 google 的服务. 在中国销售的手机甚至没有安装 google 的服务.所以, 原本由 google gcm 提供的推送消息服务, 在中国是不可以使用的.
你的 app 要在中国市场的 android 手机上可用, 需要自带消息推送. 这是可以做到的,因为 android 允许 app 使用后台服务.
重要的问题来了, 一大堆 app 在后台运行, 会减少手机电池的续航时间. 所以, 在中国销售的手机 ROM系统有一个特殊任务, 就是杀死这些后台服务. 这样的话, 你的 app 就收不到推送消息了. 为了解决这个问题, 甚至出现了“全家桶”的概念, 就是一家公司的多个 app 互相唤醒保持消息畅通.
其实, 我们 app 只要一个可靠的,不被杀死的推送消息系统, 有那么难吗?
这件事应该由手机厂商来做, 在他们的 Android ROM 中加入一个类似 gcm 的服务. 的确, 小米, 华为都做了这事. 但是, 问题来了, 软件厂商做 app 时必须对它们分别适配, 分别搞一个小米版, 华为版或者搞一个综合版. 还需要携带它们的库文件. 即便这样, 你仍然不能复盖大部分中国市场, 听说 oppo,vivo 已经占销量前两位了, 这两个厂根本就没有内置的消息推送.(如果有的话,至少他们没有广告开发者.)
我曾经看过小米和华为的推送消息 API 文档, 老实说比较复杂, 不容易上手, 放弃了. 我想就算费了老劲搞好了, 也只能管到华为的部分机型.(竟然不是全部!)
如果由某个手机厂商来号召全部手机厂商做统一的推送消息 API, 难度不是一般的大. 因为中国还没有一个绝对老大, 他们还互相不服气. 我想只能由"局外人"来号召他们坐下来谈这件事. 这个局外人只能是 app 厂商, 并且不能很强势如腾讯,百度, 否则, 他们还是不买帐. 那么我们就来做这件事吧.
我认为这个推送消息 API 这样设计,
- 实现 API 是厂商的事. 使用 API 是我们 app 的事.
- 它应该统一. 一个代码, 到处运行. 写一个 app, 适应所有的手机, 包括以后出现的新的厂商新的手机.只要这个厂商按这个 API 来做它的手机.
- 消息服务器不能由 API 指定, 应该由手机厂商决定. 原则上, 消息服务器应该由手机厂商建立和维护. 如果有第三方做这个服务器,那也是这个第三方与手机厂商的合作, 我们 app 不关心到底是谁提供了这个服务. 概念上, 我们认为是手机厂商提供了这个服务.
- 尽量少的规范和尽量使用成熟标准.
- API 必须简洁, 使用必须简单. 它做它该做的事. 不要做它不该做的事. 现在的消息系统, 甚至包括google gcm 都做了许多与消息推送无关的事.
- API 只是代码规范. 它本身不需要其它库文件.现在小米, 华为, 包括 google gcm 都带库文件, 你使用它们的推送都要带相应的库文件. 我认为这样很麻烦. 如果只是 gcm 一家, 那带一个库没有问题. 可是, 我们现在要面对很多家,如果一家一个库文件, 那结果 apk 包就太大了. 并且这样还不能适应新的厂商的手机.
- app 不需要去每个手机厂商注册. 手机厂商那么多, 一个一个注册, 很麻烦.用户也不需要另外注册. 我买了你的手机, 就应该使用你的服务.对比一下, google gcm 就很麻烦. app 需要在它的平台上注册获取 push key, 手机用户需要 google play 帐号才能使用.
提出了这么多要求, 那么, 这是不是可以做到的呢? 是可以的. 我们已经做了一个 OpenPush 的 API放到 github 上面了. 搜 OpenPush 就可以找到. 那么它的 API 到底是怎么样的呢?
我们来看它的 app, 下载 OpenPush 的 pushapp 包, 这是一个使用这个 API 的 app 例子.
首先要在 AndroidManifest.xml 声明接收广播消息 OPENPUSH.MESSAGE, 它对应到一个 receiver.
<receiver android:name=".openpush_receiver">
<intent-filter>
<action android:name="OPENPUSH.MESSAGE" />
</intent-filter>
</receiver>
这个广播只有两种消息, 一个是手机的 token 消息, 一个是推送消息 message.
我们来看看它的源代码
public class openpush_receiver extends BroadcastReceiver
{
// 接收广播
public void onReceive(Context context, Intent intent)
{
// 获取 token
String token = intent.getStringExtra("token");
if( token != null )
{
String push_server = intent.getStringExtra("server");
....
return ;
}
// 获取消息
String message = intent.getStringExtra("message");
if( message != null )
{
// 获取消息 json 格式
....
return ;
}
}
}
我们的 app 开始时应该注册推送消息服务程序, 告诉它有消息时广播给我. 下面是注册程序.我们把它写成了 receiver 的 static 过程.
public class openpush_receiver extends BroadcastReceiver
{
private static final String TAG = "TEST" ;
// 注册
public static boolean register( Context context )
{
// 查找服务程序 ACTION = "OPENPUSH.SERVICE"
Intent i = new Intent( "OPENPUSH.SERVICE" );
PackageManager pm = context.getPackageManager();
List list = pm.queryIntentServices(i, 0);
if( list.size() > 0 )
{
// 找到了它
// 向服务程序注册本 app 包名
i.putExtra( "packname", context.getPackageName() );
context.startService(i);
return true ;
}
// 不存在这个服务
return false ;
}
....
}
我们再来看这个 app 主程序,
public void onCreate(Bundle savedInstanceState)
{
......
// 如果本手机没有这个服务 ...
if( ! openpush_receiver.register( this ) )
....
}
好了, 涉及到推送消息的 app 代码全部在这里了.
你可能有个疑问, 那个名称叫 "OPENPUSH.SERVICE" 的服务程序在那里 ?
噢, 那是手机厂商内置到 ROM 中的一个服务程序, 它正是推送消息服务程序. 这是手机厂商要做的事,不是我们 app 的事, 我们只要找到这个程序并把自己注册给它, 然后坐等它广播 token 和推送消息.
提示一下, "OPENPUSH.SERVICE" 不是包名, 它只是 ACTION 名称. 厂商实现它的推送消息服务程序可能叫 com.mi.xxx 或者 com.huawei.zzz 或者其它名称. 我们不关心它到底叫什么, 只要它能响应"OPENPUSH.SERVICE" 这个 ACTION 就可以了.
推送消息
通常我们有一个 app 消息服务器,负责推送消息. 这个消息首先被送到手机厂商的推送消息服务器.
那么这个推送消息服务器的地址在那里? 我们看上面的代码, token 中就有它的地址
// 获取 token
String token = intent.getStringExtra("token");
if( token != null )
{
String push_server = intent.getStringExtra("server");
...
}
这是一个什么样的服务器 ? 我们规定这是一个 http 服务器, 所以它可能是这样的
push_server = "http://www.anybody.com/openpush/"
我们只要给这个 http 服务器 POST 消息就可以了. 这个消息格式必须是这样的,
// 消息采用 json 格式, 前 4 个字段一定要有, 其它内容自定义
{
"token" : "<token>",
"packname" : "com.test.xxx", app 包名称
"subject" : "hello",
"timeout" : 120, 秒
......
}
我们编写的 app 测试程序, 演示了如何 POST 消息.( 这本来应该是 app 消息服务器做的事. )
如何编写 推送消息服务程序 ?
如果你是手机厂商, 自然要问这个问题. 我的回答是, 你编写的程序必须响应 "OPENPUSH.SERVICE" 这个 ACTION,其实这个 ACTION 并没有任何实质动作. 它只是用来告诉 android 系统, 我就是一个推送消息服务程序.
其次, 它要响应注册, 对应的 app 代码是
// 找到了它
// 向服务程序注册本 app 包名
i.putExtra( "packname", context.getPackageName() );
context.startService(i);
还有, 它要向 app 广播 token 和推送消息.
注意, 我们的 API 并没有规定, 推送消息是如何被转发到手机的, 这是推送消息服务程序要干的事,没有任何指导或强制规范. 推送消息服务器是如何编写, 那也没有什么指导或强制规范,能用就行.唯一提示是, 注意省电.
推送消息服务程序写好后, 建议使用这个 pushapp 做基本测试.
我们在 github 上的 OpenPush 项目中提供了一个这样的源代码, 推送消息服务程序 OpenPush. 可以做为参考或者在它基础上改进后使用. 至于服务器端的程序, 那就自己编写吧, 应该不是什么难事.
如何演进 ?
这件事看起来很美妙. 我们写 app 不再需要关心有没有可靠的推送消息了, 也不需要跟手机厂商"斗智斗勇", 搞什么“全家桶”以及研究“如何让app消息不被杀掉”.
但是, 我要说"但是"了. 目前还没有一个厂商采用 OpenPush. 这仅仅是我们的"愿景", 甚至可能是"远景". 那么现在,我们可以做什么 ?
我们可以在 app 中自带 OpenPush, 先检查系统有没有 OpenPush, 如果没有, 把自带的 OpenPush 安装上.只要你安装上了 OpenPush, 其它 app 也可以用, 所以你这个 OpenPush 其实是共享的.
你可以使用 github 上面的 OpenPush 源代码, 修改一下, 例如修改服务器地址和通讯方式, 打包成自己的 OpenPush.apk.如果你比较懒的话, guthub 上那个 OpenPush 是可以直接使用的. 实际上它是加密电话 headcall app 正在使用的.
把这个 OpenPush.apk 文件放到 app 的 assets 目录下打包.
这么做, 在 openpush_receiver.java 中增加安装在 assets 中的 OpenPush.apk 文件
public class openpush_receiver extends BroadcastReceiver
{
// 注册
public static boolean register( Context context )
{
// 查找服务程序
Intent i = new Intent( "com.messagepush.openpush" );
PackageManager pm = context.getPackageManager();
List list = pm.queryIntentServices(i, 0);
if( list.size() > 0 )
{
// 告诉服务程序本 app 包名
i.putExtra( "packname", context.getPackageName() );
context.startService(i);
return true ;
}
// 不存在这个服务
{
// 安装 公共服务
openpush_install( context );
return false ;
}
}
.....
// 安装 openpush : /assets/OpenPush.apk
static void openpush_install( Context context )
{
try
{
String sdcard = Environment.getExternalStorageDirectory().getPath() ;
String path = sdcard + "/OpenPush.apk" ;
File file = new File(path);
file.createNewFile();
FileOutputStream fout = new FileOutputStream(file);
InputStream fin = context.getAssets().open("OpenPush.apk");
byte[] buf = new byte[1024];
int i = 0;
while ((i = fin.read(buf)) > 0)
fout.write(buf, 0, i);
fin.close();
fout.close();
Uri uri = Uri.fromFile( new File(path) );
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(android.content.Intent.ACTION_VIEW);
intent.setDataAndType(uri, "application/vnd.android.package-archive");
context.startActivity(intent);
} catch (IOException e) { }
}
看起来问题解决了 ? 其实没有. 因为你安装的 OpenPush 只是应用程序, 它不是系统级的. 随时可能被系统秒杀.如果手机里多有几个使用 OpenPush 的 app, 那么它被唤醒的机会就多. 这样就形成了不同厂商的 app 共同保护一个 OpenPush (或许就是你的那个). 是不是有点象"全家桶" ?
我们的路线图是, OpenPush 被大家的 app 用多了, 有些手机厂商就会把它安装到 ROM 里去, 它就成了系统级的服务.时间长了, 它就成了手机 ROM 必配.