OpenFire源码学习之十八:IOS离线推送 IOS离线推送

场景:

如果您有IOS端的APP,在会话聊天的时候,用户登陆了但可能会退出了界面。这时候其他终端给目标端发送消息时候,消息可以发送到IOS的推送服务器。用过QQ的都知道,你会有哦一条消息在您的主屏上展示。这个就是利用了IOS的推送服务器呢。那么openfire只需要判断用户不在线的时候将消息推送给IOS端。

苹果服务器的消息推送都需要手机的唯一标志,也就是唯一的终端设备号。那么IOS端在登陆的时候需要将该手机的设备号传递给OF服务器。这个传递很简单,您可以自定义发送IQ消息。也可以在登陆后绑定资料的时候添加JID属性来绑定设备。(建议用绑定资源的形式,这样在服务端判断的时候可以很方便的根据JID的属性值来决定是都推送)

 服务端实现ios消息推送所需2个证书(附件):测试推送证书.p12、正式推送正式.p12,密码都为123456.

OpenFire源码学习之十八:IOS离线推送
IOS离线推送OpenFire源码学习之十八:IOS离线推送
IOS离线推送

 2个证书的区别在于一个是用于开发测试的推送证书,一个是用于产品正式上线的推送证书。2个证书获取到的终端token是不一样的。这2个证书用于JAVA后台连接APNS的服务器地址也是不同的,测试推送证书对应服务器地址是:gateway.sandbox.push.apple.com , 正式推送证书对应的服务器地址是:gateway.push.apple.com .

具体怎么做呢:

1、安装IOS推送服务需要的证书到本地,这个在网上有很多中方法

2、IOS终端登陆发送设备消息给服务器,或者以绑定资源的形式。

3、OF服务端接收该设备ID,并保存起来。

4、当有消息推送时候,根据JID属性push消息。

OpenFire源码学习之十八:IOS离线推送
IOS离线推送


接下来具体看看源码了。

源码

OfflinePushPlugin

public class OfflinePushPlugin implements Component, Plugin, PropertyEventListener, PacketInterceptor{

	
	private static final Logger Log = LoggerFactory.getLogger(OfflinePushPlugin.class);
	
	public static final String NAMESPACE_JABBER_IQ_TOKEN_BIND= "jabber:iq:token:bind";
	public static final String NAMESPACE_JABBER_IQ_TOKEN_UNBUND= "jabber:iq:token:unbund";
	
	public static final String SERVICENAME = "plugin.offlinepush.serviceName";
    public static final String SERVICEENABLED = "plugin.offlinepush.serviceEnabled";
	
    private ComponentManager componentManager;
    private PluginManager pluginManager;

    private String serviceName;
private boolean serviceEnabled;
//证书安装的目录
    private static String dcpath = System.getProperty("openfireHome") + "\conf\";
    private String dcName;
    private String dcPassword;
    private boolean enabled;
	
    private static Map<String, String> map = new ConcurrentHashMap<String, String>(20);
    private static Map<String, Integer> count = new ConcurrentHashMap<String, Integer>(20);
    
    private static AppleNotificationServer appleServer = null;
    private static List<PayloadPerDevice> list ;
    
    public String getDcName() {
		return dcName;
	}

	public void setDcName(String dcName) {
		JiveGlobals.setProperty("plugin.offlinepush.dcName", dcName);
		this.dcName = dcName;
	}

	public String getDcPassword() {
		return dcPassword;
	}

	public void setDcPassword(String dcPassword) {
		JiveGlobals.setProperty("plugin.offlinepush.password", dcPassword);
		this.dcPassword = dcPassword;
	}

	public boolean getEnabled() {
		return enabled;
	}

	public void setEnabled(boolean enabled) {
		this.enabled = enabled;
		JiveGlobals.setProperty("plugin.offlinepush.enabled",  enabled ? "true" : "false");
	}

	public OfflinePushPlugin () {
    	serviceName = JiveGlobals.getProperty(SERVICENAME, "offlinepush");
        serviceEnabled = JiveGlobals.getBooleanProperty(SERVICEENABLED, true);
    }

	@Override
	public void xmlPropertySet(String property, Map<String, Object> params) {
		
	}

	@Override
	public void xmlPropertyDeleted(String property, Map<String, Object> params) {
	}

