createRepo(@Body Map map);
113 | }
114 |
115 | public ApiService getApiService() {
116 | OkHttpClient.Builder builder = new OkHttpClient.Builder();
117 | builder.addInterceptor(new EnhancedCacheInterceptor());
118 |
119 | // File cacheDir = new File(getCacheDir(), "response");
120 | // //缓存的最大尺寸10m
121 | // Cache cache = new Cache(cacheDir, 1024 * 1024 * 10);
122 | // builder.cache(cache);
123 | // builder.addInterceptor(new CacheInterceptor());
124 |
125 | OkHttpClient client = builder.build();
126 | Retrofit retrofit = new Retrofit.Builder()
127 | .client(client)
128 | .baseUrl(BASE_URL)
129 | .addConverterFactory(GsonConverterFactory.create())
130 | .build();
131 | return retrofit.create(ApiService.class);
132 | }
133 |
134 | public void clear(View view) {
135 | tvResponse.setText("");
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # Attempt to set APP_HOME
46 | # Resolve links: $0 may be a link
47 | PRG="$0"
48 | # Need this for relative symlinks.
49 | while [ -h "$PRG" ] ; do
50 | ls=`ls -ld "$PRG"`
51 | link=`expr "$ls" : '.*-> \(.*\)$'`
52 | if expr "$link" : '/.*' > /dev/null; then
53 | PRG="$link"
54 | else
55 | PRG=`dirname "$PRG"`"/$link"
56 | fi
57 | done
58 | SAVED="`pwd`"
59 | cd "`dirname \"$PRG\"`/" >/dev/null
60 | APP_HOME="`pwd -P`"
61 | cd "$SAVED" >/dev/null
62 |
63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
64 |
65 | # Determine the Java command to use to start the JVM.
66 | if [ -n "$JAVA_HOME" ] ; then
67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
68 | # IBM's JDK on AIX uses strange locations for the executables
69 | JAVACMD="$JAVA_HOME/jre/sh/java"
70 | else
71 | JAVACMD="$JAVA_HOME/bin/java"
72 | fi
73 | if [ ! -x "$JAVACMD" ] ; then
74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
75 |
76 | Please set the JAVA_HOME variable in your environment to match the
77 | location of your Java installation."
78 | fi
79 | else
80 | JAVACMD="java"
81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
82 |
83 | Please set the JAVA_HOME variable in your environment to match the
84 | location of your Java installation."
85 | fi
86 |
87 | # Increase the maximum file descriptors if we can.
88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
89 | MAX_FD_LIMIT=`ulimit -H -n`
90 | if [ $? -eq 0 ] ; then
91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
92 | MAX_FD="$MAX_FD_LIMIT"
93 | fi
94 | ulimit -n $MAX_FD
95 | if [ $? -ne 0 ] ; then
96 | warn "Could not set maximum file descriptor limit: $MAX_FD"
97 | fi
98 | else
99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
100 | fi
101 | fi
102 |
103 | # For Darwin, add options to specify how the application appears in the dock
104 | if $darwin; then
105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
106 | fi
107 |
108 | # For Cygwin, switch paths to Windows format before running java
109 | if $cygwin ; then
110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
112 | JAVACMD=`cygpath --unix "$JAVACMD"`
113 |
114 | # We build the pattern for arguments to be converted via cygpath
115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
116 | SEP=""
117 | for dir in $ROOTDIRSRAW ; do
118 | ROOTDIRS="$ROOTDIRS$SEP$dir"
119 | SEP="|"
120 | done
121 | OURCYGPATTERN="(^($ROOTDIRS))"
122 | # Add a user-defined pattern to the cygpath arguments
123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
125 | fi
126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
127 | i=0
128 | for arg in "$@" ; do
129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
131 |
132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
134 | else
135 | eval `echo args$i`="\"$arg\""
136 | fi
137 | i=$((i+1))
138 | done
139 | case $i in
140 | (0) set -- ;;
141 | (1) set -- "$args0" ;;
142 | (2) set -- "$args0" "$args1" ;;
143 | (3) set -- "$args0" "$args1" "$args2" ;;
144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
150 | esac
151 | fi
152 |
153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
154 | function splitJvmOpts() {
155 | JVM_OPTS=("$@")
156 | }
157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
159 |
160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
161 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wangyi/demo/cache/CacheManager.java:
--------------------------------------------------------------------------------
1 | package com.wangyi.demo.cache;
2 |
3 | import android.content.Context;
4 | import android.content.pm.PackageInfo;
5 | import android.content.pm.PackageManager;
6 | import android.util.Log;
7 |
8 | import com.wangyi.demo.AppIml;
9 |
10 | import java.io.ByteArrayOutputStream;
11 | import java.io.File;
12 | import java.io.FileInputStream;
13 | import java.io.IOException;
14 | import java.io.OutputStream;
15 | import java.io.UnsupportedEncodingException;
16 | import java.security.MessageDigest;
17 | import java.security.NoSuchAlgorithmException;
18 |
19 | /**
20 | * Created on 2016/11/29.
21 | *
22 | * @author WangYi
23 | */
24 |
25 | public final class CacheManager {
26 |
27 | public static final String TAG = "CacheManager";
28 |
29 | //max cache size 10mb
30 | private static final long DISK_CACHE_SIZE = 1024 * 1024 * 10;
31 |
32 | private static final int DISK_CACHE_INDEX = 0;
33 |
34 | private static final String CACHE_DIR = "responses";
35 |
36 | private DiskLruCache mDiskLruCache;
37 |
38 | private volatile static CacheManager mCacheManager;
39 |
40 | public static CacheManager getInstance() {
41 | if (mCacheManager == null) {
42 | synchronized (CacheManager.class) {
43 | if (mCacheManager == null) {
44 | mCacheManager = new CacheManager();
45 | }
46 | }
47 | }
48 | return mCacheManager;
49 | }
50 |
51 | private CacheManager() {
52 | File diskCacheDir = getDiskCacheDir(AppIml.appContext, CACHE_DIR);
53 | if (!diskCacheDir.exists()) {
54 | boolean b = diskCacheDir.mkdirs();
55 | Log.d(TAG, "!diskCacheDir.exists() --- diskCacheDir.mkdirs()=" + b);
56 | }
57 | if (diskCacheDir.getUsableSpace() > DISK_CACHE_SIZE) {
58 | try {
59 | mDiskLruCache = DiskLruCache.open(diskCacheDir,
60 | getAppVersion(AppIml.appContext), 1/*一个key对应多少个文件*/, DISK_CACHE_SIZE);
61 | Log.d(TAG, "mDiskLruCache created");
62 | } catch (IOException e) {
63 | e.printStackTrace();
64 | }
65 | }
66 | }
67 |
68 | /**
69 | * 同步设置缓存
70 | */
71 | public void putCache(String key, String value) {
72 | if (mDiskLruCache == null) return;
73 | OutputStream os = null;
74 | try {
75 | DiskLruCache.Editor editor = mDiskLruCache.edit(encryptMD5(key));
76 | os = editor.newOutputStream(DISK_CACHE_INDEX);
77 | os.write(value.getBytes());
78 | os.flush();
79 | editor.commit();
80 | mDiskLruCache.flush();
81 | } catch (IOException e) {
82 | e.printStackTrace();
83 | } finally {
84 | if (os != null) {
85 | try {
86 | os.close();
87 | } catch (IOException e) {
88 | e.printStackTrace();
89 | }
90 | }
91 | }
92 | }
93 |
94 | /**
95 | * 异步设置缓存
96 | */
97 | public void setCache(final String key, final String value) {
98 | new Thread() {
99 | @Override
100 | public void run() {
101 | putCache(key, value);
102 | }
103 | }.start();
104 | }
105 |
106 | /**
107 | * 同步获取缓存
108 | */
109 | public String getCache(String key) {
110 | if (mDiskLruCache == null) {
111 | return null;
112 | }
113 | FileInputStream fis = null;
114 | ByteArrayOutputStream bos = null;
115 | try {
116 | DiskLruCache.Snapshot snapshot = mDiskLruCache.get(encryptMD5(key));
117 | if (snapshot != null) {
118 | fis = (FileInputStream) snapshot.getInputStream(DISK_CACHE_INDEX);
119 | bos = new ByteArrayOutputStream();
120 | byte[] buf = new byte[1024];
121 | int len;
122 | while ((len = fis.read(buf)) != -1) {
123 | bos.write(buf, 0, len);
124 | }
125 | byte[] data = bos.toByteArray();
126 | return new String(data);
127 | }
128 | } catch (IOException e) {
129 | e.printStackTrace();
130 | } finally {
131 | if (fis != null) {
132 | try {
133 | fis.close();
134 | } catch (IOException e) {
135 | e.printStackTrace();
136 | }
137 | }
138 | if (bos != null) {
139 | try {
140 | bos.close();
141 | } catch (IOException e) {
142 | e.printStackTrace();
143 | }
144 | }
145 | }
146 | return null;
147 | }
148 |
149 | /**
150 | * 异步获取缓存
151 | */
152 | public void getCache(final String key, final CacheCallback callback) {
153 | new Thread() {
154 | @Override
155 | public void run() {
156 | String cache = getCache(key);
157 | callback.onGetCache(cache);
158 | }
159 | }.start();
160 | }
161 |
162 | /**
163 | * 移除缓存
164 | */
165 | public boolean removeCache(String key) {
166 | if (mDiskLruCache != null) {
167 | try {
168 | return mDiskLruCache.remove(encryptMD5(key));
169 | } catch (IOException e) {
170 | e.printStackTrace();
171 | }
172 | }
173 | return false;
174 | }
175 |
176 | /**
177 | * 获取缓存目录
178 | */
179 | private File getDiskCacheDir(Context context, String uniqueName) {
180 | String cachePath = context.getCacheDir().getPath();
181 | return new File(cachePath + File.separator + uniqueName);
182 | }
183 |
184 | /**
185 | * 对字符串进行MD5编码
186 | */
187 | public static String encryptMD5(String string) {
188 | try {
189 | byte[] hash = MessageDigest.getInstance("MD5").digest(
190 | string.getBytes("UTF-8"));
191 | StringBuilder hex = new StringBuilder(hash.length * 2);
192 | for (byte b : hash) {
193 | if ((b & 0xFF) < 0x10) {
194 | hex.append("0");
195 | }
196 | hex.append(Integer.toHexString(b & 0xFF));
197 | }
198 | return hex.toString();
199 | } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
200 | e.printStackTrace();
201 | }
202 | return string;
203 | }
204 |
205 | /**
206 | * 获取APP版本号
207 | */
208 | private int getAppVersion(Context context) {
209 | PackageManager pm = context.getPackageManager();
210 | try {
211 | PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
212 | return pi == null ? 0 : pi.versionCode;
213 | } catch (PackageManager.NameNotFoundException e) {
214 | e.printStackTrace();
215 | }
216 | return 0;
217 | }
218 | }
219 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wangyi/demo/User.java:
--------------------------------------------------------------------------------
1 | package com.wangyi.demo;
2 |
3 | import java.io.Serializable;
4 |
5 | /**
6 | * Created on 2016/12/4.
7 | *
8 | * @author WangYi
9 | */
10 |
11 |
12 | public class User implements Serializable {
13 |
14 | /**
15 | * owner_url : https://api.github.com/repos/api-playground/projects-test
16 | * url : https://api.github.com/projects/1002604
17 | * id : 1002604
18 | * name : Projects Documentation
19 | * body : Developer documentation project for the developer site.
20 | * number : 1
21 | * creator : {"login":"octocat","id":1,"avatar_url":"https://github.com/images/error/octocat_happy.gif","gravatar_id":"","url":"https://api.github.com/users/octocat","html_url":"https://github.com/octocat","followers_url":"https://api.github.com/users/octocat/followers","following_url":"https://api.github.com/users/octocat/following{/other_user}","gists_url":"https://api.github.com/users/octocat/gists{/gist_id}","starred_url":"https://api.github.com/users/octocat/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/octocat/subscriptions","organizations_url":"https://api.github.com/users/octocat/orgs","repos_url":"https://api.github.com/users/octocat/repos","events_url":"https://api.github.com/users/octocat/events{/privacy}","received_events_url":"https://api.github.com/users/octocat/received_events","type":"User","site_admin":false}
22 | * created_at : 2011-04-10T20:09:31Z
23 | * updated_at : 2014-03-03T18:58:10Z
24 | */
25 |
26 | private String owner_url;
27 | private String url;
28 | private int id;
29 | private String name;
30 | private String body;
31 | private int number;
32 | /**
33 | * login : octocat
34 | * id : 1
35 | * avatar_url : https://github.com/images/error/octocat_happy.gif
36 | * gravatar_id :
37 | * url : https://api.github.com/users/octocat
38 | * html_url : https://github.com/octocat
39 | * followers_url : https://api.github.com/users/octocat/followers
40 | * following_url : https://api.github.com/users/octocat/following{/other_user}
41 | * gists_url : https://api.github.com/users/octocat/gists{/gist_id}
42 | * starred_url : https://api.github.com/users/octocat/starred{/owner}{/repo}
43 | * subscriptions_url : https://api.github.com/users/octocat/subscriptions
44 | * organizations_url : https://api.github.com/users/octocat/orgs
45 | * repos_url : https://api.github.com/users/octocat/repos
46 | * events_url : https://api.github.com/users/octocat/events{/privacy}
47 | * received_events_url : https://api.github.com/users/octocat/received_events
48 | * type : User
49 | * site_admin : false
50 | */
51 |
52 | private CreatorBean creator;
53 | private String created_at;
54 | private String updated_at;
55 |
56 | public String getOwner_url() {
57 | return owner_url;
58 | }
59 |
60 | public void setOwner_url(String owner_url) {
61 | this.owner_url = owner_url;
62 | }
63 |
64 | public String getUrl() {
65 | return url;
66 | }
67 |
68 | public void setUrl(String url) {
69 | this.url = url;
70 | }
71 |
72 | public int getId() {
73 | return id;
74 | }
75 |
76 | public void setId(int id) {
77 | this.id = id;
78 | }
79 |
80 | public String getName() {
81 | return name;
82 | }
83 |
84 | public void setName(String name) {
85 | this.name = name;
86 | }
87 |
88 | public String getBody() {
89 | return body;
90 | }
91 |
92 | public void setBody(String body) {
93 | this.body = body;
94 | }
95 |
96 | public int getNumber() {
97 | return number;
98 | }
99 |
100 | public void setNumber(int number) {
101 | this.number = number;
102 | }
103 |
104 | public CreatorBean getCreator() {
105 | return creator;
106 | }
107 |
108 | public void setCreator(CreatorBean creator) {
109 | this.creator = creator;
110 | }
111 |
112 | public String getCreated_at() {
113 | return created_at;
114 | }
115 |
116 | public void setCreated_at(String created_at) {
117 | this.created_at = created_at;
118 | }
119 |
120 | public String getUpdated_at() {
121 | return updated_at;
122 | }
123 |
124 | public void setUpdated_at(String updated_at) {
125 | this.updated_at = updated_at;
126 | }
127 |
128 | public static class CreatorBean {
129 | private String login;
130 | private int id;
131 | private String avatar_url;
132 | private String gravatar_id;
133 | private String url;
134 | private String html_url;
135 | private String followers_url;
136 | private String following_url;
137 | private String gists_url;
138 | private String starred_url;
139 | private String subscriptions_url;
140 | private String organizations_url;
141 | private String repos_url;
142 | private String events_url;
143 | private String received_events_url;
144 | private String type;
145 | private boolean site_admin;
146 |
147 | public String getLogin() {
148 | return login;
149 | }
150 |
151 | public void setLogin(String login) {
152 | this.login = login;
153 | }
154 |
155 | public int getId() {
156 | return id;
157 | }
158 |
159 | public void setId(int id) {
160 | this.id = id;
161 | }
162 |
163 | public String getAvatar_url() {
164 | return avatar_url;
165 | }
166 |
167 | public void setAvatar_url(String avatar_url) {
168 | this.avatar_url = avatar_url;
169 | }
170 |
171 | public String getGravatar_id() {
172 | return gravatar_id;
173 | }
174 |
175 | public void setGravatar_id(String gravatar_id) {
176 | this.gravatar_id = gravatar_id;
177 | }
178 |
179 | public String getUrl() {
180 | return url;
181 | }
182 |
183 | public void setUrl(String url) {
184 | this.url = url;
185 | }
186 |
187 | public String getHtml_url() {
188 | return html_url;
189 | }
190 |
191 | public void setHtml_url(String html_url) {
192 | this.html_url = html_url;
193 | }
194 |
195 | public String getFollowers_url() {
196 | return followers_url;
197 | }
198 |
199 | public void setFollowers_url(String followers_url) {
200 | this.followers_url = followers_url;
201 | }
202 |
203 | public String getFollowing_url() {
204 | return following_url;
205 | }
206 |
207 | public void setFollowing_url(String following_url) {
208 | this.following_url = following_url;
209 | }
210 |
211 | public String getGists_url() {
212 | return gists_url;
213 | }
214 |
215 | public void setGists_url(String gists_url) {
216 | this.gists_url = gists_url;
217 | }
218 |
219 | public String getStarred_url() {
220 | return starred_url;
221 | }
222 |
223 | public void setStarred_url(String starred_url) {
224 | this.starred_url = starred_url;
225 | }
226 |
227 | public String getSubscriptions_url() {
228 | return subscriptions_url;
229 | }
230 |
231 | public void setSubscriptions_url(String subscriptions_url) {
232 | this.subscriptions_url = subscriptions_url;
233 | }
234 |
235 | public String getOrganizations_url() {
236 | return organizations_url;
237 | }
238 |
239 | public void setOrganizations_url(String organizations_url) {
240 | this.organizations_url = organizations_url;
241 | }
242 |
243 | public String getRepos_url() {
244 | return repos_url;
245 | }
246 |
247 | public void setRepos_url(String repos_url) {
248 | this.repos_url = repos_url;
249 | }
250 |
251 | public String getEvents_url() {
252 | return events_url;
253 | }
254 |
255 | public void setEvents_url(String events_url) {
256 | this.events_url = events_url;
257 | }
258 |
259 | public String getReceived_events_url() {
260 | return received_events_url;
261 | }
262 |
263 | public void setReceived_events_url(String received_events_url) {
264 | this.received_events_url = received_events_url;
265 | }
266 |
267 | public String getType() {
268 | return type;
269 | }
270 |
271 | public void setType(String type) {
272 | this.type = type;
273 | }
274 |
275 | public boolean isSite_admin() {
276 | return site_admin;
277 | }
278 |
279 | public void setSite_admin(boolean site_admin) {
280 | this.site_admin = site_admin;
281 | }
282 | }
283 | }
284 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wangyi/demo/UserList.java:
--------------------------------------------------------------------------------
1 | package com.wangyi.demo;
2 |
3 | /**
4 | * Created on 2016/12/4.
5 | *
6 | * @author WangYi
7 | */
8 |
9 |
10 | public class UserList {
11 |
12 | /**
13 | * login : list
14 | * id : 1133421
15 | * avatar_url : https://avatars.githubusercontent.com/u/1133421?v=3
16 | * gravatar_id :
17 | * url : https://api.github.com/users/list
18 | * html_url : https://github.com/list
19 | * followers_url : https://api.github.com/users/list/followers
20 | * following_url : https://api.github.com/users/list/following{/other_user}
21 | * gists_url : https://api.github.com/users/list/gists{/gist_id}
22 | * starred_url : https://api.github.com/users/list/starred{/owner}{/repo}
23 | * subscriptions_url : https://api.github.com/users/list/subscriptions
24 | * organizations_url : https://api.github.com/users/list/orgs
25 | * repos_url : https://api.github.com/users/list/repos
26 | * events_url : https://api.github.com/users/list/events{/privacy}
27 | * received_events_url : https://api.github.com/users/list/received_events
28 | * type : User
29 | * site_admin : false
30 | * name : null
31 | * company : null
32 | * blog : null
33 | * location : null
34 | * email : null
35 | * hireable : null
36 | * bio : null
37 | * public_repos : 1
38 | * public_gists : 3
39 | * followers : 0
40 | * following : 0
41 | * created_at : 2011-10-17T14:53:35Z
42 | * updated_at : 2016-02-27T00:59:03Z
43 | */
44 |
45 | private String login;
46 | private int id;
47 | private String avatar_url;
48 | private String gravatar_id;
49 | private String url;
50 | private String html_url;
51 | private String followers_url;
52 | private String following_url;
53 | private String gists_url;
54 | private String starred_url;
55 | private String subscriptions_url;
56 | private String organizations_url;
57 | private String repos_url;
58 | private String events_url;
59 | private String received_events_url;
60 | private String type;
61 | private boolean site_admin;
62 | private Object name;
63 | private Object company;
64 | private Object blog;
65 | private Object location;
66 | private Object email;
67 | private Object hireable;
68 | private Object bio;
69 | private int public_repos;
70 | private int public_gists;
71 | private int followers;
72 | private int following;
73 | private String created_at;
74 | private String updated_at;
75 |
76 | public String getLogin() {
77 | return login;
78 | }
79 |
80 | public void setLogin(String login) {
81 | this.login = login;
82 | }
83 |
84 | public int getId() {
85 | return id;
86 | }
87 |
88 | public void setId(int id) {
89 | this.id = id;
90 | }
91 |
92 | public String getAvatar_url() {
93 | return avatar_url;
94 | }
95 |
96 | public void setAvatar_url(String avatar_url) {
97 | this.avatar_url = avatar_url;
98 | }
99 |
100 | public String getGravatar_id() {
101 | return gravatar_id;
102 | }
103 |
104 | public void setGravatar_id(String gravatar_id) {
105 | this.gravatar_id = gravatar_id;
106 | }
107 |
108 | public String getUrl() {
109 | return url;
110 | }
111 |
112 | public void setUrl(String url) {
113 | this.url = url;
114 | }
115 |
116 | public String getHtml_url() {
117 | return html_url;
118 | }
119 |
120 | public void setHtml_url(String html_url) {
121 | this.html_url = html_url;
122 | }
123 |
124 | public String getFollowers_url() {
125 | return followers_url;
126 | }
127 |
128 | public void setFollowers_url(String followers_url) {
129 | this.followers_url = followers_url;
130 | }
131 |
132 | public String getFollowing_url() {
133 | return following_url;
134 | }
135 |
136 | public void setFollowing_url(String following_url) {
137 | this.following_url = following_url;
138 | }
139 |
140 | public String getGists_url() {
141 | return gists_url;
142 | }
143 |
144 | public void setGists_url(String gists_url) {
145 | this.gists_url = gists_url;
146 | }
147 |
148 | public String getStarred_url() {
149 | return starred_url;
150 | }
151 |
152 | public void setStarred_url(String starred_url) {
153 | this.starred_url = starred_url;
154 | }
155 |
156 | public String getSubscriptions_url() {
157 | return subscriptions_url;
158 | }
159 |
160 | public void setSubscriptions_url(String subscriptions_url) {
161 | this.subscriptions_url = subscriptions_url;
162 | }
163 |
164 | public String getOrganizations_url() {
165 | return organizations_url;
166 | }
167 |
168 | public void setOrganizations_url(String organizations_url) {
169 | this.organizations_url = organizations_url;
170 | }
171 |
172 | public String getRepos_url() {
173 | return repos_url;
174 | }
175 |
176 | public void setRepos_url(String repos_url) {
177 | this.repos_url = repos_url;
178 | }
179 |
180 | public String getEvents_url() {
181 | return events_url;
182 | }
183 |
184 | public void setEvents_url(String events_url) {
185 | this.events_url = events_url;
186 | }
187 |
188 | public String getReceived_events_url() {
189 | return received_events_url;
190 | }
191 |
192 | public void setReceived_events_url(String received_events_url) {
193 | this.received_events_url = received_events_url;
194 | }
195 |
196 | public String getType() {
197 | return type;
198 | }
199 |
200 | public void setType(String type) {
201 | this.type = type;
202 | }
203 |
204 | public boolean isSite_admin() {
205 | return site_admin;
206 | }
207 |
208 | public void setSite_admin(boolean site_admin) {
209 | this.site_admin = site_admin;
210 | }
211 |
212 | public Object getName() {
213 | return name;
214 | }
215 |
216 | public void setName(Object name) {
217 | this.name = name;
218 | }
219 |
220 | public Object getCompany() {
221 | return company;
222 | }
223 |
224 | public void setCompany(Object company) {
225 | this.company = company;
226 | }
227 |
228 | public Object getBlog() {
229 | return blog;
230 | }
231 |
232 | public void setBlog(Object blog) {
233 | this.blog = blog;
234 | }
235 |
236 | public Object getLocation() {
237 | return location;
238 | }
239 |
240 | public void setLocation(Object location) {
241 | this.location = location;
242 | }
243 |
244 | public Object getEmail() {
245 | return email;
246 | }
247 |
248 | public void setEmail(Object email) {
249 | this.email = email;
250 | }
251 |
252 | public Object getHireable() {
253 | return hireable;
254 | }
255 |
256 | public void setHireable(Object hireable) {
257 | this.hireable = hireable;
258 | }
259 |
260 | public Object getBio() {
261 | return bio;
262 | }
263 |
264 | public void setBio(Object bio) {
265 | this.bio = bio;
266 | }
267 |
268 | public int getPublic_repos() {
269 | return public_repos;
270 | }
271 |
272 | public void setPublic_repos(int public_repos) {
273 | this.public_repos = public_repos;
274 | }
275 |
276 | public int getPublic_gists() {
277 | return public_gists;
278 | }
279 |
280 | public void setPublic_gists(int public_gists) {
281 | this.public_gists = public_gists;
282 | }
283 |
284 | public int getFollowers() {
285 | return followers;
286 | }
287 |
288 | public void setFollowers(int followers) {
289 | this.followers = followers;
290 | }
291 |
292 | public int getFollowing() {
293 | return following;
294 | }
295 |
296 | public void setFollowing(int following) {
297 | this.following = following;
298 | }
299 |
300 | public String getCreated_at() {
301 | return created_at;
302 | }
303 |
304 | public void setCreated_at(String created_at) {
305 | this.created_at = created_at;
306 | }
307 |
308 | public String getUpdated_at() {
309 | return updated_at;
310 | }
311 |
312 | public void setUpdated_at(String updated_at) {
313 | this.updated_at = updated_at;
314 | }
315 |
316 | @Override
317 | public String toString() {
318 | return "UserList{" +
319 | "login='" + login + '\'' +
320 | ", id=" + id +
321 | ", avatar_url='" + avatar_url + '\'' +
322 | ", gravatar_id='" + gravatar_id + '\'' +
323 | ", url='" + url + '\'' +
324 | ", html_url='" + html_url + '\'' +
325 | ", followers_url='" + followers_url + '\'' +
326 | ", following_url='" + following_url + '\'' +
327 | ", gists_url='" + gists_url + '\'' +
328 | ", starred_url='" + starred_url + '\'' +
329 | ", subscriptions_url='" + subscriptions_url + '\'' +
330 | ", organizations_url='" + organizations_url + '\'' +
331 | ", repos_url='" + repos_url + '\'' +
332 | ", events_url='" + events_url + '\'' +
333 | ", received_events_url='" + received_events_url + '\'' +
334 | ", type='" + type + '\'' +
335 | ", site_admin=" + site_admin +
336 | ", name=" + name +
337 | ", company=" + company +
338 | ", blog=" + blog +
339 | ", location=" + location +
340 | ", email=" + email +
341 | ", hireable=" + hireable +
342 | ", bio=" + bio +
343 | ", public_repos=" + public_repos +
344 | ", public_gists=" + public_gists +
345 | ", followers=" + followers +
346 | ", following=" + following +
347 | ", created_at='" + created_at + '\'' +
348 | ", updated_at='" + updated_at + '\'' +
349 | '}';
350 | }
351 | }
352 |
--------------------------------------------------------------------------------
/app/src/main/java/com/wangyi/demo/cache/DiskLruCache.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright (C) 2011 The Android Open Source Project
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * http://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package com.wangyi.demo.cache;
18 |
19 | import java.io.BufferedInputStream;
20 | import java.io.BufferedWriter;
21 | import java.io.Closeable;
22 | import java.io.EOFException;
23 | import java.io.File;
24 | import java.io.FileInputStream;
25 | import java.io.FileNotFoundException;
26 | import java.io.FileOutputStream;
27 | import java.io.FileWriter;
28 | import java.io.FilterOutputStream;
29 | import java.io.IOException;
30 | import java.io.InputStream;
31 | import java.io.InputStreamReader;
32 | import java.io.OutputStream;
33 | import java.io.OutputStreamWriter;
34 | import java.io.Reader;
35 | import java.io.StringWriter;
36 | import java.io.Writer;
37 | import java.lang.reflect.Array;
38 | import java.nio.charset.Charset;
39 | import java.util.ArrayList;
40 | import java.util.Arrays;
41 | import java.util.Iterator;
42 | import java.util.LinkedHashMap;
43 | import java.util.Map;
44 | import java.util.concurrent.Callable;
45 | import java.util.concurrent.ExecutorService;
46 | import java.util.concurrent.LinkedBlockingQueue;
47 | import java.util.concurrent.ThreadPoolExecutor;
48 | import java.util.concurrent.TimeUnit;
49 |
50 | /**
51 | * *****************************************************************************
52 | * Taken from the JB source code, can be found in:
53 | * libcore/luni/src/main/java/libcore/io/DiskLruCache.java
54 | * or direct link:
55 | * https://android.googlesource.com/platform/libcore/+/android-4.1.1_r1/luni/src/main/java/libcore/io/DiskLruCache.java
56 | * *****************************************************************************
57 | *
58 | * A cache that uses a bounded amount of space on a filesystem. Each cache
59 | * entry has a string key and a fixed number of values. Values are byte
60 | * sequences, accessible as streams or files. Each value must be between {@code
61 | * 0} and {@code Integer.MAX_VALUE} bytes in length.
62 | *
63 | *
The cache stores its data in a directory on the filesystem. This
64 | * directory must be exclusive to the cache; the cache may delete or overwrite
65 | * files from its directory. It is an error for multiple processes to use the
66 | * same cache directory at the same time.
67 | *
68 | *
This cache limits the number of bytes that it will store on the
69 | * filesystem. When the number of stored bytes exceeds the limit, the cache will
70 | * remove entries in the background until the limit is satisfied. The limit is
71 | * not strict: the cache may temporarily exceed it while waiting for files to be
72 | * deleted. The limit does not include filesystem overhead or the cache
73 | * journal so space-sensitive applications should set a conservative limit.
74 | *
75 | *
Clients call {@link #edit} to create or update the values of an entry. An
76 | * entry may have only one editor at one time; if a value is not available to be
77 | * edited then {@link #edit} will return null.
78 | *
79 | * - When an entry is being created it is necessary to
80 | * supply a full set of values; the empty value should be used as a
81 | * placeholder if necessary.
82 | *
- When an entry is being edited, it is not necessary
83 | * to supply data for every value; values default to their previous
84 | * value.
85 | *
86 | * Every {@link #edit} call must be matched by a call to {@link Editor#commit}
87 | * or {@link Editor#abort}. Committing is atomic: a read observes the full set
88 | * of values as they were before or after the commit, but never a mix of values.
89 | *
90 | *
Clients call {@link #get} to read a snapshot of an entry. The read will
91 | * observe the value at the time that {@link #get} was called. Updates and
92 | * removals after the call do not impact ongoing reads.
93 | *
94 | *
This class is tolerant of some I/O errors. If files are missing from the
95 | * filesystem, the corresponding entries will be dropped from the cache. If
96 | * an error occurs while writing a cache value, the edit will fail silently.
97 | * Callers should handle other problems by catching {@code IOException} and
98 | * responding appropriately.
99 | */
100 | public final class DiskLruCache implements Closeable {
101 | static final String JOURNAL_FILE = "journal";
102 | static final String JOURNAL_FILE_TMP = "journal.tmp";
103 | static final String MAGIC = "libcore.io.DiskLruCache";
104 | static final String VERSION_1 = "1";
105 | static final long ANY_SEQUENCE_NUMBER = -1;
106 | private static final String CLEAN = "CLEAN";
107 | private static final String DIRTY = "DIRTY";
108 | private static final String REMOVE = "REMOVE";
109 | private static final String READ = "READ";
110 |
111 | private static final Charset UTF_8 = Charset.forName("UTF-8");
112 | private static final int IO_BUFFER_SIZE = 8 * 1024;
113 |
114 | /*
115 | * This cache uses a journal file named "journal". A typical journal file
116 | * looks like this:
117 | * libcore.io.DiskLruCache
118 | * 1
119 | * 100
120 | * 2
121 | *
122 | * CLEAN 3400330d1dfc7f3f7f4b8d4d803dfcf6 832 21054
123 | * DIRTY 335c4c6028171cfddfbaae1a9c313c52
124 | * CLEAN 335c4c6028171cfddfbaae1a9c313c52 3934 2342
125 | * REMOVE 335c4c6028171cfddfbaae1a9c313c52
126 | * DIRTY 1ab96a171faeeee38496d8b330771a7a
127 | * CLEAN 1ab96a171faeeee38496d8b330771a7a 1600 234
128 | * READ 335c4c6028171cfddfbaae1a9c313c52
129 | * READ 3400330d1dfc7f3f7f4b8d4d803dfcf6
130 | *
131 | * The first five lines of the journal form its header. They are the
132 | * constant string "libcore.io.DiskLruCache", the disk cache's version,
133 | * the application's version, the value count, and a blank line.
134 | *
135 | * Each of the subsequent lines in the file is a record of the state of a
136 | * cache entry. Each line contains space-separated values: a state, a key,
137 | * and optional state-specific values.
138 | * o DIRTY lines track that an entry is actively being created or updated.
139 | * Every successful DIRTY action should be followed by a CLEAN or REMOVE
140 | * action. DIRTY lines without a matching CLEAN or REMOVE indicate that
141 | * temporary files may need to be deleted.
142 | * o CLEAN lines track a cache entry that has been successfully published
143 | * and may be read. A publish line is followed by the lengths of each of
144 | * its values.
145 | * o READ lines track accesses for LRU.
146 | * o REMOVE lines track entries that have been deleted.
147 | *
148 | * The journal file is appended to as cache operations occur. The journal may
149 | * occasionally be compacted by dropping redundant lines. A temporary file named
150 | * "journal.tmp" will be used during compaction; that file should be deleted if
151 | * it exists when the cache is opened.
152 | */
153 |
154 | private final File directory;
155 | private final File journalFile;
156 | private final File journalFileTmp;
157 | private final int appVersion;
158 | private final long maxSize;
159 | private final int valueCount;
160 | private long size = 0;
161 | private Writer journalWriter;
162 | private final LinkedHashMap lruEntries
163 | = new LinkedHashMap(0, 0.75f, true);
164 | private int redundantOpCount;
165 |
166 | /**
167 | * To differentiate between old and current snapshots, each entry is given
168 | * a sequence number each time an edit is committed. A snapshot is stale if
169 | * its sequence number is not equal to its entry's sequence number.
170 | */
171 | private long nextSequenceNumber = 0;
172 |
173 | /* From java.util.Arrays */
174 | @SuppressWarnings("unchecked")
175 | private static T[] copyOfRange(T[] original, int start, int end) {
176 | final int originalLength = original.length; // For exception priority compatibility.
177 | if (start > end) {
178 | throw new IllegalArgumentException();
179 | }
180 | if (start < 0 || start > originalLength) {
181 | throw new ArrayIndexOutOfBoundsException();
182 | }
183 | final int resultLength = end - start;
184 | final int copyLength = Math.min(resultLength, originalLength - start);
185 | final T[] result = (T[]) Array
186 | .newInstance(original.getClass().getComponentType(), resultLength);
187 | System.arraycopy(original, start, result, 0, copyLength);
188 | return result;
189 | }
190 |
191 | /**
192 | * Returns the remainder of 'reader' as a string, closing it when done.
193 | */
194 | public static String readFully(Reader reader) throws IOException {
195 | try {
196 | StringWriter writer = new StringWriter();
197 | char[] buffer = new char[1024];
198 | int count;
199 | while ((count = reader.read(buffer)) != -1) {
200 | writer.write(buffer, 0, count);
201 | }
202 | return writer.toString();
203 | } finally {
204 | reader.close();
205 | }
206 | }
207 |
208 | /**
209 | * Returns the ASCII characters up to but not including the next "\r\n", or
210 | * "\n".
211 | *
212 | * @throws EOFException if the stream is exhausted before the next newline
213 | * character.
214 | */
215 | public static String readAsciiLine(InputStream in) throws IOException {
216 | // TODO: support UTF-8 here instead
217 |
218 | StringBuilder result = new StringBuilder(80);
219 | while (true) {
220 | int c = in.read();
221 | if (c == -1) {
222 | throw new EOFException();
223 | } else if (c == '\n') {
224 | break;
225 | }
226 |
227 | result.append((char) c);
228 | }
229 | int length = result.length();
230 | if (length > 0 && result.charAt(length - 1) == '\r') {
231 | result.setLength(length - 1);
232 | }
233 | return result.toString();
234 | }
235 |
236 | /**
237 | * Closes 'closeable', ignoring any checked exceptions. Does nothing if 'closeable' is null.
238 | */
239 | public static void closeQuietly(Closeable closeable) {
240 | if (closeable != null) {
241 | try {
242 | closeable.close();
243 | } catch (RuntimeException rethrown) {
244 | throw rethrown;
245 | } catch (Exception ignored) {
246 | }
247 | }
248 | }
249 |
250 | /**
251 | * Recursively delete everything in {@code dir}.
252 | */
253 | // TODO: this should specify paths as Strings rather than as Files
254 | public static void deleteContents(File dir) throws IOException {
255 | File[] files = dir.listFiles();
256 | if (files == null) {
257 | throw new IllegalArgumentException("not a directory: " + dir);
258 | }
259 | for (File file : files) {
260 | if (file.isDirectory()) {
261 | deleteContents(file);
262 | }
263 | if (!file.delete()) {
264 | throw new IOException("failed to delete file: " + file);
265 | }
266 | }
267 | }
268 |
269 | /**
270 | * This cache uses a single background thread to evict entries.
271 | */
272 | private final ExecutorService executorService = new ThreadPoolExecutor(0, 1,
273 | 60L, TimeUnit.SECONDS, new LinkedBlockingQueue());
274 | private final Callable cleanupCallable = new Callable() {
275 | @Override
276 | public Void call() throws Exception {
277 | synchronized (DiskLruCache.this) {
278 | if (journalWriter == null) {
279 | return null; // closed
280 | }
281 | trimToSize();
282 | if (journalRebuildRequired()) {
283 | rebuildJournal();
284 | redundantOpCount = 0;
285 | }
286 | }
287 | return null;
288 | }
289 | };
290 |
291 | private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
292 | this.directory = directory;
293 | this.appVersion = appVersion;
294 | this.journalFile = new File(directory, JOURNAL_FILE);
295 | this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
296 | this.valueCount = valueCount;
297 | this.maxSize = maxSize;
298 | }
299 |
300 | /**
301 | * Opens the cache in {@code directory}, creating a cache if none exists
302 | * there.
303 | *
304 | * @param directory a writable directory
305 | * @param appVersion
306 | * @param valueCount the number of values per cache entry. Must be positive.
307 | * @param maxSize the maximum number of bytes this cache should use to store
308 | * @throws IOException if reading or writing the cache directory fails
309 | */
310 | public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
311 | throws IOException {
312 | if (maxSize <= 0) {
313 | throw new IllegalArgumentException("maxSize <= 0");
314 | }
315 | if (valueCount <= 0) {
316 | throw new IllegalArgumentException("valueCount <= 0");
317 | }
318 |
319 | // prefer to pick up where we left off
320 | DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
321 | if (cache.journalFile.exists()) {
322 | try {
323 | cache.readJournal();
324 | cache.processJournal();
325 | cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
326 | IO_BUFFER_SIZE);
327 | return cache;
328 | } catch (IOException journalIsCorrupt) {
329 | // System.logW("DiskLruCache " + directory + " is corrupt: "
330 | // + journalIsCorrupt.getMessage() + ", removing");
331 | cache.delete();
332 | }
333 | }
334 |
335 | // create a new empty cache
336 | directory.mkdirs();
337 | cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
338 | cache.rebuildJournal();
339 | return cache;
340 | }
341 |
342 | private void readJournal() throws IOException {
343 | InputStream in = new BufferedInputStream(new FileInputStream(journalFile), IO_BUFFER_SIZE);
344 | try {
345 | String magic = readAsciiLine(in);
346 | String version = readAsciiLine(in);
347 | String appVersionString = readAsciiLine(in);
348 | String valueCountString = readAsciiLine(in);
349 | String blank = readAsciiLine(in);
350 | if (!MAGIC.equals(magic)
351 | || !VERSION_1.equals(version)
352 | || !Integer.toString(appVersion).equals(appVersionString)
353 | || !Integer.toString(valueCount).equals(valueCountString)
354 | || !"".equals(blank)) {
355 | throw new IOException("unexpected journal header: ["
356 | + magic + ", " + version + ", " + valueCountString + ", " + blank + "]");
357 | }
358 |
359 | while (true) {
360 | try {
361 | readJournalLine(readAsciiLine(in));
362 | } catch (EOFException endOfJournal) {
363 | break;
364 | }
365 | }
366 | } finally {
367 | closeQuietly(in);
368 | }
369 | }
370 |
371 | private void readJournalLine(String line) throws IOException {
372 | String[] parts = line.split(" ");
373 | if (parts.length < 2) {
374 | throw new IOException("unexpected journal line: " + line);
375 | }
376 |
377 | String key = parts[1];
378 | if (parts[0].equals(REMOVE) && parts.length == 2) {
379 | lruEntries.remove(key);
380 | return;
381 | }
382 |
383 | Entry entry = lruEntries.get(key);
384 | if (entry == null) {
385 | entry = new Entry(key);
386 | lruEntries.put(key, entry);
387 | }
388 |
389 | if (parts[0].equals(CLEAN) && parts.length == 2 + valueCount) {
390 | entry.readable = true;
391 | entry.currentEditor = null;
392 | entry.setLengths(copyOfRange(parts, 2, parts.length));
393 | } else if (parts[0].equals(DIRTY) && parts.length == 2) {
394 | entry.currentEditor = new Editor(entry);
395 | } else if (parts[0].equals(READ) && parts.length == 2) {
396 | // this work was already done by calling lruEntries.get()
397 | } else {
398 | throw new IOException("unexpected journal line: " + line);
399 | }
400 | }
401 |
402 | /**
403 | * Computes the initial size and collects garbage as a part of opening the
404 | * cache. Dirty entries are assumed to be inconsistent and will be deleted.
405 | */
406 | private void processJournal() throws IOException {
407 | deleteIfExists(journalFileTmp);
408 | for (Iterator i = lruEntries.values().iterator(); i.hasNext(); ) {
409 | Entry entry = i.next();
410 | if (entry.currentEditor == null) {
411 | for (int t = 0; t < valueCount; t++) {
412 | size += entry.lengths[t];
413 | }
414 | } else {
415 | entry.currentEditor = null;
416 | for (int t = 0; t < valueCount; t++) {
417 | deleteIfExists(entry.getCleanFile(t));
418 | deleteIfExists(entry.getDirtyFile(t));
419 | }
420 | i.remove();
421 | }
422 | }
423 | }
424 |
425 | /**
426 | * Creates a new journal that omits redundant information. This replaces the
427 | * current journal if it exists.
428 | */
429 | private synchronized void rebuildJournal() throws IOException {
430 | if (journalWriter != null) {
431 | journalWriter.close();
432 | }
433 |
434 | Writer writer = new BufferedWriter(new FileWriter(journalFileTmp), IO_BUFFER_SIZE);
435 | writer.write(MAGIC);
436 | writer.write("\n");
437 | writer.write(VERSION_1);
438 | writer.write("\n");
439 | writer.write(Integer.toString(appVersion));
440 | writer.write("\n");
441 | writer.write(Integer.toString(valueCount));
442 | writer.write("\n");
443 | writer.write("\n");
444 |
445 | for (Entry entry : lruEntries.values()) {
446 | if (entry.currentEditor != null) {
447 | writer.write(DIRTY + ' ' + entry.key + '\n');
448 | } else {
449 | writer.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
450 | }
451 | }
452 |
453 | writer.close();
454 | journalFileTmp.renameTo(journalFile);
455 | journalWriter = new BufferedWriter(new FileWriter(journalFile, true), IO_BUFFER_SIZE);
456 | }
457 |
458 | private static void deleteIfExists(File file) throws IOException {
459 | // try {
460 | // Libcore.os.remove(file.getPath());
461 | // } catch (ErrnoException errnoException) {
462 | // if (errnoException.errno != OsConstants.ENOENT) {
463 | // throw errnoException.rethrowAsIOException();
464 | // }
465 | // }
466 | if (file.exists() && !file.delete()) {
467 | throw new IOException();
468 | }
469 | }
470 |
471 | /**
472 | * Returns a snapshot of the entry named {@code key}, or null if it doesn't
473 | * exist is not currently readable. If a value is returned, it is moved to
474 | * the head of the LRU queue.
475 | */
476 | public synchronized Snapshot get(String key) throws IOException {
477 | checkNotClosed();
478 | validateKey(key);
479 | Entry entry = lruEntries.get(key);
480 | if (entry == null) {
481 | return null;
482 | }
483 |
484 | if (!entry.readable) {
485 | return null;
486 | }
487 |
488 | /*
489 | * Open all streams eagerly to guarantee that we see a single published
490 | * snapshot. If we opened streams lazily then the streams could come
491 | * from different edits.
492 | */
493 | InputStream[] ins = new InputStream[valueCount];
494 | try {
495 | for (int i = 0; i < valueCount; i++) {
496 | ins[i] = new FileInputStream(entry.getCleanFile(i));
497 | }
498 | } catch (FileNotFoundException e) {
499 | // a file must have been deleted manually!
500 | return null;
501 | }
502 |
503 | redundantOpCount++;
504 | journalWriter.append(READ + ' ' + key + '\n');
505 | if (journalRebuildRequired()) {
506 | executorService.submit(cleanupCallable);
507 | }
508 |
509 | return new Snapshot(key, entry.sequenceNumber, ins);
510 | }
511 |
512 | /**
513 | * Returns an editor for the entry named {@code key}, or null if another
514 | * edit is in progress.
515 | */
516 | public Editor edit(String key) throws IOException {
517 | return edit(key, ANY_SEQUENCE_NUMBER);
518 | }
519 |
520 | private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
521 | checkNotClosed();
522 | validateKey(key);
523 | Entry entry = lruEntries.get(key);
524 | if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
525 | && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
526 | return null; // snapshot is stale
527 | }
528 | if (entry == null) {
529 | entry = new Entry(key);
530 | lruEntries.put(key, entry);
531 | } else if (entry.currentEditor != null) {
532 | return null; // another edit is in progress
533 | }
534 |
535 | Editor editor = new Editor(entry);
536 | entry.currentEditor = editor;
537 |
538 | // flush the journal before creating files to prevent file leaks
539 | journalWriter.write(DIRTY + ' ' + key + '\n');
540 | journalWriter.flush();
541 | return editor;
542 | }
543 |
544 | /**
545 | * Returns the directory where this cache stores its data.
546 | */
547 | public File getDirectory() {
548 | return directory;
549 | }
550 |
551 | /**
552 | * Returns the maximum number of bytes that this cache should use to store
553 | * its data.
554 | */
555 | public long maxSize() {
556 | return maxSize;
557 | }
558 |
559 | /**
560 | * Returns the number of bytes currently being used to store the values in
561 | * this cache. This may be greater than the max size if a background
562 | * deletion is pending.
563 | */
564 | public synchronized long size() {
565 | return size;
566 | }
567 |
568 | private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
569 | Entry entry = editor.entry;
570 | if (entry.currentEditor != editor) {
571 | throw new IllegalStateException();
572 | }
573 |
574 | // if this edit is creating the entry for the first time, every index must have a value
575 | if (success && !entry.readable) {
576 | for (int i = 0; i < valueCount; i++) {
577 | if (!entry.getDirtyFile(i).exists()) {
578 | editor.abort();
579 | throw new IllegalStateException("edit didn't create file " + i);
580 | }
581 | }
582 | }
583 |
584 | for (int i = 0; i < valueCount; i++) {
585 | File dirty = entry.getDirtyFile(i);
586 | if (success) {
587 | if (dirty.exists()) {
588 | File clean = entry.getCleanFile(i);
589 | dirty.renameTo(clean);
590 | long oldLength = entry.lengths[i];
591 | long newLength = clean.length();
592 | entry.lengths[i] = newLength;
593 | size = size - oldLength + newLength;
594 | }
595 | } else {
596 | deleteIfExists(dirty);
597 | }
598 | }
599 |
600 | redundantOpCount++;
601 | entry.currentEditor = null;
602 | if (entry.readable | success) {
603 | entry.readable = true;
604 | journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
605 | if (success) {
606 | entry.sequenceNumber = nextSequenceNumber++;
607 | }
608 | } else {
609 | lruEntries.remove(entry.key);
610 | journalWriter.write(REMOVE + ' ' + entry.key + '\n');
611 | }
612 |
613 | if (size > maxSize || journalRebuildRequired()) {
614 | executorService.submit(cleanupCallable);
615 | }
616 | }
617 |
618 | /**
619 | * We only rebuild the journal when it will halve the size of the journal
620 | * and eliminate at least 2000 ops.
621 | */
622 | private boolean journalRebuildRequired() {
623 | final int REDUNDANT_OP_COMPACT_THRESHOLD = 2000;
624 | return redundantOpCount >= REDUNDANT_OP_COMPACT_THRESHOLD
625 | && redundantOpCount >= lruEntries.size();
626 | }
627 |
628 | /**
629 | * Drops the entry for {@code key} if it exists and can be removed. Entries
630 | * actively being edited cannot be removed.
631 | *
632 | * @return true if an entry was removed.
633 | */
634 | public synchronized boolean remove(String key) throws IOException {
635 | checkNotClosed();
636 | validateKey(key);
637 | Entry entry = lruEntries.get(key);
638 | if (entry == null || entry.currentEditor != null) {
639 | return false;
640 | }
641 |
642 | for (int i = 0; i < valueCount; i++) {
643 | File file = entry.getCleanFile(i);
644 | if (!file.delete()) {
645 | throw new IOException("failed to delete " + file);
646 | }
647 | size -= entry.lengths[i];
648 | entry.lengths[i] = 0;
649 | }
650 |
651 | redundantOpCount++;
652 | journalWriter.append(REMOVE + ' ' + key + '\n');
653 | lruEntries.remove(key);
654 |
655 | if (journalRebuildRequired()) {
656 | executorService.submit(cleanupCallable);
657 | }
658 |
659 | return true;
660 | }
661 |
662 | /**
663 | * Returns true if this cache has been closed.
664 | */
665 | public boolean isClosed() {
666 | return journalWriter == null;
667 | }
668 |
669 | private void checkNotClosed() {
670 | if (journalWriter == null) {
671 | throw new IllegalStateException("cache is closed");
672 | }
673 | }
674 |
675 | /**
676 | * Force buffered operations to the filesystem.
677 | */
678 | public synchronized void flush() throws IOException {
679 | checkNotClosed();
680 | trimToSize();
681 | journalWriter.flush();
682 | }
683 |
684 | /**
685 | * Closes this cache. Stored values will remain on the filesystem.
686 | */
687 | public synchronized void close() throws IOException {
688 | if (journalWriter == null) {
689 | return; // already closed
690 | }
691 | for (Entry entry : new ArrayList(lruEntries.values())) {
692 | if (entry.currentEditor != null) {
693 | entry.currentEditor.abort();
694 | }
695 | }
696 | trimToSize();
697 | journalWriter.close();
698 | journalWriter = null;
699 | }
700 |
701 | private void trimToSize() throws IOException {
702 | while (size > maxSize) {
703 | // Map.Entry toEvict = lruEntries.eldest();
704 | final Map.Entry toEvict = lruEntries.entrySet().iterator().next();
705 | remove(toEvict.getKey());
706 | }
707 | }
708 |
709 | /**
710 | * Closes the cache and deletes all of its stored values. This will delete
711 | * all files in the cache directory including files that weren't created by
712 | * the cache.
713 | */
714 | public void delete() throws IOException {
715 | close();
716 | deleteContents(directory);
717 | }
718 |
719 | private void validateKey(String key) {
720 | if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
721 | throw new IllegalArgumentException(
722 | "keys must not contain spaces or newlines: \"" + key + "\"");
723 | }
724 | }
725 |
726 | private static String inputStreamToString(InputStream in) throws IOException {
727 | return readFully(new InputStreamReader(in, UTF_8));
728 | }
729 |
730 | /**
731 | * A snapshot of the values for an entry.
732 | */
733 | public final class Snapshot implements Closeable {
734 | private final String key;
735 | private final long sequenceNumber;
736 | private final InputStream[] ins;
737 |
738 | private Snapshot(String key, long sequenceNumber, InputStream[] ins) {
739 | this.key = key;
740 | this.sequenceNumber = sequenceNumber;
741 | this.ins = ins;
742 | }
743 |
744 | /**
745 | * Returns an editor for this snapshot's entry, or null if either the
746 | * entry has changed since this snapshot was created or if another edit
747 | * is in progress.
748 | */
749 | public Editor edit() throws IOException {
750 | return DiskLruCache.this.edit(key, sequenceNumber);
751 | }
752 |
753 | /**
754 | * Returns the unbuffered stream with the value for {@code index}.
755 | */
756 | public InputStream getInputStream(int index) {
757 | return ins[index];
758 | }
759 |
760 | /**
761 | * Returns the string value for {@code index}.
762 | */
763 | public String getString(int index) throws IOException {
764 | return inputStreamToString(getInputStream(index));
765 | }
766 |
767 | @Override
768 | public void close() {
769 | for (InputStream in : ins) {
770 | closeQuietly(in);
771 | }
772 | }
773 | }
774 |
775 | /**
776 | * Edits the values for an entry.
777 | */
778 | public final class Editor {
779 | private final Entry entry;
780 | private boolean hasErrors;
781 |
782 | private Editor(Entry entry) {
783 | this.entry = entry;
784 | }
785 |
786 | /**
787 | * Returns an unbuffered input stream to read the last committed value,
788 | * or null if no value has been committed.
789 | */
790 | public InputStream newInputStream(int index) throws IOException {
791 | synchronized (DiskLruCache.this) {
792 | if (entry.currentEditor != this) {
793 | throw new IllegalStateException();
794 | }
795 | if (!entry.readable) {
796 | return null;
797 | }
798 | return new FileInputStream(entry.getCleanFile(index));
799 | }
800 | }
801 |
802 | /**
803 | * Returns the last committed value as a string, or null if no value
804 | * has been committed.
805 | */
806 | public String getString(int index) throws IOException {
807 | InputStream in = newInputStream(index);
808 | return in != null ? inputStreamToString(in) : null;
809 | }
810 |
811 | /**
812 | * Returns a new unbuffered output stream to write the value at
813 | * {@code index}. If the underlying output stream encounters errors
814 | * when writing to the filesystem, this edit will be aborted when
815 | * {@link #commit} is called. The returned output stream does not throw
816 | * IOExceptions.
817 | */
818 | public OutputStream newOutputStream(int index) throws IOException {
819 | synchronized (DiskLruCache.this) {
820 | if (entry.currentEditor != this) {
821 | throw new IllegalStateException();
822 | }
823 | return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
824 | }
825 | }
826 |
827 | /**
828 | * Sets the value at {@code index} to {@code value}.
829 | */
830 | public void set(int index, String value) throws IOException {
831 | Writer writer = null;
832 | try {
833 | writer = new OutputStreamWriter(newOutputStream(index), UTF_8);
834 | writer.write(value);
835 | } finally {
836 | closeQuietly(writer);
837 | }
838 | }
839 |
840 | /**
841 | * Commits this edit so it is visible to readers. This releases the
842 | * edit lock so another edit may be started on the same key.
843 | */
844 | public void commit() throws IOException {
845 | if (hasErrors) {
846 | completeEdit(this, false);
847 | remove(entry.key); // the previous entry is stale
848 | } else {
849 | completeEdit(this, true);
850 | }
851 | }
852 |
853 | /**
854 | * Aborts this edit. This releases the edit lock so another edit may be
855 | * started on the same key.
856 | */
857 | public void abort() throws IOException {
858 | completeEdit(this, false);
859 | }
860 |
861 | private class FaultHidingOutputStream extends FilterOutputStream {
862 | private FaultHidingOutputStream(OutputStream out) {
863 | super(out);
864 | }
865 |
866 | @Override
867 | public void write(int oneByte) {
868 | try {
869 | out.write(oneByte);
870 | } catch (IOException e) {
871 | hasErrors = true;
872 | }
873 | }
874 |
875 | @Override
876 | public void write(byte[] buffer, int offset, int length) {
877 | try {
878 | out.write(buffer, offset, length);
879 | } catch (IOException e) {
880 | hasErrors = true;
881 | }
882 | }
883 |
884 | @Override
885 | public void close() {
886 | try {
887 | out.close();
888 | } catch (IOException e) {
889 | hasErrors = true;
890 | }
891 | }
892 |
893 | @Override
894 | public void flush() {
895 | try {
896 | out.flush();
897 | } catch (IOException e) {
898 | hasErrors = true;
899 | }
900 | }
901 | }
902 | }
903 |
904 | private final class Entry {
905 | private final String key;
906 |
907 | /**
908 | * Lengths of this entry's files.
909 | */
910 | private final long[] lengths;
911 |
912 | /**
913 | * True if this entry has ever been published
914 | */
915 | private boolean readable;
916 |
917 | /**
918 | * The ongoing edit or null if this entry is not being edited.
919 | */
920 | private Editor currentEditor;
921 |
922 | /**
923 | * The sequence number of the most recently committed edit to this entry.
924 | */
925 | private long sequenceNumber;
926 |
927 | private Entry(String key) {
928 | this.key = key;
929 | this.lengths = new long[valueCount];
930 | }
931 |
932 | public String getLengths() throws IOException {
933 | StringBuilder result = new StringBuilder();
934 | for (long size : lengths) {
935 | result.append(' ').append(size);
936 | }
937 | return result.toString();
938 | }
939 |
940 | /**
941 | * Set lengths using decimal numbers like "10123".
942 | */
943 | private void setLengths(String[] strings) throws IOException {
944 | if (strings.length != valueCount) {
945 | throw invalidLengths(strings);
946 | }
947 |
948 | try {
949 | for (int i = 0; i < strings.length; i++) {
950 | lengths[i] = Long.parseLong(strings[i]);
951 | }
952 | } catch (NumberFormatException e) {
953 | throw invalidLengths(strings);
954 | }
955 | }
956 |
957 | private IOException invalidLengths(String[] strings) throws IOException {
958 | throw new IOException("unexpected journal line: " + Arrays.toString(strings));
959 | }
960 |
961 | public File getCleanFile(int i) {
962 | return new File(directory, key + "." + i);
963 | }
964 |
965 | public File getDirtyFile(int i) {
966 | return new File(directory, key + "." + i + ".tmp");
967 | }
968 | }
969 | }
970 |
--------------------------------------------------------------------------------