]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
474a282298248da7343b2f4f6663dbc3d897d640
[signal-cli] / lib / src / main / java / org / asamk / signal / manager / helper / AccountHelper.java
1 package org.asamk.signal.manager.helper;
2
3 import org.asamk.signal.manager.api.CaptchaRequiredException;
4 import org.asamk.signal.manager.api.DeviceLinkUrl;
5 import org.asamk.signal.manager.api.IncorrectPinException;
6 import org.asamk.signal.manager.api.InvalidDeviceLinkException;
7 import org.asamk.signal.manager.api.NonNormalizedPhoneNumberException;
8 import org.asamk.signal.manager.api.PinLockedException;
9 import org.asamk.signal.manager.api.RateLimitException;
10 import org.asamk.signal.manager.api.VerificationMethodNotAvailableException;
11 import org.asamk.signal.manager.internal.SignalDependencies;
12 import org.asamk.signal.manager.jobs.SyncStorageJob;
13 import org.asamk.signal.manager.storage.SignalAccount;
14 import org.asamk.signal.manager.util.KeyUtils;
15 import org.asamk.signal.manager.util.NumberVerificationUtils;
16 import org.asamk.signal.manager.util.Utils;
17 import org.signal.core.util.Base64;
18 import org.signal.libsignal.protocol.IdentityKeyPair;
19 import org.signal.libsignal.protocol.InvalidKeyException;
20 import org.signal.libsignal.protocol.SignalProtocolAddress;
21 import org.signal.libsignal.protocol.state.KyberPreKeyRecord;
22 import org.signal.libsignal.protocol.state.SignedPreKeyRecord;
23 import org.signal.libsignal.protocol.util.KeyHelper;
24 import org.signal.libsignal.usernames.BaseUsernameException;
25 import org.signal.libsignal.usernames.Username;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
28 import org.whispersystems.signalservice.api.account.ChangePhoneNumberRequest;
29 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
30 import org.whispersystems.signalservice.api.push.ServiceId.ACI;
31 import org.whispersystems.signalservice.api.push.ServiceId.PNI;
32 import org.whispersystems.signalservice.api.push.ServiceIdType;
33 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
34 import org.whispersystems.signalservice.api.push.SignedPreKeyEntity;
35 import org.whispersystems.signalservice.api.push.exceptions.AlreadyVerifiedException;
36 import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
37 import org.whispersystems.signalservice.api.push.exceptions.DeprecatedVersionException;
38 import org.whispersystems.signalservice.api.push.exceptions.UsernameIsNotReservedException;
39 import org.whispersystems.signalservice.api.push.exceptions.UsernameMalformedException;
40 import org.whispersystems.signalservice.api.push.exceptions.UsernameTakenException;
41 import org.whispersystems.signalservice.api.util.DeviceNameUtil;
42 import org.whispersystems.signalservice.internal.push.DeviceLimitExceededException;
43 import org.whispersystems.signalservice.internal.push.KyberPreKeyEntity;
44 import org.whispersystems.signalservice.internal.push.OutgoingPushMessage;
45 import org.whispersystems.signalservice.internal.push.SyncMessage;
46 import org.whispersystems.signalservice.internal.push.exceptions.MismatchedDevicesException;
47
48 import java.io.IOException;
49 import java.util.ArrayList;
50 import java.util.HashMap;
51 import java.util.List;
52 import java.util.Objects;
53 import java.util.Optional;
54 import java.util.concurrent.TimeUnit;
55
56 import okio.ByteString;
57
58 import static org.asamk.signal.manager.config.ServiceConfig.PREKEY_MAXIMUM_ID;
59 import static org.whispersystems.signalservice.internal.util.Util.isEmpty;
60
61 public class AccountHelper {
62
63 private static final Logger logger = LoggerFactory.getLogger(AccountHelper.class);
64
65 private final Context context;
66 private final SignalAccount account;
67 private final SignalDependencies dependencies;
68
69 private Callable unregisteredListener;
70
71 public AccountHelper(final Context context) {
72 this.account = context.getAccount();
73 this.dependencies = context.getDependencies();
74 this.context = context;
75 }
76
77 public void setUnregisteredListener(final Callable unregisteredListener) {
78 this.unregisteredListener = unregisteredListener;
79 }
80
81 public void checkAccountState() throws IOException {
82 if (account.getLastReceiveTimestamp() == 0) {
83 logger.info("The Signal protocol expects that incoming messages are regularly received.");
84 } else {
85 var diffInMilliseconds = System.currentTimeMillis() - account.getLastReceiveTimestamp();
86 long days = TimeUnit.DAYS.convert(diffInMilliseconds, TimeUnit.MILLISECONDS);
87 if (days > 7) {
88 logger.warn(
89 "Messages have been last received {} days ago. The Signal protocol expects that incoming messages are regularly received.",
90 days);
91 }
92 }
93 try {
94 updateAccountAttributes();
95 if (account.getPreviousStorageVersion() < 9) {
96 context.getPreKeyHelper().forceRefreshPreKeys();
97 } else {
98 context.getPreKeyHelper().refreshPreKeysIfNecessary();
99 }
100 if (account.getAci() == null || account.getPni() == null) {
101 checkWhoAmiI();
102 }
103 if (!account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
104 context.getSyncHelper().requestSyncPniIdentity();
105 }
106 if (account.getPreviousStorageVersion() < 4
107 && account.isPrimaryDevice()
108 && account.getRegistrationLockPin() != null) {
109 migrateRegistrationPin();
110 }
111 if (account.getUsername() != null && account.getUsernameLink() == null) {
112 try {
113 tryToSetUsernameLink(new Username(account.getUsername()));
114 } catch (BaseUsernameException e) {
115 logger.debug("Invalid local username");
116 }
117 }
118 } catch (DeprecatedVersionException e) {
119 logger.debug("Signal-Server returned deprecated version exception", e);
120 throw e;
121 } catch (AuthorizationFailedException e) {
122 account.setRegistered(false);
123 throw e;
124 }
125 }
126
127 public void checkWhoAmiI() throws IOException {
128 final var whoAmI = dependencies.getAccountManager().getWhoAmI();
129 final var number = whoAmI.getNumber();
130 final var aci = ACI.parseOrThrow(whoAmI.getAci());
131 final var pni = PNI.parseOrThrow(whoAmI.getPni());
132 if (number.equals(account.getNumber()) && aci.equals(account.getAci()) && pni.equals(account.getPni())) {
133 return;
134 }
135
136 updateSelfIdentifiers(number, aci, pni);
137 }
138
139 private void updateSelfIdentifiers(final String number, final ACI aci, final PNI pni) {
140 account.setNumber(number);
141 account.setAci(aci);
142 account.setPni(pni);
143 if (account.isPrimaryDevice() && account.getPniIdentityKeyPair() == null) {
144 account.setPniIdentityKeyPair(KeyUtils.generateIdentityKeyPair());
145 }
146 account.getRecipientTrustedResolver().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
147 context.getUnidentifiedAccessHelper().rotateSenderCertificates();
148 dependencies.resetAfterAddressChange();
149 context.getGroupV2Helper().clearAuthCredentialCache();
150 context.getAccountFileUpdater().updateAccountIdentifiers(account.getNumber(), account.getAci());
151 context.getJobExecutor().enqueueJob(new SyncStorageJob());
152 }
153
154 public void setPni(
155 final PNI updatedPni,
156 final IdentityKeyPair pniIdentityKeyPair,
157 final String number,
158 final int localPniRegistrationId,
159 final SignedPreKeyRecord pniSignedPreKey,
160 final KyberPreKeyRecord lastResortKyberPreKey
161 ) throws IOException {
162 updateSelfIdentifiers(number != null ? number : account.getNumber(), account.getAci(), updatedPni);
163 account.setNewPniIdentity(pniIdentityKeyPair, pniSignedPreKey, lastResortKyberPreKey, localPniRegistrationId);
164 context.getPreKeyHelper().refreshPreKeysIfNecessary(ServiceIdType.PNI);
165 }
166
167 public void startChangeNumber(
168 String newNumber, boolean voiceVerification, String captcha
169 ) throws IOException, CaptchaRequiredException, NonNormalizedPhoneNumberException, RateLimitException, VerificationMethodNotAvailableException {
170 final var accountManager = dependencies.createUnauthenticatedAccountManager(newNumber, account.getPassword());
171 final var registrationApi = accountManager.getRegistrationApi();
172 String sessionId = NumberVerificationUtils.handleVerificationSession(registrationApi,
173 account.getSessionId(newNumber),
174 id -> account.setSessionId(newNumber, id),
175 voiceVerification,
176 captcha);
177 NumberVerificationUtils.requestVerificationCode(registrationApi, sessionId, voiceVerification);
178 }
179
180 public void finishChangeNumber(
181 String newNumber, String verificationCode, String pin
182 ) throws IncorrectPinException, PinLockedException, IOException {
183 for (var attempts = 0; attempts < 5; attempts++) {
184 try {
185 finishChangeNumberInternal(newNumber, verificationCode, pin);
186 break;
187 } catch (MismatchedDevicesException e) {
188 logger.debug("Change number failed with mismatched devices, retrying.");
189 try {
190 dependencies.getMessageSender().handleChangeNumberMismatchDevices(e.getMismatchedDevices());
191 } catch (UntrustedIdentityException ex) {
192 throw new AssertionError(ex);
193 }
194 }
195 }
196 }
197
198 private void finishChangeNumberInternal(
199 String newNumber, String verificationCode, String pin
200 ) throws IncorrectPinException, PinLockedException, IOException {
201 final var pniIdentity = KeyUtils.generateIdentityKeyPair();
202 final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
203 final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>();
204 final var devicePniLastResortKyberPreKeys = new HashMap<Integer, KyberPreKeyEntity>();
205 final var pniRegistrationIds = new HashMap<Integer, Integer>();
206
207 final var selfDeviceId = account.getDeviceId();
208 SyncMessage.PniChangeNumber selfChangeNumber = null;
209
210 final var deviceIds = new ArrayList<Integer>();
211 deviceIds.add(SignalServiceAddress.DEFAULT_DEVICE_ID);
212 final var aci = account.getAci();
213 final var accountDataStore = account.getSignalServiceDataStore().aci();
214 final var subDeviceSessions = accountDataStore.getSubDeviceSessions(aci.toString())
215 .stream()
216 .filter(deviceId -> accountDataStore.containsSession(new SignalProtocolAddress(aci.toString(),
217 deviceId)))
218 .toList();
219 deviceIds.addAll(subDeviceSessions);
220
221 final var messageSender = dependencies.getMessageSender();
222 for (final var deviceId : deviceIds) {
223 // Signed Prekey
224 final SignedPreKeyRecord signedPreKeyRecord;
225 try {
226 signedPreKeyRecord = KeyUtils.generateSignedPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
227 pniIdentity.getPrivateKey());
228 final var signedPreKeyEntity = new SignedPreKeyEntity(signedPreKeyRecord.getId(),
229 signedPreKeyRecord.getKeyPair().getPublicKey(),
230 signedPreKeyRecord.getSignature());
231 devicePniSignedPreKeys.put(deviceId, signedPreKeyEntity);
232 } catch (InvalidKeyException e) {
233 throw new AssertionError("unexpected invalid key", e);
234 }
235
236 // Last-resort kyber prekey
237 final KyberPreKeyRecord lastResortKyberPreKeyRecord;
238 try {
239 lastResortKyberPreKeyRecord = KeyUtils.generateKyberPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
240 pniIdentity.getPrivateKey());
241 final var kyberPreKeyEntity = new KyberPreKeyEntity(lastResortKyberPreKeyRecord.getId(),
242 lastResortKyberPreKeyRecord.getKeyPair().getPublicKey(),
243 lastResortKyberPreKeyRecord.getSignature());
244 devicePniLastResortKyberPreKeys.put(deviceId, kyberPreKeyEntity);
245 } catch (InvalidKeyException e) {
246 throw new AssertionError("unexpected invalid key", e);
247 }
248
249 // Registration Id
250 var pniRegistrationId = -1;
251 while (pniRegistrationId < 0 || pniRegistrationIds.containsValue(pniRegistrationId)) {
252 pniRegistrationId = KeyHelper.generateRegistrationId(false);
253 }
254 pniRegistrationIds.put(deviceId, pniRegistrationId);
255
256 // Device Message
257 final var pniChangeNumber = new SyncMessage.PniChangeNumber.Builder().identityKeyPair(ByteString.of(
258 pniIdentity.serialize()))
259 .signedPreKey(ByteString.of(signedPreKeyRecord.serialize()))
260 .lastResortKyberPreKey(ByteString.of(lastResortKyberPreKeyRecord.serialize()))
261 .registrationId(pniRegistrationId)
262 .newE164(newNumber)
263 .build();
264
265 if (deviceId == selfDeviceId) {
266 selfChangeNumber = pniChangeNumber;
267 } else {
268 try {
269 final var message = messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId,
270 pniChangeNumber);
271 encryptedDeviceMessages.add(message);
272 } catch (UntrustedIdentityException | IOException | InvalidKeyException e) {
273 throw new RuntimeException(e);
274 }
275 }
276 }
277
278 final var sessionId = account.getSessionId(newNumber);
279 final var result = NumberVerificationUtils.verifyNumber(sessionId,
280 verificationCode,
281 pin,
282 context.getPinHelper(),
283 (sessionId1, verificationCode1, registrationLock) -> {
284 final var registrationApi = dependencies.getRegistrationApi();
285 try {
286 Utils.handleResponseException(registrationApi.verifyAccount(sessionId1, verificationCode1));
287 } catch (AlreadyVerifiedException e) {
288 // Already verified so can continue changing number
289 }
290 return Utils.handleResponseException(registrationApi.changeNumber(new ChangePhoneNumberRequest(
291 sessionId1,
292 null,
293 newNumber,
294 registrationLock,
295 pniIdentity.getPublicKey(),
296 encryptedDeviceMessages,
297 Utils.mapKeys(devicePniSignedPreKeys, Object::toString),
298 Utils.mapKeys(devicePniLastResortKyberPreKeys, Object::toString),
299 Utils.mapKeys(pniRegistrationIds, Object::toString))));
300 });
301
302 final var updatePni = PNI.parseOrThrow(result.first().getPni());
303 if (updatePni.equals(account.getPni())) {
304 logger.debug("PNI is unchanged after change number");
305 return;
306 }
307
308 handlePniChangeNumberMessage(selfChangeNumber, updatePni);
309 }
310
311 public void handlePniChangeNumberMessage(
312 final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni
313 ) {
314 if (pniChangeNumber.identityKeyPair != null
315 && pniChangeNumber.registrationId != null
316 && pniChangeNumber.signedPreKey != null) {
317 logger.debug("New PNI: {}", updatedPni);
318 try {
319 setPni(updatedPni,
320 new IdentityKeyPair(pniChangeNumber.identityKeyPair.toByteArray()),
321 pniChangeNumber.newE164,
322 pniChangeNumber.registrationId,
323 new SignedPreKeyRecord(pniChangeNumber.signedPreKey.toByteArray()),
324 pniChangeNumber.lastResortKyberPreKey != null
325 ? new KyberPreKeyRecord(pniChangeNumber.lastResortKyberPreKey.toByteArray())
326 : null);
327 } catch (Exception e) {
328 logger.warn("Failed to handle change number message", e);
329 }
330 }
331 }
332
333 public static final int USERNAME_MIN_LENGTH = 3;
334 public static final int USERNAME_MAX_LENGTH = 32;
335
336 public void reserveUsernameFromNickname(String nickname) throws IOException, BaseUsernameException {
337 final var currentUsername = account.getUsername();
338 if (currentUsername != null) {
339 final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.'));
340 if (currentNickname.equals(nickname)) {
341 try {
342 refreshCurrentUsername();
343 return;
344 } catch (IOException | BaseUsernameException e) {
345 logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
346 }
347 }
348 }
349
350 final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH);
351 reserveUsername(candidates);
352 }
353
354 public void reserveExactUsername(String username) throws IOException, BaseUsernameException {
355 final var currentUsername = account.getUsername();
356 if (currentUsername != null) {
357 if (currentUsername.equals(username)) {
358 try {
359 refreshCurrentUsername();
360 return;
361 } catch (IOException | BaseUsernameException e) {
362 logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
363 }
364 }
365 }
366
367 final var candidates = List.of(new Username(username));
368 reserveUsername(candidates);
369 }
370
371 private void reserveUsername(final List<Username> candidates) throws IOException {
372 final var candidateHashes = new ArrayList<String>();
373 for (final var candidate : candidates) {
374 candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
375 }
376
377 final var response = dependencies.getAccountManager().reserveUsername(candidateHashes);
378 final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
379 if (hashIndex == -1) {
380 logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
381 throw new IOException("Unexpected username response");
382 }
383
384 logger.debug("[reserveUsername] Successfully reserved username.");
385 final var username = candidates.get(hashIndex);
386
387 final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username);
388 account.setUsername(username.getUsername());
389 account.setUsernameLink(linkComponents);
390 account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
391 account.getRecipientStore().rotateSelfStorageId();
392 logger.debug("[confirmUsername] Successfully confirmed username.");
393 }
394
395 public void refreshCurrentUsername() throws IOException, BaseUsernameException {
396 final var localUsername = account.getUsername();
397 if (localUsername == null) {
398 return;
399 }
400
401 final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI();
402 final var serverUsernameHash = whoAmIResponse.getUsernameHash();
403 final var hasServerUsername = !isEmpty(serverUsernameHash);
404 final var username = new Username(localUsername);
405 final var localUsernameHash = Base64.encodeUrlSafeWithoutPadding(username.getHash());
406
407 if (!hasServerUsername) {
408 logger.debug("No remote username is set.");
409 }
410
411 if (!Objects.equals(localUsernameHash, serverUsernameHash)) {
412 logger.debug("Local username hash does not match server username hash.");
413 }
414
415 if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) {
416 logger.debug("Attempting to resynchronize username.");
417 try {
418 tryReserveConfirmUsername(username);
419 } catch (UsernameMalformedException | UsernameTakenException | UsernameIsNotReservedException e) {
420 logger.debug("[confirmUsername] Failed to reserve confirm username: {} ({})",
421 e.getMessage(),
422 e.getClass().getSimpleName());
423 account.setUsername(null);
424 account.setUsernameLink(null);
425 account.getRecipientStore().rotateSelfStorageId();
426 throw e;
427 }
428 } else {
429 logger.debug("Username already set, not refreshing.");
430 }
431 }
432
433 private void tryReserveConfirmUsername(final Username username) throws IOException {
434 final var usernameLink = account.getUsernameLink();
435
436 if (usernameLink == null) {
437 dependencies.getAccountManager()
438 .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash())));
439 logger.debug("[reserveUsername] Successfully reserved existing username.");
440 final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username);
441 account.setUsernameLink(linkComponents);
442 logger.debug("[confirmUsername] Successfully confirmed existing username.");
443 } else {
444 final var linkComponents = dependencies.getAccountManager().reclaimUsernameAndLink(username, usernameLink);
445 account.setUsernameLink(linkComponents);
446 logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
447 }
448 account.getRecipientStore().rotateSelfStorageId();
449 }
450
451 private void tryToSetUsernameLink(Username username) {
452 for (var i = 1; i < 4; i++) {
453 try {
454 final var linkComponents = dependencies.getAccountManager().createUsernameLink(username);
455 account.setUsernameLink(linkComponents);
456 break;
457 } catch (IOException e) {
458 logger.debug("[tryToSetUsernameLink] Failed with IOException on attempt {}/3", i, e);
459 }
460 }
461 }
462
463 public void deleteUsername() throws IOException {
464 dependencies.getAccountManager().deleteUsernameLink();
465 account.setUsernameLink(null);
466 dependencies.getAccountManager().deleteUsername();
467 account.setUsername(null);
468 logger.debug("[deleteUsername] Successfully deleted the username.");
469 }
470
471 public void setDeviceName(String deviceName) {
472 final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
473 final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
474 account.setEncryptedDeviceName(encryptedDeviceName);
475 }
476
477 public void updateAccountAttributes() throws IOException {
478 dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
479 }
480
481 public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException, org.asamk.signal.manager.api.DeviceLimitExceededException {
482 String verificationCode;
483 try {
484 verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
485 } catch (DeviceLimitExceededException e) {
486 throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e);
487 }
488
489 try {
490 dependencies.getAccountManager()
491 .addDevice(deviceLinkInfo.deviceIdentifier(),
492 deviceLinkInfo.deviceKey(),
493 account.getAciIdentityKeyPair(),
494 account.getPniIdentityKeyPair(),
495 account.getProfileKey(),
496 account.getOrCreatePinMasterKey(),
497 verificationCode);
498 } catch (InvalidKeyException e) {
499 throw new InvalidDeviceLinkException("Invalid device link", e);
500 }
501 account.setMultiDevice(true);
502 context.getJobExecutor().enqueueJob(new SyncStorageJob());
503 }
504
505 public void removeLinkedDevices(int deviceId) throws IOException {
506 dependencies.getAccountManager().removeDevice(deviceId);
507 var devices = dependencies.getAccountManager().getDevices();
508 account.setMultiDevice(devices.size() > 1);
509 }
510
511 public void migrateRegistrationPin() throws IOException {
512 var masterKey = account.getOrCreatePinMasterKey();
513
514 context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
515 dependencies.getAccountManager().enableRegistrationLock(masterKey);
516 }
517
518 public void setRegistrationPin(String pin) throws IOException {
519 var masterKey = account.getOrCreatePinMasterKey();
520
521 context.getPinHelper().setRegistrationLockPin(pin, masterKey);
522 dependencies.getAccountManager().enableRegistrationLock(masterKey);
523
524 account.setRegistrationLockPin(pin);
525 updateAccountAttributes();
526 }
527
528 public void removeRegistrationPin() throws IOException {
529 // Remove KBS Pin
530 context.getPinHelper().removeRegistrationLockPin();
531 dependencies.getAccountManager().disableRegistrationLock();
532
533 account.setRegistrationLockPin(null);
534 }
535
536 public void unregister() throws IOException {
537 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
538 // If this is the primary device, other users can't send messages to this number anymore.
539 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
540 dependencies.getAccountManager().setGcmId(Optional.empty());
541
542 account.setRegistered(false);
543 unregisteredListener.call();
544 }
545
546 public void deleteAccount() throws IOException {
547 try {
548 context.getPinHelper().removeRegistrationLockPin();
549 } catch (IOException e) {
550 logger.warn("Failed to remove registration lock pin");
551 }
552 account.setRegistrationLockPin(null);
553
554 dependencies.getAccountManager().deleteAccount();
555
556 account.setRegistered(false);
557 unregisteredListener.call();
558 }
559
560 public interface Callable {
561
562 void call();
563 }
564 }