	@Override
	public void initializePlugin(PluginManager manager, File pluginDirectory) {
		
		dcName = JiveGlobals.getProperty("plugin.offlinepush.dcName", "");
        // If no secret key has been assigned to the user service yet, assign a random one.
        if (dcName.equals("")){
        	dcName = "delementtest.p12";
            setDcName(dcName);
        }
        dcpath += dcName;
		dcPassword = JiveGlobals.getProperty("plugin.offlinepush.password", "");
		if (dcPassword.equals("")){
			dcPassword = "123456";
            setDcPassword(dcPassword);
        }
		
		enabled = JiveGlobals.getBooleanProperty("plugin.offlinepush.enabled");
        setEnabled(enabled);
		
        Log.info("dcpath: " + dcpath);
        Log.info("dcPassword: " + dcPassword);
        Log.info("enabled: " + enabled);
        
		try {
			appleServer = new AppleNotificationServerBasicImpl(dcpath, dcPassword, enabled );
			if (list == null ) {
				list = new ArrayList<PayloadPerDevice>();
			}
			
		} catch (KeystoreException e1) {
			Log.error("KeystoreException: " + e1.getMessage());
		} 
		
		pluginManager = manager;
        
        componentManager = ComponentManagerFactory.getComponentManager();
        try {
            componentManager.addComponent(serviceName, this);
        }
        catch (ComponentException e) {
            Log.error(e.getMessage(), e);
        }
        
        InterceptorManager.getInstance().addInterceptor(this);
        PropertyEventDispatcher.addListener(this);
		
	}

	@Override
	public void destroyPlugin() {
		InterceptorManager.getInstance().removeInterceptor(this);
		PropertyEventDispatcher.removeListener(this);
        pluginManager = null;
        try {
            componentManager.removeComponent(serviceName);
            componentManager = null;
        }
        catch (Exception e) {
            if (componentManager != null) {
                Log.error(e.getMessage(), e);
            }
        }
        serviceName = null;
	}

	@Override
	public String getName() {
		return pluginManager.getName(this);
	}

	@Override
	public String getDescription() {
		return pluginManager.getDescription(this);
	}

	@Override
	public void processPacket(Packet p) {
		if (!(p instanceof IQ)) {
            return;
        }
        final IQ packet = (IQ) p;

        if (packet.getType().equals(IQ.Type.error)
                || packet.getType().equals(IQ.Type.result)) {
            return;
        }
        final IQ replyPacket = handleIQRequest(packet);

        try {
            componentManager.sendPacket(this, replyPacket);
        } catch (ComponentException e) {
            Log.error(e.getMessage(), e);
        }
	}

    private IQ handleIQRequest(IQ iq) {
        final IQ replyPacket; // 'final' to ensure that it is set.

        if (iq == null) {
            throw new IllegalArgumentException("Argument 'iq' cannot be null.");
        }

        final IQ.Type type = iq.getType();
        if (type != IQ.Type.get && type != IQ.Type.set) {
            throw new IllegalArgumentException(
                    "Argument 'iq' must be of type 'get' or 'set'");
        }

        final Element childElement = iq.getChildElement();
        if (childElement == null) {
            replyPacket = IQ.createResultIQ(iq);
            replyPacket
                    .setError(new PacketError(
                            Condition.bad_request,
                            org.xmpp.packet.PacketError.Type.modify,
                            "IQ stanzas of type 'get' and 'set' MUST contain one and only one child element (RFC 3920 section 9.2.3)."));
            return replyPacket;
        }

        final String namespace = childElement.getNamespaceURI();
        if (namespace == null) {
            replyPacket = IQ.createResultIQ(iq);
            replyPacket.setError(Condition.feature_not_implemented);
            return replyPacket;
        }

        if (namespace.equals(NAMESPACE_JABBER_IQ_TOKEN_BIND)) {
            replyPacket = processSetUUID(iq, true);
        } 
        else if (namespace.equals(NAMESPACE_JABBER_IQ_TOKEN_UNBUND)) {
        	replyPacket = processSetUUID(iq, false);
        } 
        else if (namespace.equals(IQDiscoInfoHandler.NAMESPACE_DISCO_INFO)) {
            replyPacket = handleDiscoInfo(iq);
        } 
        else {
            // don't known what to do with this.
            replyPacket = IQ.createResultIQ(iq);
            replyPacket.setError(Condition.feature_not_implemented);
        }

        return replyPacket;
    }
	
