# 功能配置

# 自定义客服分配

在打开客服咨询窗口或者构造 ServiceMessageFragment 时,接口中有一个参数是 ConsultSource,通过该参数,我们可以对分配客服的流程做一些自定义操作。

# 分配客服&客服组

  1. 在 2.0 之前的版本中,请求客服时会根据企业在管理后台上对 App 客服分配的设置,来决定首先接入机器人还是人工客服,如果管理后台开启了机器人,则首先会接入机器人。如果管理后台也允许接入人工客服,那么用户可以通过回复 'RG' 或者 '人工客服' 一类的关键字切换到人工客服,界面上也能展现一个人工客服的入口,用户点击此入口,也能接入人工客服。

  2. 在 2.0 版本中,七鱼加入了访客分配的逻辑。如果 App 端开启了访客分配,那么当切换到人工客服时,会首先给出一个客服分组选择的入口,用户可以自主选择某个客服或者客服分组咨询。

  3. 在 2.2 版本中,SDK 在 ConsultSource 增加了指定客服或者客服组的参数,开发者可以在访客进入咨询界面前就指定好要为其服务的客服,例如从订单页面打开咨询界面时为其指定售后客服,在商品页面打开时,为其指定售前客服。在 ConsultSource 中,staffId 为客服 ID,groupId 为客服组 ID,其值可以在管理后台设置页面的「访客分配」页面中找到。如果这两个参数都没有设置,则按照2.0版本的逻辑分配客服,如果指定了其中一个,则只会分配指定的客服或者在指定的客服分组中分配,如果指定的客服不在线,或者指定的客服分组中没有客服在线,则会提示用户客服不在线。如果同时指定 staffIdgroupId,以 staffId 为准,忽略 groupId

  4. 机器人优先

在 3.1 版本中,SDK 又在 ConsultSource 中增加了 robotFirst 参数, 如果 robotFirst 为 true,则会先由机器人接待。

  1. 配置常见问题模板

在 3.1 版本中,为了提高机器人的效率,在 ConsultSource 中还增加了 faqGroupId 参数。 faq 由客服人员在管理后台配置。如果指定了此参数,且请求客服为机器人客服,则会下发该 ID 对应的热门问题列表。

  1. 配置分流客服组 ID

在 V5.13.0 版本中,SDK 增加了配置分流客服组 ID 的功能,在 ConsultSource 配置 groupTmpId 参数即可。开发者可以通过配置该字段指定客服分流不同的模版。具体模版 ID 的值可以通过网页端客服工作台管理端 -> 在线系统 -> 设置 -> 会话流程 -> 会话分配 -> App 分配内容区域查询

  1. 配置机器人欢迎语

开发者可以通过配置 ConsultSource 的 robotWelcomeMsgId 字段来完成机器人不同欢迎语的配置。

# 分配机器人

在 3.12 版本中,为了满足企业不同业务,在 ConsultSource 中增加了 robotId 参数,用于指定特定的机器人客服。机器人 ID 可以在管理后台 - 设置 - App接入 中查看。

# 商品卡片

在打开咨询窗口时,还可以带上用户当前正在浏览的商品或订单信息。在 ConsultSource 中,设置字段为 productDetail,其类型为 ProductDetail。各字段通过 ProductDetail.Builder 设置,可以设置的信息有:

字段 意义 备注
title 商品标题 长度限制为 100 字符,超过自动截断。
desc 商品详细描述信息。 长度限制为 300 字符,超过自动截断。
note 商品备注信息(价格,套餐等) 长度限制为 100 字符,超过自动截断。
picture 缩略图图片的 url。 该 url 需要没有跨域访问限制,否则在客服端会无法显示。
长度限制为 1000 字符, 超长不会自动截断,但会发送失败。
url 商品信息详情页 url。 长度限制为 1000 字符,超长不会自动截断,但会发送失败。
show 是否在访客端显示商品消息。 默认为0,即客服能看到此消息,但访客看不到,也不知道该消息已发送给客服。
alwaysSend 在会话开始后,仍可以发送该商品字段 默认为 false,不发送。
tags 展示在客服端的一些可操作入口 默认为空,每个 tag 在客服端将展示为一个按钮.
openTemplate 是否开启商品的最新展示样式 默认不开启,当开启的时候,发送的商品只展示配置的图片
sendByUser 是否需要用户手动发送 默认为false,当为 true 的时候,商品的下面讲出现一个按钮,用户可以点击该按钮发送商品
actionText 手动发送按钮的文本 默认文本为发送链接
actionTextColor 手动发送按钮文本的颜色 十六进制颜色例如:0xFFDB7093
isOpenReselect 是否显示重新选择按钮 默认为 false,不显示。
reselectText 重新选择按钮的文案 如果不设置,默认为"重新选择"
handlerTag 当点击重新选择按钮的时候,会将此字段回传 默认为 null
productReslectOnclickListener 商品重新选择的点击事件的回调,类似于我们普通的 OnClickListener 无默认实现
cardType 卡片类型,共三种类型。商品: 0,订单: 1,自定义: 2 默认为0,推荐必填
goodsCId 商品类目ID 长度限制为100字符,填写后有助于机器人识别商品和后续业务
goodsCName 商品类目名称 长度限制为10字,如生鲜食品
goodsId 商品ID 长度限制为100字符,商品唯一标识符,填写后有助于机器人识别商品和后续业务,当卡片类型为商品时,推荐必填
orderId 订单ID 交易订单号(父订单的交易编号),当卡片类型为订单时,推荐必填
intent 商品卡片意图 无默认实现

在3.1.0版本之前,商品信息只有在连上客服时发送一次,此后如果没有重新连接客服,无论商品信息是否改变,都不会再继续发送了。从3.1.0版本开始,开发者可以通过 alwaysSend 字段控制是否需要在中途发送新的商品信息。注意,如果上一次发送的商品链接和这一次的相同,后面一次的不会再发送给客服。在 V4.4.0 版本中,可以通过设置 ConsultSource 中的 isSendProductonRobot 去控制是否在进入机器人会话的时候发送商品

App 可以通过调用 MessageService.sendProductMessage(productDetail); 去在任何时刻发送商品

# 设置用户VIP等级

在 3.5 版本中,七鱼客服增加了设置用户 VIP 等级的功能,便于区分用户等级,允许 VIP 用户优先进线或为 VIP 用户指定专线客服,提升用户体验。

使用该功能需要在七鱼管理系统 -> 设置 -> VIP 客户设置页面打开 VIP 开关,否则设置将不会生效。

设置用户 VIP 等级需要为 ConsultSource 中的 vipLevel 字段赋值:

  • 如果为 0,为普通用户;
  • 如果为 1-10,为用户 VIP 等级 1-10 级;
  • 如果为 11,为通用 VIP 用户,即不显示用户等级。

# 控制会话过程

SDK 增加了开发者对于会话过程控制的力度,允许用户主动结束会话和主动退出排队,允许用户退出聊天界面时弹出结束会话弹框和弹出满意度评价。

设置是否允许用户主动结束会话,设置该选项后,在会话状态下,右上角会有结束会话的入口。

SessionLifeCycleOptions setCanCloseSession(boolean canCloseSession);

设置是否允许用户主动退出排队,设置该选项后,在排队状态下,右上角会有退出排队的入口。如果用户通过按返回键退出,会给出弹框提示。

SessionLifeCycleOptions setCanQuitQueue(boolean canQuitQueue);

设置排队状态下,按返回键时给用户的提示语,该选项只有在setCanQuitQueue(boolean) 设置为 true 时才有效。

SessionLifeCycleOptions setQuitQueuePrompt(String quitQueuePrompt);

设置是否允许用户退出聊天界面时弹出结束会话弹框

SessionLifeCycleOptions setCanBackPrompt(boolean canBackPrompt);

用户返回时弹出结束会话弹框时设置是否弹出满意度评价,该选项只有在setCanBackPrompt(boolean) 设置为 true 时才有效。

SessionLifeCycleOptions setCanShowEvaluation(boolean canShowEvaluation);

控制会话过程的设置类是 SessionLifeCycleOptions,可以在开始会话前设置给 ConsultSourcesessionLifeCycleOptions 字段。示例代码如下:

ConsultSource source = new ConsultSource(null, "自定义入口", null);
SessionLifeCycleOptions lifeCycleOptions = new SessionLifeCycleOptions();
lifeCycleOptions.setCanCloseSession(boolean)
        .setCanQuitQueue(boolean)
        .setQuitQueuePrompt(String)
        .setCanBackPrompt(boolean)
        .setCanShowEvaluation(boolean);
source.sessionLifeCycleOptions = lifeCycleOptions;
Unicorn.openServiceActivity(context, title, source);

如果使用 fragment 集成,则还需要对SessionLifeCycleOptionssessionLifeCycleListener字段赋值,配置会话界面退出监听器,说明如下:

/**
 * 会话界面退出监听器<br>
 * 仅在使用 fragment 集成时需要处理,如果使用 Activity 集成则无需处理。<br>
 * 如果需要在用户退出排队时给予挽留提示,则需要在宿主 activity 触发 onBackPressed 时调用 ServiceMessageFragment 的 onBackPressed 方法,
 * 只有当 fragment 的 onBackPressed 方法返回为 false 时,才能调用 Activity 的 onBackPressed。示例代码<br>
 * <pre>
 * public class ServiceMessageActivity extends Activity {
 *     public void onBackPressed() {
 *         if (!messageFragment.onBackPressed()) {
 *             super.onBackPressed();
 *         }
 *     }
 * }
 * </pre>
 * 如果给予提示后用户仍然选择退出排队,则会回调{@link #onLeaveSession()},开发者需要在回调中关闭 fragment 。
 */
public interface SessionLifeCycleListener {

    /**
     * 用户主动离开客服咨询界面,此时需要关闭 fragment 。<br>
     */
    void onLeaveSession();
}

# 设置客服信息

在 SDK 的 V4.6.0 的版本中,我们增加了会话中客服信息自定义的功能,App 方可以在进入客服界面之前进行设置客服信息,在进入会话之后,本次会话将展示设置的内容,设置方法为如下代码:

ConsultSource source = new ConsultSource(null, "自定义入口", null);
source.vipStaffid = "自定义的 vip staff id";
source.prompt = "连接客服成功的提示语";
source.VIPStaffAvatarUrl = "头像的 url";
source.vipStaffName = "客服的的名字";
source.vipStaffWelcomeMsg = "客服的欢迎语";
Unicorn.openServiceActivity(this, "网易七鱼测试", source);

# 输入栏快捷入口

在 3.13 版本中,七鱼 SDK 新增人工客服模式下的快捷入口,可用于用户选择并发送订单给客服。

快捷入口位于输入框上方,仅在人工客服和留言状态下显示。如果需要展示快捷入口,需要按照以下方法配置:

  1. ConsultSource 中的 quickEntryList 赋值,添加快捷入口
ConsultSource source = new ConsultSource(uri, title, custom);
...
source.quickEntryList = new ArrayList<>();
source.quickEntryList.add(new QuickEntry(0, "查订单", iconUrl));
source.quickEntryList.add(new QuickEntry(1, "查物流", iconUrl));
  1. YSFOptions 中的 quickEntryListener 赋值,用于监听快捷入口点击
YSFOptions options = new YSFOptions();
...
options.quickEntryListener = new QuickEntryListener() {
    @Override
    public void onClick(Context context, String shopId, QuickEntry quickEntry) {
        ToastUtils.show("点击快捷入口" + quickEntry.getId());
        if (quickEntry.getId() == 0) {
            // 这里可根据 QuickEntry 做出相应的相应,如打开订单选择窗口
        }
    }
};

如果需要在用户选择订单后将商品信息发送给客服,需要调用以下接口

// 普通企业
MessageService.sendProductMessage(productDetail);

// 平台企业
POPManager.sendProductMessage(shopId, ProductDetail);

当使用这种方法发送商品的时候,手动发送相关配置参数不再生效,openTemplate 参数不会生效。在 V4.3.0 版本我们对发送商品接口进行了改造,老版本中 ProductDetail 中的show 参数是不生效的,改造之后 show 参数开始生效

# 设置允许在当前客服界面进入新客服界面

在 V5.12.0 版本中我们在 ConsultSource 中增加了 forbidUseCleanTopStart 字段,该字段的作用为是否使用 CLEAR_TOP 的方式启动客服界面,当用户想要在当前客服界面中进入新的客服界面时,需要把该字段设置为 true

