注册
环信即时通讯云

环信即时通讯云

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

环信开发文档

Demo体验

Demo体验

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

RTE开发者社区

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

技术讨论区

技术交流、答疑
资源下载

资源下载

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

iOS Library

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

Android Library

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

环信亮相第十四届软交会,摘取“2016中国最佳SaaS产品奖”

被誉为国内软件业第一展,为期四天的2016中国国际软件和信息服务交易会于6月19日在大连落下帷幕。随着“互联网+”与各行业的深度融合,软件和信息服务行业将发挥更加重要的作用。作为企业级服务的明星垂直领域,SaaS客服也成为此次大会关注的焦点。本届软交会共吸引6...
继续阅读 »
被誉为国内软件业第一展,为期四天的2016中国国际软件和信息服务交易会于6月19日在大连落下帷幕。随着“互联网+”与各行业的深度融合,软件和信息服务行业将发挥更加重要的作用。作为企业级服务的明星垂直领域,SaaS客服也成为此次大会关注的焦点。本届软交会共吸引60多个国家和地区的743家企业、1.5万名业内人士和3.6万名观众参会。



]_`)DQ7{H{[5QWMIZX2@RS.png




“企业服务创新论坛”是一年一度的软交会的重点组成部分,前面已成功举办三届。在第十四届大连软交会“2016企业服务创新论坛”上,企业家们齐聚一堂共同探讨中国企业级服务的未来。同时,2016企业服务创新论坛金i奖颁奖盛典揭晓,环信作为中国企业级服务明星企业一举摘取“2016中国最佳SaaS产品奖”。



QQ截图20160621170708.jpg


环信荣膺“2016中国最佳SaaS产品奖”


 评委们给了环信企业级服务的最高评价:“当企业级SaaS服务如雨后春笋般涌现,我们在百花齐放的景象中能找到如此坚实沉静一朵,绽放自身的光彩。作为SaaS客服软件行业唯一一家同时拥有PaaS和SaaS产品的公司,环信以此建立其强大的壁垒;不急不缓,水到渠成,它们为中国企业提供着最高效可靠的SaaS服务。”



RVY14C11U`B4J)0398.png


环信副总裁发表主题演讲《客户服务的智能时代》


环信副总裁程旭文在论坛做主题演讲诠释了在“互联网+”时代智能SaaS客服的重要性。程旭文认为,互联网+时代的用户获客成本非常高,例如电商行业获取一个新用户成本是维护一个老用户的三倍到十倍。如何能让一个用户长期留在我们的产品和服务上?而且客户的渠道来源多种多样,解决这些问题非常重要。环信移动客服全媒体接入可以实现多渠道的用户来源一键管理一键回复。客户服务是有温度的,温度的高低同时决定了服务的满意度,环信移动客服可以实现对客户的交互进行情感分析,基于大数据技术的环信客户声音可以极大提升留存和转化从而实现精细化运营。



QQ截图20160621170339.jpg



随着人口红利消失,人工成本上升,企业客服面临的“用工荒”将持续扩大,运营成本将越来越高。环信利用智能机器人技术实现精准高效自动推送,自动问答,可以帮助客户解答80%的常见问题,极大提升效率降低成本,而环信首推的“人工+智能机器人”的协作服务被证明是现阶段最适合的客户服务方式。



QQ截图20160621170415.jpg



 本届软交会以“数据·共享,智慧·创新”为主题,企业级服务应以构建生态圈的创新能力为本,以客户为中心迭代,最终演变为“连接、创新改变生意”,环信即时通讯云就是这样一根连接“人与人”的管道。资源共享整合,以开放的态度,方能合作共赢。大数据是另一种连接,是海量的、相关或非相关的数据的连接,连接创造价值,并且数据的价值因连接的广度和共享而产生质变。环信移动客服基于大数据研发的创新产品“客户声音”,通过热点话题分析发现新畅销商品,通过情感度分析发现服务问题,帮助企业实现广度客户的连接从而创造更大价值。
环信移动客服致力于连接“人与商业”,作为全媒体智能云客服倡领者,支持全媒体接入,包括网页在线客服、社交媒体客服(微博、微信)、移动端客服和呼叫中心等多种渠道。基于业界领先的IM长连接技术保证消息必达,不丢消息不丢单,并通过强大的智能机器人技术极大降低人工客服工作量。
截至2015年底,环信移动客服共服务了12000家企业用户,现已覆盖包括电商、O2O、互联网金融、在线教育、在线旅游、移动医疗、智能硬件、游戏等20大领域的Top10客户,典型用户包括国美在线、58到家、楚楚街、随手记、海尔、51talk,链家自如客、神州专车等众多互联网和传统企业。根据易观国际发布的《中国SaaS客服市场专题研究报告2015》显示:截至2015年第三季度,环信移动客服在SaaS移动端客服用户覆盖占比为77.4%,以绝对优势稳居行业第一。
收起阅读 »

【环信招聘】精英人才邀请

  即时通讯云技术团队简介环信即时通讯云是一个提供即时通讯服务的PaaS平台,迄今为止,平台已经支持: 超过100000家App开发者 每天10000000+同时在线长连接 服务1000000000+不同用户  为了实现这个目...
继续阅读 »
 

QQ截图20160621122441.jpg


即时通讯云技术团队简介
环信即时通讯云是一个提供即时通讯服务的PaaS平台,迄今为止,平台已经支持:

超过100000家App开发者

每天10000000+同时在线长连接

服务1000000000+不同用户

 为了实现这个目标,通过分布在中国和美国的 5+ 主要IDC 以及日本、香港、欧洲多个中转节点,为全球用户提供即时可靠的通讯通道。

前端SDK已经可以横跨iOS、Android、Windows以及众多Linux系统,React Native、Swift等等跨平台技术也在不断扩展。

后台的大规模分布式的系统也正在进行微服务化改造,全球多机房同步技术、容器化技术也在深入研究。

这里的每一项技术都会直接帮助平台上的App开发者,并在真正的应用里得到实践检验。
我们不仅是技术的研发者,也是技术的输出方。

这里的每一项技术都在改变世界!

欢迎加入!我们支持开源运动!

 
查看热门职位,敬请 查看详情



系统研发高级工程师
 
您的职责:
 
负责开发和维护大规模分布式系统,为环信上亿用户 提供稳定可靠的服务;

或负责完善基础组件,完成后端系统的微服务改造;

或负责环信开放平台的开发,构建安全可靠的服务平 台;


 




通讯工程师
 
您的职责:

负责开发和维护即时通讯系统,优化提高后端服务的 承载能力;

或负责优化、改进和实现IM协议,为移动互联网以及 物联网用户提供可靠实时的即时通讯服务;


 




高级产品经理

您的职责:

负责环信即时通讯云产品;

负责编写产品需求书、产品原型,协助开发团队理解和 掌握需求,对产品求方向和易用性负责;

对所负责的产品进行用户行为数据监测、性能跟踪、数 据分析、提出产品优化方案;

研究市场上各种产品和应用,挖掘与把握在目标用户对 于信息不同层面的需求,并形成具体的产品功能;

研究市场和用户需求,竞品分析,不断优化产品,提升 用户体验及活跃度,同时策划有竞争力的产品;


 
还想看看别的高薪职位,点击查看
 

有意者请发简历至邮箱:
talent@easemob.com / job@easemob.com

  收起阅读 »

环信CEO:SaaS客服的难点和坑点全解析

移动和社交风头正劲,企业协作与通信市场风云变幻。互联网巨头和创业公司纷纷入场,共同分食企业协作与通信这块大蛋糕,也深度促进了中国企业级服务的市场繁荣。 6月14日,国内企业协作与通信市场最大的盛会——第十八届CENCE 企业协作及通信大会在北京国际会议中心盛大...
继续阅读 »



FY456RX(7W(XFR1Y



移动和社交风头正劲,企业协作与通信市场风云变幻。互联网巨头和创业公司纷纷入场,共同分食企业协作与通信这块大蛋糕,也深度促进了中国企业级服务的市场繁荣。 6月14日,国内企业协作与通信市场最大的盛会——第十八届CENCE 企业协作及通信大会在北京国际会议中心盛大开幕。包括国内三大运营商、IBM、Cisco、华为、环信等几十家行业领头羊均受邀参展本次大会,共同探讨移动社交时代的企业协作、OA、音视频通信、企业社交、CRM、客服联络中心等领域的重要议题。



4)QG7V})WHYO0HHW0VG[T4F.png




4_7CRYE0V947@B@ZX1DD3U5.png


大会深受企业IT信息主管、CIO、渠道、ISV人群高度关注


 环信展台人头攒动,同时,环信CEO刘俊彦作为企业级服务KOL受邀发表主题演讲《智能全媒体客服时代的最佳实践》,用环信的真实大客户案例与会众共同探讨客户服务的痛点以及SaaS客服的难点和坑点!



{R3~6B}]ADUQV@_{VUJS}8L.png



环信CEO刘俊彦认为,虽然目前SaaS客服产品同质化严重,但只是宣传同质化严重,主流厂商产品成熟度和差异化还是明显。比如现在领先的 SaaS客服厂商都可以提供全媒体接入。国内主流的全媒体接入渠道包括电话呼叫中心,网页在线客服,微信公共账号客服,APP内置客服。前3个渠道因为所需要的技术已经很成熟,技术难度低,所以在这3个接入渠道上,各个厂家确实是同质化的。但在APP内置客服这个接入渠道上,各个厂家的解 决方案差异会非常大。怎么解决移动弱网络环境下客服咨询绝不掉线,绝不丢消息,怎么解决数千款碎片化Android手机的适配,怎么支撑千万级乃至亿级用户同时在线等。环信客服SDK历经6万家APP验证,覆盖3.19亿独立手机终端,完美适配所有中高低端安卓和IOS机型。所以环信在全媒体接入的移动APP端是有巨大优势的。

虽然智能机器人处在风口,但是客服智能机器人有些被厂商过度消费。刘俊彦认为目前客服智能机器人的成熟应用还主要在售后阶段,解决标准化重复性问题为主,比如在电信和银行业,解答海量用户的海量重复标准化咨询问题。而在售前和售中需要深度挖掘销售机会且客单价还高的行业,比如电商、O2O、教育、医疗行业等,环信主推的“人机混合模式”将发挥更大价值,而且环信移动客服在这些行业大客户众多,优势明显。

同时刘俊彦透露环信基于大数据分析的创新产品“环信客户声音”,通过热点话题分析发现新畅销商品,通过情感度分析发现服务问题,将给SaaS客服市场带来更多惊喜。以下是本次主题演讲干货内容:

全媒体客服接入核心在于移动端,移动端核心在于用户体验为王!

环信CEO总结到:“来自不同媒体的服务请求均可以统一接入,一键回复,打造跨网、跨界、跨平台的极致客户服务体验。”链家自如客使用环信实现了全媒体客服接入,环信帮助链家自如客打通了来自App端的客服入口+网页端客服入口+微信端客服入口,不仅可以统一接入回复且后台数据打通共享,此举帮助链家自如客优化了客服团队,提升了效率,节省了成本。



DO{HF_3YGUR_SN`8@_HJOHJ.png


链家自如客使用环信实现全媒体客服




Z]1ALABY`7@I2]JL7Y{T76.png



同时,环信还提供“一体化”客服工作台,支持从APP、微信公众号、微博、网页、呼叫中心等渠道接入,且每个不同渠道均有不同标识进行识别。

从传统呼叫中心到全媒体客服,一场“效率”革命!

随着人口红利消失,呼叫中心的升级转型将越来越普遍,2015年中国劳动力规模由2012年的9.37亿降至9.11亿人。中国劳动力人口连续4年绝对值下降企业客服面临的“用工荒”将持续扩大,运营成本将越来越高,越来越多的企业将复制环信客户“学而思”的客服转型之路,从语音呼叫中心为主转而采用全媒体客服,拥抱移动互联网。

学而思每年10—11月是交费季,以前主要靠呼叫中心外呼,工作量巨大,效果不满意。学而思在2015年集成环信移动客服以后,服务模式改为 APP内缴费,并在APP内提供客服支持。整个2015年缴费季,APP客服部门数十人完美解决了往年数百个语音客服的工作。环信CEO刘俊彦认为:“APP在线客服相比电话客服,大幅度提高了服务效率,从一对一的独占式的同步沟通,变成了一对多的异步式沟通,是一场效率革命,全渠道客服也已经成为企业刚需。”

ITR将取代IVR进入触摸时代,“人工+智能机器人“混合模式将是现阶段最适合客服方式!

传统IVR,用户需要听完所有菜单再做选择。而现在主流的ITR导航,用户只需在手机上直接选择关注的问题,简单方便。其中神州租车就采购环信移动客服,其中的智能ITR大大缓解了人工客服压力。Gartner预测到2020年,ITR将完全取代IVR全面进入触摸时代。



_M~1VE{XMB~U}5CA4A`TQYD.png


神州租车使用环信智能ITR缓解人工客服压力


环信是客服行业少数自主开发智能应答机器人产品的公司,环信首推的“人机混合模式”将在现阶段的客服服务中发挥更大价值。环信知识库+智能聊天机器人可以帮助人工坐席挡住80%的常见问题。同时具有以下特性:1,灵活可定制的智能会话、自定义菜单导航功能。2,预置的行业知识库,行业相关的常见问答可以一键拥有。3,与现有知识库系统对接,机器学习,智能优化知识库。4,人机无缝配合,更少的成本,更好的客户体验 。



QQ截图20160620110628.jpg


环信首推的人机混合服务极大提高客服应答效率


环信CEO表示“人机混合服务”将是现阶段最适合也是最具效率的客户服务方式。​
服务式营销,从成本中心转向利润中心

随着客服中心不断的被新时代赋予新的含义,传统的客服中心也正逐渐从成本中心向营销中心和利润中心转化。其中众多电商标杆企业均使用环信移动客服实现了服务式反向营销,先通过“客户标签”功能+“大数据分析”找到目标群体,然后通过环信移动客服的精准营销推送接口,将富媒体商品信息定向推送给目标客户。这种反向营销不仅用户体验好,转化率也极高,帮助电商企业实现了订单数和客单价双丰收。



[7FE4{Y9Q1H`~348~MTT66X.png


移动互联网的电话外呼——金融界为电话销售配置环信APP主动营销平台




HPT53UQF{I{ZPT6979LLX1E.png



近期,环信还上线了业界首个“客户声音”产品,可以通过热点话题分析发现新畅销商品,通过情感度分析发现服务问题,来帮助企业更好的来倾听客户的声音。
 
最后,环信CEO预测:“随着国内SaaS客服产品的逐渐成熟完善,中小企业将全面拥抱SaaS客服,建设全媒体客户关系中心。而传统大型企业也将增量部署全媒体客服,保护已有投资,拥抱移动互联网。”SaaS客服也将逐渐成长为一个千亿级市场。同时,环信CEO认为未来远程办公,移动办公和众包客服将解决客服行业人力资源不足的问题,而环信移动客服的手机端工作后台将提供很大的助力。



V`~0EK7~W)`DCDKEV~_Q3EO.png



收起阅读 »

ios 关于GCD 浅谈

笔者也是在边学习边整理,有不对的地方欢迎指出​           类型:(1)并发队列(Concurrent Dispatch Queue)  可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)并发功能只有在异步(dispatch_asyn...
继续阅读 »
  • 笔者也是在边学习边整理,有不对的地方欢迎指出
​         

图片1.png

  •  类型:
  • (1)并发队列(Concurrent Dispatch Queue)
  •   可以让多个任务并发(同时)执行(自动开启多个线程同时执行任务)并发功能只有在异步(dispatch_async)函数下才有效
  •  
  • (2)串行队列(Serial Dispatch Queue)
  •   让任务一个接着一个地执行(一个任务执行完毕后,再执行下一个任务)
 
  • 将任务添加到队列中,GCD会自动将队列中的任务取出,放到对应的线程中执行,任务的取出遵循队列的FIFO原则:先进先出,后进后出
  • 同步和异步决定了要不要开启新的线程
  • 同步:在当前线程中执行任务,不具备开启新线程的能力
  • 异步:在新的线程中执行任务,具备开启新线程的能力
  •  
  • 并发和串行决定了任务的执行方式
  • 并发:多个任务并发(同时)执行
  •  
  • 串行:一个任务执行完毕后,再执行下一个任务
  • (1)用异步函数往并发队列中添加任务
  •  
  •  总结:同时开启多个子线程
  •  
  • (2)用异步函数往串行队列中添加任务
  •  
  •  总结:会开启线程,但是只开启一个线程
  •  
  • (3)用同步函数往并发队列中添加任务
  •  
  •  总结:不会开启新的线程,并发队列失去了并发的功能
  •  
  • (4)用同步函数往串行队列中添加任务
  •  
  •  总结:不会开启新的线程
  •  
  • 说明:同步函数不具备开启线程的能力,无论是什么队列都不会开启线程;异步函数具备开启线程的能力,开启几条线程由队列决定(串行队列只会开启一条新的线程,并发队列会开启多条线程)。
  •  
  • 同步函数
  •  
  • (1)并发队列:不会开线程
  •  
  • (2)串行队列:不会开线程
  •  
  • 异步函数
  •  
  • (1)并发队列:能开启N条线程
  •  
  • (2)串行队列:开启1条线程
  •  
  • dispatch_async(queue,block)  async 异步队列,dispatch_async 函数会立即返回, block会在后台异步执行。
  •  
  • dispatch_sync(queue,block)  sync 同步队列,dispatch_sync 函数不会立即返回,及阻塞当前线程,等待 block同步执行完成。
 
  • 下面上代码,可以理解的更透彻一些:
  •  
  • (1)同步串行队列
  • 这里加sleep方便从运行结果更好的看出区别
  • 如图-同步串行队列

  • 同步串行队列.png

  • 可以看出,这里的时间间隔就是sleep,也就是说会按顺序添加到队列中,并且只能等上一个任务结束才能进行下一个任务,所有的操作都是在主线程中进行,并不会开辟新的子线程。
  • 如图-同步串行队列-2
         

同步串行队列-2.png

  • (2)同步并发队列
  • 从运行结果显而易见,同步串行和同步并发结果是相同的,也是上一个任务结束才能进行下一个任务,也是在主线程中进行,并且也没有开辟新的子线程,也就是说同步函数不具备开启线程的能力。
  • 如图-同步并发队列、同步并发队列-2
       

同步并发队列.png

       

同步并发队列-2.png

  • (3)异步串行队列
  • 从结果可以看出,同步和异步串行队列的区别主要在于开辟子线程的能力,在同步串行队列中,所有的任务都是在主线程执行,在异步串行队列中,会开辟一个新的子线程,不论有多少个任务,都只会开辟一个子线程。
  • 如图-异步串行队列、异步串行队列-2
       

异步串行队列.png

       

异步串行队列-2.png

  • (4)异步并发队列
  • 从结果可以看出,异步并发队列是多个任务同时执行,并且会根据任务的多少开辟多个子线程,可以把sleep去掉会发现,输出的顺序是无序的。
  • 如图-异步并发队列、异步并发队列-2
       

异步并发队列.png

       

同步并发队列-2.png

  • (5)GCD延时操作
  • 如图-GCD延时操作
       

GCD延时操作.png

  • (6)队列组
  • 首先创建队列组,把需要执行的任务都放到队列组里,notify是监听队列组中所有任务完成之后才会调用的。
  • 如图-队列组、队列组-2
       

队列组.png

       

队列组-2.png

  • 死锁问题(笔者也是根据自己的理解,有问题之处还望指出)
  • 其实死锁的原因就是线程之间相互阻塞(相互等待)
  •  
  • 第一种情况:
  • 因为同步函数会等block块执行完成之后才接着往下执行,执行到图中所示位置时,会在主线程新加入一个队列,所以sync会等待block执行完成之后在继续往下执行,但是sync在主线程中,而且是串行队列,sync是后加入进来的,sync想执行必须等主线程执行完成,主线程等待sync返回,所以执行到此处就会造成死锁,不会继续往下执行了。简单来说,就是sync等待主线程执行完成, 主线程等待sync函数返回。
  • 如图-死锁1、死锁2
       

死锁1.png

       

死锁2.png

  • 第二种情况:
  • 对比下面图的运行结果,可以看出,异步函数中不会等待block执行完成,会立刻返回,两个同步函数之间相互等待,造成死锁。
  • 从输出时间可以看出,1和2几乎同时执行,因为是在异步函数中,当执行到2的sync时1存在两种情况:(1)执行中 (2)已经执行完成 ,而且主线程中并没有执行耗时操作,所以1和2同时完成;如果把2处改成sleep(3),那么就会出现前面说异步函数时出现的情况,输出时间至少会差3秒;如果阻塞了主线程,2处的sync就会一直等待永远都不会执行。
  • 如图-死锁3、死锁4
         

死锁3.png

         

死锁4.png


收起阅读 »

Android Lollipop (5.0) 屏幕录制实现

引言 从 Android 4.4 开始支持手机端本地录屏,但首先需要获取 root 权限才行,Android 5.0 引入 MediaProject,可以不用 root 就可以录屏,但需要弹权限获取窗口,需要用户允许才行,这里主要介绍 Android 5.0...
继续阅读 »
引言

从 Android 4.4 开始支持手机端本地录屏,但首先需要获取 root 权限才行,Android 5.0 引入 MediaProject,可以不用 root 就可以录屏,但需要弹权限获取窗口,需要用户允许才行,这里主要介绍 Android 5.0+ 利用 MediaProject 在非 root 情况下实现屏幕录制。

基本原理

在 Android 5.0,Google 终于开放了视频录制的接口,其实严格来说,是屏幕采集的接口,也就是 MediaProjection 和 MediaProjectionManager。

具体实现步骤
1 申请权限


在 AndroidManifest 中添加权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
Android 6.0 加入的动态权限申请,如果应用的 targetSdkVersion 是 23,申请敏感权限还需要动态申请
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.WRITE_EXTERNAL_STORAGE}, STORAGE_REQUEST_CODE);
}
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[] {Manifest.permission.RECORD_AUDIO}, AUDIO_REQUEST_CODE);
}
2 获取 MediaProjectionManager 实例

MediaProjectionManager 也是系统服务的一种,通过 getSystemService 来获取实例
MediaProjectionManager projectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
3 发起屏幕捕捉请求
Intent captureIntent= projectionManager.createScreenCaptureIntent(); 
startActivityForResult(captureIntent, REQUEST_CODE);
4 获取 MediaProjection

通过 onActivityResult 返回结果获取 MediaProjection
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == RECORD_REQUEST_CODE && resultCode == RESULT_OK) {
mediaProjection = projectionManager.getMediaProjection(resultCode, data);
}
}
5 创建虚拟屏幕

这一步就是通过 MediaProject 录制屏幕的关键所在,VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR 参数是指创建屏幕镜像,所以我们实际录制内容的是屏幕镜像,但内容和实际屏幕是一样的,并且这里我们把 VirtualDisplay 的渲染目标 Surface 设置为 MediaRecorder 的 getSurface,后面我就可以通过 MediaRecorder 将屏幕内容录制下来,并且存成 video 文件
private void createVirtualDisplay() {
virtualDisplay = mediaProjection.createVirtualDisplay(
"MainScreen",
width,
height,
dpi,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
mediaRecorder.getSurface(),
null, null);
}
6 录制屏幕数据

这里利用 MediaRecord 将屏幕内容保存下来,当然也可以利用其它方式保存屏幕内容,例如:ImageReader
private void initRecorder() {
File file = new File(Environment.getExternalStorageDirectory(), System.currentTimeMillis() + ".mp4");
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
mediaRecorder.setOutputFile(file.getAbsolutePath());
mediaRecorder.setVideoSize(width, height);
mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
mediaRecorder.setVideoEncodingBitRate(5 * 1024 * 1024);
mediaRecorder.setVideoFrameRate(30);
try {
mediaRecorder.prepare();
} catch (IOException e) {
e.printStackTrace();
}
}

public boolean startRecord() {
if (mediaProjection == null || running) {
return false;
}
initRecorder();
createVirtualDisplay();
mediaRecorder.start();
running = true;
return true;
}
源码地址​:
完整代码
  收起阅读 »

聊天机器人七大商业模式:客服行业将是重要变现场景!

毫无疑问,聊天机器人如今已经成为继移动应用之后最热门的话题之一。在过去的一段时间里,Google、Facebook、微软、Amazon 等众多科技巨头也都开始纷纷加强在聊天机器人领域的布局。然而很多人可能要开始问了,这些聊天机器人将如何获得盈利呢?客服行业将是...
继续阅读 »

1D74.tmp_.jpg


毫无疑问,聊天机器人如今已经成为继移动应用之后最热门的话题之一。在过去的一段时间里,Google、Facebook、微软、Amazon 等众多科技巨头也都开始纷纷加强在聊天机器人领域的布局。然而很多人可能要开始问了,这些聊天机器人将如何获得盈利呢?客服行业将是重要变现场景!

在过去的几个月里,我和我的同事体验了一些聊天机器人产品,也和很多聊天机器人的开发商就如何盈利这个问题进行了深入探讨。经过认真了解和分析,我认为聊天机器人在未来可能会有以下七种可行的商业模式:
 
商业模式一:用于零售销售的聊天机器人,未来将扩展到整个客服行业。
 



42A2.tmp_.jpg


(Kit 上的 H&M 零售聊天机器人)


聊天机器人最直接的一种使用场景是商家直接向消费者(B2C)销售产品。想象一下,沃尔玛、Harry’ s、Target、Amazon 和京东等开发了这样一种聊天机器人,你可以问聊天机器人是否销售 “牙膏” 或 “刮胡刀” 等,然后聊天机器人会直接回复你这些商品的购买链接,所以用户在于聊天机器人的交流中就能直接完成商品的购买,国内SaaS客服厂商诸如环信智能聊天机器人能够帮助客户解答80%的常见问题,极大的提高效率节省成本。客户服务行业显然是聊天机器人最容易变现的途径之一,参与交易必然离钱更近。中国SaaS客服市场已经有诸如环信等厂商开始自主研发基于深度学习和自然语义分析的智能聊天机器人,虽然目前各家SaaS客服厂商都具备智能聊天功能,未来一年将逐渐转化到拼产品成熟度和易用性阶段。
 
商业模式二:BaaS(Bots as a Services,聊天机器人即服务)

B2B 领域的聊天机器人主要是帮助用户和团队更有效率地开展工作、管理任务或解决团队沟通方面出现的问题。所以 B2B 领域的聊天机器人可能会复制目前已经存在的 B2B 软件领域的商业模式。

对于 B2B 聊天机器人而言,我个人坚信,SaaS 式的免费增值模式可能会成为它最可行的商业模式。对于一些聊天机器人来说,根据你购买的增值服务的不同,那么你能使用到的聊天机器人的功能也是不同的。根据市场调研公司 Forrester 发布的数据,在 2016年,SaaS 和基于云的商业应用服务的营收有望达到 328 亿美元。因此可以想象,B2B 聊天机器人市场的营收应该也不会低。Slack 平台上的大部分应用基本都是基础功能免费,要想使用更高级的功能则需要付费,如下图中的 Growbot.io 那样:



7B48.tmp_.jpg


(Growbot.io 的价格截图)


SaaS产品的商业模式是 B2B 领域的客户都非常熟悉的。在此基础上,聊天机器人未来可能会采取更为复杂的定价模式。
 
 商业模式三:聊天机器人 + 赞助内容和原生内容

因为 BuzzFeed、VICE 等的出现,原生内容和原生广告在过去几年里慢慢变成了一个大趋势。原生内容或赞助内容是这样一种模式:媒体公司(如 BuzzFeed)将那些付费品牌商家的赞助内容直接发布到自己的内容频道上,让读者阅读的时候感觉这篇内容好像是媒体自己创作发布的内容而非品牌商发布的广告。下面这个例子就是杜蕾斯在 BuzzFeed 上发布的原生广告内容:


97BB.tmp_.jpg


现在设想一下你正在咨询一个烹饪方面的聊天机器人,聊天机器人基于自己的原生功能可能会回答你说,在某些菜谱中,使用香菜代替茴香是可以的,然后会发给你一篇文章《这五道用 ‘是拉差辣椒酱’(一种泰式料理常用的香甜辣椒酱)烹饪的菜,吃后绝对让你惊叹不已》。当然了,这里的是拉差辣椒酱就是赞助内容。

这种原生广告的效果要比传统的横幅广告的效果要好很多。这种类型的广告对品牌商和出版商都有益。未来,你可能会看到这种广告形式将被出版商应用到聊天机器人里。
 
 商业模式四:利用聊天机器人做联盟网络营销

联盟广告营销最近很多年已经成为一种非常流行的商业化策略。联盟广告营销指的是一种网站 A 为网站 B 设置推广链接,然后从为网站 B 带来的销售额中获取一定提成的一种广告系统。

Forrester 发布的数据报告显示,2016年,美国在联盟广告营销上的花费将达到45亿美元。联盟广告营销也可以作为聊天机器人的一种商业模式。举个例子,对于聊天机器人的开发商,你可以开发一款健身方面的聊天机器人,在如何保持健康的身体方面为用户提供专业的建议,然后给用户发送一些附有商业推广链接的健身方面的产品。
 
购物聊天机器人 Kip 现在已经开始采用这种商业化策略了。用户可以问 Kip “巧克力” 或 “咖啡” 等很多产品方面的问题,然后它会回复一些产品的购买链接,如下图所示:


B037.tmp_.jpg


用户如果通过 Kip 发的链接购买产品,那么 Kip 团队就能从销售收入中收取一定的提成。下面就有一位用户说他自己是在和 Kip 聊天机器人聊天后通过 Kip 发的链接购买 Amazon Echo 这款产品的。

CDA4.tmp_.jpg


 
 商业模式五:用聊天机器人做用户调研 



E15D.tmp_.jpg


(DisOrDatBot 截图)


最近美国总统大选正在如火如荼地进行中,想了解千禧一代都是怎么看待美国总统大选的吗?你可以付费使用一些聊天机器人来进行这方面的调研。虽然我现在还没有看到有人利用聊天机器人做这方面的事,但我觉得如果有专门的 Q&A 聊天机器人来专门帮助人们做调研的话还是非常靠谱的。

目前像 DisOrDatBot 这样的聊天机器人已经开始向用户问一下调研类问题了。想象一下你作为一次活动的策划者,现在正在发愁究竟邀请哪支乐队来你所在的城市进行表演,是邀请电台司令乐队(Radiahead)还是五分钱乐队(Nickelback,加拿大的著名乐队),这时,与其花很多钱请调研公司帮你做调研,还不如使用 DisOrDatBot 进行调研,看你所在城市的用户到底喜欢哪支乐队。