    private static IQ handleDiscoInfo(IQ iq) {
        if (iq == null) {
            throw new IllegalArgumentException("Argument 'iq' cannot be null.");
        }

        if (!iq.getChildElement().getNamespaceURI().equals(
                IQDiscoInfoHandler.NAMESPACE_DISCO_INFO)
                || iq.getType() != Type.get) {
            throw new IllegalArgumentException(
                    "This is not a valid disco#info request.");
        }

        final IQ replyPacket = IQ.createResultIQ(iq);

        final Element responseElement = replyPacket.setChildElement("query",
                IQDiscoInfoHandler.NAMESPACE_DISCO_INFO);
        responseElement.addElement("identity").addAttribute("category",
                "directory").addAttribute("type", "user").addAttribute("name",
                "Offline Push");
        responseElement.addElement("feature").addAttribute("var",
                NAMESPACE_JABBER_IQ_TOKEN_BIND);
        responseElement.addElement("feature").addAttribute("var",
                IQDiscoInfoHandler.NAMESPACE_DISCO_INFO);
        responseElement.addElement("feature").addAttribute("var",
                ResultSet.NAMESPACE_RESULT_SET_MANAGEMENT);

        return replyPacket;
    }
    
    private IQ processSetUUID(IQ packet, boolean isSet) {
    	Element rsmElement = null;
        if (!packet.getType().equals(IQ.Type.set)) {
            throw new IllegalArgumentException(
                    "This method only accepts 'set' typed IQ stanzas as an argument.");
        }

        final IQ resultIQ;

        final Element incomingForm = packet.getChildElement();

        rsmElement = incomingForm.element(QName.get("info",
        		NAMESPACE_JABBER_IQ_TOKEN_UNBUND));
        
        if(rsmElement == null) {
        	rsmElement = incomingForm.element(QName.get("info",
            		NAMESPACE_JABBER_IQ_TOKEN_BIND));
        }

        resultIQ = IQ.createResultIQ(packet);
        if (rsmElement != null) {
            String osElement = rsmElement.attributeValue("os");
            String jidElement = rsmElement.attributeValue("jid");
            
            String username = new JID(jidElement).getNode();
         
            if (osElement == null || jidElement == null) {
                resultIQ.setError(Condition.bad_request);
                return resultIQ;
            }
            if (isSet) {
            	String tokenElement = rsmElement.attributeValue("token");
            	map.put(username, tokenElement);
            	count.put(username, 0);
            	Log.info("set token,username:" + username + " ,token:" + tokenElement);
            }
            else {
            	map.remove(username);
            	count.remove(username);
            	Log.info("remove token,username:" + username );
            }
        } 
        else{
            resultIQ.setError(Condition.bad_request);
        }

        return resultIQ;
    }
    
    public String getServiceName() {
        return serviceName;
    }
    
    public void setServiceName(String name) {
        JiveGlobals.setProperty(SERVICENAME, name);
    }
    
    public boolean getServiceEnabled() {
        return serviceEnabled;
    }
    
    public void setServiceEnabled(boolean enabled) {
        serviceEnabled = enabled;
        JiveGlobals.setProperty(SERVICEENABLED, enabled ? "true" : "false");
    }
    
    public void propertySet(String property, Map<String, Object> params) {
        if (property.equals(SERVICEENABLED)) {
            this.serviceEnabled = Boolean.parseBoolean((String)params.get("value"));
        }
        if (property.equals("plugin.offlinepush.dcName")) {
            this.dcName = (String)params.get("value");
        }
        else if (property.equals("plugin.offlinepush.enabled")) {
            this.enabled = Boolean.parseBoolean((String)params.get("value"));
        }
        else if (property.equals("plugin.offlinepush.password")) {
        	this.dcPassword = (String)params.get("value");
        }
    }

	/*
	 * (non-Javadoc)
	 * 
	 * @see org.jivesoftware.util.PropertyEventListener#propertyDeleted(java.lang.String,
	 *      java.util.Map)
	 */
	public void propertyDeleted(String property, Map<String, Object> params) {
        if (property.equals(SERVICEENABLED)) {
            this.serviceEnabled = true;
        }
        if (property.equals("plugin.offlinepush.dcName")) {
            this.dcName = "delementtest.p12";
        }
        else if (property.equals("plugin.offlinepush.enabled")) {
            this.enabled = false;
        }
        else if (property.equals("plugin.offlinepush.password")) {
        	this.dcPassword = "123456";
        }
    }
    