# 设置访客分流样式

在之前版本中,当企业在管理后台配置了多入口访客分流时,访客请求人工客服,返回多入口选择时,会在消息流中生成一条消息,此种方式对于用户的提示可能较弱,因此,在4.0版本中,在 YSFOptions 中增加了一个配置项 categoryDialogStyle,用于配置多入口分流的样式,其定义如下:

/**
 * 选择咨询分类时自动弹出对话框的样式<br>
 * 0 表示不弹出<br>
 * 1 表示从中间弹出<br>
 * 2 表示从底部弹出
 */
public int categoryDialogStyle;

# 聊天窗口顶部区域自定义

在 V4.7.0 版本中,SDK 加入了聊天界面顶部,App 可以通过实现 AdViewProvider 接口来自定义顶部区域的样式,下面看一下示例代码:

YSFOptions options = new YSFOptions();
IMPageViewConfig imPageViewConfig = new IMPageViewConfig();
imPageViewConfig.adViewProvider = new AdViewProvider() {
    @Override
    public View getAdview(Context context) {
    	  //每次进入客服界面都会调用该方法
        final View view = LayoutInflater.from(context).inflate(R.layout.view_ad_parent,null);
        view.setLayoutParams(new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT));
        ((TextView)view.findViewById(R.id.tv_ad_parent_close)).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                view.setVisibility(View.GONE);
            }
        });
        return view;
    }
};
options.imPageViewConfig = imPageViewConfig;

从上面的代码可以看到,如果想使用该功能,首先我们需要定义一个 IMPageViewConfig ,然后实现一个 AdViewProvider 并赋值给 IMPageViewConfig。最后把 IMPageViewConfig 赋值给 YSFOption 就可以了。当 SDK 在进入客服界面的时候会调用 getAdView 这个方法,App 方可以根据不同的场景返回不同的 view ,达到展示不同广告的效果。

# 历史消息收起

在 V4.7.0 版本中,SDK 增加了进入客服界面收起历史消息的功能,App 方可以通过如下代码设置是否加载历史消息:

//设置为 false 为默认不加载历史消息,sdk 默认设置是 true 的
YSFOptions options = new YSFOptions();
options.isDefaultLoadMsg = false;

从上面的代码中可以看出来,设置起来还是比较方便的,只需要把 isDefaultLoadMsg 的值改变一下就可以了。这里有个地方需要说明一下,就是收起历史消息的场景,只有在新一通会话才会收起,同一通会话是不会收起的。

# 图片加载

图片加载基本上每个 App 都会用到,各个 App 也都会有自己的实现或者依赖的第三方库。为了避免 SDK 引入第三方图片库后,与 App 自身依赖的图片库冲突,或者与 App 用了不同的库造成浪费,从 2.0.0 版本开始,七鱼 SDK 需要由 App 实现一个图片加载接口,并传给 YSFOptions。 下面是两个常用的第三方图片加载库的接口实现示例,开发者可以直接使用。