如果你已经开发了一个定期给一群小众用户提供有价值内容的聊天机器人,那些想触及这些用户或是想向这群用户销售产品的公司可能会比较有兴趣通过你的聊天机器人做调研。
 
商业模式六:将聊天机器人用于潜在客户开发中

我预测,聊天机器人未来将会被应用到潜在客户开发中,一开始主要利用内容去开发潜在客户。通过在房产所有权、保险、婚礼和理财等方面为用户提供专业的信息、想法和见解,聊天机器人然后将自己获得的这些用户信息给到那些销售相关产品和服务的公司。

举个例子,加入你正在和一个 “生活聊天机器人” 聊天,向聊天机器人咨询一些购房方面的问题,随着聊天的深入,聊天机器人搜集了更多有关你的信息,包括你手头有多少首付资金、你想在哪里购房定居、你是否在职、你购买的是否是你的第一套房产等等。在和聊天机器人建立起一定的关系后,聊天机器人于是问你下面这个问题:

“你是否介意我介绍一家比较适合你的房产公司和你联系?”

在经过你的同意之后,聊天机器人就会将你的信息给到你所在地区的一家房地产公司。这家房产公司第二天就会和你联系沟通你的购房需求。然后这家房产公司会给聊天机器人开发商一定的佣金作为为其开发潜在客户的报酬。

商业模式七:按完成的咨询次数或任务收费

人们都希望得到专业的好建议,也愿意为好建议付费。随着聊天机器人变得越来越专业和智能,我认为未来人们会在生活中的很多方面都希望得到聊天机器人的建议和帮助,并愿意为这些建议付费。例如,如果你需要生活方面的建议,你可以和 “Oprah 聊天机器人” 交流,如果你需要获得汽车方面的信息,那么你可以和 “机械聊天机器人” 交流,如果你希望获得匿名的婚姻咨询,你可以和 “婚姻聊天机器人” 交流咨询。当然了,为了得到聊天机器人的建议,你是需要支付一定的费用的。
 
 
(本文由36氪编译自:venturebeat.com 作者:达达 环信稍有改编) 收起阅读 »

环信(Android)设置头像和昵称的方法(最简单暴力的基于环信demo的集成)。

        最近,经常有朋友问到,如何集成环信头像,怎么才能快速显示头像,因时间紧急,很多朋友都没有时间慢慢的研究代码,这里大家稍微花10分钟看一下文章,看完后再花5分钟改一下代码,即可达到你们所要的效果。         当然这个是在你直接复制了demo...
继续阅读 »
        最近,经常有朋友问到,如何集成环信头像,怎么才能快速显示头像,因时间紧急,很多朋友都没有时间慢慢的研究代码,这里大家稍微花10分钟看一下文章,看完后再花5分钟改一下代码,即可达到你们所要的效果。
        当然这个是在你直接复制了demo中的工具类和必要的UI的前提下实现的。简短说明简单暴力的方法:除UI外的其他所有类先复制到自己工程中,UI部分MainActivity中的代码需要一个一个复制过去,如与自己项目冲突的需要调整,然后ChatActivity/ChatFragment也复制过去,这就可以进行聊天了。不过头像就是大家所遇到的头像显示不了,昵称显示为环信号码。
        环信官方是有给出头像的设置的,不过大部分朋友看了之后都是晕呼呼的。官方给出的提示如下:方法一 从APP服务器获取昵称和头像


昵称和头像的获取:当收到一条消息(群消息)时,得到发送者的用户ID,然后查找手机本地数据库是否有此用户ID的昵称和头像,如没有则调用APP服务器接口通过用户ID查询出昵称和头像,然后保存到本地数据库和缓存,下次此用户发来信息即可直接查询缓存或者本地数据库,不需要再次向APP服务器发起请求

昵称和头像的更新:当点击发送者头像时加载用户详情时从APP服务器查询此用户的具体信息然后更新本地数据库和缓存。当用户自己更新昵称或头像时,也可以发送一条透传消息到其他用户和用户所在的群,来更新该用户的昵称和头像。

方法二 从消息扩展中获取昵称和头像

昵称和头像的获取:把用户基本的昵称和头像的URL放到消息的扩展中,通过消息传递给接收方,当收到一条消息时,则能通过消息的扩展得到发送者的昵称和头像URL,然后保存到本地数据库和缓存。当显示昵称和头像时,请从本地或者缓存中读取,不要直接从消息中把赋值拿给界面(否则当用户昵称改变后,同一个人会显示不同的昵称)。

昵称和头像的更新:当扩展消息中的昵称和头像URI与当前本地数据库和缓存中的相应数据不同的时候,需要把新的昵称保存到本地数据库和缓存,并下载新的头像并保存到本地数据库和缓存。


---------------------------------------------------------------------------------------------
        个人推荐使用方法2,优势比较明显,不仅可以设置头像、昵称,甚至以后出现的是否管理员,或者自己APP中的身份标志,如:店小二、医生、客服等等都可以再定义注明,暴力而简单,不需要考虑对方更新头像或昵称,而软件没重启的情况下怎么去更新头像和昵称等等复杂的问题。本指导以最简单的集成为指导,工具类可使用自己软件中的,或通过别的方式实现。
        方法2的集成步骤:
1、在登录的时候,把自己登录成功时后台返回的信息保存到sharedpreferences中,需要包含需要的头像和昵称。
new Thread(new Runnable() {
@Override
public void run() {
EMClient.getInstance().login(username, password, new EMCallBack() {
@Override
public void onSuccess() {
// 登陆成功,保存用户昵称与头像URL
AppSPUtils.setValueToPrefrences("name", loginBean.getName());
AppSPUtils.setValueToPrefrences("logoUrl", loginBean.getLogoUrl());

// 将自己服务器返回的环信账号、昵称和头像URL设置到帮助类中。
DemoHelper.getInstance().getUserProfileManager().updateCurrentUserNickName(loginBean.getName());
DemoHelper.getInstance().getUserProfileManager().setCurrentUserAvatar(loginBean.getLogoUrl());
DemoHelper.getInstance().setCurrentUserName(loginUser.getHxId()); // 环信Id

// ------以下参考demo中的,加载群组和加载消息。然后跳转到首页-------
2、AppSPUtils是个人写的一个工具类,大家可以自己写一个,给出参考代码。
public class AppSPUtils {

private final static int MODE_SPEC = android.os.Build.VERSION.SDK_INT <= 10 ? 0 : Context.MODE_MULTI_PROCESS;

public static SharedPreferences getSharedPreferences(String name) {
return MainApplication.getContext().getSharedPreferences(name,
Context.MODE_PRIVATE | MODE_SPEC);
}

public static SharedPreferences getAppSharedPreferences() {
return getSharedPreferences(Constants.SP_APP);
}

public static String getValueFromPrefrences(String key, String defaultValue) {
return getValueFromPrefrences(getAppSharedPreferences(), key, defaultValue);
}

public static void setValueToPrefrences(String key, String value) {
try {
SharedPreferences preferences = getAppSharedPreferences();
if (null != preferences) {
preferences.edit().putString(key, value).commit();
}
} catch (Exception e) {
e.printStackTrace();
}
}

// 退出登录时要调用
public static void clean() {
try {
SharedPreferences preferences = getAppSharedPreferences();
if (null != getAppSharedPreferences()) {
getAppSharedPreferences().edit().clear().commit();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
3、保存好自己的信息后,已经成功了四分之一,现在是如何将自己的头像昵称等信息发送出去,最简单的就是使用扩展消息了,在ChatActivity中可以看到,基本上只做了一件事,保证只有一个ChatActivity,那我们的代码在哪里?就在ChatFragment里面。找到代码,可以看到其实它是继承自EaseChatFragment,很多内容在easeui中已经做好了。
这时你会发现ChatFragment类中有一个扩展属性的说明,没错,就是这个方法,把你要发送的内容尽情的发送吧,现附上发送头像和昵称的代码:
@Override
public void onSetMessageAttributes(EMMessage message) {
if (isRobot) {
// 设置消息扩展属性
message.setAttribute("em_robot_message", isRobot);
}

// 通过扩展属性,将userPic和userName发送出去。
String userPic = AppSPUtils.getValueFromPrefrences("logoUrl", "");
if (!TextUtils.isEmpty(userPic)) {
message.setAttribute("userPic", userPic);
}
String userName = AppSPUtils.getValueFromPrefrences("name", "");
if (!TextUtils.isEmpty(userName)) {
message.setAttribute("userName", userName);
}
}
4、发送完成,你已经完成了四分之二的任务了。发送完成后,肯定需要一个接收,其实demo中的广播已经接收好了,那么我们跟随着广播的脚步,来到DemoHelper这个类,初次看这个类,都是云一样的感觉,经过一番查找,发现有个onMessageReceived的方法,并且还有注释“全局监听”,赶紧开工,试试接收吧,可以自己打一下log出来看是不是拿到了我们要的。并且照着demoHelper中的保存方法,将接收到的内容保存起来。
@Override
public void onMessageReceived(List<EMMessage> messages) {
for (EMMessage message : messages) {
message.setMsgTime(System.currentTimeMillis());
//************接收并处理扩展消息***********************
String userName = message.getStringAttribute("userName", "");
String userPic = message.getStringAttribute("userPic", "");
String hxIdFrom = message.getFrom();
EaseUser easeUser = new EaseUser(hxIdFrom);
easeUser.setAvatar(userPic);
easeUser.setNick(userName);

// 存入内存
getContactList();
contactList.put(hxIdFrom, easeUser);
// 存入db
UserDao dao = new UserDao(MainApplication.getContext());
List<EaseUser> users = new ArrayList<EaseUser>();
users.add(easeUser);
dao.saveContactList(users);

getModel().setContactSynced(true);

// 通知listeners联系人同步完毕
notifyContactsSyncListener(true);
if (isGroupsSyncedWithServer()) {
notifyForRecevingEvents();
}

// ******************扩展信息处理完成**********************
EMLog.d(TAG, "onMessageReceived id : " + message.getMsgId());
// 应用在后台,不需要刷新UI,通知栏提示新消息
if (!easeUI.hasForegroundActivies()) {
getNotifier().onNewMsg(message);
}
}
}
5、信息都收到了,就差最后一步就可以显示了,不知道你们是不是激动,反正我是激动了。那在哪里进行显示呢?还是在DemoHelper中,找到getUserInfo方法,代码如下:
private EaseUser getUserInfo(String hxId) {
// 获取user信息,demo是从内存的好友列表里获取,
// 实际开发中,可能还需要从服务器获取用户信息,
// 从服务器获取的数据,最好缓存起来,避免频繁的网络请求

if (hxId.equals(EMClient.getInstance().getCurrentUser())) {
EaseUser currentUserInfo = getUserProfileManager().getCurrentUserInfo();
return currentUserInfo;
}
EaseUser easeUser;
if (contactList != null && contactList.containsKey(hxId)) {

} else { // 如果内存中没有,则将本地数据库中的取出到内存中。
getContactList();
}
// // TODO 获取不在好友列表里的群成员具体信息,即陌生人信息,demo未实现
// if (user == null && getRobotList() != null) {
// user = getRobotList().get(hxId);
// }
easeUser = contactList.get(hxId);
if(easeUser == null){
easeUser = new EaseUser(hxId);
} else {
if(TextUtils.isEmpty(easeUser.getNick())){ // 如果名字为空,则显示环信号码
easeUser.setNick(easeUser.getUsername());
}
}
return easeUser;
}
OK,大功告成,到这里你的头像已经可以显示了。。恭喜你!
有朋友还有疑问,为什么头像是方形的,要变成圆形怎么办?指个路,在easeui中的utils包下,找到EaseUserUtils,这里就是显示用户头像和用户昵称的地方,通过Glide可以轻松显示圆形头像,百度有很多方法,这里就不多讲了,同样,你也可以通过其他图形加载框架来完成。
很多朋友不明白扩展消息的是什么东西,也不明白为什么照着代码敲就能够完成头像的显示,那下面就给大家粗浅的讲讲所涉及到的逻辑关系。
扩展消息:就是你每一次发送消息,都会附带在你发送内容上面的额外消息,他会随着你的内容发送出去,每次会多一点点流量,但微乎其微,个人认为并无多大影响。
显示头像逻辑:区分为本地化缓存和运行内存缓存,在demoHelper中可以发现,有一个成员变量private Map<String, EaseUser> contactList; 这个就是用来保存在运行内存缓存的,只有通过运行缓存,显示头像是最顺畅的,如果每次都从数据库中读取的话,聊天界面会比较卡,有朋友保存在SP里面,以ID做为KEY,头像和昵称等拼接做为value进行缓存,取出后再拆分开分别取值,据说是不卡,大家可以试一下。
         本地化存储是为了在每次打开软件的时候,从本地存储中拿到运行内存中使用做准备,大家可以认真看看demoHelper和MainActivity中的代码。

----------------------------------------------------------------------------------
讲了方法2设置头像,那么方法1,个人不推荐,但这里给出处理的过程,如有个别有需求的,可以按方法1来处理:
1、登录成功后,在手机子线程,访问你们的后台服务器,拿到所有好友的环信ID,头像,昵称。然后按上面的保存方法,保存到本地中,完成后发送广播或EventBus之类到,刷新消息界面和ChatFragment。
2、在getUserInfo中,写和上面扩展消息一样的内容,将如果运行缓存和本地都拿不到,则发起网络请求到后台获取头像,等请求到内容后,再发出广播或EventBus通知刷新消息界面和ChatFragment。

-------------------------------------------------------------------------------------
以上仅为本人在开发过程中的一点小小心得,demo中的保存运存和本地储存的方法,大家也可以单独写,并不会影响程序的运行,当然也有兄弟写过发表,大概根据实际情况择优选取即可。小弟学识浅薄,如果有错漏的,欢迎大家纠正。
如有需要,可以到494167135群中与大家交流学习。本人:乐奇奇,QQ:730326762。大家优先加群,谢谢。
  收起阅读 »

【节日海报】环信祝小伙伴们端午节快乐!

农历五月初五,是中国民间的传统节日——端午节,它是中华民族古老的传统节日之一。端午也称端五,端阳。此外,端午节还有许多别称,如:午日节、重五节,五月节、浴兰节、女儿节,天中节、地腊、诗人节、龙日等等。赛龙舟是端午节的习俗,也是汉族在端午节最重要的民俗活动之一,...
继续阅读 »
农历五月初五,是中国民间的传统节日——端午节,它是中华民族古老的传统节日之一。端午也称端五,端阳。此外,端午节还有许多别称,如:午日节、重五节,五月节、浴兰节、女儿节,天中节、地腊、诗人节、龙日等等。赛龙舟是端午节的习俗,也是汉族在端午节最重要的民俗活动之一,在中国的南方普遍存在,在北方靠近河湖的城市也有赛龙舟习俗,而大部分是划旱龙舟舞龙船的形式。俗话说“水能载舟,亦可赛艇”。环信祝福小伙伴们“端午节快乐”,在工作、学习和生活中都能保持“亦可赛艇”!


33期行业活动.jpg


  收起阅读 »

Android V3.1.3 release ,支持红包功能 、适配Android 6.0

新功能/优化: 消息支持按照本地时间或服务器时间排序 实时音视频支持动态码率 Demo支持红包功能(单聊及群聊红包) Demo适配了Android 6.0运行时权限,现在把targetSdkVersion设到23程序也能正常运行 Bug fix: 修...
继续阅读 »

24958PICwjQ_1024.jpg


新功能/优化:


消息支持按照本地时间或服务器时间排序
实时音视频支持动态码率
Demo支持红包功能(单聊及群聊红包)
Demo适配了Android 6.0运行时权限,现在把targetSdkVersion设到23程序也能正常运行



Bug fix:



修复自动同意好友请求有延迟的问题
修复在targetSdkVersion设为23时,视频通话可能crash的问题


 
版本历史Android更新日志 
 
下载地址sdk下载
 
关于新版sdk使用有任何问题或建议欢迎在下方评论留言   收起阅读 »

环信移动客服v4.7产品更新--新增全新的工作量报表

环信移动客服v4.7产品更新说明 新增功能 1 全新的工作量报表     1.1 工作量总和数据     1.2 新增会话和消息趋势图     1.3 新增24小时进线量分布图     1.4 新增会话标签分布图     1.5 新增会话数分...
继续阅读 »
环信移动客服v4.7产品更新说明


QQ截图20160523180624.jpg




新增功能


1 全新的工作量报表    
1.1 工作量总和数据    
1.2 新增会话和消息趋势图    
1.3 新增24小时进线量分布图    
1.4 新增会话标签分布图    
1.5 新增会话数分布图(按单条会话消息数)    
1.6 新增会话数分布图(按会话时长)    
1.7 新增在线时长数据    


 
优化功能    


 【优化】上下班时间设置    
 【优化】绑定微博渠道界面增加仅支持认证用户的提示    


进入体验最新功能吧:http://kefu.easemob.com/
 
  收起阅读 »

android开发查找聊天记录功能

由于环信本生没有开放出查找聊天记录的接口,但是需求要实现该功能,只能用其他方式实习。   实现聊天功能需要用到EventBus   EventBus的作用的是回来传值,因为本生的Intent传值,接受不了太多的数据。   1.当在聊天页面的时候,去2级页面,获...
继续阅读 »
由于环信本生没有开放出查找聊天记录的接口,但是需求要实现该功能,只能用其他方式实习。
 
实现聊天功能需要用到EventBus
 
EventBus的作用的是回来传值,因为本生的Intent传值,接受不了太多的数据。
 
1.当在聊天页面的时候,去2级页面,获取当前聊天的总数。

2.开启一个异步线程,new EMConversation 类,然后清空

3.在拿聊天页面的当前会话类mConversation .loadMoreMsgFromDB("", 5000) 一次拉取5000条数据出来
通过EventBug 发送出去(发送EventBug的时候不能用post,而且是用postSticky),通过bug应该知道,不知道的百度下。

4.clear  new 出的新会话类

5.在用当前聊天页面的会话类mConversation.loadMoreMsgFromDB("", mMsgCount); 获取本生显示的消息数据
然后在
mConversation.getAllMessages()
这样做的目的,主要是避免内存中出现重复的数据。
 
数据已经筛选出来了,现在开始模糊查询,跳到2级页面的时候,此时因为有EventBus收到的聊天消息,通过关键字段,来模糊出当前消息,附件有配图,匹配出来后,显示聊天的适配器中,附件有配图,后面下啦的,也会拉出最新的数据附件有配图 ,模糊查询和下拉更多,就是相关逻辑了,不多说直接贴代码。
 
1.通过关键字获取当前聊天记录的数据
EMMessage m = mSearchData.get(position); 
String msgId = m.getMsgId(); mIndex = 0;
/** * 获取当前所有数据的索引 */
mTempDatas.clear(); mTempDatas.addAll(mDatas);

for (int i = 0; i < mTempDatas.size(); i++) {
if (msgId.equals(mTempDatas.get(i).getMsgId())) {
mIndex = i;
break;
}
}

/** * 获取匹配到的数据并且获得最新的10条数据 */

for (int j = mIndex; j < mTempDatas.size(); j++) {
if (j == 10) {
break;
}
messages.add(mTempDatas.get(j));
}

mTempDatas.removeAll(messages);
Collections.reverse(mTempDatas);
mChatAdapter.notifyDataSetChanged();
vList_emm.setVisibility(View.GONE);
vHint.setVisibility(View.GONE);
findViewById(R.id.rela_edit).setVisibility(View.GONE); findViewById(R.id.btn_view).setVisibility(View.GONE);
listView.setSelection(0);

 
2.下拉更多
if (mTempDatas.size() == 0) {
IShowToast("已经加载完成");
}else
{

List<EMMessage> temps = new ArrayList<EMMessage>();
temps.addAll(messages);
messages.clear()
for (int i = 0; i < mTempDatas.size(); i++) {
messages.add(mTempDatas.get(i));
if (i == 10) {
break;
}
}

int index = 0;
index = messages.size();
Collections.reverse(messages);
mTempDatas.removeAll(messages);
messages.addAll(temps);
mChatAdapter.notifyDataSetChanged();
listView.setSelection(index);

}

 
 
大致这些了,这个代码排版好麻烦。。。为了方便看,只能一排排的粘贴
 
后期有空,会在教大家如何优化聊天适配器,如何快速扩展,使代码更加清晰。
 
 
  收起阅读 »

环信北京、上海、深圳三地千张《魔兽》电影票免费送,老司机们还等神马!!!

本人强力兽人战士,操作犀利,走位风骚,意识YD,输出恐怖。魂斗罗一命通,拳皇单手无限连,DOTA美杜莎守三路无人破,扫雷12秒通关最高难度;支持TS、UT、YY、环信音视频等多种语音工具,怕延迟可接受全程电话指挥,20秒内上线,会6国语言接受各国队伍,身强体壮...
继续阅读 »

9{75J~1G@J45FK6}3{JH38.png

	本人强力兽人战士,操作犀利,走位风骚,意识YD,输出恐怖。魂斗罗一命通,拳皇单手无限连,DOTA美杜莎守三路无人破,扫雷12秒通关最高难度;支持TS、UT、YY、环信音视频等多种语音工具,怕延迟可接受全程电话指挥,20秒内上线,会6国语言接受各国队伍,身强体壮能连续战斗100场不休息不上厕所, 语音指挥5小时不喝水,战士中的战斗机,全职业认识深刻,熟练魔兽各职业战斗理论,曾发表过《魔兽世界基本理论与数据分析》《论PVP先手与反手》《全职业技能压制与反压制大全》《环信移动客服集成全攻略》等PvP专业文章,会编程能按需制作专用lua插件,熟悉各种环信文档,熟练使用各种环信API接口调用,分分钟帮魔兽主题社交APP添加IM功能。门口已埋雷无人拜访,泡面矿泉水已备,自带发电机,水表在门外,有自用电源专用网络防断电断网......
 
	6月8日环信包场北京、上海、深圳三地影院免费送千张《魔兽》电影票,我答题抢到1张免费票,但是那天刚好去不了,因为要结婚。这张票抢的很辛苦,当时不知道刚好是自己的婚礼当天。现在我想问一下:“谁那天有空能替我结婚么?"




QQ截图20160602204728.jpg


那些年为了部落的程序猿们,你听到《魔兽》的召唤了么?For The Horde!!!


 
抢票规则


每人只有一次答题机会,共计10题。答题后将生成您的专属活动页面,可将页面分享给朋友,每个通过您的专属页面参与活动的用户答题数将累计至您的总答题数,我们将在活动截止前选取前1000名用户送出电影票。例如:A答对了8题,然后A将自己的专属页面分享到朋友圈被B看到,B通过此页面答对了7题,那么此时A累计答题数是8+7=15题,B此时答题数是7题。如果B再将自己的专属页面发给C,C答对6题,那么此时B的总答题数是7+6=13题,A的答题数依旧是15题。


 观影时间、影院地址



时间:6月8日
地点:北京(美嘉欢乐影城中关村店)/上海/深圳(太平洋国际影城天利店)


 
进入抢票地址: http://www.easemob.com/event/film_ms/ 收起阅读 »

Android项目从Eclipse导入到Android Studio中遇到的一些坑

之前开发环境刚从Eclipse切换到Android Studio时做过Android项目由Elipse导入到Studio的总结,也踩过几个坑,随手解决了。但是并没有记录下来。今天遇到一个需求需要导入到Studio中跑一下项目时,费了好长时间才弄成功。干脆,就把...
继续阅读 »
之前开发环境刚从Eclipse切换到Android Studio时做过Android项目由Elipse导入到Studio的总结,也踩过几个坑,随手解决了。但是并没有记录下来。今天遇到一个需求需要导入到Studio中跑一下项目时,费了好长时间才弄成功。干脆,就把踩过的几个坑记录一下,既可以回头来看,也兴许能帮助到别人。
1.项目中使用了其他library的项目.

我导的项目刚好就是在Eclipse中依赖了两个额外的独立的library;在Android Studio 选择条目界面选择从Eclipse项目中导入时,向下continue两步就会提示无法完成设置sdk,导入不成功。
解决方案:该问题的解决办法是,到原来的Eclipse项目中,把原来的依赖解除掉。具体的步骤为:在Eclipse项目根目录下的project.properties文件中将设置的target,以及android.library.reference全部注视掉,之后再次open选择项目,就能够导入打开。

2.依赖模块指定sdk版本不存在的问题

由于很多项目都有依赖,所以导入主module之后还要依次导入依赖的module,然后做依赖关联。在进行完这些操作之后,编译项目时,有可能还会编译失败,这个时候首先要检查一下依赖是否已经进行关联,其次要检查一下配置文件是否有误,也就是各个module的gradle文件以及整个项目的build.gradle文件;很多时候都是出在配置文件上。我遇到的问题是依赖的module导入后build.gradle中指定的编译sdk为10,我本地环境不存在该版本的sdk,所以编译失败不通过。
解决方案:根据我自己的经验,说下我的解决思路,不一定对,仅供参考。遇到编译失败的问题,首先就是看主module和各依赖module的依赖关系是否已经关联,然后查看各个module的配置文件以及整个项目的build.gradle,一般的编译失败在terminal中都会有相应的错误日志,以及提示如何修改,根据日志进行修改能更明确。另外,像我遇到的各个module中指定sdk版本不一致的问题,为了方便,可以对所有的module都指定统一的编译sdk,具体的做法就是在项目根目录下的配置文件中声明指定,然后在各个module的build.gradle文件中进行引用

3.某些png图片编译时提示:libpng warning : iCCP: Not recognizeizing known sRGB profile that has been edited问题

解决了上面的问题之后就是提示该问题了,还是编译失败。其实之前在别的项目中遇到过类似的警告,没有解决也能编译通过,但是今天编译走到这就提示这些,然后编译失败,没办法,只能解决了把该问题排除。具体的原因真没弄明白,看晚上的资料说新版本编译条件比旧版本要苛刻,所以会提示这个问题。下面说下该问题的解决办法。
解决方案:解决该问题需要借助一个图片编辑工具,我选择的是Image Maglick,下载安装没得说。然后打开终端,执行如下命令:
find <path to res folder> -name *.png -exec mogrify +profile sRGB {}  \;
等待命令执行完成即可。将terminal中提示的所有的目录都执行一遍该命令,然后再进行编译就可以。具体的上面这句命令的意思大概就是:删除所有png文件内的profile sRGB。在写这篇博客的时候,我重新搜了下别人提供的答案,有人说是5.0以后编译会提示该警告,所以还有一种方案是修改编译工具的版本由
buildToolsVersion "22.0.1" 改为 buildToolsVersion "20.0.0"
这种方案我并没有试过,有遇到该问题的可以尝试解决试试。
 
4.编译提示多个资源文件被重复定义的问题

有的时候编译失败之后,terminal内的编译日志会提示多个文件重复定义的问题,导致编译不能通过。
解决方案:该问题的解决方案很简单,也很暴力,直接根据提示重复的文件名找到主module中的该资源文件,删除即可。需要注意的是,删除时要删除主module中的资源文件,保留依赖module中的声明。因为在Android Studio中主module中能够引用依赖module中的资源,而依赖module中不能引用主module中的资源。

5.Java finished with non-zero exit value 1的问题

出现这个value 1的问题就是项目中有明确的报错的问题了。比如AndroidManifest文件中存在项目中已不存在的Activity声明,比如布局文件中引用资源错误,比如drawable提示找不到或者程序中有问题等,都是value 1的失败提示。总之,value 1的问题一定是程序中有明显的错误,Android Studio编译时检查比Eclipse要严格,所以就会提示错误,这个需要自己根据自己的项目去找,思路同样也是结合terminal日志提示,外加从配置文件到程序,依次进行。

6.Java finished with non-zero exit value 2的问题

出现value 2的问题原因比较好找,就是jar包冲突,出现此问题意味着项目中引用了重复的jar包。通常最最常见的jar包冲突就是v4包的冲突。我们的项目中配置别人的依赖时也遇到过v4的冲突,这个需要在配置里面将v4去除。具体的配置格式大概如下:
compile('cn.trinea.android.view.autoscrollviewpager:android-auto-scroll-view-pager:1.1.2') {    
exclude module: 'support-v4'
}
我遇到的大概的就是这几类问题,可能有重复,也肯定是不全。有误的地方欢迎追加指出。

本文作者喜欢而非坚持
  收起阅读 »

环信编程大赛优秀开源项目之季军:咚咚,一款专注团队高效沟通的移动客户端

根据IDC数据显示,中国有近200万开发者,身为一个程序员,我们生活在一个 IT 系统越发复杂且多变化的时代。有时候执行一个简单的开源项目,开发一个基础功能都需要精准定义并耗费大量时间专注任务。随着云计算的兴起,API 和SDK开始作为软件之间重要媒介而作为一...
继续阅读 »
根据IDC数据显示,中国有近200万开发者,身为一个程序员,我们生活在一个 IT 系统越发复杂且多变化的时代。有时候执行一个简单的开源项目,开发一个基础功能都需要精准定义并耗费大量时间专注任务。随着云计算的兴起,API 和SDK开始作为软件之间重要媒介而作为一种独立应用而存在,“一切皆软件,一切皆API,一切皆SDK”。通过API和SDK可以让开发者摆脱繁重的基础功能底层开发,短时间即可让App拥有各种诸如内置IM、统计等基础功能组件能力。 

5月14日,由环信联合猿圈共同推出的“首届环信编程大赛”颁奖典礼在中关村义创空间隆重举行。本次环信编程大赛历时两个月,由线上初赛、决赛和颁奖典礼三个环节组成,总计报名人数2000+,收到决赛项目100+。最终由评委会认定的13个优秀开源项目及开发者集体亮相颁奖典礼。其中“方圆十里”、“高仿微信“和“咚咚”三个开源项目名列前三,共同分享了15000元奖金和价值12000元的专属表情包。



501377d7f597821d8d48d87e2f0c85d2[1].jpg


优秀项目开发者合影




tmpdir--16_6_1_15_39_16.jpg


小鲜肉可畏,“咚咚”项目负责人95后蔡斯仪分享技术开发细节


其余入围的十余个优秀开源项目同样引起了到场开发者的热烈追捧,环信将分期将入围的优秀项目代码免费开源给小伙伴们。今天我们带来的是本次环信编程大赛的季军选手——咚咚,一款专注团队高效沟通的移动客户端。咚咚基于环信平台进行开发,旨在打造一款团队高效沟通的移动客户端,供企业内部协作使用,适应移动办公需要,提升企业沟通协同效率,增强企业办公管理效率。


 


6963.tmp_.jpg



“咚咚”APP界面截图


1.软件介绍


咚咚基于环信平台进行开发,旨在打造一款团队高效沟通的移动客户端,供企业内部协作使用,适应移动办公需要,提升企业沟通协同效率,增强企业办公管理效率。(咚咚一期实现了用户登入登出功能、通讯功能、投票功能。)


2.功能介绍


一、用户登录注册功能

1.系统登录界面

2系统注册界面

注册功能实现:限制账号长度必须为11位,出生日期选择,头像选择(从系统自带头像中选择)

3.系统首页

登陆成功,即进入系统主页面

4.个人信息查看及修改

进入主页面后点击个人信息查看,即可查看相关信息,并对其进行修改

主界面

(1)头像修改功能实现

(2)名字修改功能实现

(3)部门修改功能实现

(4)性别选择功能实现

(5)个性签名修改功能实现

二、通讯功能

主界面

功能实现:群组聊天,单对单私人聊天,查看好友列表,查看好友详情,查看群组详情

三、投票功能

主界面

功能实现:展示用户发起的投票列表,新增投票,投票提交


3.使用技术


环信即时通讯云


4.作者心得


咚咚基于环信即时通讯云平台,避开了即时通讯等繁杂底层技术开发,使得项目的难点得以轻松解决。例如在平台上可以快速使用即时通讯功能、用户好友管理以及群组管理等功能,让项目得以快速开发成型,产品团队只需要专注于APP核心业务层开发即可,也给移动互联时代的APP开发指出了一条明路。




f5b3ff1766d133aa66bec8ad18f9ddb0[1].jpg



 特别感谢以下企业的大力支持:


义创空间提供颁奖场地
 
萌岛从自有形象库中授权一套价值12000元的表情包
 
Emokit赞助Apple Watch一台
 
猿圈全程提供技术评测支持


 git源码下载https://github.com/caisiyi/SYTeamApp
 
更多开源项目请点击http://community.easemob.com/article/825307813
 
咚咚项目作者演讲PPT下载↓↓↓
  收起阅读 »

环信DEMO群里要发红包啦~~~

本周环信将携手云账户,在环信demo里进行福利红包大派送。   这里不光可以聊工作,还能每天抢红包。环信承诺0成本+3小时,还能让你的APP轻轻松松接入红包功能。 活动参与指南: 打开环信demo, 记住是打开环信即时通讯云...
继续阅读 »
本周环信将携手云账户,在环信demo里进行福利红包大派送。
 

43期_产品快递.jpg



这里不光可以聊工作,还能每天抢红包。环信承诺0成本+3小时,还能让你的APP轻轻松松接入红包功能。

活动参与指南:



打开环信demo, 记住是打开环信即时通讯云DEMO哦,环信即时通讯DEMO哦,环信即时通讯云DEMO哦,搜索公开群申请加入(注,为防止出现领取不到红包,请更新到最新版本demo,最新版本demo下载地址:点击进入下载页面
 
还没有安装的,可以扫码下载安装哦:  


QQ截图20160531200332.jpg



加群: 

环信红包群①
1464 6843 22478 (搜索时数字间没有空格)


环信红包群②
1464 6873 54665 (搜索时数字间没有空格)

 


未命名.jpg




红包已准备好,就等你来抢!


 关于环信红包:
环信与云账户联手打造聊天红包,红包SDK产品已迭代至2.0版,目前已为拉拉公园,YOU+公寓等近100家APP接入红包功能,用户留存和活跃度大幅提升。 
收起阅读 »

楚楚街10亿元C轮融资,90后移动电商独角兽的背后!

互联网+时代,传统电商格局已定,移动电商领域仍然风起云涌。近日,定位90后个性化需求的移动电商楚楚街已完成10亿元C轮融资。本轮融资由软银中国资本、新天域资本领投,基石资本、钟鼎创投跟投。在“资本寒冬”阴影还未完全散去的当下,楚楚街的大手笔运作可谓一剂市场强心...
继续阅读 »
互联网+时代,传统电商格局已定,移动电商领域仍然风起云涌。近日,定位90后个性化需求的移动电商楚楚街已完成10亿元C轮融资。本轮融资由软银中国资本、新天域资本领投,基石资本、钟鼎创投跟投。在“资本寒冬”阴影还未完全散去的当下,楚楚街的大手笔运作可谓一剂市场强心针。同时其估值将接近70亿元,已经成长为90后移动电商领域的首个“独角兽”。

楚楚街专注于商品特卖领域,定位为时尚化、年轻化、全球化的移动电商平台,提供的服务主要包括 “全球购”、“逛啦” 及 “限时抢购” 等板块。目前,楚楚街在移动端的安装量已经超过1亿,年销售额超过50亿,平均每天超过1300万成交额。近期楚楚街将推出“609”大促来回馈客户,谁都可以“买买买”,但不是谁都可以“买的漂亮”!

ZJT77AY[U3S{VFQXO@C@5MH.png


对于楚楚街来说,每天千万级的成交额意味着每天有百万量级的下单量。这么庞大的交易规模下,需要非常稳定且能支持千万级高并发的客服技术来支撑楚楚街数万家商户完成销售闭环,这其中环信移动客服功不可没。更重要的是,快速成长的楚楚街,在客服业务上的需求以及环信提供的全媒体智能云客服解决方案,对于其他电商平台具有很强的借鉴意义,所谓窥一斑而知全豹。

在即将到来的609年中购物嘉年华中,楚楚街将带来5亿红包的史上最大力度的优惠,而环信提供的全媒体智能云客服解决方案也与楚楚街联手,为本次的“嘉年华”助力。6月7日到6月9日期间,楚楚街13大分会场+3个特别会场将全面启动,满足用户“买买买”的夏日疯狂购物需求,而“千款单品秒杀,爆款商品凑单”的重磅优惠,更是最大程度为消费者谋取福利。

初衷:帮助商户构建APP内销售闭环,用环信移动客服升级服务

楚楚街决心在自己平台中,植入在线客服功能,为商户打造一个在APP内就能完成销售闭环的客服系统。顾客无需跳出APP,在商品展示页或是订单页即可一键呼唤客服,这样就使顾客和商家之间无缝连接,让交易更加简单。

将客服功能内置入APP中,最基础的一点是要求APP具备即时通讯功能,能够实时进行商家和顾客之家的图文消息传递。但楚楚街希望为商户打造的是一个专业的客服系统,这就要求有更多的专业功能。

梳理业务需求,确定客服功能需求

在确定植入在线客服系统后,下一步要做的就是根据楚楚街的业务模式,确定客服系统功能需求。对于楚楚街这样的电商平台而言,客服系统除了会话排队、客服质检等基础功能外,又有独特的功能需求:

· 多租户支持

由于楚楚街上有数万家商户,如果让每个商户自己搭建客服系统,不仅技术上难以实现,经济上也不现实。因此,楚楚街从一开始就选择了“统一平台接入,独立运作管理”的客服平台建设思路。所有商户共享同一客服平台,但每个商户又是以“租户”的形式独立运作管理的,这种集约化的建设方式为商户解决了技术和成本难题。

· 营销型客服

对电商而言,在线客服人员提供售后服务外,更多时候承担的是销售的角色,为客户提供售前咨询服务,这是和传统商业的一个很大不同。因此,在线客服系统应当从销售的角度,具备部分营销功能。

众多SaaS客服厂商,楚楚街为什么选择环信?

当前SaaS客服厂商众多,楚楚街在经过细致评比、测试后,最终选择环信。看中的是环信解决方案的先进性、稳定性以及与楚楚街电商经营理念的契合。

楚楚街选择的是环信多租户版的全媒体移动客服,可以在一个平台上对所有商户的客服进行管理,包括商户账号设置、权限分配等。同时,每个商户也可以自行进行配置调整,如客服人员数量,接待量设置等。这样既实现了统一部署的集约化优势,又满足商户的个性化需求。

对商户而言,无需任何技术投入,马上就可以享受全媒体客服带来的优势。首先是服务渠道的一体化,不论客户是来自楚楚街APP端,还是来自商户微信公众号、微博,甚至是商户自己门户网站,都可以实现客户请求的统一接入,统一分配和统一管理。其次是服务的专业化,可以进行客户画像、订单轨迹、服务质检、报表统计等,使商户拥有了媲美大型呼叫中心的的服务品质。环信提供的智能机器人更是帮助客服大幅降低了人工服务成本,80%的重复问题机器人都可以帮助解答。在下班时间,可以由智能机器人代替人工客服,提供7*24的不间断服务。

KHJT)A13E(VG6(ARW()QE.png


除了与楚楚街的客服需求匹配度最高外,环信还具备一系列与其它同类厂商相比的竞争优势:

· 独有的主动回呼技术,帮助商户实现轻松营销

环信基于自有的长连接技术优势,在移动客服产品上提供了一个主动外呼功能,帮助商户轻松实现客服营销。长连接技术的关键之处在于为每一个客户维持一个TCP连接,可以随时向客户端发送消息(当然,这种设计对后台系统资源和调度能力有很高要求)。客服人员可以把最新的促销以后台消息的形式发送给指定人群。例如,把一款儿童座椅的促销信息发给妈妈用户。如果客户端没有打开APP,那么会在系统通知一栏呈现出该消息,客户可以在闲暇时候再打开看。这是一种无干扰式的营销手段,配合客户画像,就能实现具有良好用户体验的精准式营销。

此外,主动回呼还可以帮助商家提高订单成交率。根据统计,楚楚街每日近百万订单中,问题订单大概占千分之五,也就是说每天就会产生数千个有问题的订单。有主动回呼功能,就可以在出席问题订单时,主动联系客户,从而提高成交率,同时提供了更好的客户体验。

· 强大技术支撑,满足楚楚街未来发展预期

随着交易平台的扩大,对客服系统的即时消息传递能力也将提出更高的要求。在所有SaaS客服厂商中,环信是唯一一家同时拥有即时通讯云PaaS产品和SaaS客服产品的厂商,即时通讯技术是环信的起家之本。截至2015年12月,环信即时通讯SDK已覆盖手机终端3.19亿,日发送消息2.1亿,这些数据展示了环信即时通讯技术的强劲性能和扩展性,这是和竞争对手在技术层面的最大差异,因为大多数友商并不具备即时通讯能力,需要借用第三方平台。对于楚楚街而言,选择环信,意味着性能将不会成为担心的问题,能够为楚楚街未来快速扩张扫除障碍。

· 历经市场检验,产品成熟度更高

楚楚街在产品选型时,除了考虑功能、性能外,还尤其关注产品的稳定性。在APP中,每一次和客服的对话对商户而言都是一个潜在的商机,一旦出现服务中断或是消息丢失,损失的不单是客户体验,更是销售机会。而环信在产品稳定性和成熟度上,则明显胜于竞品。根据易观智库2016年1月的数据,在移动端SaaS客服市场,环信的市场占有率高达77.4%,产品稳定性、成熟度已经经过了海量用户的检验。

帮助商户省钱、省时、省力,楚楚街成长为90后移动电商首个独角兽!

楚楚街能够持续增长,赢得资本青睐,背后靠的是数万家商户支撑起的商业生态。通过部署环信移动客服,楚楚街使自己平台的商业价值得以充分体现: 

· 通过在一个平台内管理所有商户客服,楚楚街共创立商户客服账号20000多个,节省60%客服体系搭建成本。

· 强大的移动端客服SDK,保证了楚楚街高并发性、稳定性和可靠性,降低了20%的投诉率,提高了40%的访客转化率。

· 商户客服利用环信客服系统回呼500+次/日,减少退换货率79%。

对楚楚街而言,用移动客服帮助他的客户——也就是平台上数万家商户实现商业成功,是实现自己成功的基石,合作共赢,方能实现最大价值。 收起阅读 »

有一种六一节叫别人家的公司

员工六一假+迪斯尼礼物,环信你值得加入!环信祝福所有小朋友们六一快乐 [[礼物]] 想要工作家庭两不误还等神马?猛戳大图或者登陆拉勾网了解环信最新招聘信息tanlent@Easemob.com [[来]]  

ea790d9djw1f4djxhn24vj20c80fpgmr.jpg


员工六一假+迪斯尼礼物,环信你值得加入!环信祝福所有小朋友们六一快乐 [[礼物]] 想要工作家庭两不误还等神马?猛戳大图或者登陆拉勾网了解环信最新招聘信息tanlent@Easemob.com [[来]]

ea790d9djw1f4djxidvx4j20qo1bfgt8.jpg




ea790d9djw1f4djxj1kcyj20qo0yudn5.jpg




ea790d9djw1f4dk9rvtiyj20hs4vowr5.jpg


 

ios V3.1.3 release 支持ipv6

  IOS V3.1.3 2016-5-27 更新日志    新功能: SDK增加实时视频通话切换摄像头功能。 SDK支持ipv6。 消息支持按照本地时间或者服务器时间排序。 Demo支持单聊发送红包和群聊发送红包。 bug fix: 修复自动同意好友...
继续阅读 »

24958PICwjQ_1024.jpg


 
IOS V3.1.3 2016-5-27 更新日志 
 
新功能:


SDK增加实时视频通话切换摄像头功能。
SDK支持ipv6。
消息支持按照本地时间或者服务器时间排序。
Demo支持单聊发送红包和群聊发送红包。



bug fix:


修复自动同意好友请求有延迟的问题。



SDK细节调整:


SDK将第三方依赖从SDK静态库分离出来(libssl.a,libcrypto.a,libcurl.a)


版本历史:ios更新日志ios更新日志
下载地址:sdk下载
 
关于新版sdk使用有任何问题或建议请在下方评论留言 收起阅读 »

EMContactManager.getInstance().addContact 怎么判断是否发送成功啊

EMContactManager.getInstance().addContact  怎么判断是否发送成功啊
EMContactManager.getInstance().addContact  怎么判断是否发送成功啊

找一家靠谱SaaS服务商的五个关键因素

如今,企业都在争先恐后采用云来改造其混合架构以获取竞争优势,而SaaS(软件及服务)的出现使得企业能够轻松驾驭“云”这匹“快马”。SaaS目前被公认为最有效的帮助企业创新的方案,其通过分发IT和商业应用方案为企业解决问题,同时还能提供极佳的用户体验。 ...
继续阅读 »
如今,企业都在争先恐后采用云来改造其混合架构以获取竞争优势,而SaaS(软件及服务)的出现使得企业能够轻松驾驭“云”这匹“快马”。SaaS目前被公认为最有效的帮助企业创新的方案,其通过分发IT和商业应用方案为企业解决问题,同时还能提供极佳的用户体验。



QQ截图20160603173526.jpg



无论是通过公有云还是私有云,SaaS都能提供最及时的信息反馈和对资源的优化。而IT技术与业务范围实现精准对接使得使用者在云端拥有提供快速、高质量的应用开发的能力,这也让企业能够实现更高的运营效率。SaaS能够通过永远在线(always-on)、和即时启动(instant-on)应用分发管理工具帮助开发人员更快更高效地实现代码编写。

然而,企业管理者也不能过于随意地购买SaaS服务,您应当选择最适合您企业并能帮您的企业持续进步的服务供应商。

规划您自己的云服务实现路线

首先您要明白一点,实现混合云方案需要克服种种困难,这一点非常重要,而且您的云服务的实现路线与其他企业都会有差异。这条路线的规划受制于您的目标、成熟度、风险预测等等其他因素。所以从一开始您就要做好规划,以避免昂贵的试错成本。

挑选SaaS服务商 您需要关注哪些?

在挑选SaaS服务商的时候,您需要关注五个关键点,这五点都是SaaS应该能够给予您的看得见摸得着的对业务的帮助。在讨论这五点之前,我想要告诉您一个合格的SaaS服务提供商应该为您做什么?

一家合格的SaaS服务提供商应该能够接管您所有设施的维护和升级工作。他们应该能够高质量地保证服务的持续可用。
 
在选择SaaS服务商应关注以下这些方面
 
他们有稳健的全球性的服务拓展规划,以满足您企业在地区的和全球的商业需求;(环信全球拥有6个数据中心,包括北美、法兰克福、新加坡、日本、以及北京和杭州数据中心。海外AWS数据中心plus海外代理,拓展海外用户自此无忧。)

·拥有标准的、类似于Web服务的集成API,可以最大化地利用市场上可用的一切技术和能力;(环信免费提供上百种API功能,包括单聊、发送文字/表情/语音/地理位置/照片/视频/名片/自定义扩展消息、消息回执、集成第三方用户体系、群聊、离线消息、离线消息推送、实时音频/视频......

·这个合作伙伴与您的业务共同成长进步,这样您就可以在需要获得服务时发挥非核心职能,同时能够获得最为精确的相应等级和数量的服务。(环信CSM团队和技术支持团队7*12小时帮助客服成功

避免碎片化的SaaS解决方案极其关键,因为它们会导致您管理复杂度和成本的上升。您应该及时获取您所需要的服务,从而在操作成本和经济成本上的双重获益。
 
好的SaaS服务商应给予您的五大帮助
 
1、在您需要帮助的时候,它可以立刻给您相应的帮助。我们相信一个好的SaaS提供商应该保证您把所有注意力放在业务上,而从不需要为支持业务的软件和系统的维护发愁。需要什么,它就立刻能给您什么,不多也不少。

2、可以简便快速地访问SaaS服务。我们相信好的SaaS服务可以快速便捷的得以访问,就像使用内部应用一样。这些服务应该与已有的工作流程和谐共存。这一点的实现靠的是SaaS的提供商能够简化业务的集成流程,在增加SaaS服务的同时保护您对于已有业务的投资,并扩展您的混合网络架构。

3、保证您一直能用上最新的软件,没有延迟,也没有升级带来的麻烦。企业应用的升级通常既费时又费钱,因此许多时候因为困难和经费导致升级不及时,企业就没有办法享受到最新的功能。

4、选择已获得安全认证的企业。我们承认找一家世界级的SaaS服务提供商利用其数十年的专业运营经验来提供企业级的服务的要求有点离谱了,很少有供应商能有10年以上的SaaS服务经验并拥有服务多个客户的全球数据中心,且能够立即扩展满足您的业务需求。

5、在数据中心的管理之中追求最高服务质量和创新。我们相信数据中心的建设和管理实践对于维持最佳的服务和创新同等关键,您应该充分信任SaaS提供商的专业能力,而不是对于所有事情都亲力亲为。(环信编译自 http://community.hpe.com/ 如有转载请注明出处。) 
环信成立于2013年4月,是一家全通讯能力云服务提供商。产品包括全球最大的即时通讯云PaaS平台——环信即时通讯云,以及全球首创的全媒体智能云客服平台——环信移动客服。现已覆盖包括电商、O2O、互联网金融、在线教育、在线旅游、移动医疗、智能硬件、游戏等20大领域的Top10客户,典型用户包括国美在线、58到家、快牙、随手记、猎聘、海尔、神州专车等。截至2015年底,环信共服务了50833家 App 客户,SDK覆盖手机终端3.19亿台,平台日均发送消息2.1亿条。


JBOA)0)5DRO3H{JPDKLPM73.png


  收起阅读 »

Fmpeg惊爆漏洞:环信现有用户完全不受此漏洞影响!

 据国内媒体报道,近日全球领先的多媒体框架FFmpeg被曝出漏洞,通过该漏洞可在播放漏洞视频或在转码过程中触发本地文件,读取获得指定文件。FFmpeg已于4月发布更新,但仍有大量Android及iOS APP使用该开源程序用于播放功能。因为开放源码的便利性和强...
继续阅读 »

1.jpg


 据国内媒体报道,近日全球领先的多媒体框架FFmpeg被曝出漏洞,通过该漏洞可在播放漏洞视频或在转码过程中触发本地文件,读取获得指定文件。FFmpeg已于4月发布更新,但仍有大量Android及iOS APP使用该开源程序用于播放功能。因为开放源码的便利性和强大的多媒体功能,FFmpeg被广泛用于Android及iOS APP的播放功能,百度云、爱奇艺视频、网易云音乐、斗鱼TV、疯狂猜词等多款手机用户常用的APP均使用了FFmpeg库文件,大量用户可能受此漏洞威胁。


2.jpg



使用率最高的FFmpeg文件库top10



3.jpg



市场占有率高真不是宝宝的错

360互联网安全中心对国内主流应用市场的124371款app进行扫描,发现有超过6000款应用受此漏洞影响,占到总数的5%,受影响APP类型涵盖各个类型,其中仅通讯社交、便捷生活、影音视听三类就占到一半以上,进一步对受漏洞影响的6314款app所使用的ffmepg库文件分析发现,使用率最高的libeasemod_jni.so属于环信SDK的库文件,排名第二的libcyberplay-core.so均为目前最流行的视频SDK,拥有千万级的用户量。



4.jpg




本地打开m3u8文件并使用FFmpeg解析时就会触发漏洞 

该漏洞与HLS协议的.m3u8文件相关。攻击者可以制作一个特殊的.m3u8或其他视频文件,当ffmpeg播放此特殊文件时会把本地文件内容传送到远程服务器上。

环信SDK只使用FFmpeg作解码和录制之用,没有包含HLS相关功能,也没有对外提供任何播放视频文件的api,所以该漏洞对环信用户是没有任何影响的!

环信音视频专家通过跟360互联网安全中心工程师沟通发现,360互联网安全中心是通过检测FFmpeg版本号来判断app是否受此漏洞影响,由于环信SDK所用的FFmpeg版本较老,被360互联网安全中心误认为受此漏洞影响。

综上,环信现有用户完全不受此漏洞影响!!!环信现有用户完全不受此漏洞影响!!!环信现有用户完全不受此漏洞影响!!!重要事情说三遍!!!

后续环信的SDK也会接受360互联网安全中心建议升级到FFmpeg新版本。


漏洞新闻: http://www.ccstock.cn/finance/minshengxiaofei/2016-05-24/A1464060408687.html

俄罗斯工程师最先发现此漏洞新闻:https://habrahabr.ru/company/mailru/blog/274855/ 收起阅读 »

服务器端:关于Request body is invalid.解决

Request body is invalid是由于发起访问前会验证请求体是否为空及类型是否正确。 1、需要检查自己是否传递的参数 2、需要检查类型是否正确   环信给的java代码中有个小bug 在很多API实现中并没有传递body:例如Easem...
继续阅读 »
Request body is invalid是由于发起访问前会验证请求体是否为空及类型是否正确。
1、需要检查自己是否传递的参数
2、需要检查类型是否正确
 
环信给的java代码中有个小bug
在很多API实现中并没有传递body:例如EasemobIMUsers中的getIMUsersByUserName方法中


1.png


但是请求环信api之前增加了校验:HttpClientRestAPIInvoker
看图:

2.png




3.png


 
 
解决方案:
方案1:注释body校验的代码
方案2:添加body信息
  收起阅读 »

android中如何显示开发者服务器上的昵称和头像

本文方法已经废弃!请看作者另外一篇文章: 在android中5分钟实现昵称头像的显示  http://www.imgeek.org/article/825308757 无论是IOS还是安卓,集成环信SDK遇到的第一个问题,就是如何显示自有用户体系中的昵称...
继续阅读 »


本文方法已经废弃!请看作者另外一篇文章:



在android中5分钟实现昵称头像的显示  http://www.imgeek.org/article/825308757


无论是IOS还是安卓,集成环信SDK遇到的第一个问题,就是如何显示自有用户体系中的昵称和头像。运行环信的demo app,注册用户是直接使用环信ID(username)作为用户名,但是在我们实际应用中,需要将自有用户体系的UserId生成GUID作为环信ID(username)【参考:http://docs.easemob.com/im/100serverintegration/20users】,这时候如果不经过处理,则会显示如下界面:

444.png



那么如何处理,才能显示正确的用户昵称和头像呢?
其实官方已经提供有解决方案了,只不过没有给出示例代码而已。
http://docs.easemob.com/im/490integrationcases/10nickname
 
引用一下关键文字:
方法二:从消息扩展中获取昵称和头像

昵称和头像的获取:把用户基本的昵称和头像的URL放到消息的扩展中,通过消息传递给接收方,当收到一条消息时,则能通过消息的扩展得到发送者的昵称和头像URL,然后保存到本地数据库和缓存。当显示昵称和头像时,请从本地或者缓存中读取,不要直接从消息中把赋值拿给界面(否则当用户昵称改变后,同一个人会显示不同的昵称)。
昵称和头像的更新:当扩展消息中的昵称和头像 URI 与当前本地数据库和缓存中的相应数据不同的时候,需要把新的昵称保存到本地数据库和缓存,并下载新的头像并保存到本地数据库和缓存。
 
没错,官方提供了两种思路,鉴于项目的实际情况,我选择了【方法二】。
于是,无论安卓,还是IOS,我们都是这样处理的:
【1】.APP间传递用户属性信息:发送(文本、图片...)消息时,要在消息扩展(message.ext)中附带当前用户的属性信息;
【2】.本地缓存用户信息:在接收消息的回调函数里,读取消息扩展(message.ext)里用户属性(键值对)信息;如果本地缓存(sqlite)不存在该用户,则新增缓存记录,如果存在,则更新记录;(用户登录或注册成功后,也要更新用户缓存信息)
【3】.获取用户属性信息:在需要显示昵称的地方,根据环信ID,读取sqlite缓存数据,获取用户昵称和头像;
 
用户属性信息:ChatUserId(环信ID),ChatUserNick(用户昵称), ChatUserPic(用户头像,完整的url地址);
 
关键代码:
/***************** 环信用户缓存信息 *******************************/
public static final String ChatUserId = "ChatUserId";// 用户的环信ID
public static final String ChatUserPic = "ChatUserPic";
public static final String ChatUserNick = "ChatUserNick";
/***************** 环信用户缓存信息***********end ********************/



UserInfoCacheSvc.java是环信用户信息缓存管理类
/**
* 缓存用户信息(主要用于聊天显示昵称和头像)
*/
public class UserInfoCacheSvc {
public static List<UserApiModel> getAllList(){
Dao<UserApiModel, Integer> daoScene = SqliteHelper.getInstance().getUserDao();
try {
List<UserApiModel> list = daoScene.queryBuilder().query();
return list;
} catch (SQLException e) {
e.printStackTrace();
}

return null;
}

public static UserApiModel getByChatUserName(String chatUserName){
Dao<UserApiModel, Integer> dao = SqliteHelper.getInstance().getUserDao();
try {
UserApiModel model = dao.queryBuilder().where().eq("EaseMobUserName", chatUserName).queryForFirst();
return model;
} catch (SQLException e) {
e.printStackTrace();
}

return null;
}

public static UserApiModel getById(long id){
Dao<UserApiModel, Integer> dao = SqliteHelper.getInstance().getUserDao();
try {
UserApiModel model = dao.queryBuilder().where().eq("Id", id).queryForFirst();
return model;
} catch (SQLException e) {
e.printStackTrace();
}

return null;
}

public static boolean createOrUpdate(String chatUserName, String userNickName, String avatarUrl){
try {
Dao<UserApiModel, Integer> dao = SqliteHelper.getInstance().getUserDao();

UserApiModel user = getByChatUserName(chatUserName);

int changedLines = 0;
if (user == null){
user = new UserApiModel();
user.setUsername(userNickName);
user.setHeadImg(avatarUrl);
user.setEaseMobUserName(chatUserName);

changedLines = dao.create(user);
}else {
user.setUsername(userNickName);
user.setHeadImg(avatarUrl);
user.setEaseMobUserName(chatUserName);

changedLines = dao.update(user);
}

if(changedLines > 0){
Log.i("UserInfoCacheSvc", "操作成功~");
return true;
}
} catch (SQLException e) {
e.printStackTrace();
Log.e("UserInfoCacheSvc", "操作异常~");
}

return false;
}

public static boolean createOrUpdate(UserApiModel model){

if(model == null) return false;

try {
Dao<UserApiModel, Integer> dao = SqliteHelper.getInstance().getUserDao();

UserApiModel user = getById(model.Id);

if (!StringUtils.isNullOrEmpty(model.getHeadImg())){
String fullPath = "http://image.baidu.com" + model.getHeadImg();
//特别注意:这里用是图片的完整链接地址,如果要取缩略图,需要服务端配合;

model.setHeadImg(fullPath);
}
int changedLines = 0;
if (user == null){
changedLines = dao.create(model);
}else {
model.setRecordId(user.getRecordId());
changedLines = dao.update(model);
}

if(changedLines > 0){
Log.i("UserInfoCacheSvc", "操作成功~");
return true;
}
} catch (SQLException e) {
e.printStackTrace();
Log.e("UserInfoCacheSvc", "操作异常~");
}

return false;
}

}
首先要在用户登录或注册成功后,返回用户登录信息Model时,缓存一下用户信息,@DatabaseField注解是ormlite库的特性,文章后面的附件包含了此类库:
public class UserApiModel implements Serializable {

@DatabaseField(generatedId=true)
private int RecordId;

@DatabaseField

public long Id;

@DatabaseField
public String Username;

@DatabaseField
public String Email;

@DatabaseField
public String HeadImg;

@DatabaseField
public String EaseMobUserName;

@DatabaseField
public String EaseMobPassword;
}
 
private static void SaveUserInfo(UserApiModel userInfo){

if(userInfo == null) return;

// 缓存用户信息
PrefUtils.setUserId(userInfo.Id);
PrefUtils.setUserEmail(userInfo.Email);
PrefUtils.setUserName(userInfo.Username);
PrefUtils.setUserPic(userInfo.HeadImg);
PrefUtils.setUserChatId(userInfo.EaseMobUserName);
PrefUtils.setUserChatPwd(userInfo.EaseMobPassword);

UserInfoCacheSvc.createOrUpdate(userInfo);
}
然后在接收环信消息的回调函数里保存用户信息,ActyMain.java是我们项目的主框架,我们在这里写了回调函数,无论群聊还是单聊消息,都会调用这里:
private EMMessageListener mMessageListener = new EMMessageListener() {

@Override
public void onMessageReceived(List<EMMessage> messages) {
// 提示新消息
for (EMMessage message : messages) {

// 先将头像和昵称保存在本地缓存
try {
String chatUserId = message.getStringAttribute(SharePrefConstant.ChatUserId);
String avatarUrl = message.getStringAttribute(SharePrefConstant.ChatUserPic);
String nickName = message.getStringAttribute(SharePrefConstant.ChatUserNick);

UserInfoCacheSvc.createOrUpdate(chatUserId, nickName, avatarUrl);

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

ChatHelper.getInstance().getNotifier().onNewMsg(message);
}
refreshUIWithMessage();
}

// 此处省略N行代码....
};
在安卓项目里,环信页面显示昵称和头像时,都会统一从DemoHelper的getUserInfo函数里获取信息,所以我们要在这里从缓存取用户头像和昵称:
private EaseUser getUserInfo(String username){
//获取user信息,demo是从内存的好友列表里获取,
//实际开发中,可能还需要从服务器获取用户信息,
//从服务器获取的数据,最好缓存起来,避免频繁的网络请求
EaseUser user = null;
// 从缓存里取昵称和头像
UserApiModel userInfo = UserInfoCacheSvc.getByChatUserName(username);
if (userInfo != null){
user = new EaseUser(username);
user.setAvatar(userInfo.getHeadImg());
user.setNick(userInfo.getUsername());
}

return user;
}
最后,为了让另外一个客户端也能正确显示头像和昵称,app发送消息时,要在消息扩展里附带用户信息,代码写在
ChatFragment.java里:
    @Override
public void onSetMessageAttributes(EMMessage message) {
setUserInfoAttribute(message);
}

/**
* 设置用户的属性,
* 通过消息的扩展,传递客服系统用户的属性信息
* @param message
*/
private void setUserInfoAttribute(EMMessage message) {
try {
message.setAttribute(SharePrefConstant.ChatUserId, PrefUtils.getUserChatId());
message.setAttribute(SharePrefConstant.ChatUserNick, PrefUtils.getUserName());
message.setAttribute(SharePrefConstant.ChatUserPic, "http://image.baidu.com" + PrefUtils.getUserPic()) ;//这里用是图片的完整链接地址,如果要取缩略图,需要服务端配合;

} catch (Exception e) {
e.printStackTrace();
}
}
有不当之处,欢迎指正~~~谢谢
部分源码因为是公司项目代码,所以没有在文章里提供(而且代码量太多,也不便于阅读)。需要源码的童鞋,可以联系我。等我有空,我整理一下公司项目,然后再share开源给大家。
相关类文件下载:


 
本文方法已经废弃!请看作者另外一篇文章:
在android中5分钟实现昵称头像的显示  http://www.imgeek.org/article/825308757


如有任何问题,请咨询【环信IM互帮互助群】,群号:340452063,
或者加本人QQ:364223587,加Q请认准以下正宗小马头像:


收起阅读 »

IOS中如何显示开发者服务器上的昵称和头像

无论是IOS还是安卓,集成环信SDK遇到的第一个问题,就是如何显示自有用户体系中的昵称和头像。运行环信的demo app,注册用户是直接使用环信ID(username)作为用户名,但是在我们实际运用中,需要将自有用户体系的UserId生成GUID作为环信ID(...
继续阅读 »
无论是IOS还是安卓,集成环信SDK遇到的第一个问题,就是如何显示自有用户体系中的昵称和头像。运行环信的demo app,注册用户是直接使用环信ID(username)作为用户名,但是在我们实际运用中,需要将自有用户体系的UserId生成GUID作为环信ID(username)【参考:http://docs.easemob.com/im/100serverintegration/20users】,这时候如果不经过处理,则会显示如下界面:

1.png


 
那么如何处理,才能显示正确的用户昵称和头像呢?
其实官方已经提供有解决方案了,只不过没有给出示例代码而已。
http://docs.easemob.com/im/490integrationcases/10nickname
 
引用一下关键文字:


方法二:从消息扩展中获取昵称和头像

昵称和头像的获取:把用户基本的昵称和头像的URL放到消息的扩展中,通过消息传递给接收方,当收到一条消息时,则能通过消息的扩展得到发送者的昵称和头像URL,然后保存到本地数据库和缓存。当显示昵称和头像时,请从本地或者缓存中读取,不要直接从消息中把赋值拿给界面(否则当用户昵称改变后,同一个人会显示不同的昵称)。

昵称和头像的更新:当扩展消息中的昵称和头像 URI 与当前本地数据库和缓存中的相应数据不同的时候,需要把新的昵称保存到本地数据库和缓存,并下载新的头像并保存到本地数据库和缓存。


 
没错,官方提供了两种思路,鉴于项目的实际情况,我选择了【方法二】。
于是,无论安卓,还是IOS,我们都是这样处理的:
【1】.APP间传递用户属性信息:发送(文本、图片...)消息时,要在消息扩展(message.ext)中附带当前用户的属性信息;
【2】.本地缓存用户信息:在接收消息的回调函数里,读取消息扩展(message.ext)里用户属性(键值对)信息;如果本地缓存(sqlite)不存在该用户,则新增缓存记录,如果存在,则更新记录;(用户登录或注册成功后,也要更新用户缓存信息)
【3】.获取用户属性信息:在需要显示昵称的地方,根据环信ID,读取sqlite缓存数据,获取用户昵称和头像;
用户属性信息:ChatUserId(环信ID),ChatUserNick(用户昵称), ChatUserPic(用户头像,完整的url地址);

 
IOS关键代码:
// 环信聊天用的昵称和头像(发送聊天消息时,要附带这3个属性)
#define kChatUserId @"ChatUserId"// 环信账号
#define kChatUserNick @"ChatUserNick"
#define kChatUserPic @"ChatUserPic"

 ChatUserCacheInfo是环信用户信息缓存管理类
ChatUserCacheInfo.h
#import <Foundation/Foundation.h>

@interface ChatUserCacheInfo : NSObject
@property(nonatomic,copy)NSString* Id;
@property(nonatomic,copy)NSString* NickName;
@property(nonatomic,copy)NSString* AvatarUrl;
@end


@interface ChatUserCacheUtil : NSObject

+(void)saveInfo:(NSString *)openId
imgId:(NSString*)imgId
nickName:(NSString*)nickName;

+(void)saveDict:(NSDictionary *)userinfo;

+(void)saveModel:(UserApiModel*)user;

+(ChatUserCacheInfo*)queryById:(NSString *)userid;

@end









ChatUserCacheUtil.m
#import "ChatUserCacheUtil.h"
#import "FMDB.h"

#define DBNAME @"cache_data.db"

@implementation ChatUserCacheInfo

@end

@implementation ChatUserCacheUtil

+(void)createTable:(FMDatabase *)db
{
if ([db open]) {
if (![db tableExists :@"userinfo"]) {
if ([db executeUpdate:@"create table userinfo (userid text, username text, userimage text)"]) {
NSLog(@"create table success");
}else{
NSLog(@"fail to create table");
}
}else {
NSLog(@"table is already exist");
}
}else{
NSLog(@"fail to open");
}
}

+ (void)clearTableData:(FMDatabase *)db
{
if ([db executeUpdate:@"DELETE FROM userinfo"]) {
NSLog(@"clear successed");
}else{
NSLog(@"fail to clear");
}
}

+(FMDatabase*)getDB{
NSString *docsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *dbPath = [docsPath stringByAppendingPathComponent:DBNAME];
FMDatabase *db = [FMDatabase databaseWithPath:dbPath];
[self createTable:db];
return db;
}

+(void)saveModel:(UserApiModel*)user{
[ChatUserCacheUtil saveInfo:user.EaseMobUserName imgId:user.HeadImg nickName:user.Username];
}

+(void)saveInfo:(NSString *)openId
imgId:(NSString*)imgId
nickName:(NSString*)nickName{
NSMutableDictionary *extDic = [NSMutableDictionary dictionary];
[extDic setValue:openId forKey:kChatUserId];
[extDic setValue:@"[url=http://img.baidu.com"]http://img.baidu.com/"[/url]+imgId forKey:kChatUserPic];//完整图片路径"http://img.baidu.com/1234"。如果imgId是相对路径,那完整路径就是类似"http://img.baidu.com/abc.jpg"
[extDic setValue:nickName forKey:kChatUserNick];
[ChatUserCacheUtil saveDict:extDic];
}

+(void)saveDict:(NSDictionary *)userinfo{
FMDatabase *db = [self getDB];

NSString *userid = [userinfo objectForKey:kChatUserId];
if ([db executeUpdate:@"DELETE FROM userinfo where userid = ?", userid]) {
DLog(@"删除成功");
}else{
DLog(@"删除失败");
}
NSString *username = [userinfo objectForKey:kChatUserNick];
NSString *userimage = [userinfo objectForKey:kChatUserPic];
if ([db executeUpdate:@"INSERT INTO userinfo (userid, username, userimage) VALUES (?, ?, ?)", userid,username,userimage]) {
DLog(@"插入成功");
}else{
DLog(@"插入失败");
}

// NSLog(@"%d: %@", [db lastErrorCode], [db lastErrorMessage]);
FMResultSet *rs = [db executeQuery:@"SELECT userid, username, userimage FROM userinfo where userid = ?",userid];
if ([rs next]) {
NSString *userid = [rs stringForColumn:@"userid"];
NSString *username = [rs stringForColumn:@"username"];
NSString *userimage = [rs stringForColumn:@"userimage"];
DLog(@"查询一个 %@ %@ %@",userid,username,userimage);
}

rs = [db executeQuery:@"SELECT userid, username, userimage FROM userinfo"];
while ([rs next]) {
NSString *userid = [rs stringForColumn:@"userid"];
NSString *username = [rs stringForColumn:@"username"];
NSString *userimage = [rs stringForColumn:@"userimage"];
DLog(@"查询所有 %@ %@ %@",userid,username,userimage);
}
[rs close];
// NSLog(@"%d: %@", [db lastErrorCode], [db lastErrorMessage]);
[db close];

}

+(ChatUserCacheInfo*)queryById:(NSString *)userid{
FMDatabase *db = [self getDB];
if ([db open]) {
FMResultSet *rs = [db executeQuery:@"SELECT userid, username, userimage FROM userinfo where userid = ?",userid];
if ([rs next]) {

ChatUserCacheInfo *userInfo = [[ChatUserCacheInfo alloc] init];

userInfo.Id = [rs stringForColumn:@"userid"];
userInfo.NickName = [rs stringForColumn:@"username"];
userInfo.AvatarUrl = [rs stringForColumn:@"userimage"];
DLog(@"查询一个 %@",userInfo);
return userInfo;
}else{
return nil;
}
}else{
return nil;
}
}


@end
首先要在用户登录或注册成功后,返回用户登录信息时,缓存一下用户信息:
@protocol UserApiModel <NSObject>

@end

@interface UserApiModel : BaseJSONModel
@property(nonatomic, assign)int Id;
@property(nonatomic, copy)NSString *Username;
@property(nonatomic, copy)NSString *Email;
@property(nonatomic, copy)NSString *HeadImg;
@property(nonatomic, copy)NSString *EaseMobUserName;
@property(nonatomic, copy)NSString *EaseMobPassword;
@end

// 登录成功后返回用户model,需要为环信聊天窗口缓存用户信息
[ChatUserCacheUtil saveModel:user];

然后在接收环信消息的回调函数里保存用户信息,HsMainViewController.m是我们项目的主框架,我们在这里写了回调函数,无论群聊还是单聊消息,都会调用这里:
// 收到消息回调
-(void) didReceiveMessage:(EMMessage *)message
{
[ChatUserCacheUtil saveDict:message.ext];

BOOL needShowNotification = (message.messageType != eMessageTypeChat) ? [self needShowNotification:message.conversationChatter] : YES;
if (needShowNotification) {
#if !TARGET_IPHONE_SIMULATOR

BOOL isAppActivity = [[UIApplication sharedApplication] applicationState] == UIApplicationStateActive;
if (!isAppActivity) {
[self showNotificationWithMessage:message];
}else {
[self playSoundAndVibration];
}
#endif
}
}
环信页面主要是在ChatViewController和ConversationListViewController里显示用户对话,所以我们要在这两个页面里从缓存取用户头像和昵称,先从ChatViewController开始:
- (id<IMessageModel>)messageViewController:(EaseMessageViewController *)viewController
modelForMessage:(EMMessage *)message
{
id<IMessageModel> model = [[EaseMessageModel alloc] initWithMessage:message];

ChatUserCacheInfo *userinfo = [ChatUserCacheUtil queryById:model.nickname];
if (userinfo != nil) {
model.nickname = userinfo.NickName;
model.avatarURLPath = userinfo.AvatarUrl;
}

model.avatarImage = [UIImage imageNamed:@"EaseUIResource.bundle/user"];
model.failImageName = @"imageDownloadFail";

return model;
}
然后是ConversationListViewController:
#pragma mark - EaseConversationListViewControllerDataSource

- (id<IConversationModel>)conversationListViewController:(EaseConversationListViewController *)conversationListViewController
modelForConversation:(EMConversation *)conversation
{
EaseConversationModel *model = [[EaseConversationModel alloc] initWithConversation:conversation];
if (model.conversation.conversationType == eConversationTypeChat) {
ChatUserCacheInfo *userinfo = [ChatUserCacheUtil queryById:model.conversation.chatter];
if (userinfo != nil) {
model.title = userinfo.NickName;
model.avatarURLPath = userinfo.AvatarUrl;
}
model.avatarImage = PlaceholderImgChatUser;
} else if (model.conversation.conversationType == eConversationTypeGroupChat) {
// 此处省略100行代码........
}
}
最后,为了让另外一个客户端也能正确显示头像和昵称,app发送消息时,要在消息扩展里附带用户信息,代码写在EaseSDKHelper.m里:
// 重新消息扩展组织
+(NSMutableDictionary*)reGetMessageExt:(NSDictionary *)messageExt{
NSMutableDictionary *extDic = [NSMutableDictionary dictionaryWithDictionary:messageExt];
[extDic setValue:[SettingData share].UserChatId forKey:kChatUserId];
[extDic setValue:[SettingData share].UserHeadImg.ServerThumbUrlStr forKey:kChatUserPic];
[extDic setValue:[SettingData share].UserName forKey:kChatUserNick];
return extDic;
}

+ (EMMessage *)sendTextMessage:(NSString *)text
to:(NSString *)toUser
messageType:(EMMessageType)messageType
requireEncryption:(BOOL)requireEncryption
messageExt:(NSDictionary *)messageExt

{
// 表情映射。
NSString *willSendText = [EaseConvertToCommonEmoticonsHelper convertToCommonEmoticons:text];
EMChatText *textChat = [[EMChatText alloc] initWithText:willSendText];
EMTextMessageBody *body = [[EMTextMessageBody alloc] initWithChatObject:textChat];
EMMessage *message = [[EMMessage alloc] initWithReceiver:toUser bodies:[NSArray arrayWithObject:body]];
message.requireEncryption = requireEncryption;
message.messageType = messageType;

message.ext = [self reGetMessageExt:messageExt];
EMMessage *retMessage = [[EaseMob sharedInstance].chatManager asyncSendMessage:message
progress:nil];

return retMessage;
}


+ (EMMessage *)sendImageMessageWithImage:(UIImage *)image
to:(NSString *)to
messageType:(EMMessageType)messageType
requireEncryption:(BOOL)requireEncryption
messageExt:(NSDictionary *)messageExt
progress:(id<IEMChatProgressDelegate>)progress
{
return [self sendImageMessageWithImage:image to:to messageType:messageType requireEncryption:requireEncryption messageExt:messageExt quality:0.6 progress:progress];
}

+ (EMMessage *)sendImageMessageWithImage:(UIImage *)image
to:(NSString *)to
messageType:(EMMessageType)messageType
requireEncryption:(BOOL)requireEncryption
messageExt:(NSDictionary *)messageExt
quality:(float)quality
progress:(id<IEMChatProgressDelegate>)progress
{
// 此处省略9行代码....

message.ext = [self reGetMessageExt:messageExt];
EMMessage *retMessage = [[EaseMob sharedInstance].chatManager asyncSendMessage:message
progress:progress];

return retMessage;
}

+ (EMMessage *)sendVoiceMessageWithLocalPath:(NSString *)localPath
duration:(NSInteger)duration
to:(NSString *)to
messageType:(EMMessageType)messageType
requireEncryption:(BOOL)requireEncryption
messageExt:(NSDictionary *)messageExt
progress:(id<IEMChatProgressDelegate>)progress
{
// 此处省略4行代码....

message.ext = [self reGetMessageExt:messageExt];
EMMessage *retMessage = [[EaseMob sharedInstance].chatManager asyncSendMessage:message
progress:progress];

return retMessage;
}

+ (EMMessage *)sendVideoMessageWithURL:(NSURL *)url
to:(NSString *)to
messageType:(EMMessageType)messageType
requireEncryption:(BOOL)requireEncryption
messageExt:(NSDictionary *)messageExt
progress:(id<IEMChatProgressDelegate>)progress
{
// 此处省略4行代码....

message.ext = [self reGetMessageExt:messageExt];
EMMessage *retMessage = [[EaseMob sharedInstance].chatManager asyncSendMessage:message
progress:progress];

return retMessage;
}

+ (EMMessage *)sendFileMessage:(EMChatFile *)chatFile
to:(NSString *)to
messageType:(EMMessageType)messageType
requireEncryption:(BOOL)requireEncryption
messageExt:(NSDictionary *)messageExt
progress:(id<IEMChatProgressDelegate>)progress
{
// 此处省略4行代码....

message.ext = [self reGetMessageExt:messageExt];
EMMessage *retMessage = [[EaseMob sharedInstance].chatManager asyncSendMessage:message
progress:progress];

return retMessage;
}

有不当之处,欢迎指正~~谢谢。QQ:364223587
 
以上代码为SDK V2版本,如果集成的是V3版本,请移步源码:
http://git.oschina.net/markies/ChatDemo-UI3.00-Simple
思路其实跟V2差不多,最大区别V3的回调方法didReceiveMessages比V2多了个【s】。

ChatUserCacheUtil我已经重命名为:UserCacheManager
 

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

环信编程大赛优秀开源项目系列之二:“图忆”一款基于地理位置信息的社交APP

根据IDC数据显示,中国有近200万开发者,身为一个程序员,我们生活在一个 IT 系统越发复杂且多变化的时代。有时候执行一个简单的开源项目,开发一个基础功能都需要精准定义并耗费大量时间专注任务。随着云计算的兴起,API 和SDK开始作为软件之间重要媒介而作为一...
继续阅读 »
根据IDC数据显示,中国有近200万开发者,身为一个程序员,我们生活在一个 IT 系统越发复杂且多变化的时代。有时候执行一个简单的开源项目,开发一个基础功能都需要精准定义并耗费大量时间专注任务。随着云计算的兴起,API 和SDK开始作为软件之间重要媒介而作为一种独立应用而存在,“一切皆软件,一切皆API,一切皆SDK”。通过API和SDK可以让开发者摆脱繁重的基础功能底层开发,短时间即可让App拥有各种诸如内置IM、统计等基础功能组件能力。 

5月14日,由环信联合猿圈共同推出的“首届环信编程大赛”颁奖典礼在中关村义创空间隆重举行。本次环信编程大赛历时两个月,由线上初赛、决赛和颁奖典礼三个环节组成,总计报名人数2000+,收到决赛项目100+。最终由评委会认定的13个优秀开源项目及开发者集体亮相颁奖典礼。其中“方圆十里”、“高仿微信“和“咚咚”三个开源项目名列前三,共同分享了15000元奖金和价值12000元的专属表情包。



9d743f79a696245c9c93ac614b13fe79[1].jpg


优秀项目开发者合影




73ERYP`OJ`QEY4PXGPEJ(AG.png


“图忆”项目负责人梁桂栋分享技术开发细节


 
其余入围的十余个优秀开源项目同样引起了到场开发者的热烈追捧,环信将分期将入围的优秀项目代码免费开源给小伙伴们。今天我们带来的是一款基于地理位置信息的社交分享应用——“图忆”。图忆是一款基于地理位置信息的社交分享应用。实现了将用户记录的不同类型的事件标刻于地图之上,查看自己的记录足迹,同时用户可以轻松查看附近分享的记事,添加好友聊天,建立兴趣圈子,发现志趣相投的好友,并且用户记事可以分享到公共社区平台,分享乐趣的同时也发现了更多的乐趣,社区推荐策略让用户发现更多有价值的乐趣。



QQ截图20160523170643.jpg


 “图忆”APP界面截图


 
1.软件介绍


图忆是一款基于地理位置信息的社交分享应用。实现了将用户记录的不同类型的事件标刻于地图之上,查看自己的记录足迹,同时用户可以轻松查看附近分享的记事,添加好友聊天,建立兴趣圈子,发现志趣相投的好友,并且用户记事可以分享到公共社区平台,分享乐趣的同时也发现了更多的乐趣,社区推荐策略让用户发现更多有价值的乐趣。




2.功能介绍


【记录记忆】你可以记录自己的生活点滴在地图之上,可以公开给别人看,也可以保存为自己的私有记忆。
【离线记录】没有网络也可以轻松保存离线记录,WIFI连接后直接批量上传,省心
【地图附近】你将通过地图查看到附近用户公开的说有分享记录,当然是直接在地图上展示的哟,很直观的说,还有五个标签分类查询哟,就等你来发现了。
【雷达】发现同时在附近开启雷达的小伙伴,自定义雷达显示的内容,让小伙伴更容易发现你
【聊天圈子】与TA尽情畅聊,兴趣小伙伴建圈子一起聊。
【图忆社区】点赞,评论,分享,收藏Ta的分享



3.使用技术


环信IM
百度地图API
有盟API



4.作者心得


IM正越来越得到开发者重视,也逐渐成为APP标配,绝大部分App中都集成了即时通讯功能。将APP的核心功能紧密与即时通讯良好结合,将更有利于APP的用户体验和留存。
APP的多元发展中需要使用多功能的有机结合。而作为一个完整的SDK需要越少的干涉APP原本的逻辑,而不降低功能与体验,这些方面环信的IM SDK都做的挺好。 


 



7ec7f4aad12e067bb7bd46f03a22c657[1].jpg



  特别感谢以下企业的大力支持:


义创空间提供颁奖场地
 
萌岛从自有形象库中授权一套价值12000元的表情包
 
Emokit赞助Apple Watch一台
 
猿圈全程提供技术评测支持


 
git源码下载https://github.com/donlan/Tuyi
 
更多开源项目请点击http://community.easemob.com/article/825307813

图忆项目作者演讲PPT下载↓↓↓
  收起阅读 »

开源了一个简单实用的http服务压力测试工具Alex,自带web ui,使用golang实现

Alex ================= Alex是基于vegeta library和boom封装的压力测试web UI。Vegeta提供稳定的qps压力源,boom提供稳定的并发数压力源。 github地址 https://github.com/ir...
继续阅读 »
Alex

=================

Alex是基于vegeta library和boom封装的压力测试web UI。Vegeta提供稳定的qps压力源,boom提供稳定的并发数压力源。
github地址 https://github.com/ireaderlab/alex

English

Alex架构图

Alex ArchitectureAlex 主要功能

1. 保存压力测试参数以便反复压测

2. 保存压力测试报告以便后续查看和分享

3. 提供了简单直接的图形和文字报告

4. 可以同时对多个http接口进行压力测试

5. 可以同时对集群内多个host:port对进行压测

6. 使用多组调用参数避免压测时出现的数据热点问题

7. 使用步骤设置,生成渐进式的压力源

8. 提供简单的压测机器系统状态实时显示功能

Alex Limitations

1. Alex运行在单一进程里,如果你需要分布式的压测环境,就得部署多个节点,压测时需要多人同时操作。

2. Vegeta在压力过载时没有提供立即停止的方法。这就需要你细心设计压测步骤,仔细观察系统状态避免系统过载。

3. Qps和并发数不宜过大。我曾经使用Alex工具单进程测试了HelloWorld的web程序每个请求吐出1500字节,qps最多可以达到60000,基本让千兆网卡打满。

4. 在大型压力测试下,尽量避免Gzip解压缩。解压缩会消耗大量的cpu资源,会导致压测报告不准确。你可以通过部署多个节点来进行大型压力测试。

5. 只支持Http协议。Https协议不打算支持,因为加密解密也同样会消耗大量cpu资源,导致报告不准确。

6. 报告只是提供一种性能参考,要勇于对报告进行质疑。

7. Alex虽然有如此诸多限制,这不影响它的日常使用。

安装


install mongodb 
install golang # 1.4+ is required 
go get github.com/go-martini/martini 
go get github.com/tsenart/vegeta 
go get gopkg.in/mgo.v2 
go get github.com/shirou/gopsutil # godep restore 
git clone https://github.com/shellquery/alex.git 
cd alex 
go build 
./alex 
./alex -c config.json
open browser http://localhost:8000/ 
 


 
配置


config.json{ 
"BindAddr": "localhost:8000",
"MongoUrl": "mongodb://localhost:27017/alex",
"Teams": [ "python", "java", "php", "go" ]
 }


 
引用
棒棒的vegeta https://github.com/tsenart/vegeta

简单直接的boom https://github.com/rakyll/boom

截屏

Randomize Host:ports

Randomize Parameters

Step Settings

Benchmark Reports 收起阅读 »

没想到你们是这种环信--happy520

将这张图转发到你的朋友圈、微博并 @环信 将获得由环信送出的神秘大奖一份!  


30期_社区活动.jpg



将这张图转发到你的朋友圈、微博并 @环信 将获得由环信送出的神秘大奖一份!
 

【公告】IOS SDK 2.2.5版本已支持IPV6-only

致亲爱的环信小伙伴们   因为6.1日起苹果要求IPV6-only,不支持ipv6的app将无法通过appstore审核。我们于昨天(5.18)发布了支持ipv6版本的IOS SDK 2.2.5版本   建议进行升级,以免因为这个问题导致app新版本通不过苹果...
继续阅读 »
致亲爱的环信小伙伴们
 
因为6.1日起苹果要求IPV6-only,不支持ipv6的app将无法通过appstore审核。我们于昨天(5.18)发布了支持ipv6版本的IOS SDK 2.2.5版本
 
建议进行升级,以免因为这个问题导致app新版本通不过苹果审核。
  
新版本介绍http://community.easemob.com/article/825307836
 
新版SDK下载http://www.easemob.com/download/im
 
13个基于环信集成的开源项目集体登场http://community.easemob.com/article/825307813 收起阅读 »

优秀程序员的十个习惯

在这个世界上,有数百万的人热衷于软件开发,他们有很多名字,如:软件工程师(Software Engineer),程序员(Programmer),编码人(Coder),开发人员(Developer)。经过一段时间后,这些人也许能够成为一个优秀的编码人员,他们会非...
继续阅读 »
在这个世界上,有数百万的人热衷于软件开发,他们有很多名字,如:软件工程师(Software Engineer),程序员(Programmer),编码人(Coder),开发人员(Developer)。经过一段时间后,这些人也许能够成为一个优秀的编码人员,他们会非常熟悉如何用计算机语言来完成自己的工作。但是,如果你要成为一个优秀的程序员,你还可以需要有几件事你需要注意,如果你能让下面十个条目成为你的习惯,那么你才能真正算得上是优秀程序员。



2005483-1835f6de0ef0b7ba.png


学无止境


  1. 学无止境。就算是你有了10年以上的程序员经历,你也得要使劲地学习,因为你在计算机这个充满一创造力的领域,每天都会有很多很多的新事物出现。你需要跟上时代的步伐。你需要去了解新的程序语言,以及了解正在发展中的程序语言,以及一些编程框架。还需要去阅读一些业内的新闻,并到一些热门的社区去参与在线的讨论,这样你才能明白和了解整个软件开发的趋势。在国内,一些著名的社区例如:CSDN,ITPUB,CHINAUINX等等,在国外,建议你经常上一上digg.com去看看各种BLOG的聚合。



2005483-d942b123971dfa5e.png


掌握多种语言


  1. 掌握多种语言。程序语言总是有其最适合的领域。当你面对需要解决的问题时,你需要找到一个最适合的语言来解决这些问题。比如,如果你需要性能,可能C/C++是首选,如果你需要跨平台,可能Java是首选,如果你要写一个Web上的开发程序,那么PHP,ASP,Ajax,JSP可能会是你的选择,如果你要处理一些文本并和别的应用交互,可能Perl, Python会是最好的。所以,花一些时间去探索一下其它你并熟悉的程序语言,能让你的眼界变宽,因为你被武装得更好,你思考问题也就更为全面,这对于自己和项目都会有好的帮助。



2005483-3c0cf4d04b268055.png


理性面对不同的操作系统或技术


  1. 理性面对不同的操作系统或技术。程序员们总是有自己心目中无可比拟的技术和操作系统,有的人喜欢Ubuntu,有的人喜欢Debian,还有的人喜欢Windows,以及FreeBSD,MacOSX或Solaris等等。只有一部分优秀的程序员明白不同操作系统的优势和长处和短处,这样,在系统选型的时候,才能做到真正的客观和公正,而不会让情绪影响到自己。同样,语言也是一样,有太多的程序员总是喜欢纠缠于语言的对比,如:Java和Perl。哪个刚刚出道的程序员没有争论去类似的话题呢?比如VC++和Delphi等等。争论这些东西只能表明自己的肤浅和浮燥。优秀的程序并不会执着于这些,而是能够理性的分析和理心地面对,从而才能客观地做出正确的选择。
  2. 别把自己框在单一的开发环境中。 再一次,正如上面所述,每个程序员都有自己忠爱的工具和技术,有的喜欢老的(比如我就喜欢Vi编辑程序),而有的喜欢新的比如gedit或是Emacs等。有的喜欢使用像VC++一样的图形界面的调试器,而我更喜欢GDB命令行方面的调式器。等等等等。程序员在使用什么样的工具上的争论还少吗?到处都是啊。使用什么样的工具本来无所谓,只要你能更好更快地达到你的目的。但是有一点是优秀程序员都应该了解的——那就是应该去尝试一下别的工作环境。没有比较,你永远不知道谁好谁不好,你也永远不知道你所不知道的。

 



2005483-1ad1a0fb435fb600.png


使用版本管理工具管理你的代码。


  1. 使用版本管理工具管理你的代码。千万不要告诉我你不知道源码的版本管理,如果你的团队开发的源代码并没有版本管理系统,那么我要告诉你,你的软件开发还处于石器时代。赶快使用一个版式本管理工具吧。CVS 是一个看上去平淡无奇的版本工具,但它是被使用最广的版本管理系统,Subversion 是CVS的一个升级版,其正在开始接管CVS的领地。Git 又是一个不同的版本管理工具。还有Visual SourceSafe等。使用什么样的版本管理工具依赖于你的团队的大小和地理分布,你也许正在使用最有效率或最没有效率的工具来管理你的源代码。但一个优秀的程序员总是会使用一款源码版本管理工具来管理自己的代码。如果你要我推荐一个,我推荐你使用开源的Subversion。

 



2005483-43672577a1a07a0d.png


是一个优秀的团队成员


  1. 是一个优秀的团队成员。 除非你喜欢独奏,除非你是孤胆英雄。但我想告诉你,今天,可能没有一个成熟的软件是你一个人能做的到的,你可能是你团队中最牛的大拿,但这并不意味着你就是好的团队成员。你的能力只有放到一个团队中才能施展开来。你在和你的团队成员交流中有礼貌吗?你是否经常和他们沟通,并且大家都喜欢和你在一起讨论问题?想一想一个足球队吧,你是这个队中好的成员吗?当别人看到你在场上的跑动时,当别人看到你的传球和接球和抢断时,你的团员成员能因为你的动作受到鼓舞吗?
  2. 把你的工作变成文档。 这一条目当然包括了在代码中写注释,但那还仅仅不够,你还需要做得更多。有良好的注释风格的代码是一个文档的基础,他能够让你和你的团队容易的明白你的意图和想法。写下文档,并不仅仅是怕我们忘了当时的想法,而且还是一种团队的离线交流的方法,更是一种知识传递的方法。记录下你所知道的一切会是一个好的习惯。因为,我相信你不希望别人总是在你最忙的时候来打断你问问题,或是你在休假的时候接到公司的电话来询问你问题。而你自己如果老是守着自己的东西,其结果只可能是让你自己长时间地深陷在这块东西内,而你就更本不可以去做更多的事情。包括向上的晋升。你可能以为“教会徒弟能饿死师父”,但我告诉你,你的保守会让你失去更多更好的东西,请你相信我,我绝不是在这里耸人听闻。
  3. 注意备份和安全。 可能你觉得这是一个“废话”,你已明白了备份的重要性。但是,我还是要在这里提出,丢失东西是我们人生中的一部份,你总是会丢东西,这点你永远无法避免。比如:你的笔记本电脑被人偷了,你的硬盘损坏了,你的电脑中病毒了,你的系统被人入侵了,甚至整个大楼被烧了,等等,等等。所以,做好备份工作是非常非常重要的事情,硬盘是不可信的,所以定期的刻录光盘或是磁带可能会是一个好的方法,网络也是不可信的,所以小心病毒和黑客,不但使用软件方面的安全策略,你更需要一个健全的管理制度。此外,尽量的让你的数据放在不同的地方,并做好定期(每日,每周,每月)的备份策略。
  4. 设计要足够灵活。 可能你的需求只会要求你实现一个死的东西,但是,你作为一个优秀的程序,你应该随时在思考这个死的东西是否可以有灵活的一面,比如把一些参数变成可以配置的,把一些公用的东西形成你的函数库以便以后重用,是否提供插件方面的功能?你的模块是否要以像积木一样随意组合?如果要有修改的话,你的设计是否能够马上应付?当然,灵活的设计可能并不是要你去重新发明轮子,你应该尽可能是使用标准化的东西。所谓灵话的设计就是要让让考虑更多需求之外的东西,把需求中这一类的问题都考虑到,而不是只处理需求中所说的那一特定的东西。比如说,需要需要的屏幕分辨率是800×600,那么你的设计能否灵活于其他的分辨率?程序设计总是需要我们去处理不同的环境,以及未来的趋势。我们需要用动态的眼光去思考问题,而不是刻舟求剑。也许有一天,你今天写的程序就要移植到别的环境中去,那个时候你就能真正明白什么是灵活的设计了。
  5. 不要搬起石头砸自己的脚。程序员总是有一种不好的习惯,那就是总是想赶快地完成自己手上的工作。但情况却往往事已愿违。越是想做得快,就越是容易出问题,越是想做得快,就越是容易遗漏问题,最终,程序改过来改过去,按下葫芦起了瓢,最后花费的时间和精力反而更多。欲速而不达。优秀程序员的习惯是前面多花一些时间多作一些调查,试验一下不同的解决方案,如果时间允许,一个好的习惯是,每4个小时的编程,需要一个小时的休息,然后又是4个小时的编码。当然,这因人而异,但其目的就是让你时常回头看看,让你想一想这样三个问题:1)是否这么做是对的?2)是否这么做考虑到了所有的情况?3)是否有更好的方法?想好了再说,时常回头看看走过的路,时常总结一下过去事,会对你有很大的帮助。

 以上是十条优秀程序员的习惯或行为规范,希望其可以对你有所帮助。

本文来源于网上phil的BLOG,但我在写作过程中使用了自己的语言和方法重新描述了一下这十条,所以,我希望你在转载的时候能够注明作者和出处以表示对我的尊重。谢谢! 收起阅读 »

检测一下大家的OC基础,最新ios面试题,你能回答上哪些?

哈哈,没有地址,此时我的内心的meng 比的!  1.说说内存管理 2、ASIRequest是什么; 3、怎么输出json字符串; 4、说说http头部有哪些内容; 5、说说OC生命周期; 6、运用第三方框架,到时候出了问题,谁来负责 7、自己写...
继续阅读 »



2100853-64f1f043f15325bc.jpg


哈哈,没有地址,此时我的内心的meng 比的!


 1.说说内存管理

2、ASIRequest是什么;

3、怎么输出json字符串;

4、说说http头部有哪些内容;

5、说说OC生命周期;

6、运用第三方框架,到时候出了问题,谁来负责

7、自己写一个strcpy函数

8、字母统计(如,输入字符串“aabbbccddddaaaaa”,输出“2a3b2c4d5a”)

9、你用过哪些框架

10、进程与线程的区别

11、开辟线程的方式有哪些

12、实现进程同步的方式有哪些,或者说你怎么实现进程同步

13、请你谈谈同步和异步,用操作系统知识解释一下。

14、请你谈谈多态

15、怎么将数据写入文件(归档,解当)

16、写一个set方法(retain和copy权限)

17


Int* fun()

{

Int a=5;

Int * p=&a;

Return p;

}

请问:在主函数里面调用fun函数,这样可以吗?如果不可以,请说明为什么,并给出一种解决方案。

18、在颜色中,有GB8888和 GB565标准,前者32位,其中R占8位,G占8位,B占8位,透明度占8位,后者16位,其中,R占5位,G占6位,B占5位。现在要将一个GB8888类型颜色转换成GB565类型,怎么转

19、判断一个数是否为素数

20、优化代码

1、int a=b*4;

2、int a=b/8;

3、int a=b%1;

4、int a=b;

5、int a=(b*3)/8;

21、什么是内联函数?

22、assign,retain,copy的区别

23、面向对象的特性

24、实现一个view从顶部移到底部的动画

25、#ff3344转换成uicolor

26、判断一个链表是否有循环

27、写一个代理类

28、进程之间是怎么通信的

29、oc有哪些优点和缺点

30、什么时候用delegate,什么时候用Notification?

31、写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个。当你写下面的代码时会发生什么事?

least = MIN(*p++, b);

32、MVC模式的理解

33、堆和栈的区别

34、自动释放池是什么,如何工作

35、写一个委托的interface

36、objective-c的内存管理

37、什么是Notification?

38、下面的声明都是什么意思?

constint a;

intconst a;

constint *a;

int* const a;

intconst * a const;

  收起阅读 »

IT垂直领域的今日头条

“今天要开会,看今日头条条条~~~~!”,伴随着魔性的鬼畜广告,个性化聚合类新闻这块巨大的蛋糕, 谁都想抢一步先咬一口。    纵观目前互联网垂直个性化聚合类新闻平台鱼龙混杂,真正能做到满足用户需求,戳中用户痛点的平台有几个!    如今互联网垂直领域...
继续阅读 »
“今天要开会,看今日头条条条~~~~!”,伴随着魔性的鬼畜广告,个性化聚合类新闻这块巨大的蛋糕, 谁都想抢一步先咬一口。
 
 纵观目前互联网垂直个性化聚合类新闻平台鱼龙混杂,真正能做到满足用户需求,戳中用户痛点的平台有几个! 
 
如今互联网垂直领域的新闻聚合类平台和今日头条的差别在与范围的大小以及定位的不同,但核心意义仍不改变,就是根据推荐算法将更优质的更符合用户阅读习惯的内容呈现。 
 
据最近了解来看 , 在此处能称为IT领域的今日头条, 当属浙江网新恒天软件公司的摘客。 
 
虽然网新恒天是做外包服务为主的公司, 但是看到摘客这个产品的时候, 我便对他刮目相看。 
 
1.首先我们来说下他的页面交互 


软文用图.png


 
 身为一个外包公司来说,互联网产品的思想应该属于较弱的情况,但是他却采用较为先进的设计风格,将icon,色彩,排版都做了仔细斟酌。 
 
2.其次我们来说下他爬来的内容 
 
将内容分类,同时也可根据自己的喜好订阅相应类别。基于个人阅读的情况个性化推荐。 
 
再也不用因为找不到想看的内容而急躁! 
 
叔本华说,“每个人都将自身所感知的范围当做世界的范围”,我们能够在互联网上看到什么样的世界,就意味着自己会生活在哪类世界里,信息无价,用更好的工具来看世界,机器的来临,已然改变了信息的基本构成,你需要什么,平台就得往你需要的地方去。
 
垂直个性化聚合类平台热已来临,希望摘客能够继续坚挺。 收起阅读 »

IOS V2.2.5 AndroidV2.2.9 release ,支持ipv6,增加群红包、支持拼手气红包和普通群红包

Android V2.2.9 2016-5-18更新日志 这个版本主要对红包功能做了更新,用Eclipse导入项目的时候需要把demoui3.0根目录下的redpacketlibrary也导入到Eclipse中。1、增加群红包,可以发拼手气红包、普通群红包;...
继续阅读 »

24958PICwjQ_1024.jpg



Android V2.2.9 2016-5-18更新日志


这个版本主要对红包功能做了更新,用Eclipse导入项目的时候需要把demoui3.0根目录下的redpacketlibrary也导入到Eclipse中。1、增加群红包,可以发拼手气红包、普通群红包;

2、优化支付流程,支付更便捷;

3、优化绑卡流程,绑卡更安全;

4、HTML5的页面基于React重构,主要流程通过原生SDK实现,速度更快、交互体验更流畅;

5、增加了红包记录,可以查看收发的红包记录;

6、提供了红包产品的数据统计,App可登陆红包的管理后台查看;

7、增加了太平洋保险的账户安全险,因账户被盗导致的资金损失可以获得赔偿。


 
IOS V2.2.5 2016-05-18 更新日志 


新功能:SDK支持ipv6

红包新版本:

增加群红包,支持拼手气红包和普通群红包;

优化支付流程,支付更便捷;

优化绑卡流程,绑卡更安全;

HTML5的页面基于React重构,主要流程通过原生SDK实现,提升速度和交互体验;

增加了红包历史记录;

提供了红包产品的数据统计,App可登陆红包的管理后台查看;

增加了太平洋保险的账户安全险。

bug fix:

SDK bug:正常网络下登录、退出偶尔超时问题


 
版本历史:Android SDK更新历史  IOS SDK更新历史

下载地址:SDK下载

 
关于新版sdk使用有任何问题或建议请在下方评论留言,我们将直接现金打赏。 收起阅读 »

【创业星生代】36 氪联合环信助力最优秀的创业公司

我们知道,创业从来不是件容易或轻松的事。也许你已经拥有足够好的行业布局,却缺乏更发散的商业想象力;也许你的项目已经有绝对牛逼的团队,却没有量身定做的推广渠道及品牌曝光;也许你已打磨好一个足够打动人的故事以及一份无可挑剔的 BP,万事俱备只欠融资;当然你更拥有不...
继续阅读 »
我们知道,创业从来不是件容易或轻松的事。也许你已经拥有足够好的行业布局,却缺乏更发散的商业想象力;也许你的项目已经有绝对牛逼的团队,却没有量身定做的推广渠道及品牌曝光;也许你已打磨好一个足够打动人的故事以及一份无可挑剔的 BP,万事俱备只欠融资;当然你更拥有不少投资人人脉资源,却只差最合适、最懂你的那一人。      
如果你也有这样的“也许”,那么 36 氪倾心打造的 “创业星生代” 舞台正是你的期许。在这里,创投助手 App、媒体、社群氪空间、融资平台、企业服务被集中整合,五大机构联体,九位大咖携手,联合打造一站式创业生态闭环,解决创业者面临的最大痛点。

“创业星生代” 导师将为你的项目打分并择优约谈,通过每周、每月项目打榜的方式,产生周十强以及月十强,充分挖掘项目商业潜力,助力最优秀的团队。我们事无巨细,竭尽全力,全方位为最优秀的你提供优质、匹配的服务。


活动时间:5月16日 全面开启


 参与流程:     


 
 step1、报名,提交项目信息     

 step2、项目审核,通过后登陆「创投助手APP」    

 step3、投资人发起约谈     

 step4、依据数据产生周 10 强     

 step5、顶级投资机构进行评选,产生月10 强  



活动奖励:​


1、融资礼包:

 项目入驻 36 氪「创投助手APP」

 专享 36 氪轻 FA 服务

“创业星生代” 全国创业大赛线下直接参赛

 氪空间诊疗专家 1 对 1 项目深度辅导
 
2、品牌曝光:

36 氪媒体独家优先报道

北京地铁 LCD 大屏广告

企业服务豪华礼包(价值 10 万元)

36 氪专栏报道套餐

3、创业者社群:

直入氪空间孵化器最终面试

直入氪空间俱乐部

直接参与 36Kr Demo Day 全国线下路演


 合作机构及大咖投资人 :    


IDG 创始合伙人:熊晓鸽    

IDG 合伙人:李骁军    

北极光创投董事总经理:姜皓天    

峰瑞资本创始合伙人:李丰    

峰瑞资本创始合伙人:林中华    

高榕创始合伙人:高翔    

高榕创始合伙人:张震    

华创资本管理合伙人:吴海燕    

华创资本合伙人:熊伟铭




}KQH@TQMH(}X}6CPYET`DF.png



合作伙伴:



X85{_E{)A90K75E7VMR@R{C.png



合作伙伴环信简介:
环信成立于2013年4月,是一家全通讯能力云服务提供商。产品包括国内上线最早的即时通讯云平台——环信即时通讯云,以及移动端最佳实践的全媒体智能云客服平台——环信移动客服。公司产品现已覆盖电商、O2O、互联网金融、在线教育、在线旅游、移动医疗、智能硬件、游戏等20大领域的Top10客户,典型用户包括国美在线、58到家、快牙、楚楚街、随手记、猎聘、海尔等。截至2015年底,环信共服务了50833家App客户,SDK覆盖手机终端3.19亿,平台日均发送消息2.1亿条。根据易观智库《2015中国SaaS客服市场专题研究报告》,在中国移动端SaaS客服市场中,环信市场占有率高达77.4%。
也许你所有的坚持都在等待这一刻的爆发。现在开始,你要做的只是点击“阅读原文”,加入星生代!

阅读原文 收起阅读 »

android播放网络音频

android播放网络音频,很简单的技术,但是可以学习下   很简单的一个获取网络音频播放器,有进度条,播放,暂停,停止,重新播放,支持缓存,以下是源码,希望可以帮到大家 布局文件很简单,就几个按钮,TextView,和SeekBar。  ...
继续阅读 »
android播放网络音频,很简单的技术,但是可以学习下
 
很简单的一个获取网络音频播放器,有进度条,播放,暂停,停止,重新播放,支持缓存,以下是源码,希望可以帮到大家

M~WQX18UUTK7E4NRF3}E]W.png


布局文件很简单,就几个按钮,TextView,和SeekBar。 
activity_audio_palyer.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent" >

<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
android:orientation="vertical" >

<TextView
android:id="@+id/tips"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:text="文件地址" />

<EditText
android:id="@+id/file_name"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:text="http://sc1.111ttt.com/2016/1/02/23/195231349486.mp3" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:layout_marginTop="4.0dip"
android:orientation="horizontal" >

<Button
android:id="@+id/btnPlayUrl"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="播放" >
</Button>

<Button
android:id="@+id/btnPause"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="暂停" >
</Button>

<Button
android:id="@+id/btnStop"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="停止" >
</Button>

<Button
android:id="@+id/btnReplay"
android:layout_width="80dip"
android:layout_height="wrap_content"
android:text="重播" >
</Button>
</LinearLayout>

<LinearLayout
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="20dip"
android:orientation="horizontal" >

<SeekBar
android:id="@+id/skbProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:max="100"
android:paddingLeft="10dip"
android:paddingRight="10dip" >
</SeekBar>
</LinearLayout>
</LinearLayout>

</FrameLayout>
Player.Java文件
public class Player implements OnBufferingUpdateListener, OnCompletionListener,
MediaPlayer.OnPreparedListener {
public MediaPlayer mediaPlayer;
private SeekBar skbProgress;
private Timer mTimer = new Timer();
private String videoUrl;
private boolean pause;
private int playPosition;

public Player(String videoUrl, SeekBar skbProgress) {
this.skbProgress = skbProgress;
this.videoUrl = videoUrl;
try {
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setOnBufferingUpdateListener(this);
mediaPlayer.setOnPreparedListener(this);
} catch (Exception e) {
Log.e("mediaPlayer", "error", e);
}

mTimer.schedule(mTimerTask, 0, 1000);
}

/*******************************************************
* 通过定时器和Handler来更新进度条
******************************************************/
TimerTask mTimerTask = new TimerTask() {
@Override
public void run() {
if (mediaPlayer == null)
return;
if (mediaPlayer.isPlaying() && skbProgress.isPressed() == false) {
handleProgress.sendEmptyMessage(0);
}
}
};

Handler handleProgress = new Handler() {
public void handleMessage(Message msg) {
int position = mediaPlayer.getCurrentPosition();
int duration = mediaPlayer.getDuration();
if (duration > 0) {
long pos = skbProgress.getMax() * position / duration;
skbProgress.setProgress((int) pos);
}
};
};

/**
* 来电话了
*/
public void callIsComing() {
if (mediaPlayer.isPlaying()) {
playPosition = mediaPlayer.getCurrentPosition();// 获得当前播放位置
mediaPlayer.stop();
}
}

/**
* 通话结束
*/
public void callIsDown() {
if (playPosition > 0) {
playNet(playPosition);
playPosition = 0;
}
}

/**
* 播放
*/
public void play() {
playNet(0);
}

/**
* 重播
*/
public void replay() {
if (mediaPlayer.isPlaying()) {
mediaPlayer.seekTo(0);// 从开始位置开始播放音乐
} else {
playNet(0);
}
}

/**
* 暂停
*/
public boolean pause() {
if (mediaPlayer.isPlaying()) {// 如果正在播放
mediaPlayer.pause();// 暂停
pause = true;
} else {
if (pause) {// 如果处于暂停状态
mediaPlayer.start();// 继续播放
pause = false;
}
}
return pause;
}

/**
* 停止
*/
public void stop() {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.stop();
}
}

@Override
/**
* 通过onPrepared播放
*/
public void onPrepared(MediaPlayer arg0) {
arg0.start();
Log.e("mediaPlayer", "onPrepared");
}

@Override
public void onCompletion(MediaPlayer arg0) {
Log.e("mediaPlayer", "onCompletion");
}

@Override
public void onBufferingUpdate(MediaPlayer arg0, int bufferingProgress) {
skbProgress.setSecondaryProgress(bufferingProgress);
int currentProgress = skbProgress.getMax()
* mediaPlayer.getCurrentPosition() / mediaPlayer.getDuration();
Log.e(currentProgress + "% play", bufferingProgress + "% buffer");
}

/**
* 播放音乐
*
* @param playPosition
*/
private void playNet(int playPosition) {
try {
mediaPlayer.reset();// 把各项参数恢复到初始状态
mediaPlayer.setDataSource(videoUrl);
mediaPlayer.prepare();// 进行缓冲
mediaPlayer.setOnPreparedListener(new MyPreparedListener(
playPosition));
} catch (Exception e) {
e.printStackTrace();
}
}

private final class MyPreparedListener implements
android.media.MediaPlayer.OnPreparedListener {
private int playPosition;

public MyPreparedListener(int playPosition) {
this.playPosition = playPosition;
}

@Override
public void onPrepared(MediaPlayer mp) {
mediaPlayer.start();// 开始播放
if (playPosition > 0) {
mediaPlayer.seekTo(playPosition);
}
}
}
}
MainActivity.java文件
public class MainActivity extends Activity {

private Button btnPause, btnPlayUrl, btnStop,btnReplay;
private SeekBar skbProgress;
private Player player;
private EditText file_name_text;
private TextView tipsView;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_palyer);

this.setTitle("在线音乐播放---ouyangpeng编写");

btnPlayUrl = (Button) this.findViewById(R.id.btnPlayUrl);
btnPlayUrl.setOnClickListener(new ClickEvent());

btnPause = (Button) this.findViewById(R.id.btnPause);
btnPause.setOnClickListener(new ClickEvent());

btnStop = (Button) this.findViewById(R.id.btnStop);
btnStop.setOnClickListener(new ClickEvent());

btnReplay = (Button) this.findViewById(R.id.btnReplay);
btnReplay.setOnClickListener(new ClickEvent());

file_name_text=(EditText) this.findViewById(R.id.file_name);
tipsView=(TextView) this.findViewById(R.id.tips);

skbProgress = (SeekBar) this.findViewById(R.id.skbProgress);
skbProgress.setOnSeekBarChangeListener(new SeekBarChangeEvent());

String url=file_name_text.getText().toString();
player = new Player(url,skbProgress);

TelephonyManager telephonyManager=(TelephonyManager) getSystemService(Context.TELEPHONY_SERVICE);
telephonyManager.listen(new MyPhoneListener(), PhoneStateListener.LISTEN_CALL_STATE);
}

/**
* 只有电话来了之后才暂停音乐的播放
*/
private final class MyPhoneListener extends android.telephony.PhoneStateListener{
@Override
public void onCallStateChanged(int state, String incomingNumber) {
switch (state) {
case TelephonyManager.CALL_STATE_RINGING://电话来了
player.callIsComing();
break;
case TelephonyManager.CALL_STATE_IDLE: //通话结束
player.callIsDown();
break;
}
}
}

class ClickEvent implements OnClickListener {
@Override
public void onClick(View arg0) {
if (arg0 == btnPause) {
boolean pause=player.pause();
if (pause) {
btnPause.setText("继续");
tipsView.setText("暂停播放...");
}else{
btnPause.setText("暂停");
tipsView.setText("继续播放...");
}
} else if (arg0 == btnPlayUrl) {
player.play();
tipsView.setText("开始播放...");
} else if (arg0 == btnStop) {
player.stop();
tipsView.setText("停止播放...");
} else if (arg0==btnReplay) {
player.replay();
tipsView.setText("重新播放...");
}
}
}

class SeekBarChangeEvent implements SeekBar.OnSeekBarChangeListener {
int progress;
@Override
public void onProgressChanged(SeekBar seekBar, int progress,
boolean fromUser) {
// 原本是(progress/seekBar.getMax())*player.mediaPlayer.getDuration()
this.progress = progress * player.mediaPlayer.getDuration()
/ seekBar.getMax();
}

@Override
public void onStartTrackingTouch(SeekBar seekBar) {

}

@Override
public void onStopTrackingTouch(SeekBar seekBar) {
// seekTo()的参数是相对与影片时间的数字,而不是与seekBar.getMax()相对的数字
player.mediaPlayer.seekTo(progress);
}
}
}
OK,在项目文件AndroidManifest.xml里面添加权限
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.netmusic"
android:versionCode="1"
android:versionName="1.0" >

<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="18" />
<uses-permission android:name="android.permission.INTERNET" />
<!-- 注意:这里要加入一个监听电话的权限 -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme" >
<activity
android:name="com.example.netmusic.MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>
这样做出来的就很原始了,1毛钱特效都没有的那种。 

)7IQP84I3_JP`O(R)SML(0S.png


按钮这些可以自己定义背景,有专门的媒体按钮,去http://www.iconfont.cn/上找,很多的。 
最主要的是咱们的进度条SeekBar,,原始是不是太丑了?来,我们加个样式吧。 
在style文件里面:
 </style>
<style name="Widget.SeekBar.Normal" parent="@android:style/Widget.SeekBar">
<item name="android:maxHeight">8.0dip</item>
<item name="android:indeterminateOnly">false</item>
<item name="android:indeterminateDrawable">@android:drawable/progress_indeterminate_horizontal</item>
<item name="android:progressDrawable">@drawable/seekbar_horizontal</item>
<item name="android:minHeight">8.0dip</item>
<item name="android:thumb">@drawable/seek_thumb</item>
<item name="android:thumbOffset">10.0dip</item>
</style>
新建seekbar_horizontal.xml,drawable里面的
<?xml version="1.0" encoding="UTF-8"?>  
<layer-list
xmlns:android="http://schemas.android.com/apk/res/android">
<item android:id="@android:id/background" android:drawable="@drawable/seek_bkg" />
<item android:id="@android:id/secondaryProgress">
<clip>
<shape>
<corners android:radius="2.0dip" />
<gradient android:startColor="#80ffd300" android:endColor="#a0ffcb00" android:angle="270.0" android:centerY="0.75" android:centerColor="#80ffb600" />
</shape>
</clip>
</item>
<item android:id="@android:id/progress">
<clip android:drawable="@drawable/seek" />
</item>
</layer-list>
ok,还有几个图片素材 
 



1L0{YQ_XOX5S1Y70HKSI.png


seek.9.png




F{(26PX(W`EP}U8G0QETL]G.png


 seek_bkg.9.png




716K_JO8`CS]O98FA@XPTR.png


 seek_thumb.png


代码里面引用:
 <SeekBar
android:id="@+id/skbProgress"
style="@style/Widget.SeekBar.Normal"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:max="100"
android:paddingLeft="10dip"
android:paddingRight="10dip" >
</SeekBar>
这样就可以了,看下效果: 

 
本文由环信热心用户SeanMis小七发表,个人博客地址SeanMis小七
作者QQ:1453022932 收起阅读 »

Android Studio使用技巧

作为开题,我们先看看 log 输出的快捷键,在需要大量输出 log 调试程序时,快捷键能为我们节省大量的工作量。 //logd + Enter Log.d(TAG, "onCreate "); //logm + Enter 可以快速输...
继续阅读 »
作为开题,我们先看看 log 输出的快捷键,在需要大量输出 log 调试程序时,快捷键能为我们节省大量的工作量。
//logd + Enter
Log.d(TAG, "onCreate ");
//logm + Enter 可以快速输出方法中的参数log信息
Log.d(TAG, "onCreate() called with " + "savedInstanceState = [" + savedInstanceState + "]");
//loge + Enter
Log.e(TAG, "onCreate ");

 快捷键及相关的使用:

【1】新建了Android library module -> settings.gradle多了':mylibrary'
【2】自动导入包:File->Settings->Editor->General->Auto Import->把Java的勾都打上
【3】设置快捷键类型:File->Settings->搜索keymap
【4】Ctrl+Alt+空格 代码提示
【5】Ctrl+Shift+↑或↓ 移动代码位置
【6】复制上一行代码,并显示在当行 Ctrl+D
【7】删除一行代码 Ctrl+Y
【8】在方法间快速移动 Alt+↑或↓
【9】移动滚动条 Ctrl+↑或↓
【10】Ctrl+W 选中代码,多次按会不同效果
【11】Ctrl+N 查找类
【12】查找文件,如xml Ctrl+Shift+N
【13】在本类中按Ctrl+U 查找本类的父类
【14】选中方法按Ctrl+Alt+h 查找这个方法被调用的地方
【15】查看一个方法的实现 选中方法按Ctrl+Shift+i
【16】在本类中按Ctrl+H 查看本类的层级结构
【17】Ctrl+Alt+← 返回代码跳转前的位置
【18】Alt+→或← 切换打开的文件
【19】光标在方法里,按Ctrl + -或+ 展开或折叠方法
【20】Alt+1 隐藏或显示左侧的工程面板
【21】Ctrl+Shift+Alt+N 查找本类中的方法
【22】Ctrl+F12 查看本类的结构,显示本类的方法和数据域等, 在此基础上按Ctrl+I或打勾右边,可查看匿名内部类
【23】Ctrl+O 覆盖父类的方法
【24】光标处于方法的一个大括号,按Ctrl+ [ 或 ] 跳转到方法大括号的另一端
【25】选中模块,按Ctrl+Alt+T,可快速生成try catch等语句
【26】Ctrl+鼠标左键点击Activity左边的布局图标,可快速打开与本Activity有关联的布局
【27】Ctrl+J 快捷生成判空、循环、findViewById、Toast等代码,同时能查看其他快捷键使用方式
【28】Alt + Enter 错误提示
【29】Ctrl + F 在本类中查找相同元素
【30】Shift+F6 在本类中整体修改元素( 牵一发而动全身 )
【31】Ctrl+R 在本类中整体查找,整体替换
【32】Ctrl+E 查看最近打开的文件
【33】格式化代码 Ctrl+Alt+L

Debug介绍:

F8单步调试,F7进入方法,Shift+F8跳到另一个断点位置
若想在"不修改"代码的前提下,在控制台Console而不是Logcat中输出log,则可采用以下方法:

右键断点



1362950-9f397e6b478df701.png


点击Suspend


1362950-3016c4b437cfe513_(1).png


在Log evaluated expression中输入要打印的log

1362950-feb959ce81a1255b.png


输出效果如下,这种方法能 "避免修改代码",读者可常用

1362950-7f596df23246c09b.png


在debug过程中可快速修改变量值,在下面 "i=1" 处右键,点 Set Value 即可

1362950-7df50e09921952c0.png


也可以点击 Add to Watches 把要观察的变量添加到 Watches 中

点击上图的第二个红色按钮,View Breakpoints

1362950-614b2d8d0710b7c7.png


左侧显示了已标注的断点位置,可通过取消勾选,来实现 ”不去除断点,但不运行已取消勾选的断点“ 。 收起阅读 »

环信编程大赛优秀开源项目系列之一:“文播”一款文字直播APP

根据IDC数据显示,中国有近200万开发者,身为一个程序员,我们生活在一个 IT 系统越发复杂且多变化的时代。有时候执行一个简单的开源项目,开发一个基础功能都需要精准定义并耗费大量时间专注任务。随着云计算的兴起,API 和SDK开始作为软件之间重要媒介而作为一...
继续阅读 »
根据IDC数据显示,中国有近200万开发者,身为一个程序员,我们生活在一个 IT 系统越发复杂且多变化的时代。有时候执行一个简单的开源项目,开发一个基础功能都需要精准定义并耗费大量时间专注任务。随着云计算的兴起,API 和SDK开始作为软件之间重要媒介而作为一种独立应用而存在,“一切皆软件,一切皆API,一切皆SDK”。通过API和SDK可以让开发者摆脱繁重的基础功能底层开发,短时间即可让App拥有各种诸如内置IM、统计等基础功能组件能力。 

5月14日,由环信联合猿圈共同推出的“首届环信编程大赛”颁奖典礼在中关村义创空间隆重举行。本次环信编程大赛历时两个月,由线上初赛、决赛和颁奖典礼三个环节组成,总计报名人数2000+,收到决赛项目100+。最终由评委会认定的13个优秀开源项目及开发者集体亮相颁奖典礼。其中“方圆十里”、“高仿微信“和“咚咚”三个开源项目名列前三,共同分享了15000元奖金和价值12000元的专属表情包。



QQ截图20160516205232.jpg


优秀项目开发者合影




_OU}M`{}CR`T@5D52W6RW1.png



这枚可爱的小鲜肉竟然是本次环信编程大赛发起人,目前单身,私信可获得联系方式!


其余入围的十余个优秀开源项目同样引起了到场开发者的热烈追捧,环信将分期将入围的优秀项目代码免费开源给小伙伴们。今天我们带来的是一款基于环信sdk进行个性化改造的文字直播平台App——“文播”。典型的使用场景包括经典的文字直播项目——直播球赛,以及现在流行的直播游戏,再加上直播生活技能、直播课程等,都能在“文播”里找到对应的频道。




QQ截图20160516204700.jpg


“文播”项目负责人董艺菲分享技术开发细节


 



QQ截图20160516202827.jpg


“文播”APP界面截图


功能:


本项目是一款基于环信sdk进行个性化改造的文字直播平台性的安卓app。
在参赛报名的时候,曾想过这样一个问题:一款完全为IM而生的sdk,到底能有如何的潜力?因此,另辟蹊径将环信提供的IM群聊功能,通过重新设计,改造成了现在的文字直播的平台类型app。
每个直播间,其实就是一个“只有群创建者才能发言”的IM群组或讨论组,再进行一些界面上的改造,就可以实现一款类似于从早期非智能机时代流行至今的纯文字直播的app。
典型的使用场景包括经典的文字直播项目——直播球赛,以及现在流行的直播游戏,再加上直播生活技能、直播课程等,都能在《文播》里找到对应的频道。
提交的该版本目前为纯游客端,主播端另行实现。


技术:


·客户端使用DrCoSu工作室开源的dileber框架,MVP设计模式,整个项目冗余较低。
·融合环信SDK,并进行了个性化的改造。
·采用.9格式存储图片,ttf方式呈现界面与图标,各个机型兼容性较好。
·服务端采用Java(Spring),配合ngix和redis极大提升了访问响应速度。
·采用http通信和json、xml等数据格式,移植性和通用性好。


心得


重复造轮子虽然好,但是在实际开发中,往往可以使用更好的方式来加快你的节奏,从中获得更大的成就感。
环信SDK在即时通讯云领域是一款足够优秀的SDK。配合JPush和好的创意,能实现无限多的可能性。
创意是一款新型软件的核心竞争力。


介绍


文字的直播,一样精彩。




QQ截图20160516203637.jpg



特别感谢以下企业的大力支持:


义创空间提供颁奖场地
 
萌岛从自有形象库中授权一套价值12000元的表情包
 
Emokit赞助Apple Watch一台
 
猿圈全程提供技术评测支持


项目托管地址:https://sourceforge.net/p/wenbo-im/git/ci/master/tree/
 “文播”源码下载及演讲PPT下载↓↓↓ 收起阅读 »

环信“客户声音”后台谍照泄露

PS:小编觉得是产品故意发粗来的。 [[嘘]] 环信近期上线了业界首个“客户声音”产品,可以通过热点话题分析发现新畅销商品,通过情感度分析发现服务问题,来帮助企业更好的来倾听客户的声音。小编将持续为您报道,更多细节请密切关注
PS:小编觉得是产品故意发粗来的。 [[嘘]] 环信近期上线了业界首个“客户声音”产品,可以通过热点话题分析发现新畅销商品,通过情感度分析发现服务问题,来帮助企业更好的来倾听客户的声音。小编将持续为您报道,更多细节请密切关注

基于环信开发的部分项目展示

基于环信开发的部分项目                                 ...
继续阅读 »
基于环信开发的部分项目

1.jpg


 


3.jpg


 
 


4.png


 
 


5.png


 
 


6.jpg


 
 


9.jpg


 
 


11.png


 
 


12.png


 
 


14.jpg


 
 


34.png


 
 
  收起阅读 »

创业关闭大潮,它潜伏修炼

创业关闭大潮,它潜伏修炼    在去年通过朋友圈的推荐,下载了一款产品摘客,一开始被他们的独特的分类阅读所吸引,虽然产品当时还有很多的问题,但是对于用户来说,其独特的细分个性化阅读有别于其他APP,当时的体验还是不错的。还有其搜索功能,当时发现搜索时,很多...
继续阅读 »
创业关闭大潮,它潜伏修炼 
 
在去年通过朋友圈的推荐,下载了一款产品摘客,一开始被他们的独特的分类阅读所吸引,虽然产品当时还有很多的问题,但是对于用户来说,其独特的细分个性化阅读有别于其他APP,当时的体验还是不错的。还有其搜索功能,当时发现搜索时,很多时候都能意外的发现很多好文章,因此建立了该产品的第一映像。 
 
后来机缘巧合,认识到这款产品的团队负责人,因此也特地对摘客与其进行了相关的交流和探讨。 
 
惊讶的是整个团队不过10+人,就包括了后台、前端、测试、运营等人员,原本以为主打个性化推荐算法的产品,相对而言对产品的复杂度和门槛会高一点点,应该有几十人的团队在维护,如今日头条,虽然人家已经上千人的团队,但起步的时候也远远不止10人的团队。不过所有创业团队在起步的时候一定是精简的,一人能敌好几个人用。光这一点,对摘客又多了几分好感。 
 
当然,我也从我的角度指出了很多产品存在的问题,比如界面相较于其他和产品定位而言,UI风格的混乱和不统一;用户评论机制的鸡肋和打造UGC社区氛围问题;产品人群定位等问题都进行了探讨;
 
就个性化推荐而言,摘客切入的是互联网资讯垂直行业,相较于目前已经做得较好的科技媒体,像36氪、虎嗅、极客公园,以及同样主打个性推荐的今日头像,摘客还是有明显的差异性,找到了细分领域。而就阅读而言,用户是否真的需要这么细分的需求呢?这是产品值得思考的问题。 
 
摘客负责人告诉我们,已将上线的2.0.0版本会对目前的产品有一个较大的改进和提高,尤其是目前被人诟病的界面,因此当下就去下载更新了,大改版的白蓝为主色的界面,真实小清晰一脸啊,侧拉导航改为底部导航,也大大比旧版提高了用户的交互体验; 
 


图片1.png




图片2.png



通过深入的了解后,整个团队给人的感觉相较一般的创业团队,更耐得住性子,沉下心来打磨一件他们认为有价值的产品。相比那些为了拿钱融资,冲业务指标而放弃初心的很多创业团队,更欣赏现在还可以静心做产品的团队。 
 
值得恭喜的是,他们告诉我摘客已经在融资阶段,并且有了明确性的资金意向,具体的消息相信在不久的将来可以听到。 收起阅读 »

【猿团专访】|刘俊彦:PaaS、SaaS齐头并进 环信领军企业服务市场

刘俊彦,毕业于伦敦大学国王学院,计算机硕士,是一个有17年研发经验的老程序员。在创办环信之前,先后任职IONA,RedHat等跨国公司的研发中心,主要专注在高并发消息中间件、实时消息系统、异构分布式企业系统集成和应用服务器。同时,刘俊彦本人还是开源软件的重度倡...
继续阅读 »
刘俊彦,毕业于伦敦大学国王学院,计算机硕士,是一个有17年研发经验的老程序员。在创办环信之前,先后任职IONA,RedHat等跨国公司的研发中心,主要专注在高并发消息中间件、实时消息系统、异构分布式企业系统集成和应用服务器。同时,刘俊彦本人还是开源软件的重度倡导者,是Apache CXF、JBOSS Drools、JBPM、JBOSS ESB、SOA-P等多个世界知名开源软件项目的核心代码贡献者。


4ea62a46ff9f4512fcbdc14bfdf7fcc3.jpg


作为环信CEO,刘俊彦认为,要保持行业竞争力,最重要的是远见、视野以及执行力。这个目光如炬的男人,用他独有的市场洞察力,带领环信成为国内最大的即时通讯云平台及国内最大的SaaS客服平台。本期猿团专访,笔者将带着大家一起来了解环信背后的故事。

以下为猿团记者专访内容,原创作品,如需转载请注明出处。
 
回望:创业伊始 环信面临招聘难

和所有创业公司一样,环信最初的创立也是艰难重重,最大的挑战还是人才的招聘。刘俊彦告诉笔者,环信是从中关村创业大街上的车库咖啡起家的。当时,几位创始人带着几位初出茅庐的年轻程序员,在一个咖啡桌边一坐就是一年。由于办公环境简陋,因此招聘特别困难。每次打电话过去,候选人一听说办公地点是在一个咖啡厅,就狐疑丛生,不是当场拒绝,就是约好的面试频频被放鸽子。幸运的是,襁褓中的环信,凝聚了一批坚定的创业者,怀揣着用技术改变世界的理想,团队在一点点的壮大成熟。更为幸运的是,当时那条咖啡桌边的初出茅庐的年轻程序员们,没有一位离开环信,而且全部都成长成熟起来,在各自的岗位上发挥着中坚作用。

如今的环信,团队规模已有数百人,早已搬进高大上的办公楼,但环信“团结最优秀的人,一起用技术改变世界”的初心一直不曾改变。谈到这里,刘俊彦表示:”最近经常有人问,’现在加入环信会不会晚了点,还有没有足够好的位置留给我?’。我想说的是,不管是创业还是加入一家公司,选择永远大于努力。如果有一张登上火箭飞船的票,你不需要问这是几等舱。”

两大产品线齐头并进 环信领军通讯市场

虽然前期遭遇了种种困难,但在团队的努力下,目前环信即时通讯云目前已经是国内最大的即时通讯云平台,共服务了50833家 App 客户,SDK覆盖手机终端3.19亿台,平台日均发送消息2.1亿条。而环信移动客服也已经成为国内最大的SaaS客服平台,覆盖了电商、O2O、金融保险、教育等多个行业的众多标杆客户,典型用户包括国美在线,58到家,神州专车,金融界,学而思等。环信初步完成了对连接人与人和连接人与商业的两个核心场景的全覆盖。

91331a0a0ef3ef37cac2f69b29921efc.jpg


笔者了解到,目前环信有两条产品线。一条产品线是环信即时通讯云,是国内第一家,也是最大的一家即时通讯云平台。另一条产品线是环信移动客服,是目前国内最大的SaaS客服平台。

为什么会有两条产品线呢?刘俊彦表示,在环信看来,IM天然就有2种场景。第一种是使用IM让人和人之间沟通。这个是做社交,也就是连接人与人。不管是微信、陌陌、还是环信即时通讯云平台上的6万多家APP,他们做的都是在连接人与人。在这方面,环信即时通讯云希望帮助开发者和企业更好的连接人与人,做即时通讯云领导者。

IM的第二种场景就是使用IM让人和商家之间沟通。这个就是连接人与商业。不管是淘宝旺旺,还是微信公共账号,其本质都是用IM的方式让人(消费者)和商家更好的沟通和服务。环信移动客服的愿景就是更好的连接人与商业,领军移动客服。这个产品的主要服务对象是各行各业的企业和商家。只要商家有和消费者沟通的需求,有服务消费者的需求,就可以使用环信移动客服。

1bc87932d2c90428fe3da6507ccfa32b.jpg


好PaaS产生好SaaS 环信优势明显

环信是国内最早提供即时通讯云服务的企业,技术先发优势和获客先发优势非常明显,现在主流的大型APP市场基本都被环信垄断。在很多用户使用完环信的PaaS服务后,越来越多的涌现出客服使用的需求。而互联网的发展,也的确将客服服务从PC端转移到移动端,移动端客服成为整个客服软件市场的核心战场,谁打赢了移动端客服的战斗,谁就打赢了整个客服软件的战斗。这一点的发现,让刘俊彦坚持要把移动客服做下去。

说到环信移动客服这条产品线的开发,刘俊彦回想起当时的情景:“在最开始,我们完全没有想过要做环信移动客服这么一个SaaS形态的客服产品。甚至当我决定要做这个产品的时候,连股东内部都有不同的声音。但我顶住压力,不但做了这个产品,而且是‘all in’地去做。后来,环信移动客服一经推出,就获得了巨大成功,这也证明当初的决定是多么正确。“

根据易观国际的研究报告,环信移动客服在移动端SaaS客服市场的占有率高达77.4%。对于移动客服取得如此的成功,刘俊彦并不惊讶,因为在他看来,移动端客服软件最根本的核心技术就是IM技术。没有IM技术,就没法做移动端客服。环信作为中国最大的即时通讯云厂商,在IM技术的领先优势可想而知。这一优势直接造就了环信移动客服目前的市场地位。

目前环信是众多SaaS客服厂商中唯一一个拥有自主成熟IM技术的厂商,拥有专为移动互联网和手机终端深度优化的的私有通讯协议,同时,电信级高可靠、高并发的即时通讯公有云平台支撑移动客服的稳定性,经过实际验证,可支持亿级用户同时在线。“好PaaS必定产生好SaaS ”刘俊彦如是说。

加大智能投入 环信引领客户服务行业进入人工智能时代

和其他友商不同,环信极其重视在人工智能和大数据领域的投入。环信坚信,人工智能技术将在2年内彻底改变客户服务软件行业。环信很早就建立了人工智能和大数据研究院,数十名业界顶尖的科学家和工程师在人工智能与客户服务结合的这一新兴领域做了大量世界前沿的研究和产品化工作,尽管成本巨大,但刘俊彦认为非常值得。目前,环信已经推出了环信机器人,环信客户声音,环信智能质检,环信社交大数据等多个产品。使得环信在产品布局和产品能力上一直保持行业领先地位。

当被问及环信未来还有何发展时,刘俊彦淡淡一笑说:“当每天有几亿条消息在你的系统里流过,当每天有几百个客户,同时也是这个时代的一批最优秀的创业者向你提出他们最期待的新功能需求时,你已经‘被’站在了时代的风口浪尖,占据了制高点。只要你善于倾听,不畏惧改变,你一定不会错过时代的脉搏。所以我并不是特别确定今后我们环信公司还能为我们整个移动互联的大时代带来什么新的突破和惊喜,但我相信一点,当未来到来时,我们一定已经在那里。”
(文章来源:猿团 作者:瘦司)
  收起阅读 »

iOS项目更新之升级Xcode7 & iOS9

前言 Apple 的WWDC所发布内容在给大家带来惊喜之际,给各位iOS开发的同仁却也带来了不同程度的麻烦。首先不讲新功能,就单指原来老版本的项目升级、代码升级,就是一堆问题,而且是不得不面临的问题。下面就跟着笔者一起来回顾下,此次在项目升级过程中,所遇到的...
继续阅读 »
前言

Apple 的WWDC所发布内容在给大家带来惊喜之际,给各位iOS开发的同仁却也带来了不同程度的麻烦。首先不讲新功能,就单指原来老版本的项目升级、代码升级,就是一堆问题,而且是不得不面临的问题。下面就跟着笔者一起来回顾下,此次在项目升级过程中,所遇到的各个问题点,以及解决方案,与各位已经做过和正在做iOS代码升级的同仁共勉,也给各位将要做Xcode 7和iOS9兼容的同仁以参考。

开发环境安装
 
原本运行得好好的项目,要升级Xcode7,首先就得安装Xcode7,具体的可以从开发者官网下载(目前最新版本是Xcode_7_GM_seed).下载好后,就双击下载好的dmg包,当然,前提还是需要我们的Mac环境升级到Mac OS 10.10.4+(图1.1),就可以打开Xcode安装镜像,如图1.2:



vYjia2.png


图1.1 Mac OS 更新示意图


 



BRviYf7.png


图1.2 Xcode 7 GM安装


 接下来,我们只要将图1.1所示的Xcode拖动到指定文件夹,即可完成安装,接下来,我们只要双击运行即可。

开发环境运行

各位可能会觉得,笔者在此还要讲开发环境的运行,是不是多此一举。其实并非如此,综合笔者这几年iOS开发经验的总结,运行新版本,特别是测试版本的Xcode是一个需要格外小心的事情,讲起来都是血泪史。

在运行Beta 版本Xcode时,我们需要特别注意以下几个方面:

- 在运行Beta版本Xcode前,务必要退出原来正式版本Xcode(如Xcode 6.4)

- 在运行Beta版本Xcode时,务必要避免双击打开工程文件(也是为了避免新旧版本同时运行)。

- 如果要切换回原来版本时,一定要先退出Beta版本,而且尽可能将Xcode的缓存数据清除。

当然,可能在实际的过程中,还是会有不少朋友就这么干了,当然,如果我们App后续只需要使用新版本Xcode,自然是没有太大关系,只是对于还需要用旧版本来开发或者发布App的朋友,可能就会有点麻烦,可能在用旧版本编译App在运行的时候,就会出现各种诡异的现象(如打印信息明明是正常,App运行逻辑却不正常等)。这时,可能大家要考虑的就是把Xcode删除掉,重新来过,甚至是重装操作系统。当然,不知道是否有朋友有更好的方案。不过笔者是不再想经历这种事情了。

App 项目运行

待项目运行,首先会碰到的问题就是配置兼容,会出现如下错误



bUZNJz.png


图2.1 BitCode 错误


 当我们看到App编译报错的时候,首先想项目不兼容Xcode7,再仔细一看
ld: ‘/Volumes/MacintoshHD/…/AnimationDesk Universal/Sources/AnimaitonDesk Universal/Classes/Supporting Files/GoogleLibrary/libGoogleAnalyticsServices.a(TAGDataProvider.o)' does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture arm64

 其中 ENABLE_BITCODE 吸引了我们的注意,看结合其它的描述信息,基本可以确定是我们使用的第三方静态库(.a)不支持BitCode,当然,我们对应就有如下两种方案来解决:

方法一:更新对应的第三方静态库(现在更新的静态库,基本都能支持BitCode)

方法二:可以将Xcode7默认开启的BitCode功能关闭,如图2.2所示



nUJFJr.png


图2.2 关闭BitCode 操作示意图


 当然,除了上面的问题外,当我们在添加Framework的时候,会发现此前导入的动态链接库(dylib)他部变成了红色,如图2.3所示,所幸的是,就算不替换成Xcode 7新的动态库文件(.tbd),仍然可以正常运行.



7RrAbaF.png


图2.3 动态链接库丢失示意图


 最后,部分App在编译的时候,可能还会收到如下报错,小编也遇到过一次
All interface orientations must be supported unless the app requires full screen.
 看到这句提示,就是说App默认是有开启了多任务功能,而多任务功能是需要App支持所有方向,如果我们App是有需要支持多任务,则需要开启App对各个方向(上、下、左、右)的支持;如果App不需要开启多任务,则只需要将如下示意图的 requires full screen 勾选上就ok(如图2.4)。



vu2MB3.png


图2.4 勾选 Requires full screen示意图


 不出意外,接下来,App应该是能正常编译运行(小编的AnimationDesk Cloud接下来是可以正常运行),但紧接着,发生了更诡异的事情,以前的的网络访问,现在完全访问不通;大家也许会觉得这可能是服务器挂了,或是外网被墙了,小编最初也是这么想的,但事实上,服务器(从Safari)还是能照常被访问,只是App访问不了,于是后来联想到iOS9 WWDC讲到的网络数据传输安全部分,经过一翻折腾,最终,网络访问的部分也恢复了正常。

其实只要在App的Info.plist里面加入如下信息就可以
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
 添加成功后的示意图如下图(图2.4)



QfyIjy.png


图2.4 添加Transport Security 示意图


 其它事项

可能还有部分朋友跟小编一样,有碰到另外一个现象,就是UITextView,无论怎么设置它的textColor显示的总会是黑色,小编已找到具体的原理,准确地讲,应该是Xcode的一个Bug。

当小编在App开发时,在Xib上面设置过UITextView的背景色(BackgroundColor)为非默认颜色(WhiteColor)时,UITextView的文字颜色(textColor)无论怎么设置,都将会是黑色,如果想要颜色值正常,可以在设置好文本后,再重设一次颜色即可正常。
 
本篇笔记由CocoonJin发表在个人博客,原文地址:http://www.cnblogs.com/CocoonJin/p/4798081.html 收起阅读 »

环信CEO刘俊彦:永远领先一步 用人工智能颠覆客服行业

环信这个成立仅三年的全通讯能力云服务提供商,去年已占据国内SaaS移动端客服市场77.4%的份额。环信CEO刘俊彦在谈及过往企业级服务市场差异的时候说道:“过去中国企业的消费能力没有打开,现在随着移动互联网的发展和信息化程度的提高,市场需求被激发,中国企业级服...
继续阅读 »
环信这个成立仅三年的全通讯能力云服务提供商,去年已占据国内SaaS移动端客服市场77.4%的份额。环信CEO刘俊彦在谈及过往企业级服务市场差异的时候说道:“过去中国企业的消费能力没有打开,现在随着移动互联网的发展和信息化程度的提高,市场需求被激发,中国企业级服务市场打开了,这是最大的区别之一。”

环信现在有2条产品线。一条产品线是环信即时通讯云,是国内第一家,也是最大的一家即时通讯云平台。另一条产品线是环信移动客服,是目前国内最大的SaaS客服平台。



1462783486852n2i39.jpg


环信CEO刘俊彦



通过IM连接人与人,连接人与商业

当艾媒网记者问到刘俊彦环信为什么要做环信移动客服这个产品时,刘俊彦提到,在移动互联网时代,消费者会通过很多渠道来找商家解决问题,比如电话,网页,微博,微信,手机APP,而环信提供的是一种不管通过什么渠道都能够得到服务的这样一款产品,这就是环信的全媒体客服。

刘俊彦说道:“我们做IM起家,我们发现IM天然有两种场景,一种是连接人与人,另一种是连接人与商业。”用IM做社交,像微信那样发送文字语音图片等功能,促进人和人之间沟通,这个是连接人和人;第二种是用IM连接人和商业,最典型的是淘宝旺旺,或者微信公众账号,消费者可以向商家提出问题。环信刚开始做即时通讯云来连接人与人,后来发现连接“人与商业”这个市场和连接“人与人”的即时通讯云市场一样大,于是又做了环信移动客服这个产品。现在环信已经初步完成了对连接人与人和连接人与商业的2个核心场景的全覆盖。

刘俊彦说:“我们的规模和服务用户数量是最高的。我们有六万个APP用户,覆盖了3.1亿部活跃手机。每天发送信息3亿条,高并发能力是被验证过的。”

从移动IM技术入手,垄断行业

客服软件行业很早就存在,随着技术的进步,发展也极其迅速,大致可以分为三个阶段。前互联网时代,互联网还没有得到发展和普及,主要是电话客服,消费者给商家打电话;第二个阶段是进入互联网时代,即在2000——2011年,这时客服行业多了一个渠道,消费者可以通过网站找到商家,线上服务开始了;第三个是在移动互联网时代,据双11天猫统计的结果,用户80%的订单是在手机上完成的,说明如今消费者不再是在PC端或者是在电话上解决问题,遇到问题的第一反应是怎么通过手机上的原生应用得到商家支持。

环信的定位选择在移动端客服入口。刘俊彦说:“客服软件厂家的主战场一定是移动端客服,在这个战场上打赢了就相当于打赢整个客服软件的战斗。所以环信是从我们最擅长的移动IM技术入手,然后进入到移动端的客服软件。在移动端客服软件这个市场,可以说我们环信是一个绝对垄断的地位。”刘俊彦表示环信具有非常深的技术壁垒,从移动端客服这个点做深做透,然后再向全媒体客服市场拓展,做电话的客服,做微信的客服,做网页的客服,以点带面,逐渐垄断整个客服软件市场。刘俊彦认为这也是环信相比于其他客服企业的第一个优势。

加大人工智能投入巅峰行业

环信的第二个优势是在人工智能技术上,刘俊彦在采访时说道:“我们认为人工智能在实体行业第一个落地领域一定是客户服务领域。”客服领域当前是一个非常劳动密集型领域,这个行业面临着中国人口红利消失,90后不愿从事这样的工作,招聘非常难等很多问题。而环信认为这些问题一定要通过技术手段来解决,一些简单的重复的问题由机器人解答,一些复杂的问题也可以改为在人机混合模式下,由机器人提供备选答案,由真人来选择答案。




28期_本周头条.jpg


 


人工智能能会改变这个行业,阿尔法Go和人类的围棋大战将人工智能市场的关注度推上了高潮,在云服务市场,人工智能发展尚未普及和成熟。值得注意的是App市场已经趋于饱和,人工智能聊天正成为新的入口和蓝海。“人工智能会改变这个行业,跟其他的公司比较起来,在人工智能,我们技术的投入在行业是比较靠前的。”刘俊彦坚定地答道。大数据和人工智能的发展对这个行业会有一个颠覆性的改变。我们在大数据方面的投入也是靠前的。环信已推出全媒体智能客服,通过完全自主或人机混合模式的智能机器人技术极大降低人工客服工作量。在这个市场若想实现可持续发展,一定不是陷入同质化产品的无谓竞争,而是基础IT能力的实力较量。一针见血。这个技术出身、目前仍自诩程序员的创业者,洞察市场和未来的时候却不失犀利。

剖析用户痛点 解决用户痛处,永远领先对手一步

刘俊彦在接受艾媒网记者采访时,深入剖析了用户的痛点。首先,他认为客户需要全媒体客服解决方案,去年之前,很多企业客服是以电话为主。去年之后,消费者很快速地向移动端转移。消费者只要不是紧急和复杂的情况,都不想打电话,因为电话是一个很重的沟通方式。很多企业需要同时服务四个渠道,包括电话、网页、微信、APP,以前没有厂家提供。

其次,中国的人口红利正在消失,现在的年轻人越来越不愿意做客服工作。很多公司的客服部门经历了从北京市区迁到郊区,再迁到合肥贵州,下一步可能就要迁到老挝越南了,这是一种不可持续的局面,必须要通过技术手段来解决人力的问题。

刘俊彦针对用户的这些痛点,介绍环信的解决方案。

第一,目前环信已经把全媒体客服产品打磨的很好了。“我们是第一家真正提供全媒体客服的厂家。用户需要的是电话加网页加微信加APP的四合一的整体客服解决方案,数据完全打通。环信这种全媒体客服就解决这种痛点。”刘俊彦说道。

第二,环信已经有一套比较好用的智能机器人系统。人工智能和大数据技术将改变客服行业,将客服行业从一个劳动密集型的行业变成一个高科技驱动的行业,用机器代替一部分的人工。

刘俊彦最后总结说:“我们希望可以永远领先对手一步。当对手气喘吁吁爬上一座山岗以为追上环信时,发现环信已经不在这里了,环信已经在另外一座更高的山岗了。所以我们做了很多有别竞争对手的差异化产品。比如环信反垃圾产品。”。很多社交产品用户被垃圾消息骚扰特别严重从而导致用户流失,我们帮助企业做反垃圾服务从而帮助提高用户留存。环信也是行业首个推出反垃圾服务的厂商。

另一个和对手差异化的产品是环信社交大数据。这个产品帮助企业分析IM用户的关系链和社交行为,从而提高APP的日活和粘性。还有环信业界首推的环信红包功能,这种产品能够帮助客户提高变现能力环信永远在功能上领先对手一个层面。 收起阅读 »

环信即时通讯单聊集成,添加好友,实现单聊

前段时间由于项目需要,了解一下环信即时通讯,然后自己通过查资料写了一个基于环信的单聊demo,一下是源码,希望可以帮助到需要的小伙伴。    先上一下效果图吧 首先,我们要去环信官网注册账号,这个...
继续阅读 »
前段时间由于项目需要,了解一下环信即时通讯,然后自己通过查资料写了一个基于环信的单聊demo,一下是源码,希望可以帮助到需要的小伙伴。 
 
先上一下效果图吧



20160510104925517.png




20160510105128098.png




20160510105400382.png




20160510105511478.png




20160510105614263.png




首先,我们要去环信官网注册账号,这个我就不多说了,注册完登录,创建应用,新建两个测试IM用户,

20160510100622399.png


这里主要用到的是应用标示(Appkey) 

20160510100635834.png




D870.tmp_.jpg


好了,在环信官网下载对应的sdk,这个不多说了,最好下载一个文档,里面讲的很详细的。 
好了,一下是源码 

20160510101347448.png


AppManager.Java
public class AppManager {
private static Stack<Activity> mActivityStack;
private static AppManager mAppManager;

private AppManager() {
}

/**
* 单一实例
*/
public static AppManager getInstance() {
if (mAppManager == null) {
mAppManager = new AppManager();
}
return mAppManager;
}

/**
* 添加Activity
*/
public void addActivity(Activity activity) {
if (mActivityStack == null) {
mActivityStack = new Stack<Activity>();
}
mActivityStack.add(activity);
}

/**
* 获取栈顶Activity
*/
public Activity getTopActivity() {
Activity activity = mActivityStack.lastElement();
return activity;
}

/**
* 结束栈顶Activity
*/
public void killTopActivity() {
Activity activity = mActivityStack.lastElement();
killActivity(activity);
}

/**
* 结束指定的Activity
*/
public void killActivity(Activity activity) {
if (activity != null) {
mActivityStack.remove(activity);
activity.finish();
activity = null;
}
}

/**
* 结束指定类名的Activity
*/
public void killActivity(Class<?> cls) {
for (Activity activity : mActivityStack) {
if (activity.getClass().equals(cls)) {
killActivity(activity);
}
}
}

/**
* 结束所有Activity
*/
public void killAllActivity() {
for (int i = 0, size = mActivityStack.size(); i < size; i++) {
if (null != mActivityStack.get(i)) {
mActivityStack.get(i).finish();
}
}
mActivityStack.clear();
}

/**
* 退出应用程序
*/
public void AppExit(Context context) {
try {
killAllActivity();
ActivityManager activityMgr = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
activityMgr.restartPackage(context.getPackageName());
System.exit(0);
} catch (Exception e) {}
}
}
BaseActivity.java​
public abstract class BaseActivity extends Activity {

protected Context context = null;
protected BaseApplication mApplication;
protected Handler mHandler;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mApplication = (BaseApplication) getApplication();
AppManager.getInstance().addActivity(this);
// check netwotk
context = this;
}

@Override
public void onBackPressed() {
// TODO Auto-generated method stub
super.onBackPressed();
}

@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
}

@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();
}

@Override
protected void onResume() {
// TODO Auto-generated method stub
super.onResume();
}

}
BaseApplication.java​
public class BaseApplication extends Application {

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

private static BaseApplication mInstance = null;

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
}

/*
* (non-Javadoc)
*
* @see android.app.Application#onCreate()
*/
@Override
public void onCreate() {
// TODO Auto-generated method stub
super.onCreate();
int pid = android.os.Process.myPid();
String processAppName = getAppName(pid);
// 如果app启用了远程的service,此application:onCreate会被调用2次
// 为了防止环信SDK被初始化2次,加此判断会保证SDK被初始化1次
// 默认的app会在以包名为默认的process name下运行,如果查到的process name不是app的process
// name就立即返回

if (processAppName == null
|| !processAppName.equalsIgnoreCase("com.xmliu.imsample")) {
Log.e(TAG, "enter the service process!");
// "com.easemob.chatuidemo"为demo的包名,换到自己项目中要改成自己包名

// 则此application::onCreate 是被service 调用的,直接返回
return;
}

// EMChat.getInstance().setAutoLogin(false);
EMChat.getInstance().init(getApplicationContext());
// 在做代码混淆的时候需要设置成false
EMChat.getInstance().setDebugMode(true);
initHXOptions();
mInstance = this;

}

protected void initHXOptions() {
Log.d(TAG, "init HuanXin Options");

// 获取到EMChatOptions对象
EMChatOptions options = EMChatManager.getInstance().getChatOptions();
// 默认添加好友时,是不需要验证的true,改成需要验证false
options.setAcceptInvitationAlways(false);
// 默认环信是不维护好友关系列表的,如果app依赖环信的好友关系,把这个属性设置为true
options.setUseRoster(true);
options.setNumberOfMessagesLoaded(1);
}

private String getAppName(int pID) {
String processName = null;
ActivityManager am = (ActivityManager) this
.getSystemService(ACTIVITY_SERVICE);
List l = am.getRunningAppProcesses();
Iterator i = l.iterator();
PackageManager pm = this.getPackageManager();
while (i.hasNext()) {
ActivityManager.RunningAppProcessInfo info = (ActivityManager.RunningAppProcessInfo) (i
.next());
try {
if (info.pid == pID) {
CharSequence c = pm.getApplicationLabel(pm
.getApplicationInfo(info.processName,
PackageManager.GET_META_DATA));
// Log.d("Process", "Id: "+ info.pid +" ProcessName: "+
// info.processName +" Label: "+c.toString());
// processName = c.toString();
processName = info.processName;
return processName;
}
} catch (Exception e) {
// Log.d("Process", "Error>> :"+ e.toString());
}
}
return processName;
}

@Override
public void onLowMemory() {
// TODO Auto-generated method stub
super.onLowMemory();
Log.i(TAG, "onLowMemory");
}

@Override
public void onTerminate() {
// TODO Auto-generated method stub
Log.i(TAG, "onTerminate");
super.onTerminate();
}

public static BaseApplication getInstance() {
return mInstance;
}

}
ChatListAdapter.java​
public class ChatListAdapter extends BaseAdapter {

Context mContext;
List<ChatListData> mListData;

public ChatListAdapter(Context mContext, List<ChatListData> mListData) {
super();
this.mContext = mContext;
this.mListData = mListData;
}

@Override
public int getCount() {
// TODO Auto-generated method stub
return mListData.size();
}

@Override
public Object getItem(int arg0) {
// TODO Auto-generated method stub
return null;
}

@Override
public long getItemId(int arg0) {
// TODO Auto-generated method stub
return 0;
}

@Override
public View getView(int index, View cView, ViewGroup arg2) {
// TODO Auto-generated method stub

Holder holder;
if (cView == null) {
holder = new Holder();
cView = LayoutInflater.from(mContext).inflate(
R.layout.chat_listview_item, null);
holder.rAvatar = (Button) cView
.findViewById(R.id.listview_item_receive_avatar);
holder.rContent = (TextView) cView
.findViewById(R.id.listview_item_receive_content);
holder.chatTime = (TextView) cView
.findViewById(R.id.listview_item_time);
holder.sContent = (TextView) cView
.findViewById(R.id.listview_item_send_content);
holder.sAvatar = (Button) cView
.findViewById(R.id.listview_item_send_avatar);
holder.sName = (TextView) cView.findViewById(R.id.name1);
holder.sName1 = (TextView) cView.findViewById(R.id.name2);
cView.setTag(holder);

} else {
holder = (Holder) cView.getTag();
}

holder.chatTime.setVisibility(View.GONE);

if (mListData.get(index).getType() == 2) {
holder.rAvatar.setVisibility(View.VISIBLE);
holder.rContent.setVisibility(View.VISIBLE);
holder.sName.setVisibility(View.VISIBLE);
holder.sName.setText("您的朋友说:");
holder.sContent.setVisibility(View.GONE);
holder.sAvatar.setVisibility(View.GONE);
holder.sName1.setVisibility(View.GONE);

} else if (mListData.get(index).getType() == 1) {
holder.rAvatar.setVisibility(View.GONE);
holder.sName.setVisibility(View.GONE);
holder.rContent.setVisibility(View.GONE);
holder.sContent.setVisibility(View.VISIBLE);
holder.sAvatar.setVisibility(View.VISIBLE);
holder.sName1.setVisibility(View.VISIBLE);
holder.sName1.setText("我");
}
holder.chatTime.setText(mListData.get(index).getChatTime());
holder.rContent.setText(mListData.get(index).getReceiveContent());
holder.sContent.setText(mListData.get(index).getSendContent());

return cView;
}

class Holder {
Button rAvatar;
TextView rContent;
TextView chatTime;
TextView sContent;
TextView sName;
TextView sName1;
Button sAvatar;
}
}
FriendListAdapter.java​
public class FriendListAdapter extends BaseAdapter {

Context mContext;
List<String> mListData;

public FriendListAdapter(Context mContext, List<String> mListData) {
super();
this.mContext = mContext;
this.mListData = mListData;
}

@Override
public int getCount() {
// TODO Auto-generated method stub
return mListData.size();
}

@Override
public Object getItem(int arg0) {
// TODO Auto-generated method stub
return null;
}

@Override
public long getItemId(int arg0) {
// TODO Auto-generated method stub
return 0;
}

@Override
public View getView(int index, View cView, ViewGroup arg2) {
// TODO Auto-generated method stub
FHolder holder;
if (cView == null) {
holder = new FHolder();
cView = LayoutInflater.from(mContext).inflate(
R.layout.friend_listview_item, null);
holder.name = (TextView) cView
.findViewById(R.id.friend_listview_name);
cView.setTag(holder);

} else {
holder = (FHolder) cView.getTag();
}

holder.name.setText(mListData.get(index));

return cView;
}

class FHolder {
TextView name;
}
}
ChatListData.java​
public class ChatListData {

String receiveAvatar;
String receiveContent;
String chatTime;
String sendAvatar;
String sendContent;
/**
* 1 发送; 2接收
*/
int type;
/**
* 1 发送; 2接收
*/
public int getType() {
return type;
}
/**
* 1 发送; 2接收
*/
public void setType(int type) {
this.type = type;
}

public String getReceiveAvatar() {
return receiveAvatar;
}

public void setReceiveAvatar(String receiveAvatar) {
this.receiveAvatar = receiveAvatar;
}

public String getReceiveContent() {
return receiveContent;
}

public void setReceiveContent(String receiveContent) {
this.receiveContent = receiveContent;
}

public String getChatTime() {
return chatTime;
}

public void setChatTime(String chatTime) {
this.chatTime = chatTime;
}

public String getSendAvatar() {
return sendAvatar;
}

public void setSendAvatar(String sendAvatar) {
this.sendAvatar = sendAvatar;
}

public String getSendContent() {
return sendContent;
}

public void setSendContent(String sendContent) {
this.sendContent = sendContent;
}

}
ChatListActivity.java​
public class ChatListActivity extends BaseActivity {

private EditText contentET;
private TextView topNameTV;
private Button sendBtn;
private NewMessageBroadcastReceiver msgReceiver;

private ListView mListView;
private List<ChatListData> mListData = new ArrayList<ChatListData>();
private ChatListAdapter mAdapter;
private InputMethodManager imm;

private String receiveName = null;

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_main);

mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case 0x00001:
imm.hideSoftInputFromWindow(
contentET.getApplicationWindowToken(), 0); // 隐藏键盘
mAdapter.notifyDataSetChanged(); // 刷新聊天列表
mListView.setSelection(mListData.size()); // 跳转到listview最底部
contentET.setText(""); // 清空发送内容
break;
default:
break;
}
}

};

receiveName = this.getIntent().getStringExtra("userid");

initView();

topNameTV.setText(receiveName);
// 只有注册了广播才能接收到新消息,目前离线消息,在线消息都是走接收消息的广播(离线消息目前无法监听,在登录以后,接收消息广播会执行一次拿到所有的离线消息)
msgReceiver = new NewMessageBroadcastReceiver();
IntentFilter intentFilter = new IntentFilter(EMChatManager
.getInstance().getNewMessageBroadcastAction());
intentFilter.setPriority(3);
registerReceiver(msgReceiver, intentFilter);

imm = (InputMethodManager) contentET.getContext().getSystemService(
Context.INPUT_METHOD_SERVICE);

mAdapter = new ChatListAdapter(ChatListActivity.this, mListData);
mListView.setAdapter(mAdapter);

initEvent();
}

private void initView() {
contentET = (EditText) findViewById(R.id.chat_content);
topNameTV = (TextView) findViewById(R.id.chat_list_name);
sendBtn = (Button) findViewById(R.id.chat_send_btn);
mListView = (ListView) findViewById(R.id.chat_listview);
}

private void initEvent() {
// TODO Auto-generated method stub
sendBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
sendMsg();
}
});

contentET.setOnKeyListener(new OnKeyListener() {

@Override
public boolean onKey(View arg0, int keycode, KeyEvent arg2) {
// TODO Auto-generated method stub
if (keycode == KeyEvent.KEYCODE_ENTER
&& arg2.getAction() == KeyEvent.ACTION_DOWN) {
sendMsg();
return true;
}
return false;
}
});
}

void sendMessageHX(String username, final String content) {
// 获取到与聊天人的会话对象。参数username为聊天人的userid或者groupid,后文中的username皆是如此
EMConversation conversation = EMChatManager.getInstance()
.getConversation(username);
// 创建一条文本消息
EMMessage message = EMMessage.createSendMessage(EMMessage.Type.TXT);
// // 如果是群聊,设置chattype,默认是单聊
// message.setChatType(ChatType.GroupChat);
// 设置消息body
TextMessageBody txtBody = new TextMessageBody(content);
message.addBody(txtBody);
// 设置接收人
message.setReceipt(username);
// 把消息加入到此会话对象中
conversation.addMessage(message);
// 发送消息
EMChatManager.getInstance().sendMessage(message, new EMCallBack() {

@Override
public void onError(int arg0, String arg1) {
// TODO Auto-generated method stub
Log.i("TAG", "消息发送失败");
}

@Override
public void onProgress(int arg0, String arg1) {
// TODO Auto-generated method stub
Log.i("TAG", "正在发送消息");
}

@Override
public void onSuccess() {
// TODO Auto-generated method stub
Log.i("TAG", "消息发送成功");
ChatListData data = new ChatListData();
data.setSendContent(content);
data.setType(1);
mListData.add(data);
mHandler.sendEmptyMessage(0x00001);
}
});
}

private class NewMessageBroadcastReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
// 注销广播
abortBroadcast();

// 消息id(每条消息都会生成唯一的一个id,目前是SDK生成)
String msgId = intent.getStringExtra("msgid");
// 发送方
String username = intent.getStringExtra("from");
// 收到这个广播的时候,message已经在db和内存里了,可以通过id获取mesage对象
EMMessage message = EMChatManager.getInstance().getMessage(msgId);
EMConversation conversation = EMChatManager.getInstance()
.getConversation(username);

MessageBody tmBody = message.getBody();

ChatListData data = new ChatListData();
data.setReceiveContent(((TextMessageBody) tmBody).getMessage());
data.setType(2);
mListData.add(data);
mHandler.sendEmptyMessage(0x00001);

Log.i("TAG", "收到消息:" + ((TextMessageBody) tmBody).getMessage());
// 如果是群聊消息,获取到group id
if (message.getChatType() == ChatType.GroupChat) {
username = message.getTo();
}

if (!username.equals(username)) {
// 消息不是发给当前会话,return
return;
}
}
}

@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
unregisterReceiver(msgReceiver);

}

private void sendMsg() {
String content = contentET.getText().toString().trim();
if (TextUtils.isEmpty(content)) {
Toast.makeText(getApplicationContext(), "请输入发送的内容",
Toast.LENGTH_SHORT).show();
} else {
sendMessageHX(receiveName, content);
}
}

}
ChatLoginActivity.java​
public class ChatLoginActivity extends BaseActivity {

private EditText mUsernameET;
private EditText mPasswordET;
private TextView mPasswordForgetTV;
private Button mSigninBtn;
private TextView mSignupTV;
private CheckBox mPasswordCB;

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_login);

mUsernameET = (EditText) findViewById(R.id.chat_login_username);
mPasswordET = (EditText) findViewById(R.id.chat_login_password);
mPasswordForgetTV = (TextView) findViewById(R.id.chat_login_forget_password);
mSigninBtn = (Button) findViewById(R.id.chat_login_signin_btn);
mSignupTV = (TextView) findViewById(R.id.chat_login_signup);
mPasswordCB = (CheckBox) findViewById(R.id.chat_login_password_checkbox);

if (EMChat.getInstance().isLoggedIn()) {
Log.d("TAG", "已经登陆过");
EMGroupManager.getInstance().loadAllGroups();
EMChatManager.getInstance().loadAllConversations();
startActivity(new Intent(ChatLoginActivity.this,
MainActivity.class));
}

mPasswordCB.setOnCheckedChangeListener(new OnCheckedChangeListener() {

@Override
public void onCheckedChanged(CompoundButton arg0, boolean arg1) {
// TODO Auto-generated method stub
if (arg1) {
mPasswordCB.setChecked(true);
//动态设置密码是否可见
mPasswordET
.setTransformationMethod(HideReturnsTransformationMethod
.getInstance());
} else {
mPasswordCB.setChecked(false);
mPasswordET
.setTransformationMethod(PasswordTransformationMethod
.getInstance());
}
}
});

mSigninBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
final String userName = mUsernameET.getText().toString().trim();
final String password = mPasswordET.getText().toString().trim();

if (TextUtils.isEmpty(userName)) {
Toast.makeText(getApplicationContext(), "请输入用户名",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(password)) {
Toast.makeText(getApplicationContext(), "请输入密码",
Toast.LENGTH_SHORT).show();
} else {
EMChatManager.getInstance().login(userName, password,
new EMCallBack() {// 回调
@Override
public void onSuccess() {
runOnUiThread(new Runnable() {
public void run() {
EMGroupManager.getInstance()
.loadAllGroups();
EMChatManager.getInstance()
.loadAllConversations();
Log.d("main", "登陆聊天服务器成功!");
Toast.makeText(
getApplicationContext(),
"登陆成功", Toast.LENGTH_SHORT)
.show();
startActivity(new Intent(
ChatLoginActivity.this,
MainActivity.class));
// mApplication.mSharedPreferences
// .edit()
// .putString("loginName",
// userName).commit();
}
});
}

@Override
public void onProgress(int progress,
String status) {

}

@Override
public void onError(int code, String message) {
if (code == -1005) {
message = "用户名或密码错误";
}
final String msg = message;
runOnUiThread(new Runnable() {
public void run() {
Log.d("main", "登陆聊天服务器失败!");
Toast.makeText(
getApplicationContext(),
msg, Toast.LENGTH_SHORT)
.show();
}
});
}
});
}
}
});

mSignupTV.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
startActivity(new Intent(ChatLoginActivity.this,
ChatRegisterActivity.class));
}
});
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {

new AlertDialog.Builder(ChatLoginActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要退出"
+ getResources().getString(
R.string.app_name) + "客户端吗?")
.setPositiveButton("确定",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
AppManager.getInstance().AppExit(
ChatLoginActivity.this);
ChatLoginActivity.this.finish();
}
})
.setNegativeButton("取消",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
}
}).show();
}

