注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

Demo体验

Demo体验

场景Demo,开箱即用
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

【环信征文】Android程序员的十大转型之路

IT行业是一个瞬息万变的行业,程序员是一个不进则退的职业。我作为一个Android程序员,多年来一直保持随时可以转型其他技术领域的状态,保持对新技术敏感的嗅觉。   我先说说Android程序员不可能转型的几个方向,以下四个不靠谱方向的靠谱性递减: 首先不会转...
继续阅读 »
IT行业是一个瞬息万变的行业,程序员是一个不进则退的职业。我作为一个Android程序员,多年来一直保持随时可以转型其他技术领域的状态,保持对新技术敏感的嗅觉。
 
我先说说Android程序员不可能转型的几个方向,以下四个不靠谱方向的靠谱性递减:
首先不会转型iOS,iOS和Android工程师的工作内容都是大同小异的。
其次不会转型Windows Phone,好多Andr oid程序员就是受不了产品经理唠叨:“像QQ客户端那样做成和iOS一样”才转型的,怎么会转型比Android还难做成和iOS一样的WP?
再次不会转型Windows和MacOS等桌面软件,桌面开发周期长、难度大、升级不易,这是一个已经接近穷途末路的夕阳产业。
最后不会开JavaME或者Symbian的历史倒车,除非他有本事让每个用户都买(就一个“买”字,同时包含“想买”和“买得到”的意思)停产多年的机型。
 
我观察如今的技术形势,并亲身探索了一个Android程序员转型的几个技术方向的可行性:
 
Android病毒和恶意应用
最近肆虐全世界的WannaCry让安全成了IT圈最热的话题,开发腻了善意应用的Android工程师最便捷的转型方向就是开发Android病毒和恶意应用。在4.x时代对Android对敏感权限还不是很敏感的时候,我就研究过给肉鸡伪造短信记录和让肉鸡给通讯录里所有(或特定)联系人发送短信的病毒。去年还研究过窃取友商App推送内容、强杀友商App进程、卸载友商App甚至让友商App被卸载后就再也不能在这台肉鸡上安装的恶意应用(或应用里的恶意功能)。

 
转型建议:此外锁定肉鸡里的重要文件勒索用户(Android上的WannaCry?)和窃取肉鸡用户的支付密码的实现在技术上也像强奸8岁女童一样简单,只不过事后逍遥法外很难。这个转型方向只适合拿自己的手机当肉鸡玩玩,千万不要用这些技术赚钱

SDK
开发SDK本质上仍然在为Android应用开发软件,只是不直接开发Android应用。
 
每个Android程序员工作几年后都积累了属于自己的或大或小的类库,比如封装好的LogUtils和ToastUtils等;也都或多或少研究过常用开源框架的底层原理,比如了解Picasso和EventBus等;还应该对不开源的第三方服务有自己简单的二次封装,比如我就封装了一键实现支付宝和微信支付的moudle(免费的Ping++?)。
 
转型建议:尽管看见自己的链接出现在无数Android应用的Gradle文件的compile后面,开发了无数软件的一部分的成就感不会比开发完整的软件差。但是几乎没有老板会为了支持你开发开源软件发你工资。
 
JavaEE
Android程序员转型Java在基础知识方面是没什么难度的,毕竟语言相通,特性相似。同时每个Android程序员在大学时J2EE课程学得都不会很差,不过有些知识是该忘掉的,比如Hibernate已经落后于时代了,SpringMVC的全面使用才是Java后台的大势所趋。
 
转型建议:建议不想每天改UI的刚入行不久的Android工程师转型,我有好几个学弟就是参加工作后从Android转型Java的,他们过得都不错。很多工作年限较长的Android工程师本来就是JavaEE转型来的,就别转回去了。
 
手游
首先考虑不放弃Java语言和Android开发习惯的情况:最合适的就是能把游戏view直接插入普通layout里的AndEngine,前几年大红大紫的Flappy Bird就是用它开发的。AndEngine的开发方式和Android别无二致,且有丰富的开源demo。不过AndEngine没有官方文档,理论学习上有一定难度。我用AndEngine开发了我的毕业设计,参加工作后也用AndEngine获得了几个奖,我珍藏着一本AndEngine的非官方文档《Android游戏开发实践指南》(全新未拆封),期待着有一天能回到2014年把它送给那个买不起它的毕业生。

 
提到了AndEngine就不得不提国产AndEngine——OGEngine,它是基于AndEngine衍生的游戏引擎,有详细的纯中文文档和说汉语的技术支持杨城(笔名:小城),极适合开发Android TV游戏。OGEngine目前已停止更新,这个国产游戏引擎的悲剧在于推出时间太早,希望Android TV普及的时候卷土重来的OGEngine能让中国在游戏引擎方面领跑全世界。
 
LibGDX是一个跨平台的游戏开发框架,同样使用Java作为开发语言,前文所说的AndEngine就是基于LiBGDX实现的。LibGDX最大的优点就是极强的兼容性,不仅兼容Android和iOS,还兼容Windows、Linux、Max OS X等桌面系统。极强的兼容性还为开发提供了便利——不必打开Android模拟器,直接用电脑debug你的应用。在LibGDX和Android之间相互转型都很容易,知名的Android专家宋志辉、吴佳俊等都是从LibGDX转型Android的。
 
如果不要Java语言,那就有Cosos2d-x可供选择。《Cocos2d-x游戏开发实战精解》的作者欧桐桐(笔名:OTT)认为Android程序员一般对面向对象的知识掌握的比较全面,上手Cosos2d-x比较容易,并且Cosos2d-x是中国人维护的,文档全、资源多、教程多。OTT在得知我是和他一样的藏书人士后还特地送我一本他的大作鼓励我。

转型建议:做好心理准备,国内手游行业比普通的移动互联网行业加班更疯狂,建议刚入行没多久的Android工程师为了加班费转型,不建议30岁以上的Android工程师转型。
 
HTML5
HTML5也是Android工程师改行的好方向,HTML5在移动互联网领域应用非常广泛,比如混合开发、手机站、小游戏、微信公众号、微信小程序等。简单的手机站和对性能要求不高小游戏直接用从懒人模板(http://www.lanrenmb.com/)上找到的资源稍微修改一下即可,这里我只说说的混合开发应用和的小游戏怎么开发。
 
最著名的HTML5移动开发框架当属Facebook发布于2015年的React Native,这是一套跨平台、动态更新的 Javascript 框架,口号是“Learn once, write anywhere”。与之类似有同属舶来的PhoneGap等。
 
国产的HTML5开发框架在国内也百家争鸣,常见的有HBuilder和AppCan,二者共同特点是都为了便于新手入门制作了专用的编译器。2016年,在Qcon大会上宣布开源的Weex也异军突起,来自阿里的它因为开发的软件与原生App别无二致受到很多人的青睐。
 
开发对性能要求比较高的HTML5游戏,靠模板是不行的。2014年2月创立于北京的Egret是一套完整的HTML5游戏开发解决方案,其核心产品白鹭引擎(Egret Engine)凭借上手简便、性能强大已占据国内超七成的手机页游引擎市场份额。
 
Egret布道师徐聪(笔名:臭臭打不死人)还送我了Egret官方教程《Egret——HTML5游戏开发指南》和Egret吉祥物。
 
转型建议:一般来说,除非手机页游或商场,大多数用HTML5开发的Android应用就是胡闹。这条路线几乎是专为电商和小游戏行业准备的,如果公司有这方面的需求,Android程序员可以凭借平时自学的这方面技术完成任务。
 
VR
2015年底游戏外设王者雷蛇推出了VR游戏头显,2016年各大游戏厂商和小工作室争先恐后开发VR游戏争夺市场,开启了“中国VR元年”。虽然目前VR主要用在娱乐领域,被很多人视为玩具,但是VR所具有的价值却远远超出“玩具”的范畴。
 
前文讨论游戏引擎的时候没说Unity-3d不是疏漏,而是要把Unity-3d放在这儿谈。Unity-3d 是Unity公司开发的一个3D游戏开发工具,近年来的新版本不断加强对VR硬件系统的支持。Android程序员转型VR不仅可以实现自己从小就想让游戏跳出四角方框的梦想,还有Unity-3d所用的C#语言本来就是嚷着“我不是Java语言”的Java语言的学习优势。
 
转型建议:VR现在正是一片蓝海,只要自学能力够强,转型VR就像2015年在合肥买房一样明智。当然前提是你能找到愿意出钱的老板或投资人。
 
大数据
移动互联网时代是一个科技发达,信息流通的时代,大数据就是这个高科技时代的产物。马云曾在演讲中提到:未来的时代将不是IT时代,而是DT的时代。DT就是Data Technology(数据科技)的缩写,大数据的合理利用与否成了很多行业成败的关键。
 
移动互联网经过这些年的发展,拿O2O和当噱头已经唬不住投资人了。Hadoop也就自然而然受到了青睐,很多每4个月“生产”一批“两年经验”的“程序员”的培训机构也问我:“Android和iOS现在不吃香了,你能帮我介绍几个Hadoop讲师吗?”
 
转型建议:与转型Java后台一样,Android程序员转型Hadoop也具备语言相通,特性相似的优势。目前各大培训机构已经如蝇逐臭争相批量生产Hadoop程序员,如果你是因为陷入了他们培训的Android程序员造成的红海才转型的话,建议你不要转型,提升自己的竞争力才是王道。
 
人工智能和深度学习
前一阵子AlphaGo战胜了人类世界的围棋世界冠军柯洁,轰动了全世界。柯洁认为AlphaGo是能够打败一切的围棋上帝,这个说法我不敢苟同,毕竟它没有和“天”对弈过,但存在能“胜天半子”的人类——祁同伟。即使AlphaGo不能打败一切,也没有人有理由认为人工智能和深度学习不能成为IT届的重要发展方向。
 
TensorFlow是谷歌基于DistBelief进行研发的第二代人工智能学习系统,具备极佳的灵活性和可延展性,在和人工智能相关的领域都有广泛的应用。TensorFlow是开源的,会大大降低深度学习在各个行业中的应用难度,有远大的发展前景。
 
转型建议:尽管我坚信将来会T(ensor)F(low)的boys受女性欢迎程度不亚于TFboys,但TensorFlow暂时很不成熟,这个“将来”距今多久还是未知数。
 
Android系统
Linux作为目前大多数服务器的操作系统,学习Linux的大多数人的目的是做一个运维。然而把脑洞再开大一点的话,Android程序员精通了Linux之后可以开发一套属于自己的Android系统。《Linux大棚命令百篇》的作者吴鹏冲(笔名:Roc,和我一样也是水浒迷)和《循序渐进Linux》的作者高俊峰都送了一本自己的作品鼓励我开发属于自己的Android ORM。

这张照片摄于2016年3月30日我拿着《循序渐进Linux(第二版)》回到母校的自习室里攻读想成为像高老师一样能定制自己的Android系统的Linux专家的路上(双关)
 
转型建议:如果Android程序员准备跳槽到生产手机等搭载Android系统的硬件的厂商的话学习Linux再合适不过了,否则就只能自己刷机玩了。
 
产品经理
每个人都可能变成自己最讨厌的人,我也不例外。我从《人人都是产品经理》中学到了产品经理的情怀,还从《从点子到产品》中学到了产品经理的技术。还有幸赶上了今年3月《从点子到产品》的作者刘飞收徒。关于我转型产品经理失败的情况是一个发生在我和刘飞之间的“挖隋炀帝坟墓的开发商名叫杨勇”的故事:
2016年初,我带新人,没有收刘飞(同名学弟)为徒
2017年初,刘飞带新人,不肯收我为徒
 
转型建议:产品经理也是技术岗位,只不过写的是给人看的需求文档。如果一个Android程序员写的代码只能让电脑看懂而不能让负责维护的程序员看懂,那么就不要转型产品经理。
 
Android程序员转型机会虽然多,但不要因为看招聘网站上某个职业平均工资高就转型,随波逐流的弄潮儿必然会在浪潮之巅摔得好惨。培训机构常说“Android不吃香了,移动互联网的寒冬来了”来吸引人报名学习速成的Hadoop和TensorFlow,其实遭遇寒冬的不是某个行业,而是某些没有打好基础的人。
  收起阅读 »

Android 头像和昵称的修改

1.在EaseChatFragment中的EaseChatFragmentHelper类中的onSetMessageAttributes的方法中设置自己的头像和昵称, @Override public void onSetMessageAttri...
继续阅读 »
1.在EaseChatFragment中的EaseChatFragmentHelper类中的onSetMessageAttributes的方法中设置自己的头像和昵称,
    @Override
public void onSetMessageAttributes(EMMessage message) {
String username = xxx;//自定义名称
String avatar = yyy;//自定义头像

message.setAttribute("avatar", avatar);
message.setAttribute("username", username);
}


2.在easeUI中的EaseUserUtils类里面有两个方法setUserNick和setUserAvatar,分别是设置昵称和头像。
将setUserAvatar改为:
/**
* 显示头像
* set user avatar
* @param message
*/
public static void setUserAvatar(Context context, EMMessage message, ImageView imageView){
if (message == null){
return;
}

//发送消息 显示本地头像
if (message.direct() == EMMessage.Direct.SEND){
Glide.with(context).load(localUrl).into(imageView);
return;
}else {
EaseUser easeUser = UserProfileCache.GetSpCacheUser(context, message.getFrom());
//本地已缓存了这个用户信息 直接绑定
if (easeUser != null){
String avatar = easeUser.getAvatar();
if(avatar != null){
try {
Glide.with(context).load(avatar).into(imageView);
} catch (Exception e) {
//use default avatar
Glide.with(context).load(R.drawable.ease_default_avatar).into(imageView); }
}else{
Glide.with(context).load(R.drawable.ease_default_avatar).into(imageView);
}
}
//本地没有缓存了这个用户信息 保存本地
else {
try {
String avatar = message.getStringAttribute("avatar");

if(avatar != null){
try {
Glide.with(context).load(avatar).into(imageView);
} catch (Exception e) {
//use default avatar
Glide.with(context).load(R.drawable.ease_default_avatar).into(imageView); }
}else{
Glide.with(context).load(R.drawable.ease_default_avatar).into(imageView);
}
EaseUserUtils.getUserInfo(context, message);
} catch (HyphenateException e) {
e.printStackTrace();
}
}
}
}

public static EaseUser getUserInfo(Context context, EMMessage message){

EaseUser user = null;
try {
String avatar = message.getStringAttribute("avatar");
String nick = message.getStringAttribute("username");

user = new EaseUser(message.getFrom());
user.setAvatar(avatar);
user.setNickname(nick);
EaseCommonUtils.setUserInitialLetter(user);
UserProfileCache.setSpCacheUser(context, message.getFrom(), user);
} catch (HyphenateException e) {
e.printStackTrace();
}
return user;
}

UserProfileCache为用户缓存类。
public class UserProfileCache {

private static Gson gson = new Gson();
public static String SP_CACHE_USER = "sp_cache_user";

private static SharedPreferences spf;

public static void setSpCacheUser(Context context, String user_id, EaseUser cacheUser){
if (user_id == null){
return;
}
if (spf == null){
spf = context.getSharedPreferences(SP_CACHE_USER, Context.MODE_PRIVATE);
}
SharedPreferences.Editor editor = spf.edit();
editor.putString(user_id, gson.toJson(cacheUser));
editor.commit();
}

public static EaseUser GetSpCacheUser(Context context, String userId){
if (userId == null){
return null;
}
if (spf == null){
spf = context.getSharedPreferences(SP_CACHE_USER, Context.MODE_PRIVATE);
}
String cacheUserString = spf.getString(userId,null);
if (cacheUserString == null){
return null;
}else {
return gson.fromJson(cacheUserString,EaseUser.class);
}
}

}

注:这种方式不用再创建数据库,只是对本地消息记录的显示做了下调整。其实是可以做的更好,比如把消息记录存到自己服务器,什么问题都不是问题,但是目前这里没有过多要求,所以还是不去麻烦后台爸爸,自己在客户端改了。
  收起阅读 »

环信进阶篇-实现名片|红包|话题聊天室等自定义cell

    伴随着即时通讯成为了越来越多APP的刚需,匿名社交、阅后即焚、红包等新玩法层出不穷,基本的聊天方式越来越难以满足变态的需求。环信提供的自定义扩展属性功能非常的强大,能够帮助我们在cell中的各种需求做定制处理。这是分享一个之前做过的方法及实现,大家可以...
继续阅读 »
    伴随着即时通讯成为了越来越多APP的刚需,匿名社交、阅后即焚、红包等新玩法层出不穷,基本的聊天方式越来越难以满足变态的需求。环信提供的自定义扩展属性功能非常的强大,能够帮助我们在cell中的各种需求做定制处理。这是分享一个之前做过的方法及实现,大家可以借鉴处理的过程及思路,如有不妥之处,请大家及时留言告知,谢谢。 
今天就给大家介绍下怎么对cell中的各种需求的定制处理


 类型一:在现有会话cell上修改UI效果

类似于上面给出的截图,我们有时候需要对环信官方给出的cell进行些许的调整。例如:项目中加入了不同于普通群聊或者聊天室的功能需求
点击话题聊天,大家加入聊天室,这里发出的各种就是不同于普通聊天,普通的聊天只需展示文字、地址、图片等等,但是这里的需求是得加上时间、私聊按钮,没砍需求之前是还有点赞和取消赞的按钮。
我们在普通聊天的基础上新建几个cell,文字、语音、图片、地图等等,不能和原有的普通cell混合起来,因为需求有普通聊天。

直接把普通聊天cell中的代码拷贝过来,再在此基础上进行cell的UI自定义处理,就拿文字聊天时的处理情况为例:


1、拷贝复制原有普通聊天cell内的代码

2、把需要的新增的UI控件初始化

3、适配各类控件

4、传值及赋值

5、新增按钮点击和本身cell的点击效果处理(别和cell上的点击效果混到一起)

6、耐心调整cell上UI效果

以上基本就是简单的自定义cell步骤了,有基础的小伙伴看下步骤应该就有思路了


类型二:类似于红包和名片Cell的UI效果


通常在我们项目中,并不只有文字、图片等等这些简单的聊天内容,有时候我们需要把自己的信息作为一张名片发给对方、发个红包给好朋友、发一个项目中的一个模块介绍给对方等等功能要求。
我们就拿雷哥的这张假名片为例:
/*!
@method
@brief 新增一个新的功能按钮
@discussion
@param image 按钮图片
@param highLightedImage 高亮图片
@param title 按钮标题
@result
*/
- (void)insertItemWithImage:(UIImage*)image
highlightedImage:(UIImage*)highLightedImage
title:(NSString*)title;
/*!
@method
@brief 修改功能按钮图片
@discussion
@param image 按钮图片
@param highLightedImage 高亮图片
@param title 按钮标题
@param index 按钮索引
@result
*/
- (void)updateItemWithImage:(UIImage*)image
highlightedImage:(UIImage*)highLightedImage
title:(NSString*)title
atIndex:(NSInteger)index;
/*!
@method
@brief 根据索引删除功能按钮
@discussion
@param index 按钮索引
@result
*/
- (void)removeItematIndex:(NSInteger)index;
*  消息体类型
typedef enum{
EMMessageBodyTypeText  = 1,    /*! \~chinese 文本类型 \~english Text */
EMMessageBodyTypeImage,        /*! \~chinese 图片类型 \~english Image */
EMMessageBodyTypeVideo,        /*! \~chinese 视频类型 \~english Video */
EMMessageBodyTypeLocation,      /*! \~chinese 位置类型 \~english Location */
EMMessageBodyTypeVoice,        /*! \~chinese 语音类型 \~english Voice */
EMMessageBodyTypeFile,          /*! \~chinese 文件类型 \~english File */
EMMessageBodyTypeCmd,          /*! \~chinese 命令类型 \~english Command */
}EMMessageBodyType;
如果环信把这个开放出来,或许我们就更加简单了我们只需自己修改成自己对应的类型即可。但是这个目前就想想,所以我们可以在以上类型中找一个出来,在它的基础上做些文章,变成我们想要的类型。

红包和名片最像什么。。。。对,不就和图片差不多嘛,不过小伙伴也不要以为只能拿图片来做文章,其他的我们都可以拿来用,这里就拿文字类型来作为例子(原理都一样)。

01.jpg


名片类型
这里我们只简要介绍怎么根据会话类型来显示名片,具体传值等怎么做,有基础的小伙伴应该都懂,不懂的小伙伴见文章底部。
 
  1. 我们需要在发送名片时,在拓展消息里面存一个名片的字段,这个字段可以被用来判断是名片、红包等等。
  2. 名片、红包等等中内容,同样也存在拓展属性中(这里不做过多介绍)
  3. 我们在展示自己的消息和接收到对方的消息时,在文字类型的基础上再进一步判断是什么类型,加载对应类型的视图,如果是红包就加载红包的view,如果是名片就展示名片view......



02.jpg


加载不同类型的cell
好了,以上就是我们所要介绍的两种不同类型cell的处理办法。


以下是补充自定义cell时遇到的各种情况及处理:
1、cell上语音、图片等原始点击和新增按钮点击冲突处理:

注释掉原有的点击方法,把原有的点击方法放到具体的控件上去,避免cell上多个控件点击的冲突

重点:记得把气泡上的点击权限打开
_backgroundImageView.userInteractionEnabled = YES;



03.jpg


解决点击冲突
2、cell上语音气泡长度的改变,避免过段影响布局

我们只需把原有语音上的语音长度Label距语音图片控件调大一点距离就能自动把语音类气泡拉长。(其他类型一样处理原理)

04.jpg


语音气泡拉长
3、因新增控件导致在原有cell上高度的变化处理
/*! @method @brief 根据消息的内容,获取当前cell的高度 @discussion @param model        消息对象model @result 返回cell高度 */
+ (CGFloat)cellHeightWithModel:(id)model
在原cell高度处理的情况下,根据各种类型的判断进行cell高度的自适应。

05.jpg


cell高度处理
4、文字类型气泡长度的处理

我暂时的处理方法:判断输入的文字长度,加入文字长度小于10,我会在后面自动补全5个空格,被动撑长气泡的长度。

假如小伙伴们有更好的建议也可以留言,谢谢! 收起阅读 »

环信Web IM 新版本发布,提供更为丰富的群组、聊天室功能,现在更新就送小风扇!

  六月的骄阳,暑气留恋,但这风风火火却远不能掩盖季节的丰富内涵。环信发布了WEB新版本,十余项更新,带来了更加丰富的群组、聊天室功能。环信还为小伙伴们准备了一批小风扇,参与使用新版本并在文章下方跟帖使用反馈,就能获得环信usb小风扇,数量有限,先到先得! ...
继续阅读 »
  六月的骄阳,暑气留恋,但这风风火火却远不能掩盖季节的丰富内涵。环信发布了WEB新版本,十余项更新,带来了更加丰富的群组、聊天室功能。环信还为小伙伴们准备了一批小风扇,参与使用新版本并在文章下方跟帖使用反馈,就能获得环信usb小风扇,数量有限,先到先得!



d1fddd2e88747ea05968d89f6b696963.jpg


环信USB小风扇


Web IM v1.4.11 2017-06-14

 新功能:
  • [sdk] debug.js融合到sdk当中,优化日志内容输出
  • [sdk] 通过Rest屏蔽群组
  • [sdk] 通过Rest发出入群申请
  • [sdk] 通过Rest获取群组列表
  • [sdk] 通过Rest根据groupid获取群组详情
  • [sdk] 通过Rest列出某用户所加入的所有群组
  • [sdk] 通过Rest列出群组的所有成员
  • [sdk] 通过Rest禁止群用户发言
  • [sdk] 通过Rest取消对用户禁言的禁止
  • [sdk] 通过Rest获取群组下所有管理员
  • [sdk] 通过Rest获取群组下所有被禁言成员
  • [sdk] 通过Rest设置群管理员
  • [sdk] 通过Rest取消群管理员
  • [sdk] 通过Rest同意用户加入群
  • [sdk] 通过Rest拒绝用户加入群
  • [sdk] 通过Rest添加用户至群组黑名单(单个)
  • [sdk] 通过Rest添加用户至群组黑名单(批量)
  • [sdk] 通过Rest将用户从群黑名单移除(单个)
  • [sdk] 通过Rest将用户从群黑名单移除(批量)
  • [demo] 聊天窗口中记录可清空
  • [demo] 聊天窗口中发送方聊天记录显示状态(未送达、已送达、已读)
  • [demo] 查看聊天室成员
  • [demo] 通过链接直接打开与好友的对话框
  • [demo] 新增申请加入公开群面板
  • [demo] 在申请加入公开群面板可下拉分页获取公开群
  • [demo] 在申请加入公开群面板可点击群名称可查看群详情
  • [demo] 在申请加入公开群面板可搜索群查看群详情
  • [demo] 在申请加入公开群面板群详情页面可申请加入群组
  • [demo] 群主可同意、拒绝加群申请
  • [demo] 在群主的群成员列表中新增添加/移除管理员、禁言/解禁群成员按钮
Bug修复:
  • [sdk] 添加好友会产生多余的订阅消息
  • [sdk] 频繁的发送消息会导致消息id重复的问题
  • [sdk] 适配SDK发送文件和图片的大小
  • [demo] 优化sdk/demo.html,修复某些依赖文件找不到的问题
  • [demo] 修复离线消息数量统计不准确问题

 
webim在线体验:https://webim.easemob.com

版本历史:更新日志
 
SDK下载:下载地址 收起阅读 »

Android 守护进程的实现方式

     “我的APP像微信那样能一直在手机运行吗?”关于 Android 平台的进程保活,一 直是所有Android 开发者瞩目的内容之一,也是环信小伙们比较关心的问题,本篇文章给大家分享关于微信进程保活的原理及Android守护进程的实现教程。   为什么...
继续阅读 »
 
   “我的APP像微信那样能一直在手机运行吗?”关于 Android 平台的进程保活,一 直是所有Android 开发者瞩目的内容之一,也是环信小伙们比较关心的问题,本篇文章给大家分享关于微信进程保活的原理及Android守护进程的实现教程。
 
为什么微信可以一直在手机后台跑着能收到消息?

    国内手机厂商对 android rom 进行了定制,对后台服务以及运行在后台的程序进行了严格的限制,微信等这些大厂商的 app 都已经通过和设备厂商合作在安装时都已经加入了系统的白名单,因此设备并不会限制对方 app 在后台运行;

我自己的APP该如何实现进程保活?
 
  1. 引导用户把当前 app 加入到设备的白名单中,解除设备对 app 的限制;
  2. 小米和华为设备可以集成对应的推送实现在app 被干掉后依然收推送通知;
  3. 可以自己在 app 端实现守护进程的方式,让 app 在系统级别自动回收的情况下减少被杀死的概率,这种方式对用户主动回收无效。


第一条和第二条就不多说了,环信imgeek社区里已经有了相应的文章(http://www.imgeek.org/article/825308754 )接下来介绍守护进程的实现。
 
友情提示:
   本篇教程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的,所以,想让自己程序在Android里永生不死的,请忽略本文!请忽略!
 什么是守护进程?

    守护进程能让我们的程序在后台运行的时间长一些,或者在被干掉的时候,能够重新站起来,需要注意不是每次都有效,也不是在所有的设备的上都有效的。
 
守护进程的实现,本文两个核心观点:
 
  1. 提高进程优先级,降低被回收或杀死概率
  2. 在进程被干掉后,进行拉起

 要实现实现上边所说,通过下边几点来实现,首先我们需要了解下进程的优先级划分:

Process Importance记录在ActivityManager.java类中:
**
* Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
*
* 这个进程正在运行前台UI,也就是说,它是当前在屏幕顶部的东西,用户正在进行交互的而进程
*/
public static final int IMPORTANCE_FOREGROUND = 100;

/**
* 此进程正在运行前台服务,即使用户不是在应用中时也执行音乐播放,这一般表示该进程正在做用户积极关心的事情
*/
public static final int IMPORTANCE_FOREGROUND_SERVICE = 125;
/**
* 这个过程不是用户的直接意识到,但在某种程度上是他们可以察觉的。
*/
public static final int IMPORTANCE_PERCEPTIBLE = 130;

/**
* 此进程正在运行前台UI,但设备处于睡眠状态,因此用户不可见,意思是用户意识不到的进程,因为他们看不到或与它交互,
* 但它是相当重要,因为用户解锁设备时期望的返回到这个进程
*/
public static final int IMPORTANCE_TOP_SLEEPING = 150;

/**
* 进程在后台,但我们不能恢复它的状态,所以我们想尽量避免杀死它,不然这个而进程就丢了
*/
public static final int IMPORTANCE_CANT_SAVE_STATE = 170;

/**
* 此进程正在运行某些对用户主动可见的内容,但不是直接显示在UI,
* 这可能运行在当前前台之后的窗口(因此暂停并且其状态被保存,不与用户交互,但在某种程度上对他们可见);
* 也可能在系统的控制下运行其他服务,
*/
public static final int IMPORTANCE_VISIBLE = 200;

/**
* 服务进程,此进程包含在后台保持运行的服务,这些后台服务用户察觉不到,是无感知的,所以它们可以由系统相对自由地杀死
*/
public static final int IMPORTANCE_SERVICE = 300;

/**
* 后台进程
*/
public static final int IMPORTANCE_BACKGROUND = 400;

/**
* 空进程,此进程没有任何正在运行的代码
*/
public static final int IMPORTANCE_EMPTY = 500;

// 此过程不存在。
public static final int IMPORTANCE_GONE = 1000;
进程回收机制

了解进程优先级之后,我们还需要知道一个进程回收机制的东西;这里参考AngelDevil在博客园上的一篇文章:
 
详情参考:【Android Low Memory Killer】
 
Android的Low Memory Killer基于Linux的OOM机制,在Linux中,内存是以页面为单位分配的,当申请页面分配时如果内存不足会通过以下流程选择bad进程来杀掉从而释放内存:
alloc_pages -> out_of_memory() -> select_bad_process() -> badness()
在Low Memory Killer中通过进程的oom_adj与占用内存的大小决定要杀死的进程,oom_adj越小越不容易被杀死;
Low Memory Killer Driver在用户空间指定了一组内存临界值及与之一一对应的一组oom_adj值,当系统剩余内存位于内存临界值中的一个范围内时,如果一个进程的oom_adj值大于或等于这个临界值对应的oom_adj值就会被杀掉。

下边是表示Process State(即老版本里的OOM_ADJ)数值对照表,数值越大,重要性越低,在新版SDK中已经在android层去除了小于0的进程状态
// Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java 
// 进程不存在。
public static final int PROCESS_STATE_NONEXISTENT = -1;
// 进程是一个持久的系统进程,一般指当前 UI 进程
public static final int PROCESS_STATE_PERSISTENT = 0;
// 进程是一个持久的系统进程,正在做和 UI 相关的操作,但不直接显示
public static final int PROCESS_STATE_PERSISTENT_UI = 1;
// 进程正在托管当前的顶级活动。请注意,这涵盖了用户可见的所有活动。
public static final int PROCESS_STATE_TOP = 2;
// 进程由于系统绑定而托管前台服务。
public static final int PROCESS_STATE_BOUND_FOREGROUND_SERVICE = 3;
// 进程正在托管前台服务。
public static final int PROCESS_STATE_FOREGROUND_SERVICE = 4;
// 与{@link #PROCESS_STATE_TOP}相同,但设备处于睡眠状态。
public static final int PROCESS_STATE_TOP_SLEEPING = 5;
// 进程对用户很重要,是他们知道的东西
public static final int PROCESS_STATE_IMPORTANT_FOREGROUND = 6;
// 进程对用户很重要,但不是他们知道的
public static final int PROCESS_STATE_IMPORTANT_BACKGROUND = 7;
// 进程在后台运行备份/恢复操作
public static final int PROCESS_STATE_BACKUP = 8;
// 进程在后台,但我们不能恢复它的状态,所以我们想尽量避免杀死它,不然这个而进程就丢了
public static final int PROCESS_STATE_HEAVY_WEIGHT = 9;
// 进程在后台运行一个服务,与oom_adj不同,此级别用于正常运行在后台状态和执行操作状态。
public static final int PROCESS_STATE_SERVICE = 10;
// 进程在后台运行一个接收器,注意,从oom_adj接收器的角度来看,在较高的前台级运行,但是对于我们的优先级,这不是必需的,并且将它们置于服务之下意味着当它们接收广播时,一些进程状态中的更少的改变。
public static final int PROCESS_STATE_RECEIVER = 11;
// 进程在后台,但主持家庭活动
public static final int PROCESS_STATE_HOME = 12;
// 进程在后台,但托管最后显示的活动
public static final int PROCESS_STATE_LAST_ACTIVITY = 13;
// 进程正在缓存以供以后使用,并包含活动
public static final int PROCESS_STATE_CACHED_ACTIVITY = 14;
// 进程正在缓存供以后使用,并且是包含活动的另一个缓存进程的客户端
public static final int PROCESS_STATE_CACHED_ACTIVITY_CLIENT = 15;
// 进程正在缓存以供以后使用,并且为空
public static final int PROCESS_STATE_CACHED_EMPTY = 16;
Process State(即老版本的OOM_ADJ)与Process Importance对应关系,这个方法也是在ActivityManager.java类中,有了这个关系,就知道可以知道我们的应用处于哪个级别,对于我们后边优化有个很好地参考
/** 
* Path:SDK/sources/android-25/android/app/ActivityManager#RunningAppProcessInfo.java
*
* 通过这个方法,将Linux底层的 OOM_ADJ级别码和 android 层面的进程重要程度联系了起来
*/
public static int procStateToImportance(int procState) {
if (procState == PROCESS_STATE_NONEXISTENT) {
return IMPORTANCE_GONE;
} else if (procState >= PROCESS_STATE_HOME) {
return IMPORTANCE_BACKGROUND;
} else if (procState >= PROCESS_STATE_SERVICE) {
return IMPORTANCE_SERVICE;
} else if (procState > PROCESS_STATE_HEAVY_WEIGHT) {
return IMPORTANCE_CANT_SAVE_STATE;
} else if (procState >= PROCESS_STATE_IMPORTANT_BACKGROUND) {
return IMPORTANCE_PERCEPTIBLE;
} else if (procState >= PROCESS_STATE_IMPORTANT_FOREGROUND) {
return IMPORTANCE_VISIBLE;
} else if (procState >= PROCESS_STATE_TOP_SLEEPING) {
return IMPORTANCE_TOP_SLEEPING;
} else if (procState >= PROCESS_STATE_FOREGROUND_SERVICE) {
return IMPORTANCE_FOREGROUND_SERVICE;
} else {
return IMPORTANCE_FOREGROUND;
}
}
一般情况下,设备端进程被干掉有一下几种情况

QQ截图20170615143439.jpg


由以上分析,我们可以可以总结出,如果想提高我们应用后台运行时间,就需要提高当前应用进程优先级,来减少被杀死的概率

守护进程的实现

分析了那么多,现在对Android自身后台进程管理,以及进程的回收也有了一个大致的了解,后边我们要做的就是想尽一切办法去提高应用进程优先级,降低进程被杀的概率;或者是在被杀死后能够重新启动后台守护进程

1.模拟前台进程

第一种方式就是利用系统漏洞,使用startForeground()将当前进程伪装成前台进程,将进程优先级提高到最高(这里所说的最高是服务所能达到的最高,即1);

这种方式在7.x之前都是很好用的,QQ、微信、IReader、Keep 等好多应用都是用的这种方式实现;因为在7.x 以后的设备上,这种伪装前台进程的方式也会显示出来通知栏提醒,这个是取消不掉的,虽然Google现在还没有对这种方式加以限制,不过这个已经能够被用户感知到了,这种方式估计也用不了多久了

下边看下实现方式,这边这个VMDaemonService就是一个守护进程服务,其中在服务的onStartCommand()方法中调用startForeground()将服务进程设置为前台进程,当运行在 API18 以下的设备是可以直接设置,API18 以上需要实现一个内部的Service,这个内部类实现和外部类同样的操作,然后结束自己;当这个服务启动后就会创建一个定时器去发送广播,当我们的核心服务被干掉后,就由另外的广播接收器去接收我们守护进程发出的广播,然后唤醒我们的核心服务;
/**
* 以实现内部 Service 类的方式实现守护进程,这里是利用 android 漏洞提高当前进程优先级
*
* Created by lzan13 on 2017/3/7.
*/
public class VMDaemonService extends Service {

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

// 定时唤醒的时间间隔,这里为了自己测试方边设置了一分钟
private final static int ALARM_INTERVAL = 1 * 60 * 1000;
// 发送唤醒广播请求码
private final static int WAKE_REQUEST_CODE = 5121;
// 守护进程 Service ID
private final static int DAEMON_SERVICE_ID = -5121;

@Override public void onCreate() {
Log.i(TAG, "VMDaemonService->onCreate");
super.onCreate();
}

@Override public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "VMDaemonService->onStartCommand");
// 利用 Android 漏洞提高进程优先级,
startForeground(DAEMON_SERVICE_ID, new Notification());
// 当 SDk 版本大于18时,需要通过内部 Service 类启动同样 id 的 Service
if (Build.VERSION.SDK_INT >= 18) {
Intent innerIntent = new Intent(this, DaemonInnerService.class);
startService(innerIntent);
}

// 发送唤醒广播来促使挂掉的UI进程重新启动起来
AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
Intent alarmIntent = new Intent();
alarmIntent.setAction(VMWakeReceiver.DAEMON_WAKE_ACTION);

PendingIntent operation = PendingIntent.getBroadcast(this, WAKE_REQUEST_CODE, alarmIntent,
PendingIntent.FLAG_UPDATE_CURRENT);

alarmManager.setInexactRepeating(AlarmManager.RTC_WAKEUP, System.currentTimeMillis(),
ALARM_INTERVAL, operation);

/**
* 这里返回值是使用系统 Service 的机制自动重新启动,不过这种方式以下两种方式不适用:
* 1.Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
* 2.进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
*/
return START_STICKY;
}

@Override public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("onBind 未实现");
}