图片加载参数中的 uri 格式,只要是 App 选用的图片框架支持即可。但请注意,必须支持本地文件 uri (file://)和 网络文件 uri (http:// 或者 https://) 这两种格式。

# UniversalImageLoader实现

该实现依赖 universal-image-loader 库,需要在工程的 build.gradle 文件中添加依赖:

compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'

如果使用的 IDE 是 Eclipse,则需要在 libs 中添加 universal-image-loader 的 jar 包。

public class UILImageLoader implements UnicornImageLoader {
    private static final String TAG = "UILImageLoader";

    @Override
    public Bitmap loadImageSync(String uri, int width, int height) {
        DisplayImageOptions options = new DisplayImageOptions.Builder()
                .cacheInMemory(true)
                .cacheOnDisk(false)
                .bitmapConfig(Bitmap.Config.RGB_565)
                .build();

        // check cache
        boolean cached = true;
        ImageDownloader.Scheme scheme = ImageDownloader.Scheme.ofUri(uri);
        if (scheme == ImageDownloader.Scheme.HTTP
                || scheme == ImageDownloader.Scheme.HTTPS
                || scheme == ImageDownloader.Scheme.UNKNOWN) {
            // non local resource
            cached = MemoryCacheUtils.findCachedBitmapsForImageUri(uri, ImageLoader.getInstance().getMemoryCache()).size() > 0
                    || DiskCacheUtils.findInCache(uri, ImageLoader.getInstance().getDiskCache()) != null;
        }

        if (cached) {
            ImageSize imageSize = (width > 0 && height > 0) ? new ImageSize(width, height) : null;
            Bitmap bitmap = ImageLoader.getInstance().loadImageSync(uri, imageSize, options);
            if (bitmap == null) {
                Log.e(TAG, "load cached image failed, uri =" + uri);
            }
            return bitmap;
        }

        return null;
    }

    @Override
    public void loadImage(String uri, int width, int height, final ImageLoaderListener listener) {
        DisplayImageOptions options = new DisplayImageOptions.Builder()
                .cacheInMemory(true)
                .cacheOnDisk(false)
                .bitmapConfig(Bitmap.Config.RGB_565)
                .build();
        ImageSize imageSize = (width > 0 && height > 0) ? new ImageSize(width, height) : null;

        ImageLoader.getInstance().loadImage(uri, imageSize, options, new SimpleImageLoadingListener() {
            @Override
            public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
                super.onLoadingComplete(imageUri, view, loadedImage);
                if (listener != null) {
                    listener.onLoadComplete(loadedImage);
                }
            }

            @Override
            public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
                super.onLoadingFailed(imageUri, view, failReason);
                if (listener != null) {
                    listener.onLoadFailed(failReason.getCause());
                }
            }
        });
    }
}

# fresco实现

fresco 库本身提供了一整套图片缓存和加载的功能,由于 SDK 中部分 ImageView 需要做自定义的绘制,因此只会用到其下载,解码和缓存的逻辑。

该实现依赖 fresco 库,需要在工程的 build.gradle 文件中添加依赖:

compile 'com.facebook.fresco:fresco:0.9.0'

如果使用的 IDE 是 Eclipse,则需要在 libs 中添加 fresco 的 jar 包。

public class FrescoImageLoader implements UnicornImageLoader {
    private Context context;

    public FrescoImageLoader(Context context) {
        this.context = context.getApplicationContext();
    }

    @Override
    public Bitmap loadImageSync(String uri, int width, int height) {
        Bitmap resultBitmap = null;
        ImagePipeline imagePipeline = Fresco.getImagePipeline();
        boolean inMemoryCache = imagePipeline.isInBitmapMemoryCache(Uri.parse(uri));
        if (inMemoryCache) {
            ImageRequestBuilder builder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(uri));
            if (width > 0 && height > 0) {
                builder.setResizeOptions(new ResizeOptions(width, height));
            }
            ImageRequest imageRequest = builder.build();
            DataSource<CloseableReference<CloseableImage>> dataSource =
                    imagePipeline.fetchImageFromBitmapCache(imageRequest, context);
            CloseableReference<CloseableImage> imageReference = dataSource.getResult();
            try {
                if (imageReference != null) {
                    CloseableImage closeableImage = imageReference.get();
                    if (closeableImage != null && closeableImage instanceof CloseableBitmap) {
                        Bitmap underlyingBitmap = ((CloseableBitmap) closeableImage).getUnderlyingBitmap();
                        if (underlyingBitmap != null && !underlyingBitmap.isRecycled()) {
                            resultBitmap = underlyingBitmap.copy(Bitmap.Config.RGB_565, false);
                        }
                    }
                }
            } finally {
                dataSource.close();
                CloseableReference.closeSafely(imageReference);
            }
        }
        return resultBitmap;
    }

    @Override
    public void loadImage(String uri, int width, int height, final ImageLoaderListener listener) {
        ImageRequestBuilder builder = ImageRequestBuilder.newBuilderWithSource(Uri.parse(uri));
        if (width > 0 && height > 0) {
            builder.setResizeOptions(new ResizeOptions(width, height));
        }
        ImageRequest imageRequest = builder.build();

        ImagePipeline imagePipeline = Fresco.getImagePipeline();
        DataSource<CloseableReference<CloseableImage>> dataSource = imagePipeline.fetchDecodedImage(imageRequest, context);

        BaseBitmapDataSubscriber subscriber = new BaseBitmapDataSubscriber() {
            @Override
            public void onNewResultImpl(@Nullable Bitmap bitmap) {
                if (listener != null) {
                    new AsyncTask<Bitmap, Void, Bitmap>() {
                        @Override
                        protected Bitmap doInBackground(Bitmap... params) {
                            Bitmap bitmap = params[0];
                            Bitmap result = null;
                            if (bitmap != null && !bitmap.isRecycled()) {
                                result = bitmap.copy(Bitmap.Config.RGB_565, false);
                            }
                            return result;
                        }

                        @Override
                        protected void onPostExecute(Bitmap bitmap) {
                            if (bitmap != null) {
                                listener.onLoadComplete(bitmap);
                            } else {
                                listener.onLoadFailed(null);
                            }
                        }
                    }.execute(bitmap);
                }
            }

            @Override
            public void onFailureImpl(DataSource dataSource) {
                if (listener != null) {
                    listener.onLoadFailed(dataSource.getFailureCause());
                }
            }
        };

        dataSource.subscribe(subscriber, UiThreadImmediateExecutorService.getInstance());
    }
}