return super.onKeyDown(keyCode, event);
}
}
ChatRegisterActivity.java​
public class ChatRegisterActivity extends BaseActivity {

private EditText mUsernameET;
private EditText mPasswordET;
private EditText mCodeET;
private Button mSignupBtn;
private Handler mHandler;
private CheckBox mPasswordCB;
private TextView mBackTV;
private ImageView mCodeIV;
private String currCode;

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_register);

mHandler = new Handler() {
public void handleMessage(android.os.Message msg) {
switch (msg.what) {
case 1000:
Toast.makeText(getApplicationContext(), "注册成功",
Toast.LENGTH_SHORT).show();
break;
case 1001:
Toast.makeText(getApplicationContext(), "网络异常,请检查网络!",
Toast.LENGTH_SHORT).show();
break;
case 1002:
Toast.makeText(getApplicationContext(), "用户已存在!",
Toast.LENGTH_SHORT).show();
break;
case 1003:
Toast.makeText(getApplicationContext(), "注册失败,无权限",
Toast.LENGTH_SHORT).show();
break;
case 1004:
Toast.makeText(getApplicationContext(),
"注册失败: " + (String) msg.obj, Toast.LENGTH_SHORT)
.show();
break;

default:
break;
}
};
};

mUsernameET = (EditText) findViewById(R.id.chat_register_username);
mPasswordET = (EditText) findViewById(R.id.chat_register_password);
mCodeET = (EditText) findViewById(R.id.chat_register_code);
mSignupBtn = (Button) findViewById(R.id.chat_register_signup_btn);
mPasswordCB = (CheckBox) findViewById(R.id.chat_register_password_checkbox);
mBackTV = (TextView) findViewById(R.id.chat_register_back);
mCodeIV = (ImageView) findViewById(R.id.chat_register_password_code);

