点击左下角的写短信按钮,进入的页面是NewConversationActivity(继承自ContactSelectionActivity),页面是ContactSelectionListFragment,数据是由ContactsCursorLoader 产生的。

Typing类型的消息

发送Typing类型消息的是TypingSendJob,也是加密的,对方收到后通过PushDecryptMessageJob解密。

第一次收到对方的消息

第一次收到对方发来的的消息时,在聊天页面底部会显示是否接受对方消息的提示框:

点击Accept:

D/RecipientDatabase: Attempting to mark RecipientId::2 with dirty state UPDATE
I/JobSchedulerScheduler: Scheduling a run in 0 ms.
I/Job: [JOB::8b334e47-aea5-4f58-8049-03bb240e2a4f][ProfileKeySendJob] onSubmit() (Time Since Submission: 2 ms, Lifespan: 86400000 ms, Run Attempt: 1/Unlimited)
I/JobRunner: [JOB::8b334e47-aea5-4f58-8049-03bb240e2a4f][ProfileKeySendJob][3] Running job. (Time Since Submission: 3 ms, Lifespan: 86400000 ms, Run Attempt: 1/Unlimited)
I/ConversationActivity: onModified(RecipientId::2) REGISTERED
I/ConversationActivity: updateDefaultSubscriptionId(null)
I/ConversationActivity: handleSecurityChange(true, false)
D/ConversationActivity: Setting link preview: false
I/ConversationActivity: Resolving registered state...
I/ConversationActivity: Checking through resolved recipient
I/ConversationActivity: Resolved registered state: REGISTERED
I/ConversationActivity: Returning registered state...
I/UnidentifiedAccessUtil: Their access key present? true | Our access key present? true | Our certificate present? true | UUID certificate supported? false
D/ConversationActivity: [presentMessageRequestDisplayState] DISPLAY_NONE
I/JobSchedulerScheduler: Scheduling a run in 0 ms.
I/ConversationActivity: onSecurityUpdated()
I/ConversationActivity: updateDefaultSubscriptionId(null)
D/JobSchedulerScheduler: onStartJob()
I/UnidentifiedAccessUtil: Their access key present? true | Our access key present? true | Our certificate present? true | UUID certificate supported? falseD/WebSocketConnection: onMessage() -- response received in 72 ms
D/SignalServiceMessageSender: [sendMessage] Completed over unidentified pipe in 123 ms and 1 attempt(s)
D/SignalServiceMessageSender: Completed send to 1 recipients in 123 ms
D/WebSocketConnection: onMessage() -- response received in 74 ms
D/SignalServiceMessageSender: [sendMessage] Completed over unidentified pipe in 111 ms and 1 attempt(s)D/WebSocketConnection: onMessage() -- incoming request
I/IncomingMessageObserver: Retrieved envelope! 1621312971191
D/IncomingMessageProcesso: Lock acquired by thread 574 (MessageRetrievalService)
I/IncomingMessageProcesso: Received message. Inserting in PushDatabase.
I/JobSchedulerScheduler: Scheduling a run in 0 ms.IncomingMessageObserver: Network requirement: true, app visible: true, gcm disabled: true
/IncomingMessageObserver: Reading message...
D/JobSchedulerScheduler: onStartJob()
I/PushDecryptMessageJob: handleMessage(), 收到消息...

重新登录账号的情况

1.账号A与账号B正常通信,如果账号B清除数据后(到设置页面清除app数据)重新登录(或者卸载重新安装,或者换个新设备登录这个账号),此时账号A不能再给账号B发送消息。账号A的聊天页面会显示如下的提示消息:

Your safety number with %s has changed.

即提示对方可能重新安装或者更换了新设备。

2.如果账号B退出登录后重新登录(没有到设置页面清除app数据)是正常的,不存在上述的情况。

上述情况1的发送消息失败时的调用栈:

