讓你的 Android 應用也能聊天(第二版)

簡介

自去年 LeanCloud 發布實時通信(IM)服務之后,基于用戶反饋和工程師對需求的消化和對業務的提煉,上周正式發布了「實時通信 2.0 」。設計理念依然是「靈活、解耦、可組合、可定制」,具體可以參考《實時通信開發指南》,了解 LeanCloud 實時通信的基本概念和模型。

下載和安裝

可以到 LeanCloud 官方下載點下載 LeanCloud IM SDK v2 版本。將下載到的 jar 包加入工程即可。

一對一的文本聊天

我們先從最簡單的環節入手,看看怎么用 LeanCloud IM SDK v2 實現一對一文本聊天。

初始化

和 LeanCloud 其他服務一樣,實時聊天服務的初始化也是在 Application 的 onCreate 方法中進行的:

public class MyApplication extends Application{

    public void onCreate(){
      ...
      AVOSCloud.initialize(this,"{{appId}}","{{appKey}}");
      ...
    }
}

并且在AndroidManifest.xml中間聲明:

<manifest>
   ...

   <application
        android:name=".MyApplication"
        ....>
        ...

        <service android:name="com.avos.avoscloud.PushService" />

        <receiver android:name="com.avos.avoscloud.AVBroadcastReceiver">
            <intent-filter>
                <action android:name="android.intent.action.BOOT_COMPLETED" />
                <action android:name="android.intent.action.USER_PRESENT" />
            </intent-filter>
        </receiver>
        ...
   </application>

</manifest>

接下來我們需要完成用戶登錄。

登錄

假定聊天發起方名叫 Tom,為直觀起見,我們使用用戶名來作為 clientId 登錄聊天系統(LeanCloud 云端只要求 clientId 在應用內唯一即可,具體用什么數據由應用層決定),代碼如下:

AVIMClient imClient = AVIMClient.getInstance("Tom");
imClient.open(new IMClientCallback(){
  @Override
  public void done(AVIMClient client, AVException e) {
    if (null != e) {
      // 出錯了,可能是網絡問題無法連接 LeanCloud 云端,請檢查網絡之后重試。
      // 此時聊天服務不可用。
      e.printStackTrace();
    } else {
      // 成功登錄,可以開始進行聊天了(假設為 MainActivity)。
      Intent intent = new Intent(currentActivity, MainActivity.class);
      currentActivity.startActivity(intent);
    };
  }
});

建立對話

假定我們要跟「Bob」這個用戶進行聊天,我們先創建一個對話,代碼如下:

List<String> clientIds = new ArrayList<String>();
clientIds.add("Tom");
clientIds.add("Bob");

// 我們給對話增加一個自定義屬性 type,表示單聊還是群聊
// 常量定義:
// int ConversationType_OneOne = 0; // 兩個人之間的單聊
// int ConversationType_Group = 1;  // 多人之間的群聊
Map<String, Object> attr = new HashMap<String, Object>();
attr.put("type", ConversationType_OneOne);

imClient.createConversation(clientIds, attr, new AVIMConversationCreatedCallback() {
  @Override
  public void done(AVIMConversation conversation, AVException e) {
    if (null != conversation) {
      // 成功了,這時候可以顯示對話的 Activity 頁面(假定為 ChatActivity)了。
      Intent intent = new Intent(this, ChatActivity.class);
      Intent.putExtra(“conversation”, conversation);
      startActivity(intent);
    }
  }
});

建立的「對話」在控制臺怎么查看

如你所見,我們創建一個對話的時候,指定了成員(Tom 和 Bob)和一個額外的屬性({type: 0})。這些數據保存到云端后,你在 控制臺 -> 存儲 -> 數據 里面會看到,_Conversation 表中增加了一條記錄,新記錄的 m 屬性值為["Tom", "Bob"]attr 屬性值為{"type":0}。如你所料,m 屬性就是對應著成員列表,attr 屬性就是用戶增加的額外屬性值(以對象的形式存儲)。

發送消息

建立好對話之后,要發送消息是很簡單的:

AVIMMessage message = new AVIMMessage();
message.setContent("hello");
conversation.sendMessage(message, new AVIMConversationCallback() {
  @Override
  public void done(AVException e) {
    if (null != e) {
      // 出錯了。。。
      e.printStackTrace();
    } else {
      Logger.d("發送成功,msgId=" + message.getMessageId());
    }
  }
});

好了,這樣一條消息就發送過去了。但是問題來了,對于「Bob」而言,他怎么才能收到別人發給他的消息呢?

消息接收

在 Bob 這一端,要能接收到消息,需要如下幾步:

1,進行初始化;

2,實現自己的 AVIMMessageHandler,響應新消息到達通知,主要是如下函數:

public void onMessage(AVIMMessage message, AVIMConversation conversation, AVIMClient client);

對于 Tom 發過來的消息,要顯示出來,我們只需實現 onMessage 即可,示例代碼如下:

class CustomMessageHandler extends AVIMMessageHandler {
  @Override
  public void onMessage(AVIMMessage message, AVIMConversation conversation, AVIMClient client) {
    // 新消息到來了。在這里增加你自己的處理代碼。
    String msgContent = message.getContent();
    Logger.d(conversation.getConversationid() + " 收到一條新消息:" + msgContent);
  }
}

3,進行登錄,代碼也與發送端一樣。

完整代碼如下:

// 自定義消息響應類
class CustomMessageHandler extends AVIMMessageHandler {
  @Override
  public void onMessage(AVIMMessage message, AVIMConversation conversation, AVIMClient client) {
    // 新消息到來了。在這里增加你自己的處理代碼。
    String msgContent = message.getContent();
    Logger.d(conversation.getConversationid() + " 收到一條新消息:" + msgContent);
  }
}