mCodeIV.setImageBitmap(IdentifyCode.getInstance().createBitmap());
currCode = IdentifyCode.getInstance().getCode();

mCodeIV.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
mCodeIV.setImageBitmap(IdentifyCode.getInstance()
.createBitmap());
currCode = IdentifyCode.getInstance().getCode();
Log.i("TAG", "currentCode==>" + currCode);
}
});

mBackTV.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
finish();
}
});
mPasswordCB.setOnCheckedChangeListener(new OnCheckedChangeListener() {

@Override
public void onCheckedChanged(CompoundButton arg0, boolean arg1) {
// TODO Auto-generated method stub
if (arg1) {
mPasswordCB.setChecked(true);
mPasswordET
.setTransformationMethod(HideReturnsTransformationMethod
.getInstance());
} else {
mPasswordCB.setChecked(false);
mPasswordET
.setTransformationMethod(PasswordTransformationMethod
.getInstance());
}
}
});
mSignupBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
final String userName = mUsernameET.getText().toString().trim();
final String password = mPasswordET.getText().toString().trim();
final String code = mCodeET.getText().toString().trim();

if (TextUtils.isEmpty(userName)) {
Toast.makeText(getApplicationContext(), "请输入用户名",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(password)) {
Toast.makeText(getApplicationContext(), "请输入密码",
Toast.LENGTH_SHORT).show();
} else if (TextUtils.isEmpty(code)) {
Toast.makeText(getApplicationContext(), "请输入验证码",
Toast.LENGTH_SHORT).show();
} else if (!code.equals(currCode.toLowerCase())) {
Toast.makeText(getApplicationContext(), "验证码输入不正确",
Toast.LENGTH_SHORT).show();
} else {
new Thread(new Runnable() {

@Override
public void run() {
// TODO Auto-generated method stub
try {
// 调用sdk注册方法
EMChatManager.getInstance()
.createAccountOnServer(userName,
password);
mHandler.sendEmptyMessage(1000);
} catch (final EaseMobException e) {
// 注册失败
Log.i("TAG", "getErrorCode:" + e.getErrorCode());
int errorCode = e.getErrorCode();
if (errorCode == EMError.NONETWORK_ERROR) {
mHandler.sendEmptyMessage(1001);
} else if (errorCode == EMError.USER_ALREADY_EXISTS) {
mHandler.sendEmptyMessage(1002);
} else if (errorCode == EMError.UNAUTHORIZED) {
mHandler.sendEmptyMessage(1003);
} else {
Message msg = Message.obtain();
msg.what = 1004;
msg.obj = e.getMessage();
mHandler.sendMessage(msg);
}
}
}
}).start();
}
}
});
}
}
MainActivity.java​
public class MainActivity extends BaseActivity {

private ListView mListView;
private Button mAddBtn;
private Button logoutBtn;
private View addView;
private EditText mIdET;
private EditText mReasonET;
private TextView mUserTV;
private TextView mGoTV;
private FriendListAdapter mAdapter;
private List<String> userList = new ArrayList<String>();

/* 常量 */
private final int CODE_ADD_FRIEND = 0x00001;

@Override
protected void onDestroy() {
// TODO Auto-generated method stub
super.onDestroy();
}

@Override
protected void onCreate(Bundle savedInstanceState) {
// TODO Auto-generated method stub
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_chat_friends);

mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
// TODO Auto-generated method stub
super.handleMessage(msg);
switch (msg.what) {
case CODE_ADD_FRIEND:
Toast.makeText(getApplicationContext(), "请求发送成功,等待对方验证",
Toast.LENGTH_SHORT).show();
break;

default:
break;
}
}

};