2021-05-18 11:16:01.714 8223-8463/org.securesms D/PushServiceSocket: Opening URL: http://13.229.209.248:33451/v2/keys/d1c12141-cb99-4a16-b52c-f2238fc89d38/*
2021-05-18 11:16:01.714 8223-8463/org.securesms D/PushServiceSocket: Opening URL: <REDACTED>
2021-05-18 11:16:01.859 8223-8463/org.securesms I/PushServiceSocket: responseCode=413 ,responseMessage=Request Entity Too Large ,responseBody=okhttp3.internal.http.RealResponseBody@a4c58a6
2021-05-18 11:16:01.860 8223-8279/org.securesms W/BaseJob: [JOB::53df023d-dd05-47d1-ae44-a97a4124b6bd][TypingSendJob] Encountered a failing exception. (Time Since Submission: 150 ms, Lifespan: 5000 ms, Run Attempt: 1/1)java.io.IOException: java.util.concurrent.ExecutionException: org.whispersystems.signalservice.api.push.exceptions.RateLimitException: Rate limit exceeded: 413at org.whispersystems.signalservice.api.SignalServiceMessageSender.sendMessage(SignalServiceMessageSender.java:1254)at org.whispersystems.signalservice.api.SignalServiceMessageSender.sendTyping(SignalServiceMessageSender.java:223)at org.securesms.jobs.TypingSendJob.onRun(TypingSendJob.java:120)at org.securesms.jobs.BaseJob.run(BaseJob.java:25)at org.securesms.jobmanager.JobRunner.run(JobRunner.java:85)at org.securesms.jobmanager.JobRunner.run(JobRunner.java:48)Caused by: java.util.concurrent.ExecutionException: org.whispersystems.signalservice.api.push.exceptions.RateLimitException: Rate limit exceeded: 413at java.util.concurrent.FutureTask.report(FutureTask.java:123)at java.util.concurrent.FutureTask.get(FutureTask.java:193)at org.whispersystems.signalservice.api.SignalServiceMessageSender.sendMessage(SignalServiceMessageSender.java:1242)at org.whispersystems.signalservice.api.SignalServiceMessageSender.sendTyping(SignalServiceMessageSender.java:223) at org.securesms.jobs.TypingSendJob.onRun(TypingSendJob.java:120) at org.securesms.jobs.BaseJob.run(BaseJob.java:25) at org.securesms.jobmanager.JobRunner.run(JobRunner.java:85) at org.securesms.jobmanager.JobRunner.run(JobRunner.java:48) Caused by: org.whispersystems.signalservice.api.push.exceptions.RateLimitException: Rate limit exceeded: 413at org.whispersystems.signalservice.internal.push.PushServiceSocket.validateServiceResponse(PushServiceSocket.java:1481)at org.whispersystems.signalservice.internal.push.PushServiceSocket.makeServiceRequest(PushServiceSocket.java:1443)at org.whispersystems.signalservice.internal.push.PushServiceSocket.makeServiceBodyRequest(PushServiceSocket.java:1428)at org.whispersystems.signalservice.internal.push.PushServiceSocket.makeServiceRequest(PushServiceSocket.java:1327)at org.whispersystems.signalservice.internal.push.PushServiceSocket.makeServiceRequest(PushServiceSocket.java:1321)at org.whispersystems.signalservice.internal.push.PushServiceSocket.getPreKeys(PushServiceSocket.java:499)at org.whispersystems.signalservice.api.SignalServiceMessageSender.getEncryptedMessage(SignalServiceMessageSender.java:1456)at org.whispersystems.signalservice.api.SignalServiceMessageSender.getEncryptedMessages(SignalServiceMessageSender.java:1432)at org.whispersystems.signalservice.api.SignalServiceMessageSender.sendMessage(SignalServiceMessageSender.java:1281)at org.whispersystems.signalservice.api.SignalServiceMessageSender.lambda$sendMessage$2$SignalServiceMessageSender(SignalServiceMessageSender.java:1233)at org.whispersystems.signalservice.api.-$$Lambda$SignalServiceMessageSender$p90MRHuC544oIeAxTVmhsHU7uh8.call(Unknown Source:14)at java.util.concurrent.FutureTask.run(FutureTask.java:266)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)at java.lang.Thread.run(Thread.java:923)

当再次输入文字时会发送TypingSendJob,发送typing消息时会进行判断:

//TypingSendJob.java...@Overridepublic void onRun() throws Exception {...try {messageSender.sendTyping(addresses, unidentifiedAccess, typingMessage, this::isCanceled);} catch (CancelationException e) {Log.w(TAG, "Canceled during send!");}}
...
//SignalServiceMessageSender.javapublic void sendTyping(List<SignalServiceAddress>             recipients,List<Optional<UnidentifiedAccessPair>> unidentifiedAccess,SignalServiceTypingMessage             message,CancelationSignal                      cancelationSignal)throws IOException{byte[] content = createTypingContent(message);sendMessage(recipients, getTargetUnidentifiedAccess(unidentifiedAccess), message.getTimestamp(), content, true, cancelationSignal);}
//SignalServiceMessageSender.javaprivate OutgoingPushMessage getEncryptedMessage(PushServiceSocket            socket,SignalServiceAddress         recipient,Optional<UnidentifiedAccess> unidentifiedAccess,int                          deviceId,byte[]                       plaintext)throws IOException, InvalidKeyException, UntrustedIdentityException{SignalProtocolAddress signalProtocolAddress = new SignalProtocolAddress(recipient.getIdentifier(), deviceId);SignalServiceCipher   cipher                = new SignalServiceCipher(localAddress, store, null);/**Determine whether there is a committed SessionRecord for a recipientId + deviceId tuple.Parameters:address - the address of the remote client.Returns:true if a SessionRecord exists, false otherwise.*/if (!store.containsSession(signalProtocolAddress)) {try {List<PreKeyBundle> preKeys = socket.getPreKeys(recipient, unidentifiedAccess, deviceId);for (PreKeyBundle preKey : preKeys) {try {SignalProtocolAddress preKeyAddress  = new SignalProtocolAddress(recipient.getIdentifier(), preKey.getDeviceId());SessionBuilder        sessionBuilder = new SessionBuilder(store, preKeyAddress);sessionBuilder.process(preKey);} catch (org.whispersystems.libsignal.UntrustedIdentityException e) {throw new UntrustedIdentityException("Untrusted identity key!", recipient.getIdentifier(), preKey.getIdentityKey());}}...try {return cipher.encrypt(signalProtocolAddress, unidentifiedAccess, plaintext);} catch (org.whispersystems.libsignal.UntrustedIdentityException e) {throw new UntrustedIdentityException("Untrusted on send", recipient.getIdentifier(), e.getUntrustedIdentity());}}

也就是发送消息时,在加密发送的消息前,会先调用containsSession()方法判断对方是否是安全的会话,如果不是,则先调用getPreKeys()去向后台重新请求key, 然后对结果进行验证,验证通过后可以继续发送消息。

调用store.containsSession机芯判断,这个store是SignalProtocolStoreImpl

//SignalProtocolStoreImpl.java@Overridepublic boolean containsSession(SignalProtocolAddress axolotlAddress) {return sessionStore.containsSession(axolotlAddress);}

sessionStore是TextSecureSessionStore

//TextSecureSessionStore.java@Overridepublic boolean containsSession(SignalProtocolAddress address) {synchronized (FILE_LOCK) {if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {RecipientId   recipientId   = Recipient.external(context, address.getName()).getId();SessionRecord sessionRecord = DatabaseFactory.getSessionDatabase(context).load(recipientId, address.getDeviceId());return sessionRecord != null &&sessionRecord.getSessionState().hasSenderChain() &&sessionRecord.getSessionState().getSessionVersion() == CiphertextMessage.CURRENT_VERSION;} else {return false;}}}
//PushServiceSocket.javapublic List<PreKeyBundle> getPreKeys(SignalServiceAddress destination,Optional<UnidentifiedAccess> unidentifiedAccess,int deviceIdInteger)throws IOException{try {String deviceId = String.valueOf(deviceIdInteger);if (deviceId.equals("1"))deviceId = "*";String path = String.format(PREKEY_DEVICE_PATH, destination.getIdentifier(), deviceId);if (destination.getRelay().isPresent()) {path = path + "?relay=" + destination.getRelay().get();}String             responseText = makeServiceRequest(path, "GET", null, NO_HEADERS, unidentifiedAccess);...
//PushServiceSocket.javaprivate String makeServiceRequest(String urlFragment, String method, String jsonBody, Map<String, String> headers, Optional<UnidentifiedAccess> unidentifiedAccessKey)throws NonSuccessfulResponseCodeException, PushNetworkException{return makeServiceRequest(urlFragment, method, jsonBody, headers, NO_HANDLER, unidentifiedAccessKey);}
//PushServiceSocket.javaprivate String makeServiceRequest(String urlFragment, String method, String jsonBody, Map<String, String> headers, ResponseCodeHandler responseCodeHandler, Optional<UnidentifiedAccess> unidentifiedAccessKey)throws NonSuccessfulResponseCodeException, PushNetworkException{ResponseBody responseBody = makeServiceBodyRequest(urlFragment, method, jsonRequestBody(jsonBody), headers, responseCodeHandler, unidentifiedAccessKey);try {return responseBody.string();} catch (IOException e) {throw new PushNetworkException(e);}}
//PushServiceSocket.javaprivate ResponseBody makeServiceBodyRequest(String urlFragment,String method,RequestBody body,Map<String, String> headers,ResponseCodeHandler responseCodeHandler,Optional<UnidentifiedAccess> unidentifiedAccessKey)throws NonSuccessfulResponseCodeException, PushNetworkException{return makeServiceRequest(urlFragment, method, body, headers, responseCodeHandler, unidentifiedAccessKey).body();}
//PushServiceSocket.javaprivate Response makeServiceRequest(String urlFragment,String method,RequestBody body,Map<String, String> headers,ResponseCodeHandler responseCodeHandler,Optional<UnidentifiedAccess> unidentifiedAccessKey)throws NonSuccessfulResponseCodeException, PushNetworkException{Response response = getServiceConnection(urlFragment, method, body, headers, unidentifiedAccessKey);responseCodeHandler.handle(response.code());return validateServiceResponse(response);}
//PushServiceSocket.javaprivate Response validateServiceResponse(Response response) throws NonSuccessfulResponseCodeException, PushNetworkException {int          responseCode    = response.code();String       responseMessage = response.message();ResponseBody responseBody    = response.body();Log.i(TAG, "responseCode="+ responseCode + " ,responseMessage=" + responseMessage + " ,responseBody=" + responseBody.toString());switch (responseCode) {case 413:throw new RateLimitException("Rate limit exceeded: " + responseCode);...

