最近开发的一款人脸识别终端管理系统,主要包括运营平台、企业后台管理系统、App端、智能人脸识别终端模块;下图是系统的架构图:
其中各个模块之间都需要即时通讯,比如:
…大概十多项通知都离不开消息的推送,此时最先想到的自然是集成第三方稳定高效的即时推送方案(App端还需要有强大的离线推送功能)。
尊重原创,转载请注明出处,原文地址: http://blog.csdn.net/qq137722697
集成第三方推送无非出于以下几点考虑:推送稳定高效、方便集成、App离线模式支持更多的机型(通道)、App进程互相拉起、有效的数据统计,最重要的一点要免费;无疑极光推送都满足了这几方面,本人是极光推送几年的老用户,,并且在几年前写过一篇【 (快速搞定)2分钟集成极光推送(极光推送Android端集成)】的文章,当时使用极光推送的时候还没有几家推送SDK,可以说极光推送是较早的一批推送方案了,这么多年的积累 稳定性方面毋庸置疑。
下面一步一步教大家快速集成极光推送(尽量按下面的方式进行,说不定可以少走很多弯路哦):
官方可能要考虑一些额外的情况加了个一个手动集成的教程,现在都2020年了当然得用依赖jcenter项目的方式来集成啦,如需手动集成请移步官方稳定;
开发环境:
开发者账号注册、创建应用这些就不说了哈,根据官网提示即可创建完成。
apply plugin: 'com.android.application'
android {
compileSdkVersion 26
defaultConfig {
applicationId "com.hdl.smartface"//JPush 上注册的包名.
minSdkVersion 17
targetSdkVersion 22
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
//极光推送配置----------开始-----------
ndk {
//一般armeabi-v7a已能适配绝大部分机型
abiFilters 'armeabi-v7a'
// 还可以添加 'armeabi', 'arm64-v8a', 'x86', 'x86_64', 'mips', 'mips64'
}
manifestPlaceholders = [
JPUSH_PKGNAME: applicationId,
JPUSH_APPKEY : "307d4e883d70b727687597fb", //JPush 上注册的包名对应的 Appkey.
JPUSH_CHANNEL: "developer-default", //暂时填写默认值即可.
]
//极光推送配置----------结束-----------
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:26.1.0'
testImplementation 'junit:junit:4.12'
androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
//极光推送相关库
implementation 'cn.jiguang.sdk:jpush:3.5.4'
implementation 'cn.jiguang.sdk:jcore:2.2.6'
//日志库
implementation 'com.hdl:elog:v2.0.2'
}
在AndroidManifest.xml的application中加入极光推送必要的配置
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.hdl.smartface">
<application
android:name=".base.MyApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
intent-filter>
activity>
<activity
android:name="cn.jpush.android.ui.PopWinActivity"
android:exported="true"
android:theme="@style/MyDialogStyle"
tools:node="replace">
<intent-filter>
<category android:name="android.intent.category.DEFAULT" />
<action android:name="cn.jpush.android.ui.PopWinActivity" />
<category android:name="${applicationId}" />
intent-filter>
activity>
<activity
android:name="cn.jpush.android.ui.PushActivity"
android:configChanges="orientation|keyboardHidden"
android:exported="true"
android:theme="@android:style/Theme.NoTitleBar"
tools:node="replace">
<intent-filter>
<action android:name="cn.jpush.android.ui.PushActivity" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="${applicationId}" />
intent-filter>
activity>
<receiver
android:name=".receiver.JpushReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="cn.jpush.android.intent.REGISTRATION" />
<action android:name="cn.jpush.android.intent.MESSAGE_RECEIVED" />
<action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED" />
<action android:name="cn.jpush.android.intent.NOTIFICATION_OPENED" />
<action android:name="cn.jpush.android.intent.CONNECTION" />
<category android:name="com.hdl.smartface" />
intent-filter>
receiver>
<receiver android:name=".receiver.MyJpushMessageReceiver">
<intent-filter>
<action android:name="cn.jpush.android.intent.RECEIVE_MESSAGE" />
<category android:name="com.hdl.smartface">category>
intent-filter>
receiver>
<receiver
android:name="cn.jpush.android.service.PushReceiver"
android:enabled="true"
android:exported="false">
<intent-filter android:priority="1000">
<action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED_PROXY" />
<category android:name="com.hdl.smartface" />
intent-filter>
<intent-filter>
<action android:name="android.intent.action.USER_PRESENT" />
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
intent-filter>
<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>
<receiver
android:name="cn.jpush.android.service.AlarmReceiver"
android:exported="false" />
<service
android:name="cn.jpush.android.service.PushService"
android:exported="false"
android:process=":pushcore">
<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>
<service
android:name=".services.PushService"
android:process=":pushcore">
<intent-filter>
<action android:name="cn.jiguang.user.service.action" />
intent-filter>
service>
application>
manifest>
在proguard-rules.pro文件中加入一下配置
#极光推送混淆配置-------------开始------------
-dontoptimize
-dontpreverify
-dontwarn cn.jpush.**
-keep class cn.jpush.** { *; }
-keep class * extends cn.jpush.android.helpers.JPushMessageReceiver { *; }
-dontwarn cn.jiguang.**
-keep class cn.jiguang.** { *; }
#极光推送混淆配置-------------结束------------
package com.hdl.smartface.receiver;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.text.TextUtils;
import com.hdl.elog.ELog;
import org.json.JSONException;
import org.json.JSONObject;
import java.util.Iterator;
import cn.jpush.android.api.JPushInterface;
import cn.jpush.android.helper.Logger;
/**
* 自定义接收器
*
* 如果不定义这个 Receiver,则:
* 1) 默认用户会打开主界面
* 2) 接收不到自定义消息
*
* @author Admin
*/
public class JpushReceiver extends BroadcastReceiver {
private static final String TAG = "JpushReceiver";
@Override
public void onReceive(Context context, Intent intent) {
try {
Bundle bundle = intent.getExtras();
ELog.e(TAG, "[JpushReceiver] onReceive - " + intent.getAction() + ", extras: " + printBundle(bundle));
if (JPushInterface.ACTION_REGISTRATION_ID.equals(intent.getAction())) {
String regId = bundle.getString(JPushInterface.EXTRA_REGISTRATION_ID);
ELog.e(TAG, "[JpushReceiver] 接收Registration Id : " + regId);
//send the Registration Id to your server...
} else if (JPushInterface.ACTION_MESSAGE_RECEIVED.equals(intent.getAction())) {
ELog.e(TAG, "[JpushReceiver] 接收到推送下来的自定义消息: " + bundle.getString(JPushInterface.EXTRA_MESSAGE));
// processCustomMessage(context, bundle);
} else if (JPushInterface.ACTION_NOTIFICATION_RECEIVED.equals(intent.getAction())) {
ELog.e(TAG, "[JpushReceiver] 接收到推送下来的通知");
int notifactionId = bundle.getInt(JPushInterface.EXTRA_NOTIFICATION_ID);
ELog.e(TAG, "[JpushReceiver] 接收到推送下来的通知的ID: " + notifactionId);
} else if (JPushInterface.ACTION_NOTIFICATION_OPENED.equals(intent.getAction())) {
ELog.e(TAG, "[JpushReceiver] 用户点击打开了通知");
//打开自定义的Activity
// Intent i = new Intent(context, TestActivity.class);
// i.putExtras(bundle);
//i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
// i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP );
// context.startActivity(i);
} else if (JPushInterface.ACTION_RICHPUSH_CALLBACK.equals(intent.getAction())) {
ELog.e(TAG, "[JpushReceiver] 用户收到到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);
Logger.w(TAG, "[JpushReceiver]" + intent.getAction() + " connected state change to " + connected);
} else {
ELog.e(TAG, "[JpushReceiver] Unhandled intent - " + intent.getAction());
}
} catch (Exception e) {
}
}
/**
* 打印所有的 intent extra 数据
*
* @param bundle
* @return
*/
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))) {
Logger.i(TAG, "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();
sb.append("\nkey:" + key + ", value: [" +
myKey + " - " + json.optString(myKey) + "]");
}
} catch (JSONException e) {
Logger.e(TAG, "Get message extra JSON error!");
}
} else {
sb.append("\nkey:" + key + ", value:" + bundle.get(key));
}
}
return sb.toString();
}
}
package com.hdl.smartface.receiver;
import android.content.Context;
import android.util.Log;
import cn.jpush.android.api.JPushMessage;
import cn.jpush.android.service.JPushMessageReceiver;
/**
* 自定义JPush message 接收器,包括操作tag/alias的结果返回(仅仅包含tag/alias新接口部分)
*
* @author Admin
*/
public class MyJpushMessageReceiver extends JPushMessageReceiver {
private static final String TAG = "MyJpushMessageReceiver";
@Override
public void onTagOperatorResult(Context context, JPushMessage jPushMessage) {
super.onTagOperatorResult(context, jPushMessage);
Log.e(TAG, "onTagOperatorResult: code = " + jPushMessage.getErrorCode());
}
@Override
public void onCheckTagOperatorResult(Context context, JPushMessage jPushMessage) {
super.onCheckTagOperatorResult(context, jPushMessage);
Log.e(TAG, "onCheckTagOperatorResult: code = " + jPushMessage.getErrorCode());
}
@Override
public void onAliasOperatorResult(Context context, JPushMessage jPushMessage) {
super.onAliasOperatorResult(context, jPushMessage);
Log.e(TAG, "onAliasOperatorResult: code = " + jPushMessage.getErrorCode());
}
@Override
public void onMobileNumberOperatorResult(Context context, JPushMessage jPushMessage) {
super.onMobileNumberOperatorResult(context, jPushMessage);
Log.e(TAG, "onMobileNumberOperatorResult: code = " + jPushMessage.getErrorCode());
}
}
新建应用启动类MyApp(继承至Application的类–如已有此类请忽略新建过程)并在onCreate中初始化极光推送
package com.hdl.smartface.base;
import android.app.Application;
import cn.jpush.android.api.JPushInterface;
/**
* 应用启动类
* Created by Admin on 2020/1/11.
*
* @author Admin
*/
public class MyApp extends Application {
@Override
public void onCreate() {
super.onCreate();
initJpush();
}
/**
* 初始化极光推送
*/
private void initJpush() {
//调试模式开关,正式版需设置未false
JPushInterface.setDebugMode(true);
//初始化极光推送
JPushInterface.init(this);
}
}
同时AndroidMenifest.xml中的application标签加入
...
<application
...
android:name=".base.MyApp"
...>
....
本项目中智能人脸识别终端都是使用的自定义消息(不用弹出通知),通知主要是针对用户App端(会在手机通知栏弹出通知信息)
打开极光推送后台,“发送通知”页面,输入标题和内容,点击发送预览即可
此时App端能收到以下信息说明集成成功:
打开极光推送后台,“自定义消息”页面,输入通知内容,点击发送预览即可
此时App端能收到以下信息说明接收自定义消息成功:
消息接收正常,只需要根据消息内容来处理相应的逻辑即可,所以只需要前后端商量好消息的协议即可,倒数第二章会给出一些消息协议的定制建议以便于前端解析协议。
你可能已经发现,发送消息是通过极光推送的后台管理页面发送的,实际业务中总不能跑他们官网一条消息一条消息的发吧!
是的,极光推送是提供得有服务端推送SDK的,我们只需要集成他们的SDK来发送消息,app端负责接收消息即可。
本项目的服务端是基于java开发的,所以使用极光推送服务端SDK for Java版来做示例。
创建一个maven项目,加入极光推送相关推送
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>2.2.2.RELEASEversion>
<relativePath/>
parent>
<groupId>com.hdlgroupId>
<artifactId>smartfaceartifactId>
<version>0.0.1-SNAPSHOTversion>
<name>smartfacename>
<description>Demo project for Spring Bootdescription>
<properties>
<java.version>1.8java.version>
properties>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>cn.jpush.apigroupId>
<artifactId>jpush-clientartifactId>
<version>3.4.3version>
dependency>
<dependency>
<groupId>cn.jpush.apigroupId>
<artifactId>jiguang-commonartifactId>
<version>1.1.4version>
dependency>
<dependency>
<groupId>io.nettygroupId>
<artifactId>netty-allartifactId>
<version>4.1.6.Finalversion>
<scope>compilescope>
dependency>
<dependency>
<groupId>com.google.code.gsongroupId>
<artifactId>gsonartifactId>
<version>2.3version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-apiartifactId>
<version>1.7.7version>
dependency>
<dependency>
<groupId>org.slf4jgroupId>
<artifactId>slf4j-log4j12artifactId>
<version>1.7.7version>
dependency>
<dependency>
<groupId>log4jgroupId>
<artifactId>log4jartifactId>
<version>1.2.17version>
dependency>
dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-maven-pluginartifactId>
plugin>
plugins>
build>
project>
同步完成,记下来开始测试消息发送
package com.hdl.smartface;
import cn.jiguang.common.ClientConfig;
import cn.jiguang.common.resp.APIConnectionException;
import cn.jiguang.common.resp.APIRequestException;
import cn.jpush.api.JPushClient;
import cn.jpush.api.push.PushResult;
import cn.jpush.api.push.model.PushPayload;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static cn.jpush.api.push.model.notification.PlatformNotification.ALERT;
public class JPushTest {
protected static final Logger LOG = LoggerFactory.getLogger(SmartfaceApplicationTests.class);
/**
* 极光推送应用管理中获取
*/
private static final String MASTER_SECRET = "6031870790216966f74410d1";
/**
* 极光推送应用管理中获取
*/
private static final String APP_KEY = "307d4e883d70b727687597fb";
public static PushPayload buildPushObject_all_all_alert() {
return PushPayload.alertAll(ALERT);
}
@Test
public void testPushMsg() {
JPushClient jpushClient = new JPushClient(MASTER_SECRET, APP_KEY, null, ClientConfig.getInstance());
// For push, all you need do is to build PushPayload object.
PushPayload payload = buildPushObject_all_all_alert();
try {
PushResult result = jpushClient.sendPush(payload);
LOG.info("Got result - " + result);
} catch (APIConnectionException e) {
// Connection error, should retry later
LOG.error("Connection error, should retry later", e);
} catch (APIRequestException e) {
// Should review the error, and fix the request
LOG.error("Should review the error, and fix the request", e);
LOG.info("HTTP Status: " + e.getStatus());
LOG.info("Error Code: " + e.getErrorCode());
LOG.info("Error Message: " + e.getErrorMessage());
}
}
}
此时服务端控制台输入以下消息说明推送成功
Android控制台输入以下信息说明接收消息成功:
发送自定义消息,只需调用方法PushPayload.Builder.setMessage(Message.content(“要发送的自定义消息”))即可
/**
* 测试推送自定义消息
*/
@Test
public void testPushCustomMsg() {
JPushClient jpushClient = new JPushClient(MASTER_SECRET, APP_KEY, null, ClientConfig.getInstance());
PushPayload pushPayload = PushPayload.newBuilder()
.setPlatform(Platform.all())
.setAudience(Audience.all())
.setMessage(Message.content("This is a custom msg"))
.build();
try {
PushResult result = jpushClient.sendPush(pushPayload);
LOG.info("Got result - " + result);
} catch (APIConnectionException e) {
// Connection error, should retry later
LOG.error("Connection error, should retry later", e);
} catch (APIRequestException e) {
// Should review the error, and fix the request
LOG.error("Should review the error, and fix the request", e);
LOG.info("HTTP Status: " + e.getStatus());
LOG.info("Error Code: " + e.getErrorCode());
LOG.info("Error Message: " + e.getErrorMessage());
}
}
先来看看人员信息实体类:
package com.hdl.smartface.model;
/**
* 人脸用户信息
*/
public class FaceUser {
/**
* 姓名
*/
private String name;
/**
* 手机号
*/
private String phone;
/**
* 人脸特征值
*/
private String featrue;
/**
* 人脸图片
*/
private String pic;
...省略getter setter toString
}
公共自定义消息实体类(定义说明请看下一节介绍)
package com.hdl.smartface.base;
/**
* 公共自定义消息返回体
*/
public class BaseCustomMessage<T> {
/**
* 消息类型
*/
private String type;
/**
* 消息简介
*/
private String msg;
/**
* 可变参数【扩展参数】
*/
private T data;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
@Override
public String toString() {
return "BaseCustomMessage{" +
"type='" + type + '\'' +
", msg='" + msg + '\'' +
", data=" + data +
'}';
}
}
服务端发送测试消息:
/**
* 测试发送新增人脸信息消息
*/
@Test
public void testSendAddFaceUserMessage() {
JPushClient jpushClient = new JPushClient(MASTER_SECRET, APP_KEY, null, ClientConfig.getInstance());
//模拟构建需要下发的人员信息
FaceUser faceUser = new FaceUser();
faceUser.setName("大力哥");
faceUser.setPhone("155959595959");
faceUser.setFeatrue("http://face.hdl.com/featrue/155959595959.data");
faceUser.setPic("http://face.hdl.com/pic/155959595959.png");
//模拟构建自定义消息体
BaseCustomMessage<FaceUser> faceUserMessage = new BaseCustomMessage<>();
faceUserMessage.setType("1001001");//方便测试,这里写死,type和msg建议按下一章的建议来制定
faceUserMessage.setMsg("新人脸更新");
faceUserMessage.setData(faceUser);
PushPayload pushPayload = PushPayload.newBuilder()
.setPlatform(Platform.all())
.setAudience(Audience.all())
.setMessage(Message.content(new Gson().toJson(faceUserMessage)))//使用gson将对象转成json
.build();
try {
PushResult result = jpushClient.sendPush(pushPayload);
LOG.info("Got result - " + result);
} catch (APIConnectionException e) {
// Connection error, should retry later
LOG.error("Connection error, should retry later", e);
} catch (APIRequestException e) {
// Should review the error, and fix the request
LOG.error("Should review the error, and fix the request", e);
LOG.info("HTTP Status: " + e.getStatus());
LOG.info("Error Code: " + e.getErrorCode());
LOG.info("Error Message: " + e.getErrorMessage());
}
}
此处只作为参考,最终还是得根据你们的实际情况来制定。
协议使用json来制定,分为公共参数和可变参数部分(或者叫扩展参数)
{
"type":"消息类型编号",
"msg":"消息说明",
"data":根据消息的类型传递不同的json对象
}
此处的type定义,建议后端使用字典来管理或者使用枚举也行,类型的编号建议有一定的编码信息,便于业务的统计与分析,如下图:
当服务端新增人脸消息时,需要及时下发消息给相应的人脸识别终端下载最新的人脸信息,此时消息协议可以这么定
{
"type":"1001001",
"msg":"新人脸更新",
"data":{
"name":"大力哥",
"phone":"155959595959",
"featrue":"http://face.hdl.com/featrue/155959595959.data",
"pic":"http://face.hdl.com/pic/155959595959.png"
}
}
data是可变参数字段(或者叫扩展参数字段),根据不同的type放回不同的实体即可,再来一个下发人脸识别终端广告的协议:
{
"type":"20010658",
"msg":"广告更新通知",
"data":[
{
"url":"http://face.hdl.com/adv/qhkhk3g523424aqat.png",
"sourceType":"img",
"duration":5,
"title":"恭祝大家新春快乐"
},
{
"url":"http://face.hdl.com/adv/ioweybzgkshywet12b34.mp4",
"sourceType":"vedio",
"duration":-1,
"title":"给大家拜年啦"
},
{
"url":"http://face.hdl.com/adv/shdgwsgsd124sgs.png",
"sourceType":"img",
"duration":5,
"title":"年货节火热进行中"
}
]
}
此时的data可以是一个集合,表示下发三条广告信息。
阅读完本文相信您已经基本了解到如何将极光推送应用到实际项目中,并且能够快速集成Android端SDK和Java服务端SDK,限于篇幅很多功能未能一一介绍,如看完本文还有疑问可评论留言或者移步官方网站;
下面给出本文的demo地址:
尊重原创,转载请注明出处,原文地址: http://blog.csdn.net/qq137722697