EMContactManager.getInstance().setContactListener(
new MyContactListener());
EMChat.getInstance().setAppInited();

mListView = (ListView) findViewById(R.id.chat_listview);
mAddBtn = (Button) findViewById(R.id.chat_add_btn);
mUserTV = (TextView) findViewById(R.id.current_user);
mGoTV = (TextView) findViewById(R.id.friend_list_go);
logoutBtn = (Button) findViewById(R.id.chat_logout_btn);

mUserTV.setText(EMChatManager.getInstance().getCurrentUser());

initList();

mAddBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub
addView = LayoutInflater.from(MainActivity.this).inflate(
R.layout.chat_add_friends, null);
mIdET = (EditText) addView
.findViewById(R.id.chat_add_friend_id);
mReasonET = (EditText) addView
.findViewById(R.id.chat_add_friend_reason);
new AlertDialog.Builder(MainActivity.this)
.setTitle("添加好友")
.setView(addView)
.setPositiveButton("确定",
new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog,
int which) {
dialog.dismiss();
String idStr = mIdET.getText()
.toString().trim();
String reasonStr = mReasonET.getText()
.toString().trim();
try {
EMContactManager.getInstance()
.addContact(idStr,
reasonStr);
mHandler.sendEmptyMessage(CODE_ADD_FRIEND);
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG", "addContacterrcode==>"
+ e.getErrorCode());
}// 需异步处理
}
})
.setNegativeButton("取消",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,
int whichButton) {
dialog.dismiss();
}
}).create().show();

}
});
logoutBtn.setOnClickListener(new OnClickListener() {

@Override
public void onClick(View arg0) {
// TODO Auto-generated method stub

showLogoutDialog();

}
});

