-
Notifications
You must be signed in to change notification settings - Fork 289
Expand file tree
/
Copy pathProgressManager.java
More file actions
494 lines (454 loc) · 23.8 KB
/
ProgressManager.java
File metadata and controls
494 lines (454 loc) · 23.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
/*
* Copyright 2017 JessYan
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package me.jessyan.progressmanager;
import android.app.Activity;
import android.app.Fragment;
import android.os.Handler;
import android.os.Looper;
import android.os.SystemClock;
import android.text.TextUtils;
import java.io.IOException;
import java.lang.ref.ReferenceQueue;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import me.jessyan.progressmanager.body.ProgressInfo;
import me.jessyan.progressmanager.body.ProgressRequestBody;
import me.jessyan.progressmanager.body.ProgressResponseBody;
import okhttp3.Interceptor;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.internal.http2.Header;
/**
* ================================================
* ProgressManager 一行代码即可监听 App 中所有网络链接的上传以及下载进度,包括 Glide(需要将下载引擎切换为 Okhttp)的图片加载进度,
* 基于 Okhttp 的 {@link Interceptor},所以使用前请确保你使用 Okhttp 或 Retrofit 进行网络请求
* 实现原理类似 EventBus,你可在 App 中的任何地方,将多个监听器,以 {@code url} 地址作为标识符,注册到本管理器
* 当此 {@code url} 地址存在下载或者上传的动作时,管理器会主动调用所有使用此 {@code url} 地址注册过的监听器,达到多个模块的同步更新
* <p>
* Created by JessYan on 02/06/2017 18:37
* <a href="mailto:jess.yan.effort@gmail.com">Contact me</a>
* <a href="https://github.com/JessYanCoding">Follow me</a>
* ================================================
*/
public final class ProgressManager {
/**
* 因为 {@link WeakHashMap} 将 {@code key} 作为弱键 (弱引用的键), 所以当 java 虚拟机 GC 时会将某个不在被引用的 {@code key} 回收并加入 {@link ReferenceQueue}
* 在下一次操作 {@link WeakHashMap} 时,会比对 {@link ReferenceQueue} 中的 {@code key}
* 将 {@link WeakHashMap} 中对应的 {@code key} 连同 {@code value} 一起移除,从而免去了手动 {@link Map#remove(Object)}
* <p>
* 建议你们在日常使用中,将这个 {@code key} 和对应的 {@link Activity}/{@link Fragment} 生命周期同步,也就使用全局变量持有
* 最重要的是使用 String mUrl = new String("url");, 而不是 String mUrl = "url";
* 为什么这样做? 因为如果直接使用 String mUrl = "url", 这个 {@code url} 字符串会被加入全局字符串常量池, 池中的字符串将不会被回收
* 既然 {@code key} 没被回收, 那 {@link WeakHashMap} 中的值也不会被移除
*/
private final Map<String, Set<ProgressListener>> mRequestListeners = new HashMap<>();
private final Map<String, Set<ProgressListener>> mResponseListeners = new HashMap<>();
private final Handler mHandler; //所有监听器在 Handler 中被执行,所以可以保证所有监听器在主线程中被执行
private final Interceptor mInterceptor;
private int mRefreshTime = DEFAULT_REFRESH_TIME; //进度刷新时间(单位ms),避免高频率调用
private static volatile ProgressManager mProgressManager;
private static final String OKHTTP_PACKAGE_NAME = "okhttp3.OkHttpClient";
private static final boolean DEPENDENCY_OKHTTP;
private static final int DEFAULT_REFRESH_TIME = 150;
private static final String IDENTIFICATION_NUMBER = "?JessYan=";
private static final String IDENTIFICATION_HEADER = "JessYan";
private static final String LOCATION_HEADER = "Location";
static {
boolean hasDependency;
try {
Class.forName(OKHTTP_PACKAGE_NAME);
hasDependency = true;
} catch (ClassNotFoundException e) {
hasDependency = false;
}
DEPENDENCY_OKHTTP = hasDependency;
}
private ProgressManager() {
this.mHandler = new Handler(Looper.getMainLooper());
this.mInterceptor = new Interceptor() {
@Override
public Response intercept(Chain chain) throws IOException {
return wrapResponseBody(chain.proceed(wrapRequestBody(chain.request())));
}
};
}
public static final ProgressManager getInstance() {
if (mProgressManager == null) {
if (!DEPENDENCY_OKHTTP) { //使用本管理器必须依赖 Okhttp
throw new IllegalStateException("Must be dependency Okhttp");
}
synchronized (ProgressManager.class) {
if (mProgressManager == null) {
mProgressManager = new ProgressManager();
}
}
}
return mProgressManager;
}
/**
* 设置 {@link ProgressListener#onProgress(ProgressInfo)} 每次被调用的间隔时间
*
* @param refreshTime 间隔时间,单位毫秒
*/
public void setRefreshTime(int refreshTime) {
if (refreshTime < 0) throw new IllegalArgumentException("refreshTime must be >= 0");
this.mRefreshTime = refreshTime;
}
/**
* 将需要被监听上传进度的 {@code url} 注册到管理器,此操作请在页面初始化时进行,切勿多次注册同一个(内容相同)监听器
*
* @param url {@code url} 作为标识符
* @param listener 当此 {@code url} 地址存在上传的动作时,此监听器将被调用
*/
public void addRequestListener(String url, ProgressListener listener) {
checkNotNull(url, "url cannot be null");
checkNotNull(listener, "listener cannot be null");
url = new String(url);
Set<ProgressListener> progressListeners;
synchronized (ProgressManager.class) {
progressListeners = mRequestListeners.get(url);
if (progressListeners == null) {
progressListeners = Collections.newSetFromMap(new WeakHashMap<ProgressListener, Boolean>());
mRequestListeners.put(url, progressListeners);
}
}
progressListeners.add(listener);
}
/**
* 将需要被监听下载进度的 {@code url} 注册到管理器,此操作请在页面初始化时进行,切勿多次注册同一个(内容相同)监听器
*
* @param url {@code url} 作为标识符
* @param listener 当此 {@code url} 地址存在下载的动作时,此监听器将被调用
*/
public void addResponseListener(String url, ProgressListener listener) {
checkNotNull(url, "url cannot be null");
checkNotNull(listener, "listener cannot be null");
url = new String(url);
Set<ProgressListener> progressListeners;
synchronized (ProgressManager.class) {
progressListeners = mResponseListeners.get(url);
if (progressListeners == null) {
progressListeners = Collections.newSetFromMap(new WeakHashMap<ProgressListener, Boolean>());
mResponseListeners.put(url, progressListeners);
}
}
progressListeners.add(listener);
}
/**
* 当在 {@link ProgressRequestBody} 和 {@link ProgressResponseBody} 内部处理二进制流时发生错误
* 会主动调用 {@link ProgressListener#onError(long, Exception)},但是有些错误并不是在它们内部发生的
* 但同样会引起网络请求的失败,所以向外面提供{@link ProgressManager#notifyOnError},当外部发生错误时
* 手动调用此方法,以通知所有的监听器
*
* @param url {@code url} 作为标识符
* @param e 错误
*/
public void notifyOnError(String url, Exception e) {
checkNotNull(url, "url cannot be null");
forEachListenersOnError(mRequestListeners, url, e);
forEachListenersOnError(mResponseListeners, url, e);
}
/**
* 将 {@link okhttp3.OkHttpClient.Builder} 传入,配置一些本管理器需要的参数
*
* @param builder
* @return
*/
public OkHttpClient.Builder with(OkHttpClient.Builder builder) {
checkNotNull(builder, "builder cannot be null");
return builder
.addNetworkInterceptor(mInterceptor);
}
/**
* 将 {@link Request} 传入,配置一些本框架需要的参数,常用于自定义 {@link Interceptor}
* 如已使用 {@link ProgressManager#with(OkHttpClient.Builder)},就不会用到此方法
*
* @param request 原始的 {@link Request}
* @return
*/
public Request wrapRequestBody(Request request) {
if (request == null)
return request;
String key = request.url().toString();
request = pruneIdentification(key, request);
if (request.body() == null)
return request;
//Double Check
Set<ProgressListener> listeners = mRequestListeners.get(key);
if (listeners == null){
synchronized (ProgressManager.class){
listeners = mRequestListeners.get(key);
if (listeners == null){
listeners = Collections.newSetFromMap(new WeakHashMap<ProgressListener, Boolean>());
mRequestListeners.put(key,listeners);
}
}
}
return request.newBuilder()
.method(request.method(), new ProgressRequestBody(mHandler, request.body(), listeners, mRefreshTime))
.build();
}
/**
* 如果 {@code url} 中含有 {@link #addDiffResponseListenerOnSameUrl(String, String, ProgressListener)}
* 或 {@link #addDiffRequestListenerOnSameUrl(String, String, ProgressListener)} 加入的标识符,则将加入标识符
* 从 {code url} 中删除掉,继续使用 {@code originUrl} 进行请求
*
* @param url {@code url} 地址
* @param request 原始 {@link Request}
* @return 返回可能被修改过的 {@link Request}
*/
private Request pruneIdentification(String url, Request request) {
boolean needPrune = url.contains(IDENTIFICATION_NUMBER);
if (!needPrune)
return request;
return request.newBuilder()
.url(url.substring(0, url.indexOf(IDENTIFICATION_NUMBER))) //删除掉标识符
.header(IDENTIFICATION_HEADER, url) //将有标识符的 url 加入 header中, 便于wrapResponseBody(Response) 做处理
.build();
}
/**
* 将 {@link Response} 传入,配置一些本框架需要的参数,常用于自定义 {@link Interceptor}
* 如已使用 {@link ProgressManager#with(OkHttpClient.Builder)},就不会用到此方法
*
* @param response 原始的 {@link Response}
* @return
*/
public Response wrapResponseBody(Response response) {
if (response == null)
return response;
String key = response.request().url().toString();
if (!TextUtils.isEmpty(response.request().header(IDENTIFICATION_HEADER))) { //从 header 中拿出有标识符的 url
key = response.request().header(IDENTIFICATION_HEADER);
}
if (response.isRedirect()) {
resolveRedirect(mRequestListeners, response, key);
String location = resolveRedirect(mResponseListeners, response, key);
response = modifyLocation(response, location);
return response;
}
if (response.body() == null)
return response;
Set<ProgressListener> listeners = mResponseListeners.get(key);
if (listeners == null){
synchronized (ProgressManager.class){
listeners = mResponseListeners.get(key);
if (listeners == null){
listeners = Collections.newSetFromMap(new WeakHashMap<ProgressListener, Boolean>());
mResponseListeners.put(key,listeners);
}
}
}
return response.newBuilder()
.body(new ProgressResponseBody(mHandler, response.body(), listeners, mRefreshTime))
.build();
}
/**
* 查看 location 是否被加入了标识符, 如果是, 则放入 {@link Header} 中重新定义 {@link Response}
*
* @param response 原始的 {@link Response}
* @param location {@code location} 重定向地址
* @return 返回可能被修改过的 {@link Response}
*/
private Response modifyLocation(Response response, String location) {
if (!TextUtils.isEmpty(location) && location.contains(IDENTIFICATION_NUMBER)) { //将被加入标识符的新的 location 放入 header 中
response = response.newBuilder()
.header(LOCATION_HEADER, location)
.build();
}
return response;
}
/**
* 当出现需要使用同一个 {@code url} 根据 Post 请求参数的不同而下载不同资源的情况
* 请使用 {@link #addDiffResponseListenerOnSameUrl(String, ProgressListener)} 代替 {@link #addResponseListener}
* {@link #addDiffResponseListenerOnSameUrl(String, ProgressListener)} 会返回一个加入了时间戳的新的 {@code url}
* 请使用这个新的 {@code url} 去代替 {@code originUrl} 进行下载的请求即可 (当实际请求时依然使用 {@code originUrl} 进行网络请求)
* <p>
* {@link #addDiffResponseListenerOnSameUrl(String, ProgressListener)} 与 {@link #addDiffResponseListenerOnSameUrl(String, String, ProgressListener)}
* 的区别在于:
* {@link #addDiffResponseListenerOnSameUrl(String, String, ProgressListener)} 可以使用不同的 {@code key} 来自定义新的 {@code url}
* {@link #addDiffResponseListenerOnSameUrl(String, ProgressListener)} 是直接使用时间戳来生成新的 {@code url}
* <p>
*
* @param originUrl {@code originUrl} 作为基础并结合时间戳用于生成新的 {@code url} 作为标识符
* @param listener 当加入了时间戳的新的 {@code url} 地址存在下载的动作时,此监听器将被调用
* @return 加入了时间戳的新的 {@code url}
*/
public String addDiffResponseListenerOnSameUrl(String originUrl, ProgressListener listener) {
return addDiffResponseListenerOnSameUrl(originUrl, String.valueOf(SystemClock.elapsedRealtime() + listener.hashCode()), listener);
}
/**
* 当出现需要使用同一个 {@code url} 根据 Post 请求参数的不同而下载不同资源的情况
* 请使用 {@link #addDiffResponseListenerOnSameUrl(String, String, ProgressListener)} 代替 {@link #addResponseListener}
* 请使用 {@link #addDiffResponseListenerOnSameUrl(String, String, ProgressListener)} 会返回一个 {@code originUrl} 结合 {@code key} 生成的新的 {@code url}
* 请使用这个新的 {@code url} 去代替 {@code originUrl} 进行下载的请求即可 (当实际请求时依然使用 {@code originUrl} 进行网络请求)
* <p>
* {@link #addDiffResponseListenerOnSameUrl(String, ProgressListener)} 与 {@link #addDiffResponseListenerOnSameUrl(String, String, ProgressListener)}
* 的区别在于:
* {@link #addDiffResponseListenerOnSameUrl(String, String, ProgressListener)} 可以使用不同的 {@code key} 来自定义新的 {@code url}
* {@link #addDiffResponseListenerOnSameUrl(String, ProgressListener)} 是直接使用时间戳来生成新的 {@code url}
* <p>
* Example usage:
* <pre>
* String newUrl = ProgressManager.getInstance().addDiffResponseListenerOnSameUrl(DOWNLOAD_URL, "id", getDownloadListener());
* new Thread(new Runnable() {
*
* public void run() {
* try {
* Request request = new Request.Builder()
* .url(newUrl) // 请一定使用 addDiffResponseListenerOnSameUrl 返回的 newUrl 做请求
* .build();
*
* Response response = mOkHttpClient.newCall(request).execute();
* } catch (IOException e) {
* e.printStackTrace();
* //当外部发生错误时,使用此方法可以通知所有监听器的 onError 方法,这里也要使用 newUrl
* ProgressManager.getInstance().notifyOnError(newUrl, e);
* }
* }
* }).start();
* </pre>
*
* @param originUrl {@code originUrl} 作为基础并结合 {@code key} 用于生成新的 {@code url} 作为标识符
* @param key {@code originUrl} 作为基础并结合 {@code key} 用于生成新的 {@code url} 作为标识符
* @param listener 当 {@code originUrl} 结合 {@code key} 生成的新的 {@code url} 地址存在下载的动作时,此监听器将被调用
* @return {@code originUrl} 结合 {@code key} 生成的新的 {@code url}
*/
public String addDiffResponseListenerOnSameUrl(String originUrl, String key, ProgressListener listener) {
String newUrl = originUrl + IDENTIFICATION_NUMBER + key;
addResponseListener(newUrl, listener);
return newUrl;
}
/**
* 当出现需要使用同一个 {@code url} 根据 Post 请求参数的不同而上传不同资源的情况
* 请使用 {@link #addDiffRequestListenerOnSameUrl(String, ProgressListener)} 代替 {@link #addRequestListener}
* {@link #addDiffRequestListenerOnSameUrl(String, ProgressListener)} 会返回一个加入了时间戳的新的 {@code url}
* 请使用这个新的 {@code url} 去代替 {@code originUrl} 进行上传的请求即可 (当实际请求时依然使用 {@code originUrl} 进行网络请求)
* <p>
* {@link #addDiffRequestListenerOnSameUrl(String, ProgressListener)} 与 {@link #addDiffRequestListenerOnSameUrl(String, String, ProgressListener)}
* 的区别在于:
* {@link #addDiffRequestListenerOnSameUrl(String, String, ProgressListener)} 可以使用不同的 {@code key} 来自定义新的 {@code url}
* {@link #addDiffRequestListenerOnSameUrl(String, ProgressListener)} 是直接使用时间戳来生成新的 {@code url}
* <p>
*
* @param originUrl {@code originUrl} 作为基础并结合时间戳用于生成新的 {@code url} 作为标识符
* @param listener 当加入了时间戳的新的 {@code url} 地址存在上传的动作时,此监听器将被调用
* @return 加入了时间戳的新的 {@code url}
*/
public String addDiffRequestListenerOnSameUrl(String originUrl, ProgressListener listener) {
return addDiffRequestListenerOnSameUrl(originUrl, String.valueOf(SystemClock.elapsedRealtime() + listener.hashCode()), listener);
}
/**
* 当出现需要使用同一个 {@code url} 根据 Post 请求参数的不同而上传不同资源的情况
* 请使用 {@link #addDiffRequestListenerOnSameUrl(String, ProgressListener)} 代替 {@link #addRequestListener}
* {@link #addDiffRequestListenerOnSameUrl(String, ProgressListener)} 会返回一个 {@code originUrl} 结合 {@code key} 生成的新的 {@code url}
* 请使用这个新的 {@code url} 去代替 {@code originUrl} 进行上传的请求即可 (当实际请求时依然使用 {@code originUrl} 进行网络请求)
* <p>
* {@link #addDiffRequestListenerOnSameUrl(String, ProgressListener)} 与 {@link #addDiffRequestListenerOnSameUrl(String, String, ProgressListener)}
* 的区别在于:
* {@link #addDiffRequestListenerOnSameUrl(String, String, ProgressListener)} 可以使用不同的 {@code key} 来自定义新的 {@code url}
* {@link #addDiffRequestListenerOnSameUrl(String, ProgressListener)} 是直接使用时间戳来生成新的 {@code url}
* <p>
* Example usage:
* <pre>
* String newUrl = ProgressManager.getInstance().addDiffRequestListenerOnSameUrl(UPLOAD_URL, "id", getUploadListener());
* new Thread(new Runnable() {
*
* public void run() {
* try {
* File file = new File(getCacheDir(), "cache");
*
* Request request = new Request.Builder()
* .url(newUrl) // 请一定使用 addDiffRequestListenerOnSameUrl 返回的 newUrl 做请求
* .post(RequestBody.create(MediaType.parse("multipart/form-data"), file))
* .build();
*
* Response response = mOkHttpClient.newCall(request).execute();
* } catch (IOException e) {
* e.printStackTrace();
* //当外部发生错误时,使用此方法可以通知所有监听器的 onError 方法,这里也要使用 newUrl
* ProgressManager.getInstance().notifyOnError(newUrl, e);
* }
* }
* }).start();
* </pre>
*
* @param originUrl {@code originUrl} 作为基础并结合 {@code key} 用于生成新的 {@code url} 作为标识符
* @param key {@code originUrl} 作为基础并结合 {@code key} 用于生成新的 {@code url} 作为标识符
* @param listener 当 {@code originUrl} 结合 {@code key} 生成的新的 {@code url} 地址存在上传的动作时,此监听器将被调用
* @return {@code originUrl} 结合 {@code key} 生成的新的 {@code url}
*/
public String addDiffRequestListenerOnSameUrl(String originUrl, String key, ProgressListener listener) {
String newUrl = originUrl + IDENTIFICATION_NUMBER + key;
addRequestListener(newUrl, listener);
return newUrl;
}
/**
* 解决请求地址重定向后的兼容问题
*
* @param map {@link #mRequestListeners} 或者 {@link #mResponseListeners}
* @param response 原始的 {@link Response}
* @param url {@code url} 地址
*/
private String resolveRedirect(Map<String, Set<ProgressListener>> map, Response response, String url) {
String location = null;
Set<ProgressListener> progressListeners = map.get(url); //查看此重定向 url ,是否已经注册过监听器
if (progressListeners == null){
synchronized (ProgressManager.class){
progressListeners = map.get(url);
if (progressListeners == null){
progressListeners = Collections.newSetFromMap(new WeakHashMap<ProgressListener, Boolean>());
map.put(url,progressListeners);
}
}
}
location = response.header(LOCATION_HEADER);// 重定向地址
if (!TextUtils.isEmpty(location)) {
if (url.contains(IDENTIFICATION_NUMBER) && !location.contains(IDENTIFICATION_NUMBER)) { //如果 url 有标识符,那也将标识符加入用于重定向的 location
location += url.substring(url.indexOf(IDENTIFICATION_NUMBER), url.length());
}
if (!map.containsKey(location)) {
map.put(location, progressListeners); //将需要重定向地址的监听器,提供给重定向地址,保证重定向后也可以监听进度
} else {
Set<ProgressListener> locationListeners = map.get(location);
for (ProgressListener listener : progressListeners) {
if (!locationListeners.contains(listener)) {
locationListeners.add(listener);
}
}
}
}
return location;
}
private void forEachListenersOnError(Map<String, Set<ProgressListener>> map, String url, Exception e) {
if (map.containsKey(url)) {
Set<ProgressListener> progressListeners = map.get(url);
ProgressListener[] array = progressListeners.toArray(new ProgressListener[progressListeners.size()]);
for (int i = 0; i < array.length; i++) {
array[i].onError(-1, e);
}
}
}
static <T> T checkNotNull(T object, String message) {
if (object == null) {
throw new NullPointerException(message);
}
return object;
}
}