AbstractConnectionManager.java

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