mListView.setOnItemClickListener(new OnItemClickListener() {

@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2,
long arg3) {
// TODO Auto-generated method stub
startActivity(new Intent(MainActivity.this,
ChatListActivity.class).putExtra("userid",
userList.get(arg2)));
}
});

mListView.setOnItemLongClickListener(new OnItemLongClickListener() {

@Override
public boolean onItemLongClick(AdapterView<?> arg0, View arg1,
int arg2, long arg3) {
// TODO Auto-generated method stub
showDeleteDialog(userList.get(arg2));
return true;
}
});
}

private void initList() {
try {
userList.clear();
userList = EMContactManager.getInstance().getContactUserNames();
mAdapter = new FriendListAdapter(MainActivity.this, userList);
mListView.setAdapter(mAdapter);
} catch (EaseMobException e1) {
// TODO Auto-generated catch block
e1.printStackTrace();
Log.i("TAG", "usernames errcode==>" + e1.getErrorCode());
Log.i("TAG", "usernames errcode==>" + e1.getMessage());
}// 需异步执行
}

private class MyContactListener implements EMContactListener {

@Override
public void onContactAgreed(String username) {
// 好友请求被同意
Log.i("TAG", "onContactAgreed==>" + username);
// 提示有新消息
EMNotifier.getInstance(getApplicationContext()).notifyOnNewMsg();
Toast.makeText(getApplicationContext(), username + "同意了你的好友请求",
Toast.LENGTH_SHORT).show();
}

@Override
public void onContactRefused(String username) {
// 好友请求被拒绝
Log.i("TAG", "onContactRefused==>" + username);
}

@Override
public void onContactInvited(String username, String reason) {
// 收到好友添加请求
Log.i("TAG", username + "onContactInvited==>" + reason);
showAgreedDialog(username, reason);
EMNotifier.getInstance(getApplicationContext()).notifyOnNewMsg();
}

@Override
public void onContactDeleted(List<String> usernameList) {
// 好友被删除时回调此方法
Log.i("TAG", "usernameListDeleted==>" + usernameList.size());
}

@Override
public void onContactAdded(List<String> usernameList) {
// 添加了新的好友时回调此方法
for (String str : usernameList) {
Log.i("TAG", "usernameListAdded==>" + str);
}
}
}

@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_BACK) {

showExitDialog();
}

return super.onKeyDown(keyCode, event);
}

private void showLogoutDialog() {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要注销" + EMChatManager.getInstance().getCurrentUser()
+ "用户吗?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// EMChatManager.getInstance().logout();
logout(new EMCallBack() {

@Override
public void onSuccess() {
// TODO Auto-generated method stub
startActivity(new Intent(MainActivity.this,
ChatLoginActivity.class));
}

@Override
public void onProgress(int arg0, String arg1) {
// TODO Auto-generated method stub

}

@Override
public void onError(int arg0, String arg1) {
// TODO Auto-generated method stub

}
});

}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).show();
}

public void logout(final EMCallBack callback) {
// setPassword(null);
EMChatManager.getInstance().logout(new EMCallBack() {

@Override
public void onSuccess() {
// TODO Auto-generated method stub
if (callback != null) {
callback.onSuccess();
}
}

@Override
public void onError(int code, String message) {
// TODO Auto-generated method stub

}

@Override
public void onProgress(int progress, String status) {
// TODO Auto-generated method stub
if (callback != null) {
callback.onProgress(progress, status);
}
}

});
}

private void showAgreedDialog(final String user, String reason) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"用户 " + user + " 想要添加您为好友,是否同意?\n" + "验证信息:" + reason)
.setPositiveButton("同意", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
EMChatManager.getInstance().acceptInvitation(user);
initList();
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog1==>" + e.getErrorCode());
}
}
})
.setNegativeButton("拒绝", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
try {
EMChatManager.getInstance().refuseInvitation(user);
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog2==>" + e.getErrorCode());
}
}
})
.setNeutralButton("忽略", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dialog.dismiss();
}
}).show();
}

private void showDeleteDialog(final String user) {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage("确定删除好友 " + user + " 吗?\n")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
try {
EMContactManager.getInstance().deleteContact(user);
initList();
} catch (EaseMobException e) {
// TODO Auto-generated catch block
e.printStackTrace();
Log.i("TAG",
"showAgreedDialog1==>" + e.getErrorCode());
}
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
dialog.dismiss();
}
}).show();
}

private void showExitDialog() {
new AlertDialog.Builder(MainActivity.this)
.setTitle("应用提示")
.setMessage(
"确定要退出" + getResources().getString(R.string.app_name)
+ "客户端吗?")
.setPositiveButton("确定", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
AppManager.getInstance().AppExit(MainActivity.this);
MainActivity.this.finish();
}
})
.setNegativeButton("取消", new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int whichButton) {
}
}).show();
}

}
IdentifyCode.java​
public class IdentifyCode {

private static final char CHARS = { '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'j', 'k', 'l', 'm',
'n', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', 'A',
'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z' };

private static IdentifyCode bmpCode;

public static IdentifyCode getInstance() {
if (bmpCode == null)
bmpCode = new IdentifyCode();
return bmpCode;
}

// default settings
private static final int DEFAULT_CODE_LENGTH = 3;
private static final int DEFAULT_FONT_SIZE = 25;
private static final int DEFAULT_LINE_NUMBER = 2;
private static final int BASE_PADDING_LEFT = 5, RANGE_PADDING_LEFT = 15,
BASE_PADDING_TOP = 15, RANGE_PADDING_TOP = 20;
private static final int DEFAULT_WIDTH = 60, DEFAULT_HEIGHT = 40;

// settings decided by the layout xml
// canvas width and height
private int width = DEFAULT_WIDTH, height = DEFAULT_HEIGHT;

// random word space and pading_top
private int base_padding_left = BASE_PADDING_LEFT,
range_padding_left = RANGE_PADDING_LEFT,
base_padding_top = BASE_PADDING_TOP,
range_padding_top = RANGE_PADDING_TOP;

// number of chars, lines; font size
private int codeLength = DEFAULT_CODE_LENGTH,
line_number = DEFAULT_LINE_NUMBER, font_size = DEFAULT_FONT_SIZE;

// variables
private String code;
private int padding_left, padding_top;
private Random random = new Random();

// 验证码图�?
public Bitmap createBitmap() {
padding_left = 0;

Bitmap bp = Bitmap.createBitmap(width, height, Config.ARGB_8888);
Canvas c = new Canvas(bp);

code = createCode();

c.drawColor(Color.WHITE);
Paint paint = new Paint();
paint.setTextSize(font_size);

for (int i = 0; i < code.length(); i++) {
randomTextStyle(paint);
randomPadding();
c.drawText(code.charAt(i) + "", padding_left, padding_top, paint);
}

for (int i = 0; i < line_number; i++) {
drawLine(c, paint);
}

c.save(Canvas.ALL_SAVE_FLAG);// 保存
c.restore();//
return bp;
}

public String getCode() {
return code;
}

// 验证�?
private String createCode() {
StringBuilder buffer = new StringBuilder();
for (int i = 0; i < codeLength; i++) {
buffer.append(CHARS[random.nextInt(CHARS.length)]);
}
return buffer.toString();
}

private void drawLine(Canvas canvas, Paint paint) {
int color = randomColor();
int startX = random.nextInt(width);
int startY = random.nextInt(height);
int stopX = random.nextInt(width);
int stopY = random.nextInt(height);
paint.setStrokeWidth(1);
paint.setColor(color);
canvas.drawLine(startX, startY, stopX, stopY, paint);
}

private int randomColor() {
return randomColor(1);
}

private int randomColor(int rate) {
int red = random.nextInt(256) / rate;
int green = random.nextInt(256) / rate;
int blue = random.nextInt(256) / rate;
return Color.rgb(red, green, blue);
}

private void randomTextStyle(Paint paint) {
int color = randomColor();
paint.setColor(color);
paint.setFakeBoldText(random.nextBoolean()); // true为粗体,false为非粗体
float skewX = random.nextInt(11) / 10;
skewX = random.nextBoolean() ? skewX : -skewX;
paint.setTextSkewX(skewX); // float类型参数,负数表示右斜,整数左斜
// paint.setUnderlineText(true); //true为下划线,false为非下划线?
// paint.setStrikeThruText(true); //true为删除线,false为非删除线?
}

private void randomPadding() {
padding_left += base_padding_left + random.nextInt(range_padding_left);
padding_top = base_padding_top + random.nextInt(range_padding_top);
}
}
布局文件就相对简单很多了,登录页面很简单,还是贴出来吧。 
activity_chat_login.xml​
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<TextView
android:id="@+id/chat_login_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#131313"
android:gravity="center"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:text="登录"
android:textColor="#fff" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_weight="1"
android:background="#CAFFFF"
android:orientation="vertical"
android:paddingBottom="30dp"
android:paddingLeft="30dp"
android:paddingRight="30dp"
android:paddingTop="60dp" >

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#131313"
android:orientation="vertical" >