返回的结果的状态码是413.

总结:
发送消息时与对方建立的会话会保存在sessions表中,操作类是SessionDatabase
下次发送消息时会从数据库中读取保存的会话数据,并判断本次的接收方是否已经在数据库中存在:

//TextSecureSessionStore.java@Overridepublic boolean containsSession(SignalProtocolAddress address) {synchronized (FILE_LOCK) {if (DatabaseFactory.getRecipientDatabase(context).containsPhoneOrUuid(address.getName())) {RecipientId   recipientId   = Recipient.external(context, address.getName()).getId();SessionRecord sessionRecord = DatabaseFactory.getSessionDatabase(context).load(recipientId, address.getDeviceId());return sessionRecord != null &&sessionRecord.getSessionState().hasSenderChain() &&sessionRecord.getSessionState().getSessionVersion() == CiphertextMessage.CURRENT_VERSION;} else {return false;}}}

SignalProtocolAddress存储的是uuid和设备id

Recipient转为SignalServiceAddress:

//PushTextSendJob.javaprivate boolean deliver(SmsMessageRecord message)throws UntrustedIdentityException, InsecureFallbackApprovalException, RetryLaterException{try {rotateSenderCertificateIfNecessary();Recipient                        messageRecipient   = message.getIndividualRecipient().fresh();SignalServiceMessageSender       messageSender      = ApplicationDependencies.getSignalServiceMessageSender();//Recipient转为SignalServiceAddressSignalServiceAddress             address            = getPushAddress(messageRecipient);...
//PushSendJob.javaprotected SignalServiceAddress getPushAddress(@NonNull Recipient recipient) {return RecipientUtil.toSignalServiceAddress(context, recipient);}
//RecipientUtil.java.../*** This method will do it's best to craft a fully-populated {@link SignalServiceAddress} based on* the provided recipient. This includes performing a possible network request if no UUID is* available.*/@WorkerThreadpublic static @NonNull SignalServiceAddress toSignalServiceAddress(@NonNull Context context, @NonNull Recipient recipient) {recipient = recipient.resolve();if (!recipient.getUuid().isPresent() && !recipient.getE164().isPresent()) {throw new AssertionError(recipient.getId() + " - No UUID or phone number!");}if (FeatureFlags.cds() && !recipient.getUuid().isPresent()) {Log.i(TAG, recipient.getId() + " is missing a UUID...");try {RegisteredState state = DirectoryHelper.refreshDirectoryFor(context, recipient, false);recipient = Recipient.resolved(recipient.getId());Log.i(TAG, "Successfully performed a UUID fetch for " + recipient.getId() + ". Registered: " + state);} catch (IOException e) {Log.w(TAG, "Failed to fetch a UUID for " + recipient.getId() + ". Scheduling a future fetch and building an address without one.");ApplicationDependencies.getJobManager().add(new DirectoryRefreshJob(recipient, false));}}return new SignalServiceAddress(Optional.fromNullable(recipient.getUuid().orNull()), Optional.fromNullable(recipient.resolve().getE164().orNull()));}...