@Override public void onDestroy() {
Log.i(TAG, "VMDaemonService->onDestroy");
super.onDestroy();
}

/**
* 实现一个内部的 Service,实现让后台服务的优先级提高到前台服务,这里利用了 android 系统的漏洞,
* 不保证所有系统可用,测试在7.1.1 之前大部分系统都是可以的,不排除个别厂商优化限制
*/
public static class DaemonInnerService extends Service {

@Override public void onCreate() {
Log.i(TAG, "DaemonInnerService -> onCreate");
super.onCreate();
}

@Override public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "DaemonInnerService -> onStartCommand");
startForeground(DAEMON_SERVICE_ID, new Notification());
stopSelf();
return super.onStartCommand(intent, flags, startId);
}

@Override public IBinder onBind(Intent intent) {
// TODO: Return the communication channel to the service.
throw new UnsupportedOperationException("onBind 未实现");
}

@Override public void onDestroy() {
Log.i(TAG, "DaemonInnerService -> onDestroy");
super.onDestroy();
}
}
}
当我们启动这个守护进程的时候,就可以使用以下adb命令查看当前程序的进程情况(需要adb shell进去设备),
为了等下区分进程优先级,我启动了一个普通的后台进程,两外两个一个是我们启动的守护进程,一个是当前程序的核心进程,可以看到除了后台进程外,另外两个进程都带有isForeground=true的属性:
# 这个命令的 services 可以换成 service,这样会只显示当前,进程,不显示详细内容
# dumpsys activity services <Your Package Name>
root@vbox86p:/ # dumpsys activity services com.vmloft.develop.daemon
ACTIVITY MANAGER SERVICES (dumpsys activity services)
User 0 active services:
* ServiceRecord{170fe1dd u0 com.vmloft.develop.daemon/.services.VMDaemonService}
intent={cmp=com.vmloft.develop.daemon/.services.VMDaemonService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon:daemon
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{173fe77f 2370:com.vmloft.develop.daemon:daemon/u0a68}
isForeground=true foregroundId=-5121 foregroundNoti=Notification(pri=0 contentView=com.vmloft.develop.daemon/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0xff607d8b vis=PRIVATE)
createTime=-6s196ms startingBgTimeout=--
lastActivity=-6s157ms restartTime=-6s157ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

* ServiceRecord{2fee4f84 u0 com.vmloft.develop.daemon/.services.VMCoreService}
intent={cmp=com.vmloft.develop.daemon/.services.VMCoreService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{18c6a1b4 2343:com.vmloft.develop.daemon/u0a68}
isForeground=true foregroundId=-5120 foregroundNoti=Notification(pri=0 contentView=com.vmloft.develop.daemon/0x1090077 vibrate=null sound=null defaults=0x0 flags=0x62 color=0xff607d8b vis=PRIVATE)
createTime=-28s136ms startingBgTimeout=--
lastActivity=-28s136ms restartTime=-28s136ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1

* ServiceRecord{2ef6909e u0 com.vmloft.develop.daemon/.services.VMBackgroundService}
intent={cmp=com.vmloft.develop.daemon/.services.VMBackgroundService}
packageName=com.vmloft.develop.daemon
processName=com.vmloft.develop.daemon:background
baseDir=/data/app/com.vmloft.develop.daemon-1/base.apk
dataDir=/data/data/com.vmloft.develop.daemon
app=ProcessRecord{29f8734c 2388:com.vmloft.develop.daemon:background/u0a68}
createTime=-3s279ms startingBgTimeout=--
lastActivity=-3s262ms restartTime=-3s262ms createdFromFg=true
startRequested=true delayedStop=false stopIfKilled=false callStart=true lastStartId=1
然后我们可以用下边的命令查看ProcessID
# 这个命令可以查看当前DProcessID(数据结果第二列),我们可以看到当前程序有两个进程
# ps | grep com.vmloft.develop.daemon
root@vbox86p:/ # ps | grep com.vmloft.develop.daemon
u0_a68 2343 274 1012408 42188 ffffffff f74f1b45 S com.vmloft.develop.daemon
u0_a68 2370 274 997012 26152 ffffffff f74f1b45 S com.vmloft.develop.daemon:daemon
u0_a68 2388 274 997012 25668 ffffffff f74f1b45 S com.vmloft.develop.daemon:background
有了ProcessID之后,我们可以根据这个ProcessID获取到当前进程的优先级状态Process State,对应Linux层的oom_adj
可以看到当前核心进程的级别为0,因为这个表示当前程序运行在前台 UI 界面,守护进程级别为1,因为我们利用漏洞设置成了前台进程,虽然不可见,但是他的级别也是比较高的,仅次于前台 UI 进程,然后普通后台进程级别为4;当我们退到后台时,可以看到核心进程的级别变为1了,这就是因为我们利用startForeground()将进程设置成前台进程的原因,这样就降低了进程被系统回收的概率了;
# 这个命令就是通过 ProcessID 输出其对应 oom_adj
# cat /proc/ProcessID/oom_adj
# 程序在前台时,查询进程级别
root@vbox86p:/ # cat /proc/2343/oom_adj
0
root@vbox86p:/ # cat /proc/2370/oom_adj
1
root@vbox86p:/ # cat /proc/2388/oom_adj
4
# 当程序退到后台时,再次查看进程级别
root@vbox86p:/ # cat /proc/2343/oom_adj
1
root@vbox86p:/ # cat /proc/2370/oom_adj
1
root@vbox86p:/ # cat /proc/2388/oom_adj
4
可以看到这种方式确实能够提高进程优先级,但是在一些国产的设备上还是会被杀死的,比我我测试的时候小米点击清空最近运行的应用进程就别干掉了;当把应用加入到设备白名单里就不会被杀死了,微信就是这样,人家直接装上之后就已经在白名单里了,我们要做的就是在用户使用中引导他们将我们的程序设置进白名单,将守护进程和白名单结合起来,这样才能保证我们的应用持续或者

2.JobScheduler机制唤醒

Android系统在5.x以上版本提供了一个JobSchedule接口,系统会根据自己实现定时去调用改接口传递的进程去实现一些操作,而且这个接口在被强制停止后依然能够正常的启动;不过在一些国产设备上可能无效,比如小米;
下边是 JobServcie 的实现:
/**
* 5.x 以上使用 JobService 实现守护进程,这个守护进程要做的工作很简单,就是启动应用的核心进程
* Created by lzan13 on 2017/3/8.
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP) public class VMDaemonJobService extends JobService {

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

@Override public boolean onStartJob(JobParameters params) {
Log.d(TAG, "onStartJob");
// 这里为了掩饰直接启动核心进程,没有做其他判断操作
startService(new Intent(getApplicationContext(), VMCoreService.class));
return false;
}

@Override public boolean onStopJob(JobParameters params) {
Log.d(TAG, "onStopJob");
return false;
}
}
我们要做的就是在需要的时候调用JobSchedule的schedule来启动任务;剩下的就不需要关心了,JobSchedule会帮我们做好,下边就是我这边实现的启动任务的方法:
/**
* 5.x以上系统启用 JobScheduler API 进行实现守护进程的唤醒操作
*/
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void startJobScheduler() {
int jobId = 1;
JobInfo.Builder jobInfo = new JobInfo.Builder(jobId, new ComponentName(this, VMDaemonJobService.class));
jobInfo.setPeriodic(10000);
jobInfo.setPersisted(true);
JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
jobScheduler.schedule(jobInfo.build());
}
3.系统 Service START_STICKY 机制重启

在实现Service类时,将onStartCommand()返回值设置为START_STICKY,利用系统机制在Service挂掉后自动拉活;不过这种方式只适合比较原生一些的系统,像小米,华为等这些定制化比较高的第三方厂商,他们都已经把这些给限制掉了;
@Override 
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i(TAG, "VMDaemonService->onStartCommand");
/**
* 这里返回值是使用系统 Service 的机制自动重新启动,不过这种方式以下两种方式不适用:
* 1.Service 第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内 Service 被杀死达到5次,则系统不再拉起。
* 2.进程被取得 Root 权限的管理工具或系统工具通过 forestop 停止掉,无法重启。
* 3.一些定制化比较高的第三方系统也不适用
*/
return START_STICKY;
}
这种方式在以下两种情况无效:
  • Service第一次被异常杀死后会在5秒内重启,第二次被杀死会在10秒内重启,第三次会在20秒内重启,一旦在短时间内Service被杀死达到5次,这个服务就不能再次重启了;
  • 进程被取得Root权限的管理工具或系统工具通过fores-top方式停止掉,无法重启;
  • 一些定制化比较高的第三方系统也不适用
4.其他保活方式

  • 利用 Native 本地进程,这个主要使用到 jni 调用底层实现,而且在 Android 5.x 以后对这个限制也比较高,不适用了,暂时不研究
  • 集成第三方SDK互相唤醒,这个只要正常集成了第三方的SDK,并使用了他们对应的服务,当一个设备安装的多个应用都集成了某一个第三方SDK时,启动任意一个 app 都会唤醒其他的 app,不过这个在一些新版的国内厂商系统也是做了限制,这种方式并没有什么效果
  • 一像素的 Activity 方式(流氓方式),经测试一些手机系统无法检测到解锁和锁屏,不确定是否系统修改了解锁或者锁屏的广播,还是禁用了这些广播,因此此方式无效;


结语

事事没有绝对,万物总有一些漏洞,就算上边的那些方式不可用了,后边肯定还会出现其他的方式;我们不能保证我们的应用不死,但我们可以提高存活率;

其实最好的方式还是把程序做好,让程序本身深入人心,别人喜欢你了,就算你被干掉了,他们也会主动的把你拉起来,然后把你加入他们的白名单,然后我们的目的就实现了不是 收起阅读 »

获取好友列表,报错SERVER_UNKNOWN_ERROR = 303

List<String> usernames = EMClient.getInstance().contactManager().getAllContactsFromServer(); 获取好友列表在第一次登录成功后获取正常,但是当我在其他界面调...
继续阅读 »
List<String> usernames = EMClient.getInstance().contactManager().getAllContactsFromServer();

获取好友列表在第一次登录成功后获取正常,但是当我在其他界面调用这个方法的时候去报错如下:
 
com.hyphenate.exceptions.HyphenateException: Unknown server error
 
解决方法:要放到子线程中。
 
注:我想知道why,why,why!!!!!!!! 收起阅读 »

环信移动客服v5.20已发布,支持自助开通工单功能以及客服直接处理工单

客服模式 支持自助开通工单功能以及客服直接处理工单 新增工单页面,支持自助开通工单功能,以及客服直接处理工单,包括新建工单、回复工单、分配工单、修改帮助主题、修改工单状态,筛选工单,等等。 工单功能为增值服务,如需开通,请在工单页面提交申请,环信商务经理...
继续阅读 »
客服模式

支持自助开通工单功能以及客服直接处理工单

新增工单页面,支持自助开通工单功能,以及客服直接处理工单,包括新建工单、回复工单、分配工单、修改帮助主题、修改工单状态,筛选工单,等等。

工单功能为增值服务,如需开通,请在工单页面提交申请,环信商务经理会主动联系您。

申请开通工单功能

工单功能可以帮助您实现高效的跨部门协作,只需提交申请,并绑定邮箱,即可启动您的工单功能服务。

步骤如下:
  1. 在工单页面,点击“申请工单功能”;
  2. 填写姓名、电话、企业名称、邮箱,点击“下一步”;
  3. 填写帮助主题(帮助主题为工单的类别),点击“下一步”;
  4. 填写系统邮箱(用于接收和发送工单相关的邮件),点击“提交”。


提交申请后,请耐心等待,环信商务经理会尽快与您联系。 


01.png


新建工单

客服与客户聊天过程中,可以为客户创建工单。

步骤如下:
  1. 在会话页面,点击输入框上方的工单按钮;
  2. 填写工单标题,选择优先级、帮助主题、分配技能组、分配坐席,填写工单内容,勾选“附带访客信息”,并保存。


勾选“附带访客信息”时,工单包含对应的客户信息,可以在工单详情的“客户资料”页签查看。 


02.png



处理工单

客服可以对工单进行回复,分配工单给技能组或坐席,修改帮助主题、优先级、工单状态,查看工单进度,查看客户资料。

在工单页面,点击任意工单,即可查看工单详情。并执行下述工单处理操作:

回复工单:回复工单时,如果勾选“发布为公开回复”,工单系统将回复内容通知客户;不勾选时,回复仅客服可以查看。
  • 工单处理:分配技能组、分配坐席、修改帮助主题、优先级、状态。
  • 工单进度:查看工单进度。
  • 客户资料:查看客户资料。

03.png

筛选工单在工单页面已为您创建一些默认工单筛选器,帮助您对工单进行分类管理。您还可以创建自定义的筛选器,以满足更具体的需求。创建自定义筛选器步骤如下:在工单页面,点击“自定义筛选”;填写筛选器名称,选择筛选条件,如工单创建时间段、工单创建人、客户名称、工单编号、工单标题、分配技能组、分配坐席、帮助主题、工单状态,并点击“确定”。创建成功后,可以根据自定义筛选器对工单进行筛选。您还可以编辑或删除自定义筛选器。 

04.png

工单通知当工单创建成功、分配技能组或客服、得到回复、状态变更时,创建工单的客服将会收到系统消息。 

05.png

会话面板聊天窗口优化会话面板聊天窗口优化,支持拖动扩展输入框,以显示更多正在输入的内容。 

06.png

留言支持根据更新时间进行筛选留言页面显示留言的更新时间,并支持根据更新时间进行筛选和排序。更新时间指,留言分配的客服变更或留言状态变更的时间。
  • 筛选:点击“自定义留言筛选”,选择“更新时间”范围,点击“筛选查询”,对留言进行筛选。
  • 排序:点击留言列表中“更新时间”右侧的箭头,使留言按照更新时间升序或降序排列。

07.png

客户中心显示客户的真实姓名客户中心新增“名字”一列,显示客户的真实姓名。 

08.png

管理员模式新增REST API渠道环信移动客服新增REST API渠道。开通REST API渠道并配置服务器信息后,客服向客户回复的消息,将被环信转发到服务器的回调地址中。该功能可用于环信移动客服与第三方服务器之间的消息传递。REST API渠道支持创建多个REST关联,每个REST关联均可作为环信与您的服务器之间收发消息的通道。创建REST关联:[list=1]
  • 进入“管理员模式 > 渠道管理 > REST API”页面;
  • 点击“添加REST关联”按钮,填写关联名称、回调地址,并保存。


  • 系统自动为您生成Client ID、Client Secret、POST API。Client ID和Client Secret用于向环信发送消息时的身份认证;消息API为您向环信发送消息时使用的REST API接口,方法为POST。 


    09.png




    关于REST API渠道的身份认证方式、消息格式,请参考:REST API渠道集成

    注:REST API渠道为增值服务,如需开通,请提供租户ID并联系环信商务经理。

    新增APP关联演示视频

    APP关联信息页新增APP关联的演示视频,视频说明了APP关联的Client ID和IM服务号与即时通讯云的应用之间的关系,以及如何添加APP关联。

    如果已有即时通讯云的应用,可以采取“关联IM账号”的方式创建APP关联。对应关系如下:

    Client ID: APP关联的Client ID与即时通讯云的应用的Client ID一致;
    IM服务号:APP关联的IM服务号对应即时通讯云的应用的一个IM用户。

    进入“管理员模式 > 渠道管理 > 手机APP”页面,点击APP关联信息页中Client ID和IM服务号右侧的问号,可以查看相应的演示视频。

    客户标签支持导入导出


    10.png





    客户标签支持下载模版、导入、导出,方便管理员对客户标签进行批量整理。

    在导入客户标签时,会自动过滤已存在的客户标签,只导入新增的客户标签。 
    attach]7544[/attach]
    “不活跃会话超时自动结束”开关优化

    优化“不活跃会话超时自动结束”开关,该开关不再对待接入会话生效。优化后,该开关打开时,对于客服的进行中会话,如果客户和客服在设定时间内均未回复消息,系统将自动发送提示语给客户,并结束会话。

    如果需要自动结束超时的进行中会话,进入“管理员模式 > 设置 > 系统开关”页面,打开“不活跃会话超时自动结束”开关。
    如果需要自动结束超时的待接入会话,进入“管理员模式 > 设置 > 系统开关”页面,打开“待接入超时结束会话”开关。

    客服列表优化

    优化“管理员模式 > 成员管理 > 客服”页面的客服列表,删除原导出日志按钮。所有客服的登录日志,均可以在“管理员模式 > 统计查询 > 客服时长统计”页面查看并导出。

    Android客服工作台

    当前版本:V3.0

    新功能:

    管理员模式,当前会话支持筛选、转接、关闭
    留言支持批量分配

    iOS客服工作台

    当前版本:V2.1.9

    新功能:

    新增管理员模式,包含管理员首页的数据展示
    支持查看通知详情

    移动客服Android SDK

    当前版本:V1.0.7

    新功能:

    新增发送和接收短视频功能

    移动客服iOS SDK

    当前版本:V1.1.0

    新功能:

    SDK全面升级为动态库,集成更简单,功能更全面
    离线推送支持推送详情
    优化升级HelpDeskUI

    环信移动客服更新日志http://docs.easemob.com/cs/releasenote/5.20 

    环信移动客服登陆地址http://kefu.easemob.com/ 收起阅读 »

    环信网页的onready 轨迹回调在电脑浏览器上有效 但是在手机浏览器上就不会触发了

           环信网页的onready 轨迹回调在电脑浏览器上有效 但是在手机浏览器上就不会触发了 有大神指导是怎么回事吗? 附上代码: <script>     var saleprice = document.getElementById(&...
    继续阅读 »
           环信网页的onready 轨迹回调在电脑浏览器上有效 但是在手机浏览器上就不会触发了 有大神指导是怎么回事吗?

    附上代码:
    <script>
        var saleprice = document.getElementById("spSalaPrice");
        var productName = document.getElementById("productName");
        var productPic = document.getElementById("productPic").src;
        window.easemobim = window.easemobim || {};
        easemobim.config = {
            
            //是否隐藏小的悬浮按钮
            hide: true,
            //自动连接
            autoConnect: true,
            //聊天窗口加载成功回调
            onready: function () {
                easemobim.sendExt({
                    ext: {
                        "imageName": "mallImage3.png",
                        //custom代表自定义消息,无需修改
                        "type": "custom",
                        "msgtype": {
                            "track": {
                                "title": "我正在看:",
                                "price": "$: " + saleprice.textContent,
                                "desc": productName.textContent,
                                "img_url": productPic,
                                "item_url": window.location.href
                            }
                        }
                    }
                });
            },
        };
    </script> 收起阅读 »

    云安全fun享会 | 第三期 《未知安全威胁的检测与防御》

    活动时间:2017年06月24日 13:30—16:30 活动地点:北京市朝阳区酒仙桥北路9号恒通国际创新园C8栋 MeePark    WannaCry、Struts 2等安全事件告诉我们,用规则去防御安全漏洞永远比黑客慢一步,如何在与黑客抗...
    继续阅读 »
    活动时间:2017年06月24日 13:30—16:30
    活动地点:北京市朝阳区酒仙桥北路9号恒通国际创新园C8栋 MeePark



    01.jpg



       WannaCry、Struts 2等安全事件告诉我们,用规则去防御安全漏洞永远比黑客慢一步,如何在与黑客抗争中先知先觉,占据主动地位,是未来信息安全战争的关键!

       云安全fun享会第三期《未知安全威胁的检测与防御》,邀请业界安全专家,与您分享沙盒、RASP、蜜罐、安全态势感知等对抗未知威胁的利器。

    与其惧怕0day,不如来听听我们的安全沙龙!

    02.png




    03.jpg


    椒图科技助理总经理 吴康
    《利用沙盒检测加密及未知WebShell》




    04.jpg


    云锁产品总监 田强
    《RASP技术在中国的落地与实践》


    更多议题陆续添加中,也欢迎您的议题投稿:lidong@jowto.com



    05.png


    2017年06月24日 13:30
    北京朝阳区酒仙桥北路9号恒通国际创新园C8栋 MeePark
    除了精彩的议题外,我们还准备点心和礼品,等你到来
    会务联络:sunyx@jowto.com


    06.jpg




    07.jpg



    08.jpg




    09.png



    10.jpg




    11.png


    本次fun享会活动场地由 MEE-PARK 智能活动空间提供,特此感谢。

    12.png




    活动报名:报名地址 收起阅读 »

    环信推送的一些常见问题

    原文地址 :  http://blog.csdn.net/jyt199011302/article/details/72829520 参考资料 APNS证书创建和上传到环信后台 : http://www.imgeek.org/article/82530874...
    继续阅读 »
    原文地址 :  http://blog.csdn.net/jyt199011302/article/details/72829520

    参考资料
    APNS证书创建和上传到环信后台 : http://www.imgeek.org/article/825308748 
    APNS离线推送文档 : http://docs.easemob.com/im/300iosclientintegration/75apns
    离线推送的集成代码这里就不一一介绍了, 上面文档中写的 很明白了, 接下来说下比较容易误会的几点
    一. 离线推送
    如果app集成时添加
    - (void)applicationDidEnterBackground:(UIApplication *)application {
    [[EMClient sharedClient] applicationDidEnterBackground:application];
    }
    - (void)applicationWillEnterForeground:(UIApplication *)application {
    [[EMClient sharedClient] applicationWillEnterForeground:application];
    }

    App后台静默后,能够保持长连接3分钟左右。超过3分钟,长连接会断开,当前登录的账号,在服务端被认为离线。消息会存入离线消息空间,之后接收的消息会在再次登录后,连接上服务器,然后通过长连接把消息取走,投递给此用户。如果app配置了推送证书,上传了推送证书并且集成了推送功能,服务器会给接收方发一个APNs推送,则会对离线消息进行APNs推送提示消息内容,通知接收方有一条新消息。
    如果想自定义推送的alert,可以在发消息的时候,在消息扩展中添加相应的字段。文档见:APNS内容解析

    离线推送 : 当app被杀死或者进入后台三分钟之后
    消息回调 : app在前台及app进入后台三分钟之内

    注意 : 环信支持推送消息,只是目前还不能根据标签推送给特定用户组,也暂不支持推送模板。CMD消息没有推送,好友请求也没有推送

    收不到离线推送时可以从下面几个方面找下原因

    1.测试apns推送的时候,接受消息方的app是杀掉状态吗,或者进入后台三分钟以后
    2.看看你环信后台上传的证书名称与工程中初始化SDK那里填的证书名 是不是相同的
    3.配置证书时候填的id与你工程中的bundle id 是否相同
    4.devicetoken有没有传给环信SDK。即查看管理后台中,对应 IM 账户下是否有您刚刚写的证书名。(如果没有,请检查您是否得到了 deviceToken)
    5.确认Xcode环境是否配置正确 ,Build Settings---signing,看Debug对应的是不是开发的,Release对应的是不是生产的
    6.在确认xcode运行环境是否正确 (Product-->Scheme-->Edit Scheme, 开发证书选Debug,生产证书选Release)
    7.证书制作上传过程是否有问题,配置证书的时候是否设置了密码,正确的步骤可以参考:http://www.imgeek.org/article/825308748。另外可以用推送工具进行验证。
    8.如果以上都没有问题,可以尝试重新制作上传一下推送证书。
    对照这些检查一下,基本上就是这些原因
    如果上面几点都符合的话,看下重新登录之后是否可以收到之前收不到推送消息
    可以的,话提供一下AppKey,证书名 (查下证书是否被封)以及收不到的推送消息的消息id及发送方和接收方log
    log导出请看这篇文章: http://www.imgeek.org/article/825308785 然后转成txt格式上传到工单上,同时注明上述配置都正确 , 环信这边来查下消息推送记录

    注意 :后台没有证书名 是指用户列表后面没有显示证书名。这个证书名是SDK初始化的时候传的字符串,用户登录之后会进行绑定。
    如果用户没有绑定证书名的话,肯定收不到推送的。这个证书名是用户登录之后绑定的,要确认下初始化SDK的时候有没有传。 options.apnsCertName = apnsCertName;

    ==============常见问题==============

    Q : iOS apns离线推送证书apns的离线推送可以和友盟(极光)推送共用一个证书吗?
    A : 环信的推送只要和后台上传的证书对应就可以实现,其他的不关心。
    首先苹果推送证书的生成都是统一的方式,这个不区分是极光(友盟)推送证书还是个推证书等等。使用的推送证书只要按照正确的苹果推送证书生成流程创建,都可以使用。
    环信添加推送证书可以看http://www.imgeek.org/article/825308748 不是要求必须重新生成推送证书 
    Q : 好友申请通知的离线推送?
    A : 我们的好友体系,添加好友的申请不支持离线推送。
    如果你们是使用App本身的好友体系,可以在app的添加好友业务上向被添加的好友发送文本消息,在EMMessage的ext中设置自定义字段,来区分此条文本消息是否用于好友申请提示,由此来判断处理UI的显示。

    Q : iOS的杀死进程远程推送和服务端有关么
    A : 如果客户端把远程通知给关了肯定就收不到通知,我们服务器会检测客户端是否有deviceToken,有的话才会把消息发送到deviceToken对应的设备上

    Q : 每个项目创建了一个开发的推送证书一个生产的推送证书。这俩证书什么时候要做切换?
    A : 在App上传AppStore前需要修改App内初始化SDK设置的推送证书名,EMOptions的apnsCertName。
    注意,这里的值需要和在Console管理后天上传时设置的证书名一致。

    Q : 绑定devicetoken的时候是否需要先登录到环信?
    A : 绑定是需要登录过之后才进行的,
     - (EMError *)bindDeviceToken:(NSData *)aDeviceToken; 是把deviceToken传给SDK。调用登录,SDK会进行绑定。也可以调用 - (void)registerForRemoteNotificationsWithDeviceToken:(NSData *)aDeviceToken
    completion:(void (^)(EMError *aError))aCompletionBlock;自己绑定
    需要判断是否已经登录,如果已经有登录的账号,再登录会返回 已登录的错误。

    Q : 离线推送在客户端怎么设置显示详情?
    A :
     EMPushOptions *pushOptions = [[EMClient sharedClient] pushOptions];
    pushOptions.displayStyle = EMPushDisplayStyleMessageSummary;

    可以设置离线推送消息显示具体内容还是只显示-您收到一条消息
    要设置在登录成功之后,然后要用服务器拉取一遍APNS 属性
    EMError *error = nil;
    EMPushOptions *options = [[EMClient sharedClient] getPushOptionsFromServerWithError:&error];

    然后在修改displayStyle

    Q : 两个APP通信,如果只希望其中一类APP能收到推送,而另一端的APP不希望收到推送,是不是不希望收到推送的APP不配置证书就好了?
    A : 两个App的推送证书都是在同一appkey下单独配置的,如果不希望收到推送,可以对此App不配置推送证书,同时在App代码中注释掉注册远程通知的相关代码。
    bundle id对应的证书也可以取消push的功能,针对App不使用任何远程推送服务,包括其他第三方的,如果App还需要其他第三方的推送服务,请忽略这句话。

    Q : 不配置推送证书的APP是不是只有刷新的情况下才会显示新的消息?不刷新的情况下APP是看不到新的消息?
    A : 不配置推送功能的App,只有在用户登录成功后,才能通过长连接的接收消息回掉中拿到消息体。

    Q : 在开发环境下收到了离线推送消息,但是在生成环境下没有收到?
    A : 看一下SDK初始化时,是否设置的apnsCerName与生产环境证书上传时填写的证书名一致,还有是否为adhoc打包成ipa文件安装测试的。

    Q :发送消息1,2,3,4,5 对方收到推送 2 1 5 顺序不对而且丢失, app角标也不对
    A : 1.首先,苹果不保证所有远程推送的到达率。这个可以看苹果官方文档。
    Because the delivery of remote notifications is not guaranteed, never include sensitive data or data that can be retrieved by other means in your payload.
    2.我们只保证,把离线消息执行远程推送,发给苹果服务器。苹果服务器是否能够百分之百把所有推送送达到指定移动端,这个根据苹果的策略,当 APNs 向你发送了多条推送,你的设备在 APNs 那里下线了,这时 APNs 到你的手机的链路上有多条任务堆积,APNs 的处理方式是,只保留最后一条消息推送给你,然后告知你推送数。那么其他消息会被APNs丢弃。
    3.我们保证的是离线消息,当用户重新登录时,可以都接收到。
    4. 如果需要找后台查询离线消息(前提接收方已绑定deviceToken)是否成功,需要提供离线消息的messageId,接收方环信id
    5.我们的推送角标,是接收方的在服务端的离线消息数。
    https://developer.apple.com/library/prerelease/content/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/APNSOverview.html#//apple_ref/doc/uid/TP40008194-CH8-SW1
    这是官方文档

    20170605122458543-1.jpeg



    Q11 :ios是怎么判断离线了 然后发推送的啊 有时候把应用杀掉后 半天收不到推送
    A : rest可以查用户的状态,推送前提是此用户有devicetoken已经绑定成功
    如果账号所有配置都没问题,杀掉后,其他人发的消息,过几秒就能看到推送
    Q12 : Q11不管用会是什么原因呢
    A : 配置,还有账号在我们这绑的deviceToken

    Q : 多个app共用一个appkey 推送证书怎么配置呢
    A : 后台可以上传多套推送证书。

    Q :程序关闭后推送了一个消息,点击后怎样获取到环信传过来的数据
    A : 需要用户点击横幅后,重新启动App,这时从- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions中,获取字典launchOptions,UIApplicationLaunchOptionsRemoteNotificationKey这个key下的数据,就是aps的字典数据

    二 . 消息回调
    app的长连接存在的时候,环信服务器检测您为在线状态,是不会给app推送消息的。app端在线的情况下,消息会通过长连接直接收取(didreceivemessage),收到消息,SDK会通过回调通知给上层。app通过收消息的回调拿到消息对象,然后解析并展示UI。
    目前我们不支持App切后台后,可以一直执行。
    我们SDK在切后台后,实现[[EMClient sharedClient] applicationDidEnterBackground:application];,会保持,直到被系统释放iOS目前其他方式应该都无法去实现一直保持App的活跃状态了。

    ==============常见问题==============
    Q : 本地推送声音设置在哪 ?一条消息推送两声
    A : 推送声音设置的要自己实现,具体可参考demo里的ChatDemoHelper类和MainViewController类里的
    - (void)showNotificationWithMessage:(EMMessage *)message方法,该方法中有发送本地推送做的一系列操作,本地通知怎么做的,本地通知触发几次,一条消息推送几声,一下接收到多条消息响几声,都需要用户自己实现.

    Q : 视频通话,推送怎么实现?
    iOS 3.2.3之后,如果在实时通话接收方不在线时,发送提醒。
    A : 1.在发起实时音视频通话前,需要设置EMCallOptions对象属性isSendPushIfOffline为YES;
    2.遵守协议EMCallBuilderDelegate,实现其中的- (void)callRemoteOffline:(NSString *)aRemoteName 委托方法。
    3.在第2步的方法中向 aRemoteName用户发送单聊消息。
    如果被叫方已注册远程通知且绑定deviceToken,会收到对应消息的APNs推送,点击横幅来唤醒App。
    上面是接收方离线的情况。如果接收方长连接还未断开,只是App切到后台,需要在回调- (void)callDidReceive:(EMCallSession *)aSession中判断当前App是否在后台,如果是弹出本地通知。

    Q : EMCallOptions *options = [[EMClient sharedClient].callManager getCallOptions]; //当对方不在线时,是否给对方发送离线消息和推送,并等待对方回应 options.isSendPushIfOffline = YES; [[EMClient sharedClient].callManager setCallOptions:options];isSendPushIfOffline设置为YES后,A用户呼叫B用户,B用户处于离线状态,但B用户没有收到推送。
    A : 1. 先确接收方杀掉App后,文本消息是否能收到APNs推送。
    2.在1点确认App杀掉可以收到推送前提下,确认实时音视频发送方代码执行顺序如下:
    (1) EMCallOptions *callOptions = [[EMClient sharedClient].callManager getCallOptions];
    callOptions.isSendPushIfOffline = YES;
    callOptions.offlineMessageText = @"提示文本";//可选
    [[EMClient sharedClient].callManager setCallOptions:callOptions];

    (2) callManager调用
    - (void)startVideoCall:(NSString *)aUsername
    completion:(void (^)(EMCallSession *aCallSession, EMError *aError))aCompletionBlock;

    或者
    - (void)startVoiceCall:(NSString *)aUsername
    completion:(void (^)(EMCallSession *aCallSession, EMError *aError))aCompletionBlock;

    Q : 推送的提示音可不可以自定义啊。
    A: 推送的提示音目前不支持自定义,本地通知的你们以自己去设置。
     
    Q : app压后台,立刻收到聊天推送来的信息,点击通知栏信息,捕获不到唤起程序事件
    A : App切后台后,长连接为断开前,当前弹出的横幅是本地通知,那么此时唤醒时间是本地通知的回调
    - (void)application:(UIApplication *)application didReceiveLocalNotification:(UILocalNotification *)notification;

    iOS10后
    - (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)())completionHandler;

    Q : 环信在离线状态下能收消息,但是程序运行状态按home按键进入后台的时候无法接受消息,怎么处理
    A : App切入后台一段时间内,长连接还未断开,这时候接收消息都是通过SDK的接收消息回调(EMChatManagerDelegate)来收消息,不会执行APNs推送。
    如果是需要弹出横幅提醒,需要在接收消息的回调方法中,判断[[UIApplication sharedApplication] applicationState]为UIApplicationStateBackground,然后实现本地通知。可以参考demo中的处理
    (ChatDemoHelper的- (void)didReceiveMessages:(NSArray *)aMessages)
    [attach]7500[/attach][size=13]


    [/size]
    收起阅读 »

    环信CEC亮相GMIS 2017峰会,智能客服大有可为!

        近日,2017全球机器智能峰会(GMIS 2017)在北京圆满举行,47位重磅嘉宾带来的32场演讲、4个圆桌论坛、1场人机大战及5个主题Session轮番上演,使GMIS 2017成为聚焦人工智能及相关领域的顶级行业盛宴。说出来你可能不信,环信是唯一受...
    继续阅读 »
        近日,2017全球机器智能峰会(GMIS 2017)在北京圆满举行,47位重磅嘉宾带来的32场演讲、4个圆桌论坛、1场人机大战及5个主题Session轮番上演,使GMIS 2017成为聚焦人工智能及相关领域的顶级行业盛宴。说出来你可能不信,环信是唯一受邀参展GMIS 2017的智能客服公司!

    001.jpg


       LSTM 之父&Dalle Molle 人工智能研究所副主任Jürgen Schmidhube大胆预测,在未来几年人类将创造出具有灵长类动物智能的人工智能系统。而现阶段AI在行业发挥最大生产力更可能是在垂直行业,特别是客服行业,智能客服聊天机器人已经展示给世人强大的生产力。

    002.jpg


      也许你已经被环信诸如“全球最大、国际领先、国内市场占有率第一...”等狂轰滥炸的晕头转向了,我也很懵逼啊,我只是一个新媒体小编,对于市场第一描述词汇量的匮乏我也很绝望啊...

    003.jpg


       环信作为智能客服企业的先行者,基于自然语言处理和机器学习技术推出了环信智能客服机器人,辅助或代替人工客服精准回答常见或高频问题,降低企业客服人力成本。目前,环信在客服领域已经服务了58541家标杆客户,积累了人工智能在客户服务行业落地的大量最佳实践。



    004.jpg


    为什么环信在AI方面有领先优势,投资人说的对!




    005.jpg


       主会场演讲嘉宾美国通用电气GE Transportation CTO Wesly Mukai谈到机器学习目前已经应用在美国铁路运输这种非常实际的领域中,为提高效率做出了很大贡献。Wesly先生会后来到环信展台深入了解国内智能客服机器人在客服行业的应用,他认真听取了环信客服聊天机器人的实现方式(单轮会话、多轮会话、人机协作...)和应用场景以及帮助客户取得的效果和成绩,他认为中国企业在智能客服行业的探索已经走在了世界前列,同时他很看好AI在垂直领域所爆发的强大生产力。



    006.jpg


    Wesly和环信员工谈笑风生(照片由Wesly先生私人翻译帮助拍摄提供)




    007.jpg


       Citadel 首席人工智能官邓力发表了以“无监督学习的最新进展(Recent Advances in Unsupervised Learning)”为主题的演讲。他认为,聚类方法、GAN 和变分自编码器(VAE)等传统无监督学习方法关注的重点是对输入数据的结构建模。腾讯 AI Lab 副主任俞栋在大会上洞悉了语音识别领域的前沿研究,今日头条、第四范式等嘉宾们从语音交互领域、自然语言处理及人工智能平台等领域切入,详细解构了其在人工智能时代所实践的产业创新,展示出AI技术在不同领域产生的巨大价值及未来机遇。

    008jpg.jpg


       当下,科技和创新进入拐点式爆发,人工智能的浪潮席卷全球。在此背景下,GMIS 2017作为国内首次汇集起全球人工智能和机器人领域顶级专家的大会,中立、权威、系统地呈现了机器智能相关技术的前沿研究,为全球人工智能领域前沿专家学者共同探讨机器智能如何从技术转化成产品和应用提供了绝佳平台,同时深切关注人工智能未来能够解决哪些具体问题,及如何帮助人类智慧生活的体验得到提升。GMIS将对行业产生积极而深远的影响,并开启人工智能发展的新起点。

    009.jpg


       最后提前剧透:环信联合Gartner即将在国内发布客服行业首个机器人选型报告《智能客服机器人之客户服务行业最佳实践》我会到处乱说么?
     
    报告抢先看
     
       市场上关于机器人的分类很多,误区也有很多。往往人们会将客服机器人等同于聊天机器人,但客服机器人其实只是聊天机器人的一种。聊天机器人主要分为两个大类:闲聊机器人与Task Oriented 机器人。Task Oriented 机器人是以任务目的为导向的机器人,又包括个人助理机器人与客服机器人。


    010.jpg


    上表对闲聊机器人、个人助理机器人、客服机器人从解决问题领域、平台系统开放性、技术方案的角度进行了详细比较。


    011.jpg


    智能客服机器人概述与分类 收起阅读 »

    IM-SDK和客服SDK并存开发指南—Android篇

          环信作为国内领先的企业级软件服务提供商,产品包括国内上线最早规模最大的即时通讯云平台——环信即时通讯云,以及国内领先的全媒体智能云客服平台——环信移动客服。区别于环信即时通讯云SDK(IM SDK),环信移动客服也专门提供了访客端SDK。很多小伙伴...
    继续阅读 »
      
       环信作为国内领先的企业级软件服务提供商,产品包括国内上线最早规模最大的即时通讯云平台——环信即时通讯云,以及国内领先的全媒体智能云客服平台——环信移动客服。区别于环信即时通讯云SDK(IM SDK),环信移动客服也专门提供了访客端SDK。很多小伙伴在环信开发过程中,同时使用了环信即时通讯云和环信移动客服服务,就会有这样的疑问,这个时候应该使用哪个SDK 呢?这两个有什么区别?于是让开发者惊呼“还有这种操作”的《IM-SDK和客服-SDK并存开发指南》应运而生,希望能帮助小伙伴们更快速、高效集成环信。嗯,就是有这种操作,不服就来尬舞啊!!!
     一、SDK介绍
    1. 访客端客服SDK基于IM-SDK 3.x开发,包含了IM-SDK所有的API和功能,当同时使用IM和客服时,只需要在初始化、登录、登出操作时使用访客端客服 SDK 提供的相应API即可。
    2. UI部分集成需要分别导入Kefu-easeui和EaseUI(IM的EaseUI),也可以自己写UI部分。


    二、注意事项
    1. 开发过程中 初始化、登录、登出,务必使用客服SDK的API。
    2. IM-SDK和客服SDK都包含了armeabi、armeabi-v7、arm64-v8a、x86的CPU架构,在发版的时候,可以选择部分CPU,例如剔除模拟器用的x86架构,在build.gradle中配置即可。

     
    三、资源准备
    1. 到环信官网下载访客端客服SDK+Demo源码,下载链接:http://www.easemob.com/download/cs选择“Android客服访客端”下载(如下图)。 

      001.jpg

    2. 到环信官网下载IM的SDK+Demo源码,下载链接:http://www.easemob.com/download/im 选择Android SDK(如下图)。  

      002.jpg



    四、资源简介
    1. 解压后的SDK压缩包中,含有基础版和实时音视频版,根据需求的不同使用不同的SDK。默认EaseUI使用的是libs文件夹内的SDK,此SDK含有实时音视频功能,因此比较大,如果不需要使用实时音视频功能的,可以使用libs.without.audio文件夹下的SDK。
    2. 从官网下载的客服访客端SDK包括以下目录: 



    003.png


    其中:kefu-android-demo为包含实时音视频的商城demo,可以直接运行。
     libs 为客服访客端SDK,包含实时音视频功能。
    libs.without.audio 为不包含实时音视频功能的客服访客端SDK。
     
    五、集成步骤
    1. 参考客服访客端文档或demo源码集成客服的访客端SDK,文档地址:http://docs.easemob.com/cs/300visitoraccess/androidsdk 。
    2. 可以把Kefu-Easeui作为一个module,libs放入客服的SDK(kefu-sdk_*.jar和相关so),import IM的easeUI到项目中,去掉里面的hyphenate_*.jar以及相关so。注释掉EaseUI.java中的IMSDK的初始化方法,最终结果为:app项目依赖IM-easeui module,IM-easeui 依赖kefu-easeui module。
    3. 在自己项目中的Application中的onCreate方法中,先调用客服SDK的初始化方法,再调动IM-easeui和Kefu-easeui的初始化方法(如果不用IM或客服的EaseUI则不需要对EaseUI做初始化)。
    4. 在app项目中,调用登录、登出方法需要调动客服的API,其他的API为各自的API。在调用EaseUI相关的Activity时,如为IM-easeui的Activity需要在AndroidManifest.xml中注册,kefu-easeui则不需要再注册(因为在kefu-easeui这个module中的AndroidManifest.xml中已注册)。Demo和EaseUI的源码是开源的,也可查看下。

     
    六、注意事项

       APP的通知栏提醒,客服和IM的easeui中均有自己的通知栏,代码均开源,可按照自己的方式去修改,具体应用可看下载的Demo源码中的演示实例。
     
    提供的兼容Demo介绍:
    1.  Demo是在客服的商城Demo上修改,在左上角添加了一个聊天室的按钮,点击按钮会根据appkey随机创建一个账号并登录,登录成功后会进入聊天室列表界面,点击某个聊天室可以在聊天室中聊天。
    2.  Demo中客服部分功能还是和原商城Demo功能一致。
    3.  Demo中为了演示因此采用随机注册账号的方式,对于用户场景中,可以先注册好这些账号和自己的账号绑定,这样每次咨询客服就都是同一个人了,也可以显示这个访客曾经的聊天记录。

     
    Demo源码地址:http://kefu-prod-apk.oss-cn-hangzhou.aliyuncs.com/kefu-android-demo.zip 收起阅读 »

    【环信公开课第13期视频回放】智能硬件创业供应商选型经验指南

    罗飞老师已经近一年多没有对外分享,全情投入到人工智能硬件创业上。这次分享罗飞老师带来了他这一年多的心血结晶~ 能说会道,宝宝的好伙伴-小墨​ 语音交互 | 运动控制 | 远程监控 | 视频通话 | 海量内容    随着移动互联时代向DT时代的逐渐...
    继续阅读 »
    罗飞老师已经近一年多没有对外分享,全情投入到人工智能硬件创业上。这次分享罗飞老师带来了他这一年多的心血结晶~



    微信图片_20170531170220.jpg


    能说会道,宝宝的好伙伴-小墨​
    语音交互 | 运动控制 | 远程监控 | 视频通话 | 海量内容


       随着移动互联时代向DT时代的逐渐演进,各种新模式新技术层出不穷,作为众多行业的顶层设计AI才慢慢向世人展示出了其惊人的生产力,人工智能时代也随之悄然来临!但人工智能落地并不是这么容易,它的创业门槛更高, 会涉及到包括软硬件等方方面面。

       如果您是一位想在机器人、智能家居、智能硬件等行业创业的老铁,选择对了靠谱的供应商很大程度上决定了创业成败。上周四我们邀请到智众互动CEO老司机罗飞,他和大家一起分享关于了“小墨机器人”项目创业的选型实践。
     
       罗飞丨智众互动CEO,国内流行thinkphp核心开发者之一,曾就职于新浪、创新工场。著有《内外兼修》、《swift入门实战》等书, 其中《内外兼修》是由李开复、刘东华、鸟哥等写序推荐。环信公开课第13期主题回顾



    微信图片_20170531172941.jpg


    罗飞老师在直播现场


    环信公开课第13期主题回顾


    1.智能硬件音视频通话哪家强?环信音视频通话的最佳实践!

    • 环信语义理解:是以人机交互为核心技术、语义理解为核心应用的人工智能交互服务。
    • 环信人脸识别:是一款用于提供图像和视频帧中人物分析的在线视频通话服务。
    • 环信视频处理:智能避障能在有障碍物的情况下判断并自动躲避。
    • 环信智能监控:嵌入强大的监控系统,实时监测孩子最新的动态。


    2.科大讯飞、思必驰、云知声的语音识别和语音合成各有特点,如何选择?  

    3.瑞芯微、全志,哪家芯片适合自己?  

    4.如何做大数据,通过语义分析让机器人可以像人这样回答问题?


    参加环信公开课还有礼物相送 



    QQ图片20170531172203.jpg



    罗飞老师签名书籍


     
    公开课视频回放
     







     
    公开课合作及更多信息,请添加“环信MM”


    扫码.gif


     
    小墨机器人正在众筹:小墨机器人 收起阅读 »

    环信移动客服v5.19已发布,视频客服新增截图、静音等功能

     客服模式 会话标签支持hover显示 当会话标签过长无法在列表中完整显示时,可以将鼠标放在标签上,查看完整的会话标签。  实时视频支持截图、静音功能 Web版客服工作台的实时视频支持截图、静音功能。和app、网页客户进行视频聊天时,客服可以截...
    继续阅读 »
     客服模式

    会话标签支持hover显示


    当会话标签过长无法在列表中完整显示时,可以将鼠标放在标签上,查看完整的会话标签。 

    001.png


    实时视频支持截图、静音功能

    Web版客服工作台的实时视频支持截图、静音功能。和app、网页客户进行视频聊天时,客服可以截取客户的视频截图;需要和同事讨论时,可以暂时对己方视频进行静音。

    注:实时视频功能为增值服务。仅Chrome浏览器在https模式下支持。

    管理员模式

    关键字匹配


    新增关键字匹配功能,支持为客户消息中的关键字设置正则表达式和自动回复,当关键字匹配正则表达式时,系统自动回复一条消息。使用关键字匹配功能,可以帮助客服和机器人更加灵活、快速、准确地解答客户的问题。

    关键字匹配功能为增值服务,如需开通,请提供租户ID并联系环信商务经理。

    使用示例:

    1. 进入“设置 > 关键字匹配”页面,点击“添加新的匹配组合”,设置规则名称、正则表达式、系统自动回复,并保存。
    • 正则表达式:设置正则表达式用于匹配客户消息中的关键字;
    • 系统自动回复:设置系统自动回复。在任意位置添加##,可在##位置显示匹配到的关键字。

    002.png

    2. 当客户消息中的关键字匹配上设置的规则时,系统自动回复一条消息。访客端示例: 

    003.png

    支持导出自定义报表自定义报表功能允许管理员根据不同的时间段、指标项目和指标维度自由搭配出不同的报表,能够满足多样化的报表需求。新增自定义报表导出功能,可以将数据导出为CSV格式的文件,便于在本地保存。注:自定义报表功能为增值服务,如需开通,请提供租户ID并联系环信商务经理。支持导出公共常用语公共常用语支持全部导出,方便管理员对公共常用语进行批量整理。进入“设置 > 公共常用语”页面,点击“导出常用语”按钮,导出全部常用语。在本地对公共常用语进行编辑后,可将系统中的常用语全部删除,并重新导入编辑后的常用语。 

    004.png

    自定义客户资料支持级联列表客户资料的自定义字段支持级联列表,满足多样化的客户资料需求。进入“设置 > 客户资料自定义”页面,点击“添加自定义字段”按钮,可以添加级联列表。级联列表选项最多支持10级。 

    005.png

    管理员通知支持发送全员和技能组在消息中心页面,管理员发送通知给客服团队时,可以选择全部、技能组、或客服成员。选择收件人的方法:
    • 输入@、@全部、@技能组名称、@客服昵称,在下拉列表中选择收件人。
    • 在右侧客服列表中直接选择全部、技能组、客服,或搜索成员并选择。


    注:输入@符号时,请切换至英文输入法。 

    006.png


    管理员模式顶部导航栏优化

    优化管理员模式顶部导航栏,删除体验指南页面。原体验指南提供的“商城”demo,可以前往“渠道管理 > 手机APP”页面,通过扫描app关联页面的二维码下载。 
     
    Android客服工作台

    当前版本:V2.9

    新功能/优化:

    管理员模式新增实时监控功能
    支持查看与客服同事的历史消息
    支持在进行中会话查看客户的历史消息(可跨会话)
     
    移动客服Android SDK

    当前版本:V1.0.6

    新功能/优化:

    优化部分API,使用更简单
    商城demo与EaseUI全面使用亮丽新UI
    UI全面支持国际化(根据手机设备语言切换) 
    环信移动客服更新日志http://docs.easemob.com/cs/releasenote/5.19 

    环信移动客服登陆地址http://kefu.easemob.com/ 收起阅读 »

    Error:warning: Ignoring InnerClasses attribute for an anonymous inner class 解决方案

    首先修改Gradle配置文件,启用MultiDex并包含MultiDex支持: defaultConfig {         multiDexEnabled true    } dependencies { compile 'com.Andr...
    继续阅读 »


    20160701171527638.png


    首先修改Gradle配置文件,启用MultiDex并包含MultiDex支持:

    defaultConfig {
            multiDexEnabled true
       }

    dependencies { compile 'com.Android.support:multidex:1.0.1' } 

    然后让应用支持多DEX文件。在MultiDexApplication JavaDoc中描述了三种可选方法:

    1、在AndroidManifest.xml的application中声明android.support.multidex.MultiDexApplication;
    2、如果你已经有自己的Application类,让其继承MultiDexApplication;
    3、如果你的Application类已经继承自其它类,你不想修改它,那么可以重写attachBaseContext()方法:
     
    @Override   
    protected void attachBaseContext(Context base) {  
        super.attachBaseContext(base); MultiDex.install(this);  
    }  
     
    运行就可以了,也可能打包了,这个问题貌似是工程中的方法数量超过安卓规定65536个方法数了,,,
      收起阅读 »

    iOS SDK 日志文件的导出

    环信SDK提供2.x和3.x两个版本。SDK会写入日志文件到本地。日志文件路径如下: 2.x 沙箱Library/EaseMobLog3.x 沙箱Documents/HyphenateSDK/easemoblog 访问沙箱目录模拟器 打印NSHomeDirec...
    继续阅读 »
    环信SDK提供2.x和3.x两个版本。SDK会写入日志文件到本地。日志文件路径如下:
    • 2.x 沙箱Library/EaseMobLog
    • 3.x 沙箱Documents/HyphenateSDK/easemoblog
     访问沙箱目录
    • 模拟器 
    • 打印NSHomeDirectory()
    • 打开Finder前往

    sandboxPath.png


    goFinder.png

    • 真机
    • 打开Xcode连接设备,前往Xcode --> Window --> Devices

      aDevice.png

        • 进入Devices界面

          deviceDownloadContainer.png

            • 选择Download Container之后会下载到本地一个.xcappdata文件。选中这个文件鼠标右键显示包内容。

              showContainer.png

                • 可以访问到沙箱目录

                  deviceSbPath.png

                  SDK日志
                  • ​2.x

                  2xLog.png

                  • 3.x

                  3xLog.png


                    收起阅读 »

                    集成环信遇到的相关问题整理

                    最近在整理这段时间被别人问到引入环信可能会出现的问题,记得的也不太多,想到一个就在这里记录一个吧,如果有遇到过本文中没有列出来的,可以问我,我会一一解答的 原文地址: http://blog.csdn.net/jyt199011302/article/deta...
                    继续阅读 »
                    最近在整理这段时间被别人问到引入环信可能会出现的问题,记得的也不太多,想到一个就在这里记录一个吧,如果有遇到过本文中没有列出来的,可以问我,我会一一解答的
                    原文地址: http://blog.csdn.net/jyt199011302/article/details/68483995

                    1. pod引入的Hyphenate里面的.h文件中和手动下载的sdk相比会缺少Hyphenate.h 。
                    A :  主要是pod 问题 本地仓库太旧了, 终端行pod repo update, 之后在pod search 'Hyphenate' 如果可以找到3.3.0版本, 就可以下载了 podfile 里面 platform 要指定8.0

                    2. iOS SDK 从低版本 升到3.3.0 后运行报错 (集成动态库版本报错)
                    dyld: Library not loaded: @rpath/Hyphenate.framework/Hyphenate
                      Referenced from: /Users/white/Library/Developer/CoreSimulator/Devices/BE0DDC26-96AE-4396-A6C5-48DC6938042B/data/Containers/Bundle/Application/4F9F570A-44B5-4F81-AD19-F7AA38D26E40/SYSchoolProject.app/SYSchoolProject
                      Reason: image not found

                    20170330110241533.jpeg


                    A : 在Build setting -> General这里加上。 还有这里也加上 改不能成optional,
                    注意 : 改成optional之后会导致初始化为null

                    20170330104308569.jpeg




                    3.在AppDelegate中执行[EaseMob sharedInstance]崩溃
                    A : other link flags添加“-ObjC”选项(注意:O和C大写)


                    4. pod导入EaseUI 时报错 
                    A : 先进入Podfile文件中,添加pod 'EaseUI', :git => 'https://github.com/easemob/easeui-ios-hyphenate-cocoapods.git' ,保存退出之后执行pod update即可 ,如果还是失败,可以升级一下pod版本

                    屏幕快照_2017-05-22_上午10.27_.07_.png



                    5.‘Hyphenate/EMSDK.h’ file no found
                    A : 换下引用#import <HyphenateLite/HyphenateLite.h>
                         或者#import <Hyphenate/Hyphenate.h>
                         如果此方法不行, 可以试试选中你的项目中的Pods -> EaseUI->Build Phases->Link Binary With Libraries ,点➕->Add Other ,找到工程里面,Pods里面的Hyphenate文件夹下面的Hyphenate.framework 点击open,重新编译就好了

                    20170331200729906.jpeg




                    6. 

                    20170331110834145.jpeg


                    A :  可以参考问题2的基础上, 再看下相对路径那里


                    7.集成动态库上传AppStore出现问题, 打包上线时报错
                    ERROR ITMS-90087: "Unsupported Architectures. The executable for xiantaiApp.app/Frameworks/Hyphenate.framework contains unsupported architectures '[x86_64, i386]'."
                    A :  遇到这个问题的小伙伴一定是没有认真看咱们环信的官方文档,
                    由于 iOS 编译的特殊性,为了方便开发者使用,我们将 i386 x86_64 armv7 arm64 几个平台都合并到了一起,所以使用动态库上传appstore时需要将i386 x86_64两个平台删除后,才能正常提交审核

                    在SDK当前路径下执行以下命令删除i386 x86_64两个平台
                    实时音视频版本Hyphenate.frameworklipo Hyphenate.framework/Hyphenate -thin armv7 -output Hyphenate_armv7 lipo Hyphenate.framework/Hyphenate -thin arm64 -output Hyphenate_arm64 lipo -create Hyphenate_armv7 Hyphenate_arm64 -output Hyphenate mv Hyphenate Hyphenate.framework/
                     
                    不包含实时音视频版本HyphenateLite.frameworklipo HyphenateLite.framework/HyphenateLite -thin armv7 -output HyphenateLite_armv7 lipo HyphenateLite.framework/HyphenateLite -thin arm64 -output HyphenateLite_arm64 lipo -create HyphenateLite_armv7 HyphenateLite_arm64 -output HyphenateLite mv HyphenateLite HyphenateLite.framework/
                    拿实时音视频版本版本为例 : 执行完以上命令如图所示

                    20170401112052481.png


                    运行完毕后得到的Hyphenate.framework就是最后的结果,拖进工程,编译打包上架。

                    20170401112216045.png


                    注意 : 
                    1. 最后得到的包必须真机编译运行,并且工程要设置编译二进制文件General->Embedded Bunaries.
                    2. 删除i386、x86_64平台后,SDK会无法支持模拟器编译,只需要在上传AppStore时在进行删除,上传后,替换为删除前的SDK,建议先分别把i386、x86_64、arm64、armv7各平台的包拆分到本地,上传App Store时合并arm64、armv7平台,并移入Hyphenate.framework内。上传后,重新把各平台包合并移入动态库


                    打包时还有可能报这个错误
                    ERROR ITMS-90535: "Unexpected CFBundleExecutable Key. The bundle at 'Payload/xiantaiApp.app/EaseUIResource.bundle' does not contain a bundle executable. If this bundle intentionally does not contain an executable, consider removing the CFBundleExecutable key from its Info.plist and using a CFBundlePackageType of BNDL. If this bundle is part of a third-party framework, consider contacting the developer of the framework for an update to address this issue."
                    A :  ​从EaseUIResource.bundle中找到info.plist删掉CFBundleExecutable,或者整个info.plist删掉



                    8.ios apns推送是什么原因导致这个错误
                    注册deviceToken失败:application:didFailToRegisterForRemoteNotificationsWithError: Error Domain=NSCocoaErrorDomain Code=3000 "未找到应用程序的“aps-environment”的授权字符串" UserInfo={NSLocalizedDescription=未找到应用程序的“aps-environment”的授权字符串}
                    A: 工程配置没有打开推送功能。

                    9.运行demo报这个错误

                    20170519110027739.png


                    A: 没有存储空间了。
                     
                     
                    10. SDK3.3.1 以上版本手动导入EaseUI报错
                    A : 由于demo是用pod集成的,所以直接引入demo中的EaseUI会缺少相关文件,可以直接拖入附件中的EaseUI
                    如果引入之后报如下图的错误

                    10.1_.png



                    10.2_.png


                    其实碰到上面这个问题还是很好解决的,这个是因为用到了UIKit里的类,但是只导入了Foundation框架,这个错误在其他类里也会出现,我们可以手动修改Founfation为UIKit,但是我不建议这么做,第一这个做法的工程量比较大, 在其他类里面也要导入,二,不利于移植,当以后环信更新的时候我们还是需要做同样的操作,这里我的做法的创建一个pch文件,在pch文件里面导入UIKit。解决办法:建一个PCH文件在里面添加如下代码:

                    10.3_.png



                    以上应该会正常了,但是如果集成的是不包含实时音视频的SDK, 您导入的EaseUI不是Lite版的,  那么此时还会报跟第六点一样的错误 , 需要导入EaseUILite 版本,所以会找不到Hyphenate/Hyphenate.h,如果是手动集成,建议在xcode设置一下Build Settings> GCC_PREPROCESSOR_DEFINITIONS >ENABLE_LITE=1,这样easeui就去找HyphenateLite/HyphenateLite.h
                    也可以通过pod集成,文档上针对easeui集成Full版本和Lite版本sdk特殊的说明http://docs.easemob.com/im/300iosclientintegration/85easeuiguide
                     
                    或者不想导入Lite版的 , 只想引入EaseUI 
                    这时需要把 #import <Hyphenate/Hyphenate.h>注释掉,然后把报错地方的Hyphenate换成HyphenateLite就可以了
                     
                     
                    11. 

                    1.png


                    A : 可以删除或者重命名Podfile.lock文件,重新执行pod install命令 收起阅读 »

                    【新手快速入门】集成环信常见问题+解决方案汇总

                       这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。   ios篇 APNs证书创建和上传到环信后台头像昵称的简述和处理方案音视频离线推送Demo实现环信服务器聊天记录...
                    继续阅读 »
                       这里整理了集成环信的常见问题和一些功能的实现思路,希望能帮助到大家。感谢热心的开发者贡献,大家在观看过程中有不明白的地方欢迎直接跟帖咨询。
                     
                    ios篇
                     Android篇昵称头像篇 直播篇[list=1]
                  • 一言不合你就搞个直播APP
                  •  客服集成[list=1]
                  • IM-SDK和客服SDK并存开发指南—Android篇
                  • IM-SDK和客服SDK并存开发指南—iOS篇
                  •  开源项目
                     
                    持续更新ing...小伙伴们还有什么想知道欢迎跟帖提出。
                      收起阅读 »

                    环信CEC(客户互动云):“云通讯+服务云+智能营销”构建从用户服务到用户营销的完整闭环

                        近日,客户世界-洞察者2017夏季论坛在深圳顺利举行,会上发布了行业权威的《中国客户中心现状与变革报告(2017)》。基于报告的调研分析结果以及专家评委会的打分,环信移动客服荣获《客户世界》2017年度编辑推荐“全媒体客服”标杆品牌。环信CEO刘俊彦表...
                    继续阅读 »
                        近日,客户世界-洞察者2017夏季论坛在深圳顺利举行,会上发布了行业权威的《中国客户中心现状与变革报告(2017)》。基于报告的调研分析结果以及专家评委会的打分,环信移动客服荣获《客户世界》2017年度编辑推荐“全媒体客服”标杆品牌。环信CEO刘俊彦表示:“中国客户中心发展经历了从呼叫中心(Call Center)到接触中心(Contact Center)的蜕变,再到互动中心(Engagement Center)的变革,全媒体客服已经从传统客户服务形态的终点逐渐转化成了SaaS客户互动形态的起点。随着2017年环信CEC(客户互动云)的发布,人工智能驱动的互动中心(AI-driven Engagement Center)大幕即将拉开,整个SaaS客服行业将被重构和赋能。”

                    ][F6S4LQU291EE7O[}HHPKN.png


                    环信荣膺《客户世界》全媒体客服标杆品牌
                     
                       AI技术经过整整60年的发展在包含客服在内的一些特定的业务场景实现了突破。技术应用的准确率达到大规模商业应用的要求,开始广泛在一些特定的领域进入实际应用阶段。消费者连接技术、接触技术和体验技术的不断创新正深刻改变着客服行业。随着全新的人机智能时代的到来,深度学习、认知计算、服务机器人、增强现实空间又在持续改变客户交互的方式、能力与体验,以致冲击客户中心现有的运营方式。共享经济理念、区块链技术等甚至会对服务与管理范式带来更巨大的革命。
                     
                       《中国客户中心现状与变革报告(2017)》研究项目以技术为前导,关注客服场景下各种新渠道、新触点、新运营、新分析;关注新技术潮流推动下客户中心的日常管理、团队建设、组织变革、人才培养等方面的变革和趋势。面向超过200家本领域甲方企业客服高级负责人(客服总经理/呼叫中心总经理/运营总监/技术总监)发放调研问卷,搜集反馈及评价意见。环信凭借其稳定先进的产品技术能力,优秀的服务水平以及大客户优势获得了调研的一致肯定。
                     
                    《中国客户中心现状与变革报告(2017)》对环信评价

                       环信是国内最早提供全渠道客服解决方案的厂商之一,产品结构完整,其即时通信云产品、移动客服产品、智能客服机器人产品和营销云产品构成从客户互动渠道,到客户服务,到主动营销的客户互动中心完整解决方案。环信服务大客户能力强,在电商、保险、证券、汽车等主要行业的龙头企业客户中拥有成熟的案例。环信独具优势的IM长连接技术,可以提供具备更高可靠性的全渠道客户互动体验;环信一直致力于推动人工智能和大数据在客服行业的落地,其智能客服机器人产品技术先进,有众多大型企业成功实施案例,其“客户声音”产品具有行业前瞻性,值得期待!

                    )DLD4)GOJZZT{_H((IYOM.png


                       环信CEO刘俊彦给与会行业技术领袖作了关于《人工智能驱动的客户互动云(Customer Engagement Cloud)》的主题分享。他在接受年度推荐全媒体客服标杆品牌奖项时表示 :“环信一直致力于推动整个SaaS客服行业的蓬勃发展,随着全媒体客服的完善以及AI的逐渐成熟, 环信预测“云通讯+服务云+智能营销”将构成从用户服务到用户营销的完整闭环。因此,2017年 ,环信整合旗下即时通信云、移动客服、智能客服机器人和主动营销产品线,推出环信CEC (Customer Engagement Cloud),向企业提供从客户互动渠道,到客户服务,到精准客户营销 的客户互动全流程解决方案。”同时,刘俊彦认为:“全媒体客服已经从传统客户服务形态的终点转化成了SaaS客户互动形态的起点,随着2017年环信CEC(客户互动云)的发布,人工智能驱动的互动中心(AI-driven Engagement Center)即将来临,整个SaaS客服行业将被重构和赋能。”

                    QQ图片20170524105853.png


                    环信CEC:“云通讯+服务云+智能营销”构建从用户服务到用户营销的完整闭环

                    QQ图片20170524105924.png


                    环信眼中的客服行业智能变革:客户服务全面人工智能化

                    QQ图片20170524105957.png


                    环信行业标杆客户全面覆盖

                    环信CEC(客户互动云)矩阵

                    QQ图片20170524110032.png


                    客户互动云(Engagement Cloud)核心特性

                    1、全渠道客户互动:全面支持网页、微信、微博、APP/IM、工单和呼叫中心等主流客户互动渠道。其中,环信业界领先的IM长连接技术支持千万级并发,保证消息必达,助力企业打造极致的移动端客户服务体验。所有渠道支持双向互动,如主动回呼,多渠道统一推送,基于用户行为的自动营销等,真正将服务通道与营销通道融合,实现客户中心从成本中心向利润中心的升级。

                    2、视频客服:实时双向视频客服,支持Android、iOS、Pad及主流PC和手机浏览器等多平台接入,低延迟,1080P高清,支持客户端和服务器端录制,可控灵活。

                    3、全渠道客服:环信移动客服作为业内广泛使用的客户中心系统,囊括多项行业主流大奖,拥有多项国际PCT专利和国内专利,深受客户好评。环信移动客服产品成熟可靠,功能完善,全面覆盖了全渠道接入管理,客户服务与客户互动管理,运营与运维管理,工单系统,现场管理,智能报表,质检等客户中心功能。

                    4、客户声音:环信客户声音是基于人工智能和大数据挖掘的客户体验透析产品。对来自多个渠道的非结构化客服会话数据进行自然语言解析,主题聚类和情感度建模,挖掘和分析热点话题,发现服务运营问题,寻找畅销或问题产品,洞察销售机会。客户声音系统可以帮助企业识别和改善客户旅程的各个阶段。

                    5. 智能客服机器人:环信智能客服机器人不仅在常见的单轮对话能力上表现优异,预装多种行业知识库,还可以快速开发多轮对话,支持人机协作以便在复杂场景下对人工客服提供全面AI辅助支持。同时,环信智能客服机器人的自动学习能力极大的降低了机器人知识库的维护成本。

                    6、精准营销及自动化营销:大数据和AI驱动的营销功能,如自动化消息模板和自动化规则管理及A/B测试,营销计划管理,基于用户行为轨迹、用户画像和用户会话内容的自动化消息和访客CTA(Call To Action)等。 收起阅读 »

                    环信Android/ios V3.3.2 SDK 已发布,新增群组、聊天室群公告及群文件功能

                     Android V3.3.2 2017-05-18 增加群、聊天室公告相关API群组支持上传及下载共享文件群组支持设置扩展属性EMLocalSurfaceView 和 EMOppositeSurfaceView 合为同一个控件 EMCallSurfaceVi...
                    继续阅读 »

                    QQ图片20170522115322.png


                     Android V3.3.2 2017-05-18
                    1. 增加群、聊天室公告相关API
                    2. 群组支持上传及下载共享文件
                    3. 群组支持设置扩展属性
                    4. EMLocalSurfaceView 和 EMOppositeSurfaceView 合为同一个控件 EMCallSurfaceView
                    5. Demo及EaseUI改成纯Android Studio结构,不再支持Eclicpse导入
                    6. easeui没有包含SDK的jar和so, 使用需要自己拷贝libs下的库文件,或者执行copyLibs.sh完成拷贝。

                     
                     iOS V3.3.2 2017-05-18
                     
                    新功能:
                    1. 新增:修改获取群公告,上传下载删除群共享文件,修改群扩展信息接口(接口详情请查看文档群组管理
                    2. 新增:修改获取聊天室公告(接口详情请查看文档聊天室管理
                    3. 新增:批量设置群组免打扰接口


                    修复:
                    1. 修复有时调用getAllConversations时返回为空的bug
                    2. 修复获取已加入群组超时的bug

                     
                     
                    版本历史:AndroidSDK 更新日志  ios SDK更新日志
                    下载地址:SDK下载
                    收起阅读 »

                    环信Android消息回撤

                    环信现在的消息回撤开发文档没有更新, 所以得自己去写, 本人贡献点小东西.本项目用的SDK版本为3.3.1. 1. 首先在聊天消息里添加消息长按事件监听,里面添加撤回消息选项.     撤回点击之后处理为:  发送撤回消息!!!!!!cmdMsg = EMMe...
                    继续阅读 »
                    环信现在的消息回撤开发文档没有更新, 所以得自己去写, 本人贡献点小东西.本项目用的SDK版本为3.3.1.
                    1. 首先在聊天消息里添加消息长按事件监听,里面添加撤回消息选项. 
                       撤回点击之后处理为:  发送撤回消息!!!!!!
                    cmdMsg = EMMessage.createSendMessage(EMMessage.Type.CMD);
                    // 如果是群聊, 设置chatType, 默认是单聊
                    if(chatType == Constant.CHATTYPE_GROUP){
                    cmdMsg.setChatType(ChatType.GroupChat);
                    }
                    String action = "REVOKE_FLAG";
                    EMCmdMessageBody cmdBody=new EMCmdMessageBody(action);
                    // 设置消息body
                    cmdMsg.addBody(cmdBody);
                    // 设置要发给谁, 用户username 或者群聊 grouid
                    cmdMsg.setTo(toChatUsername);
                    // 通过扩展字段添加要撤回消息的iD
                    cmdMsg.setAttribute("msgId", msgid); // 长按的时候, 获取本信息的message的Id
                    // long aa = cmdMsg.getMsgTime(); // 获取这个消息的发送时间
                    // 获取当前系统的时间
                    long time = new Date().getTime();
                    long minite = (time - aa - 6000)/1000; // 1s = 1000
                    if(minite <= 120){
                    EMClient.getInstance().chatManager().sendMessage(cmdMsg);
                    cmdMsg.setMessageStatusCallback(new EMCallBack() {

                    @Override
                    public void onSuccess() {
                    conversation.removeMessage(msgid);
                    handler.sendEmptyMessage(1);
                    }

                    @Override
                    public void onProgress(int arg0, String arg1) {
                    }

                    @Override
                    public void onError(int arg0, String arg1) {
                    // TODO Auto-generated method stub
                    String a = "";
                    // conversation.removeMessage(msgid);
                    }
                    });

                    }else{
                    ToastUtils.ToastShortMessage(getActivity(), "发送时间超过2分钟的消息!不能被撤回!");
                    }
                    break;





                    此处handler.sendEmptyMessage(1);中的内容是:         
                    ToastUtils.ToastShortMessage(getActivity(), "消息已撤回!");
                    messageList.refresh();





                    2. 环信在获取CMD消息监听有三个地方: 分别为, EaseChatFragment, MainActivity, DemoHelper(此处为App后台运行时, 消息撤回的处理)
                    在EMMessageListener下的onCmdMessageReceived()中处理接受到的CMD消息, 首先贴上的为EaseChatFragment里面的: 
                    for(EMMessage emMessage : messages){
                    EMCmdMessageBody cmdMessageBody = (EMCmdMessageBody)emMessage.getBody();
                    String action = cmdMessageBody.action();
                    if(action.equals("REVOKE_FLAG")){
                    try {
                    msgId = emMessage.getStringAttribute("msgId");
                    conversation1 = EMClient.getInstance().chatManager().getConversation(emMessage.getFrom());
                    if(emMessage.getChatType() == ChatType.GroupChat){
                    messageList.refreshSelectLast(); //刷新UI
                    }else{
                    handler.sendEmptyMessage(1);
                    }
                    } catch (HyphenateException e) {
                    e.printStackTrace();

                    }

                    }
                    }
                    此处handler.sendEmptyMessage(1)中的内容是:
                    // 删除表示撤销
                    conversation1.removeMessage(msgId);
                    messageList.refreshSelectLast();





                    3. MainActivity里面的处理方式:
                        for(EMMessage emMessage : messages){
                    EMCmdMessageBody cmdMessageBody = (EMCmdMessageBody)emMessage.getBody();
                    String action = cmdMessageBody.action();
                    if(action.equals("REVOKE_FLAG")){
                    try {
                    msgId = emMessage.getStringAttribute("msgId");
                    conversation1 = EMClient.getInstance().chatManager().getConversation(emMessage.getFrom());
                    if(emMessage.getChatType() == ChatType.GroupChat){
                    refreshUIWithMessage(); // 刷新UI
                    }else{
                    handler.sendEmptyMessage(1);
                    }

                    } catch (HyphenateException e) {
                    e.printStackTrace();

                    }

                    }
                    }
                     
                    此时handler.sendEmperymessage(1)中: 
                    conversation1.removeMessage(msgId);
                    refreshUIWithMessage();
                    4. DemoHelper里面的处理方式:
                    for (final EMMessage message : messages) {
                    // 获取消息body
                    EMCmdMessageBody cmdMsgBody = (EMCmdMessageBody) message.getBody();
                    final String action = cmdMsgBody.action();// 获取自定义action
                    // 发送一个透传消息
                    if(action.equals("REVOKE_FLAG")){
                    try {
                    if(message.getChatType() == ChatType.GroupChat){ // 群组处理方式
                    conversation1 = EMClient.getInstance().chatManager().getConversation(message.getTo(), EaseCommonUtils.getConversationType(2), true);
                    }else{
                    conversation1 = EMClient.getInstance().chatManager().getConversation(message.getFrom());
                    }
                    msgId = message.getStringAttribute("msgId");

                    handler.sendEmptyMessage(1);
                    } catch (HyphenateException e) {
                    e.printStackTrace();
                    }
                    }
                    }
                    此时handler.sendEmpertyMessage(1)中的方法是: 
                    conversation1.removeMessage(msgId);
                    至此,环信消息回调完成, 没有去做撤回回调处理,直接删除不好,如果想做的请自行处理.谢谢,本文纯属原创,如果有问题,可与我联系,QQ邮箱: 277667430@qq.com.本人姓氏: 侯 收起阅读 »

                    【客户世界·洞察者】智能客服机器人是下一代客服的核心驱动力(附Gartner报告全文)

                        上期我们谈完了工具层、知识层一个领先的SaaS客服厂商是如何做的,接着我们再来聊聊现在最火的AI。随着全媒体客服的普及和广泛应用导致企业和消费者多点接触,同时用户体验得到了企业的重视,导致客服咨询量暴增,企业有限的客服人力资源与日益增加的客服请求之间的...
                    继续阅读 »
                        上期我们谈完了工具层、知识层一个领先的SaaS客服厂商是如何做的,接着我们再来聊聊现在最火的AI。随着全媒体客服的普及和广泛应用导致企业和消费者多点接触,同时用户体验得到了企业的重视,导致客服咨询量暴增,企业有限的客服人力资源与日益增加的客服请求之间的矛盾日益尖锐,如何用有限的客服资源服务不断增长的海量客服请求需要一个颠覆型的技术来解决。相比人工客服,智能客服机器人将提供极大的效率优势。
                     
                       Gartner报告指出智能客服机器人(VCA-virtual customer assistance)的使用正处于临界点。大幅改进的自然语言处理技术,以聊天为中心的移动渠道与客户互动的应用,以及客户对机器人技术的接受程度,这些因素使得人们对VCA的兴趣越來越大。从被动的被人类编程出来的可以在结构化和非结构化内容库中找到问题答案的虚拟助手,到主动的有时候是机器学习得到的VCA的转变,其考察个人的特征并代表他们行动。虚拟助手正在经历从被动的被人类编程出来在结构化和非结构化内容库中找到问题答案到主动的通过机器学习能够理解用户个性化的需求并且随之采取灵活应对行为的转变。
                     
                       环信作为智能客服企业的先行者,基于自然语言处理和机器学习技术推出了环信智能客服机器人,辅助或代替人工客服精准回答常见或高频问题,降低企业客服人力成本。目前,环信在客服领域已经服务了58541家标杆客户,积累了人工智能在客户服务行业落地的大量最佳实践。
                     
                    视频观看地址:点击观看
                     
                    3.1,智能客服机器人在客服场景下的最佳实践:
                     
                    3.1.1,无需人工标记和人工维护的机器人单轮会话,极大降低客服机器人的维护成本。

                       一些问题是不依赖于对话历史,仅根据当前句子就能给出答案,难点在于机器能否理解同一语义的不同表达方法。环信智能机器人采用自然语言处理技术和深度学习技术建立对话模型,使用海量数据对模型进行训练,并借助客服系统中访客和客服的实时反馈来增强学习,精准识别用户意图,帮助人工客服回答各种问题。相比基于关键词匹配和人工定义规则大量标注数据的传统问答技术,环信智能机器人无需人工标记和人工维护相似问法,就可以在会话过程中识别同一问题的多种不同问法。

                    001.png


                    图1示例:环信机器人无需人工维护相似问法,就可以在会话过程中识别同一问题的多种不同问法。

                    3.1.2机器人多轮会话,支持更多复杂业务,进一步拓展机器人使用场景。

                       而另一些问题则由于缺少足够信息或者过于模糊,需要通过多轮对话的方式来明确用户的需求。比如用户想查物流,但是缺少订单号等信息,机器人需要引导用户提供这些数据。这和单轮的问答相比,多轮对话的技术难点更多,比如指代的理解,句子的省略,用户状态的维护等等。

                       环信智能机器人支持上下文语义和多轮会话,并预装多行业的领域知识,如电商行业的物流状态查询模型,产品保修会话模型等。这种基于行业领域和业务模型的多轮会话能力,相比单轮会话,进一步扩展了客服机器人对复杂客服业务场景的自动支撑能力。

                    002.png


                    图2示例:环信机器人通过多轮会话支持查询物流状态,并和企业业务系统做集成,真正意义上节省人工。

                    3.1.3无缝人机协作体验,复杂场景下最佳用户体验的客服模式。

                       在一些比较复杂和特殊的服务场景,比如高客单价的金融行业售前咨询,机器人客服不能完全理解客户的个性化咨询要求的时候,我们可以无缝进入人机混合模式。在人机混合模式下,环信智能客服机器人向人工客服推荐备选答案,人工客服起到了保证答案质量充当专家客服的角色,这样既保证了客服的响应速度又提高了问题的回答准确性,同时降低了人工客服的工作量。

                    3.1.4,智能质检,准确率达到替代人工质检水平。

                       环信机器人还提供自动智能质检功能,可以对全部客服会话进行实时或离线质检。 智能质检是基于环信在线客服各个领域的海量用户对话,提取出数百个客服对话特征,并用这些特征训练得到的一个通用质检模型。智能质检的准确率达到替代人工质检水平。

                    3.1.5. 支持智能自主学习,更高效的知识库

                       环信智能机器人可以快速高效搭建知识库。既支持批量导入FAQ或用户手动维护问答知识,也支持智能自主学习。智能自主学习是指客服机器人自主学习人工客服的会话,自动生成新知识规则。相比手动维护问答知识,智能自主学习能力显著降低了客服机器人的维护成本,提高了知识库的准确性和时效性。环信智能机器人可预装多个领域的行业知识库。

                    附录:Gartner研究——虚拟客户助手(智能客服机器人) 分析师:布莱恩·玛纳萨马

                    定义:虚拟客户助理(VCA),代表公司进行模拟对话以传递信息和/或代表客户采取行动并执行交易。 VCA由四部分组成:

                       ■接收请求和传递回应的用户界面■用于文本和语音的自然语言处理引擎■可以检索知识和内容数据存储库的搜索和知识引擎■用于分析意图的上下文引擎一些VCA还具有机器学习功能。

                       定位和市场接受速度:IBM Watson,Microsoft Cortana,Next-IT,Creative Virtual和其他VCA供应商的工作正在提高人们对作为实用工具的虚拟助理(VA)技术的认知。 VCA的使用正处于临界点。大幅改进的自然语言处理技术,以聊天为中心的移动渠道与客户互动的应用,以及客户对机器人技术的接受程度,这些因素使得人们对VCA的兴趣越來越大。从被动的被人类编程出来的可以在结构化和非结构化内容库中找到问题答案的虚拟助手,到主动的有时候是机器学习得到的VCA的转变,其考察个人的特征并代表他们行动。虚拟助手正在经历从被动的被人类编程出来在结构化和非结构化内容库中找到问题答案到主动的通过机器学习能够理解用户个性化的需求并且随之采取灵活应对行为的转变。VCA技术有望在两至五年内成为主流。随着移动优先的用户体验转向,许多VCA都亟待更新,以支持多渠道客户与统一知识库的互动,特别是支持客户手机。

                       用户建议:确定客户服务平台的当前状态和所需状态。今天您将使用什么样的客服方式和客服工具?您是否使用自然语言处理技术来确定客户到底咨询了什么?是将这个呼叫咨询分派给正确的客服接线员还是让客服机器人提供自动回复?VCA将成为支持多个客服渠道的起点。 VCA未来将可能改变你的日常生活;它可以是一个帮助你在移动设备上购买新健身设备的向导,也可以是一个帮助你开设银行帐户的虚拟客服人员。

                       市场正在发生一种变化,对虚拟客服助手的逐渐重视以及使用频次的减少,这个现象已不如以前那么明显。在数字渠道中提供拟人化体验的驱动力正在发生改变。随着客户逐渐适应和接受与计算机的互动,其对具有拟人化情感的3D图像的需求正在减少。公司部署虚拟客服助手时他们发现有对于提升品牌的附加价值,而非仅仅模拟一种店内体验。VCA不仅是面向客户的,而是越来越多地部署为面向员工——帮助客户服务中心减少人工坐席操作时间以及保障客户咨询回复的一致性。

                       将一组简单的串行项目与一个复杂的大型项目进行比较,以满足所有确定的需求。找到构成完整呼叫的最高频简单对话,以简单的方式实现自动化和提升客户满意度。然后,识别下一组完整的呼叫:在一段时间,技术与人工可进行合作处理这组呼叫,即当技术检测到问题(如技术储备的知识不足、客户声音难以辨别、或客户通过正确的操作明确要求由人类进行对话)时,人工操作员将接管此组呼叫。

                       业务影响:VCA是狭义的、具有特殊用途的VA,用于销售、客户服务和数字商务,且具有独特的目标。VCA的商业案例有三方面。其解决了以下需求:

                       ■满足客户对网络和移动渠道中客户支持的期望——更高的互动频率;全天候、即时的聊天可用性■将互动转向价格更低的客户自助服务渠道,更快获取解决方案;降低服务成本■提供积极的建议和参与,培养忠诚度和客户满意度。

                        VCA的有效使用便于组织衡量其——特别是联络中心的——参与数量。在数字亭或自动取款机上使用启用语音的VCA可降低对类型化干预的需求,且有助于为非传统受众提供有趣的互动。

                    好处评级:高

                    市场渗透率:目标受众的5%至20%

                    成熟度:未成熟
                     
                    点击查看Gartner报告全文 收起阅读 »

                    拍照闪退

                    在聊天页面点击拍照时闪退——>7.0及以上的系统手机处理拍照处理与原有的方法不一样了 参考: http://blog.csdn.net/ganshenml/article/details/72315636
                    在聊天页面点击拍照时闪退——>7.0及以上的系统手机处理拍照处理与原有的方法不一样了
                    参考:
                    http://blog.csdn.net/ganshenml/article/details/72315636

                    【视频教程+源码】基于环信IM做一个仿微信APP-更新ing

                    我只是一个普通人,做人要谦虚。 我不是大神,我也不是很厉害的。 天外有天,人外有人。 老师引入门,修行靠个人。 希望能帮助大家,谢谢。     大家好,我是郭永峰(峰哥) | 一个普通大学计算机系毕业的大学生,曾就职于澳门遊澳集团有限公司,负责大型银联支付业务...
                    继续阅读 »
                    我只是一个普通人,做人要谦虚。
                    我不是大神,我也不是很厉害的。
                    天外有天,人外有人。
                    老师引入门,修行靠个人。
                    希望能帮助大家,谢谢。

                        大家好,我是郭永峰(峰哥) | 一个普通大学计算机系毕业的大学生,曾就职于澳门遊澳集团有限公司,负责大型银联支付业务系统、跨国际短信业务系统(基于电信的SGIP)以及集团内部通讯系统 (负责android和openfire后台二次开发)的主要开发任务,担任项目负责人。13年就职于广州拓谷科技有限公司负责“酷蛙”车联网产品研发及汽车销售产品研发。14来年到17年2月份,就职于国内知名教育机构,负责教学研发及授课的工作。
                     
                    本人现状况:
                      
                       在家录制教学视频(无收入),搞工作室,组建团队成立公司,如果大家觉得分享内容很喜欢,可以给我打点赏支持本人的工作室,二维码在文末,就不影响阅读了。

                     
                    郭永峰IT教育工作室于2017年4月12日成立!
                     
                    成立原因:

                    希望把近10年来从事IT互联网的知识分享给大家,包括Linux,WindowServer,Java,PHP,Android,iOS,H5等等等。
                     

                    进入正题,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
                    1. 4月12号成立工作室,现在18号,过了一个星期
                    2. 一个星期录了5天的环信教程视频,我将放在网盘免费分享
                    3. 环信的教程视频主要是针对有开发经验者
                    4. 教程主要是使用环信来模仿微信来做一个即时通讯的案例
                    5. 课程主要是先讲socket基础 -> 环信 ->自定义协议
                    6. 希望这些教程视频能帮助大家,对即时通讯、socket和自定义协议有个较深入的了解
                    7. 同时能希望大家在面试时,在即时通讯这块不在陌生

                      持续更新

                    第一阶段:即时通讯的了解和微信APP开发前的准备!

                    【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)

                    【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)

                    【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)

                    【视频教程+源码】基于环信IM做一个仿微信APP-04.环信简介(了解) 

                    【视频教程+源码】基于环信IM做一个仿微信APP-05.集成环信的前提准备(掌握)
                     
                    【视频教程+源码】基于环信IM做一个仿微信APP-06.环信SDK的版本的区别(掌握)

                    【视频教程+源码】基于环信IM做一个仿微信APP-07.微信-项目创建及代码目录结构规范(MVC)

                    【视频教程+源码】基于环信IM做一个仿微信APP-08.微信-集成环信SDK

                    【视频教程+源码】基于环信IM做一个仿微信APP-09.微信-登录界面排版

                    【视频教程+源码】基于环信IM做一个仿微信APP-10.微信-主界面搭建

                    【视频教程+源码】基于环信IM做一个仿微信APP-11.微信-注册功能

                    【视频教程+源码】基于环信IM做一个仿微信APP- 12.微信-登录功能

                    【视频教程+源码】基于环信IM做一个仿微信APP- 13.微信-自动登录

                    【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出
                     
                    【视频教程+源码】基于环信IM做一个仿微信APP-15.微信-在其它设备登录
                     
                    整个项目源码,git地址https://github.com/mayaole/fWeiXin

                     微信打赏


                    微信.png



                    支付宝打赏


                    支付宝.png



                    谢谢大家的支持,个人微信号清扫描下面张图


                    个人.png



                     
                    郭永峰IT交流QQ群请加:596441895 收起阅读 »

                    【视频教程+源码】基于环信IM做一个仿微信APP-15.微信-在其它设备登录

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
                    继续阅读 »
                    接上篇

                    【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)
                    【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)
                    【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)
                    【视频教程+源码】基于环信IM做一个仿微信APP-04.环信简介(了解)
                    【视频教程+源码】基于环信IM做一个仿微信APP-05.集成环信的前提准备(掌握)
                    【视频教程+源码】基于环信IM做一个仿微信APP-06.环信SDK的版本的区别(掌握)
                    【视频教程+源码】基于环信IM做一个仿微信APP-07.微信-项目创建及代码目录结构规范(MVC)
                    【视频教程+源码】基于环信IM做一个仿微信APP-08.微信-集成环信SDK
                    【视频教程+源码】基于环信IM做一个仿微信APP-09.微信-登录界面排版
                    【视频教程+源码】基于环信IM做一个仿微信APP-10.微信-主界面搭建
                    【视频教程+源码】基于环信IM做一个仿微信APP-11.微信-注册功能
                    【视频教程+源码】基于环信IM做一个仿微信APP- 12.微信-登录功能
                    【视频教程+源码】基于环信IM做一个仿微信APP- 13.微信-自动登录
                    【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出
                     
                     
                    我是郭永峰,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。​
                     
                    15.微信-在其它设备登录


                    收起阅读 »

                    【视频教程+源码】基于环信IM做一个仿微信APP- 14.微信-主动退出

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解) ...
                    继续阅读 »

                    【视频教程+源码】基于环信IM做一个仿微信APP- 13.微信-自动登录

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
                    继续阅读 »

                    【视频教程+源码】基于环信IM做一个仿微信APP- 12.微信-登录功能

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
                    继续阅读 »

                    【视频教程+源码】基于环信IM做一个仿微信APP-11.微信-注册功能

                    【视频教程+源码】基于环信IM做一个仿微信APP-07.微信-项目创建及代码目录结构规范(MVC)

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
                    继续阅读 »

                    【视频教程+源码】基于环信IM做一个仿微信APP-06.环信SDK的版本的区别(掌握)

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)...
                    继续阅读 »

                    【视频教程+源码】基于环信IM做一个仿微信APP-05.集成环信的前提准备(掌握)

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解) ...
                    继续阅读 »

                    【视频教程+源码】基于环信IM做一个仿微信APP-04.环信简介(了解)

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解) ...
                    继续阅读 »
                    接上篇
                    【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)
                    【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)
                    【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)
                     
                    我是郭永峰,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
                     
                    04.环信简介(了解)
                     





                    未来还会出其它的教程视频,尽请期待,如果可以把文章分享出去,让更多的人学习环信!
                     
                    凡是有支持过本人工作室的人,以后有什么新技术的整套视频会优先免费或者是打折出售。因为现在的工作室是没有收入的,前期靠网友的支持。支持过的网友,我都会用一个表格记录在案。
                     
                    打赏二维码请看第一篇。

                      收起阅读 »

                    【视频教程+源码】基于环信IM做一个仿微信APP-03.XMPP实现即时通信的准备工作(了解)

                    接上篇 【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解) 【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)   我是郭永峰,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任...
                    继续阅读 »
                    接上篇
                    【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)
                    【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)
                     
                    我是郭永峰,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
                     
                    03.XMPP实现即时通信的准备工作(了解)​



                     
                    未来还会出其它的教程视频,尽请期待,如果可以把文章分享出去,让更多的人学习环信!
                     
                    凡是有支持过本人工作室的人,以后有什么新技术的整套视频会优先免费或者是打折出售。因为现在的工作室是没有收入的,前期靠网友的支持。支持过的网友,我都会用一个表格记录在案。
                     
                    打赏二维码请看第一篇、,如果你有心的话。 收起阅读 »

                    【视频教程+源码】基于环信IM做一个仿微信APP-02.XMPP简介(了解)

                      接上文【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)我只是一个普通人,做人要谦虚。 我不是大神,我也不是很厉害的。 天外有天,人外有人。 老师引入门,修行靠个人。 希望能帮助大家,谢谢。我是郭永峰,本套课程基于环信IM教大家如...
                    继续阅读 »
                     
                    接上文【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)
                    我只是一个普通人,做人要谦虚。
                    我不是大神,我也不是很厉害的。
                    天外有天,人外有人。
                    老师引入门,修行靠个人。
                    希望能帮助大家,谢谢。
                    我是郭永峰,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。

                    02.XMPP简介(了解)​
                     



                     
                     
                    未来还会出其它的教程视频,尽请期待,如果可以把文章分享出去,让更多的人学习环信!
                     
                    凡是有支持过本人工作室的人,以后有什么新技术的整套视频会优先免费或者是打折出售。因为现在的工作室是没有收入的,前期靠网友的支持。支持过的网友,我都会用一个表格记录在案。
                     
                    打赏二维码在上一篇文章,如果你有心的话。 收起阅读 »

                    【视频教程+源码】基于环信IM做一个仿微信APP-01.即时通讯简介(了解)

                     我只是一个普通人,做人要谦虚。 我不是大神,我也不是很厉害的。 天外有天,人外有人。 老师引入门,修行靠个人。 希望能帮助大家,谢谢。大家好,我是郭永峰,从11年开始玩openfire服务器,再到后面的XMPP,socket,一路走来,可以说即时通入门到放弃...
                    继续阅读 »
                     
                    我只是一个普通人,做人要谦虚。
                    我不是大神,我也不是很厉害的。
                    天外有天,人外有人。
                    老师引入门,修行靠个人。
                    希望能帮助大家,谢谢。
                    大家好,我是郭永峰,从11年开始玩openfire服务器,再到后面的XMPP,socket,一路走来,可以说即时通入门到放弃,最终选择了环信这样的即时通讯提供商。
                     
                    郭永峰IT教育工作室于2017年4月12日成立!
                     
                    成立原因:
                    希望把近10年来从事IT互联网的知识分享给大家,包括Linux,WindowServer,Java,PHP,Android,iOS,H5等等等。
                     
                    本人现状况:
                    在家录制教学视频(无收入),搞工作室,组建团队成立公司,如果大家觉得分享内容很喜欢,可以给我打点赏支持本人的工作室,二维码在文末,就不强求了。

                    进入正题,本套课程基于环信IM教大家如何做一个类似微信的APP,只用于技术交流,请勿用于任何商业用途。
                    1. 4月12号成立工作室,现在18号,过了一个星期
                    2. 一个星期录了5天的环信教程视频,我将放在网盘免费分享
                    3. 环信的教程视频主要是针对有开发经验者
                    4. 教程主要是使用环信来模仿微信来做一个即时通讯的案例
                    5. 课程主要是先讲socket基础 -> 环信 ->自定义协议
                    6. 希望这些教程视频能帮助大家,对即时通讯、socket和自定义协议有个较深入的了解
                    7. 同时能希望大家在面试时,在即时通讯这块不在陌生

                     
                    我们先来看第一个视频--01.即时通讯简介(了解) 







                     

                    微信打赏

                    QQ图片20170515172743.png


                    支付宝打赏

                    QQ图片20170515172833.png


                    未来还会出其它的教程视频,尽请期待,如果可以把文章分享出去,让更多的人学习环信


                    QQ图片20170515172529.png


                     
                    凡是有支持过本人工作室的人,以后有什么新技术的整套视频会优先免费或者是打折出售。因为现在的工作室是没有收入的,前期靠网友的支持。支持过的网友,我都会用一个表格记录在案。
                      收起阅读 »

                    环信移动客服v5.18已发布,增加机器人新手引导及机器人知识库测试功能

                    客服模式 支持根据会话类型筛选历史会话 历史会话页面显示会话类型(呼入、回呼),并支持根据会话类型进行筛选。 呼入:客户主动发起的会话;回呼:客服手动回呼的会话。 管理员模式机器人新手引导新增机器人新手引导功能。引导管理员在创建机器人时,对机器人的基...
                    继续阅读 »
                    客服模式

                    支持根据会话类型筛选历史会话


                    历史会话页面显示会话类型(呼入、回呼),并支持根据会话类型进行筛选。
                    • 呼入:客户主动发起的会话;
                    • 回呼:客服手动回呼的会话。

                    001.png

                    管理员模式机器人新手引导新增机器人新手引导功能。引导管理员在创建机器人时,对机器人的基础信息、自动回复、知识规则、自定义菜单进行设置,并通过知识库测试对知识规则和自定义菜单进行优化。该新手引导用于帮助客户更好地使用机器人功能,对初次使用移动客服系统的客户和开通多机器人功能的客户均有效。 

                    002.png

                    机器人知识库测试新增机器人知识库测试功能。管理员可以随时与机器人对话,测试机器人的回复,快速优化知识规则和自定义菜单。知识库测试方法:1. 进入“管理员模式 > 智能机器人 > 机器人设置 > 知识规则”tab页签。 

                    003png.png

                    2. 点击右上角的“知识库测试”按钮,开始与机器人对话。对话窗口右侧显示机器人回复所匹配的知识规则或菜单,以及匹配率。 

                    004.png

                    3. 点击匹配到的规则,可以进入知识规则页面,对该条规则进行优化。当未匹配到知识规则时,可以快速添加知识规则。 

                    005.png

                    支持为待接入超时提示语添加留言按钮新增“为待接入超时提示语添加留言按钮”开关。当客户处于排队状态,无法及时得到接待时,允许客户发起留言(同时结束会话)。进入“管理员模式 > 设置 > 系统开关”页面,依次打开“待接入超时提醒”和“为待接入超时提示语添加留言按钮”开关,并设置超时提示语等。 

                    006.png

                    客户在网页聊天窗口和H5网页收到的超时提示语如下图所示。点击“留言”按钮后,即可进入留言页面进行留言。同时,会话自动结束。 

                    007.png

                    权限管理支持设置数据权限在权限管理页面,可以为管理员、自定义角色设置管理员模式下页面的数据权限,分为租户和技能组。
                    • 赋予角色某个页面“租户”级别的数据权限时,该角色对应的管理员可以查看并操作该页面的所有数据,包括所有技能组的数据。
                    • 赋予角色某个页面“技能组”级别的数据权限时,该角色对应的管理员只可以查看并操作该页面中自己所属技能组的数据。
                    管理员模式的以下页面支持设置数据权限:客户中心、历史会话、当前会话。 

                    008.png

                    Web插件(访客端)当前版本:v47.9在消息中显示留言按钮网页访客端支持在待接入超时提示语中显示“留言”按钮,方便等待中的客户主动发起留言(点击留言按钮后,会话自动结束)。前提:管理员在移动客服系统中“管理员模式 > 设置 > 系统开关”页面,打开“为待接入超时提示语添加留言按钮”开关。支持全屏显示图片网页访客端支持全屏显示图片。客户与客服聊天时,如果收到图片消息:
                    •  
                    • 桌面聊天窗口:点击图片,可将图片放大至浏览器全屏查看;
                    • H5网页:点击图片,可将图片放大至手机全屏查看。


                    第二通道支持图片

                    网页访客端内置第二通道功能,当IM消息通道(第一通道)出现短暂的消息发送失败的情况时,自动调用第二通道将客户消息发送至移动客服系统,确保客户的所有消息均能准时送达。

                    在之前的版本,web插件的第二通道仅支持发送文本消息;从该版本开始,第二通道支持发送文本、图片消息。

                    显示客服输入状态

                    网页访客端支持显示客服输入状态。客户通过桌面聊天窗口或H5网页与客服聊天时,可以查看客服的输入状态。

                    “显示客服输入状态”为增值服务,如需开通,请提供租户ID并联系环信商务经理。 

                    009.png


                    环信移动客服更新日志http://docs.easemob.com/cs/releasenote/5.18 

                    环信移动客服登陆地址http://kefu.easemob.com/ 收起阅读 »

                    环信聊天游客身份和正常用户身份的切换

                         最近搞环信聊天,需求是游客身份也可以进行聊天,当用户注册了我们的APP后也需要把游客身份切换过来进行聊天,首先我们的环信注册,登录全都放前段处理了,下面就按照我们的需求逻辑来如何切换游客。      1.APP用户的注册,也就注册环信,APP的登录返...
                    继续阅读 »
                     
                       最近搞环信聊天,需求是游客身份也可以进行聊天,当用户注册了我们的APP后也需要把游客身份切换过来进行聊天,首先我们的环信注册,登录全都放前段处理了,下面就按照我们的需求逻辑来如何切换游客。
                     
                       1.APP用户的注册,也就注册环信,APP的登录返回的有用户ID,这个时候并没有让他登录环信,只是保存了返回的ID,下面就是用ID来判断该用户是否注册过环信的依据
                     
                    大致说明一下,代码中用到一个类来保证uuid不会改变的状态,为防止app卸载后uuid的改变,我们把他存储到钥匙串里面来保存

                    下面用图来表示
                    我们先来看下整个身份切换实现的逻辑图

                    4861502-fa2f7d87d00c78d7.jpg



                    下面就上代码了,第一步从图中第一步来说判断userID是否存在

                    这个地方是在点击聊天按钮开始判断的
                     
                    -(void)releaseInfo:(UIButton*)sender{
                    NSString*Hxusername=[userdic objectForKey:@"useid"];//获取保存的userID
                    NSString*phonestr=  [[NSUserDefaults standardUserDefaults]objectForKey:@"phonenum"];
                    NSString*chatid=[[phonestr md5String]substringFromIndex:16];//这个是获取客服的欢信ID
                    //单例里面处理用户是否登录,以及游客随机分配uuid来注册环信IM号
                    DataManager*datamage= [DataManager shareDataManager];
                    //判断用户ID是否存在,也就证明是否注册过环信
                    if (Hxusername.length>0) {
                    if ([datamage loginKefuSDK])//判断用户是否登录
                    {//单聊
                    ChattingViewController *chatController = [[ChattingViewController alloc] initWithConversationChatter:chatid conversationType:EMConversationTypeChat];[self.navigationController pushViewController:chatController animated:YES];
                    }
                    }else{
                    //游客身份的判断
                    if ([datamage customelogin]) {
                    //单聊
                    ChattingViewController *chatController = [[ChattingViewController alloc] initWithConversationChatter:chatid conversationType:EMConversationTypeChat];[self.navigationController pushViewController:chatController animated:YES];
                    }
                    }
                    }上面这是按钮方法里面的数据下面来说,DataManager*datamage= [DataManager shareDataManager];这个单利的方法
                    DataManager.h
                    @interface DataManager : NSObject
                    -(BOOL)customelogin;//判断游客之前是否有登录
                    -(void)requestchattphone;//获取美容院客服聊天的对象电话
                    @end
                    DataManager.m


                    @implementation DataManager
                    +(instancetype)shareDataManager{
                    static DataManager *manager;
                    static dispatch_once_t onceToken;
                    dispatch_once(&onceToken, ^{
                    manager = [[DataManager alloc] init];
                    });
                    return manager;
                    }
                    //userID存在的时候 登录IM
                    - (BOOL)loginKefuSDK {
                    NSDictionary*userdic=[[NSUserDefaults standardUserDefaults]objectForKey:@"userMessage"];//接受用户是否登录
                    NSString*loguser=[NSString stringWithFormat:@"%@",[userdic objectForKey:@"useid"] ];
                    EMClient *client = [EMClient sharedClient];
                    //用户已经登录
                    if (client.isLoggedIn) {
                    if ([loguser isEqualToString:client.currentUsername])//当前登录用户的ID和即将要登录人的ID是否一样
                    {
                    return YES;
                    }else
                    {
                    EMError *error = [[EMClient sharedClient] logout:YES];
                    if (!error) {
                    NSLog(@"退出成功");
                    }
                    }
                    }//这里APP用户登录环信的密码统统是123456
                    EMError *error = [[EMClient sharedClient] loginWithUsername:loguser password:@"123456"];
                    if (!error) { //IM登录成功
                    return YES;
                    } else { //登录失败
                    NSLog(@"登录失败 error code :%d,error description:%@",error.code,error.errorDescription);
                    return NO;
                    }
                    return NO;
                    }
                    //游客身份的登录方法
                    -(BOOL)customelogin
                    {
                    EMClient *client = [EMClient sharedClient];
                    //用户已经登录
                    if (client.isLoggedIn) {
                    return YES;
                    }//该用户没有注册,来用改设备UUID来给用户注册环信,并登录环信
                    if (![self registerIMuser]) {
                    return NO;
                    }
                    EMError *error = [[EMClient sharedClient] loginWithUsername:self.Hxusername password:@"123456"];
                    if (!error) { //IM登录成功
                    return YES;
                    } else { //登录失败
                    NSLog(@"登录失败 error code :%d,error description:%@",error.code,error.errorDescription);
                    return NO;
                    }
                    return NO;
                    }
                    - (BOOL)registerIMuser { //举个栗子。注册建议在服务端创建环信id与自己app的账号一一对应,\
                    而不要放到APP中,可以在登录自己APP时从返回的结果中获取环信账号再登录环信服务器
                    EMError *error = nil;
                    NSString *newUser = [self getrandomUsername];
                    self.Hxusername = newUser;
                    error = [[EMClient sharedClient] registerWithUsername:newUser password:@"123456"];
                    if (error &&  error.code != EMErrorUserAlreadyExist) {
                    NSLog(@"注册失败;error code:%d,error description :%@",error.code,error.errorDescription);
                    return NO;
                    }return YES;
                    }
                    //创建一个随机的用户名,这里是设备UUID来代替的​
                    - (NSString *)getrandomUsername {
                    //第一种方法:
                    /*NSString *username = nil;
                    UIDevice *device = [UIDevice currentDevice];//创建设备对象
                    NSString *deviceUID = [[NSString alloc] initWithString:[[device identifierForVendor] UUIDString]];
                    if ([deviceUID length] == 0) {
                    CFUUIDRef uuid = CFUUIDCreate(NULL);
                    if (uuid)
                    {
                    deviceUID = (__bridge_transfer NSString *)CFUUIDCreateString(NULL, uuid);
                    CFRelease(uuid);
                    }
                    username = [deviceUID stringByReplacingOccurrencesOfString:@"-" withString:@""];
                    username = [username stringByAppendingString:[NSString stringWithFormat:@"%u",arc4random()0000]];
                    return username;*/
                    //第二种方法
                    //加上build ID是为了保证设备的唯一性,如果这里的buildID换了,设备的uuid也会变,这里的解决办法也就是放倒了钥匙串里面,不会因卸载程序,程序升级设备的标识会改变
                    NSString *SERVICE_NAME = NAVI_TEST_BUNDLE_ID;//最好用程序的bundle id
                    NSString * str =  [SFHFKeychainUtils getPasswordForUsername:@"UUID" andServiceName:SERVICE_NAME error:nil];  // 从keychain获取数据
                    if ([str length]<=0)
                    {
                    str  = [[[UIDevice currentDevice] identifierForVendor] UUIDString];  // 保存UUID作为手机唯一标识符[SFHFKeychainUtils storeUsername:@"UUID"   andPassword:str    forServiceName:SERVICE_NAME updateExisting:1  error:nil];  // 往keychain添加数据
                    }
                    str = [str stringByReplacingOccurrencesOfString:@"-" withString:@""];
                    return str;
                    }在这里用到了一个类来处理的UUID不变(APP卸载后不会改变)
                    SFHFKeychainUtils.h
                    #import@interface SFHFKeychainUtils : NSObject
                    + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
                    + (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error;
                    + (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
                    @end
                    SFHFKeychainUtils.m​
                    #import "SFHFKeychainUtils.h"
                    static NSString *SFHFKeychainUtilsErrorDomain = @"SFHFKeychainUtilsErrorDomain";
                    #if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
                    @interface SFHFKeychainUtils (PrivateMethods)
                    + (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error;
                    @end
                    #endif
                    @implementation SFHFKeychainUtils
                    #if __IPHONE_OS_VERSION_MIN_REQUIRED < 30000 && TARGET_IPHONE_SIMULATOR
                    + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error { if (!username || !serviceName) { *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil]; return nil; }      SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];      if (*error || !item) { return nil; }
                    // from Advanced Mac OS X Programming, ch. 16
                    UInt32 length;
                    char *password;
                    SecKeychainAttribute attributes[8];
                    SecKeychainAttributeList list;
                    attributes[0].tag = kSecAccountItemAttr;
                    attributes[1].tag = kSecDescriptionItemAttr;
                    attributes[2].tag = kSecLabelItemAttr;
                    attributes[3].tag = kSecModDateItemAttr;
                    list.count = 4;
                    list.attr = attributes;
                    OSStatus status = SecKeychainItemCopyContent(item, NULL, &list, &length, (void **)&password);
                    if (status != noErr)
                    {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    return nil;
                    }
                    NSString *passwordString = nil;
                    if (password != NULL)
                    { char passwordBuffer[1024];
                    if (length > 1023) {length = 1023;
                    }
                    strncpy(passwordBuffer, password, length);
                    passwordBuffer[length] = '\0';
                    passwordString = [NSString stringWithCString:passwordBuffer];
                    }SecKeychainItemFreeContent(&list, password);CFRelease(item);return passwordString;
                    }
                    + (void) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error {
                    if (!username || !password || !serviceName) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
                    return;
                    }
                    OSStatus status = noErr;
                    SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
                    if (*error && [*error code] != noErr) {
                    return;
                    }
                    *error = nil;
                    if (item) {
                    status = SecKeychainItemModifyAttributesAndData(item,
                    NULL,
                    strlen([password UTF8String]),
                    [password UTF8String]);
                    CFRelease(item);
                    }
                    else {
                    status = SecKeychainAddGenericPassword(NULL,
                    strlen([serviceName UTF8String]),
                    [serviceName UTF8String],
                    strlen([username UTF8String]),
                    [username UTF8String],
                    strlen([password UTF8String]),
                    [password UTF8String],
                    NULL);
                    }
                    if (status != noErr) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    }
                    }
                    + (void) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
                    if (!username || !serviceName) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: 2000 userInfo: nil];
                    return;
                    }
                    *error = nil;
                    SecKeychainItemRef item = [SFHFKeychainUtils getKeychainItemReferenceForUsername: username andServiceName: serviceName error: error];
                    if (*error && [*error code] != noErr) {
                    return;
                    }
                    OSStatus status;
                    if (item) {
                    status = SecKeychainItemDelete(item);
                    CFRelease(item);
                    }
                    if (status != noErr) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    }
                    }
                    + (SecKeychainItemRef) getKeychainItemReferenceForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
                    if (!username || !serviceName) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
                    return nil;
                    }
                    *error = nil;
                    SecKeychainItemRef item;
                    OSStatus status = SecKeychainFindGenericPassword(NULL,
                    strlen([serviceName UTF8String]),
                    [serviceName UTF8String],
                    strlen([username UTF8String]),
                    [username UTF8String],
                    NULL,
                    NULL,
                    &item);
                    if (status != noErr) {
                    if (status != errSecItemNotFound) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    }
                    return nil;
                    }
                    return item;
                    }
                    #else
                    + (NSString *) getPasswordForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error {
                    if (!username || !serviceName) {
                    if (error != nil) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
                    }
                    return nil;
                    }
                    if (error != nil) {
                    *error = nil;
                    }
                    // Set up a query dictionary with the base query attributes: item type (generic), username, and service
                    NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, kSecAttrAccount, kSecAttrService, nil];
                    NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, username, serviceName, nil];
                    NSMutableDictionary *query = [[NSMutableDictionary alloc] initWithObjects: objects forKeys: keys];
                    // First do a query for attributes, in case we already have a Keychain item with no password data set.
                    // One likely way such an incorrect item could have come about is due to the previous (incorrect)
                    // version of this code (which set the password as a generic attribute instead of password data).
                    NSMutableDictionary *attributeQuery = [query mutableCopy];
                    [attributeQuery setObject: (id) kCFBooleanTrue forKey:(__bridge_transfer id) kSecReturnAttributes];
                    CFTypeRef attrResult = NULL;
                    OSStatus status = SecItemCopyMatching((__bridge_retained CFDictionaryRef) attributeQuery, &attrResult);
                    //NSDictionary *attributeResult = (__bridge_transfer NSDictionary *)attrResult;
                    if (status != noErr) {
                    // No existing item found--simply return nil for the password
                    if (error != nil && status != errSecItemNotFound) {
                    //Only return an error if a real exception happened--not simply for "not found."
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    }
                    return nil;
                    }
                    // We have an existing item, now query for the password data associated with it.
                    NSMutableDictionary *passwordQuery = [query mutableCopy];
                    [passwordQuery setObject: (id) kCFBooleanTrue forKey: (__bridge_transfer id) kSecReturnData];
                    CFTypeRef resData = NULL;
                    status = SecItemCopyMatching((__bridge_retained CFDictionaryRef) passwordQuery, (CFTypeRef *) &resData);
                    NSData *resultData = (__bridge_transfer NSData *)resData;
                    if (status != noErr) {
                    if (status == errSecItemNotFound) {
                    // We found attributes for the item previously, but no password now, so return a special error.
                    // Users of this API will probably want to detect this error and prompt the user to
                    // re-enter their credentials.  When you attempt to store the re-entered credentials
                    // using storeUsername:andPassword:forServiceName:updateExisting:error
                    // the old, incorrect entry will be deleted and a new one with a properly encrypted
                    // password will be added.
                    if (error != nil) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil];
                    }
                    }
                    else {
                    // Something else went wrong. Simply return the normal Keychain API error code.
                    if (error != nil) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    }
                    }
                    return nil;
                    }
                    NSString *password = nil;
                    if (resultData) {
                    password = [[NSString alloc] initWithData: resultData encoding: NSUTF8StringEncoding];
                    }
                    else {
                    // There is an existing item, but we weren't able to get password data for it for some reason,
                    // Possibly as a result of an item being incorrectly entered by the previous code.
                    // Set the -1999 error so the code above us can prompt the user again.
                    if (error != nil) {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -1999 userInfo: nil];
                    }
                    }
                    return password;
                    }
                    + (BOOL) storeUsername: (NSString *) username andPassword: (NSString *) password forServiceName: (NSString *) serviceName updateExisting: (BOOL) updateExisting error: (NSError **) error
                    {
                    if (!username || !password || !serviceName)
                    {
                    if (error != nil)
                    {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
                    }
                    return NO;
                    }
                    // See if we already have a password entered for these credentials.
                    NSError *getError = nil;
                    NSString *existingPassword = [SFHFKeychainUtils getPasswordForUsername: username andServiceName: serviceName error:&getError];
                    if ([getError code] == -1999)
                    {
                    // There is an existing entry without a password properly stored (possibly as a result of the previous incorrect version of this code.
                    // Delete the existing item before moving on entering a correct one.
                    getError = nil;
                    [self deleteItemForUsername: username andServiceName: serviceName error: &getError];
                    if ([getError code] != noErr)
                    {
                    if (error != nil)
                    {
                    *error = getError;
                    }
                    return NO;
                    }
                    }
                    else if ([getError code] != noErr)
                    {
                    if (error != nil)
                    {
                    *error = getError;
                    }
                    return NO;
                    }
                    if (error != nil)
                    {
                    *error = nil;
                    }
                    OSStatus status = noErr;
                    if (existingPassword)
                    {
                    // We have an existing, properly entered item with a password.
                    // Update the existing item.
                    if (![existingPassword isEqualToString:password] && updateExisting)
                    {
                    //Only update if we're allowed to update existing.  If not, simply do nothing.
                    NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass,
                    kSecAttrService,
                    kSecAttrLabel,
                    kSecAttrAccount,
                    nil];
                    NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword,
                    serviceName,
                    serviceName,
                    username,
                    nil];
                    NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];
                    status = SecItemUpdate((__bridge_retained CFDictionaryRef) query, (__bridge_retained CFDictionaryRef) [NSDictionary dictionaryWithObject: [password dataUsingEncoding: NSUTF8StringEncoding] forKey: (__bridge_transfer NSString *) kSecValueData]);
                    }
                    }
                    else
                    {
                    // No existing entry (or an existing, improperly entered, and therefore now
                    // deleted, entry).  Create a new entry.
                    NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass,
                    kSecAttrService,
                    kSecAttrLabel,
                    kSecAttrAccount,
                    kSecValueData,
                    nil];
                    NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword,
                    serviceName,
                    serviceName,
                    username,
                    [password dataUsingEncoding: NSUTF8StringEncoding],
                    nil];
                    NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];
                    status = SecItemAdd((__bridge_retained CFDictionaryRef) query, NULL);
                    }
                    if (error != nil && status != noErr)
                    {
                    // Something went wrong with adding the new item. Return the Keychain error code.
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    return NO;
                    }
                    return YES;
                    }
                    + (BOOL) deleteItemForUsername: (NSString *) username andServiceName: (NSString *) serviceName error: (NSError **) error
                    {
                    if (!username || !serviceName)
                    {
                    if (error != nil)
                    {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: -2000 userInfo: nil];
                    }
                    return NO;
                    }
                    if (error != nil)
                    {
                    *error = nil;
                    }
                    NSArray *keys = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClass, kSecAttrAccount, kSecAttrService, kSecReturnAttributes, nil];
                    NSArray *objects = [[NSArray alloc] initWithObjects: (__bridge_transfer NSString *) kSecClassGenericPassword, username, serviceName, kCFBooleanTrue, nil];
                    NSDictionary *query = [[NSDictionary alloc] initWithObjects: objects forKeys: keys];
                    OSStatus status = SecItemDelete((__bridge_retained CFDictionaryRef) query);
                    if (error != nil && status != noErr)
                    {
                    *error = [NSError errorWithDomain: SFHFKeychainUtilsErrorDomain code: status userInfo: nil];
                    return NO;
                    }
                    return YES;
                    }
                    #endif
                    @end
                    收起阅读 »

                    【环信征文】在android中5分钟实现环信昵称头像的显示

                    老司机带你们5分钟实现昵称头像的显示,车要开了,话不多说,快快上车~ 一、将简版demo里的cache包(5个java文件)复制到自己项目里。 下载环信android简版Demo: 环信Android简版DEMO 昵称头像用到的工具类、mode...
                    继续阅读 »
                    老司机带你们5分钟实现昵称头像的显示,车要开了,话不多说,快快上车~


                    23.jpg



                    一、将简版demo里的cache包(5个java文件)复制到自己项目里。
                    下载环信android简版Demo:
                    环信Android简版DEMO

                    昵称头像用到的工具类、model都在这个cache包里。 

                    1.png


                    类介绍:

                    2.png



                    二、显示昵称头像有两种解决方案:
                    1. 从APP服务器获取;
                    2. 从消息扩展属性中获取。


                    接下来,我们就以官方demo为例,一步步教大家实现昵称头像的显示。
                    无论哪种方案,先要设置3步:
                    1.更改环信appkey;
                    在AndroidManifest.xml中更改appkey,改为【xmxx#xmxx】,如下所示。 

                    android:name="EASEMOB_APPKEY"
                    android:value=“xmxx#xmxx" />

                    2.增加第三方依赖库。根目录下的 build.gradle 下:
                    compile 'com.j256.ormlite:ormlite-android:5.0'
                    compile 'com.google.code.gson:gson:2.8.0'
                    ormlite:操作sqlite数据库
                    gson:json对象转换 

                    3.设置用户信息提供者:
                    在DemoHelper.java的getUserInfo函数里(第824行)增加如下代码: 


                    环信头像昵称显示使用的是提供者模式(EaseUserProfileProvider),只要设置了用户信息提供者(setUserProfileProvider),EaseUI界面里显示用户昵称和头像时,就会调用这个getUserInfo函数。


                    // 从本地缓存中获取用户昵称头像
                    EaseUser user = UserCacheManager.getEaseUser(username);

                    并且注释之前获取昵称头像的方法:
                    // EaseUser user = null;
                    // if(username.equals(EMClient.getInstance().getCurrentUser()))
                    // return getUserProfileManager().getCurrentUserInfo();
                    // user = getContactList().get(username);
                    // if(user == null && getRobotList() != null){
                    // user = getRobotList().get(username);
                    // }

                    然后,根据不同的方案,开发者可以选择不同步骤:

                    三、从开发者自己的APP服务器获取的步骤:
                    将UserCacheManager.java中第54行-62行代码,换成:通过okhttp(或者retrofit、volley)调用api接口,根据用户环信ID,从开发app服务器获取用户昵称头像。下面两张图是改之前和改之后的效果:
                     
                    改之前(用第三方云存储):

                    1111.png



                    改之后(用开发者自己服务器):

                    2222.png


                     

                    四、从第三方后端云存储获取的步骤:

                    1.配置后端云服务器;
                    (注意:用开发者APP服务器存储昵称头像可以忽略这一步):但是必须直接实现调用API从开发者服务器中获取昵称头像。
                    首先打开根目录下的 build.gradle 进行如下标准配置
                    buildscript {    
                    repositories {
                    jcenter() //这里是 LeanCloud 的包仓库
                    maven {
                    url "http://mvn.leancloud.cn/nexus/ ... ot%3B
                    }
                    }

                    dependencies {
                    classpath 'com.android.tools.build:gradle:1.0.0'
                    }
                    }

                    allprojects {
                    repositories {
                    jcenter() //这里是 LeanCloud 的包仓库
                    maven { url "http://mvn.leancloud.cn/nexus/ ... ot%3B }
                    }
                    }
                    继续在根目录下的build.gradle增加如下配置:
                    android {    
                    //为了解决部分第三方库重复打包了META-INF的问题
                    packagingOptions{
                    exclude 'META-INF/LICENSE.txt'
                    exclude 'META-INF/NOTICE.txt'
                    }
                    lintOptions {
                    abortOnError false
                    }
                    }
                    dependencies {
                    // LeanCloud 基础包
                    compile ('cn.leancloud.android:avoscloud-sdk:v3.+'
                    )
                    }
                    注意:以上代码,由于论坛里的编辑器会产生乱码,所以建议去【环信Android简版Demo】里复制。或者直接打开:
                    https://github.com/mengmakies/ChatDemo-UI3.00-Simple-Android/blob/master/examples/ChatDemoUI3.0/build.gradle​ 


                     3.在DemoApplication.java第49行初始化后端云。(注意:用开发者APP服务器存储昵称头像可以忽略这一步)
                    UserWebManager.config(this,"VBYeQuiVPD9CWS2aINWrwBv0-gzGzoHsz","1LItewi0x6hlkgYHxi8DERNN");
                     
                    4.在环信IM登录成功后,在后端云里新增用户。
                    参考LoginActivity.java第173行代码。(注意:用开发者APP服务器存储昵称头像可以忽略这一步) 
                    // 登录成功后,如果后端云没有缓存用户信息,则新增一个用户
                    UserWebManager.createUser(userId, nickName, avatarUrl);

                    如果需要看到完整的效果,可以用以下随机用户昵称头像的生成代码:
                    Random random=new Random();
                    String userId = EMClient.getInstance().getCurrentUser();// 用户环信ID
                    String nickName = String.format("小草%d",random.nextInt(10000));// 用户昵称
                    String avatarUrl = String.format("http://duoroux.com/chat/avatar/%d.jpg",random.nextInt(10));// 用户头像(绝对路

                    编译运行。这样就可以用方案一从APP服务端获取昵称头像了。

                    五、从消息扩展属性中获取昵称头像。
                    1.首先,要注释从APP服务器获取昵称头像的方法。
                    删除类文件:UserWebInfo.java和UserWebManager.java。
                    UserCacheManager.java中注释第57-68行,不从APP服务器中获取昵称头像: 
                    // 如果本地缓存不存在或者过期,则从存储服务器获取
                    if (notExistedOrExpired(userId)){
                    UserWebManager.getUserInfoAync(userId, new UserWebManager.UserCallback() {
                    @Override
                    public void onCompleted(UserWebInfo info) {
                    if(info == null) return;

                    // 缓存到本地
                    save(userId, info.getNickName(),info.getAvatarUrl());
                    }
                    });
                    }
                    2.登录(或注册)成功后,需要缓存当前用户的昵称头像。
                    在登录(或注册)服务端回调(不是环信IM登录回调)里,增加如下代码: 
                    // 登录成功,将用户的环信ID、昵称和头像缓存在本地
                    UserCacheManager.save(userId, nickName, avatarUrl);

                    如果需要看到完整的效果,可以用以下随机用户昵称头像的生成代码:
                    Random random=new Random();
                    String userId = EMClient.getInstance().getCurrentUser();// 用户环信ID
                    String nickName = String.format("小草%d",random.nextInt(10000));// 用户昵称
                    String avatarUrl = String.format("http://duoroux.com/chat/avatar/%d.jpg",random.nextInt(10));// 用户头像(绝对路径)
                    3.发送消息时携带昵称头像。
                    ChatFragment.java里的 onSetMessageAttributes函数。第231行增加代码:
                    // 设置消息的扩展属性,携带昵称头像
                    UserCacheManager.setMsgExt(message);

                    4.接收消息时携带昵称头像。
                    DemoHelper.java里的 onMessageReceived函数。第856行增加代码:
                    // 从消息的扩展属性里获取昵称头像
                    UserCacheManager.save(message.ext());


                    5.另外。音视频通话里,昵称头像也要进行处理。(不需要音视频通话功能的开发者可以省略后面所有步骤)
                    发送音视频通话请求时携带昵称头像。
                    CallActivity.java里第162行代码更改为: 
                    // 通过扩展属性将昵称头像传给对方
                    String ext = UserCacheManager.getMyInfoStr();
                    if (msg.what == MSG_CALL_MAKE_VIDEO) {
                    EMClient.getInstance().callManager().makeVideoCall(username,ext);
                    } else {
                    EMClient.getInstance().callManager().makeVoiceCall(username,ext);
                    }

                    6.接收音视频通话时保存昵称头像。
                    CallReceiver.java第33行增加代码: 
                    // 缓存用户昵称头像
                    String ext = EMClient.getInstance().callManager().getCurrentCallSession().getExt();
                    UserCacheManager.save(ext);

                    7.音频通话里显示昵称头像。
                    VoiceCallActivity.java第114行代码改为: 
                    // 显示昵称头像
                    UserCacheInfo user = UserCacheManager.get(username);
                    if (user != null){
                    nickTextView.setText(user.getNickName());
                    //Glide.with(VoiceCallActivity.this).load(user.getAvatarUrl()).placeholder(R.drawable.em_default_avatar).into(avatarImage);
                    }else {
                    nickTextView.setText(username);
                    }


                    8.视频通话里显示昵称头像。
                    VideoCallActivity.java第171行代码改为: 
                    // 显示昵称头像
                    UserCacheInfo user = UserCacheManager.get(username);
                    if (user != null){
                    nickTextView.setText(user.getNickName());
                    }else {
                    nickTextView.setText(username);
                    }

                    编译运行即可使用扩展消息的方式实现昵称头像显示了!
                    可以通过这个web页面发送消息测试是否设置成功:
                    http://duoroux.com/chat 

                    测试账号:
                    1.环信ID:biaoge  密码:biaoge
                    2.环信ID:biaomei  密码:biaomei

                    两种方案的完整代码,大家可以下载环信android简版Demo:
                    https://github.com/mengmakies/ChatDemo-UI3.00-Simple-Android 

                    IOS版简版demo下载地址:
                    https://github.com/mengmakies/ChatDemo-UI3.00-Simple 


                    如有任何问题,请咨询【环信IM互帮互助群】,群号:340452063 


                    注意事项


                    将此方案集成到自己项目时,要把SqliteHelper构造函数里的“DemoApplication.getInstance()”换成自己项目的应用程序类实例!




                      收起阅读 »

                    接私活发现甲方比自己还穷是什么样的体验

                    最近关于接私活的文章不少,有赞成私活的,也有反对私活的,无论怎样,接私活成了最近最火的话题。我来给大家分享一个新鲜出炉的接私活故事: 故事开始之前,我先教大家一个获取黄网的办法:和边检缉毒专查面黄肌瘦,神情萎靡,眼神游离的人一样,你观察你身边的朋友谁穷得买...
                    继续阅读 »
                    最近关于接私活的文章不少,有赞成私活的,也有反对私活的,无论怎样,接私活成了最近最火的话题。我来给大家分享一个新鲜出炉的接私活故事:


                    故事开始之前,我先教大家一个获取黄网的办法:和边检缉毒专查面黄肌瘦,神情萎靡,眼神游离的人一样,你观察你身边的朋友谁穷得买不起毒品却有和瘾君子一样的肾虚症状,那么他一定有资源。

                    昨天我从我的diao丝同事那里要来一个打擦边球的公众号,有图有文没视频那种。忽然在一篇小黄图文下面看见这么一行字:“如有能力制作开发本平台APP的个人或公司,请联系QQ:***,具体事宜详谈!”

                    我:(真没想到有机会接黄站的私活,赶紧加Q)需要Android外包?

                    他:(没错,尽管资料填“女”,但我仍能看出他是抠脚大汉)我就要简单的社区 + 相册,相册是自己的作品集

                    我:资料发我,我估个价,再找iOS同事给你做个iOS的

                    他:APP目前的功能就是自己的作品集

                    他:然后社区

                    我:(根据公众号给的链接打开了网站的手机版,然后开电脑看了电脑版,是一个基于discuzz的擦边球社区)看了网站手机版感觉做成APP难度不大

                    他:大概要多少钱?

                    我:(发了一份报价单)Android估价2000左右,最高不超过3000,iOS的价格也差不多

                    他:好贵呀

                    我:你知道一个程序员每天多少钱工资吗?

                    他:我每个月没钱吃饭啊

                    他:(过了一会)相册和怎么做成那种看12章以后要会员 才能看?或这收费

                    我:(连这个都不会弄,还能给终端提供API)我和你家技术谈谈吧

                    他:(开启连珠炮模式)米技术啊

                    他:就我和我姐妹弄的

                    他:哪有技术

                    他:全部自己学的

                    他:网站都是自己装的

                    我:我业余搞个外包容易吗?咋遇到的发包方个个比我还穷?

                    他:那肿么办?

                    他:袜子都买不起了

                    我:(他缺的不是Android和iOS上的两个软件,而是一整套技术体系)我有个免费的方案,用HBuilder打包你的手机站,这样Android和iOS的APP就都有了,零成本

                    他:作为粉丝的你不支持下我啊

                    真没想到找私活的甲方比程序员还穷,让我免费做个APP真是讹上我了,瞬间觉得高中时班主任语文老师的顺口溜“上辈子杀猪,这辈子教书;上辈子杀人,这辈子教语文”有了下联:“上辈子叛党,这辈子搞互联网;上辈子卖国,这辈子搞安卓”

                    第二天,那个公众号推送的小黄图文下面有这么一行字:“有木有洞APP开发架设的粉丝,如果有请自告奋勇联系小编,当然惊喜肯定给足” 收起阅读 »

                    【环信公开课第12期视频回放】-所有关于环信IM昵称头像的问题听这课就够了

                     ​ 青年的思想愈被榜样的力量所激动,就愈会发出强烈的光辉-法捷耶夫   在刚刚过去的五四青年节,环信公开课第12期如期举行,这期公开课嘉宾“环信大表哥”一人手写三端,Android/IOS/服务端信手捏来,关于用户体系更是引经据典一番,整场公开课妙语连珠、妙...
                    继续阅读 »
                     
                    ​ 青年的思想愈被榜样的力量所激动,就愈会发出强烈的光辉-法捷耶夫

                      在刚刚过去的五四青年节,环信公开课第12期如期举行,这期公开课嘉宾“环信大表哥”一人手写三端,Android/IOS/服务端信手捏来,关于用户体系更是引经据典一番,整场公开课妙语连珠、妙趣横生。加上环信MM的现场答疑,这样一来便消除了拘谨,活跃了气氛,环信小伙伴们在欢快愉悦的气氛中度过了这个有意义的五四青年节! 

                    环信公开课12期讲了什么?
                    1. 如何利用消息扩展属性显示昵称头像?
                    2. 如何通过APP服务器处理昵称头像的显示?
                    3. 昵称头像的本地缓存策略?
                    4. 音视频通话如何显示昵称头像?


                    关于环信大表哥:


                       马骏斌丨美国海遇网络CTO,传说中的祖传CTO“环信大表哥”,10年研发经验,现任美国海遇网络CTO。曾就职于北京超图从事GIS研发,负责基于SuperMap平台研究GIS算法以及图形处理工具,优化底层三维渲染引擎,协助产品研发进行数据处理或空间分析工作。

                    环信简版demo作者,开源了基于环信的直播项目-小马直播间,在imgeek、简书等社区发表了数十篇环信教程,他的教程和开源项目累计帮助了超过1W多名开发者集成环信,自己创建了环信互帮互助群,每天"夜黑风高"活跃在群里回答问题+远程写代码,人送外号“环信大表哥”。
                     
                    自2011年2月创办www.C3DN.net(中国3D技术开发者社区),并兼任C3DN站长。创办C3DN,不仅为了延续对3D技术的热爱,最主要是想帮助更多需要帮助的人学习3D开发技术;



                    先放一张大表哥PPT截图,这个style!你们感(la)受(yan)下(jing)。

                    QQ截图20170508115114.jpg



                    环信公开课视频回放:视频观看地址
                      
                    环信公开课第12期讲师PPT在文末下载,最后引用一句某知名互联网公司创始人名言致大表哥“百年之后,你我的肉身终将陨灭,而我们的精神和梦想依然可以在代码中相见
                    ios简版demo地址
                    Android简版demo地址 收起阅读 »

                    【环信3.0SDK集成小米推送教程】实现离线消息推送和后台视频电话通知

                    教程所用到的DEMO源码地址:【lzan13 / VMChatDemoCall】 前言   从APP有了聊天功能起,就是为了让用户更畅快的沟通,但有的时候,用户将APP退到了后台,甚至kill掉程序(术语:划掉了应用进程),这种情况下再有消息过来或者有视频...
                    继续阅读 »


                    教程所用到的DEMO源码地址:【lzan13 / VMChatDemoCall


                    前言

                      从APP有了聊天功能起,就是为了让用户更畅快的沟通,但有的时候,用户将APP退到了后台,甚至kill掉程序(术语:划掉了应用进程),这种情况下再有消息过来或者有视频通话请求就不能再走之前的聊天通道了,所以就要用到我们今天的主角-推送。苹果手机自带了apns,上传推送证书到环信后台就可以实现ios手机的消息推送,Android虽然也有gcm, 但是在大陆地区是不能正常使用的(海外APP不受影响),那么在国内Android的APP就需要用到第三方推送,环信调研了市场设备情况,选择集成了两家厂商推送,分别是小米推送和华为推送,最大程度保证了应用在后台被杀死的情况下也收到离线消息的通知。


                    banner.jpg



                    废话不多说,今天就通过集成最新的小米推送来实现下消息的离线推送通知,以及被呼叫方离线时方推送提醒对方启动 app 接听通话;其实都是通过集成推送完成!

                    准备工作

                    首先你的项目需要集成环信 sdk,并且已经实现了发送消息以及音视频通话功能(这个可以直接用我上边 github 上的项目);
                    然后你需要有小米的开发者账户,需要创建一个应用,包名要和你自己的项目一样,然后需要用到的就是应用的appId、appKey、appSecret,这些在环信开发者后台上传小米证书,以及在项目中初始化小米推送需要用到;

                    开始集成

                    首先这边先把证书弄好了,证书的名字和秘钥以及包名一定要对应:

                    QQ20170505-162024.png


                     
                    然后需要做的就是在代码中集成小米推送,需要做的有两个地方:

                    在初始化 sdk 的时候调用 options 设置小米的 appId 和 appKey
                    在 AndroidManifest配置文件配置相应的权限和广播接收器以及服务
                        /**
                    * 初始化环信sdk,并做一些注册监听的操作,这里把其他的处理都去掉了只写了小米推送
                    */
                    private void initHyphenate() {
                    // 初始化sdk的一些配置
                    EMOptions options = new EMOptions();
                    // 设置小米推送 appID 和 appKey
                    options.setMipushConfig("2882303761517573806", "5981757315806");
                    // 初始化环信SDK,一定要先调用init()
                    EMClient.getInstance().init(context, options);
                    // 开启 debug 模式
                    EMClient.getInstance().setDebugMode(true);
                    }

                    然后就是AndroidManifest配置
                    <?xml version="1.0" encoding="utf-8"?>
                    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
                    package="com.vmloft.develop.app.demo.call">

                    <!-- 项目权限配置 -->
                    <!--小米推送相关权限-->
                    <permission
                    android:name="com.vmloft.develop.app.demo.call.permission.MIPUSH_RECEIVE"
                    android:protectionLevel="signature"/>
                    <uses-permission android:name="com.vmloft.develop.app.demo.call.permission.MIPUSH_RECEIVE"/>
                    <!--小米推送权限 end-->
                    <!--程序入口-->
                    <application
                    android:name="com.vmloft.develop.app.demo.call.AppApplication"
                    android:allowBackup="true"
                    android:icon="@mipmap/ic_launcher"
                    android:label="@string/app_name"
                    android:largeHeap="true"
                    android:supportsRtl="true"
                    android:theme="@style/AppTheme">
                    ...
                    <!--小米推送相关配置-->
                    <service
                    android:name="com.xiaomi.push.service.XMJobService"
                    android:enabled="true"
                    android:exported="false"
                    android:permission="android.permission.BIND_JOB_SERVICE"
                    android:process=":pushservice"/>

                    <service
                    android:name="com.xiaomi.push.service.XMPushService"
                    android:enabled="true"
                    android:process=":pushservice"/>

                    <service
                    android:name="com.xiaomi.mipush.sdk.PushMessageHandler"
                    android:enabled="true"
                    android:exported="true"/>
                    <service
                    android:name="com.xiaomi.mipush.sdk.MessageHandleService"
                    android:enabled="true"/>

                    <!--推送消息广播接收器-->
                    <receiver
                    android:name=".push.MIPushReceiver"
                    android:exported="true">
                    <intent-filter>
                    <action android:name="com.xiaomi.mipush.RECEIVE_MESSAGE"/>
                    </intent-filter>
                    <intent-filter>
                    <action android:name="com.xiaomi.mipush.MESSAGE_ARRIVED"/>
                    </intent-filter>
                    <intent-filter>
                    <action android:name="com.xiaomi.mipush.ERROR"/>
                    </intent-filter>
                    </receiver>
                    <receiver
                    android:name="com.xiaomi.push.service.receivers.NetworkStatusReceiver"
                    android:exported="true">
                    <intent-filter>
                    <action android:name="android.net.conn.CONNECTIVITY_CHANGE"/>

                    <category android:name="android.intent.category.DEFAULT"/>
                    </intent-filter>
                    </receiver>
                    <receiver
                    android:name="com.xiaomi.push.service.receivers.PingReceiver"
                    android:exported="false"
                    android:process=":pushservice">
                    <intent-filter>
                    <action android:name="com.xiaomi.push.PING_TIMER"/>
                    </intent-filter>
                    </receiver>
                    <!--小米推送配置 end-->
                    </application>
                    </manifest>










                    其中MIPushReceiver这个广播接收器可以不用自己实现,环信 sdk 已经集成小米广播接收器EMMipushReceiver实现,可以直接用(这里如果需要自己与自己的业务处理可以继承它去处理自己的逻辑;详细可以根据小米推送官方 sdk 文档进行了解下);

                    当我们做完这些之后在收到离线消息后就可以收到推送通知了,只不过这个推送通知我们不能自定义,因为这些都是服务器推什么我们接受什么,这点比较坑!

                    通话的离线通知

                    上边已经实现了消息的离线通知,我们下边就要做当呼叫对方时,对方却不在线,我们怎么通知对方打开 app 进行接听呢?
                    曾经集成过环信用户应该知道,在呼叫对方不在线后会马上结束通话,回调对方不在线,在新版3.2.2的 sdk 中新增设置音视频参数及呼叫时对方离线是否发推送的接口,在初始化的时候进行以下设置:
                    // 设置通话过程中对方如果离线是否发送离线推送通知,默认 false
                    EMClient.getInstance().callManager().getCallOptions().setIsSendPushIfOffline(true);
                    // 设置了这个之后就不会在通话状态监听中回调对方不在线,需要实现另外一个回调
                    ...
                    // 设置音频通话推送提供者,在 onRemoteOffline()回调中给对方发送消息就行了
                    EMClient.getInstance().callManager().setPushProvider(EMCallManager.EMCallPushProvider {
                    @Override public void onRemoteOffline(String username) {
                    EMMessage message = EMMessage.createTxtSendMessage("有人呼叫你,开启 APP 接听吧", username);
                    // 设置强制推送
                    message.setAttribute("em_force_notification", "true");
                    // 设置自定义推送提示
                    JSONObject extObj = new JSONObject();
                    try {
                    extObj.put("em_push_title", "有人呼叫你,开启 APP 接听吧");
                    extObj.put("extern", "定义推送扩展内容");
                    } catch (JSONException e) {
                    e.printStackTrace();
                    }
                    message.setAttribute("em_apns_ext", extObj);
                    message.setMessageStatusCallback(new EMCallBack() {
                    @Override public void onSuccess() {
                    // 在这里可以删除消息
                    }
                    @Override public void onError(int i, String s) {
                    // 在这里可以删除消息
                    }
                    @Override public void onProgress(int i, String s) {}
                    });
                    EMClient.getInstance().chatManager().sendMessage(message);
                    }
                    });

                    实现了上边的这个推送提供者之后,当对方不在线就会回调 onRemoteOffline()方法,就可以发送一条消息给对方,然后上边我们已经集成了小米推送,就可以通过离线推送的方式通知对方有新消息,对方看到后点击通知栏就可以打开 app了,这个时候我们的语音或视频呼叫还在一直呼叫,然后就可以连通了!

                    结语

                    OK 到这里基本就已经完成了,大家可以运行自己的项目,或者我上边的 demo 测试下,我这边通过小米5测试 OK;其实集成推送部分并不难,只是有几点需要注意:

                    环信开发者后台的推送证书设置时一定要注意应用包名和小米推送后台的应用包名以及自己项目的包名,三个地方一定要一致
                    初始化设置一定要通过环信的 options 去设置小米推送的 appId 和 appKey,不需要用小米的注册方法自己注册;
                    Androidmanifest 一定要加上环信的广播接收器,或者继承自环信封装的广播接收器

                    注意以上几点基本推送就没有问题了,如果不行可以先通过小米开发者后台的推送工具测试推送是否通了,然后检查以上几点;


                    PS:华为推送相关其实一样,不过因为华为不允许个人开发者注册账户,所以这里暂时不赘述



                    参考资料

                    小米推送 Android SDK文档
                    环信推送相关文档收起阅读 »

                    环信移动客服v5.17已发布,客服可以通过名字、昵称搜索会话,查找客户更方便

                    客服模式 支持快速搜索客户会话 在进行中会话列表,支持通过客户的名字、昵称进行搜索,以便客服能够快速查找客户的会话。  管理员模式 支持设置上班时间才自动分配会话 新增“只有上班时间才自动分配会话”开关。开关打开时,上班时间开始自动调度会话...
                    继续阅读 »
                    客服模式

                    支持快速搜索客户会话


                    在进行中会话列表,支持通过客户的名字、昵称进行搜索,以便客服能够快速查找客户的会话。 


                    001.png


                    管理员模式

                    支持设置上班时间才自动分配会话


                    新增“只有上班时间才自动分配会话”开关。开关打开时,上班时间开始自动调度会话给客服,下班时间结束自动调度。该开关可以与“允许客服手动接入会话”开关配合使用,以便所有客服均只在上班时间接待会话,使工作量分配更加公平。

                    可以进入“管理员模式 > 设置 > 系统开关”页面,打开该开关。 

                    002.png


                    公共常用语支持全部删除

                    公共常用语支持全部删除,方便管理员使用模版批量添加常用语。 

                    003.png


                    在多个页面显示Iframe页签

                    在客服模式的会话、历史会话页面,管理员模式的历史会话、质量检查页面,均支持显示Iframe页签,方便已集成CRM系统的客户通过Iframe查看、编辑客户资料。

                    关于如何集成CRM系统,请查看CRM系统对接。 

                    004.png


                    【优化】筛选时支持对客服进行多选

                    在客服模式的留言页面,管理员模式的留言、历史会话、当前会话页面,对留言、会话进行筛选时,支持同时选择多名客服。
                    • 留言:点击“自定义留言筛选”,对留言进行筛选;
                    • 历史会话、当前会话:点击“筛选排序”,对会话进行筛选。


                    【优化】筛选时支持对渠道、关联进行多选

                    管理员模式下,在统计查询的工作量、工作质量页面,对统计数据进行筛选时,支持同时选择多条渠道、多个关联。  收起阅读 »

                    环信SDK3.1.0-3.3.1升级改动

                    1. EaseUI--EaseNotifer.java if(EMClient.getInstance().chatManager().isSlientMessage(message)){ return; ...
                    继续阅读 »
                    1. EaseUI--EaseNotifer.java 
                    if(EMClient.getInstance().chatManager().isSlientMessage(message)){
                    return;
                    } (326, 175, 126)
                    改为if(EaseCommonUtils.isSilentMessage(messages.get(messages.size()-1))){
                    // 修改的地方 messages.size()-1)有的参数时message , 请注意
                    return;
                    }
                    2. EaseConversationAdapter 
                    performFiltering() -- conversationId() 替换  getUserName();  
                    3. LnConversationAdapter 
                    (378, 146, 258)(149  本来就存在)  conversationId() 替换  getUserName();
                    4.  EaseChatFragment 
                    (746) 少去两个方法 (onMessageReadAckReceived, onMessageDeliveryAckReceived)
                    添加两个方法(onMessageDelivered, onMessageRead)
                    5. EaseChatFragment
                    EMChatRoomChangeListener -- 全部方法被注释掉了!
                    6. EaseGroupRemoveListener 
                    EaseGroupListener  -- (1885)
                    7. CallActivity
                    (175) EMClient.getInstance().callManager().endCall(); 添加异常处理
                    8.  CallActivity 
                    (333) setReceipt() 改为 : setTo ()  
                    9.  conversationListFragment
                     (118, 121) -- getUserName() 改为 : conversationId()
                    10.  DiagnoseActivity 
                    (67)  VERSION 修改! 
                    11. GroupDetailsActivity
                    (1638) 修改了GroupChangeLister 
                    12. GroupDetailsActivity 
                    (739) 修改了GroupChangeLister   添加方法 updateGroup(); refreshMembers();
                    13. MainActivity
                    修改了EMMessageListener()实现的方法  (309)
                    14.  MainActivity
                    修改了EMContactListener()要实现的方法 (435) 
                    15. ChatFragment 
                    (750) position = conversation.getAllMessages().indexOf(message);
                    16. PublicChatRooomsActivity
                    (132) ChatRoomChangeListener修改  -- (206) 复写监听方法
                    17. VideoCallActivity 
                    (274, 235) 电话断了  枚举变量修改   
                    18. VideoCallActivity 
                    (431, 432, 433 )  -- getVideoTimedelay 改为  getVideoLatency
                    getVideoFramerate 改为 getVideoFrameRate getVideoLostcnt 改为
                    19. VoiceCallActivity 
                    (203, )DISCONNNECTED (挂断电话) 改为  DISCONNECTED  (249)
                    ERROR --
                    20. EaseChatRow
                    (339) 重写updateView方法, 添加多参数方法  
                    21. EaseUI 
                    (147) options.setNumberOfMessagesLoaded(1); 方法弃用  
                    22.  EaseUI 
                    (176, 181) onMessageDeliveryAckReceived() onMessageReadAckReceived()方法弃用  重新添加两个方法 
                    onMessageDelivered 和 onMessageRead



                    23. DemoHelper
                    (524) 原有方法 : onInvitationReceived, onInvitationAccpted, onInvitationDeclined
                    onUserRemoved, onGroupDestroy, onApplicationReceived, onApplicationAccept, onApplicationDeclined
                    onAutoAcceptInvitationFromGroup,
                    onInvitationAccpted, -- 方法名错误 少个Accpted 改为 Accepted
                    onGroupDestroy 改为onGroupDestroyed
                    24. DemoHelper
                    (846) EMContactListener -- onContactAgreed 改为 onFriendRequestAccepted 
                    onContactRefused 改为 onFriendRequestDeclined
                    25. DemoHelper 
                    (1119) onMessageReadAckReceived, onMessageDeliveryAckReceived 去除      
                    添加 onMessageDelivered , onMessageRead
                    本文章纯属原创,转载请说明出处!  谢谢!!!!!! 收起阅读 »

                    头像昵称的简述和处理方案

                    环信的头像昵称,是一个老大难的问题,但是环信负责的是im的通讯能力,它只负责消息的通畅,头像昵称啥的,不是它要处理的,所以,只能给出一些建议性的解决方案。   从实际角度出发,有几种场景需要使用头像: 1、聊天的详情页面。 2、会话列表页面。 3、好...
                    继续阅读 »
                    环信的头像昵称,是一个老大难的问题,但是环信负责的是im的通讯能力,它只负责消息的通畅,头像昵称啥的,不是它要处理的,所以,只能给出一些建议性的解决方案。
                     
                    从实际角度出发,有几种场景需要使用头像:
                    1、聊天的详情页面。
                    2、会话列表页面。
                    3、好友页面。
                     
                    针对以上几个场景,
                    在聊天页面,接受消息的时候,需要知道对方叫什么,所以只需要发送方发消息中,把自己的名字,昵称带过来就行。
                    这里可以选择用message的属性ext来实现。
                     
                    message.ext = @{@"nick":@"李明"};


                    9CF6F919-BA41-48C7-8D8C-85D83E1838D8.png


                     
                     
                    当接收方(韩梅梅)收到消息后,只需要解析message中的ext字段,就可以知道发送方叫李明。
                    之后再ui上显示就可以了。此处的处理,单聊和群聊是一样的。
                     
                    注意事项:
                    发送方设置ext一定要在消息发送之前。
                     
                     
                    2、会话列表中展示:
                    上文说道,ext属性是message的,而会话的对象是conversation。此时,如果需要得到对方最后一条消息,可以使用
                    EMConversation对应的属性lastReceivedMessage。这个属性的描述是
                     
                    /*!
                     *  \~chinese
                     *  收到的对方发送的最后一条消息
                     *
                     *  @result 消息实例
                     *
                     *  \~english
                     *  Get last received message
                     *
                     *  @result Message instance
                     */
                    - (EMMessage *)lastReceivedMessage;
                     
                    这个时候,我们只需要去去 conversation.lastReceivedMessage.ext, 也就可以得到会话列表的头像了。
                     
                     
                    注意:
                    群聊里,不能通过最后一条对方发的消息来处理,环信的群属性中,没有头像这一条,所以也不能直接从环信这边取,但是它有群描述。这里提供两个思路:
                    * 把群属性放到自己的服务器,群头像也放到自己的服务器上,然后根据群id去自己服务器获取。
                    * 把头像放到群名描述里,之后用自己定义的格式隔开,取头像的时候,就直接从群描述的url来取。
                     
                    3、好友列表中头像和昵称的获取。
                    好友列表,好友的信息环信并没有提供多少属性,只提供了环信id。所以这个地方我们就没办法从环信这边获取任何信息了。这个地方说下环信demo的解决方式:
                     
                    环信demo中,用了一个第三方的云服务叫Parse,Parse的作用,是可以把k-v的键值对存到云服务器上。环信demo就把好友列表中,好友的环信id作为key,其他属性作为value存到了Parse上。这样在展示的时候,就直接去parse上下载,得到对应的昵称和url。 
                     
                    注意:
                    不过就我了解,国外的Parse服务已经停止了。目前demo里用的是环信自己搭建的一个Parse服务,环信官方也没有承诺该服务始终免费,记得当时也是说过是为了demo展示用才搭建的,所以建议还是自己找一些其他提供类似功能的云服务来使用比较好。
                     
                     
                      收起阅读 »

                    APNs证书创建和上传到环信后台

                    在iOS中,当app进程不存在的情况下,如果需要向设备发送通知,可以苹果提供的APNs 下面大概讲一下如果创建APNs证书和上传到环信。(首先需要有一个付费的苹果开发账号,否则无法创建相关证书) 文章最后有常见问题 1、前期准备   创建根证书很重要,要...
                    继续阅读 »
                    在iOS中,当app进程不存在的情况下,如果需要向设备发送通知,可以苹果提供的APNs
                    下面大概讲一下如果创建APNs证书和上传到环信。(首先需要有一个付费的苹果开发账号,否则无法创建相关证书)

                    文章最后有常见问题


                    1、前期准备
                     
                    创建根证书很重要,要确保创建根证书的电脑和最好导出P12的电脑是一台,否者可能无法创建成功。
                     
                    打开电脑的“钥匙串访问”并按照以下操作


                    196FFE18-F615-441B-955B-C22526FC7550.png


                     


                    439D8CD0-B95E-4639-923E-E39EB862FAAB.png


                    邮箱需要符合邮箱格式,名称随意,之后保存到本地。
                     

                    06DF2A8F-8599-415A-B132-ABC0FA326C6F.png



                    2、创建支持推送的APP
                     


                    07CFC71F-8F5A-4AF6-9716-0698572F483C.png




                    D6439761-4693-4C83-BEC4-2A804A089C6D.png




                    5B03EE6F-82CF-4B95-8079-298192464261.png




                    588C53C4-951C-4175-BA3E-FABD79C0C714.png




                    273DB89F-0D81-4AD2-99B5-1C9E6A539EF5.png



                    3、创建推送证书
                    此处以开发推送证书为例


                    1F0696C9-4C96-482E-84F8-14AC2CC6BD09.png




                    18F988F1-C700-4161-90DA-E2E4332FDE36.png




                    F911C057-8279-4CFC-B1E2-0C6D54C66992.png




                    F923A032-1318-4686-A6F7-69FA5CE015B1.png




                    71C69E1B-D0A7-4929-9641-873D6FEB7FCF.png




                    69093353-7D5E-4864-A42B-06D5EBD0E24B.png




                    6CB97BF4-DB2D-4F31-B611-EA4F848A9133.png




                    C8F22BCF-82B8-4BE3-87C9-972A88A5B567.png




                    37F49798-C4C4-475A-93AC-5E4FE32B37D6.png




                    77B837F9-3F76-4431-8C45-C0967A4F46F0.png




                    5980CC0C-00F0-40E0-8B39-8A4642CCF55D.png



                    再用同样的方式创建生产证书,注意命名要有区别。
                    此时,我们应该有三个文件:


                    8D1E5C71-B73D-47D5-AA63-EE682F1B3630.png


                     
                    4、制作环信用的P12推送证书
                     
                    同样以开发证书为例,双击导入aps_development.cer,


                    778565F3-543F-4AE6-9CDA-E22F1CFF0DF5.png




                    EC7CEA55-9CF5-4CB1-ADF0-10AA10652E0A.png




                    9AB6B10F-3773-4F05-977F-0DA4FB8F4C93.png




                    BA715C6D-684B-4BCF-9BBD-457C0763088B.png





                    8A6A3280-1C1F-426E-8B2E-DF54AB90E3F0.png


                     
                    以同样的方式再生成生产用推送证书。此时应该一共有5个文件。


                    9ADEA2A5-B424-49C7-B2C6-7A9F4E3667F8.png


                     
                    5、上传到环信
                     


                    CBA586BA-AB11-4A4A-AA35-9F3AEC7E8321.png




                    195BA6B9-EBAA-47A6-B2A9-CFEC9C6987D9.png




                    592ED719-5220-4D2F-B5DC-8AA98BA6AA89.png




                    5BF934C9-2DD6-4CFD-B832-5B87E592E4A1.png




                    7FAC6438-1349-414B-AE3C-90AC13AC704B.png




                    759E8827-4DA4-459B-9079-5726FE7F9E71.png




                    8946D27B-9EF2-424C-B2CC-2B96A83C5A67.png



                     
                    如果需要填写bundle id,一定要确保填写正确。
                     


                    CEAFFB8B-ED09-48CD-8F61-FA2A7260BB5F.png


                    注意:此处也需要选择正确,是开发模式还是生产模式。
                    密码就是到导出证书时候的密码。
                     
                    注意事项:
                    app工程里要打开推送开关


                    872EEB37-A79A-4221-9CCF-CFE6ABAB090B.png


                     
                     
                    ==============================
                     
                    常见问题:
                    为什么我按照配置后,app后台了还是收不到推送!


                    环信的长连接存在的情况下,在服务器就属于在线状态,环信不会通过苹果的APNs 给你发推送,而是直接通过环信的长连接,只有app后台被系统挂起或者是进程被杀死了,才会走APNs,先出怎么处理:
                     
                    在appdelegate文件里,也实现消息的监听,这样有消息了,也能收到回调。
                    收到回调后,先判断当前app的状态是在前台还是后台,如果是前台就忽视这条消息,如果在后台,就自己从代码里实现一个本地通知,把需要展示的消息内容得到后,自己发localNotifications. 本地通知的实现方式很简单,网上百度就行。
                     
                    下面我说下这种方式的好处与坏处。
                    好处:在正常使用场景中,app之间切换很正常,这样的好处就是不需要频繁的断开重连,速度会很快,同时也会比较省电,而且用户体验会更好。
                     
                    坏处:其他app里,app的icon上的角标和app内部的角标数量是一致的,但是像自己弹出的处理方式,会可能导致角标不一致,因为apns的角标是服务器发过来的,而localNotification的角标是由app自己设置的。
                     
                    顺便说一句,目前微信的实现方式也是后台的时候长连接保持,app角标也存在不一致的情况。
                     
                    APP之前没有用推送,现在需要用了,我按照上面设置后还是不行。
                     
                    需要删除本地的描述文件,重新去开发者中心下载,描述文件就是Provisioning Profile
                     
                    APP上线之前,如何测试生产的推送是否好用?
                    这个情况苹果已经替我们想好了,在打包的时候,有一个选项是ad-hoc。这个选项就是打一个生产用的包,并且可以导出保存到本地,之后用itunes安装就可以了。这个地方一定要注意,这个使用使用的证书需要是生产的证书了哦~ 收起阅读 »