]> nmode's Git Repositories - signal-cli/blob - src/main/java/org/asamk/signal/Manager.java
d626ccaf4c583a33b33453e96ed81a948c0848f0
[signal-cli] / src / main / java / org / asamk / signal / Manager.java
1 /**
2 * Copyright (C) 2015 AsamK
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17 package org.asamk.signal;
18
19 import com.fasterxml.jackson.annotation.JsonAutoDetect;
20 import com.fasterxml.jackson.annotation.PropertyAccessor;
21 import com.fasterxml.jackson.databind.DeserializationFeature;
22 import com.fasterxml.jackson.databind.JsonNode;
23 import com.fasterxml.jackson.databind.ObjectMapper;
24 import com.fasterxml.jackson.databind.SerializationFeature;
25 import com.fasterxml.jackson.databind.node.ObjectNode;
26 import org.asamk.Signal;
27 import org.whispersystems.libsignal.*;
28 import org.whispersystems.libsignal.ecc.Curve;
29 import org.whispersystems.libsignal.ecc.ECKeyPair;
30 import org.whispersystems.libsignal.state.PreKeyRecord;
31 import org.whispersystems.libsignal.state.SignalProtocolStore;
32 import org.whispersystems.libsignal.state.SignedPreKeyRecord;
33 import org.whispersystems.libsignal.util.KeyHelper;
34 import org.whispersystems.libsignal.util.Medium;
35 import org.whispersystems.libsignal.util.guava.Optional;
36 import org.whispersystems.signalservice.api.SignalServiceAccountManager;
37 import org.whispersystems.signalservice.api.SignalServiceMessagePipe;
38 import org.whispersystems.signalservice.api.SignalServiceMessageReceiver;
39 import org.whispersystems.signalservice.api.SignalServiceMessageSender;
40 import org.whispersystems.signalservice.api.crypto.*;
41 import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
42 import org.whispersystems.signalservice.api.messages.*;
43 import org.whispersystems.signalservice.api.messages.multidevice.SignalServiceSyncMessage;
44 import org.whispersystems.signalservice.api.push.SignalServiceAddress;
45 import org.whispersystems.signalservice.api.push.TrustStore;
46 import org.whispersystems.signalservice.api.push.exceptions.AuthorizationFailedException;
47 import org.whispersystems.signalservice.api.push.exceptions.EncapsulatedExceptions;
48 import org.whispersystems.signalservice.api.util.InvalidNumberException;
49 import org.whispersystems.signalservice.api.util.PhoneNumberFormatter;
50
51 import java.io.*;
52 import java.nio.file.Files;
53 import java.nio.file.Paths;
54 import java.util.*;
55 import java.util.concurrent.TimeUnit;
56 import java.util.concurrent.TimeoutException;
57
58 class Manager implements Signal {
59 private final static String URL = "https://textsecure-service.whispersystems.org";
60 private final static TrustStore TRUST_STORE = new WhisperTrustStore();
61
62 public final static String PROJECT_NAME = Manager.class.getPackage().getImplementationTitle();
63 public final static String PROJECT_VERSION = Manager.class.getPackage().getImplementationVersion();
64 private final static String USER_AGENT = PROJECT_NAME == null ? null : PROJECT_NAME + " " + PROJECT_VERSION;
65
66 private final static int PREKEY_MINIMUM_COUNT = 20;
67 private static final int PREKEY_BATCH_SIZE = 100;
68
69 private final String settingsPath;
70 private final String dataPath;
71 private final String attachmentsPath;
72
73 private final ObjectMapper jsonProcessot = new ObjectMapper();
74 private String username;
75 private String password;
76 private String signalingKey;
77 private int preKeyIdOffset;
78 private int nextSignedPreKeyId;
79
80 private boolean registered = false;
81
82 private SignalProtocolStore signalProtocolStore;
83 private SignalServiceAccountManager accountManager;
84 private JsonGroupStore groupStore;
85
86 public Manager(String username, String settingsPath) {
87 this.username = username;
88 this.settingsPath = settingsPath;
89 this.dataPath = this.settingsPath + "/data";
90 this.attachmentsPath = this.settingsPath + "/attachments";
91
92 jsonProcessot.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.NONE); // disable autodetect
93 jsonProcessot.enable(SerializationFeature.INDENT_OUTPUT); // for pretty print, you can disable it.
94 jsonProcessot.enable(SerializationFeature.WRITE_NULL_MAP_VALUES);
95 jsonProcessot.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
96 }
97
98 public String getFileName() {
99 new File(dataPath).mkdirs();
100 return dataPath + "/" + username;
101 }
102
103 public boolean userExists() {
104 File f = new File(getFileName());
105 return !(!f.exists() || f.isDirectory());
106 }
107
108 public boolean userHasKeys() {
109 return signalProtocolStore != null;
110 }
111
112 private JsonNode getNotNullNode(JsonNode parent, String name) throws InvalidObjectException {
113 JsonNode node = parent.get(name);
114 if (node == null) {
115 throw new InvalidObjectException(String.format("Incorrect file format: expected parameter %s not found ", name));
116 }
117
118 return node;
119 }
120
121 public void load() throws IOException, InvalidKeyException {
122 JsonNode rootNode = jsonProcessot.readTree(new File(getFileName()));
123
124 username = getNotNullNode(rootNode, "username").asText();
125 password = getNotNullNode(rootNode, "password").asText();
126 if (rootNode.has("signalingKey")) {
127 signalingKey = getNotNullNode(rootNode, "signalingKey").asText();
128 }
129 if (rootNode.has("preKeyIdOffset")) {
130 preKeyIdOffset = getNotNullNode(rootNode, "preKeyIdOffset").asInt(0);
131 } else {
132 preKeyIdOffset = 0;
133 }
134 if (rootNode.has("nextSignedPreKeyId")) {
135 nextSignedPreKeyId = getNotNullNode(rootNode, "nextSignedPreKeyId").asInt();
136 } else {
137 nextSignedPreKeyId = 0;
138 }
139 signalProtocolStore = jsonProcessot.convertValue(getNotNullNode(rootNode, "axolotlStore"), JsonSignalProtocolStore.class);
140 registered = getNotNullNode(rootNode, "registered").asBoolean();
141 JsonNode groupStoreNode = rootNode.get("groupStore");
142 if (groupStoreNode != null) {
143 groupStore = jsonProcessot.convertValue(groupStoreNode, JsonGroupStore.class);
144 }
145 if (groupStore == null) {
146 groupStore = new JsonGroupStore();
147 }
148 accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
149 try {
150 if (registered && accountManager.getPreKeysCount() < PREKEY_MINIMUM_COUNT) {
151 refreshPreKeys();
152 save();
153 }
154 } catch (AuthorizationFailedException e) {
155 System.err.println("Authorization failed, was the number registered elsewhere?");
156 }
157 }
158
159 private void save() {
160 ObjectNode rootNode = jsonProcessot.createObjectNode();
161 rootNode.put("username", username)
162 .put("password", password)
163 .put("signalingKey", signalingKey)
164 .put("preKeyIdOffset", preKeyIdOffset)
165 .put("nextSignedPreKeyId", nextSignedPreKeyId)
166 .put("registered", registered)
167 .putPOJO("axolotlStore", signalProtocolStore)
168 .putPOJO("groupStore", groupStore)
169 ;
170 try {
171 jsonProcessot.writeValue(new File(getFileName()), rootNode);
172 } catch (Exception e) {
173 System.err.println(String.format("Error saving file: %s", e.getMessage()));
174 }
175 }
176
177 public void createNewIdentity() {
178 IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair();
179 int registrationId = KeyHelper.generateRegistrationId(false);
180 signalProtocolStore = new JsonSignalProtocolStore(identityKey, registrationId);
181 groupStore = new JsonGroupStore();
182 registered = false;
183 save();
184 }
185
186 public boolean isRegistered() {
187 return registered;
188 }
189
190 public void register(boolean voiceVerication) throws IOException {
191 password = Util.getSecret(18);
192
193 accountManager = new SignalServiceAccountManager(URL, TRUST_STORE, username, password, USER_AGENT);
194
195 if (voiceVerication)
196 accountManager.requestVoiceVerificationCode();
197 else
198 accountManager.requestSmsVerificationCode();
199
200 registered = false;
201 save();
202 }
203
204 private List<PreKeyRecord> generatePreKeys() {
205 List<PreKeyRecord> records = new LinkedList<>();
206
207 for (int i = 0; i < PREKEY_BATCH_SIZE; i++) {
208 int preKeyId = (preKeyIdOffset + i) % Medium.MAX_VALUE;
209 ECKeyPair keyPair = Curve.generateKeyPair();
210 PreKeyRecord record = new PreKeyRecord(preKeyId, keyPair);
211
212 signalProtocolStore.storePreKey(preKeyId, record);
213 records.add(record);
214 }
215
216 preKeyIdOffset = (preKeyIdOffset + PREKEY_BATCH_SIZE + 1) % Medium.MAX_VALUE;
217 save();
218
219 return records;
220 }
221
222 private PreKeyRecord getOrGenerateLastResortPreKey() {
223 if (signalProtocolStore.containsPreKey(Medium.MAX_VALUE)) {
224 try {
225 return signalProtocolStore.loadPreKey(Medium.MAX_VALUE);
226 } catch (InvalidKeyIdException e) {
227 signalProtocolStore.removePreKey(Medium.MAX_VALUE);
228 }
229 }
230
231 ECKeyPair keyPair = Curve.generateKeyPair();
232 PreKeyRecord record = new PreKeyRecord(Medium.MAX_VALUE, keyPair);
233
234 signalProtocolStore.storePreKey(Medium.MAX_VALUE, record);
235 save();
236
237 return record;
238 }
239
240 private SignedPreKeyRecord generateSignedPreKey(IdentityKeyPair identityKeyPair) {
241 try {
242 ECKeyPair keyPair = Curve.generateKeyPair();
243 byte[] signature = Curve.calculateSignature(identityKeyPair.getPrivateKey(), keyPair.getPublicKey().serialize());
244 SignedPreKeyRecord record = new SignedPreKeyRecord(nextSignedPreKeyId, System.currentTimeMillis(), keyPair, signature);
245
246 signalProtocolStore.storeSignedPreKey(nextSignedPreKeyId, record);
247 nextSignedPreKeyId = (nextSignedPreKeyId + 1) % Medium.MAX_VALUE;
248 save();
249
250 return record;
251 } catch (InvalidKeyException e) {
252 throw new AssertionError(e);
253 }
254 }
255
256 public void verifyAccount(String verificationCode) throws IOException {
257 verificationCode = verificationCode.replace("-", "");
258 signalingKey = Util.getSecret(52);
259 accountManager.verifyAccountWithCode(verificationCode, signalingKey, signalProtocolStore.getLocalRegistrationId(), false, true);
260
261 //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID)));
262 registered = true;
263
264 refreshPreKeys();
265 save();
266 }
267
268 private void refreshPreKeys() throws IOException {
269 List<PreKeyRecord> oneTimePreKeys = generatePreKeys();
270 PreKeyRecord lastResortKey = getOrGenerateLastResortPreKey();
271 SignedPreKeyRecord signedPreKeyRecord = generateSignedPreKey(signalProtocolStore.getIdentityKeyPair());
272
273 accountManager.setPreKeys(signalProtocolStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys);
274 }
275
276
277 private static List<SignalServiceAttachment> getSignalServiceAttachments(List<String> attachments) throws AttachmentInvalidException {
278 List<SignalServiceAttachment> SignalServiceAttachments = null;
279 if (attachments != null) {
280 SignalServiceAttachments = new ArrayList<>(attachments.size());
281 for (String attachment : attachments) {
282 try {
283 SignalServiceAttachments.add(createAttachment(attachment));
284 } catch (IOException e) {
285 throw new AttachmentInvalidException(attachment, e);
286 }
287 }
288 }
289 return SignalServiceAttachments;
290 }
291
292 private static SignalServiceAttachmentStream createAttachment(String attachment) throws IOException {
293 File attachmentFile = new File(attachment);
294 InputStream attachmentStream = new FileInputStream(attachmentFile);
295 final long attachmentSize = attachmentFile.length();
296 String mime = Files.probeContentType(Paths.get(attachment));
297 return new SignalServiceAttachmentStream(attachmentStream, mime, attachmentSize, null);
298 }
299
300 @Override
301 public void sendGroupMessage(String messageText, List<String> attachments,
302 byte[] groupId)
303 throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException {
304 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
305 if (attachments != null) {
306 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
307 }
308 if (groupId != null) {
309 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.DELIVER)
310 .withId(groupId)
311 .build();
312 messageBuilder.asGroupMessage(group);
313 }
314 SignalServiceDataMessage message = messageBuilder.build();
315
316 sendMessage(message, groupStore.getGroup(groupId).members);
317 }
318
319 public void sendQuitGroupMessage(byte[] groupId) throws GroupNotFoundException, IOException, EncapsulatedExceptions, UntrustedIdentityException {
320 SignalServiceGroup group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.QUIT)
321 .withId(groupId)
322 .build();
323
324 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
325 .asGroupMessage(group)
326 .build();
327
328 sendMessage(message, groupStore.getGroup(groupId).members);
329 }
330
331 public byte[] sendUpdateGroupMessage(byte[] groupId, String name, Collection<String> members, String avatarFile) throws IOException, EncapsulatedExceptions, GroupNotFoundException, AttachmentInvalidException, UntrustedIdentityException {
332 GroupInfo g;
333 if (groupId == null) {
334 // Create new group
335 g = new GroupInfo(Util.getSecretBytes(16));
336 g.members.add(username);
337 } else {
338 g = groupStore.getGroup(groupId);
339 }
340
341 if (name != null) {
342 g.name = name;
343 }
344
345 if (members != null) {
346 for (String member : members) {
347 try {
348 g.members.add(canonicalizeNumber(member));
349 } catch (InvalidNumberException e) {
350 System.err.println("Failed to add member \"" + member + "\" to group: " + e.getMessage());
351 System.err.println("Aborting…");
352 System.exit(1);
353 }
354 }
355 }
356
357 SignalServiceGroup.Builder group = SignalServiceGroup.newBuilder(SignalServiceGroup.Type.UPDATE)
358 .withId(g.groupId)
359 .withName(g.name)
360 .withMembers(new ArrayList<>(g.members));
361
362 if (avatarFile != null) {
363 try {
364 group.withAvatar(createAttachment(avatarFile));
365 // TODO
366 g.avatarId = 0;
367 } catch (IOException e) {
368 throw new AttachmentInvalidException(avatarFile, e);
369 }
370 }
371
372 groupStore.updateGroup(g);
373
374 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
375 .asGroupMessage(group.build())
376 .build();
377
378 sendMessage(message, g.members);
379 return g.groupId;
380 }
381
382 @Override
383 public void sendMessage(String message, List<String> attachments, String recipient)
384 throws EncapsulatedExceptions, AttachmentInvalidException, IOException, UntrustedIdentityException {
385 List<String> recipients = new ArrayList<>(1);
386 recipients.add(recipient);
387 sendMessage(message, attachments, recipients);
388 }
389
390 @Override
391 public void sendMessage(String messageText, List<String> attachments,
392 List<String> recipients)
393 throws IOException, EncapsulatedExceptions, AttachmentInvalidException, UntrustedIdentityException {
394 final SignalServiceDataMessage.Builder messageBuilder = SignalServiceDataMessage.newBuilder().withBody(messageText);
395 if (attachments != null) {
396 messageBuilder.withAttachments(getSignalServiceAttachments(attachments));
397 }
398 SignalServiceDataMessage message = messageBuilder.build();
399
400 sendMessage(message, recipients);
401 }
402
403 @Override
404 public void sendEndSessionMessage(List<String> recipients) throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
405 SignalServiceDataMessage message = SignalServiceDataMessage.newBuilder()
406 .asEndSessionMessage()
407 .build();
408
409 sendMessage(message, recipients);
410 }
411
412 private void sendMessage(SignalServiceDataMessage message, Collection<String> recipients)
413 throws IOException, EncapsulatedExceptions, UntrustedIdentityException {
414 SignalServiceMessageSender messageSender = new SignalServiceMessageSender(URL, TRUST_STORE, username, password,
415 signalProtocolStore, USER_AGENT, Optional.<SignalServiceMessageSender.EventListener>absent());
416
417 Set<SignalServiceAddress> recipientsTS = new HashSet<>(recipients.size());
418 for (String recipient : recipients) {
419 try {
420 recipientsTS.add(getPushAddress(recipient));
421 } catch (InvalidNumberException e) {
422 System.err.println("Failed to add recipient \"" + recipient + "\": " + e.getMessage());
423 System.err.println("Aborting sending.");
424 save();
425 return;
426 }
427 }
428
429 if (message.getGroupInfo().isPresent()) {
430 messageSender.sendMessage(new ArrayList<>(recipientsTS), message);
431 } else {
432 // Send to all individually, so sync messages are sent correctly
433 for (SignalServiceAddress address : recipientsTS) {
434 messageSender.sendMessage(address, message);
435 }
436 }
437
438 if (message.isEndSession()) {
439 for (SignalServiceAddress recipient : recipientsTS) {
440 handleEndSession(recipient.getNumber());
441 }
442 }
443 save();
444 }
445
446 private SignalServiceContent decryptMessage(SignalServiceEnvelope envelope) {
447 SignalServiceCipher cipher = new SignalServiceCipher(new SignalServiceAddress(username), signalProtocolStore);
448 try {
449 return cipher.decrypt(envelope);
450 } catch (Exception e) {
451 // TODO handle all exceptions
452 e.printStackTrace();
453 return null;
454 }
455 }
456
457 private void handleEndSession(String source) {
458 signalProtocolStore.deleteAllSessions(source);
459 }
460
461 public interface ReceiveMessageHandler {
462 void handleMessage(SignalServiceEnvelope envelope, SignalServiceContent decryptedContent, GroupInfo group);
463 }
464
465 private GroupInfo handleSignalServiceDataMessage(SignalServiceDataMessage message, boolean isSync, String source, String destination) {
466 GroupInfo group = null;
467 if (message.getGroupInfo().isPresent()) {
468 SignalServiceGroup groupInfo = message.getGroupInfo().get();
469 switch (groupInfo.getType()) {
470 case UPDATE:
471 try {
472 group = groupStore.getGroup(groupInfo.getGroupId());
473 } catch (GroupNotFoundException e) {
474 group = new GroupInfo(groupInfo.getGroupId());
475 }
476
477 if (groupInfo.getAvatar().isPresent()) {
478 SignalServiceAttachment avatar = groupInfo.getAvatar().get();
479 if (avatar.isPointer()) {
480 long avatarId = avatar.asPointer().getId();
481 try {
482 retrieveAttachment(avatar.asPointer());
483 group.avatarId = avatarId;
484 } catch (IOException | InvalidMessageException e) {
485 System.err.println("Failed to retrieve group avatar (" + avatarId + "): " + e.getMessage());
486 }
487 }
488 }
489
490 if (groupInfo.getName().isPresent()) {
491 group.name = groupInfo.getName().get();
492 }
493
494 if (groupInfo.getMembers().isPresent()) {
495 group.members.addAll(groupInfo.getMembers().get());
496 }
497
498 groupStore.updateGroup(group);
499 break;
500 case DELIVER:
501 try {
502 group = groupStore.getGroup(groupInfo.getGroupId());
503 } catch (GroupNotFoundException e) {
504 }
505 break;
506 case QUIT:
507 try {
508 group = groupStore.getGroup(groupInfo.getGroupId());
509 group.members.remove(source);
510 } catch (GroupNotFoundException e) {
511 }
512 break;
513 }
514 }
515 if (message.isEndSession()) {
516 handleEndSession(isSync ? destination : source);
517 }
518 if (message.getAttachments().isPresent()) {
519 for (SignalServiceAttachment attachment : message.getAttachments().get()) {
520 if (attachment.isPointer()) {
521 try {
522 retrieveAttachment(attachment.asPointer());
523 } catch (IOException | InvalidMessageException e) {
524 System.err.println("Failed to retrieve attachment (" + attachment.asPointer().getId() + "): " + e.getMessage());
525 }
526 }
527 }
528 }
529 return group;
530 }
531
532 public void receiveMessages(int timeoutSeconds, boolean returnOnTimeout, ReceiveMessageHandler handler) throws IOException {
533 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT);
534 SignalServiceMessagePipe messagePipe = null;
535
536 try {
537 messagePipe = messageReceiver.createMessagePipe();
538
539 while (true) {
540 SignalServiceEnvelope envelope;
541 SignalServiceContent content = null;
542 GroupInfo group = null;
543 try {
544 envelope = messagePipe.read(timeoutSeconds, TimeUnit.SECONDS);
545 if (!envelope.isReceipt()) {
546 content = decryptMessage(envelope);
547 if (content != null) {
548 if (content.getDataMessage().isPresent()) {
549 SignalServiceDataMessage message = content.getDataMessage().get();
550 group = handleSignalServiceDataMessage(message, false, envelope.getSource(), username);
551 }
552 if (content.getSyncMessage().isPresent()) {
553 SignalServiceSyncMessage syncMessage = content.getSyncMessage().get();
554 if (syncMessage.getSent().isPresent()) {
555 SignalServiceDataMessage message = syncMessage.getSent().get().getMessage();
556 group = handleSignalServiceDataMessage(message, true, envelope.getSource(), syncMessage.getSent().get().getDestination().get());
557 }
558 if (syncMessage.getRequest().isPresent()) {
559 // TODO
560 }
561 if (syncMessage.getGroups().isPresent()) {
562 // TODO
563 }
564 }
565 }
566 }
567 save();
568 handler.handleMessage(envelope, content, group);
569 } catch (TimeoutException e) {
570 if (returnOnTimeout)
571 return;
572 } catch (InvalidVersionException e) {
573 System.err.println("Ignoring error: " + e.getMessage());
574 }
575 }
576 } finally {
577 if (messagePipe != null)
578 messagePipe.shutdown();
579 }
580 }
581
582 public File getAttachmentFile(long attachmentId) {
583 return new File(attachmentsPath, attachmentId + "");
584 }
585
586 private File retrieveAttachment(SignalServiceAttachmentPointer pointer) throws IOException, InvalidMessageException {
587 final SignalServiceMessageReceiver messageReceiver = new SignalServiceMessageReceiver(URL, TRUST_STORE, username, password, signalingKey, USER_AGENT);
588
589 File tmpFile = File.createTempFile("ts_attach_" + pointer.getId(), ".tmp");
590 InputStream input = messageReceiver.retrieveAttachment(pointer, tmpFile);
591
592 new File(attachmentsPath).mkdirs();
593 File outputFile = getAttachmentFile(pointer.getId());
594 OutputStream output = null;
595 try {
596 output = new FileOutputStream(outputFile);
597 byte[] buffer = new byte[4096];
598 int read;
599
600 while ((read = input.read(buffer)) != -1) {
601 output.write(buffer, 0, read);
602 }
603 } catch (FileNotFoundException e) {
604 e.printStackTrace();
605 return null;
606 } finally {
607 if (output != null) {
608 output.close();
609 output = null;
610 }
611 if (!tmpFile.delete()) {
612 System.err.println("Failed to delete temp file: " + tmpFile);
613 }
614 }
615 if (pointer.getPreview().isPresent()) {
616 File previewFile = new File(outputFile + ".preview");
617 try {
618 output = new FileOutputStream(previewFile);
619 byte[] preview = pointer.getPreview().get();
620 output.write(preview, 0, preview.length);
621 } catch (FileNotFoundException e) {
622 e.printStackTrace();
623 return null;
624 } finally {
625 if (output != null) {
626 output.close();
627 }
628 }
629 }
630 return outputFile;
631 }
632
633 private String canonicalizeNumber(String number) throws InvalidNumberException {
634 String localNumber = username;
635 return PhoneNumberFormatter.formatNumber(number, localNumber);
636 }
637
638 private SignalServiceAddress getPushAddress(String number) throws InvalidNumberException {
639 String e164number = canonicalizeNumber(number);
640 return new SignalServiceAddress(e164number);
641 }
642
643 @Override
644 public boolean isRemote() {
645 return false;
646 }
647 }