AbstractConnectionManager.java

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