AbstractConnectionManager.java

  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}