极光推送客户端和服务器端开发
2017-03-10 本文已影响1447人
田间小鹿
现在大不分应用都有推送的功能,今天总结下最近用到的极光推动的android端和服务器端开发过程。
1.1推送的原理
分服务器和客户端分别进行说明:
1.推送框架图
jpush.png- 推送的数据源:自己开发的服务器端或者使用极光推送官网的WEB后台(++用于客户自定义服务器向推送框架进行推送++)
- JPush API:部署在服务器端,开发者的服务器端发起推送时,将数据传到JPush API中,然后向下传递
- 建立长链接:集成JPush的SDK客户端启动后会建立一个到JPush Cloud的长链接(++手机客户端收到推送消息++)
1.2客户端原理
客户端需要和服务器保存长连接状态。jpush SDK提供了TCP Long Connection。实现的原理:
- 心跳:为了长时间保持外网IP,需要客户端定期发送心跳给运营商,以便刷新NAT列表
- Timer定时方法:该类计划循环执行定时任务,但是使用该类会使CPU保持唤醒状态,比较费电。
- AlarmManager定时方法:该类封装了Android手机的RTC硬件时钟模块,可以在CPU休眠时正常运行,保持任务执行时再唤醒CPU,这样做到了电量节省。
2.实现步骤
2.1注册极光推送
极光推送网址:https://www.jiguang.cn
进行自行注册,然后进入控制台,添加自己的应用。在应用设置中就能看到自己的AppKey的值,这个是此用于的唯一标示符,在客户端开发和服务器端开发中都会用到此值。
2.2集成android客户端
集成android.png按照上图的提示进行炒作即可。
下面在android studio中介绍下集成流程:
- builde.gradle中添加引用,即导入依赖包
compile 'cn.jiguang.sdk:jpush:3.0.0'
compile 'cn.jiguang.sdk:jcore:1.0.0'
2.添加统计信息,也可以不使用
@Override
protected void onResume() {
super.onResume();
MobclickAgent.onResume(this);
}
@Override
protected void onPause() {
super.onPause();
MobclickAgent.onPause(this);
}
3.jpush初始化
public class MyApplication extends Application {
public static String APPID = "915a5566e71a88d33a2851e0d2b19512";
@Override
public void onCreate() {
JPushInterface.setDebugMode(true); // 设置开启日志,发布时请关闭日志
JPushInterface.init(this); // 初始化 JPush
}
}
4.在AndroidMainfest.xml添加
<!-- Required 极光推送添加 -->
<permission
android:name="com.ylink.MGessTraderYlink_gxjszx.permission.JPUSH_MESSAGE"
android:protectionLevel="signature" />
<uses-permission android:name="com.ylink.MGessTraderYlink_gxjszx.permission.JPUSH_MESSAGE" />
<uses-permission android:name="android.permission.RECEIVE_USER_PRESENT" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WRITE_SETTINGS" />
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<!-- Required SDK核心功能 -->
<activity
android:name="cn.jpush.android.ui.PushActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="false"
android:theme="@android:style/Theme.NoTitleBar">
<intent-filter>
<action android:name="cn.jpush.android.ui.PushActivity" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="com.ylink.MGessTraderYlink_gxjszx" />
</intent-filter>
</activity>
<!-- Required SDK核心功能 -->
<service
android:name="cn.jpush.android.service.DownloadService"
android:enabled="true"
android:exported="false" />
<!-- Required SDK 核心功能 -->
<!-- 可配置android:process参数将PushService放在其他进程中 -->
<service
android:name="cn.jpush.android.service.PushService"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="cn.jpush.android.intent.REGISTER" />
<action android:name="cn.jpush.android.intent.REPORT" />
<action android:name="cn.jpush.android.intent.PushService" />
<action android:name="cn.jpush.android.intent.PUSH_TIME" />
</intent-filter>
</service>
<!-- Required SDK核心功能 -->
<receiver
android:name="cn.jpush.android.service.PushReceiver"
android:enabled="true">
<intent-filter android:priority="1000">
<action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED_PROXY" /> <!-- Required 显示通知栏 -->
<category android:name="com.ylink.MGessTraderYlink_gxjszx" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.USER_PRESENT" />
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
</intent-filter>
<!-- Optional -->
<intent-filter>
<action android:name="android.intent.action.PACKAGE_ADDED" />
<action android:name="android.intent.action.PACKAGE_REMOVED" />
<data android:scheme="package" />
</intent-filter>
</receiver>
<!-- Required SDK核心功能 -->
<receiver
android:name="cn.jpush.android.service.AlarmReceiver"
android:exported="false" />
<!-- User defined. For test only 用户自定义的广播接收器 -->
<receiver
android:name=".MyReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="cn.jpush.android.intent.REGISTRATION" /> <!-- Required 用户注册SDK的intent -->
<action android:name="cn.jpush.android.intent.MESSAGE_RECEIVED" /> <!-- Required 用户接收SDK消息的intent -->
<action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED" /> <!-- Required 用户接收SDK通知栏信息的intent -->
<action android:name="cn.jpush.android.intent.NOTIFICATION_OPENED" /> <!-- Required 用户打开自定义通知栏的intent -->
<action android:name="cn.jpush.android.intent.CONNECTION" /> <!-- 接收网络变化 连接/断开 since 1.6.3 -->
<category android:name="com.ylink.MGessTraderYlink_gxjszx" />
</intent-filter>
</receiver>
<!-- Required . Enable it you can get statistics data with channel -->
<meta-data
android:name="JPUSH_CHANNEL"
android:value="developer-default" />
<meta-data
android:name="JPUSH_APPKEY"
android:value="yourappkey" />
5.自定义MyReceiver
/**
* 自定义接收器
*
* 如果不定义这个 Receiver,则:
* 1) 默认用户会打开主界面
* 2) 接收不到自定义消息
*/
public class MyReceiver extends BroadcastReceiver {
private NotificationManager notificationManager;
private int reqCode = 0;
private NotificationImpl notificationImpl;
@Override
public void onReceive(Context context, Intent intent) {
if (notificationManager == null) {
notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
}
Bundle bundle = intent.getExtras();
MyLog.i("[MyReceiver] onReceive - " + intent.getAction() + ", extras: " + printBundle(bundle));
if (JPushInterface.ACTION_REGISTRATION_ID.equals(intent.getAction())) {
String regId = bundle.getString(JPushInterface.EXTRA_REGISTRATION_ID);
MyLog.i("[MyReceiver] 接收Registration Id : " + regId);
} else if (JPushInterface.ACTION_MESSAGE_RECEIVED.equals(intent.getAction())) {
MyLog.i("[MyReceiver] 接收到推送下来的自定义消息: " + bundle.getString(JPushInterface.EXTRA_MESSAGE));
showNotification(context,bundle);
} else if (JPushInterface.ACTION_NOTIFICATION_RECEIVED.equals(intent.getAction())) {
MyLog.i("[MyReceiver] 接收到推送下来的通知");
int notifactionId = bundle.getInt(JPushInterface.EXTRA_NOTIFICATION_ID);
MyLog.i("[MyReceiver] 接收到推送下来的通知的ID: " + notifactionId);
receivingNotification(context,bundle);
} else if (JPushInterface.ACTION_NOTIFICATION_OPENED.equals(intent.getAction())) {
MyLog.i("[MyReceiver] 用户点击打开了通知");
openNotification(context,bundle);
} else if (JPushInterface.ACTION_RICHPUSH_CALLBACK.equals(intent.getAction())) {
MyLog.i("[MyReceiver] 用户收到到RICH PUSH CALLBACK: " + bundle.getString(JPushInterface.EXTRA_EXTRA));
//在这里根据 JPushInterface.EXTRA_EXTRA 的内容处理代码,比如打开新的Activity, 打开一个网页等..
} else if(JPushInterface.ACTION_CONNECTION_CHANGE.equals(intent.getAction())) {
boolean connected = intent.getBooleanExtra(JPushInterface.EXTRA_CONNECTION_CHANGE, false);
MyLog.i("[MyReceiver]" + intent.getAction() +" connected state change to "+connected);
} else {
MyLog.i("[MyReceiver] Unhandled intent - " + intent.getAction());
}
}
//自定义通知框
private void showNotification(Context context, Bundle bundle) {
String extras = bundle.getString(JPushInterface.EXTRA_EXTRA);
reqCode++;
String notificationType = "0";
try {
JSONObject extrasJson = new JSONObject(extras);
notificationType = extrasJson.optString("Notificaction");
} catch (Exception e) {
MyLog.e("Unexpected: extras is not a valid json" + e.toString());
}
if (notificationImpl == null) {
notificationImpl = new NotificationImpl(context);
}
notificationImpl.setRequestCode(reqCode);
switch (notificationType){
case "0":
notificationImpl.notify_normal_singLine(bundle);
break;
case "1":
notificationImpl.notify_normal_moreLine(bundle);
break;
case "2":
notificationImpl.notify_mailbox(bundle);
break;
case "3":
notificationImpl.notify_bigPic(bundle);
break;
case "4":
notificationImpl.notify_customview(bundle);
break;
case "5":
notificationImpl.notify_buttom(bundle);
break;
case "6":
notificationImpl.notify_progress(bundle);
break;
case "7":
notificationImpl.notify_headUp(bundle);
break;
default:
break;
}
}
private void receivingNotification(Context context, Bundle bundle) {
CustomPushNotificationBuilder builder = new
CustomPushNotificationBuilder(context,
R.layout.yyb_notification,
R.id.icon,
R.id.title,
R.id.text);
// 指定定制的 Notification Layout
builder.statusBarDrawable = R.drawable.down;
// 指定最顶层状态栏小图标
builder.layoutIconDrawable = R.drawable.arrow_up;
// 指定下拉状态栏时显示的通知图标
JPushInterface.setPushNotificationBuilder(2, builder);
String title = bundle.getString(JPushInterface.EXTRA_NOTIFICATION_TITLE);
MyLog.i( " title : " + title);
String message = bundle.getString(JPushInterface.EXTRA_ALERT);
MyLog.i( "message : " + message);
String extras = bundle.getString(JPushInterface.EXTRA_EXTRA);
MyLog.i( "extras : " + extras);
}
private void openNotification(Context context, Bundle bundle) {
String extras = bundle.getString(JPushInterface.EXTRA_EXTRA);
String myValue = "";
try {
JSONObject extrasJson = new JSONObject(extras);
myValue = extrasJson.optString("myKey");
} catch (Exception e) {
MyLog.e("Unexpected: extras is not a valid json" + e.toString());
return;
}
}
// 打印所有的 intent extra 数据
private static String printBundle(Bundle bundle) {
StringBuilder sb = new StringBuilder();
for (String key : bundle.keySet()) {
if (key.equals(JPushInterface.EXTRA_NOTIFICATION_ID)) {
sb.append("\nkey:" + key + ", value:" + bundle.getInt(key));
}else if(key.equals(JPushInterface.EXTRA_CONNECTION_CHANGE)){
sb.append("\nkey:" + key + ", value:" + bundle.getBoolean(key));
} else if (key.equals(JPushInterface.EXTRA_EXTRA)) {
if (TextUtils.isEmpty(bundle.getString(JPushInterface.EXTRA_EXTRA))) {
MyLog.d("This message has no Extra data");
continue;
}
try {
JSONObject json = new JSONObject(bundle.getString(JPushInterface.EXTRA_EXTRA));
Iterator<String> it = json.keys();
while (it.hasNext()) {
String myKey = it.next().toString();
sb.append("\nkey:" + key + ", value: [" +
myKey + " - " +json.optString(myKey) + "]");
}
} catch (JSONException e) {
MyLog.e("Get message extra JSON error!");
}
} else {
sb.append("\nkey:" + key + ", value:" + bundle.getString(key));
}
}
return sb.toString();
}
}
ps:
- 通知信息和自定义信息的区别是:通知会在通知栏中显示,自定义需要自己出来,可以自定义显示。
- 通知状态栏也可以自行定义。
- showNotification可以自行定义通知框的显示。但是需要后台服务器中下发对应的字段进行控制。这里使用了一个开源通知库: https://github.com/wenmingvs/NotifyUtil。 但是小米mui8手机上通知信息达不到效果。在其他的手机上还是有效果。
6.定向推送
在开发中为了定向推送到指定的手机上,所以需要在客户端中绑定相应的别名,这样就可以定向推送到指定的手机上了。因为客户端和服务器端都是自己开发,可以通过用户名做为别名进行绑定。
// 调用 Handler 来异步设置别名
if (!user_id.equals(SpUtils.get(getApplicationContext(),"TAG_ALIAS",""))){
SpUtils.remove(getApplicationContext(),"TAG_ALIAS");
mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_ALIAS, MUtils.user_id));
}
private final TagAliasCallback mAliasCallback = new TagAliasCallback() {
@Override
public void gotResult(int code, String alias, Set<String> tags) {
String logs ;
switch (code) {
case 0:
logs = "Set tag and alias success";
MyLog.i(logs);
SpUtils.put(getApplicationContext(),"TAG_ALIAS",alias);
break;
case 6002:
logs = "Failed to set alias and tags due to timeout. Try again after 60s.";
MyLog.i( logs);
// 延迟 60 秒来调用 Handler 设置别名
mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SET_ALIAS, alias), 1000 * 60);
break;
default:
logs = "Failed with errorCode = " + code;
MyLog.i(logs);
}
}
};
private static final int MSG_SET_ALIAS = 1001;
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(android.os.Message msg) {
super.handleMessage(msg);
switch (msg.what) {
case MSG_SET_ALIAS:
MyLog.i("Set alias in handler.");
// 调用 JPush 接口来设置别名。
JPushInterface.setAliasAndTags(getApplicationContext(),
(String) msg.obj,
null,
mAliasCallback);
break;
default:
MyLog.i( "Unhandled msg - " + msg.what);
}
}
};
这里使用SharedPreferences
public class SpUtils
{
/**
* 保存在手机里面的文件名
*/
public static final String FILE_NAME = "share_data";
/**
* 保存数据的方法,我们需要拿到保存数据的具体类型,然后根据类型调用不同的保存方法
*
* @param context
* @param key
* @param object
*/
public static void put(Context context, String key, Object object)
{
SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
if (object instanceof String)
{
editor.putString(key, (String) object);
} else if (object instanceof Integer)
{
editor.putInt(key, (Integer) object);
} else if (object instanceof Boolean)
{
editor.putBoolean(key, (Boolean) object);
} else if (object instanceof Float)
{
editor.putFloat(key, (Float) object);
} else if (object instanceof Long)
{
editor.putLong(key, (Long) object);
} else
{
editor.putString(key, object.toString());
}
SharedPreferencesCompat.apply(editor);
}
/**
* 得到保存数据的方法,我们根据默认值得到保存的数据的具体类型,然后调用相对于的方法获取值
*
* @param context
* @param key
* @param defaultObject
* @return
*/
public static Object get(Context context, String key, Object defaultObject)
{
SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
Context.MODE_PRIVATE);
if (defaultObject instanceof String)
{
return sp.getString(key, (String) defaultObject);
} else if (defaultObject instanceof Integer)
{
return sp.getInt(key, (Integer) defaultObject);
} else if (defaultObject instanceof Boolean)
{
return sp.getBoolean(key, (Boolean) defaultObject);
} else if (defaultObject instanceof Float)
{
return sp.getFloat(key, (Float) defaultObject);
} else if (defaultObject instanceof Long)
{
return sp.getLong(key, (Long) defaultObject);
}
return null;
}
/**
* 移除某个key值已经对应的值
* @param context
* @param key
*/
public static void remove(Context context, String key)
{
SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.remove(key);
SharedPreferencesCompat.apply(editor);
}
/**
* 清除所有数据
* @param context
*/
public static void clear(Context context)
{
SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
Context.MODE_PRIVATE);
SharedPreferences.Editor editor = sp.edit();
editor.clear();
SharedPreferencesCompat.apply(editor);
}
/**
* 查询某个key是否已经存在
* @param context
* @param key
* @return
*/
public static boolean contains(Context context, String key)
{
SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
Context.MODE_PRIVATE);
return sp.contains(key);
}
/**
* 返回所有的键值对
*
* @param context
* @return
*/
public static Map<String, ?> getAll(Context context)
{
SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
Context.MODE_PRIVATE);
return sp.getAll();
}
/**
* 创建一个解决SharedPreferencesCompat.apply方法的一个兼容类
*
* @author zhy
*
*/
private static class SharedPreferencesCompat
{
private static final Method sApplyMethod = findApplyMethod();
/**
* 反射查找apply的方法
*
* @return
*/
@SuppressWarnings({ "unchecked", "rawtypes" })
private static Method findApplyMethod()
{
try
{
Class clz = SharedPreferences.Editor.class;
return clz.getMethod("apply");
} catch (NoSuchMethodException e)
{
}
return null;
}
/**
* 如果找到则使用apply执行,否则使用commit
*
* @param editor
*/
public static void apply(SharedPreferences.Editor editor)
{
try
{
if (sApplyMethod != null)
{
sApplyMethod.invoke(editor);
return;
}
} catch (IllegalArgumentException e)
{
} catch (IllegalAccessException e)
{
} catch (InvocationTargetException e)
{
}
editor.commit();
}
}
}
到此android客户端集成结束。
2.3服务端集成
- 1.这里我使用的是java服务器端。源码地址为: https://github.com/jpush/jpush-api-java-client
- 2.这里还需要依赖一个公共包。源码: https://github.com/jpush/jiguang-java-client-common
- 3.如果只是使用可以直接下载jar包即可 https://github.com/jpush/jpush-api-java-client/releases
引入到现有工程中。把1中的jar和测试文件到入到工程中,把3中的jar文件导入到工程中。
jpush_web.png详细的使用可以参考1中的介绍。我这只对发通知和消息和指定对象推送进行测试
public class Test {
protected static final Logger LOG = LoggerFactory.getLogger(Test.class);
public static final String ALERT = "数据";
public static void main(String[] args) {
testSendPush();
}
public static void testSendPush() {
JPushClient jpushClient = new JPushClient(Config.masterSecret, Config.appKey);
PushPayload payload = buildPushObject_all_all_alert();
try {
PushResult result = jpushClient.sendPush(payload);
LOG.info("Got result - " + result);
} catch (APIConnectionException e) {
LOG.error("Connection error. Should retry later. ", e);
} catch (APIRequestException e) {
LOG.error("Error response from JPush server. Should review and fix it. ", e);
LOG.info("HTTP Status: " + e.getStatus());
LOG.info("Error Code: " + e.getErrorCode());
LOG.info("Error Message: " + e.getErrorMessage());
LOG.info("Msg ID: " + e.getMsgId());
}
}
public static PushPayload buildPushObject_all_all_alert() {
// PushPayload payload = PushPayload
// .newBuilder()
// .setPlatform(Platform.android())
// .setAudience(Audience.tag("tag1"))
// .setNotification(
// Notification
// .newBuilder()
// .setAlert(ALERT)
// .addPlatformNotification(
// IosNotification.newBuilder().incrBadge(1)
// .addExtra("extra_key", "extra_value").build())
// .build()).build();
// return payload;
Map<String, String> extras = new HashMap<String, String>();
extras.put("Notificaction", "0");
extras.put("URL", "www.baidu.com");
Message.content("数据");
PushPayload payload = PushPayload.newBuilder()
.setPlatform(Platform.android())
.setAudience(Audience.alias("0123456789"))
.setOptions(Options.sendno())
.setNotification(Notification.android("一个惊喜", "号外", extras))
//.setMessage(Message.newBuilder().setMsgContent("").addExtra("Notificaction", "0").build())
// .setMessage(
// Message.newBuilder().setMsgContent("数据").addExtra("Notificaction", "7").setTitle("大爷的")
// .build())
.build();
return payload;
}
}
到处服务端集成成功。