diff --git a/AndroidManifest.xml b/AndroidManifest.xml index 67d6a0600..3e91140f8 100644 --- a/AndroidManifest.xml +++ b/AndroidManifest.xml @@ -2,7 +2,7 @@ + android:versionName="0.3.0" > + android:versionName="0.3.0" > @@ -21,8 +21,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginTop="4dp" - android:layout_marginLeft="4dp" - android:layout_marginRight="4dp" + android:layout_marginLeft="8dp" + android:layout_marginRight="8dp" android:layout_marginBottom="16dp" android:fontFamily="sans-serif" android:hint="@string/password"/> diff --git a/res/layout/menu_login.xml b/res/layout/menu_login.xml index 424cd61af..d309e9540 100644 --- a/res/layout/menu_login.xml +++ b/res/layout/menu_login.xml @@ -15,7 +15,7 @@ android:text="@string/login" /> 发现新版本:%s\n大小:%s\n%s\n\n是否更新? + 点击检查更新 + 正在检查更新 + 已是最新版 + 发现新版本 上传者:%s @@ -97,6 +101,7 @@ 无效的请求或无效的响应 存入缓存错误 网址错误 + 用户名或密码错误 获取用户信息错误 退出错误 网址错误 @@ -106,6 +111,8 @@ 无网络连接 你所在的网络无法连接到 e-hentai 无网络连接? + 请求错误 + 主机错误 刷新 @@ -203,6 +210,6 @@ 更新 感谢 - + 开发 高级搜索请左划 \ No newline at end of file diff --git a/res/values/strings.xml b/res/values/strings.xml index 472ec7f6f..ba05200e1 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -62,6 +62,10 @@ Found new version : %s\nSize : %s\n%s\n\nUpdate ? + Tap to check update + checking updating + Up to date + Found update Uploader: %s @@ -101,6 +105,7 @@ Invalid request or invalid response Put cache error Url format error + Username or password error Get user info error Logout error Url error @@ -110,6 +115,8 @@ No network Your network can not reach e-hentai No network ? + Request error + Host error Refresh @@ -210,6 +217,6 @@ Update Thanks - + Develop Advance search in left \ No newline at end of file diff --git a/res/xml/settings.xml b/res/xml/settings.xml index a1a0993ab..7cda1dbc5 100644 --- a/res/xml/settings.xml +++ b/res/xml/settings.xml @@ -19,7 +19,6 @@ android:title="@string/preference_page_scaling" /> - + + - + + + + + \ No newline at end of file diff --git a/src/com/hippo/ehviewer/activity/MangaListActivity.java b/src/com/hippo/ehviewer/activity/MangaListActivity.java index 74cf98b2d..fcbb3719f 100644 --- a/src/com/hippo/ehviewer/activity/MangaListActivity.java +++ b/src/com/hippo/ehviewer/activity/MangaListActivity.java @@ -1,5 +1,6 @@ package com.hippo.ehviewer.activity; +import java.io.File; import java.net.MalformedURLException; import java.util.ArrayList; @@ -7,6 +8,7 @@ import com.hippo.ehviewer.BeautifyScreen; import com.hippo.ehviewer.ListMangaDetail; import com.hippo.ehviewer.ListUrls; import com.hippo.ehviewer.R; +import com.hippo.ehviewer.UpdateHelper; import com.hippo.ehviewer.dialog.DialogBuilder; import com.hippo.ehviewer.dialog.SuperDialogUtil; import com.hippo.ehviewer.network.Downloader; @@ -29,6 +31,7 @@ import com.hippo.ehviewer.widget.PullListView; import com.hippo.ehviewer.widget.PullListView.OnFooterRefreshListener; import com.hippo.ehviewer.widget.PullListView.OnHeaderRefreshListener; +import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.app.Activity; @@ -109,7 +112,6 @@ public class MangaListActivity extends Activity { private boolean mListFirst = true; private boolean mLoadListOver = false; - private AlertDialog checkLoginDialog; private AlertDialog loginDialog; private AlertDialog filterDialog; private AlertDialog longClickDialog; @@ -132,6 +134,7 @@ public class MangaListActivity extends Activity { private int visiblePage = 0; private String title; + private String updateFileName; private class RefreshPackage { public ListUrls listUrls; @@ -145,17 +148,6 @@ public class MangaListActivity extends Activity { private DownloadServiceConnection mServiceConn = new DownloadServiceConnection(); - private AlertDialog createCheckLoginDialog() { - LayoutInflater inflater = this.getLayoutInflater(); - View view = inflater.inflate(R.layout.wait, null); - TextView tv = (TextView)view.findViewById(R.id.wait_message); - tv.setText(R.string.dailog_check_login); - - return new DialogBuilder(this).setCancelable(false) - .setTitle(R.string.wait) - .setView(view, true).create(); - } - private AlertDialog createLoginDialog() { LayoutInflater inflater = this.getLayoutInflater(); View view = inflater.inflate(R.layout.login, null); @@ -174,7 +166,7 @@ public class MangaListActivity extends Activity { EhClient.login(username, password, new EhClient.OnLoginListener() { @Override public void onSuccess() { - checkLogin(); + checkLogin(true); } @Override public void onFailure(int errorMessageId) { @@ -183,6 +175,7 @@ public class MangaListActivity extends Activity { Toast.makeText(MangaListActivity.this, getString(errorMessageId), Toast.LENGTH_SHORT).show(); + loginDialog.show(); } }); } @@ -1034,7 +1027,6 @@ public class MangaListActivity extends Activity { visiblePage = lus.getPage(); // Init dialog - checkLoginDialog = createCheckLoginDialog(); loginDialog = createLoginDialog(); filterDialog = createFilterDialog(); longClickDialog = createLongClickDialog(); @@ -1291,7 +1283,7 @@ public class MangaListActivity extends Activity { checkUpdate(); // Check login - checkLogin(); + checkLogin(false); // get MangeList title = listMenuTitle.get(0); @@ -1400,102 +1392,22 @@ public class MangaListActivity extends Activity { } } - private class UpdateListener implements Downloader.OnDownloadListener { - - private NotificationManager mNotifyManager; - private NotificationCompat.Builder mBuilder; - - public UpdateListener() { - mNotifyManager = (NotificationManager) - getSystemService(Context.NOTIFICATION_SERVICE); - mBuilder = new NotificationCompat.Builder(MangaListActivity.this.getApplication()); - mBuilder.setSmallIcon(R.drawable.ic_launcher); - } - - @Override - public void onDownloadStart(int totalSize) { - mBuilder.setContentTitle("正在下载更新") - .setContentText(null) - .setProgress(0, 0, true).setOngoing(true).setAutoCancel(false); - mNotifyManager.notify(233, mBuilder.build()); - } - @Override - public void onDownloadStatusUpdate( - int downloadSize, int totalSize) { - mBuilder.setContentTitle("正在下载更新") - .setContentText(String.format("%.2f / %.2f KB", downloadSize/1024.0f, totalSize/1024.0f)) - .setProgress(totalSize, downloadSize, false).setOngoing(true).setAutoCancel(false); - mNotifyManager.notify(233, mBuilder.build()); - - } - @Override - public void onDownloadOver(boolean ok, int eMesgId) { - mBuilder.setContentTitle("更新下载完成") - .setContentText("点击更新") - .setProgress(0, 0, false).setOngoing(false).setAutoCancel(true); - mNotifyManager.notify(233, mBuilder.build()); - - } - } - private void checkUpdate() { - EhClient.checkUpdate(new EhClient.OnCheckUpdateListener() { - @Override - public void onSuccess(String pageContext) { - String[] items = pageContext.split("\n"); - if (items.length > 3) { - String newVer = items[0]; - final String url = EhClient.UPDATE_URL + items[1]; - final String name = url.substring(url.lastIndexOf('/')+1); - String size = items[2]; - String info = items[3]; - - AlertDialog dialog = new DialogBuilder(MangaListActivity.this).setTitle(R.string.update) - .setMessage(String.format(getString(R.string.update_message), newVer, size, info)) - .setNegativeButton(android.R.string.cancel, new View.OnClickListener() { - @Override - public void onClick(View v) { - ((AlertButton)v).dialog.dismiss(); - } - }).setPositiveButton(android.R.string.ok, new View.OnClickListener() { - @Override - public void onClick(View v) { - ((AlertButton)v).dialog.dismiss(); - Downloader d = new Downloader(); - try { - Downloader.Controlor controlor = d.resetData(Config.getDownloadPath(), name, url); - d.setOnDownloadListener(new UpdateListener()); - new Thread(d).start(); - } catch (MalformedURLException e) { - e.printStackTrace(); - } - } - }).create(); - if (!MangaListActivity.this.isFinishing()) - dialog.show(); - } - } - - @Override - public void onFailure(int errorMessageId) { - Log.d(TAG, getString(errorMessageId)); - } - }); + new UpdateHelper(this).checkUpdate(); } - private void checkLogin() { - if (EhClient.hasLogin()) + private void checkLogin(boolean force) { + if (Config.isLogin() || force) { loginButton.setVisibility(View.GONE); waitloginView.setVisibility(View.VISIBLE); EhClient.checkLogin(new EhClient.OnCheckLoginListener() { @Override public void onSuccess() { - loginButton.setVisibility(View.VISIBLE); - waitloginView.setVisibility(View.GONE); + Config.loginNow(); Toast.makeText(MangaListActivity.this, getString(R.string.toast_login_succeeded), Toast.LENGTH_SHORT).show(); - layoutDrawRight(); + layoutDrawRight(); } @Override @@ -1507,16 +1419,23 @@ public class MangaListActivity extends Activity { Toast.LENGTH_SHORT).show(); } }); + } } private void layoutDrawRight() { if (EhClient.isLogin()) { // If have login loginView.setVisibility(View.GONE); loginOverView.setVisibility(View.VISIBLE); + usernameText.setText(EhClient.getUsername()); + logoutButton.setVisibility(View.VISIBLE); + waitlogoutView.setVisibility(View.GONE); } else { loginView.setVisibility(View.VISIBLE); loginOverView.setVisibility(View.GONE); + + loginButton.setVisibility(View.VISIBLE); + waitloginView.setVisibility(View.GONE); } } @@ -1549,34 +1468,6 @@ public class MangaListActivity extends Activity { } return re; } - - - // CheckLogin - /* - private void checkLogin() { - checkLoginDialog.show(); - EhClient.checkLogin(new EhClient.OnCheckLoginListener() { - @Override - public void onSuccess() { - checkLoginDialog.dismiss(); - Toast.makeText(MangaListActivity.this, - getString(R.string.toast_login_succeeded), - Toast.LENGTH_SHORT).show(); - layoutDrawRight(); - } - - @Override - public void onFailure(int errorMessageId) { - checkLoginDialog.dismiss(); - Toast.makeText(MangaListActivity.this, - getString(errorMessageId), - Toast.LENGTH_SHORT).show(); - loginDialog.show(); - } - }); - }*/ - - // *** Button onclick ***// public void buttonRefresh(View arg0) { refresh(lus.clone(), listMenuTitle.get(0)); @@ -1591,8 +1482,6 @@ public class MangaListActivity extends Activity { Toast.makeText(MangaListActivity.this, getString(R.string.toast_logout_succeeded), Toast.LENGTH_SHORT).show(); - logoutButton.setVisibility(View.VISIBLE); - waitlogoutView.setVisibility(View.GONE); Config.logoutNow(); layoutDrawRight(); } @@ -1612,5 +1501,11 @@ public class MangaListActivity extends Activity { loginDialog.show(); } - // *** Button onclick end ***// + class ImageLoadTask { + + } + + + + } diff --git a/src/com/hippo/ehviewer/activity/SettingsActivity.java b/src/com/hippo/ehviewer/activity/SettingsActivity.java index 62b97196f..20b5caee3 100644 --- a/src/com/hippo/ehviewer/activity/SettingsActivity.java +++ b/src/com/hippo/ehviewer/activity/SettingsActivity.java @@ -8,6 +8,7 @@ import java.io.UnsupportedEncodingException; import com.hippo.ehviewer.BeautifyScreen; import com.hippo.ehviewer.DiskCache; import com.hippo.ehviewer.R; +import com.hippo.ehviewer.UpdateHelper; import com.hippo.ehviewer.util.Cache; import com.hippo.ehviewer.util.Config; import com.hippo.ehviewer.util.Util; @@ -16,6 +17,7 @@ import com.hippo.ehviewer.dialog.DialogBuilder; import com.hippo.ehviewer.preference.AutoListPreference; import android.annotation.SuppressLint; +import android.app.NotificationManager; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; @@ -55,6 +57,8 @@ public class SettingsActivity extends PreferenceActivity { private String SCREEN_ORI = "preference_screen_ori"; private String CHANGELOG = "preference_changelog"; private String THANKS = "preference_thanks"; + private String UPDATE = "preference_update"; + private String RAN = "preference_remove_all_noification"; private EditTextPreference cpCachePre; private Preference clearCpCachePre; @@ -283,6 +287,45 @@ public class SettingsActivity extends PreferenceActivity { return true; } }); + + final Preference update = (Preference)screen.findPreference(UPDATE); + update.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + update.setSummary(R.string.checking_update); + new UpdateHelper(SettingsActivity.this) + .SetOnCheckUpdateListener(new UpdateHelper.OnCheckUpdateListener() { + @Override + public void onSuccess(String pageContext) { + update.setSummary(R.string.found_update); + update.setEnabled(true); + } + @Override + public void onNoUpdate() { + update.setSummary(R.string.up_to_date); + update.setEnabled(true); + } + @Override + public void onFailure(int eMesgId) { + update.setSummary(eMesgId); + update.setEnabled(true); + } + }).checkUpdate(); + update.setEnabled(false); + return true; + } + }); + + Preference ran = (Preference)screen.findPreference(RAN); + ran.setOnPreferenceClickListener(new OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference arg0) { + NotificationManager mNotifyManager = (NotificationManager) + getSystemService(Context.NOTIFICATION_SERVICE); + mNotifyManager.cancelAll(); + return true; + } + }); } private void updateCpCacheSummary() { diff --git a/src/com/hippo/ehviewer/util/Config.java b/src/com/hippo/ehviewer/util/Config.java index 6c7944c31..5ffa749c2 100644 --- a/src/com/hippo/ehviewer/util/Config.java +++ b/src/com/hippo/ehviewer/util/Config.java @@ -103,9 +103,6 @@ public class Config { * It is first time to launch the application */ public static void firstTime() { - if (!mInit) { - throw new IllegalStateException(EMSG); - } mConfigPre.edit().putBoolean(KEY_FIRST, false).apply(); } @@ -115,28 +112,15 @@ public class Config { * @return */ public static boolean isLogin() { - if (!mInit) { - throw new IllegalStateException(EMSG); - } return mConfigPre.getBoolean(KEY_LOGIN, false); } public static void loginNow() { - if (!mInit) { - throw new IllegalStateException(EMSG); - } - Editor editor = mConfigPre.edit(); - editor.putBoolean(KEY_LOGIN, true); - editor.commit(); + mConfigPre.edit().putBoolean(KEY_LOGIN, true).apply(); } public static void logoutNow() { - if (!mInit) { - throw new IllegalStateException(EMSG); - } - Editor editor = mConfigPre.edit(); - editor.putBoolean(KEY_LOGIN, false); - editor.commit(); + mConfigPre.edit().putBoolean(KEY_LOGIN, false).apply(); } public static String getDownloadPath() { @@ -149,9 +133,6 @@ public class Config { * @return */ public static int getCoverDiskCacheSize() { - if (!mInit) { - throw new IllegalStateException(EMSG); - } try { return Integer.parseInt(mConfigPre.getString(CP_CACHE, CP_CACHE_DEFAULT)); } catch (Exception e) { diff --git a/src/com/hippo/ehviewer/util/EhClient.java b/src/com/hippo/ehviewer/util/EhClient.java index 9cdedacc7..34222e693 100644 --- a/src/com/hippo/ehviewer/util/EhClient.java +++ b/src/com/hippo/ehviewer/util/EhClient.java @@ -594,7 +594,7 @@ public class EhClient { if (pageContent.contains("

You are now logged in as: ")) { mOK = 1; } else if (errorMessageId == R.string.em_unknown_error) - errorMessageId = R.string.em_check_login_error; + errorMessageId = R.string.em_login_error; } Message msg = new Message(); msg.what = errorMessageId; diff --git a/src/com/hippo/ehviewer/util/Ui.java b/src/com/hippo/ehviewer/util/Ui.java index a721ef1de..7787201e7 100644 --- a/src/com/hippo/ehviewer/util/Ui.java +++ b/src/com/hippo/ehviewer/util/Ui.java @@ -58,6 +58,7 @@ public class Ui { opt.inPurgeable = true; opt.inInputShareable = true; + /* BITMAP_DOUJINSHI = BitmapFactory.decodeStream(mContext.getResources().openRawResource(R.drawable.doujinshi), null, opt); BITMAP_MANGAI = BitmapFactory.decodeStream(mContext.getResources().openRawResource(R.drawable.manga), null, opt); BITMAP_ARTIST_CG = BitmapFactory.decodeStream(mContext.getResources().openRawResource(R.drawable.artistcg), null, opt); @@ -73,7 +74,7 @@ public class Ui { BITMAP_START = BitmapFactory.decodeStream(mContext.getResources().openRawResource(R.drawable.star), null, opt); BITMAP_START_HALF = BitmapFactory.decodeStream(mContext.getResources().openRawResource(R.drawable.star_half), null, opt); BITMAP_START_EMPTY = BitmapFactory.decodeStream(mContext.getResources().openRawResource(R.drawable.star_empty), null, opt); - + */ OlImageView.setDefaultImage(BitmapFactory.decodeStream(mContext.getResources().openRawResource(R.drawable.ic_launcher)) , BitmapFactory.decodeStream(mContext.getResources().openRawResource(R.drawable.ic_touch), null, opt)); } diff --git a/src/com/jakewharton/disklrucache/DiskLruCache.java b/src/com/jakewharton/disklrucache/DiskLruCache.java index cacbc8cbe..89b2ee33f 100644 --- a/src/com/jakewharton/disklrucache/DiskLruCache.java +++ b/src/com/jakewharton/disklrucache/DiskLruCache.java @@ -44,897 +44,968 @@ import java.util.regex.Pattern; import android.util.Log; /** - * A cache that uses a bounded amount of space on a filesystem. Each cache - * entry has a string key and a fixed number of values. Each key must match - * the regex [a-z0-9_-]{1,64}. Values are byte sequences, - * accessible as streams or files. Each value must be between {@code 0} and + * A cache that uses a bounded amount of space on a filesystem. Each cache entry + * has a string key and a fixed number of values. Each key must match the regex + * [a-z0-9_-]{1,64}. Values are byte sequences, accessible as + * streams or files. Each value must be between {@code 0} and * {@code Integer.MAX_VALUE} bytes in length. - * - *

The cache stores its data in a directory on the filesystem. This - * directory must be exclusive to the cache; the cache may delete or overwrite - * files from its directory. It is an error for multiple processes to use the - * same cache directory at the same time. - * - *

This cache limits the number of bytes that it will store on the - * filesystem. When the number of stored bytes exceeds the limit, the cache will - * remove entries in the background until the limit is satisfied. The limit is - * not strict: the cache may temporarily exceed it while waiting for files to be - * deleted. The limit does not include filesystem overhead or the cache - * journal so space-sensitive applications should set a conservative limit. - * - *

Clients call {@link #edit} to create or update the values of an entry. An + * + *

+ * The cache stores its data in a directory on the filesystem. This directory + * must be exclusive to the cache; the cache may delete or overwrite files from + * its directory. It is an error for multiple processes to use the same cache + * directory at the same time. + * + *

+ * This cache limits the number of bytes that it will store on the filesystem. + * When the number of stored bytes exceeds the limit, the cache will remove + * entries in the background until the limit is satisfied. The limit is not + * strict: the cache may temporarily exceed it while waiting for files to be + * deleted. The limit does not include filesystem overhead or the cache journal + * so space-sensitive applications should set a conservative limit. + * + *

+ * Clients call {@link #edit} to create or update the values of an entry. An * entry may have only one editor at one time; if a value is not available to be * edited then {@link #edit} will return null. *

    - *
  • When an entry is being created it is necessary to - * supply a full set of values; the empty value should be used as a - * placeholder if necessary. - *
  • When an entry is being edited, it is not necessary - * to supply data for every value; values default to their previous - * value. + *
  • When an entry is being created it is necessary to supply + * a full set of values; the empty value should be used as a placeholder if + * necessary. + *
  • When an entry is being edited, it is not necessary to + * supply data for every value; values default to their previous value. *
* Every {@link #edit} call must be matched by a call to {@link Editor#commit} * or {@link Editor#abort}. Committing is atomic: a read observes the full set * of values as they were before or after the commit, but never a mix of values. - * - *

Clients call {@link #get} to read a snapshot of an entry. The read will + * + *

+ * Clients call {@link #get} to read a snapshot of an entry. The read will * observe the value at the time that {@link #get} was called. Updates and * removals after the call do not impact ongoing reads. - * - *

This class is tolerant of some I/O errors. If files are missing from the - * filesystem, the corresponding entries will be dropped from the cache. If - * an error occurs while writing a cache value, the edit will fail silently. + * + *

+ * This class is tolerant of some I/O errors. If files are missing from the + * filesystem, the corresponding entries will be dropped from the cache. If an + * error occurs while writing a cache value, the edit will fail silently. * Callers should handle other problems by catching {@code IOException} and * responding appropriately. */ public final class DiskLruCache implements Closeable { static final String TAG = "DiskLruCache"; - static final String JOURNAL_FILE = "journal"; - static final String JOURNAL_FILE_TEMP = "journal.tmp"; - static final String JOURNAL_FILE_BACKUP = "journal.bkp"; - static final String MAGIC = "libcore.io.DiskLruCache"; - static final String VERSION_1 = "1"; - static final long ANY_SEQUENCE_NUMBER = -1; - static final Pattern LEGAL_KEY_PATTERN = Pattern.compile("[a-z0-9_-]{1,64}"); - private static final String CLEAN = "CLEAN"; - private static final String DIRTY = "DIRTY"; - private static final String REMOVE = "REMOVE"; - private static final String READ = "READ"; + static final String JOURNAL_FILE = "journal"; + static final String JOURNAL_FILE_TEMP = "journal.tmp"; + static final String JOURNAL_FILE_BACKUP = "journal.bkp"; + static final String MAGIC = "libcore.io.DiskLruCache"; + static final String VERSION_1 = "1"; + static final long ANY_SEQUENCE_NUMBER = -1; + static final Pattern LEGAL_KEY_PATTERN = Pattern + .compile("[a-z0-9_-]{1,64}"); + private static final String CLEAN = "CLEAN"; + private static final String DIRTY = "DIRTY"; + private static final String REMOVE = "REMOVE"; + private static final String READ = "READ"; /* * This cache uses a journal file named "journal". A typical journal file - * looks like this: - * libcore.io.DiskLruCache - * 1 - * 100 - * 2 - * - * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 - * DIRTY 335c4c6028171cfddfbaae1a9c313c52 - * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342 - * REMOVE 335c4c6028171cfddfbaae1a9c313c52 - * DIRTY 1ab96a171faeeee38496d8b330771a7a - * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234 - * READ 335c4c6028171cfddfbaae1a9c313c52 - * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6 - * + * looks like this: libcore.io.DiskLruCache 1 100 2 + * + * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054 DIRTY + * 335c4c6028171cfddfbaae1a9c313c52 CLEAN 335c4c6028171cfddfbaae1a9c313c52 + * 3934 2342 REMOVE 335c4c6028171cfddfbaae1a9c313c52 DIRTY + * 1ab96a171faeeee38496d8b330771a7a CLEAN 1ab96a171faeeee38496d8b330771a7a + * 1600 234 READ 335c4c6028171cfddfbaae1a9c313c52 READ + * 3400330d1dfc7f3f7f4b8d4d803dfcf6 + * * The first five lines of the journal form its header. They are the - * constant string "libcore.io.DiskLruCache", the disk cache's version, - * the application's version, the value count, and a blank line. - * + * constant string "libcore.io.DiskLruCache", the disk cache's version, the + * application's version, the value count, and a blank line. + * * Each of the subsequent lines in the file is a record of the state of a * cache entry. Each line contains space-separated values: a state, a key, - * and optional state-specific values. - * o DIRTY lines track that an entry is actively being created or updated. - * Every successful DIRTY action should be followed by a CLEAN or REMOVE - * action. DIRTY lines without a matching CLEAN or REMOVE indicate that - * temporary files may need to be deleted. - * o CLEAN lines track a cache entry that has been successfully published - * and may be read. A publish line is followed by the lengths of each of - * its values. - * o READ lines track accesses for LRU. - * o REMOVE lines track entries that have been deleted. - * - * The journal file is appended to as cache operations occur. The journal may - * occasionally be compacted by dropping redundant lines. A temporary file named - * "journal.tmp" will be used during compaction; that file should be deleted if - * it exists when the cache is opened. + * and optional state-specific values. o DIRTY lines track that an entry is + * actively being created or updated. Every successful DIRTY action should + * be followed by a CLEAN or REMOVE action. DIRTY lines without a matching + * CLEAN or REMOVE indicate that temporary files may need to be deleted. o + * CLEAN lines track a cache entry that has been successfully published and + * may be read. A publish line is followed by the lengths of each of its + * values. o READ lines track accesses for LRU. o REMOVE lines track entries + * that have been deleted. + * + * The journal file is appended to as cache operations occur. The journal + * may occasionally be compacted by dropping redundant lines. A temporary + * file named "journal.tmp" will be used during compaction; that file should + * be deleted if it exists when the cache is opened. */ - private final File directory; - private final File journalFile; - private final File journalFileTmp; - private final File journalFileBackup; - private final int appVersion; - private long maxSize; - private final int valueCount; - private long size = 0; - private Writer journalWriter; - private final LinkedHashMap lruEntries = - new LinkedHashMap(0, 0.75f, true); - private int redundantOpCount; + private final File directory; + private final File journalFile; + private final File journalFileTmp; + private final File journalFileBackup; + private final int appVersion; + private long maxSize; + private final int valueCount; + private long size = 0; + private Writer journalWriter; + private final LinkedHashMap lruEntries = new LinkedHashMap( + 0, 0.75f, true); + private int redundantOpCount; - /** - * To differentiate between old and current snapshots, each entry is given - * a sequence number each time an edit is committed. A snapshot is stale if - * its sequence number is not equal to its entry's sequence number. - */ - private long nextSequenceNumber = 0; + /** + * To differentiate between old and current snapshots, each entry is given a + * sequence number each time an edit is committed. A snapshot is stale if + * its sequence number is not equal to its entry's sequence number. + */ + private long nextSequenceNumber = 0; - /** This cache uses a single background thread to evict entries. */ - final ThreadPoolExecutor executorService = - new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); - private final Callable cleanupCallable = new Callable() { - public Void call() throws Exception { - synchronized (DiskLruCache.this) { + /** This cache uses a single background thread to evict entries. */ + final ThreadPoolExecutor executorService = new ThreadPoolExecutor(0, 1, + 60L, TimeUnit.SECONDS, new LinkedBlockingQueue()); + private final Callable cleanupCallable = new Callable() { + public Void call() throws Exception { + synchronized (DiskLruCache.this) { + if (journalWriter == null) { + return null; // Closed. + } + trimToSize(); + if (journalRebuildRequired()) { + rebuildJournal(); + redundantOpCount = 0; + } + } + return null; + } + }; + + private DiskLruCache(File directory, int appVersion, int valueCount, + long maxSize) { + this.directory = directory; + this.appVersion = appVersion; + this.journalFile = new File(directory, JOURNAL_FILE); + this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP); + this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP); + this.valueCount = valueCount; + this.maxSize = maxSize; + } + + /** + * Opens the cache in {@code directory}, creating a cache if none exists + * there. + * + * @param directory + * a writable directory + * @param valueCount + * the number of values per cache entry. Must be positive. + * @param maxSize + * the maximum number of bytes this cache should use to store + * @throws IOException + * if reading or writing the cache directory fails + */ + public static DiskLruCache open(File directory, int appVersion, + int valueCount, long maxSize) throws IOException { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + if (valueCount <= 0) { + throw new IllegalArgumentException("valueCount <= 0"); + } + + // If a bkp file exists, use it instead. + File backupFile = new File(directory, JOURNAL_FILE_BACKUP); + if (backupFile.exists()) { + File journalFile = new File(directory, JOURNAL_FILE); + // If journal file also exists just delete backup file. + if (journalFile.exists()) { + backupFile.delete(); + } else { + renameTo(backupFile, journalFile, false); + } + } + + // Prefer to pick up where we left off. + DiskLruCache cache = new DiskLruCache(directory, appVersion, + valueCount, maxSize); + if (cache.journalFile.exists()) { + try { + cache.readJournal(); + cache.processJournal(); + cache.journalWriter = new BufferedWriter( + new OutputStreamWriter(new FileOutputStream( + cache.journalFile, true), Util.US_ASCII)); + return cache; + } catch (IOException journalIsCorrupt) { + System.out.println("DiskLruCache " + directory + + " is corrupt: " + journalIsCorrupt.getMessage() + + ", removing"); + cache.delete(); + } + } + + // Create a new empty cache. + directory.mkdirs(); + cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); + cache.rebuildJournal(); + return cache; + } + + private void readJournal() throws IOException { + StrictLineReader reader = new StrictLineReader(new FileInputStream( + journalFile), Util.US_ASCII); + try { + String magic = reader.readLine(); + String version = reader.readLine(); + String appVersionString = reader.readLine(); + String valueCountString = reader.readLine(); + String blank = reader.readLine(); + if (!MAGIC.equals(magic) || !VERSION_1.equals(version) + || !Integer.toString(appVersion).equals(appVersionString) + || !Integer.toString(valueCount).equals(valueCountString) + || !"".equals(blank)) { + throw new IOException("unexpected journal header: [" + magic + + ", " + version + ", " + valueCountString + ", " + + blank + "]"); + } + + int lineCount = 0; + while (true) { + try { + readJournalLine(reader.readLine()); + lineCount++; + } catch (EOFException endOfJournal) { + break; + } + } + redundantOpCount = lineCount - lruEntries.size(); + } finally { + Util.closeQuietly(reader); + } + } + + private void readJournalLine(String line) throws IOException { + int firstSpace = line.indexOf(' '); + if (firstSpace == -1) { + throw new IOException("unexpected journal line: " + line); + } + + int keyBegin = firstSpace + 1; + int secondSpace = line.indexOf(' ', keyBegin); + final String key; + if (secondSpace == -1) { + key = line.substring(keyBegin); + if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { + lruEntries.remove(key); + return; + } + } else { + key = line.substring(keyBegin, secondSpace); + } + + Entry entry = lruEntries.get(key); + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } + + if (secondSpace != -1 && firstSpace == CLEAN.length() + && line.startsWith(CLEAN)) { + String[] parts = line.substring(secondSpace + 1).split(" "); + entry.readable = true; + entry.currentEditor = null; + entry.setLengths(parts); + } else if (secondSpace == -1 && firstSpace == DIRTY.length() + && line.startsWith(DIRTY)) { + entry.currentEditor = new Editor(entry); + } else if (secondSpace == -1 && firstSpace == READ.length() + && line.startsWith(READ)) { + // This work was already done by calling lruEntries.get(). + } else { + throw new IOException("unexpected journal line: " + line); + } + } + + /** + * Computes the initial size and collects garbage as a part of opening the + * cache. Dirty entries are assumed to be inconsistent and will be deleted. + */ + private void processJournal() throws IOException { + deleteIfExists(journalFileTmp); + for (Iterator i = lruEntries.values().iterator(); i.hasNext();) { + Entry entry = i.next(); + if (entry.currentEditor == null) { + for (int t = 0; t < valueCount; t++) { + size += entry.lengths[t]; + } + } else { + entry.currentEditor = null; + for (int t = 0; t < valueCount; t++) { + deleteIfExists(entry.getCleanFile(t)); + deleteIfExists(entry.getDirtyFile(t)); + } + i.remove(); + } + } + } + + /** + * Creates a new journal that omits redundant information. This replaces the + * current journal if it exists. + */ + private synchronized void rebuildJournal() throws IOException { + if (journalWriter != null) { + journalWriter.close(); + } + + Writer writer = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(journalFileTmp), Util.US_ASCII)); + try { + writer.write(MAGIC); + writer.write("\n"); + writer.write(VERSION_1); + writer.write("\n"); + writer.write(Integer.toString(appVersion)); + writer.write("\n"); + writer.write(Integer.toString(valueCount)); + writer.write("\n"); + writer.write("\n"); + + for (Entry entry : lruEntries.values()) { + if (entry.currentEditor != null) { + writer.write(DIRTY + ' ' + entry.key + '\n'); + } else { + writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + + '\n'); + } + } + } finally { + writer.close(); + } + + if (journalFile.exists()) { + renameTo(journalFile, journalFileBackup, true); + } + renameTo(journalFileTmp, journalFile, false); + journalFileBackup.delete(); + + journalWriter = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(journalFile, true), Util.US_ASCII)); + } + + private static void deleteIfExists(File file) throws IOException { + if (file.exists() && !file.delete()) { + throw new IOException(); + } + } + + private static void renameTo(File from, File to, boolean deleteDestination) + throws IOException { + if (deleteDestination) { + deleteIfExists(to); + } + if (!from.renameTo(to)) { + throw new IOException(); + } + } + + /** + * Returns a snapshot of the entry named {@code key}, or null if it doesn't + * exist is not currently readable. If a value is returned, it is moved to + * the head of the LRU queue. + */ + public synchronized Snapshot get(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null) { + return null; + } + + if (!entry.readable) { + return null; + } + + // Open all streams eagerly to guarantee that we see a single published + // snapshot. If we opened streams lazily then the streams could come + // from different edits. + InputStream[] ins = new InputStream[valueCount]; + try { + for (int i = 0; i < valueCount; i++) { + ins[i] = new FileInputStream(entry.getCleanFile(i)); + } + } catch (FileNotFoundException e) { + // A file must have been deleted manually! + for (int i = 0; i < valueCount; i++) { + if (ins[i] != null) { + Util.closeQuietly(ins[i]); + } else { + break; + } + } + return null; + } + + redundantOpCount++; + journalWriter.append(READ + ' ' + key + '\n'); + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths); + } + + /** + * Returns an editor for the entry named {@code key}, or null if another + * edit is in progress. + */ + public Editor edit(String key) throws IOException { + return edit(key, ANY_SEQUENCE_NUMBER); + } + + private synchronized Editor edit(String key, long expectedSequenceNumber) + throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER + && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) { + return null; // Snapshot is stale. + } + if (entry == null) { + entry = new Entry(key); + lruEntries.put(key, entry); + } else if (entry.currentEditor != null) { + return null; // Another edit is in progress. + } + + Editor editor = new Editor(entry); + entry.currentEditor = editor; + + // Flush the journal before creating files to prevent file leaks. + journalWriter.write(DIRTY + ' ' + key + '\n'); + journalWriter.flush(); + return editor; + } + + /** Returns the directory where this cache stores its data. */ + public File getDirectory() { + return directory; + } + + /** + * Returns the maximum number of bytes that this cache should use to store + * its data. + */ + public synchronized long getMaxSize() { + return maxSize; + } + + /** + * Changes the maximum number of bytes the cache can store and queues a job + * to trim the existing store, if necessary. + */ + public synchronized void setMaxSize(long maxSize) { + if (maxSize <= 0) { + throw new IllegalArgumentException("maxSize <= 0"); + } + this.maxSize = maxSize; + executorService.submit(cleanupCallable); + } + + /** + * Returns the number of bytes currently being used to store the values in + * this cache. This may be greater than the max size if a background + * deletion is pending. + */ + public synchronized long size() { + return size; + } + + private synchronized void completeEdit(Editor editor, boolean success) + throws IOException { + Entry entry = editor.entry; + if (entry.currentEditor != editor) { + throw new IllegalStateException(); + } + + // If this edit is creating the entry for the first time, every index + // must have a value. + if (success && !entry.readable) { + for (int i = 0; i < valueCount; i++) { + if (!editor.written[i]) { + editor.abort(); + throw new IllegalStateException( + "Newly created entry didn't create value for index " + + i); + } + if (!entry.getDirtyFile(i).exists()) { + editor.abort(); + return; + } + } + } + + for (int i = 0; i < valueCount; i++) { + File dirty = entry.getDirtyFile(i); + if (success) { + if (dirty.exists()) { + File clean = entry.getCleanFile(i); + dirty.renameTo(clean); + long oldLength = entry.lengths[i]; + long newLength = clean.length(); + entry.lengths[i] = newLength; + size = size - oldLength + newLength; + } + } else { + deleteIfExists(dirty); + } + } + + redundantOpCount++; + entry.currentEditor = null; + if (entry.readable | success) { + entry.readable = true; + journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + + '\n'); + if (success) { + entry.sequenceNumber = nextSequenceNumber++; + } + } else { + lruEntries.remove(entry.key); + journalWriter.write(REMOVE + ' ' + entry.key + '\n'); + } + journalWriter.flush(); + + if (size > maxSize || journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + } + + /** + * We only rebuild the journal when it will halve the size of the journal + * and eliminate at least 2000 ops. + */ + private boolean journalRebuildRequired() { + final int redundantOpCompactThreshold = 2000; + return redundantOpCount >= redundantOpCompactThreshold // + && redundantOpCount >= lruEntries.size(); + } + + /** + * Drops the entry for {@code key} if it exists and can be removed. Entries + * actively being edited cannot be removed. + * + * @return true if an entry was removed. + */ + public synchronized boolean remove(String key) throws IOException { + checkNotClosed(); + validateKey(key); + Entry entry = lruEntries.get(key); + if (entry == null || entry.currentEditor != null) { + return false; + } + + for (int i = 0; i < valueCount; i++) { + File file = entry.getCleanFile(i); + if (file.exists() && !file.delete()) { + throw new IOException("failed to delete " + file); + } + size -= entry.lengths[i]; + entry.lengths[i] = 0; + } + + redundantOpCount++; + journalWriter.append(REMOVE + ' ' + key + '\n'); + lruEntries.remove(key); + + if (journalRebuildRequired()) { + executorService.submit(cleanupCallable); + } + + return true; + } + + /** Returns true if this cache has been closed. */ + public synchronized boolean isClosed() { + return journalWriter == null; + } + + private void checkNotClosed() { if (journalWriter == null) { - return null; // Closed. + throw new IllegalStateException("cache is closed"); + } + } + + /** Force buffered operations to the filesystem. */ + public synchronized void flush() throws IOException { + checkNotClosed(); + trimToSize(); + journalWriter.flush(); + } + + /** Closes this cache. Stored values will remain on the filesystem. */ + public synchronized void close() throws IOException { + if (journalWriter == null) { + return; // Already closed. + } + for (Entry entry : new ArrayList(lruEntries.values())) { + if (entry.currentEditor != null) { + entry.currentEditor.abort(); + } } trimToSize(); - if (journalRebuildRequired()) { - rebuildJournal(); - redundantOpCount = 0; + journalWriter.close(); + journalWriter = null; + } + + private void trimToSize() throws IOException { + while (size > maxSize) { + Map.Entry toEvict = lruEntries.entrySet().iterator() + .next(); + remove(toEvict.getKey()); } - } - return null; - } - }; - - private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) { - this.directory = directory; - this.appVersion = appVersion; - this.journalFile = new File(directory, JOURNAL_FILE); - this.journalFileTmp = new File(directory, JOURNAL_FILE_TEMP); - this.journalFileBackup = new File(directory, JOURNAL_FILE_BACKUP); - this.valueCount = valueCount; - this.maxSize = maxSize; - } - - /** - * Opens the cache in {@code directory}, creating a cache if none exists - * there. - * - * @param directory a writable directory - * @param valueCount the number of values per cache entry. Must be positive. - * @param maxSize the maximum number of bytes this cache should use to store - * @throws IOException if reading or writing the cache directory fails - */ - public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize) - throws IOException { - if (maxSize <= 0) { - throw new IllegalArgumentException("maxSize <= 0"); - } - if (valueCount <= 0) { - throw new IllegalArgumentException("valueCount <= 0"); - } - - // If a bkp file exists, use it instead. - File backupFile = new File(directory, JOURNAL_FILE_BACKUP); - if (backupFile.exists()) { - File journalFile = new File(directory, JOURNAL_FILE); - // If journal file also exists just delete backup file. - if (journalFile.exists()) { - backupFile.delete(); - } else { - renameTo(backupFile, journalFile, false); - } - } - - // Prefer to pick up where we left off. - DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); - if (cache.journalFile.exists()) { - try { - cache.readJournal(); - cache.processJournal(); - cache.journalWriter = new BufferedWriter( - new OutputStreamWriter(new FileOutputStream(cache.journalFile, true), Util.US_ASCII)); - return cache; - } catch (IOException journalIsCorrupt) { - System.out - .println("DiskLruCache " - + directory - + " is corrupt: " - + journalIsCorrupt.getMessage() - + ", removing"); - cache.delete(); - } - } - - // Create a new empty cache. - directory.mkdirs(); - cache = new DiskLruCache(directory, appVersion, valueCount, maxSize); - cache.rebuildJournal(); - return cache; - } - - private void readJournal() throws IOException { - StrictLineReader reader = new StrictLineReader(new FileInputStream(journalFile), Util.US_ASCII); - try { - String magic = reader.readLine(); - String version = reader.readLine(); - String appVersionString = reader.readLine(); - String valueCountString = reader.readLine(); - String blank = reader.readLine(); - if (!MAGIC.equals(magic) - || !VERSION_1.equals(version) - || !Integer.toString(appVersion).equals(appVersionString) - || !Integer.toString(valueCount).equals(valueCountString) - || !"".equals(blank)) { - throw new IOException("unexpected journal header: [" + magic + ", " + version + ", " - + valueCountString + ", " + blank + "]"); - } - - int lineCount = 0; - while (true) { - try { - readJournalLine(reader.readLine()); - lineCount++; - } catch (EOFException endOfJournal) { - break; - } - } - redundantOpCount = lineCount - lruEntries.size(); - } finally { - Util.closeQuietly(reader); - } - } - - private void readJournalLine(String line) throws IOException { - int firstSpace = line.indexOf(' '); - if (firstSpace == -1) { - throw new IOException("unexpected journal line: " + line); - } - - int keyBegin = firstSpace + 1; - int secondSpace = line.indexOf(' ', keyBegin); - final String key; - if (secondSpace == -1) { - key = line.substring(keyBegin); - if (firstSpace == REMOVE.length() && line.startsWith(REMOVE)) { - lruEntries.remove(key); - return; - } - } else { - key = line.substring(keyBegin, secondSpace); - } - - Entry entry = lruEntries.get(key); - if (entry == null) { - entry = new Entry(key); - lruEntries.put(key, entry); - } - - if (secondSpace != -1 && firstSpace == CLEAN.length() && line.startsWith(CLEAN)) { - String[] parts = line.substring(secondSpace + 1).split(" "); - entry.readable = true; - entry.currentEditor = null; - entry.setLengths(parts); - } else if (secondSpace == -1 && firstSpace == DIRTY.length() && line.startsWith(DIRTY)) { - entry.currentEditor = new Editor(entry); - } else if (secondSpace == -1 && firstSpace == READ.length() && line.startsWith(READ)) { - // This work was already done by calling lruEntries.get(). - } else { - throw new IOException("unexpected journal line: " + line); - } - } - - /** - * Computes the initial size and collects garbage as a part of opening the - * cache. Dirty entries are assumed to be inconsistent and will be deleted. - */ - private void processJournal() throws IOException { - deleteIfExists(journalFileTmp); - for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) { - Entry entry = i.next(); - if (entry.currentEditor == null) { - for (int t = 0; t < valueCount; t++) { - size += entry.lengths[t]; - } - } else { - entry.currentEditor = null; - for (int t = 0; t < valueCount; t++) { - deleteIfExists(entry.getCleanFile(t)); - deleteIfExists(entry.getDirtyFile(t)); - } - i.remove(); - } - } - } - - /** - * Creates a new journal that omits redundant information. This replaces the - * current journal if it exists. - */ - private synchronized void rebuildJournal() throws IOException { - if (journalWriter != null) { - journalWriter.close(); - } - - Writer writer = new BufferedWriter( - new OutputStreamWriter(new FileOutputStream(journalFileTmp), Util.US_ASCII)); - try { - writer.write(MAGIC); - writer.write("\n"); - writer.write(VERSION_1); - writer.write("\n"); - writer.write(Integer.toString(appVersion)); - writer.write("\n"); - writer.write(Integer.toString(valueCount)); - writer.write("\n"); - writer.write("\n"); - - for (Entry entry : lruEntries.values()) { - if (entry.currentEditor != null) { - writer.write(DIRTY + ' ' + entry.key + '\n'); - } else { - writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); - } - } - } finally { - writer.close(); - } - - if (journalFile.exists()) { - renameTo(journalFile, journalFileBackup, true); - } - renameTo(journalFileTmp, journalFile, false); - journalFileBackup.delete(); - - journalWriter = new BufferedWriter( - new OutputStreamWriter(new FileOutputStream(journalFile, true), Util.US_ASCII)); - } - - private static void deleteIfExists(File file) throws IOException { - if (file.exists() && !file.delete()) { - throw new IOException(); - } - } - - private static void renameTo(File from, File to, boolean deleteDestination) throws IOException { - if (deleteDestination) { - deleteIfExists(to); - } - if (!from.renameTo(to)) { - throw new IOException(); - } - } - - /** - * Returns a snapshot of the entry named {@code key}, or null if it doesn't - * exist is not currently readable. If a value is returned, it is moved to - * the head of the LRU queue. - */ - public synchronized Snapshot get(String key) throws IOException { - checkNotClosed(); - validateKey(key); - Entry entry = lruEntries.get(key); - if (entry == null) { - return null; - } - - if (!entry.readable) { - return null; - } - - // Open all streams eagerly to guarantee that we see a single published - // snapshot. If we opened streams lazily then the streams could come - // from different edits. - InputStream[] ins = new InputStream[valueCount]; - try { - for (int i = 0; i < valueCount; i++) { - ins[i] = new FileInputStream(entry.getCleanFile(i)); - } - } catch (FileNotFoundException e) { - // A file must have been deleted manually! - for (int i = 0; i < valueCount; i++) { - if (ins[i] != null) { - Util.closeQuietly(ins[i]); - } else { - break; - } - } - return null; - } - - redundantOpCount++; - journalWriter.append(READ + ' ' + key + '\n'); - if (journalRebuildRequired()) { - executorService.submit(cleanupCallable); - } - - return new Snapshot(key, entry.sequenceNumber, ins, entry.lengths); - } - - /** - * Returns an editor for the entry named {@code key}, or null if another - * edit is in progress. - */ - public Editor edit(String key) throws IOException { - return edit(key, ANY_SEQUENCE_NUMBER); - } - - private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException { - checkNotClosed(); - validateKey(key); - Entry entry = lruEntries.get(key); - if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER && (entry == null - || entry.sequenceNumber != expectedSequenceNumber)) { - return null; // Snapshot is stale. - } - if (entry == null) { - entry = new Entry(key); - lruEntries.put(key, entry); - } else if (entry.currentEditor != null) { - return null; // Another edit is in progress. - } - - Editor editor = new Editor(entry); - entry.currentEditor = editor; - - // Flush the journal before creating files to prevent file leaks. - journalWriter.write(DIRTY + ' ' + key + '\n'); - journalWriter.flush(); - return editor; - } - - /** Returns the directory where this cache stores its data. */ - public File getDirectory() { - return directory; - } - - /** - * Returns the maximum number of bytes that this cache should use to store - * its data. - */ - public synchronized long getMaxSize() { - return maxSize; - } - - /** - * Changes the maximum number of bytes the cache can store and queues a job - * to trim the existing store, if necessary. - */ - public synchronized void setMaxSize(long maxSize) { - if (maxSize <= 0) { - throw new IllegalArgumentException("maxSize <= 0"); - } - this.maxSize = maxSize; - executorService.submit(cleanupCallable); - } - - /** - * Returns the number of bytes currently being used to store the values in - * this cache. This may be greater than the max size if a background - * deletion is pending. - */ - public synchronized long size() { - return size; - } - - private synchronized void completeEdit(Editor editor, boolean success) throws IOException { - Entry entry = editor.entry; - if (entry.currentEditor != editor) { - throw new IllegalStateException(); - } - - // If this edit is creating the entry for the first time, every index must have a value. - if (success && !entry.readable) { - for (int i = 0; i < valueCount; i++) { - if (!editor.written[i]) { - editor.abort(); - throw new IllegalStateException("Newly created entry didn't create value for index " + i); - } - if (!entry.getDirtyFile(i).exists()) { - editor.abort(); - return; - } - } - } - - for (int i = 0; i < valueCount; i++) { - File dirty = entry.getDirtyFile(i); - if (success) { - if (dirty.exists()) { - File clean = entry.getCleanFile(i); - dirty.renameTo(clean); - long oldLength = entry.lengths[i]; - long newLength = clean.length(); - entry.lengths[i] = newLength; - size = size - oldLength + newLength; - } - } else { - deleteIfExists(dirty); - } - } - - redundantOpCount++; - entry.currentEditor = null; - if (entry.readable | success) { - entry.readable = true; - journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n'); - if (success) { - entry.sequenceNumber = nextSequenceNumber++; - } - } else { - lruEntries.remove(entry.key); - journalWriter.write(REMOVE + ' ' + entry.key + '\n'); - } - journalWriter.flush(); - - if (size > maxSize || journalRebuildRequired()) { - executorService.submit(cleanupCallable); - } - } - - /** - * We only rebuild the journal when it will halve the size of the journal - * and eliminate at least 2000 ops. - */ - private boolean journalRebuildRequired() { - final int redundantOpCompactThreshold = 2000; - return redundantOpCount >= redundantOpCompactThreshold // - && redundantOpCount >= lruEntries.size(); - } - - /** - * Drops the entry for {@code key} if it exists and can be removed. Entries - * actively being edited cannot be removed. - * - * @return true if an entry was removed. - */ - public synchronized boolean remove(String key) throws IOException { - checkNotClosed(); - validateKey(key); - Entry entry = lruEntries.get(key); - if (entry == null || entry.currentEditor != null) { - return false; - } - - for (int i = 0; i < valueCount; i++) { - File file = entry.getCleanFile(i); - if (file.exists() && !file.delete()) { - throw new IOException("failed to delete " + file); - } - size -= entry.lengths[i]; - entry.lengths[i] = 0; - } - - redundantOpCount++; - journalWriter.append(REMOVE + ' ' + key + '\n'); - lruEntries.remove(key); - - if (journalRebuildRequired()) { - executorService.submit(cleanupCallable); - } - - return true; - } - - /** Returns true if this cache has been closed. */ - public synchronized boolean isClosed() { - return journalWriter == null; - } - - private void checkNotClosed() { - if (journalWriter == null) { - throw new IllegalStateException("cache is closed"); - } - } - - /** Force buffered operations to the filesystem. */ - public synchronized void flush() throws IOException { - checkNotClosed(); - trimToSize(); - journalWriter.flush(); - } - - /** Closes this cache. Stored values will remain on the filesystem. */ - public synchronized void close() throws IOException { - if (journalWriter == null) { - return; // Already closed. - } - for (Entry entry : new ArrayList(lruEntries.values())) { - if (entry.currentEditor != null) { - entry.currentEditor.abort(); - } - } - trimToSize(); - journalWriter.close(); - journalWriter = null; - } - - private void trimToSize() throws IOException { - while (size > maxSize) { - Map.Entry toEvict = lruEntries.entrySet().iterator().next(); - remove(toEvict.getKey()); - } - } - - /** - * Closes the cache and deletes all of its stored values. This will delete - * all files in the cache directory including files that weren't created by - * the cache. - */ - public void delete() throws IOException { - close(); - Util.deleteContents(directory); - // TODO I add - size = 0; - lruEntries.clear(); - rebuildJournal(); - } - - private void validateKey(String key) { - Matcher matcher = LEGAL_KEY_PATTERN.matcher(key); - if (!matcher.matches()) { - throw new IllegalArgumentException("keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\""); - } - } - - private static String inputStreamToString(InputStream in) throws IOException { - return Util.readFully(new InputStreamReader(in, Util.UTF_8)); - } - - /** A snapshot of the values for an entry. */ - public final class Snapshot implements Closeable { - private final String key; - private final long sequenceNumber; - private final InputStream[] ins; - private final long[] lengths; - - private Snapshot(String key, long sequenceNumber, InputStream[] ins, long[] lengths) { - this.key = key; - this.sequenceNumber = sequenceNumber; - this.ins = ins; - this.lengths = lengths; } /** - * Returns an editor for this snapshot's entry, or null if either the - * entry has changed since this snapshot was created or if another edit - * is in progress. + * Closes the cache and deletes all of its stored values. This will delete + * all files in the cache directory including files that weren't created by + * the cache. */ - public Editor edit() throws IOException { - return DiskLruCache.this.edit(key, sequenceNumber); - } - - /** Returns the unbuffered stream with the value for {@code index}. */ - public InputStream getInputStream(int index) { - return ins[index]; - } - - /** Returns the string value for {@code index}. */ - public String getString(int index) throws IOException { - return inputStreamToString(getInputStream(index)); - } - - /** Returns the byte length of the value for {@code index}. */ - public long getLength(int index) { - return lengths[index]; - } - - public void close() { - for (InputStream in : ins) { - Util.closeQuietly(in); - } - } - } - - private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() { - @Override - public void write(int b) throws IOException { - // Eat all writes silently. Nom nom. - } - }; - - /** Edits the values for an entry. */ - public final class Editor { - private final Entry entry; - private final boolean[] written; - private boolean hasErrors; - private boolean committed; - - private Editor(Entry entry) { - this.entry = entry; - this.written = (entry.readable) ? null : new boolean[valueCount]; - } - - /** - * Returns an unbuffered input stream to read the last committed value, - * or null if no value has been committed. - */ - public InputStream newInputStream(int index) throws IOException { - synchronized (DiskLruCache.this) { - if (entry.currentEditor != this) { - throw new IllegalStateException(); + public synchronized void delete() throws IOException { + // Close + if (journalWriter == null) { + return; // Already closed. } - if (!entry.readable) { - return null; + for (Entry entry : new ArrayList(lruEntries.values())) { + if (entry.currentEditor != null) { + entry.currentEditor.abort(); + } } + trimToSize(); + journalWriter.close(); + journalWriter = null; + Util.deleteContents(directory); + // TODO I add + size = 0; + lruEntries.clear(); + + // rebuildJournal + Writer writer = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(journalFileTmp), Util.US_ASCII)); try { - return new FileInputStream(entry.getCleanFile(index)); - } catch (FileNotFoundException e) { - return null; + writer.write(MAGIC); + writer.write("\n"); + writer.write(VERSION_1); + writer.write("\n"); + writer.write(Integer.toString(appVersion)); + writer.write("\n"); + writer.write(Integer.toString(valueCount)); + writer.write("\n"); + writer.write("\n"); + + for (Entry entry : lruEntries.values()) { + if (entry.currentEditor != null) { + writer.write(DIRTY + ' ' + entry.key + '\n'); + } else { + writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + + '\n'); + } + } + } finally { + writer.close(); } - } - } - /** - * Returns the last committed value as a string, or null if no value - * has been committed. - */ - public String getString(int index) throws IOException { - InputStream in = newInputStream(index); - return in != null ? inputStreamToString(in) : null; - } - - /** - * Returns a new unbuffered output stream to write the value at - * {@code index}. If the underlying output stream encounters errors - * when writing to the filesystem, this edit will be aborted when - * {@link #commit} is called. The returned output stream does not throw - * IOExceptions. - */ - public OutputStream newOutputStream(int index) throws IOException { - synchronized (DiskLruCache.this) { - if (entry.currentEditor != this) { - throw new IllegalStateException(); + if (journalFile.exists()) { + renameTo(journalFile, journalFileBackup, true); } - if (!entry.readable) { - written[index] = true; + renameTo(journalFileTmp, journalFile, false); + journalFileBackup.delete(); + + journalWriter = new BufferedWriter(new OutputStreamWriter( + new FileOutputStream(journalFile, true), Util.US_ASCII)); + } + + private void validateKey(String key) { + Matcher matcher = LEGAL_KEY_PATTERN.matcher(key); + if (!matcher.matches()) { + throw new IllegalArgumentException( + "keys must match regex [a-z0-9_-]{1,64}: \"" + key + "\""); } - File dirtyFile = entry.getDirtyFile(index); - FileOutputStream outputStream; - try { - outputStream = new FileOutputStream(dirtyFile); - } catch (FileNotFoundException e) { - // Attempt to recreate the cache directory. - directory.mkdirs(); - try { - outputStream = new FileOutputStream(dirtyFile); - } catch (FileNotFoundException e2) { - // We are unable to recover. Silently eat the writes. - return NULL_OUTPUT_STREAM; - } + } + + private static String inputStreamToString(InputStream in) + throws IOException { + return Util.readFully(new InputStreamReader(in, Util.UTF_8)); + } + + /** A snapshot of the values for an entry. */ + public final class Snapshot implements Closeable { + private final String key; + private final long sequenceNumber; + private final InputStream[] ins; + private final long[] lengths; + + private Snapshot(String key, long sequenceNumber, InputStream[] ins, + long[] lengths) { + this.key = key; + this.sequenceNumber = sequenceNumber; + this.ins = ins; + this.lengths = lengths; } - return new FaultHidingOutputStream(outputStream); - } - } - /** Sets the value at {@code index} to {@code value}. */ - public void set(int index, String value) throws IOException { - Writer writer = null; - try { - writer = new OutputStreamWriter(newOutputStream(index), Util.UTF_8); - writer.write(value); - } finally { - Util.closeQuietly(writer); - } - } - - /** - * Commits this edit so it is visible to readers. This releases the - * edit lock so another edit may be started on the same key. - */ - public void commit() throws IOException { - if (hasErrors) { - completeEdit(this, false); - remove(entry.key); // The previous entry is stale. - } else { - completeEdit(this, true); - } - committed = true; - } - - /** - * Aborts this edit. This releases the edit lock so another edit may be - * started on the same key. - */ - public void abort() throws IOException { - completeEdit(this, false); - } - - public void abortUnlessCommitted() { - if (!committed) { - try { - abort(); - } catch (IOException ignored) { + /** + * Returns an editor for this snapshot's entry, or null if either the + * entry has changed since this snapshot was created or if another edit + * is in progress. + */ + public Editor edit() throws IOException { + return DiskLruCache.this.edit(key, sequenceNumber); } - } - } - private class FaultHidingOutputStream extends FilterOutputStream { - private FaultHidingOutputStream(OutputStream out) { - super(out); - } - - @Override public void write(int oneByte) { - try { - out.write(oneByte); - } catch (IOException e) { - hasErrors = true; + /** Returns the unbuffered stream with the value for {@code index}. */ + public InputStream getInputStream(int index) { + return ins[index]; } - } - @Override public void write(byte[] buffer, int offset, int length) { - try { - out.write(buffer, offset, length); - } catch (IOException e) { - hasErrors = true; + /** Returns the string value for {@code index}. */ + public String getString(int index) throws IOException { + return inputStreamToString(getInputStream(index)); } - } - @Override public void close() { - try { - out.close(); - } catch (IOException e) { - hasErrors = true; + /** Returns the byte length of the value for {@code index}. */ + public long getLength(int index) { + return lengths[index]; } - } - @Override public void flush() { - try { - out.flush(); - } catch (IOException e) { - hasErrors = true; + public void close() { + for (InputStream in : ins) { + Util.closeQuietly(in); + } } - } - } - } - - private final class Entry { - private final String key; - - /** Lengths of this entry's files. */ - private final long[] lengths; - - /** True if this entry has ever been published. */ - private boolean readable; - - /** The ongoing edit or null if this entry is not being edited. */ - private Editor currentEditor; - - /** The sequence number of the most recently committed edit to this entry. */ - private long sequenceNumber; - - private Entry(String key) { - this.key = key; - this.lengths = new long[valueCount]; } - public String getLengths() throws IOException { - StringBuilder result = new StringBuilder(); - for (long size : lengths) { - result.append(' ').append(size); - } - return result.toString(); - } - - /** Set lengths using decimal numbers like "10123". */ - private void setLengths(String[] strings) throws IOException { - if (strings.length != valueCount) { - throw invalidLengths(strings); - } - - try { - for (int i = 0; i < strings.length; i++) { - lengths[i] = Long.parseLong(strings[i]); + private static final OutputStream NULL_OUTPUT_STREAM = new OutputStream() { + @Override + public void write(int b) throws IOException { + // Eat all writes silently. Nom nom. + } + }; + + /** Edits the values for an entry. */ + public final class Editor { + private final Entry entry; + private final boolean[] written; + private boolean hasErrors; + private boolean committed; + + private Editor(Entry entry) { + this.entry = entry; + this.written = (entry.readable) ? null : new boolean[valueCount]; + } + + /** + * Returns an unbuffered input stream to read the last committed value, + * or null if no value has been committed. + */ + public InputStream newInputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + return null; + } + try { + return new FileInputStream(entry.getCleanFile(index)); + } catch (FileNotFoundException e) { + return null; + } + } + } + + /** + * Returns the last committed value as a string, or null if no value has + * been committed. + */ + public String getString(int index) throws IOException { + InputStream in = newInputStream(index); + return in != null ? inputStreamToString(in) : null; + } + + /** + * Returns a new unbuffered output stream to write the value at + * {@code index}. If the underlying output stream encounters errors when + * writing to the filesystem, this edit will be aborted when + * {@link #commit} is called. The returned output stream does not throw + * IOExceptions. + */ + public OutputStream newOutputStream(int index) throws IOException { + synchronized (DiskLruCache.this) { + if (entry.currentEditor != this) { + throw new IllegalStateException(); + } + if (!entry.readable) { + written[index] = true; + } + File dirtyFile = entry.getDirtyFile(index); + FileOutputStream outputStream; + try { + outputStream = new FileOutputStream(dirtyFile); + } catch (FileNotFoundException e) { + // Attempt to recreate the cache directory. + directory.mkdirs(); + try { + outputStream = new FileOutputStream(dirtyFile); + } catch (FileNotFoundException e2) { + // We are unable to recover. Silently eat the writes. + return NULL_OUTPUT_STREAM; + } + } + return new FaultHidingOutputStream(outputStream); + } + } + + /** Sets the value at {@code index} to {@code value}. */ + public void set(int index, String value) throws IOException { + Writer writer = null; + try { + writer = new OutputStreamWriter(newOutputStream(index), + Util.UTF_8); + writer.write(value); + } finally { + Util.closeQuietly(writer); + } + } + + /** + * Commits this edit so it is visible to readers. This releases the edit + * lock so another edit may be started on the same key. + */ + public void commit() throws IOException { + if (hasErrors) { + completeEdit(this, false); + remove(entry.key); // The previous entry is stale. + } else { + completeEdit(this, true); + } + committed = true; + } + + /** + * Aborts this edit. This releases the edit lock so another edit may be + * started on the same key. + */ + public void abort() throws IOException { + completeEdit(this, false); + } + + public void abortUnlessCommitted() { + if (!committed) { + try { + abort(); + } catch (IOException ignored) { + } + } + } + + private class FaultHidingOutputStream extends FilterOutputStream { + private FaultHidingOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(int oneByte) { + try { + out.write(oneByte); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override + public void write(byte[] buffer, int offset, int length) { + try { + out.write(buffer, offset, length); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override + public void close() { + try { + out.close(); + } catch (IOException e) { + hasErrors = true; + } + } + + @Override + public void flush() { + try { + out.flush(); + } catch (IOException e) { + hasErrors = true; + } + } } - } catch (NumberFormatException e) { - throw invalidLengths(strings); - } } - private IOException invalidLengths(String[] strings) throws IOException { - throw new IOException("unexpected journal line: " + java.util.Arrays.toString(strings)); - } + private final class Entry { + private final String key; - public File getCleanFile(int i) { - return new File(directory, key + "." + i); - } + /** Lengths of this entry's files. */ + private final long[] lengths; - public File getDirtyFile(int i) { - return new File(directory, key + "." + i + ".tmp"); + /** True if this entry has ever been published. */ + private boolean readable; + + /** The ongoing edit or null if this entry is not being edited. */ + private Editor currentEditor; + + /** + * The sequence number of the most recently committed edit to this + * entry. + */ + private long sequenceNumber; + + private Entry(String key) { + this.key = key; + this.lengths = new long[valueCount]; + } + + public String getLengths() throws IOException { + StringBuilder result = new StringBuilder(); + for (long size : lengths) { + result.append(' ').append(size); + } + return result.toString(); + } + + /** Set lengths using decimal numbers like "10123". */ + private void setLengths(String[] strings) throws IOException { + if (strings.length != valueCount) { + throw invalidLengths(strings); + } + + try { + for (int i = 0; i < strings.length; i++) { + lengths[i] = Long.parseLong(strings[i]); + } + } catch (NumberFormatException e) { + throw invalidLengths(strings); + } + } + + private IOException invalidLengths(String[] strings) throws IOException { + throw new IOException("unexpected journal line: " + + java.util.Arrays.toString(strings)); + } + + public File getCleanFile(int i) { + return new File(directory, key + "." + i); + } + + public File getDirtyFile(int i) { + return new File(directory, key + "." + i + ".tmp"); + } } - } }