// application 的初始化部分
public void onCreate(){
  ...
  AVOSCloud.initialize(this,"{{appId}}","{{appKey}}");
  AVIMMessageManager.registerDefaultMessageHandler(new CustomMessageHandler());
  ...
}

// 用戶登錄部分
AVIMClient imClient = AVIMClient.getInstance("Bob");
imClient.open(new IMClientCallback(){
  @Override
  public void done(AVIMClient client, AVException e) {
    if (null != e) {
      // 出錯了,可能是網絡問題無法連接 LeanCloud 云端,請檢查網絡之后重試。
      // 此時聊天服務不可用。
      e.printStackTrace();
    } else {
      // 成功登錄,可以開始進行聊天了。
    };
  }
});

注意!
AVIMMessageManager.registerDefaultMessageHandler() 一定要在 AVIMClient.open() 之前調用,否則可能導致服務器發回來的部分消息丟失。

幾個主要的回調接口

從上面的例子中可以看到,要接收到別人給你發送的消息,需要重載 AVIMMessageHandler 類。從 v2 版開始,LeanCloud IM SDK 大量采用回調來反饋操作結果,但是對于一些被動的消息通知,則還是采用接口來實現的,包括:

  • 當前網絡出現變化
  • 對話中有新的消息
  • 對話中有新成員加入
  • 對話中有成員離開
  • 被邀請加入某對話
  • 被踢出對話

LeanCloud IM SDK 內部使用了三種接口來響應這些事件。

網絡事件響應接口

主要用來處理網絡變化事件,接口定義在 AVIMClientEventHandler,主要函數為:

  /**
   * 實現本方法以處理網絡斷開事件
   */
  public abstract void onConnectionPaused(AVIMClient client);

  /**
   * 實現本方法以處理網絡恢復事件
   */
  public abstract void onConnectionResume(AVIMClient client);

在網絡中斷的情況下,所有的消息收發和對話操作都會出現問題。

通過 AVIMClient.setClientEventHandler(AVIMClientEventHandler handler) 可以設定全局的 ClientEventHandler。

對話成員變化響應接口

主要用來處理對話中成員變化的事件,接口定義在 AVIMConversationEventHandler,主要函數為:

  /**
   * 實現本方法以處理聊天對話中的參與者離開事件
   *
   * @param members 離開的參與者
   * @param kickedBy 踢人者,自愿退出的情況下踢人者就是參與者
   */
  public abstract void onMemberLeft(AVIMClient client,
      AVIMConversation conversation, List<String> members, String kickedBy);

  /**
   * 實現本方法以處理聊天對話中的參與者加入事件
   *
   * @param members 加入的參與者
   * @param invitedBy 邀請人,有可能是加入的參與者本身
   */
  public abstract void onMemberJoined(AVIMClient client,
      AVIMConversation conversation, List<String> members, String invitedBy);

  /**
   * 實現本方法來處理當前用戶被踢出某個聊天對話事件
   *
   * @param kickedBy 踢出你的人
   */
  public abstract void onKicked(AVIMClient client, AVIMConversation conversation,
      String kickedBy);

  /**
   * 實現本方法來處理當前用戶被邀請到某個聊天對話事件
   *
   * @param conversation 被邀請的聊天對話
   * @param operator 邀請你的人
   */
  public abstract void onInvited(AVIMClient client, AVIMConversation conversation,
      String operator);

通過 AVIMMessageManager.setConversationEventHandler(AVIMConversationEventHandler handler) 可以設置全局的 ConversationEventHandler。

消息響應接口

主要用來處理新消息到達事件,接口定義在 MessageHandlerAVIMMessageHandler 是一個空的實現類,我們應該通過重載 AVIMMessageHandler 的相關方法來完成消息處理。主要的方法有:

  // 收到新的消息
  @Override
  public void onMessage(AVIMMessage message, AVIMConversation conversation);

  // 自己發送的消息已經被對方接收
  @Override
  public void onMessageReceipt(AVIMMessage message, AVIMConversation conversation, AVIMClient client);

通過 AVIMMessageManager.registerDefaultMessageHandler(handler) 可以設置全局的 MessageHandler。

我們實現這三類接口,就可以處理所有的通知消息了。示例代碼如下:

class CustomNetworkHandler extends AVIMClientEventHandler {
  @Override
  public void onConnectionPaused(AVIMClient client) {
    // 請按自己需求改寫
    Logger.d("connect paused");
  }

  @Override
  public void onConnectionResume(AVIMClient client) {
    // 請按自己需求改寫
    Logger.d("connect resume");
  }
}

class CustomConversationHandler extends AVIMConversationEventHandler {
  public private Context gContext = null;
  private void toast(String str) {
    Toast.makeText(gContext, str, Toast.LENGTH_SHORT).show();
  }
  private void toast(Context context, String str) {
    Toast.makeText(context, str, Toast.LENGTH_SHORT).show();
  }

  @Override
  public void onMemberLeft(AVIMClient client, AVIMConversation conversation, List<String> members, String kickedBy) {
    // 請按自己需求改寫
    toast(MsgUtils.nameByUserIds(members) + " left, kicked by " + MsgUtils.nameByUserId(kickedBy));
    //注:MsgUtils 是一個輔助類,nameByUserIds 用來將 userId 轉換成用戶名
  }

  @Override
  public void onMemberJoined(AVIMClient client, AVIMConversation conversation, List<String> members, String invitedBy) {
    // 請按自己需求改寫
    toast(MsgUtils.nameByUserIds(members) + " joined , invited by " + MsgUtils.nameByUserId(invitedBy));
    //注:MsgUtils 是一個輔助類,nameByUserIds 用來將 userId 轉換成用戶名
  }

