Fix scrolling issues in dictonary list
CHANGELOG.md
| 8 | 8 | ||
| 9 | 9 | * Display expected file size and entry count in dictionary list. | |
| 10 | 10 | ||
| 11 | + | ### Bug Fixes | |
| 12 | + | ||
| 13 | + | * Scrolling in dictionary view was slow because we computed the hash of downloaded | |
| 14 | + | files. We now download to a temporary file, so checking a download now is | |
| 15 | + | instantaneous and scrolling is smooth again. | |
| 16 | + | ||
| 11 | 17 | Changes In 0.2.2 | |
| 12 | 18 | ------------------- | |
| 13 | 19 |
app/src/main/java/eu/lepiller/nani/DictionaryDownloadActivity.java
| 11 | 11 | import androidx.appcompat.app.AppCompatActivity; | |
| 12 | 12 | import android.os.Bundle; | |
| 13 | 13 | import android.util.Log; | |
| 14 | + | import android.util.Pair; | |
| 14 | 15 | import android.view.View; | |
| 15 | 16 | import android.widget.ImageButton; | |
| 16 | 17 | import android.widget.ImageView; | |
… | |||
| 178 | 179 | ||
| 179 | 180 | Log.d(TAG, "Start downloading"); | |
| 180 | 181 | ||
| 181 | - | for (Map.Entry<String, File> e : d.getDownloads().entrySet()) { | |
| 182 | + | for (Map.Entry<String, Pair<File, File>> e : d.getDownloads().entrySet()) { | |
| 182 | 183 | try { | |
| 183 | 184 | String uri = e.getKey(); | |
| 184 | 185 | Log.d(TAG, "URL: " + uri); | |
| 185 | 186 | ||
| 187 | + | File temporaryFile = e.getValue().first; | |
| 188 | + | File cacheFile = e.getValue().second; | |
| 189 | + | ||
| 186 | 190 | // Download a .sha256 file if it exists | |
| 187 | 191 | URL url = new URL(uri + ".sha256"); | |
| 188 | - | byte data[] = new byte[4096]; | |
| 192 | + | byte[] data = new byte[4096]; | |
| 189 | 193 | int count; | |
| 190 | 194 | File file; | |
| 191 | 195 | ||
… | |||
| 193 | 197 | connection.connect(); | |
| 194 | 198 | if(connection.getResponseCode() == HttpURLConnection.HTTP_OK) { | |
| 195 | 199 | input = connection.getInputStream(); | |
| 196 | - | file = new File(e.getValue() + ".sha256"); | |
| 200 | + | file = new File(cacheFile + ".sha256"); | |
| 197 | 201 | file.getParentFile().mkdirs(); | |
| 198 | 202 | File old_file = new File(file + ".old"); | |
| 199 | 203 | if(old_file.exists()) { | |
… | |||
| 227 | 231 | ||
| 228 | 232 | // .sha256 file now downloaded | |
| 229 | 233 | url = new URL(uri); | |
| 230 | - | file = e.getValue(); | |
| 234 | + | file = temporaryFile; | |
| 235 | + | file.getParentFile().mkdirs(); | |
| 231 | 236 | long expectedLength = -1; | |
| 232 | 237 | boolean acceptRanges = false; | |
| 233 | 238 | if(file.exists()) { | |
… | |||
| 331 | 336 | download_bar.setProgress(100); | |
| 332 | 337 | removeProgress(); | |
| 333 | 338 | currentDownloadTask = null; | |
| 334 | - | updateLayout(d); | |
| 339 | + | d.switchToCacheFile(); | |
| 335 | 340 | if(!d.isDownloaded() && d.getExpectedFileSize() <= d.getSize()) { | |
| 336 | 341 | Snackbar.make(findViewById(R.id.name_view), getString(R.string.error_dico_checksum_fail), | |
| 337 | 342 | Snackbar.LENGTH_LONG).show(); | |
| 338 | 343 | d.remove(); | |
| 339 | 344 | } | |
| 345 | + | updateLayout(d); | |
| 340 | 346 | } | |
| 341 | 347 | } | |
| 342 | 348 | } | |
app/src/main/java/eu/lepiller/nani/dictionary/DictionariesAdapter.java
| 62 | 62 | ||
| 63 | 63 | int size = dictionary.getExpectedFileSize(); | |
| 64 | 64 | if(size < 1500) | |
| 65 | - | size_view.setText(String.format(context.getString(R.string.dictionary_size_b), size)); | |
| 65 | + | size_view.setText(String.format(context.getString(R.string.dictionary_expected_size_b), size)); | |
| 66 | 66 | else if(size < 1500000) | |
| 67 | - | size_view.setText(String.format(context.getString(R.string.dictionary_size_kb), size/1000)); | |
| 67 | + | size_view.setText(String.format(context.getString(R.string.dictionary_expected_size_kb), size/1000)); | |
| 68 | 68 | else | |
| 69 | - | size_view.setText(String.format(context.getString(R.string.dictionary_size_mb), size/1000000)); | |
| 69 | + | size_view.setText(String.format(context.getString(R.string.dictionary_expected_size_mb), size/1000000)); | |
| 70 | 70 | ||
| 71 | 71 | int entries = dictionary.getExpectedEntries(); | |
| 72 | 72 | NumberFormat nf = NumberFormat.getInstance(); |
app/src/main/java/eu/lepiller/nani/dictionary/Dictionary.java
| 4 | 4 | import android.graphics.drawable.Drawable; | |
| 5 | 5 | import android.os.Build; | |
| 6 | 6 | import android.util.Log; | |
| 7 | + | import android.util.Pair; | |
| 7 | 8 | ||
| 8 | 9 | import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat; | |
| 9 | 10 | ||
| 10 | 11 | import java.io.File; | |
| 12 | + | import java.io.FileInputStream; | |
| 11 | 13 | import java.io.FileNotFoundException; | |
| 12 | 14 | import java.io.FileReader; | |
| 13 | 15 | import java.io.IOException; | |
| 16 | + | import java.math.BigInteger; | |
| 14 | 17 | import java.net.URL; | |
| 18 | + | import java.security.MessageDigest; | |
| 19 | + | import java.security.NoSuchAlgorithmException; | |
| 15 | 20 | import java.util.Map; | |
| 16 | 21 | ||
| 17 | 22 | abstract public class Dictionary { | |
… | |||
| 20 | 25 | private int expectedFileSize; | |
| 21 | 26 | private int expectedEntries; | |
| 22 | 27 | private String sha256; | |
| 23 | - | File file; | |
| 28 | + | File file, temporaryFile; | |
| 24 | 29 | ||
| 25 | 30 | Dictionary(String n, String descr, String fullDescr, File cacheDir, int fileSize, int entries, String hash) { | |
| 26 | 31 | name = n; | |
… | |||
| 30 | 35 | expectedFileSize = fileSize; | |
| 31 | 36 | sha256 = hash; | |
| 32 | 37 | this.file = new File(cacheDir, "/dico/" + name); | |
| 38 | + | this.temporaryFile = new File(cacheDir, "/tmp/" + name); | |
| 33 | 39 | } | |
| 34 | 40 | ||
| 35 | 41 | public String getName() { | |
… | |||
| 52 | 58 | } | |
| 53 | 59 | abstract public boolean isUpToDate(); | |
| 54 | 60 | ||
| 55 | - | protected File getFile() { | |
| 61 | + | File getFile() { | |
| 56 | 62 | return file; | |
| 57 | 63 | } | |
| 64 | + | File getTemporaryFile() { | |
| 65 | + | return temporaryFile; | |
| 66 | + | } | |
| 58 | 67 | ||
| 59 | 68 | abstract public int getSize(); | |
| 60 | 69 | ||
| 61 | 70 | abstract public boolean isDownloaded(); | |
| 71 | + | abstract public void switchToCacheFile(); | |
| 62 | 72 | ||
| 63 | 73 | abstract public int size(); | |
| 64 | 74 | ||
| 65 | 75 | // Used for downloads: tells what size we currently have downloaded | |
| 66 | 76 | abstract int currentSize(); | |
| 67 | 77 | ||
| 68 | - | public abstract Map<String, File> getDownloads(); | |
| 78 | + | public abstract Map<String, Pair<File, File>> getDownloads(); | |
| 69 | 79 | ||
| 70 | 80 | public Drawable getDrawable(Context context) { | |
| 71 | 81 | Drawable drawable; | |
… | |||
| 93 | 103 | ||
| 94 | 104 | return sb.toString().trim(); | |
| 95 | 105 | } | |
| 106 | + | ||
| 107 | + | public static String sha256OfFile(File file) { | |
| 108 | + | MessageDigest digest=null; | |
| 109 | + | try { | |
| 110 | + | digest = MessageDigest.getInstance("SHA-256"); | |
| 111 | + | } catch (NoSuchAlgorithmException e) { | |
| 112 | + | e.printStackTrace(); | |
| 113 | + | } | |
| 114 | + | digest.reset(); | |
| 115 | + | try { | |
| 116 | + | byte data[] = new byte[4096]; | |
| 117 | + | FileInputStream fr = new FileInputStream(file); | |
| 118 | + | int count; | |
| 119 | + | while((count = fr.read(data)) != -1) { | |
| 120 | + | digest.update(data, 0, count); | |
| 121 | + | } | |
| 122 | + | } catch (FileNotFoundException e) { | |
| 123 | + | e.printStackTrace(); | |
| 124 | + | } catch (IOException e) { | |
| 125 | + | e.printStackTrace(); | |
| 126 | + | } | |
| 127 | + | byte[] result = digest.digest(); | |
| 128 | + | return String.format("%0" + (result.length*2) + "X", new BigInteger(1, result)).toLowerCase(); | |
| 129 | + | } | |
| 96 | 130 | } | |
app/src/main/java/eu/lepiller/nani/dictionary/FileDictionary.java
| 1 | 1 | package eu.lepiller.nani.dictionary; | |
| 2 | 2 | ||
| 3 | 3 | import android.util.Log; | |
| 4 | + | import android.util.Pair; | |
| 4 | 5 | ||
| 5 | 6 | import java.io.File; | |
| 6 | - | import java.io.FileInputStream; | |
| 7 | 7 | import java.io.FileNotFoundException; | |
| 8 | - | import java.io.FileReader; | |
| 9 | 8 | import java.io.IOException; | |
| 10 | 9 | import java.io.RandomAccessFile; | |
| 11 | - | import java.math.BigInteger; | |
| 12 | - | import java.net.MalformedURLException; | |
| 13 | - | import java.net.URL; | |
| 14 | - | import java.security.DigestException; | |
| 15 | - | import java.security.MessageDigest; | |
| 16 | - | import java.security.NoSuchAlgorithmException; | |
| 17 | 10 | import java.util.ArrayList; | |
| 18 | 11 | import java.util.HashMap; | |
| 19 | 12 | import java.util.Map; | |
… | |||
| 47 | 40 | ||
| 48 | 41 | @Override | |
| 49 | 42 | public boolean isDownloaded() { | |
| 50 | - | File f = getFile(); | |
| 43 | + | return getFile().exists(); | |
| 44 | + | } | |
| 45 | + | ||
| 46 | + | @Override | |
| 47 | + | public void switchToCacheFile() { | |
| 48 | + | if(checkTemporaryFile()) { | |
| 49 | + | if(getFile().exists()) { | |
| 50 | + | getFile().delete(); | |
| 51 | + | } | |
| 52 | + | getTemporaryFile().renameTo(getFile()); | |
| 53 | + | } | |
| 54 | + | } | |
| 55 | + | ||
| 56 | + | private boolean checkTemporaryFile() { | |
| 57 | + | File f = getTemporaryFile(); | |
| 51 | 58 | if(f.exists()) { | |
| 52 | 59 | File sha256 = new File(getFile() + ".sha256"); | |
| 53 | 60 | if(sha256.exists()) { | |
| 54 | - | MessageDigest digest=null; | |
| 55 | - | try { | |
| 56 | - | digest = MessageDigest.getInstance("SHA-256"); | |
| 57 | - | } catch (NoSuchAlgorithmException e) { | |
| 58 | - | e.printStackTrace(); | |
| 59 | - | } | |
| 60 | - | digest.reset(); | |
| 61 | - | try { | |
| 62 | - | byte data[] = new byte[4096]; | |
| 63 | - | FileInputStream fr = new FileInputStream(f); | |
| 64 | - | int count; | |
| 65 | - | while((count = fr.read(data)) != -1) { | |
| 66 | - | digest.update(data, 0, count); | |
| 67 | - | } | |
| 68 | - | } catch (FileNotFoundException e) { | |
| 69 | - | e.printStackTrace(); | |
| 70 | - | } catch (IOException e) { | |
| 71 | - | e.printStackTrace(); | |
| 72 | - | } | |
| 73 | - | byte[] result = digest.digest(); | |
| 74 | - | String hash = String.format("%0" + (result.length*2) + "X", new BigInteger(1, result)).toLowerCase(); | |
| 61 | + | String hash = sha256OfFile(f); | |
| 75 | 62 | ||
| 76 | 63 | try { | |
| 77 | 64 | String expected = readSha256FromFile(sha256); | |
… | |||
| 95 | 82 | ||
| 96 | 83 | public boolean isUpToDate() { | |
| 97 | 84 | File sha256 = new File(getFile() + ".sha256"); | |
| 85 | + | if(!sha256.exists()) | |
| 86 | + | return true; | |
| 87 | + | ||
| 98 | 88 | try { | |
| 99 | 89 | String current = readSha256FromFile(sha256); | |
| 100 | 90 | String latest = getSha256(); | |
… | |||
| 117 | 107 | } | |
| 118 | 108 | ||
| 119 | 109 | @Override | |
| 120 | - | public Map<String, File> getDownloads() { | |
| 121 | - | HashMap<String, File> result = new HashMap<>(); | |
| 122 | - | result.put(mUrl, getFile()); | |
| 110 | + | public Map<String, Pair<File, File>> getDownloads() { | |
| 111 | + | HashMap<String, Pair<File, File>> result = new HashMap<>(); | |
| 112 | + | Pair<File, File> pair = new Pair<>(getTemporaryFile(), getFile()); | |
| 113 | + | result.put(mUrl, pair); | |
| 123 | 114 | return result; | |
| 124 | 115 | } | |
| 125 | 116 | ||
| 126 | 117 | @Override | |
| 127 | 118 | public void remove() { | |
| 128 | - | File file = getFile(); | |
| 129 | - | file.delete(); | |
| 119 | + | if(getFile().exists()) | |
| 120 | + | getFile().delete(); | |
| 121 | + | if(getTemporaryFile().exists()) | |
| 122 | + | getTemporaryFile().delete(); | |
| 130 | 123 | } | |
| 131 | 124 | ||
| 132 | 125 | ||
| 133 | 126 | public int getSize() { | |
| 134 | - | if(file.exists()) | |
| 127 | + | if(getFile().exists()) | |
| 135 | 128 | return (int) (getFile().length()); | |
| 129 | + | else if(getTemporaryFile().exists()) | |
| 130 | + | return (int) (getTemporaryFile().length()); | |
| 136 | 131 | else | |
| 137 | 132 | return 0; | |
| 138 | 133 | } | |