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