  @Override
  public void onKicked(AVIMClient client, AVIMConversation conversation, String kickedBy) {
    // 請按自己需求改寫
    toast("you are kicked by " + MsgUtils.nameByUserId(kickedBy));
  }

  @Override
  public void onInvited(AVIMClient client, AVIMConversation conversation, String operator) {
    // 請按自己需求改寫
    toast("you are invited by " + MsgUtils.nameByUserId(operator));
  }
};

class CustomMsgHandler extends AVIMMessageHandler {
  @Override
  public void onMessage(AVIMMessage message, AVIMConversation conversation, AVIMClient client) {
    // 請按自己需求改寫
    String msgContent = message.getContent();
    Logger.d(conversation.getConversationid() + " 收到一條新消息:" + msgContent);
  }

  @Override
  public void onMessageReceipt(AVIMMessage message, AVIMConversation conversation, AVIMClient client) {
    // 請按自己需求改寫
    Logger.d("發往對話 " + conversation.getConversationid() + " 的消息 "+ message.getMessageId() +" 已被接收");
  }
}

// 設置事件響應接口
AVIMClient.setClientEventHandler(new CustomNetworkHandler());
AVIMMessageManager.setConversationEventHandler(new CustomConversationHandler());
AVIMMessageManager.registerDefaultMessageHandler(new CustomMsgHandler());

支持富媒體的聊天消息

上面的代碼演示了如何發送簡單文本信息,但是現在的交互方式已經越來越多樣化,圖像、語音、視頻已是非常普遍的消息類型。v2 版的 LeanCloud IM SDK 已經可以很好地支持這些富媒體消息,具體說明如下:

基類:AVIMTypedMessage

所有富媒體消息的基類,其聲明為

//SDK定義的消息類型,LeanCloud SDK 自身使用的類型是負數,所有正數留給開發者自定義擴展類型使用,0 作為「沒有類型」被保留起來。
enum AVIMReservedMessageType {
  UnsupportedMessageType(0),
  TextMessageType(-1),
  ImageMessageType(-2),
  AudioMessageType(-3),
  VideoMessageType(-4),
  LocationMessageType(-5),
  FileMessageType(-6);
};

public abstract class AVIMTypedMessage extends AVIMMessage {
  public AVIMTypedMessage();

  public int getMessageType();

  @Override
  public final String getContent();

  @Override
  public final void setContent(String content);
}

文本消息(AVIMTextMessage)

AVIMTypedMessage 子類,表示一般的文本消息,其聲明為

public class AVIMTextMessage extends AVIMTypedMessage {
  public String getText();
  public void setText(String text);

  public Map<String, Object> getAttrs();
  public void setAttrs(Map<String, Object> attr);
}

可以看到,對于文本消息,主要的屬性有 textattr 兩個,通過簡單的 getter/setter 就可以訪問到。要發送文本消息,示例代碼為:

AVIMTextMessage message = new AVIMTextMessage();
message.setText("hello");
conversation.sendMessage(message, new AVIMConversationCallback() {
  @Override
  public void done(AVException e) {
    if (null != e) {
      // 出錯了。。。
      e.printStackTrace();
    } else {
      Logger.d("message sent.");
    }
  }
});

文件消息(AVIMFileMessage)

AVIMTypedMessage 子類,用來發送帶附件的消息,開發者可以用它來發送「離線文件」。對于此類消息,LeanCloud IM SDK 內部會先把文件上傳到 LeanCloud 文件存儲服務器(自帶 CDN 功能),然后把文件元數據(url,文件大小等等)放在消息包內發送到 LeanCloud 實時通信服務端。其構造函數聲明為:

// 傳入本地文件路徑,構造消息對象
public AVIMFileessage(String localPath) throws FileNotFoundException, IOException;
// 傳入本地文件,構造消息對象
public AVIMFileMessage(File localFile) throws FileNotFoundException, IOException;
// 傳入 AVFile 實例,構造消息對象
public AVIMFileMessage(AVFile file);

與文本消息類似,文件消息也支持附帶文本和其他自定義屬性,可以通過如下方法添加 / 獲取更多信息:

  • String getText() / void setText(String text)
  • Map<String, Object> getAttrs() / void setAttrs(Map<String, Object> attr);

發送文件消息的示例代碼為:

String localZipfilePath;
try {
  AVIMFileMessage message = new AVIMFileMessage(localZipfilePath);
  message.setText("這是你要的文檔");
  conversation.sendMessage(message, new AVIMConversationCallback() {
    @Override
    public void done(AVException e) {
      if (null != e) {
        // 出錯了。。。
        e.printStackTrace();
      } else {
        Logger.d("message sent");
      }
    }
  });
} catch (Exception ex) {
}

接收到這樣消息之后,開發者可以通過以下方法,獲取到文件元數據(size 等)和一個包含二進制數據的 AVFile 對象:

  • AVFile getAVFile() 方法會返回一個二進制文件的 AVFile 實例,之后可以通過 AVFile 來完成數據下載或者其他操作,具體可以參見 AVFile 說明
  • String getFileUrl() 方法會返回二進制文件的 url
  • long getSize() 方法會返回二進制文件的實際大小(單位:byte)
  • Map<String, Object> getFileMetaData() 可以獲取二進制文件的其他元數據信息。

圖像消息(AVIMImageMessage)

AVIMFileMessage 子類,專門用來發送圖像和附帶文本的混合消息,其構造函數聲明為:

// 傳入本地文件路徑,構造消息對象
public AVIMImageMessage(String localPath) throws FileNotFoundException, IOException;
// 傳入本地文件,構造消息對象
public AVIMImageMessage(File localFile) throws FileNotFoundException, IOException;
// 傳入 AVFile 實例,構造消息對象
public AVIMImageMessage(AVFile file);

