]> nmode's Git Repositories - signal-cli/blob - lib/src/main/java/org/asamk/signal/manager/helper/AccountHelper.java
Update libsignal-service-java
[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 String sessionId = NumberVerificationUtils.handleVerificationSession(accountManager,
172 account.getSessionId(newNumber),
173 id -> account.setSessionId(newNumber, id),
174 voiceVerification,
175 captcha);
176 NumberVerificationUtils.requestVerificationCode(accountManager, sessionId, voiceVerification);
177 }
178
179 public void finishChangeNumber(
180 String newNumber, String verificationCode, String pin
181 ) throws IncorrectPinException, PinLockedException, IOException {
182 for (var attempts = 0; attempts < 5; attempts++) {
183 try {
184 finishChangeNumberInternal(newNumber, verificationCode, pin);
185 break;
186 } catch (MismatchedDevicesException e) {
187 logger.debug("Change number failed with mismatched devices, retrying.");
188 try {
189 dependencies.getMessageSender().handleChangeNumberMismatchDevices(e.getMismatchedDevices());
190 } catch (UntrustedIdentityException ex) {
191 throw new AssertionError(ex);
192 }
193 }
194 }
195 }
196
197 private void finishChangeNumberInternal(
198 String newNumber, String verificationCode, String pin
199 ) throws IncorrectPinException, PinLockedException, IOException {
200 final var pniIdentity = KeyUtils.generateIdentityKeyPair();
201 final var encryptedDeviceMessages = new ArrayList<OutgoingPushMessage>();
202 final var devicePniSignedPreKeys = new HashMap<Integer, SignedPreKeyEntity>();
203 final var devicePniLastResortKyberPreKeys = new HashMap<Integer, KyberPreKeyEntity>();
204 final var pniRegistrationIds = new HashMap<Integer, Integer>();
205
206 final var selfDeviceId = account.getDeviceId();
207 SyncMessage.PniChangeNumber selfChangeNumber = null;
208
209 final var deviceIds = new ArrayList<Integer>();
210 deviceIds.add(SignalServiceAddress.DEFAULT_DEVICE_ID);
211 final var aci = account.getAci();
212 final var accountDataStore = account.getSignalServiceDataStore().aci();
213 final var subDeviceSessions = accountDataStore.getSubDeviceSessions(aci.toString())
214 .stream()
215 .filter(deviceId -> accountDataStore.containsSession(new SignalProtocolAddress(aci.toString(),
216 deviceId)))
217 .toList();
218 deviceIds.addAll(subDeviceSessions);
219
220 final var messageSender = dependencies.getMessageSender();
221 for (final var deviceId : deviceIds) {
222 // Signed Prekey
223 final SignedPreKeyRecord signedPreKeyRecord;
224 try {
225 signedPreKeyRecord = KeyUtils.generateSignedPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
226 pniIdentity.getPrivateKey());
227 final var signedPreKeyEntity = new SignedPreKeyEntity(signedPreKeyRecord.getId(),
228 signedPreKeyRecord.getKeyPair().getPublicKey(),
229 signedPreKeyRecord.getSignature());
230 devicePniSignedPreKeys.put(deviceId, signedPreKeyEntity);
231 } catch (InvalidKeyException e) {
232 throw new AssertionError("unexpected invalid key", e);
233 }
234
235 // Last-resort kyber prekey
236 final KyberPreKeyRecord lastResortKyberPreKeyRecord;
237 try {
238 lastResortKyberPreKeyRecord = KeyUtils.generateKyberPreKeyRecord(KeyUtils.getRandomInt(PREKEY_MAXIMUM_ID),
239 pniIdentity.getPrivateKey());
240 final var kyberPreKeyEntity = new KyberPreKeyEntity(lastResortKyberPreKeyRecord.getId(),
241 lastResortKyberPreKeyRecord.getKeyPair().getPublicKey(),
242 lastResortKyberPreKeyRecord.getSignature());
243 devicePniLastResortKyberPreKeys.put(deviceId, kyberPreKeyEntity);
244 } catch (InvalidKeyException e) {
245 throw new AssertionError("unexpected invalid key", e);
246 }
247
248 // Registration Id
249 var pniRegistrationId = -1;
250 while (pniRegistrationId < 0 || pniRegistrationIds.containsValue(pniRegistrationId)) {
251 pniRegistrationId = KeyHelper.generateRegistrationId(false);
252 }
253 pniRegistrationIds.put(deviceId, pniRegistrationId);
254
255 // Device Message
256 final var pniChangeNumber = new SyncMessage.PniChangeNumber.Builder().identityKeyPair(ByteString.of(
257 pniIdentity.serialize()))
258 .signedPreKey(ByteString.of(signedPreKeyRecord.serialize()))
259 .lastResortKyberPreKey(ByteString.of(lastResortKyberPreKeyRecord.serialize()))
260 .registrationId(pniRegistrationId)
261 .newE164(newNumber)
262 .build();
263
264 if (deviceId == selfDeviceId) {
265 selfChangeNumber = pniChangeNumber;
266 } else {
267 try {
268 final var message = messageSender.getEncryptedSyncPniInitializeDeviceMessage(deviceId,
269 pniChangeNumber);
270 encryptedDeviceMessages.add(message);
271 } catch (UntrustedIdentityException | IOException | InvalidKeyException e) {
272 throw new RuntimeException(e);
273 }
274 }
275 }
276
277 final var sessionId = account.getSessionId(newNumber);
278 final var result = NumberVerificationUtils.verifyNumber(sessionId,
279 verificationCode,
280 pin,
281 context.getPinHelper(),
282 (sessionId1, verificationCode1, registrationLock) -> {
283 final var accountManager = dependencies.getAccountManager();
284 try {
285 Utils.handleResponseException(accountManager.verifyAccount(verificationCode1, sessionId1));
286 } catch (AlreadyVerifiedException e) {
287 // Already verified so can continue changing number
288 }
289 return Utils.handleResponseException(accountManager.changeNumber(new ChangePhoneNumberRequest(
290 sessionId1,
291 null,
292 newNumber,
293 registrationLock,
294 pniIdentity.getPublicKey(),
295 encryptedDeviceMessages,
296 Utils.mapKeys(devicePniSignedPreKeys, Object::toString),
297 Utils.mapKeys(devicePniLastResortKyberPreKeys, Object::toString),
298 Utils.mapKeys(pniRegistrationIds, Object::toString))));
299 });
300
301 final var updatePni = PNI.parseOrThrow(result.first().getPni());
302 if (updatePni.equals(account.getPni())) {
303 logger.debug("PNI is unchanged after change number");
304 return;
305 }
306
307 handlePniChangeNumberMessage(selfChangeNumber, updatePni);
308 }
309
310 public void handlePniChangeNumberMessage(
311 final SyncMessage.PniChangeNumber pniChangeNumber, final PNI updatedPni
312 ) {
313 if (pniChangeNumber.identityKeyPair != null
314 && pniChangeNumber.registrationId != null
315 && pniChangeNumber.signedPreKey != null) {
316 logger.debug("New PNI: {}", updatedPni);
317 try {
318 setPni(updatedPni,
319 new IdentityKeyPair(pniChangeNumber.identityKeyPair.toByteArray()),
320 pniChangeNumber.newE164,
321 pniChangeNumber.registrationId,
322 new SignedPreKeyRecord(pniChangeNumber.signedPreKey.toByteArray()),
323 pniChangeNumber.lastResortKyberPreKey != null
324 ? new KyberPreKeyRecord(pniChangeNumber.lastResortKyberPreKey.toByteArray())
325 : null);
326 } catch (Exception e) {
327 logger.warn("Failed to handle change number message", e);
328 }
329 }
330 }
331
332 public static final int USERNAME_MIN_LENGTH = 3;
333 public static final int USERNAME_MAX_LENGTH = 32;
334
335 public void reserveUsernameFromNickname(String nickname) throws IOException, BaseUsernameException {
336 final var currentUsername = account.getUsername();
337 if (currentUsername != null) {
338 final var currentNickname = currentUsername.substring(0, currentUsername.indexOf('.'));
339 if (currentNickname.equals(nickname)) {
340 try {
341 refreshCurrentUsername();
342 return;
343 } catch (IOException | BaseUsernameException e) {
344 logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
345 }
346 }
347 }
348
349 final var candidates = Username.candidatesFrom(nickname, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH);
350 reserveUsername(candidates);
351 }
352
353 public void reserveExactUsername(String username) throws IOException, BaseUsernameException {
354 final var currentUsername = account.getUsername();
355 if (currentUsername != null) {
356 if (currentUsername.equals(username)) {
357 try {
358 refreshCurrentUsername();
359 return;
360 } catch (IOException | BaseUsernameException e) {
361 logger.warn("[reserveUsername] Failed to refresh current username, trying to claim new username");
362 }
363 }
364 }
365
366 final var candidates = List.of(new Username(username));
367 reserveUsername(candidates);
368 }
369
370 private void reserveUsername(final List<Username> candidates) throws IOException {
371 final var candidateHashes = new ArrayList<String>();
372 for (final var candidate : candidates) {
373 candidateHashes.add(Base64.encodeUrlSafeWithoutPadding(candidate.getHash()));
374 }
375
376 final var response = dependencies.getAccountManager().reserveUsername(candidateHashes);
377 final var hashIndex = candidateHashes.indexOf(response.getUsernameHash());
378 if (hashIndex == -1) {
379 logger.warn("[reserveUsername] The response hash could not be found in our set of candidateHashes.");
380 throw new IOException("Unexpected username response");
381 }
382
383 logger.debug("[reserveUsername] Successfully reserved username.");
384 final var username = candidates.get(hashIndex);
385
386 final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username);
387 account.setUsername(username.getUsername());
388 account.setUsernameLink(linkComponents);
389 account.getRecipientStore().resolveSelfRecipientTrusted(account.getSelfRecipientAddress());
390 account.getRecipientStore().rotateSelfStorageId();
391 logger.debug("[confirmUsername] Successfully confirmed username.");
392 }
393
394 public void refreshCurrentUsername() throws IOException, BaseUsernameException {
395 final var localUsername = account.getUsername();
396 if (localUsername == null) {
397 return;
398 }
399
400 final var whoAmIResponse = dependencies.getAccountManager().getWhoAmI();
401 final var serverUsernameHash = whoAmIResponse.getUsernameHash();
402 final var hasServerUsername = !isEmpty(serverUsernameHash);
403 final var username = new Username(localUsername);
404 final var localUsernameHash = Base64.encodeUrlSafeWithoutPadding(username.getHash());
405
406 if (!hasServerUsername) {
407 logger.debug("No remote username is set.");
408 }
409
410 if (!Objects.equals(localUsernameHash, serverUsernameHash)) {
411 logger.debug("Local username hash does not match server username hash.");
412 }
413
414 if (!hasServerUsername || !Objects.equals(localUsernameHash, serverUsernameHash)) {
415 logger.debug("Attempting to resynchronize username.");
416 try {
417 tryReserveConfirmUsername(username);
418 } catch (UsernameMalformedException | UsernameTakenException | UsernameIsNotReservedException e) {
419 logger.debug("[confirmUsername] Failed to reserve confirm username: {} ({})",
420 e.getMessage(),
421 e.getClass().getSimpleName());
422 account.setUsername(null);
423 account.setUsernameLink(null);
424 account.getRecipientStore().rotateSelfStorageId();
425 throw e;
426 }
427 } else {
428 logger.debug("Username already set, not refreshing.");
429 }
430 }
431
432 private void tryReserveConfirmUsername(final Username username) throws IOException {
433 final var usernameLink = account.getUsernameLink();
434
435 if (usernameLink == null) {
436 dependencies.getAccountManager()
437 .reserveUsername(List.of(Base64.encodeUrlSafeWithoutPadding(username.getHash())));
438 logger.debug("[reserveUsername] Successfully reserved existing username.");
439 final var linkComponents = dependencies.getAccountManager().confirmUsernameAndCreateNewLink(username);
440 account.setUsernameLink(linkComponents);
441 logger.debug("[confirmUsername] Successfully confirmed existing username.");
442 } else {
443 final var linkComponents = dependencies.getAccountManager().reclaimUsernameAndLink(username, usernameLink);
444 account.setUsernameLink(linkComponents);
445 logger.debug("[confirmUsername] Successfully reclaimed existing username and link.");
446 }
447 account.getRecipientStore().rotateSelfStorageId();
448 }
449
450 private void tryToSetUsernameLink(Username username) {
451 for (var i = 1; i < 4; i++) {
452 try {
453 final var linkComponents = dependencies.getAccountManager().createUsernameLink(username);
454 account.setUsernameLink(linkComponents);
455 break;
456 } catch (IOException e) {
457 logger.debug("[tryToSetUsernameLink] Failed with IOException on attempt {}/3", i, e);
458 }
459 }
460 }
461
462 public void deleteUsername() throws IOException {
463 dependencies.getAccountManager().deleteUsernameLink();
464 account.setUsernameLink(null);
465 dependencies.getAccountManager().deleteUsername();
466 account.setUsername(null);
467 logger.debug("[deleteUsername] Successfully deleted the username.");
468 }
469
470 public void setDeviceName(String deviceName) {
471 final var privateKey = account.getAciIdentityKeyPair().getPrivateKey();
472 final var encryptedDeviceName = DeviceNameUtil.encryptDeviceName(deviceName, privateKey);
473 account.setEncryptedDeviceName(encryptedDeviceName);
474 }
475
476 public void updateAccountAttributes() throws IOException {
477 dependencies.getAccountManager().setAccountAttributes(account.getAccountAttributes(null));
478 }
479
480 public void addDevice(DeviceLinkUrl deviceLinkInfo) throws IOException, InvalidDeviceLinkException, org.asamk.signal.manager.api.DeviceLimitExceededException {
481 String verificationCode;
482 try {
483 verificationCode = dependencies.getAccountManager().getNewDeviceVerificationCode();
484 } catch (DeviceLimitExceededException e) {
485 throw new org.asamk.signal.manager.api.DeviceLimitExceededException("Too many linked devices", e);
486 }
487
488 try {
489 dependencies.getAccountManager()
490 .addDevice(deviceLinkInfo.deviceIdentifier(),
491 deviceLinkInfo.deviceKey(),
492 account.getAciIdentityKeyPair(),
493 account.getPniIdentityKeyPair(),
494 account.getProfileKey(),
495 account.getOrCreatePinMasterKey(),
496 verificationCode);
497 } catch (InvalidKeyException e) {
498 throw new InvalidDeviceLinkException("Invalid device link", e);
499 }
500 account.setMultiDevice(true);
501 context.getJobExecutor().enqueueJob(new SyncStorageJob());
502 }
503
504 public void removeLinkedDevices(int deviceId) throws IOException {
505 dependencies.getAccountManager().removeDevice(deviceId);
506 var devices = dependencies.getAccountManager().getDevices();
507 account.setMultiDevice(devices.size() > 1);
508 }
509
510 public void migrateRegistrationPin() throws IOException {
511 var masterKey = account.getOrCreatePinMasterKey();
512
513 context.getPinHelper().migrateRegistrationLockPin(account.getRegistrationLockPin(), masterKey);
514 dependencies.getAccountManager().enableRegistrationLock(masterKey);
515 }
516
517 public void setRegistrationPin(String pin) throws IOException {
518 var masterKey = account.getOrCreatePinMasterKey();
519
520 context.getPinHelper().setRegistrationLockPin(pin, masterKey);
521 dependencies.getAccountManager().enableRegistrationLock(masterKey);
522
523 account.setRegistrationLockPin(pin);
524 updateAccountAttributes();
525 }
526
527 public void removeRegistrationPin() throws IOException {
528 // Remove KBS Pin
529 context.getPinHelper().removeRegistrationLockPin();
530 dependencies.getAccountManager().disableRegistrationLock();
531
532 account.setRegistrationLockPin(null);
533 }
534
535 public void unregister() throws IOException {
536 // When setting an empty GCM id, the Signal-Server also sets the fetchesMessages property to false.
537 // If this is the primary device, other users can't send messages to this number anymore.
538 // If this is a linked device, other users can still send messages, but this device doesn't receive them anymore.
539 dependencies.getAccountManager().setGcmId(Optional.empty());
540
541 account.setRegistered(false);
542 unregisteredListener.call();
543 }
544
545 public void deleteAccount() throws IOException {
546 try {
547 context.getPinHelper().removeRegistrationLockPin();
548 } catch (IOException e) {
549 logger.warn("Failed to remove registration lock pin");
550 }
551 account.setRegistrationLockPin(null);
552
553 dependencies.getAccountManager().deleteAccount();
554
555 account.setRegistered(false);
556 unregisteredListener.call();
557 }
558
559 public interface Callable {
560
561 void call();
562 }
563 }