# Glide实现

该实现依赖 Glide 库,需要在工程的 build.gradle 文件中添加依赖:

compile 'com.github.bumptech.glide:glide:3.7.0'

如果使用的 IDE 是 Eclipse,则需要在 libs 中添加 Glide 的 jar 包。

public class GlideImageLoader implements UnicornImageLoader {
    private Context context;

    public GlideImageLoader(Context context) {
        this.context = context.getApplicationContext();
    }

    @Nullable
    @Override
    public Bitmap loadImageSync(String uri, int width, int height) {
        return null;
    }

    @Override
    public void loadImage(String uri, int width, int height, final ImageLoaderListener listener) {
        if (width <= 0 || height <= 0) {
            width = height = Integer.MIN_VALUE;
        }

        Glide.with(context).load(uri).asBitmap().into(new SimpleTarget<Bitmap>(width, height) {
            @Override
            public void onResourceReady(Bitmap resource, GlideAnimation<? super Bitmap> glideAnimation) {
                if (listener != null) {
                    listener.onLoadComplete(resource);
                }
            }

            @Override
            public void onLoadFailed(Exception e, Drawable errorDrawable) {
                if (listener != null) {
                    listener.onLoadFailed(e);
                }
            }
        });
    }
}

# Glide V4 实现

该实现需要依赖 Glide 4.0 以上的版本 具体实现方式如下面代码:

public class GlideImageLoader implements UnicornImageLoader {
    private Context context;

    public GlideImageLoader(Context context) {
        this.context = context.getApplicationContext();
    }

    @Override
    public Bitmap loadImageSync(String uri, int width, int height) {
        return null;
    }

    @Override
    public void loadImage(String uri, int width, int height, final ImageLoaderListener listener) {
        if (width <= 0 || height <= 0) {
            width = height = Integer.MIN_VALUE;
        }

        Glide.with(context).asBitmap().load(uri).into(new CustomTarget<Bitmap>(width, height) {

            @Override
            public void onLoadStarted(@Nullable Drawable placeholder) {

            }

            @Override
            public void onLoadFailed(@Nullable Drawable errorDrawable) {

            }

            @Override
            public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
                if (listener != null) {
                    listener.onLoadComplete(resource);
                }
            }

            @Override
            public void onLoadCleared(@Nullable Drawable placeholder) {

            }
        });
    }
}

# Picasso实现

该实现依赖 Picasso 库,需要在工程的 build.gradle 文件中添加依赖:

compile 'com.squareup.picasso:picasso:2.5.2'

如果使用的 IDE 是 Eclipse,则需要在 libs 中添加 Picasso 的 jar 包。

public class PicassoImageLoader implements UnicornImageLoader {
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private Context context;
    private ExecutorService threadPool;
    private Handler uiHandler;

    public PicassoImageLoader(Context context) {
        this.context = context.getApplicationContext();
        uiHandler = new Handler(Looper.getMainLooper());
        threadPool = Executors.newFixedThreadPool(CPU_COUNT + 1);
    }

    @Nullable
    @Override
    public Bitmap loadImageSync(String uri, int width, int height) {
        return null;
    }