發送圖像消息的示例代碼為:

String localImagePath;
try {
  AVIMImageMessage message = new AVIMImageMessage(localImagePath);
  message.setText("你說我好看不?");
  conversation.sendMessage(message, new AVIMConversationCallback() {
    @Override
    public void done(AVException e) {
      if (null != e) {
        // 出錯了。。。
        e.printStackTrace();
      } else {
        Logger.d("message sent");
      }
    }
  });
} catch (Exception ex) {
}

接收到這樣消息之后,開發者可以通過如下方法,獲取到若干圖像元數據(width,height,圖像 size)和一個包含圖像數據的 AVFile 對象:

  • int getWidth() 方法會返回圖像的寬度(單位:pixel)
  • int getHeight() 方法會返回圖像的高度(單位:pixel)
  • AVFile getAVFile() (繼承自 AVIMFileMessage)方法會返回一個圖像文件的 AVFile 實例
  • String getFileUrl() (繼承自 AVIMFileMessage)方法會返回圖像文件的 url
  • long getSize() (繼承自 AVIMFileMessage)方法會返回圖像文件的實際大小(單位:byte)
  • String getText() (繼承自 AVIMFileMessage)方法會返回隨圖像一起發送的文本信息。
  • Map<String, Object> getFileMetaData() (繼承自 AVIMFileMessage)可以獲取圖像的其他元數據信息。

音頻消息(AVIMAudioMessage)

AVIMFileMessage 子類,專門用來發送語音和附帶文本的混合消息,其構造函數聲明為:

// 傳入本地文件路徑,構造消息對象
public AVIMAudioMessage(String localPath) throws FileNotFoundException, IOException;
// 傳入本地文件,構造消息對象
public AVIMAudioMessage(File localFile) throws FileNotFoundException, IOException;   
// 傳入 AVFile 實例,構造消息對象
public AVIMAudioMessage(AVFile file);

發送音頻消息的示例代碼為:

String localAudioPath;
try {
  AVIMAudioMessage message = new AVIMAudioMessage(localAudioPath);
  message.setText("聽聽我唱的小蘋果:)");
  conversation.sendMessage(message, new AVIMConversationCallback() {
    @Override
    public void done(AVException e) {
      if (null != e) {
        // 出錯了。。。
        e.printStackTrace();
      } else {
        Logger.d("message sent");
      }
    }
  });
} catch (Exception ex) {
}

接收到這樣消息之后,開發者可以通過如下方法,獲取到若干音頻元數據(時長 duration、音頻 size)和一個包含音頻數據的 AVFile 對象:

  • double getDuration() 方法會返回音頻的長度(單位:秒)
  • AVFile getAVFile() (繼承自 AVIMFileMessage)方法會返回一個音頻文件的 AVFile 實例
  • String getFileUrl() (繼承自 AVIMFileMessage)方法會返回音頻文件的 url
  • long getSize() (繼承自 AVIMFileMessage)方法會返回音頻文件的實際大小(單位:byte)
  • String getText() (繼承自 AVIMFileMessage)方法會返回隨音頻一起發送的文本信息。
  • Map<String, Object> getFileMetaData() (繼承自 AVIMFileMessage)可以獲取音頻的其他元數據信息。

視頻消息(AVIMVideoMessage)

AVIMFileMessage 子類,專門用來發送視頻和附帶文本的混合消息,其構造函數聲明為:

// 傳入本地文件路徑,構造消息對象
public AVIMVideoMessage(String localPath) throws FileNotFoundException, IOException;
// 傳入本地文件,構造消息對象
public AVIMVideoMessage(File localFile) throws FileNotFoundException, IOException;
// 傳入 AVFile 文件,構造消息對象
public AVIMVideoMessage(AVFile file);

發送視頻消息的示例代碼為:

String localVideoPath;
try {
  AVIMVideoMessage message = new AVIMVideoMessage(localVideoPath);
  message.setText("敢不敢跟我比一比");
  conversation.sendMessage(message, new AVIMConversationCallback() {
    @Override
    public void done(AVException e) {
      if (null != e) {
        // 出錯了。。。
        e.printStackTrace();
      } else {
        Logger.d("message sent");
      }
    }
  });
} catch (Exception ex) {
}

接收到這樣消息之后,開發者可以可以通過如下方法,獲取到若干視頻元數據(時長 duration、視頻 size)和一個包含視頻數據的 AVFile 對象:

  • double getDuration() 方法會返回視頻的長度(單位:秒)
  • AVFile getAVFile() (繼承自 AVIMFileMessage)方法會返回一個視頻文件的 AVFile 實例
  • String getFileUrl() (繼承自 AVIMFileMessage)方法會返回視頻文件的 url
  • long getSize() (繼承自 AVIMFileMessage)方法會返回視頻文件的實際大小(單位:byte)
  • String getText() (繼承自 AVIMFileMessage)方法會返回隨視頻一起發送的文本信息。
  • Map<String, Object> getFileMetaData() (繼承自 AVIMFileMessage)可以獲取視頻的其他元數據信息。

地理位置消息(AVIMLocationMessage)

AVIMTypedMessage 子類,支持發送地理位置信息和附帶文本的混合消息,其聲明為:

public class AVIMLocationMessage extends AVIMTypedMessage {
  public String getText();
  public void setText(String text);

  public Map<String, Object> getAttrs();
  public void setAttrs(Map<String, Object> attr);
  
  public AVGeoPoint getLocation();
  public void setLocation(AVGeoPoint location);
}

與文本消息類似,地理位置消息只是增加了一個 AVGeoPoint 的 Location 屬性。要發送位置消息的示例代碼為:

