package com.dianping.loader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.os.Build;
import android.os.SystemClock;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import com.dianping.loader.model.FileSpec;
public class RepositoryManager {
/**
* 已经下载并校验完毕,可以直接加载
*/
static final String STATUS_DONE = "DONE";
/**
* 未开始下载
*/
static final String STATUS_IDLE = "IDLE";
/**
* 当前下载
*/
static final String STATUS_RUNNING = "RUNNING";
/**
* call in the background thread
*/
static interface StatusChangeListener {
void onStatusChanged(FileSpec file, String newStatus);
}
private final Context context;
private final ConnectivityManager connManager;
// ./repo/<id>/<md5 or 1>.apk
// ./repo/<id>/dexout
private final File repoDir;
// ./repo/tmp/<id>.<random.4>
private final File tmpDir;
private final LinkedList<FileSpec> order = new LinkedList<FileSpec>();
private final HashMap<String, FileSpec> map = new HashMap<String, FileSpec>();
private final HashMap<String, String> status = new HashMap<String, String>();
private final HashMap<String, Integer> require = new HashMap<String, Integer>();
private final ArrayList<StatusChangeListener> listeners = new ArrayList<StatusChangeListener>();
private Worker running;
public RepositoryManager(Context context) {
this.context = context;
ConnectivityManager cm = null;
try {
cm = (ConnectivityManager) context
.getSystemService(Context.CONNECTIVITY_SERVICE);
} catch (Exception e) {
Log.w("loader",
"repository manager start without connectivity manager", e);
}
connManager = cm;
repoDir = new File(context.getFilesDir(), "repo");
repoDir.mkdir();
tmpDir = new File(repoDir, "tmp");
tmpDir.mkdir();
File[] tmps = tmpDir.listFiles();
if (tmps != null) {
for (File tmpf : tmps) {
tmpf.delete();
}
}
disableConnectionReuseIfNecessary();
}
//
// internal
//
void addListener(StatusChangeListener listener) {
listeners.add(listener);
}
void removeListener(StatusChangeListener listener) {
listeners.remove(listener);
}
synchronized int runningCount() {
int count = 0;
for (String str : status.values()) {
if (STATUS_RUNNING == str)
count++;
}
return count;
}
synchronized int totalCount() {
return map.size();
}
synchronized String getStatus(String fileId) {
if (status.get(fileId) == null) {
FileSpec file = map.get(fileId);
if (file != null) {
File path = getPath(file);
status.put(fileId, path.isFile() ? STATUS_DONE : STATUS_IDLE);
} else {
return null;
}
}
return status.get(fileId);
}
File getDir() {
return repoDir;
}
public File getPath(FileSpec file) {
File dir = new File(repoDir, file.id());
File path = new File(dir, TextUtils.isEmpty(file.md5()) ? "1.apk"
: file.md5() + ".apk");
return path;
}
public synchronized void addFiles(FileSpec[] files) {
for (FileSpec f : files) {
map.put(f.id(), f);
}
for (FileSpec f : files) {
appendDepsList(map, order, f.id());
}
}
// return false if missing file or loop deps
public boolean appendDepsList(List<FileSpec> list, String fileId) {
return appendDepsList(map, list, fileId);
}
// return false if missing file or loop deps
public static boolean appendDepsList(HashMap<String, FileSpec> map,
List<FileSpec> list, String fileId) {
FileSpec f = map.get(fileId);
if (f == null)
return false;
if (list.contains(f))
return true;
if (f.deps() != null) {
for (String dep : f.deps()) {
if (!appendDepsList(map, list, dep))
return false;
}
}
if (list.contains(f))
return false;
list.add(f);
return true;
}
public synchronized void notifyConnectivityChanged() {
if (running != null)
return;
if (pickFromQueue() == null)
return;
start();
}
synchronized void start() {
if (running == null) {
if (status.size() == map.size()) {
int idleCount = 0;
for (String str : status.values()) {
if (STATUS_IDLE == str)
idleCount++;
}
if (idleCount == 0)
return;
}
running = new Worker();
running.start();
}
}
public synchronized void require(FileSpec... files) {
order.removeAll(Arrays.asList(files));
for (int i = files.length - 1; i >= 0; i--) {
FileSpec file = files[i];
order.addFirst(file);
Integer rc = require.get(file.id());
if (rc == null) {
require.put(file.id(), 1);
} else {
require.put(file.id(), rc + 1);
}
}
running = new Worker();
running.start();
}
public synchronized void dismiss(FileSpec... files) {
for (FileSpec file : files) {
Integer rc = require.get(file.id());
if (rc != null && rc > 0) {
require.put(file.id(), rc - 1);
}
}
}
//
// worker
//
private synchronized FileSpec pickFromQueue() {
int networkType = -1;
for (FileSpec file : order) {
if (getStatus(file.id()) == STATUS_IDLE) {
Integer rc = require.get(file.id());
if (rc != null && rc > 0) {
return file;
}
if (file.down() >= FileSpec.DOWN_ALWAYS)
return file;
if (file.down() <= FileSpec.DOWN_NONE)
continue;
if (networkType < 0) {
networkType = getNetworkType();
}
switch (file.down()) {
case FileSpec.DOWN_WIFI:
if (networkType > 3)
return file;
break;
case FileSpec.DOWN_3G:
if (networkType > 2)
return file;
}
}
}
return null;
}
private class Worker extends Thread {
private int failCounter = 0;
@Override
public void run() {
FileSpec current;
while (running == this && (current = pickFromQueue()) != null) {
final FileSpec fCurrent = current;
Log.i("loader", "start download " + current.id() + " from "
+ current.url());
long startMs = SystemClock.elapsedRealtime();
status.put(current.id(), STATUS_RUNNING);
for (StatusChangeListener l : listeners) {
l.onStatusChanged(fCurrent, STATUS_RUNNING);
}
String rnd = Integer.toHexString(new Random(System
.currentTimeMillis()).nextInt(0xf000) + 0x1000);
File f = new File(tmpDir, current.id() + "." + rnd);
boolean succeed = false;
try {
URL url = new URL(current.url());
HttpURLConnection conn = (HttpURLConnection) url
.openConnection();
conn.setConnectTimeout(15000);
// conn.setRequestProperty("User-Agent",
// Environment.mapiUserAgent());
InputStream ins = conn.getInputStream();
FileOutputStream fos = new FileOutputStream(f);
byte[] buf = new byte[1024 * 4]; // 4k buffer
int l;
while ((l = ins.read(buf, 0, buf.length)) != -1) {
fos.write(buf, 0, l);
}
fos.close();
ins.close();
conn.disconnect();
succeed = true;
} catch (Exception e) {
Log.w("loader", "fail to download " + current.id()
+ " from " + current.url(), e);
}
if (f.length() > 0 && !TextUtils.isEmpty(current.md5())) {
succeed = false;
try {
MessageDigest m = MessageDigest.getInstance("MD5");
m.reset();
FileInputStream fis = new FileInputStream(f);
byte[] buf = new byte[1024 * 4]; // 4k buffer
int l;
while ((l = fis.read(buf, 0, buf.length)) != -1) {
m.update(buf, 0, l);
}
fis.close();
String md5 = byteArrayToHexString(m.digest());
succeed = current.md5().equals(md5);
if (!succeed) {
Log.e("loader", "fail to match " + current.id()
+ " md5, " + md5 + " / " + current.md5());
}
} catch (Exception e) {
Log.e("loader",
"fail to verify file " + f.getAbsolutePath(), e);
}
}
File path = getPath(current);
if (succeed) {
path.getParentFile().mkdir();
succeed = f.renameTo(path);
if (!succeed) {
Log.e("loader", "fail to move " + current.id()
+ " from " + f.getAbsolutePath() + " to "
+ path.getAbsolutePath());
}
}
if (!succeed) {
// delete tmp file if not succeed
f.delete();
}
succeed = path.isFile();
final String newStatus = succeed ? STATUS_DONE : STATUS_IDLE;
status.put(current.id(), newStatus);
if (succeed) {
long elapse = SystemClock.elapsedRealtime() - startMs;
Log.i("loader", current.id() + " (" + path.length()
+ " bytes) finished in " + elapse + "ms");
if (failCounter > 0)
failCounter--;
} else {
order.remove(current);
order.addLast(current);
failCounter++;
}
for (StatusChangeListener l : listeners) {
l.onStatusChanged(fCurrent, newStatus);
}
if (failCounter >= 3) {
Log.w("loader", "download fail 3 times, abort");
break;
}
}
synchronized (RepositoryManager.this) {
if (running == this) {
running = null;
}
}
}
}
//
// utils
//
/**
* 0: Unknown<br>
* 1: 2G<br>
* 2: 3G or faster<br>
* 3: Wifi<br>
*/
int getNetworkType() {
try {
NetworkInfo info = connManager.getActiveNetworkInfo();
if (info.getType() == ConnectivityManager.TYPE_MOBILE) {
switch (info.getSubtype()) {
case TelephonyManager.NETWORK_TYPE_1xRTT:// ~ 50-100 kbps
case TelephonyManager.NETWORK_TYPE_CDMA:// ~ 14-64 kbps
case TelephonyManager.NETWORK_TYPE_EDGE:// ~ 50-100 kbps
case TelephonyManager.NETWORK_TYPE_GPRS:// ~ 100 kbps
case TelephonyManager.NETWORK_TYPE_IDEN:// ~25 kbps
return 1;
default:
return 2;
}
} else if (info.getType() == ConnectivityManager.TYPE_WIFI) {
return 3;
}
} catch (Exception e) {
}
return 0;
}
private void disableConnectionReuseIfNecessary() {
// HTTP connection reuse which was buggy pre-froyo
if (Integer.parseInt(Build.VERSION.SDK) < 8) {
System.setProperty("http.keepAlive", "false");
}
}
private final static String[] hexDigits = { "0", "1", "2", "3", "4", "5",
"6", "7", "8", "9", "a", "b", "c", "d", "e", "f" };
public static String byteArrayToHexString(byte[] b) {
StringBuilder resultSb = new StringBuilder();
for (int i = 0; i < b.length; i++) {
resultSb.append(byteToHexString(b[i]));
}
return resultSb.toString();
}
private static String byteToHexString(byte b) {
int n = b;
if (n < 0)
n = 0x100 + n;
int d1 = n >> 4;
int d2 = n & 0xF;
return hexDigits[d1] + hexDigits[d2];
}
}