    @Override
    public void loadImage(final String uri, final int width, final int height, final ImageLoaderListener listener) {
        threadPool.execute(new Runnable() {
            @Override
            public void run() {
                RequestCreator requestCreator = Picasso
                        .with(context)
                        .load(uri)
                        .config(Bitmap.Config.RGB_565);
                if (width > 0 && height > 0) {
                    requestCreator = requestCreator
                            .resize(width, height)
                            .centerCrop();
                }

                Bitmap bitmap = null;
                try {
                    bitmap = requestCreator.get();
                } catch (IOException e) {
                    e.printStackTrace();
                }

                if (listener == null) {
                    return;
                }

                if (bitmap != null && !bitmap.isRecycled()) {
                    final Bitmap finalBitmap = bitmap;
                    uiHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            listener.onLoadComplete(finalBitmap);
                        }
                    });
                } else {
                    uiHandler.post(new Runnable() {
                        @Override
                        public void run() {
                            listener.onLoadFailed(null);
                        }
                    });
                }
            }
        });
    }
}

# gif 图片加载

在 V4.9.0 版本中我们开放了加载 gif 图片的功能,用户可以通过实现 UnicornGifImageLoader 接口来完成 gif 图片的加载,是在加载 gif 图片需要两步,第一,实现自己的 UnicornGifImageLoader ,第二,将实现的 UnicornGifImageLoader 设置到 option 中。下面看代码:


//第一步
public class GlideGifImagerLoader implements UnicornGifImageLoader {

    Context context;

    public GlideGifImagerLoader(Context context) {
        this.context = context.getApplicationContext();
    }

    //当需要加载 gif 图片的时候会回调该方法
    @Override
    public void loadGifImage(String url, ImageView imageView,String imgName) {
        if (url == null || imgName == null) {
            return;
        }
        Glide.with(context).load(url).placeholder(R.drawable.ic_launcher).error(R.drawable.nim_default_img_failed).into(imageView);
    }
}

//第二步在设置 App 的 option 的时候需要加上下面一行代码
options.gifImageLoader = new GlideGifImagerLoader(YSFDemoApplication.this);

上面的代码只是展示了 Glide 的时候,因为我们的 loadGifImage 方法已经把 url 和 ImageView 都给到 App 侧了,所以具体实现可以自由发挥

# 手动启动 SDK 的视频和图片界面(只适用 Fragment 的接入方式)

在 V5.4.0 版本中,SDK 增加了启动拍摄视频、拍摄照片、选择本地视频、选择本地照片的四个方法。用户可以通过相关方法启动 SDK 中的界面,然后通过相关回调发送图片和视频,具体操作分为一下几步:

  1. 创建 UnicornVideoPickHelper 和 UnicornPickImageHelper 并在初始化方法中加入回调
  2. 通过 UnicornVideoPickHelper 和 UnicornPickImageHelper 启动 SDK 原生视频或相册界面
  3. 在 Activity 中通过 onActivityResult 方法把视频和图片回传给 UnicornVideoPickHelper 和 UnicornPickImageHelper。

下面看具体实现的代码:

//第一步
//当使用视频相关功能时,创建 UnicornVideoPickHelper
unicornVideoPickHelper = new UnicornVideoPickHelper(this, new UnicornVideoPickHelper.UincornVideoSelectListener() {

            @Override
            public void onVideoPicked(File file, String md5) {
            //当视频处理成功的时候,我们需要去发送视频类型的消息
                MediaPlayer mediaPlayer = getVideoMediaPlayer(file);
                long duration = mediaPlayer == null ? 0 : mediaPlayer.getDuration();
                int height = mediaPlayer == null ? 0 : mediaPlayer.getVideoHeight();
                int width = mediaPlayer == null ? 0 : mediaPlayer.getVideoWidth();
                IMMessage message = UnicornMessageBuilder.buildVideoMessage(file, duration, width, height, md5);
                MessageService.sendMessage(message);
            }
        });
//当使用图片相关功能时,创建 UnicornVideoPickHelper
        unicornPickImageHelper = new UnicornPickImageHelper(this, new UnicornPickImageHelper.SendImageCallback() {
            @Override
            public void sendImage(File file, String origName, boolean isOrig) {
            //当图片处理成功的时候,发送图片类型的消息
                IMMessage message = UnicornMessageBuilder.buildImageMessage(UnicornMessageBuilder.getSessionId(), file, file.getName());
                MessageService.sendMessage(message);
            }
        });