MismatchedDevicesException和StaleDevicesException会调用TextSecureSessionStore的deleteSession()方法

uuid如何生成的?

同一个手机号,清除数据后或者卸载重装,或者在新设备登录后的uuid是一样的吗?

//TextSecurePreferences.javapublic static UUID getLocalUuid(Context context) {return UuidUtil.parseOrNull(getStringPreference(context, LOCAL_UUID_PREF, null));}public static void setLocalUuid(Context context, UUID uuid) {setStringPreference(context, LOCAL_UUID_PREF, uuid.toString());}
//CodeVerificationRequest.javaprivate static void verifyAccount(@NonNull Context context,@NonNull Credentials credentials,@NonNull String code,@Nullable String pin,@Nullable TokenResponse kbsTokenResponse,@Nullable String kbsStorageCredentials,@Nullable String fcmToken)throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException{...VerifyAccountResponse response = accountManager.verifyAccountWithCode(code,null,registrationId,!hasFcm,registrationLockV1,registrationLockV2,unidentifiedAccessKey,universalUnidentifiedAccess,AppCapabilities.getCapabilities(true));UUID    uuid   = UuidUtil.parseOrThrow(response.getUuid());...

输入验证码进行验证成功后,返回的结果就有uuid,所以uuid是服务端的校验验证码的接口生成并返回给客户端的,同一个手机号每次登陆时返回的uuid都是一样的。

SignalProtocolAddress

public class SignalProtocolAddress {private final String name;private final int    deviceId;...

name 是账户的uuid,deviceId就是设备id

SignalServiceAddress

/*** A class representing a message destination or origin.*/
public class SignalServiceAddress {public static final int DEFAULT_DEVICE_ID = 1;private final Optional<UUID>   uuid;private final Optional<String> e164;private final Optional<String> relay;...

登录账号时也会请求/v2/keys/这个接口


...D/PushServiceSocket: Opening URL: http://13.229.209.248:33451/v2/keys/
D/PushServiceSocket: Opening URL: <REDACTED>
I/PushServiceSocket: responseCode=204 ,responseMessage=No Content ,responseBody=okhttp3.internal.http.RealResponseBody@9b17db7...

调用栈:

 D/PushServiceSocket: Opening URL: http://13.229.209.248:33451/v2/keys/java.lang.Exceptionat org.whispersystems.signalservice.internal.push.PushServiceSocket.buildServiceRequest(PushServiceSocket.java:1634)at org.whispersystems.signalservice.internal.push.PushServiceSocket.getServiceConnection(PushServiceSocket.java:1558)at org.whispersystems.signalservice.internal.push.PushServiceSocket.makeServiceRequest(PushServiceSocket.java:1439)at org.whispersystems.signalservice.internal.push.PushServiceSocket.makeServiceBodyRequest(PushServiceSocket.java:1428)at org.whispersystems.signalservice.internal.push.PushServiceSocket.makeServiceRequest(PushServiceSocket.java:1327)at org.whispersystems.signalservice.internal.push.PushServiceSocket.makeServiceRequest(PushServiceSocket.java:1303)at org.whispersystems.signalservice.internal.push.PushServiceSocket.registerPreKeys(PushServiceSocket.java:471)at org.whispersystems.signalservice.api.SignalServiceAccountManager.setPreKeys(SignalServiceAccountManager.java:299)at org.securesms.registration.service.CodeVerificationRequest.verifyAccount(CodeVerificationRequest.java:233)at org.securesms.registration.service.CodeVerificationRequest.access$000(CodeVerificationRequest.java:51)at org.securesms.registration.service.CodeVerificationRequest$1.doInBackground(CodeVerificationRequest.java:95)at org.securesms.registration.service.CodeVerificationRequest$1.doInBackground(CodeVerificationRequest.java:83)at android.os.AsyncTask$3.call(AsyncTask.java:394)at java.util.concurrent.FutureTask.run(FutureTask.java:266)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)at java.lang.Thread.run(Thread.java:923)
//CodeVerificationRequest.javaprivate static void verifyAccount(@NonNull Context context,@NonNull Credentials credentials,@NonNull String code,@Nullable String pin,@Nullable TokenResponse kbsTokenResponse,@Nullable String kbsStorageCredentials,@Nullable String fcmToken)throws IOException, KeyBackupSystemWrongPinException, KeyBackupSystemNoDataException{boolean    isV2RegistrationLock        = kbsTokenResponse != null;int        registrationId              = KeyHelper.generateRegistrationId(false);boolean    universalUnidentifiedAccess = TextSecurePreferences.isUniversalUnidentifiedAccess(context);ProfileKey profileKey                  = findExistingProfileKey(context, credentials.getE164number());if (profileKey == null) {profileKey = ProfileKeyUtil.createNew();Log.i(TAG, "No profile key found, created a new one");}byte[] unidentifiedAccessKey = UnidentifiedAccess.deriveAccessKeyFrom(profileKey);TextSecurePreferences.setLocalRegistrationId(context, registrationId);SessionUtil.archiveAllSessions(context);SignalServiceAccountManager accountManager     = AccountManagerFactory.createUnauthenticated(context, credentials.getE164number(), credentials.getPassword());KbsPinData                  kbsData            = isV2RegistrationLock ? PinState.restoreMasterKey(pin, kbsStorageCredentials, kbsTokenResponse) : null;String                      registrationLockV2 = kbsData != null ? kbsData.getMasterKey().deriveRegistrationLock() : null;String                      registrationLockV1 = isV2RegistrationLock ? null : pin;boolean                     hasFcm             = fcmToken != null;Log.i(TAG, "Calling verifyAccountWithCode(): reglockV1? " + !TextUtils.isEmpty(registrationLockV1) + ", reglockV2? " + !TextUtils.isEmpty(registrationLockV2));//1. 向后台请求核对验证码VerifyAccountResponse response = accountManager.verifyAccountWithCode(code,null,registrationId,!hasFcm,registrationLockV1,registrationLockV2,unidentifiedAccessKey,universalUnidentifiedAccess,AppCapabilities.getCapabilities(true));UUID    uuid   = UuidUtil.parseOrThrow(response.getUuid());boolean hasPin = response.isStorageCapable();IdentityKeyPair    identityKey  = IdentityKeyUtil.getIdentityKeyPair(context);List<PreKeyRecord> records      = PreKeyUtil.generatePreKeys(context);SignedPreKeyRecord signedPreKey = PreKeyUtil.generateSignedPreKey(context, identityKey, true);accountManager = AccountManagerFactory.createAuthenticated(context, uuid, credentials.getE164number(), credentials.getPassword());//2.向后台注册一系列keyaccountManager.setPreKeys(identityKey.getPublicKey(), signedPreKey, records);if (hasFcm) {accountManager.setGcmId(Optional.fromNullable(fcmToken));}...

即登录账号时,校验完验证码之后向后台注册一系列key,看下这个方法:

//SignalServiceAccountManager.java/*** Register an identity key, signed prekey, and list of one time prekeys* with the server.** @param identityKey The client's long-term identity keypair.* @param signedPreKey The client's signed prekey.* @param oneTimePreKeys The client's list of one-time prekeys.** @throws IOException*/public void setPreKeys(IdentityKey identityKey, SignedPreKeyRecord signedPreKey, List<PreKeyRecord> oneTimePreKeys)throws IOException{this.pushServiceSocket.registerPreKeys(identityKey, signedPreKey, oneTimePreKeys);}

注册了三种不同的类型。

聊天(三)—清除数据后账号重登相关推荐

  1. 手机如何永久删除数据?清除数据后记得这样做

    现如今手机更新换代得很快,也有很多人为了追求新手机的新功能,会频繁更换手机,而在更换手机的时候,除了将旧手机的数据传输到新手机上,还有旧手机上的数据如何永久删除也是一个新的问题,手机如何永久删除数据? ...

  2. 清除应用数据后,应用对应的widget的数据无法刷新

    进入设置,清除应用的数据后,widget的内容不再更新,查找原因,发现清除数据后会杀死应用所有的服务,详细的过程可参考:http://blog.csdn.net/Gaugamela/article/d ...

  3. Android清除缓存、清除数据