AVIMLocationMessage message = new AVIMLocationMessage();
message.setText("快點過來!");
message.setLocation(new AVGeoPoint(15.9, 56.4));
conversation.sendMessage(message, new AVIMConversationCallback() {
  @Override
  public void done(AVException e) {
    if (null != e) {
      // 出錯了。。。
      e.printStackTrace();
    } else {
      Logger.d("message sent");
    }
  }
});

接收到這樣的消息之后,開發者可以獲取到具體的地理位置數據。

如何接收富媒體消息

新版 LeanCloud IM SDK 內部封裝了對富媒體消息的支持,所有富媒體消息都是從 AVIMTypedMessage 派生出來的。發送的時候可以直接調用 conversation.sendMessage() 函數。在接收端,我們也專門增加了一類回調接口 AVIMTypedMessageHandler,其定義為:

public class AVIMTypedMessageHandler<T extends AVIMTypedMessage> extends MessageHandler<T> {

  @Override
  public void onMessage(T message, AVIMConversation conversation, AVIMClient client);

  @Override
  public void onMessageReceipt(T message, AVIMConversation conversation, AVIMClient client);
}

開發者可以編寫自己的消息處理 handler,然后調用 AVIMMessageManager.registerMessageHandler(Class<? extends AVIMMessage> clazz, MessageHandler<?> handler) 函數來注冊目標 handler。

接收端對于富媒體消息的通知處理的示例代碼如下:

class MsgHandler extends AVIMTypedMessageHandler<AVIMTypedMessage> {

  @Override
  public void onMessage(AVIMTypedMessage message, AVIMConversation conversation, AVIMClient client) {
    // 請按自己需求改寫
    switch(message.getMessageType()) {
    case AVIMReservedMessageType.TextMessageType:
      AVIMTextMessage textMsg = (AVIMTextMessage)message;
      Logger.d("收到文本消息:" + textMsg.getText() + ", msgId:" + textMsg.getMessageId());
      break;
    case AVIMReservedMessageType.FileMessageType:
      AVIMFileMessage fileMsg = (AVIMFileMessage)message;
      Logger.id("收到文件消息。msgId=" + fileMsg.getMessageId() + ", url=" + fileMsg.getFileUrl() + ", size=" + fileMsg.getSize());
      break;
    case AVIMReservedMessageType.ImageMessageType:
      AVIMImageMessage imageMsg = (AVIMImageMessage)message;
      Logger.id("收到圖片消息。msgId=" + imageMsg.getMessageId() + ", url=" + imageMsg.getFileUrl() + ", width=" + imageMsg.getWidth() + ", height=" + imageMsg.getHeight());
      break;
    case AVIMReservedMessageType.AudioMessageType:
      AVIMAudioMessage audioMsg = (AVIMAudioMessage)message;
      Logger.id("收到音頻消息。msgId=" + audioMsg.getMessageId() + ", url=" + audioMsg.getFileUrl() + ", duration=" + audioMsg.getDuration());
      break;
    case AVIMReservedMessageType.VideoMessageType:
      AVIMVideoMessage videoMsg = (AVIMAudioMessage)message;
      Logger.id("收到視頻消息。msgId=" + videoMsg.getMessageId() + ", url=" + videoMsg.getFileUrl() + ", duration=" + videoMsg.getDuration());
      break;
    case AVIMReservedMessageType.LocationMessageType:
      AVIMLocationMessage locMsg = (AVIMLocationMessage)message;
      Logger.id("收到位置消息。msgId=" + locMsg.getMessageId() + ", latitude=" + locMsg.getLocation().getLatitude() + ", longitude=" + locMsg.getLocation().getLongitude());
      break;
    }
  }

  @Override
  public void onMessageReceipt(AVIMTypedMessage message, AVIMConversation conversation, AVIMClient client) {
  }
}
MsgHandler msgHandler = new MsgHandler();
AVIMMessageManager.registerMessageHandler(AVIMTypedMessage.class, msgHandler);

LeanCloud IM SDK 內部消息分發的邏輯是這樣的:對于收到的任一新消息,SDK 內部都會先解析消息的類型,根據類型找到開發者為這一類型注冊的處理 handler,然后逐一調用這些 handler 的 onMessage 函數。如果沒有找到專門處理這一類型消息的 handler,就會轉交給 defaultHandler 處理。

這樣一來,在開發者為 TypedMessage(及其子類) 指定了專門的 handler,也指定了全局的 defaultHandler 了的時候,如果發送端發送的是通用的 AVIMMessage 消息,那么接受端就是 AVIMMessageManager.registerDefaultMessageHandler()中指定的 handler 被調用;如果發送的是 AVIMTypedMessage(及其子類)的消息,那么接受端就是 AVIMMessageManager.registerMessageHandler()中指定的 handler 被調用。

如何擴展自己的富媒體消息

繼承于 AVIMTypedMessage,開發者也可以擴展自己的富媒體消息。其要求和步驟是:

  • 實現新的消息類型,繼承自 AVIMTypedMessage。這里需要注意兩點:
    • 在 class 上增加一個 @AVIMMessageType(type=123) 的 Annotation,具體消息類型的值(123)由開發者自己決定(LeanCloud 內建的消息類型使用負數,所有正數都預留給開發者擴展使用)。
    • 在消息內部屬性上要增加 @AVIMMessageField(name="") 的 Annotation,name 為可選字段在聲明字段屬性,同時自定義的字段要有對應的 getter/setter 方法。
  • 調用 AVIMMessageManager.registerAVIMMessageType(Class<? extends AVIMTypedMessage> messageType) 函數進行類型注冊
  • 調用 AVIMMessageManager.registerMessageHandler(Class<? extends AVIMMessage> clazz, MessageHandler<?> handler) 函數進行消息處理 handler 注冊。