//第二步,在 APP 想要启动 SDK 界面的时候,调用相关方法启动
private void addPhoneMenu() {
        addImageMenu(R.drawable.ic_action_album).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //启动选择相册图片
                unicornPickImageHelper.goUnicornAlbum(ALBUM_IMAGE_REQUEST_CODE);
            }
        });
        addImageMenu(R.drawable.ic_action_camera).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //启动拍摄照片
                unicornPickImageHelper.goUnicornCapturePhoto(CAPTURE_IMAGE_REQUEST_CODE);
            }
        });
        addImageMenu(R.drawable.ic_action_take_video).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //启动拍摄视频界面
                unicornVideoPickHelper.goCaptureVideo(CAPTURE_VIDEO_REQUEST_CODE);
            }
        });
        addImageMenu(R.drawable.ic_action_select_video).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                //启动选择视频界面
                unicornVideoPickHelper.goVideoAlbum(LOCAL_VIDEO_REQUEST_CODE);
            }
        });
    }


//第三步,在 onActivityResult 中把数据回传给 SDK
@Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        switch (requestCode) {
            case CAPTURE_VIDEO_REQUEST_CODE:
                //拍摄视频使用
                unicornVideoPickHelper.onCaptureVideoResult(data);
                break;
            case LOCAL_VIDEO_REQUEST_CODE:
                //从相册中选择视频使用
                unicornVideoPickHelper.onSelectLocalVideoResult(data);
                break;
            case ALBUM_IMAGE_REQUEST_CODE:
                //从相册中选择照片使用
                unicornPickImageHelper.onAlbumResult(data);
                break;
            case CAPTURE_IMAGE_REQUEST_CODE:
                //拍照中使用
                unicornPickImageHelper.onCapturePhotoResult(data, CAPTURE_IMAGE_PROCESS_REQUEST_CODE);
                break;
            case CAPTURE_IMAGE_PROCESS_REQUEST_CODE:
                //拍照中使用
                unicornPickImageHelper.onCapturePhotoPorcessResult(data, CAPTURE_IMAGE_REQUEST_CODE);
                break;
        }
    }

上面的代码已经比较清晰了,如果想查看全部代码,请参考 demo 工程中的 ServiceActivity.class

# 手动启动 SDK 选择文件界面(只适用 Fragment 的接入方式)

在 V5.6.0 版本中, SDK 新增了选择文件的功能,用户可以通过后台设置,或者通过配置InputPanelOptions 中的 getActionList 方法返回 PickFileAction 添加选择文件的功能。 用户也可以自己调用选择文件的界面,然后再把选择之后的数据回传给 SDK 就可以了

该功能需要两步实现:

第一步: 当想要启动文件选择的时候调用 UnicornPickFileHelper.goPickFileActivity 方法,请看如下代码:

UnicornPickFileHelper.goPickFileActivity(this, makeRequestCode(RequestCode.SELECT_FILE_REQUEST_CODE));

第二步: 在 Activity 的 onActivityResult 方法中将 data 回传给 SDK,请看如下代码:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
        case RequestCode.SELECT_FILE_REQUEST_CODE:
            if (resultCode == RESULT_OK) {
                UnicornPickFileHelper.onPickFileResult(this, data);
            }
            break;
    }

}

上面两步就可以完成文件选择的功能了

# 漫游消息拉取

七鱼客服系统支持账号信息打通,对于企业通过setUserInfo:接口传入的userId,服务端会合并不同访客端产生的消息记录。SDK 默认不从服务端拉取漫游消息,仅读取本地数据库持久化数据。若开启漫游消息拉取功能,则在企业调用setUserInfo:时会联网请求该userId账户对应的漫游历史消息,并与本地数据库进行对比过滤重复数据。漫游功能独立于聊天页面,可在初始化 SDK 传入的 YsfOption 中配置,可以通过如下代码配置:

YSFOptions options = new YSFOptions();
options.isPullMessageFromServer = true;

在 V5.15.0 版本中,SDK 增加了 pullMessageCount 配置拉取未读消息的数量,如果没有配置该字段默认拉取 20 条,最多拉取 100 条