1package eu.siacs.conversations.services;
2
3import android.content.Context;
4import android.os.PowerManager;
5import android.os.SystemClock;
6import android.util.Log;
7
8import androidx.core.content.ContextCompat;
9
10import org.bouncycastle.crypto.engines.AESEngine;
11import org.bouncycastle.crypto.io.CipherInputStream;
12import org.bouncycastle.crypto.io.CipherOutputStream;
13import org.bouncycastle.crypto.modes.AEADBlockCipher;
14import org.bouncycastle.crypto.modes.GCMBlockCipher;
15import org.bouncycastle.crypto.params.AEADParameters;
16import org.bouncycastle.crypto.params.KeyParameter;
17
18import java.io.FileInputStream;
19import java.io.FileNotFoundException;
20import java.io.FileOutputStream;
21import java.io.IOException;
22import java.io.InputStream;
23import java.io.OutputStream;
24import java.util.concurrent.atomic.AtomicLong;
25
26import javax.annotation.Nullable;
27
28import eu.siacs.conversations.Config;
29import eu.siacs.conversations.R;
30import eu.siacs.conversations.entities.DownloadableFile;
31import eu.siacs.conversations.utils.Compatibility;
32import okhttp3.MediaType;
33import okhttp3.RequestBody;
34import okio.BufferedSink;
35import okio.Okio;
36import okio.Source;
37
38import static eu.siacs.conversations.entities.Transferable.VALID_CRYPTO_EXTENSIONS;
39
40public class AbstractConnectionManager {
41
42 private static final int UI_REFRESH_THRESHOLD = 250;
43 private static final AtomicLong LAST_UI_UPDATE_CALL = new AtomicLong(0);
44 protected XmppConnectionService mXmppConnectionService;
45
46 public AbstractConnectionManager(XmppConnectionService service) {
47 this.mXmppConnectionService = service;
48 }
49
50 public static InputStream upgrade(DownloadableFile file, InputStream is) {
51 if (file.getKey() != null && file.getIv() != null) {
52 AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
53 cipher.init(true, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
54 return new CipherInputStream(is, cipher);
55 } else {
56 return is;
57 }
58 }
59
60
61 //For progress tracking see:
62 //https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java
63
64 public static RequestBody requestBody(final DownloadableFile file, final ProgressListener progressListener) {
65 return new RequestBody() {
66
67 @Override
68 public long contentLength() {
69 return file.getSize() + (file.getKey() != null ? 16 : 0);
70 }
71
72 @Nullable
73 @Override
74 public MediaType contentType() {
75 return MediaType.parse(file.getMimeType());
76 }
77
78 @Override
79 public void writeTo(final BufferedSink sink) throws IOException {
80 long transmitted = 0;
81 try (final Source source = Okio.source(upgrade(file, new FileInputStream(file)))) {
82 long read;
83 while ((read = source.read(sink.buffer(), 8196)) != -1) {
84 transmitted += read;
85 sink.flush();
86 progressListener.onProgress(transmitted);
87 }
88 }
89 }
90 };
91 }
92
93 public interface ProgressListener {
94 void onProgress(long progress);
95 }
96
97 public static OutputStream createOutputStream(DownloadableFile file, boolean append, boolean decrypt) {
98 FileOutputStream os;
99 try {
100 os = new FileOutputStream(file, append);
101 if (file.getKey() == null || !decrypt) {
102 return os;
103 }
104 } catch (FileNotFoundException e) {
105 Log.d(Config.LOGTAG, "unable to create output stream", e);
106 return null;
107 }
108 try {
109 AEADBlockCipher cipher = new GCMBlockCipher(new AESEngine());
110 cipher.init(false, new AEADParameters(new KeyParameter(file.getKey()), 128, file.getIv()));
111 return new CipherOutputStream(os, cipher);
112 } catch (Exception e) {
113 Log.d(Config.LOGTAG, "unable to create cipher output stream", e);
114 return null;
115 }
116 }
117
118 public XmppConnectionService getXmppConnectionService() {
119 return this.mXmppConnectionService;
120 }
121
122 public long getAutoAcceptFileSize() {
123 final long autoAcceptFileSize = this.mXmppConnectionService.getLongPreference("auto_accept_file_size", R.integer.auto_accept_filesize);
124 return autoAcceptFileSize <= 0 ? -1 : autoAcceptFileSize;
125 }
126
127 public boolean hasStoragePermission() {
128 return Compatibility.hasStoragePermission(mXmppConnectionService);
129 }
130
131 public void updateConversationUi(boolean force) {
132 synchronized (LAST_UI_UPDATE_CALL) {
133 if (force || SystemClock.elapsedRealtime() - LAST_UI_UPDATE_CALL.get() >= UI_REFRESH_THRESHOLD) {
134 LAST_UI_UPDATE_CALL.set(SystemClock.elapsedRealtime());
135 mXmppConnectionService.updateConversationUi();
136 }
137 }
138 }
139
140 public PowerManager.WakeLock createWakeLock(final String name) {
141 final PowerManager powerManager = ContextCompat.getSystemService(mXmppConnectionService, PowerManager.class);
142 return powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, name);
143 }
144
145 public static class Extension {
146 public final String main;
147 public final String secondary;
148
149 private Extension(String main, String secondary) {
150 this.main = main;
151 this.secondary = secondary;
152 }
153
154 public String getExtension() {
155 if (VALID_CRYPTO_EXTENSIONS.contains(main)) {
156 return secondary;
157 } else {
158 return main;
159 }
160 }
161
162 public static Extension of(String path) {
163 //TODO accept List<String> pathSegments
164 final int pos = path.lastIndexOf('/');
165 final String filename = path.substring(pos + 1).toLowerCase();
166 final String[] parts = filename.split("\\.");
167 final String main = parts.length >= 2 ? parts[parts.length - 1] : null;
168 final String secondary = parts.length >= 3 ? parts[parts.length - 2] : null;
169 return new Extension(main, secondary);
170 }
171 }
172}