    一.概念 清除数据.清除缓存的区别 清除数据主要是清除用户配置,比如SharedPreferences.数据库等等,这些数据都是在程序运行过程中保存的用户配置信息,清除数据后,下次进入程序就和第一次进 ...

  4. Android清除缓存,清除数据

    概念: 清除数据.清除缓存的区别 清除数据主要是清除用户配置,比如SharedPreferences,数据库等等,这些数据都是在程序运行过程中保存的用户配置信息,清除数据后,下次进入程序就和第一次进入 ...

  5. 使用代码实现Android的清除数据的功能

    清除数据 清除数据主要是清除用户配置,比如SharedPreferences.数据库等等,这些数据都是在程序运行过程中保存的用户配置信息,清除数据后,下次进入程序就和第一次进入程序时一样: 代码实现方 ...

  6. Android缓存处理和清除数据、清除缓存、一键清理的区别

      在Android设备中,我们经常会看到与系统或者应用相关的清除功能有:清除数据.清除缓存.一键清理,这么多清除功能对于一个程序猿就够难理解了,偏偏很多安卓设备上都有这些功能,对于用户来说就更难理解 ...

  7. 每日新闻:微软加入Linux相关专利池;英特尔发布Win10核显驱动;谷歌丑闻后推新品;马云重登中国首富;腾讯云举办智慧社区大会...

    关注中国软件网 最新鲜的企业级干货聚集地 今日热点 微软加入Linux相关专利池 有意停止对Android专利战 10月11日消息,在对谷歌基于Linux的Android系统发动多年的专利战之后,微软 ...

  8. 企业运维之域控篇(九)--辅助域强制占用后的操作--清除数据

    ----------------------------------------------------------- 企业运维之域控篇(九)--辅助域强制占用后的操作--清除数据  企业运维之域控篇 ...

  9. NC65 对上年度反结账,调整数据后重新结账后,对本年度年初重算时系统报错:更新记数错误。

    1.对上年度反结账,调整数据后重新结账后,对本年度年初重算时系统报错:更新记数错误. 解决方案: 1.在期初余额节点,按Ctrl+ALT+A重建期初凭证: 2.到结账节点,重建余额表,选择有问题的财务 ...

最新文章

  1. CSRF verification failed. Request aborted. 表单提交方法为POST时的报错
  2. php微信级联菜单,php微信公众号开发之二级菜单
  3. java file 如何关闭,java – 如何正确关闭从FileOutputStream获取的FileChannel
  4. 废弃电器电子产品回收:需要的不仅是补贴 !
  5. Android Studio 使用Lambda
  6. 实录分享 | 计算未来轻沙龙:图神经网络前沿研讨会
  7. 【学习】SpringBoot之自定义拦截器
  8. 丹佛大学计算机科学专业,丹佛大学
  9. ThinkPHP6项目基操(15.实战部分 阿里云短信redis)
  10. npp夜光数据介绍 viirs_优化的NPP夜光月度数据下载
  11. RESTful Webservice 和 SOAP Webserivce 对比及区别
  12. Oracle Siebel CRM技术的前景
  13. 多线程等待唤醒机制之生产消费者模式
  14. Python使用matplotlib可视化环形图
  15. 计算机丢失deferrd.dll怎么解决,被Defer后怎么办?如何在RD调整策略绝地反击?!...
  16. scrapy爬取晋江免费小说(章节)+ cookie爬vip章节
  17. Redis 学习 - hiredis(官网 2021-01-06)
  18. 成功解决h5py\_init_.py
  19. 生成式人工智能是否会是下一个风口?
  20. Python——时间与时间戳之间的转换

热门文章

  1. 华为P8正式发布 售价分别为549欧元和649欧元
  2. myatoi, mystcmp, mystrcasecmp,mystrncmp
  3. 面试官:如何防止你的 jar 包被反编译
  4. m8解锁后一键root,M87怎么解锁
  5. SSM+MyBatis-Plus+EasyExcel+腾讯云tianai滑动验证码接入项目搭建+简单实现增、删、改、查、导入、滑动验证码功能
  6. Th3.9:友元函数、友元类、友元成员函数详述
  7. qHD、HD、FHD、QHD、UHD的区别
  8. _wps_2020073016_wps 图表配置
  9. bootstrap table export插件导出pdf格式文件中文乱码问题解决办法
  10. 依赖倒置原则——面向对象设计原则