	@Override
	public void initialize(JID jid, ComponentManager componentManager)
			throws ComponentException {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void start() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void shutdown() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void interceptPacket(Packet packet, Session session,
			boolean incoming, boolean processed) throws PacketRejectedException {
		if (processed && incoming) {
			if (packet instanceof Message) {
				if (((Message) packet).getBody() == null) {
					return;
				}
				JID jid = packet.getTo();
                //获取用户的设备标志id
				String uuid = map.get(jid.getNode());
				if (uuid != null && !"".equals(uuid)) {
					User user = null;
					try {
						user = XMPPServer.getInstance().getUserManager().getUser(jid.getNode());
					} catch (UserNotFoundException e2) {
						e2.printStackTrace();
					}
					PresenceManager presenceManager = XMPPServer.getInstance().getPresenceManager();
					org.xmpp.packet.Presence presence = presenceManager.getPresence(user);
					if (presence == null) {
						
						String body = ((Message) packet).getBody();
						JSONObject jb = null;
						String msgType = "10015";
						try {
 
							jb = new JSONObject(body);
							msgType = jb.getString("msgType");
							if ("10012".equals(msgType) || "10001".equals(msgType) || "10002".equals(msgType)) {
								return;
							}
						} catch (JSONException e) {
							try {
                              //根据不同的消息类型,发送不通的提示语
								msgType = jb.getInt("msgType")+"";
								if ("10012".equals(msgType) || "10001".equals(msgType) || "10002".equals(msgType)) {
									return;
								}
							} catch (JSONException e1) {
								msgType = "10015";
							}
						}
						if (msgType != null) {
							//msgType = "offlinepush." + msgType;
							String pushCont = LocaleUtils.getLocalizedString("offlinepush.10015", "offlinepush");
							if (!"10000".equals(msgType)) {
								msgType = "offlinepush." + msgType;
								pushCont = LocaleUtils.getLocalizedString(msgType, "offlinepush");
							}
							else {
								pushCont = LocaleUtils.getLocalizedString("offlinepush.10000", "offlinepush");
								String cont = LocaleUtils.getLocalizedString("offlinepush.other", "offlinepush");;
								String mtype  = "";
								try {
									mtype = jb.getString("mtype");
								} catch (JSONException e) {
									try {
										mtype = jb.getInt("mtype") + "";
									} catch (JSONException e1) {
										msgType = "10015";
									}
								}
								if ("0".equals(mtype)) {
									try {
										cont = jb.getString("Cnt");
										if (cont.length() > 20) {
											cont = cont.substring(0, 20);
											cont += "...";
										}
										
									} catch (JSONException e) {
									}
									
											
								}
								else if ("1".equals(mtype)) {
									cont = LocaleUtils.getLocalizedString("offlinepush.image", "offlinepush"); 
								}
								else if ("2".equals(mtype)) {
									cont = LocaleUtils.getLocalizedString("offlinepush.audio", "offlinepush"); 
								}
								else if ("4".equals(mtype)) {
									cont = LocaleUtils.getLocalizedString("offlinepush.file", "offlinepush"); 
								}
								else if ("3".equals(mtype)) {
									cont = LocaleUtils.getLocalizedString("offlinepush.location", "offlinepush"); 
								}
								else if ("6".equals(mtype)) {
									cont = LocaleUtils.getLocalizedString("offlinepush.video", "offlinepush");
								}
								pushCont += cont;
							}
							pushOfflineMsg(uuid, pushCont, jid);
						}
					}
				}
			}
		}
		
	}

	private void pushOfflineMsg(String token, String pushCont, JID jid) {
			NotificationThreads work = null;
        try {
        	
        	Integer size = count.get(jid.getNode()) + 1; 
        	if (size <= 1000)
        		count.put(jid.getNode(), size);
        	List<PayloadPerDevice> list = new ArrayList<PayloadPerDevice>();
        	PushNotificationPayload payload = new PushNotificationPayload(); 
			payload.addAlert(pushCont);
			payload.addSound("default"); 
	        payload.addBadge(size);
	        payload.addCustomDictionary("jid", jid.toString());
	        PayloadPerDevice pay = new PayloadPerDevice(payload, token);
	        list.add(pay); 
	        work = new NotificationThreads(appleServer,list,1);
	        work.setListener(DEBUGGING_PROGRESS_LISTENER);
	        work.start(); 
		} catch (JSONException e) {
			Log.error("JSONException:" + e.getMessage());
		} catch (InvalidDeviceTokenFormatException e) {
			Log.error("InvalidDeviceTokenFormatException:" + e.getMessage());
		}finally{
			work.destroy();
			Log.info("push to apple: username: " + jid.getNode() + " ,context" + pushCont);
		}
	}
	
	public Runnable createTask(final String token, final String msgType, final JID jid) {
		return new Runnable() {
			@Override
			public void run() {
				PushNotificationPayload payload = new PushNotificationPayload();  
		        try {
		        	String pushCont = LocaleUtils.getLocalizedString(msgType, "offlinepush");
		        	List<PayloadPerDevice> list = new ArrayList<PayloadPerDevice>();
					payload.addAlert(pushCont);
					payload.addSound("default"); 
			        payload.addBadge(1);
			        payload.addCustomDictionary("jid", jid.toString());
			        PayloadPerDevice pay = new PayloadPerDevice(payload, token);
			        list.add(pay); 
			        NotificationThreads work = new NotificationThreads(appleServer,list,1);
			        work.setListener(DEBUGGING_PROGRESS_LISTENER);
			        work.start(); 
				} catch (JSONException e) {
					Log.error("JSONException:" + e.getMessage());
				} catch (InvalidDeviceTokenFormatException e) {
					Log.error("InvalidDeviceTokenFormatException:" + e.getMessage());
				} 
			}
		};
		
	}
	
	public static final NotificationProgressListener DEBUGGING_PROGRESS_LISTENER = new NotificationProgressListener() {  
        public void eventThreadStarted(NotificationThread notificationThread) {  
            System.out.println("   [EVENT]: thread #" + notificationThread.getThreadNumber() + " started with " + " devices beginning at message id #" + notificationThread.getFirstMessageIdentifier());  
        }  
        public void eventThreadFinished(NotificationThread thread) {  
            System.out.println("   [EVENT]: thread #" + thread.getThreadNumber() + " finished: pushed messages #" + thread.getFirstMessageIdentifier() + " to " + thread.getLastMessageIdentifier() + " toward "+ " devices");  
        }  
        public void eventConnectionRestarted(NotificationThread thread) {  
            System.out.println("   [EVENT]: connection restarted in thread #" + thread.getThreadNumber() + " because it reached " + thread.getMaxNotificationsPerConnection() + " notifications per connection");  
        }  
        public void eventAllThreadsStarted(NotificationThreads notificationThreads) {  
            System.out.println("   [EVENT]: all threads started: " + notificationThreads.getThreads().size());  
        }  
        public void eventAllThreadsFinished(NotificationThreads notificationThreads) {  
            System.out.println("   [EVENT]: all threads finished: " + notificationThreads.getThreads().size());  
        }  
        public void eventCriticalException(NotificationThread notificationThread, Exception exception) {  
            System.out.println("   [EVENT]: critical exception occurred: " + exception);  
        }  
     }; 
}

Plugin.xml

<?xml version="1.0" encoding="UTF-8"?>

<plugin>
    <class>com.....offlinepush.plugin.OfflinePushPlugin</class>
    <name>offlinepush</name>
    <description>.......</description>
    <author>huwenfeng</author>
    <version>1.5.1</version>
    <date>1/2/2014</date>
    <minServerVersion>3.7.0</minServerVersion>
</plugin>

资源文件:offlinepush_i18n_zh_CN.properties

offlinepush.10000=u65B0u6D88u606FuFF1A
offlinepush.10001=u7528u6237u64CDu4F5C
offlinepush.image=[u56FEu7247]
offlinepush.audio=[u8BEDu97F3]
offlinepush.file=[u6587u4EF6]
offlinepush.other=[u5176u4ED6]
offlinepush.location=[u4F4Du7F6E]
offlinepush.video=[u89C6u9891]
...... 

需要的jar包。

OpenFire源码学习之十八:IOS离线推送
IOS离线推送OpenFire源码学习之十八:IOS离线推送
IOS离线推送OpenFire源码学习之十八:IOS离线推送
IOS离线推送

OK啦。

注意:IOS的推送服务器有两种模式都是免费,一种是测试的还一种是正式使用的。

所以这里最好将推送服务的使用模式在OF的管理台做配置。

本人在控制台配置了三个属性值:

OpenFire源码学习之十八:IOS离线推送
IOS离线推送