AVIMTextMessage 的源碼如下,可供參考:

@AVIMMessageType(type = -1)
public class AVIMTextMessage extends AVIMTypedMessage {

  @AVIMMessageField(name = "_lctext")
  String text;
  @AVIMMessageField(name = "_lcattrs")
  Map<String, Object> attrs;

  public String getText() {
    return this.text;
  }

  public void setText(String text) {
    this.text = text;
  }

  public Map<String, Object> getAttrs() {
    return this.attrs;
  }

  public void setAttrs(Map<String, Object> attr) {
    this.attrs = attr;
  }
}

群組聊天

與前面的單聊類似,群組聊天也需要先建立一個對話(AVIMConversation),然后發送、接收新的消息。

創建群組

和單聊類似,建立一個多人聊天的群組也是很簡單的。例如:

Map<String, Object> attr = new HashMap<String, Object>();
attr.put("type", ConversationType_Group);
imClient.createConversation(clientIds, attr, new AVIMConversationCreatedCallback() {
  @Override
  public void done(AVIMConversation conversation, AVException e) {
    if (null != conversation) {
      // 成功了!
      Intent intent = new Intent(currentActivity, ChatActivity.class);
      Intent.putExtra(“conversation”, conversation);
      currentActivity.startActivity(intent);
    }
  }
});

成功之后,我們就可以進入聊天界面了。

往群組發送消息

發送消息非常簡單,與前面單聊的場景一樣。

我們會注意到,AVIMConversation 還有一個發送消息的方法:

public void sendMessage(final AVIMMessage message, final int messageFlag,
      final AVIMConversationCallback callback)

而這里 flag 的定義有如下三種類型:

  • 暫態消息(AVIMConversation.TRANSIENT_MESSAGE_FLAG)。這種消息不會被自動保存(以后在歷史消息中無法找到它),也不支持延遲接收,離線用戶更不會收到推送通知,所以適合用來做控制協議。譬如聊天過程中「某某正在輸入中...」這樣的狀態信息,就適合通過暫態消息來發送。
  • 普通消息(AVIMConversation.NONTRANSIENT_MESSAGE_FLAG)。這種消息就是我們最常用的消息類型,在 LeanCloud 云端會自動保存起來,支持延遲接收和離線推送,以后在歷史消息中可以找到它。
  • 待回執消息(AVIMConversation.RECEIPT_MESSAGE_FLAG)。這也是一種普通消息,只是消息被對方收到之后 LeanCloud 服務端會發送一個回執通知給發送方(這就是 AVIMMessageHandler 中 public void onMessageReceipt(AVIMMessage message, AVIMConversation conversation, AVIMClient client) 函數被調用的時機)。

接收群組消息

接收一個群組的消息,與接收單聊的消息也是一樣的。

成員管理

在查詢到聊天室成員之后,可以讓用戶邀請一些自己的朋友加入,作為管理員也可以剔除一些「可怕」的成員。
加入新成員的 API 如下:

// 假設需要邀請 Alex,Ben,Chad 三人加入對話
List<String> userIds = new ArrayList<String>();
userIds.add("Alex");
userIds.add("Ben");
userIds.add("Chad");
conversation.addMembers(userIds, new AVIMConversationCallback() {
  @Override
  public void done(AVException error) {
    if (null != error) {
      // 加入失敗,報錯.
      error.printStackTrace();
    } else {
      // 發出邀請,此后新成員就可以看到這個對話中的所有消息了。
      Logger.d("invited.");
    }
  }
});

邀請成功以后,相關方收到通知的時序是這樣的:

    操作者(管理員)                    被邀請者                        其他人
1, 發出請求 addMembers
2,                               收到 onInvited 通知
3, 收到 onMemberJoined 通知      收到 onMemberJoined 通知      收到 onMemberJoined 通知

相應地,踢人時的調用 API 是:

List<String> userIds = new ArrayList<String>();
userIds.add("Alex");
conversation.kickMembers(userIds, new AVIMConversationCallback() {
  @Override
  public void done(AVException error) {
    if (null != error) {
      // 失敗,報錯.
      error.printStackTrace();
    } else {
      // 成功。
      Logger.d("kicked.");
    }
  }
});

踢人時,相關方收到通知的時序如下:

    操作者(管理員)                被踢者                       其他人
1, 發出請求 kickMembers
2,                          收到 onKicked 通知
3, 收到 onMemberLeft 通知                             收到 onMemberLeft 通知

注意!
如果邀請、踢人操作發生的時候,被邀請者/被踢者當前不在線,那么通知消息并不會被離線緩存,所以他們再上線的時候將不會收到通知。

獲取歷史消息

LeanMessage 會將非暫態消息自動保存在云端,之后開發者可以通過 AVIMConversation 來獲取該對話的所有歷史消息。獲取歷史消息的 API 如下:

String oldestMsgId;
long oldestMsgTimestamp;
conversation.queryMessages(oldestMsgId,oldestMsgTimestamp, limit, new AVIMHistoryMessageCallback(){
  @Override
  public void done(List<AVIMMessage> messages, AVException e) {
    if (null != e) {
      // 出錯了:(
    } else {
      // 成功,可以將消息加入緩存,同時更新 UI
    }
  }
});

注意:
獲取歷史消息的時候,LeanCloud 云端是從某條消息開始,往前查找開發者指定的 N 條消息,返回給客戶端。為此,獲取歷史消息需要傳入三個參數:起始消息的 msgId,起始消息的發送時間戳,需要獲取的消息條數。

通過這一 API 拿到的消息就是 AVIMMessage 或者 AVIMTypedMessage 實例數組,開發者可以像之前收到新消息通知一樣處理。