<EditText
android:id="@+id/chat_login_username"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="#00000000"
android:drawableLeft="@drawable/login_user"
android:drawablePadding="5dp"
android:ems="10"
android:hint="用户名"
android:inputType="textPersonName"
android:textColor="#fff"
android:textSize="12sp" />

<View
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="#000000" />

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal" >

<EditText
android:id="@+id/chat_login_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:layout_weight="1"
android:background="#00000000"
android:drawableLeft="@drawable/login_password"
android:drawablePadding="5dp"
android:ems="10"
android:hint="密码"
android:inputType="textPassword"
android:textColor="#fff"
android:textSize="12sp" />

<CheckBox
android:id="@+id/chat_login_password_checkbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginRight="5dp"
android:button="@drawable/password_checkbox" />
</LinearLayout>
</LinearLayout>

<Button
android:id="@+id/chat_login_signin_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:background="#359D90"
android:paddingBottom="10dp"
android:paddingTop="10dp"
android:text="登录"
android:textColor="#fff" />

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:orientation="horizontal" >

<TextView
android:id="@+id/chat_login_signup0"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
android:textColor="#5D5D5D"
android:textSize="12sp" />

<TextView
android:id="@+id/chat_login_signup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toRightOf="@id/chat_login_signup0"
android:text="注册用户"
android:textColor="#6F6F6F"
android:textSize="12sp"
android:textStyle="bold" />

<TextView
android:id="@+id/chat_login_forget_password"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:text="忘记密码"
android:textColor="#5D5D5D"
android:textSize="12sp" />
</RelativeLayout>
</LinearLayout>

</LinearLayout>
好友列表页 
activity_chat_friends.xml​
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical" >

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#359D90"
android:orientation="horizontal"
android:paddingBottom="5dp"
android:paddingTop="5dp" >

<TextView
android:id="@+id/current_user"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="我的好友"
android:textColor="#fff" />

<Button
android:id="@+id/chat_logout_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_marginRight="5dp"
android:background="@drawable/chat_logout_icon" />
</RelativeLayout>

<TextView
android:id="@+id/friend_list_go"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="10dp"
android:textStyle="bold|italic"
android:textColor="#000fff"
android:text="好友列表" />

<View
android:layout_width="match_parent"
android:layout_height="1px"
android:layout_marginTop="5dp"
android:background="#DDDDDD" />

<ListView
android:id="@+id/chat_listview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:scrollbars="none" />

<Button
android:id="@+id/chat_add_btn"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:background="@drawable/send_btn_bg"
android:paddingBottom="12dp"
android:paddingLeft="10
本文由环信热心用户SeanMis小七发表,个人博客地址SeanMis小七 收起阅读 »

【演讲视频】环信:全媒体智能客服时代的最佳实践

  由CTI论坛主办的2016中国呼叫中心及企业通信大会于4月14至15日在北京辽宁大厦隆重举行,本次会议的主题是:联络中心的数字化DNA。环信创始人刘俊彦出席此次会议并发表主题演讲《全媒体智能客服时代的最佳实践》,以下为演讲视频。 视频地址http://ww...
继续阅读 »
  由CTI论坛主办的2016中国呼叫中心及企业通信大会于4月14至15日在北京辽宁大厦隆重举行,本次会议的主题是:联络中心的数字化DNA。环信创始人刘俊彦出席此次会议并发表主题演讲《全媒体智能客服时代的最佳实践》,以下为演讲视频。
视频地址http://www.ctiforum.com/news/shiping/482405.html 收起阅读 »

Parse 移植:如何将 Parse 服务器迁移部署到 Heroku 或 AWS 上

原文链接:http://www.appcoda.com/parse-server-installation/ 作者:Gregg Mojica at AppCoda 原文日期:2016-04-16 译者:Crystal Sun   继续我之前的这篇文章...
继续阅读 »
原文链接:http://www.appcoda.com/parse-server-installation/
作者:Gregg Mojica at AppCoda
原文日期:2016-04-16
译者:Crystal Sun
 
继续我之前的这篇文章 migrating a parse database to a self-host MongoDB instance,在这次的春季辅导教程中,我们看一下如何将 parse 服务器迁移到 Heroku 和 Amazon Web Service。

对于还不了解 Parse 之死的人来说,这意味着服务器(处理数据,与数据库互动,发送接收请求等待)需要迁移到其他地方了。Parse,后端即服务(BaaS),为开发者提供服务器和数据库的服务。然而,随着 Parse 即将在一月份关闭,官方建议,在2017年1月28日彻底停止服务之前,请迁移 Parse 应用。Parse 官方建议你先迁移数据库,然后在迁移服务器。本节教程会假定你一看完成了数据库的迁移,正如我们在上篇教程第一部分中所做的。

幸运的是,parse-server(GitHub 项目,由 Facebook 开源,伟大的 Parse 统治者)可以部署在大部分的云服务上。在本节教程里,我们会讲述如何将 parse-serve 部署到 Heroku,Salesforce 旗下知名的云服务供应商。在本篇文章的最后部分,我们会演示如何部署到 Amazon Web Services(AWS)上,世界上很多知名的 App 都在使用 AWS 的服务。

准备开始

首先到 Heroku.com 网站注册一个帐号。为了演示 demo,我选择了免费方案。你根据自己的需要,选择合适的方案,比如付费方案。你可以在这里看到所有的付费方案。

部署到 heroku 有两种方法可供选择。第一种是点击 Deploy to Heroku 按钮,然后出现一步接一步的提示流程,因为 Parse 已经在 Heroku 的服务器上设置过 parse-server 了,对非 Javascript 程序员来说,这可能是最简单的方法了。如果你熟悉 git 和命令行,请随意使用克隆应用然后用命令行完成。话虽如此,但是你不能一辈子都避免使用命令行。不管你选择那种方式,都会涉及到命令行的。

Option 1: 使用 Heroku 按钮


68747470733a2f2f7777772e6865726f6b7563646e2e636f6d2f6465706c6f792f627574746f6e2e706e67.png


点击上面的按钮,创建一个新的 heroku 应用,你会看到类似下方图片的界面:


178769-23db0d3bc3bb96b9.png



设置向导出现,让你输入应用名称(全部小写不允许有空格)。

接下来,选择 runtime 选项。如果你住在美国,选择 United States(美国),其他地方,选择 Europe(欧洲)。runtime 选项,就是你希望你的应用部署在哪个地方。考虑到性能和速度,最好将应用服务器部署在离你较近的地方。



178769-926ab59bbb354d32.png


接下来更新配置,填写 Parse 账户里对应的密钥(或者生成新的密钥,如果你不是迁移现存应用的话,这点以后再说)。安装路径为 /parse。

当你填完所有的字段后,点击 deploy 按钮,暂时先空着 MongoLab(也就做 mLab)开发。


178769-68ef235cdeb48627.png


可能需要你输入你的信用卡。

Option 2: 克隆 Heroku 应用

parse-server 是开源项目,目前可以在 GitHub 上下载。如果你选择的是命令行,而不是点击 heroku 按钮,那么继续下方的操作。开始前,先打开终端(Terminal),使用下方的命令来克隆应用:
 
cd ~
cd Desktop

git clone https://github.com/ParsePlatform/parse-server-example.git
git add .
git init
git commit -m "Initial Commit"


178769-5121361f7445b4cc.png


现在,你已经成功地将 parse-server 克隆到桌面上了。

修改数据库的 URI

不管你在上面选择了哪个方式,现在你的应用在一定程度上已经设置过了。如果你使用是 Option 1,你需要在你电脑里复制一份本地代码副本,首先用下列命令行(也会将 App 克隆到电脑桌面)。
注意:下方的选项适用于选择了 Option 1 的人

 
$ heroku login 

$ cd ~/Desktop
$ heroku git:clone -a your-app-name
$ cd your-app-name

$ git add .
$ git commit -am "make it better"
$ git push heroku master

登录后,需要输入认证(之后会详细说明,不过现在只需要输入 Heroku 帐号的邮箱和密码,密码不会出现在屏幕上)。

现在,打开你最喜欢的文本编辑器(我比较喜欢 Sublime Text),打开新克隆的库(repository)(对于新手来说,你可以直接将整个文件夹拖到 sublime text 图标上,然后 sulime text 会自动文件,或者使用顶部菜单的 File -> Open)。



178769-4209280fb02995e1.png


现在,我们需要打开 index.js 文件,修改 API 变量。注意第 14-23 行。

178769-64760bbb74648fba.png


从第 14 行开始,我们需要修改 databaseURL 参数。替换路径,使用在本教程第一部分生成的路径,例如,我会使用下面的 url,不过你必须用你自己的 url 来替换。
mongodb://admin:mypassword@ds017678.mlab.com:17678/appcoda-test

接下来,我们需要填写 appId 和 masterKey 参数。如果你是在迁移一个已经存在的应用,到 parse.com 上找到对应的数据。如果这是你第一次使用 parse-server 创建一个新工程,你可以生成随机的字幕数字组成的密钥。

在 parse.com 网站上登录你的 Parse 帐号,找到 Settings(设置),在这里,选择 Security & Keys。复制粘贴你的 Application ID(这个应用的,不要复制成其他应用的)和 Master Key。下面的图片可供你参考(我的密钥出于安全考虑遮挡住了)。



178769-f2f761775be12eb5.png

注意:如果你选的是 Option 1,你已经设置了你的密钥,你可以直接跳过这一步。即使如此,我还是建议你看一下,这样你能对 parse-server 的工作机制有更深入的理解。
在 index.js 文件里替换上你刚刚复制来的新密钥,你也可以添加 clientKey 作为一个参数,从 Parse 中获取。

最后,记住保存你的操作,快捷键 Command+S(Mac电脑上)。

如果你不是迁移应用,那么使用随机生成器(例如 random.org 或其他类似的东西)来生成字母数字密钥。

接下来,部署 Heroku。

将 Parse 服务器部署到 Heroku

首先在电脑上安装 Heroku 工具条,从链接中可以找到官方安装指南。安装完成后,在终端(Terminal)中输入下列命令行:
heroku login
接下来输入登录 Heroku 信息,注意当你输入密码的时候,密码不会出现在屏幕上。

如果你选择的是 Option 1,就没有必要用下面的命令行创建一个 Heroku 应用了。如果你选择的是 Option 2,确保输入下列命令行来创建一个 Heroku 应用。
heroku create
Heroku 会给你创建一个应用,现在提交修改内容,代码如下:
git add .
git init
git commit -m "Updated api config"
git push heroku master
现在,你已经成功部署了 Heroku!如果你遇到任何错误,请在下方的评论栏中留意,我将尽力帮助你。
 
设置 Heroku 的环境变量

接下来,我们需要设置 Heroku 的环境变量,回到终端(Terminal),输入下列命令行(使用你的 MongoDB 实例中的URI,我们之前谈论过)。
heroku config:set DATABASE_URI=mongodb://admin:mypassword@ds017678.mlab.com:17678/appcoda-test
回到 Heroku 网页上,点击你的应用,在 Settings tab 页下,点击 reveal config variables。
 


178769-00bc8f6ebe084ef5.png


现在你应该可以看到 Heroku 的 config Variables 里有了 database URI。

恭喜你!你的 parse-server 已经成功地部署到了 Heroku。唯一的问题是:还没有连接到你的 iOS 应用上

定位 Parse 服务器的 URL

为了能够将你的应用连接到新的 parse-server,首先要从 Heroku 应用设置里定位托管地址(hosting url)。



178769-975a0fa5659dca4e.png


回到 index.js ,找到第 27 行,注意找 moutPath 变量是 /parse。

178769-1601201470ce9628.png


这个变量表示 parse 在 Heroku 服务器上的地址。目前来说,地址是 /parse。所以,可以在 yourapp.herokuapp.com/parse(改成你自己的域名) 中访问 parse-server。

设置 iOS 应用

现在,我们已经正确地配置和部署了服务器,是时候来设置 iOS 应用设置选项了,让 iOS 应用连接到新的 parse 服务器上。

在 Xcode 里,打开应用,选择 appdelegate.swift 文件,删除你以前的 app key 和 client key(然后写上你自己的密钥和服务器的 url)。

把下面这段代码删掉:
Parse.setApplicationId(“xxxxxxxxxxxxxxxxxxxxxxxx”, clientKey: “xxxxxxxxxxxxxxxxxxxxxxxx”)
替换成:
let config = ParseClientConfiguration(block: {
(ParseMutableClientConfiguration) -> Void in
ParseMutableClientConfiguration.applicationId = "xxxxxxxxxxxxxxxxxxxxxxxx";
ParseMutableClientConfiguration.clientKey = "xxxxxxxxxxxxxxxxxxxxxxxx";
ParseMutableClientConfiguration.server = "xxxxxxxxxxxxxxxxxxxxxxxx.com/parse";
});

Parse.initializeWithConfiguration(config);
完成操作后,点击 Run 按钮,测试一下应用。正常情况下应用会和迁移以前一样运行。如果你使用的YY待命,你可能需要修改一下代码,来适应新的 parse 服务器环境。我们会在下一个教程中涉及这个话题。另外,在下一个教程里,我们还会介绍在服务器里托管 Parse 的 dashboard。不过现在而言,你可以继续使用 parse.com 的 dashboard,知道官方彻底关闭服务,也就是在 2017 年的一月。

恭喜你!你已经成功地在 Heroku 上部署了 parse-server。

将 Parse 服务器部署到 AWS
注意:如果你已经将 parse-server 部署到了 Heroku 上,那么就不需要再部署到 AWS 上了,毕竟你的服务器只能使用一个云服务。这部分主要是用来参考的。如果你不想使用 Heroku,想使用 AWS,你可以继续阅读下面的章节。然而,我会假设你已经阅读过上面 Heroku 部分的教程内容,如果出现同样的设置内容,我不会再次赘述了。
Amazon Web Services(AWS)是全球知名的云服务提供商,为科技界许多知名的大型公司提供云服务。实际上,很大大型科技公司都在使用 AWS 的服务,例如苹果公司的 iCloud,Hulu,AirBnb,Lyft,Adobe,Slack (这些都是国外知名的科技公司)等等,这些只是使用 AWS 云存储服务的众多公司中一小部分。

那么,为什么我先介绍 Heroku 呢?不同于 AWS 的是,Heroku 更容易设置。对于大部分的设置,你可以直接进行无需输入账单信息。AWS 则不一样,设置方法比较复杂。为了演示如何部署到 AWS 上,我们将使用另外一个部署按钮和设置向导,来让所有的工作简单流畅。

再次强调一下,如果你已经将应用部署到了 Heroku,而且对 Heroku 的服务比较满意,你可以直接跳过这部分了。然而,如果你对如何部署到 AWS 上感兴趣,那么让我们开始吧!

第一件事,到 AWS 上注册一个 AWS 帐号,需要提供你的付款信息,这样才能使用免费方案。

完成后,点击下方的按钮,创建一个新的 AWS 应用,AWS 提供一组云服务工具,每个工具都有自己的独特的功能,在本节教程中,我们使用 Elastic Beanstalk(和 Elastic Cloud Compute Engine 或简称 EC2 紧密相关)。



687474703a2f2f64302e6177737374617469632e636f6d2f70726f647563742d6d61726b6574696e672f456c61737469632532304265616e7374616c6b2f6465706c6f792d746f2d6177732e706e67.png

什么是 Elastic Beanstalk ?
根据 Amazon 上的简介,Elastic Beanstalk 是一个易于使用的,用于部署和扩展网页应用和服务,适用的语言有 Java、.NET、PHP、Node.js、Python、Ruby、Go、Docker,例如 Apache, Nginx, Passenger,和 IIS。

好炫的语言是吧?或许吧,总而言之,我们将使用这个服务来设置和运行我们的 parse 服务器。如果你想了解更多有关 Elastic Beanstalk 的信息,请参考官方网页。
点击按钮后,会出现一个增加应用名称的界面,如下图。

178769-8682e3249bc4e858.png


下一步,确保你的设置如下图,然后继续。

178769-d9e2f39ce95712d1.png


在接下来的界面里使用正确的密钥上传 parse 设置,parse 装在 /parse 下。

178769-b450f4cfe8a4a2e5.png


恭喜你!你成功将 parse 服务器部署到了 AWS 上!剩下需要做的事情就是用适当的密钥和新的服务器 url 来设置 iOS 应用(后缀 /parse)。
结束

在本节教程中,我们深入了解了部署 parse 服务器的过程,估计现在你对部署过程已经掌握的比较牢固了。

然而,我们还留下了一些小细节没有处理(感觉这句话翻译的不对)。如果你使用的是云代码,你不得不修改代码,来保证运行正常。另外,你可能还想要一个 Parse dashboard 的替代品。幸运的是,Parse 团队已经将 dashboard 开源了,并提供了分步指南,供你更新云代码。在之后即将到来的教程中,我们会详细讨论这些内容。不过现在,你还是应用集中将应用部署到 AWS 或 Heroku 上!

你觉得本教程怎么样?请尽情地留下评论,分享你的想法。
本文由 SwiftGG 翻译组翻译,已经获得作者翻译授权。

  收起阅读 »

Android V2.2.8 ,ios V2.2.4release,视频通话时支持根据当前网速自动调整码率

  更新简介: 1、视频通话时支持根据当前网速自动调整码率。(android&ios) 之前的版本中视频的码率是固定的,该版本增加了自动调整的API接口,可以设置成根据当时的网络状况自动调整码率,保证视频的流畅性。 2、修复消息扩展字...
继续阅读 »
 


24958PICwjQ_1024.jpg


更新简介:


1、视频通话时支持根据当前网速自动调整码率。(android&ios)

之前的版本中视频的码率是固定的,该版本增加了自动调整的API接口,可以设置成根据当时的网络状况自动调整码率,保证视频的流畅性。

2、修复消息扩展字段里包含特殊字符,接收方kill程序再进的时候消息不显示的bug(android)

3、修复一个登录相关的问题(android&ios)

4、修复一些ui相关的问题,如发送视频消息时,视频时长显示不对(android)

5、修复收到异常消息crash问题。(ios)

rest发的消息中value中的value为null的话之前会crash,现在是忽略掉这这种消息。


 
版本历史:ios更新日志 Android更新日志
下载地址:SDK下载
 
SDK接入过程中有任何问题建议请直接回复跟帖。 收起阅读 »

4.5亿用户,环信助力快牙打造跨平台传输神器

快牙全球用户已超过4.5亿,遍及全球200多个国家和地区,被用户称为“怪兽级传输神器”,快牙的创建连接群和好友即时聊天功能都是集成环信的服务。快牙CEO王晓东表示:“在互联网的发展下,还需要把快牙的社交性发挥出来,我们不是做一个社交产品,而是用社交互动来助推快...
继续阅读 »
快牙全球用户已超过4.5亿,遍及全球200多个国家和地区,被用户称为“怪兽级传输神器”,快牙的创建连接群和好友即时聊天功能都是集成环信的服务。快牙CEO王晓东表示:“在互联网的发展下,还需要把快牙的社交性发挥出来,我们不是做一个社交产品,而是用社交互动来助推快牙的传播,使得我们的核心数据大涨。”截止2015年,快牙月活跃用户8000万,日传输超1.2亿次,以绝对优势位居内容传输APP之首,这其中通过集成环信即时通讯云服务提高APP社交活跃度功不可没。



28期_行业新闻.jpg




快牙简介
 
快牙是众多快速发展、跨平台的无线文件传输工具之一。它传输文件的速度远超蓝牙、NFC和Airdrop,让用户无需使用移动网络流量时即可分享apps、照片、音乐、视频或者其他任意一种文件。除了作为WI-FI文件传输工具外,快牙还提供可以本地安装的网络游戏供用户娱乐。
 
快牙的工作原理 
 
快牙可以让任何内容(包括apps)从一个手机直接复制到另一个手机。但是,如果你朋友还未安装快牙,快牙也提供了简便的方式让你朋友直接从你的手机上把其复制过去,所以你能在任何情况下与你的朋友们分享任何文件。用一句话来说,如果你的朋友向你推荐了一款app而你并不想搜索整个app商店去下载,你只要直接使用快牙就能马上安装这个app。 
 
快牙背后的故事
 
快牙起家于2011年,刚开始是为各个vendors在安卓API中提供无线热点功能的定制化开发。由于它不需要占用移动流量也不需要WI-FI网络连接就能进行大文件(包括图片和电影)分享,快牙迅速在学生及其他负担不起4G网络和大流量套餐的人群中流行开来。同时,其也在运营商信号不稳定的地方极其流行,比如说:中国、东南亚、中东和印度。它在发展中国家增长的速度最快,在想要花尽量少的钱又可以传输文件的劳动人群和学生人群里面最为流行。
 
快牙的进化和挑战:
 
使用环信之前 在刚开始,快牙就在寻找可以增长用户和让他们的用户更有参与感的方式。但他们发现一但学生或者工人身份的用户离开了他们群组的物理地点,对快牙的使用量就马上降低了。此外,文件分享也不是一个每日都会被使用到的工具,这也是快牙用户增长面临的难题之一。但是,快牙通过观察开始注意到这个app其实有着社交的因子,并且发现如果给他们的app加一层社交网络的元素会让app的使用体验更丰富。这也是他们首次开始思考给app增加即时通讯功能来服务他们的用户。同时,快牙还想在未来打造以兴趣为主题的用户群组。
 
快牙还发现在2014年底,他们现有的2亿用户已经给他们的后端系统带来了压力。例如在周末时间段,大量用户分享图片时,数据有达到峰值的趋势。所以,他们开始寻找更好的即时通讯解决办法来助力于他们的高速增长和足以应付突发的流量峰值。很多游戏公司告诉他们,应该要自主开发一套即时通讯功能供app使用,但他们不希望自主开发即时通讯系统使开发团队的重心从他们的核心功能上转移。在他们开始探索可能的解决方案时,最终发现如果执意要自主开发一个能够支持大流量、可扩展、高性价比的即时通讯系统将会是一个十分艰难的挑战。 
 
在估算成本投入时,快牙发现如果他们的用户增长与流量继续按现在这种爆发式增长时,成本的投入将不可负担。在测试使用自主开发的即时通讯系统时发现,如果想要在每个服务器上满足接近一百万用户的连接量时,需要花费几乎不可承担的极高成本。
 
 如果开发一个能满足其流量需求的系统,除了非常昂贵外,还将面临极大的系统风险。因为如果其中一个服务器瘫痪了,将由于资源不足无法导流而使服务中断。如果要消除这种隐患,必须使用更多的服务器,以降低每台服务器的工作负载。然而,这将使系统成本和复杂性急剧增加。 
 
因此,快牙需要一个能处理峰值流量,具备扩展性能,同时拥有低成本及容错性的第三方即时通讯解决方案。
 
快牙的选择和收获:使用环信后 
在对比自主研发和其他厂商的解决方案后,快牙选择了环信。环信在技术和成本上提供了最佳解决方案,同时还拥有一支行动快速、反馈及时的技术团队,能让快牙在最短时间内运行在高容错性、可扩展、低成本的企业级系统上。 
 
如果当时没有环信,快牙将浪费很多时间、金钱,并且将使产品研发重心转移。此外,环信的IM长连接技术使快牙能够通过发送一些专业市场营销内容,大幅提升日活用户数。
 
把复杂的大规模即时通讯移动端优化问题交由环信专家解决后,快牙就能专注于产品和app的快速迭代。快牙现在每日能处理1.2亿并发的消息并且还在持续增长。与优秀的即时通讯厂商合作,使他们对于快速扩张没有任何顾虑。


快牙的挑战 
减轻后端的数据存储负担 
创建客户通讯能力
快速提升并发连接能力 
控制成本的增长 
系统具备容错能力 
产品研发仍聚焦快牙核心竞争力
快速部署 
解决方案: 
环信提供的即时通讯云服务 
具备1000台服务器规模的IM 
系统部署能力 
IM长连接让快牙能与用户主动沟通 
扩展性—按需提供,弹性扩容 
数据在服务器之间平均分配,更具容错性 
即时通讯和后端扩展交由环信,快牙专注于自身产品 
三个月即可就绪的企业级解决方案 


 结果: 
 
快牙成为中国排名第一的工具类App--提供本地文件分享与游戏功能(BT和WIFI)。截止2015年,快牙月活跃用户8000万,日传输超1.2亿次。期间共计54个迭代版本,新增改进功能累计270余个。近期随着快牙4.0的发布,将开启一场自我颠覆的革命。新快牙,是分享,是朋友们发现和获取私密内容及猛料的服务,相信环信即时通讯云服务也将发挥更大的价值!
  收起阅读 »

内容为王 一个好的摘客

“这是一篇不完全的摘客产品报告。” 如何发现互联网的内容时代来了? “我是papi酱,一个集美貌与才华于一身的女子。” 没有看过papi酱的视频,那你也一定听过papi酱的段子,这就是新一代的网红papi酱...
继续阅读 »
“这是一篇不完全的摘客产品报告。”




如何发现互联网的内容时代来了?

“我是papi酱,一个集美貌与才华于一身的女子。”



图片1.png




没有看过papi酱的视频,那你也一定听过papi酱的段子,这就是新一代的网红papi酱,从秒拍开始,逐渐将短视频带领进新时代的网红。


360的总裁齐向东主导的新闻团队即将从360公司脱离出来,成立一个新的公司,专注打造新媒体产品“北京时间”;新一代网红Papi酱拿到了1200万人民币投资,罗辑思维公布了其与Papi酱的具体合作,即拍卖Papi酱视频贴片广告一次,并由罗辑思维全程策划监制服务,掀起内容运营的蓝海;爱奇艺《奇葩说2》首播破4000万,更是引领纯网内容进入黄金时代。

互联网正在越来越成为一个内容创业的平台。做好内容,产品才能有市场。




产品定位

正如应用商店所写的介绍那样,摘客是“个性化资讯推荐”软件。摘客的主要功能是阅读,却又不只是一个简单的阅读平台,应该说摘客是一个包含了推荐、分享、交流等功能的更加懂你的推荐阅读平台。



需求分析

摘客作为一款深度个性化定制的资讯阅读应用,整合了各大门户网站、博客、论坛的资讯,以杂志式的排版方式进行内容再现。做好内容,满足用户阅读的需求,这是摘客产品的初衷。

用户群体是广大对IT资讯感兴趣的读者。他们需要能够找到更多自己感兴趣的文章,获得有趣的阅读体验或者是学习专业知识。



用户真的需要个性化推荐?

网易云音乐的个性化推荐在推荐算法领域风生水起。推荐算法并不能保证推出的东西一定得到该用户的喜爱,但至少能保证推荐出的一些东西一定有用户喜爱的。这其实是一个快速筛选的过程,在互联网阅读碎片化且快节奏的环境下是非常凑效的。

大多数的资讯类产品都有“订阅”的功能,而个性化推荐其实正是订阅的一个隐形形式。所以摘客最主打的部分就是个性化推荐,同样作为摘客产品一个亮点的是热点推荐。




图片2.png



摘客APP首页


与竞争产品的优势?

摘客与网易新闻、新浪新闻的关系应该就像店铺与淘宝的关系。摘客将新闻像商品一样上架对其进行统一格式的整理,分类,最后尽量精确的分发给用户。各类知名网站就是我们商品的货源。

与摘客最相似的两个竞争产品应该是今日头条和推酷。大家都是做内容聚合的平台,且都“不生产内容,只做内容的搬运工”。这个竞争的问题留给摘客的挑战是什么样的呢?

答案是:再做一次有选择的聚合。摘客的重点还是归于“摘”,直接从新闻来源控制质量,再经过每一轮的算法筛选,以留下最精准和优质的文章为目标。相对于今日头条,摘客做更细分的领域;相对于推酷,摘客做更广的领域。




图片3.png



今日头条app首页



图片4.png



推酷市场介绍


摘客的价值在于? 
 
既然作为一个产品就要有价值的体现,而资讯的价值就是摘客的价值。资讯的价值在于传播和学习,摘客作为一个聚合阅读的平台希望给用户正真“干”的东西。未来的摘客有很多可能,可能会侧重发掘展示UGC(用户原创)的内容,或者会借势网红经济的风口再成长?都是摘客作为内容型产品可能在思考的。 
 
更长远来说,资讯本身就是一种商品,所以内容收费未来可能成为趋势,借鉴知乎最新的产品“值乎”。摘客的未来可能走内容收费模式,做高端定制内容需求,其商业模式的想象空间仍然很大。 最后说说这个团队 最后来说说这个产品的制作团队,前端后台一共10个人不到,都是研究型的人才。最开始做的是大数据分析系统,之后也做一些舆情分析,最后发现已经储藏了很多,于是摘客也就诞生了。 
 
因为摘客中的好多技术其实在做旧的项目时都已经有雏形,有些甚至直接是研发人员学位研究课题,正好借摘客的平台付诸实践和尝试,所以摘客也是新思维碰撞的产物。 需要说明的是,产品的更新迭代一直没有停止,摘客也是有很大改进空间的,作为一个用户,最希望的就是web2.0,也就是用户原创内容,在摘客有展示平台就更好了。 
 
最后再说一下,摘客的制作团队是网新恒天的HTA项目组,有兴趣可以来参观交流,可借鉴内容很多~

  收起阅读 »

【公告】环信直播课堂内容、界面全面升级--本期暂停直播

环信直播课堂创建于2016-02-19,迄今为止,已经陪伴了我们走过九期,历史收看人数6191。 非常感谢为我们直播讲解的每一位老师,@beyond @zhuhy @一鸣 @FUccc @shenc @东海 同事也感谢小伙伴们一直以来的陪伴!   ...
继续阅读 »


47z58PICmkp_1024.jpg


环信直播课堂创建于2016-02-19,迄今为止,已经陪伴了我们走过九期,历史收看人数6191。

非常感谢为我们直播讲解的每一位老师,@beyond @zhuhy @一鸣 @FUccc @shenc @东海
同事也感谢小伙伴们一直以来的陪伴!
 
在历经9期的直播里,很高兴服务了那么多小伙伴,同时,也看到了很多我们的不足。

从4月份起,就一直在筹备环信直播课堂的升级,我们新版本将集投票、点播、订阅、聊天于一体,打造最人性化的环信直播课堂(环信小伙伴也可以申请当主播哦!)
 
暂停一期环信直播课堂,下次上线时间待通知。
 
您可以把对环信直播课堂的建议和想听的内容直接跟帖回复。
 
THX!
  收起阅读 »