啟用離線消息推送(僅對 iOS 平臺用戶有效)

不管是單聊還是群聊,當用戶 A 發出消息后,如果目標對話中有部分用戶當前不在線,LeanCloud 云端可以提供離線推送的方式來提醒用戶。這一功能默認是關閉的,你可以在 LeanCloud 應用控制臺中開啟它。開啟方法如下:

  • 登錄 LeanCloud 應用控制臺,選擇正確的應用進入;
  • 選擇最頂端的「消息」服務,依次點擊左側菜單「實時消息」->「設置」;
  • 在右側「iOS 用戶離線時的推送內容」下填好你要推送出去的消息內容,保存;

這樣 iOS 平臺上的用戶就可以收到 Push Notification 了(當然,前提是應用本身申請到了 RemoteNotification 權限,也將正確的推送證書上傳到了 LeanCloud 控制臺)。

群組消息免打擾(僅對 iOS 平臺用戶有效)

不管是單聊還是群聊,對于發往普通的 Conversation 的普通消息,如果接收方當前不在線,LeanCloud 云端支持通過 Push Notification 的方式進行提醒。一般情況下這都是很好的,但是如果某個群組特別活躍,那離線用戶就會收到過多的推送,會形成不小的干擾。

對此 LeanCloud IM 服務也允許單個用戶來關閉/打開某個對話的離線推送功能。

搜索群組

不管是單聊,還是群聊,在 LeanCloud IM SDK 里面都是對話(Conversation)。我們給對話設置了如下幾種屬性:

  • conversationId,字符串,對話 id,只讀,對話創建之后由 LeanCloud 云端賦予一個全局唯一的 id。
  • creator,字符串,對話創建者 id,只讀,標識對話創建者信息
  • members,數組,對話參與者,這里記錄了所有的參與者
  • name,字符串,對話的名字,optional,可用來對于群組命名
  • attributes,Map/Dict,自定義屬性,optional,供開發者自己擴展用。

我們提供了專門的類,來搜索特定的群組:通過 imClient.getQuery() 得到一個 AVIMConversationQuery 實例,然后調用 AVIMConversationQuery.wherexxx 系列方法來增加約束條件。例如要搜索當前登錄用戶參與的所有群聊對話,其代碼為

// 搜索 Tom 參與的所有群組對話
List<String> clients = new ArrayList<String>();
clients.add("Tom");
AVIMConversationQuery conversationQuery = imClient.getQuery();
conversationQuery.containsMember(clients);

// 之前有常量定義:
// const int ConversationType_OneOne = 0;
// const int ConversationType_Group = 1;
conversationQuery.whereEqualTo("attr.type", ConversationType_Group);

conversationQuery.findInBackground(new AVIMConversationQueryCallback(){
  @Override
  public void done(List<AVIMConversation> conversations, AVException e) {
    if (null != e) {
      // 出錯了。。。
      e.printStackTrace();
    } else {
      if (null != conversation) {
        Logger.d("找到了符合條件的 " + conversations.size() + " 個對話");
      } else {
        Logger.d("沒有找到符合條件的對話");
      }
    }
  }
});

AVIMConversationQuery 中設置條件的方法與 AVQuery 類似。這里 conversationQuery.containsMember() 表示對話的成員中至少包含這些人員,可用來根據部分成員查找對話;與此類似的還有一個 conversationQuery.withMembers() 則表示有且僅有這些成員,用來根據所有成員查找目標對話;conversationQuery.whereXXX() 系列方法可用來限定對話名稱和自定義屬性,這里要強調的一點是,對于自定義屬性的約束條件,屬性名一定要以 attr 開頭,如上例所示,限定額外的 type 條件的時候需要指定的屬性名是 attr.type。具體可以參看其頭文件。

開放聊天室

開放聊天室(也叫暫態對話)可以用于很多地方,譬如彈幕、直播等等。在 LeanCloud IM SDK 中,開放聊天室是一類特殊的群組,它也支持創建、加入/踢出成員等操作,消息記錄會被保存并可供獲取;與普通群組不一樣的地方具體體現為:

  • 不支持查詢成員列表,你可以通過相關 API 查詢在線人數;
  • 不支持離線消息、離線推送通知等功能;
  • 沒有成員加入、離開的通知;
  • 一個用戶一次登錄只能加入一個開放聊天室,加入新的開放聊天室后會自動離開原來的聊天室;
  • 加入后半小時內斷網重連會自動加入原聊天室,超過這個時間則需要重新加入;

創建開放聊天室

和普通的群組類似,建立一個開放聊天室也是很簡單的,只是在 AVIMClient.createConversation(conversationMembers, name, attributes, isTransient, callback) 中我們需要傳入 isTransient=true 選項。例如:

Map<String, Object> attr = new HashMap<String, Object>();
attr.put("type", ConversationType_Group);
imClient.createConversation(clientIds, name, attr, true, new AVIMConversationCreatedCallback() {
  @Override
  public void done(AVIMConversation conversation, AVException e) {
    if (null != conversation) {
      // 成功了,進入聊天室
      Intent intent = new Intent(currentActivity, ChatActivity.class);
      Intent.putExtra(“conversation”, conversation);
      currentActivity.startActivity(intent);
    }
  }
});

創建成功之后,我們就可以進入聊天界面了。開放聊天室的其他操作,都與普通群組操作一樣。

加入開放聊天室

假定任何終端用戶都可以加入開放聊天室。作為開發者,我們可以通過通過特定條件檢索到所有開放聊天室,然后允許用戶自由加入,其示例代碼為:

conversation.join(new AVIMConversationCallback(){
  @Override
  public void done(AVException e) {
    if (null != e) {
      // 出錯了:(
    } else {
      // 成功,此時可以進入聊天界面了。。。
      Intent intent = new Intent(currentActivity, ChatActivity.class);
      Intent.putExtra(“conversation”, conversation);
      currentActivity.startActivity(intent);
    }
  }
});

查詢在線人數

通過 AVIMConversation.getMemberCount() 方法可以實時查詢開放聊天室的在線人數。示例代碼如下:

conversation.getMemberCount(new AVIMConversationMemberCountCallback(){
  @Override
  public void done(Integer memberCount, AVException e) {
    if (null != e) {
      // 出錯了:(
    } else {
      // 成功,此時 memberCount 的數值就是實時在線人數
    }
  }
});

簽名和安全

為了滿足開發者對權限和認證的要求,LeanCloud 還設計了操作簽名的機制。我們可以在 LeanCloud 應用控制臺中的「設置」->「應用選項」->「聊天推送」下面勾選「聊天服務簽名認證」來啟用簽名(強烈推薦這樣做)。啟用后,所有的用戶登錄、對話創建/加入、邀請成員、踢出成員等操作都需要驗證簽名,這樣開發者就可以對消息進行充分的控制。

客戶端這邊究竟該如何使用呢?我們只需要實現 SignatureFactory 接口,然后在用戶登錄之前,把這個接口的實例賦值給 AVIMClient 即可(AVIMClient.setSignatureFactory(factory))。

設定了 signatureFactory 之后,對于需要鑒權的操作,LeanCloud IM SDK 與服務器端通訊的時候都會帶上應用自己生成的 Signature 信息,LeanCloud 云端會使用 app 的 masterKey 來驗證信息的有效性,保證聊天渠道的安全。

對于 SignatureFactory 接口,我們只需要實現這兩個函數即可:

  /**
   * 實現一個基礎簽名方法 其中的簽名算法會在SessionManager和AVIMClient(V2)中被使用
   */
  public Signature createSignature(String peerId, List<String> watchIds) throws SignatureException;

  /**
   * 實現AVIMConversation相關的簽名計算
   */
  public Signature createConversationSignature(String conversationId, String clientId,
      List<String> targetIds, String action) throws SignatureException;

createSignature 函數會在用戶登錄的時候被調用,createConversationSignature 會在對話創建/加入、邀請成員、踢出成員等操作時被調用。

你需要做的就是按照前文所述的簽名算法實現簽名,其中 Signature 聲明如下:

public class Signature {
  public List<String> getSignedPeerIds();
  public void setSignedPeerIds(List<String> signedPeerIds);

  public String getSignature();
  public void setSignature(String signature);

  public long getTimestamp();
  public void setTimestamp(long timestamp);

  public String getNonce();
  public void setNonce(String nonce);
}

其中四個屬性分別是:

  • signature 簽名
  • timestamp 時間戳,單位秒
  • nonce 隨機字符串 nonce
  • signedPeerIds 放行的 clientId 列表,v2 中已經廢棄不用

下面的代碼展示了基于 LeanCloud 云代碼進行簽名時,客戶端的實現片段,你可以參考它來完成自己的邏輯實現:

public class KeepAliveSignatureFactory implements SignatureFactory {
 @Override
 public Signature createSignature(String peerId, List<String> watchIds) {
   Map<String,Object> params = new HashMap<String,Object>();
   params.put("self_id",peerId);
   params.put("watch_ids",watchIds);

   try{
     Object result =  AVCloud.callFunction("sign",params);
     if(result instanceof Map){
       Map<String,Object> serverSignature = (Map<String,Object>) result;
       Signature signature = new Signature();
       signature.setSignature((String)serverSignature.get("signature"));
       signature.setTimestamp((Long)serverSignature.get("timestamp"));
       signature.setNonce((String)serverSignature.get("nonce"));
       return signature;
     }
   }catch(AVException e){
     throw (SignatureFactory.SignatureException) e;
   }
   return null;
 }

  @Override
  public Signature createConversationSignature(String convId, String peerId, List<String> targetPeerIds,String action){
   Map<String,Object> params = new HashMap<String,Object>();
   params.put("self_id",peerId);
   params.put("group_id",convId);
   params.put("group_peer_ids",targetPeerIds);
   params.put("action",action);

   try{
     Object result = AVCloud.callFunction("group_sign",params);
     if(result instanceof Map){
        Map<String,Object> serverSignature = (Map<String,Object>) result;
        Signature signature = new Signature();
        signature.setSignature((String)serverSignature.get("signature"));
        signature.setTimestamp((Long)serverSignature.get("timestamp"));
        signature.setNonce((String)serverSignature.get("nonce"));
        return signature;
     }
   }catch(AVException e){
     throw (SignatureFactory.SignatureException) e;
   }
   return null;
  }
}

LeanCloud IM SDK 專注做好底層的通訊服務,有更多可以定制化的地方,譬如說:

  • 賬戶系統和 IM 系統是分離的;
  • 消息變成離線推送的時候,推送內容開發者是可以定制的;
  • 通過 web hook,開發者可以對消息進行更多處理;
  • 聊天過程中通過消息鑒權機制,開發者可以有更多控制;

因為缺少 UI 組件,實事求是地講在新用戶接入成本可能稍高,但是在業務規模擴大、產品需求變多之后,相信大家會越來越喜歡 LeanCloud 這種自由靈活的使用體驗,以及穩定迅捷的服務質量。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,505評論 6 533
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,556評論 3 418
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,463評論 0 376
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,009評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,778評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,218評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,281評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,436評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,969評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,795評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,993評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,537評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,229評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,659評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,917評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,687評論 3 392
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,990評論 2 374

推薦閱讀更多精彩內容