package jp.agentec.abook.abv.bl.download;

import java.io.File;
import java.net.MalformedURLException;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Observable;
import java.util.Observer;
import java.util.Set;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;

import jp.agentec.abook.abv.bl.acms.client.AcmsClient;
import jp.agentec.abook.abv.bl.acms.client.parameters.ContentDownloadLogParameters;
import jp.agentec.abook.abv.bl.acms.client.parameters.GetContentParameters;
import jp.agentec.abook.abv.bl.acms.type.AcmsApis;
import jp.agentec.abook.abv.bl.acms.type.ContentZipType;
import jp.agentec.abook.abv.bl.acms.type.DownloadStatusType;
import jp.agentec.abook.abv.bl.common.ABVEnvironment;
import jp.agentec.abook.abv.bl.common.CommonExecutor;
import jp.agentec.abook.abv.bl.common.MultiLock;
import jp.agentec.abook.abv.bl.common.MultiLock.Lock;
import jp.agentec.abook.abv.bl.common.exception.ABVException;
import jp.agentec.abook.abv.bl.common.exception.ABVExceptionCode;
import jp.agentec.abook.abv.bl.common.exception.AcmsException;
import jp.agentec.abook.abv.bl.common.exception.NetworkDisconnectedException;
import jp.agentec.abook.abv.bl.common.log.Logger;
import jp.agentec.abook.abv.bl.common.nw.NetworkAdapter;
import jp.agentec.abook.abv.bl.common.util.SecurityUtil;
import jp.agentec.abook.abv.bl.data.ABVDataCache;
import jp.agentec.abook.abv.bl.data.dao.AbstractDao;
import jp.agentec.abook.abv.bl.data.dao.ContentDao;
import jp.agentec.abook.abv.bl.dto.ContentDto;
import jp.agentec.abook.abv.bl.logic.AbstractLogic;
import jp.agentec.abook.abv.bl.logic.ContentLogic;
import jp.agentec.adf.net.http.HttpDownloadNotification;
import jp.agentec.adf.net.http.HttpDownloadSimpleNotification;
import jp.agentec.adf.net.http.HttpDownloadState;
import jp.agentec.adf.net.http.HttpFileDownloader;
import jp.agentec.adf.util.ArrayUtil;
import jp.agentec.adf.util.DateTimeUtil;
import jp.agentec.adf.util.FileUtil;
import jp.agentec.adf.util.StringUtil;

/**
 * コンテンツダウンロードに関する機能を提供します。
 * (循環参照を防ぐため、Logicではこのインスタンスをメンバーに持たないこと)
 * 
 * @author Taejin Hong
 * @version 1.1.1
 */
public class ContentDownloader {
	private static final String TAG = "ContentDownloader";

	private static ContentDownloader instance; 
	
	private ContentLogic contentLogic = AbstractLogic.getLogic(ContentLogic.class);
	private ContentDao contentDao = AbstractDao.getDao(ContentDao.class);
	private ContentFileExtractor contentFileExtractor = ContentFileExtractor.getInstance();

	private ABVDataCache cache = ABVDataCache.getInstance();
	private NetworkAdapter networkAdapter = ABVEnvironment.getInstance().networkAdapter;

	private DownloadObserver downloadObserver = new DownloadObserver();
	private Set<ContentDownloadListener> contentDownloadListenerSet = Collections.newSetFromMap(new ConcurrentHashMap<ContentDownloadListener, Boolean>());
	// ダウンローダマップ（Downloading/Paused/AutoPausedのみがセットされる（他のステータスに変わった後削除））
	private Map<Long, ContentDto> targetContentMap = new LinkedHashMap<Long, ContentDto>(); // Collections.synchronizedMapはIteratorでエラーとなる。GoogleのConcurrentLinkedHashMapはキー順にソートされてしまう
    // 排他制御用オブジェクト
    private MultiLock multiLock = new MultiLock("contentId");

	private int maximumDownloadable = 1;
	private boolean autoPaused = false;
	private boolean isAutoDownload = false; // 自動DL
	private List<Long> autoDownloadList = new ArrayList<Long>();
	private int downloadingPercentageNotifyInterval = 100; // Viewに通知するインターバル（ミリ秒）

	public String account; // GoogleAccount：決済でのみ使用
	private Object kickTaskLock = new Object(); // kickTask()のロック用
	private Timer kickTaskTimer;
	private volatile boolean isKickTask;

    // lastFetchDateの更新判定用フラグ
    // 新着ZIPのダウンロード失敗した場合、lastFetchDate保存しない
    private boolean downloadSucceededFlag = true;

	private ContentDownloader() {
	}

	/**
	 * {@link ContentDownloader} クラスのインスタンスを取得します。
	 * 
	 * @return {@link ContentDownloader} クラスのインスタンスを返します。
	 * @since 1.0.0
	 */
	public static ContentDownloader getInstance() {
		if (instance == null) {
			synchronized (ContentDownloader.class) {
				if (instance == null) {
					instance = new ContentDownloader();
					instance.initialize();
				}
			}
		}

		return instance;
	}

	public void initialize() {
		// コア数に応じて同時並行数を決める
		int core = Runtime.getRuntime().availableProcessors();
		Logger.i(TAG, "availableProcessors: " + core);
		if (core > 1) {
			maximumDownloadable = core / 2 * 3;
		}
	}


	public void addContentDownloadListener(ContentDownloadListener contentDownloadListener) {
		contentDownloadListenerSet.add(contentDownloadListener);
	}

	public void removeContentDownloadListener(ContentDownloadListener contentDownloadListener) {
		contentDownloadListenerSet.remove(contentDownloadListener);
	}

	public void setMaximumDownloadable(int maximumDownloadable) {
		this.maximumDownloadable = maximumDownloadable;
	}

	public void setDownloadingPercentageNotifyInterval(int downloadingPercentageNotifyInterval) {
		this.downloadingPercentageNotifyInterval = downloadingPercentageNotifyInterval;
	}

	public void addAutoDownload(long contentId) {
		autoDownloadList.add(contentId);
	}



	//// Kick Downloader ////

	/**
	 * kickTaskメソッド
	 * 
	 * Singletonであるため、アプリ内で同時に１回のみ呼ばれる
	 * 
	 */
	public void kickTask() {
		synchronized (kickTaskLock) {
			if (isKickTask) { // すでに呼ばれている場合は何もしない
				Logger.w(TAG, "Another thread executing kickTask. Skip this operation.");
				return;
			}
			isKickTask = true;
		}
		CommonExecutor.execute(new Runnable() {
			@Override
			public void run() {
				try {
					execKickTask();
				} catch (AcmsException e) {
					Logger.e(TAG, "execKickTask encountered an exception.", e);
				} catch (NetworkDisconnectedException e) {
					Logger.e(TAG, "execKickTask encountered an exception.", e);
				} finally {
					isKickTask = false;
				}
			}
		});
	}

	private void execKickTask() throws AcmsException, NetworkDisconnectedException {
		autoPaused = false;
		if (Logger.isDebugEnabled()) {
			Logger.d(TAG, "downloaderMap %s", getDownloadMapForDebug());
		}

		if (isFullActive()) { // 同時並行数を超える場合は実行を10秒後にする
			Logger.w(TAG, "Active download count is full. A task has been canceled. try later.");
			resuchedule();
			return;
		}
		
		// 新着更新が実行されている場合停止
		ContentRefresher.getInstance().stopRefresh();
		
		// 自動停止したDLの再開
		for (Long contentId: getAutoPausedList()) {
			if (isFullActive()) {
				break;
			}
			resume(contentId);
		}

		// DB上のDL待ちの実行
		loadWaitingContentFromDB();
		for (ContentDto dto : targetContentMapValues()) {
			if (dto.isDownloadWaiting()) {
				if (isFullActive()) {
					break;
				}
				startDownload(dto);
			}
		}
	}

	/**
	 * KickTaskを10秒後に再実行
	 */
	private synchronized void resuchedule() {
		//noinspection VariableNotUsedInsideIf
		if (kickTaskTimer != null) {
			return;
		}
		kickTaskTimer = new Timer();
		kickTaskTimer.schedule(new TimerTask() {
			@Override
			public void run() {
				kickTask();
				kickTaskTimer.cancel();
				synchronized (kickTaskLock) {
					kickTaskTimer = null;
				}
			}
		}, 10000L);
	}


	//// Downloader Map関係  ////
	
	/**
	 * MapのvalueをListにして返す
	 * for targetContentMap.values()でループを回すと、その全体を排他制御する必要があり、
	 * その中で重い処理がある場合、排他区間が長くなり、UIスレッドからの呼出しでも待ちが長くなる。
	 * またそのループの中で他の排他制御を取得することがあればデッドロックになりうる。
	 * そのため、MapのIteratorを使用しないリストにして返す。
	 * このメソッドでリストアップしたあとで、追加・削除が行われた場合、呼び出し側での処理にはそれは
	 * 反映されないので注意する必要がある。getActiveCount()で誤判定がありうるが、これは
	 * 新着更新ボタンと同時にDLボタンを押さない限りあり得ない。自動DLは新着更新の中で実行されるため
	 * リストの取得後のチェック中にダウンロード追加はない。また、kickTask実行時に追加された場合も、追加直後は
	 * Waitingのステータスのため、特に問題にはならない。
	 * 
	 */
	private List<ContentDto> targetContentMapValues() {
		List<ContentDto> list = new ArrayList<ContentDto>();
		synchronized (targetContentMap) {
			for (ContentDto dto : targetContentMap.values()) {
				list.add(dto);
			}
		}
		return list;
	}

	/**
	 * メモリ上のダウンロードマップ情報をすべて空にする
	 * 
	 */
	void clearAllDownloadMap() {
		Logger.i(TAG, "clearAllDownloadMap");
		pauseAll();
		synchronized (targetContentMap) {
			targetContentMap.clear();
		}
	}

	public HttpFileDownloader getDownloader(long contentId) {
		ContentDto dto = targetContentMap.get(contentId);
		if (dto != null) {
			return dto.downloader;
		}
		return null;
	}

	private List<Long> getAutoPausedList() {
		List<Long> list = new ArrayList<Long>();
		for (ContentDto dto : targetContentMapValues()) {
			if (dto.autoPaused) {
				list.add(dto.contentId);
			}
		}
		Logger.d(TAG, "autoPaused download list : %s", list);
		return list;
	}

	private void removeDownload(Long contentId) {
		Logger.d(TAG, "removeDownloader contentId=%s", contentId);
		synchronized (targetContentMap) {
			targetContentMap.remove(contentId);
		}
		if (targetContentMap.isEmpty()) {
			Logger.i(TAG, "AutoDownloadEnd");
		}
	}

	/**
	 * DL実行中の数が最大数以上かどうか
	 * 
	 * @return
	 */
	private boolean isFullActive() {
		return (getActiveCountWithoutWaiting() >= maximumDownloadable);
	}

	/**
	 * 実行待ちを含めてすべてのダウンローダの数を返す
	 * 
	 * @return
	 */
	int getActiveCount() {
		int activeCount = 0;
		for (ContentDto dto : targetContentMapValues()) {
			if (ArrayUtil.equalsAny(dto.getDownloadStatus(), DownloadStatusType.Waiting, DownloadStatusType.Downloading, DownloadStatusType.Initializing)) {
				activeCount++;
			}
		}
		return activeCount;
	}

	/**
	 * 実行待ちを除いたダウンローダの数を返す
	 * 
	 * @return
	 */
	public int getActiveCountWithoutWaiting() {
		int activeCount = 0;
		for (ContentDto dto : targetContentMapValues()) {
			if (ArrayUtil.equalsAny(dto.getDownloadStatus(), DownloadStatusType.Downloading, DownloadStatusType.Initializing)) {
				activeCount++;
			}
		}
		return activeCount;
	}

	/**
	 * DB上のコンテンツで、ダウンローダ登録されていないものをロードする
	 * 
	 * @return
	 * @throws ABVException 
	 */
	private void loadWaitingContentFromDB() {
		for (ContentDto contentDto : contentDao.getDownloadWaiting(maximumDownloadable)) {
			if (!targetContentMap.containsKey(contentDto.contentId)) {
				getOperableContent(contentDto.contentId, null); // Mapにセットされるので戻り値は使用しない
			}
		}
	}

	/**
	 * 操作可能なコンテンツを取得する。
	 * Mapにコンテンツがない場合はDBからロードする。
	 * （同じコンテンツに同一の操作が行われないか排他制御を行ってチェックする）
	 * 
	 * @param contentId
	 * @return
	 * @throws ABVException 
	 */
	private ContentDto getOperableContent(long contentId, DownloadStatusType nextTarget) {
		Lock lock = null;
		try {
			// 同じcontentIdは、1スレッドのみ通す
			lock = multiLock.acquireLock(contentId);
			synchronized (lock) {
				ContentDto contentDto = targetContentMap.get(contentId);
				if (contentDto == null) {
					if (nextTarget == DownloadStatusType.Paused) {
						Logger.e(TAG, "the content is not downloading. contentId=" + contentId);
						return null;
					}
					
					contentDto = contentDao.getContent(contentId);
					if (contentDto == null) {
						throw new RuntimeException(new ABVException(ABVExceptionCode.C_E_CONTENT_1002, "contentId:" + contentId));
					}
					else {
						if (!contentDto.downloadedFlg || contentDto.updatedFlg) { // statusはチェックしなくても左記のフラグでカバーされる
							contentDto.status = DownloadStatusType.Waiting.type();
							contentDto.downloadingFlg = true;
							contentDao.updateDownload(contentDto);
							synchronized (targetContentMap) {
								targetContentMap.put(contentId, contentDto);
							}
							return contentDto;
						}
						else {
							Logger.e(TAG, "the content is downloaded or not updated. contentId=" + contentId);
							return null;
						}
					}
				}
				else {
					if (nextTarget == null && (contentDto.isDownloadWaiting() || contentDto.isDownloadPaused())) {
						return contentDto;
					}
					else if (nextTarget == DownloadStatusType.Paused && contentDto.isDownloading()) {
						return contentDto;
					}
					else if (nextTarget == DownloadStatusType.Downloading && contentDto.isDownloadPaused()) {
						contentDto.downloadingFlg = true;
						contentDto.status = DownloadStatusType.Waiting.type();
						contentDao.updateDownload(contentDto);
						return contentDto;
					}
				}
				return null;
			}
		}
		finally {
			// ロックを解放する
			if (lock != null) {
				multiLock.releaseLock(lock);
			}
		}
	}



	//// DL操作 ////

	/**
	 * コンテンツをダウンロード（予約）する。
	 * (ネットワークに接続されていない場合はエラーを投げる）
	 * 
	 * @param contentId ダウンロードするコンテンツのID
	 * @throws NetworkDisconnectedException 
	 * @throws ABVException 
	 * @since 1.0.0
	 */
	public void download(long contentId) throws NetworkDisconnectedException, ABVException {
		if (!networkAdapter.isNetworkConnected()) {
			throw new NetworkDisconnectedException();
		}

		ContentRefresher.getInstance().stopRefresh();
		reserveDownload(contentId, true);
	}

	/**
	 * コンテンツをダウンロード（予約）する。
	 * DBに登録するのみで、kickTaskでDL実行
	 * 
	 * @param contentId
	 * @param kickDownloaderNow
	 * @throws ABVException
	 */
	public void reserveDownload(long contentId, boolean kickDownloaderNow) {
		ContentDto contentDto = getOperableContent(contentId, null);
		if (contentDto == null) {
			Logger.w(TAG, "reserveDownload called but already another thread is processing the contentId(%s).", contentId);
			return;
		}
		if (kickDownloaderNow) {
			kickTask();
		}
	}

	/**
	 * ダウンロードの再開
	 * (ダウンローダがある場合はKickTaskなしに即レジュームを実行する)
	 * 
	 * @param contentId
	 * @throws NetworkDisconnectedException 
	 * @throws AcmsException 
	 * @since 1.0.0
	 */
	public void resume(long contentId) throws NetworkDisconnectedException, AcmsException {
		if (!networkAdapter.isNetworkConnected()) {
			throw new NetworkDisconnectedException();
		}

		ContentDto contentDto = getOperableContent(contentId, DownloadStatusType.Downloading);
		if (contentDto == null) {
			Logger.w(TAG, "resume called but already another thread is processing the contentId(%s).", contentId);
			return;
		}
		synchronized (contentDto) {
			if (contentDto.isDownloading()) {
				Logger.w(TAG, "resume called but already another thread is processing the contentId(%s).", contentId);
				return;
			}
			if (contentDto.downloader != null && !isFullActive()) {
				contentDto.status = DownloadStatusType.Downloading.type();
				contentDto.downloader.resume();
				new DownloadingPercentageWorker(contentDto.downloader).start();
			}
			else {
				contentDto.status = DownloadStatusType.Waiting.type();
				kickTask();
			}
		}
	}

	public void autoDownload() {
		CommonExecutor.execute(new Runnable() {
			@Override
			public void run() {
				// コンテンツを自動DLする
				Logger.d("autoDownload:" + autoDownloadList.size());
				int targetCount = 0;
				for (Long contentId : autoDownloadList) {
					ContentDto contentDto = contentDao.getContent(contentId);
					if (contentDto.isLinkType() && contentDto.isDownloadable(true)) {
						reserveDownload(contentId, false);
						targetCount++;
					}
				}
				if (targetCount > 0) {
					kickTask();
					Logger.i(TAG, "AutoDownloadStart. DownloadTargetCount:" + targetCount);
				}
				autoDownloadList.clear();
			}
		});
	}


	/**
	 * 指定したコンテンツのダウンロードを一時停止
	 * @param contentId
	 * @param autoPaused
	 */
	public void pause(long contentId, boolean autoPaused) {
		ContentDto contentDto = getOperableContent(contentId, DownloadStatusType.Paused);
		if (contentDto == null) {
			Logger.w(TAG, "pause called but already another thread is processing the contentId(%s) or not downloading.", contentId);
			return;
		}
		synchronized (contentDto) {
			if (contentDto.downloader != null && contentDto.downloader.isDownloading()) {
				contentDto.autoPaused = autoPaused;
				contentDto.downloader.pause();
			}
		}
	}

	public void pauseAll() {
		CommonExecutor.execute(new Runnable() {
			@Override
			public void run() {
				autoPaused = true;
				for (Long contentId : targetContentMap.keySet()) { // synchronized不要
					pause(contentId, true);
				}
			}
		});
	}


	/**
	 * ダウンロードをキャンセル
	 * ダウンロードしていたファイルは削除<br>
	 * ★ダウンロード・zip解凍中は呼び出さないこと。停止中の時のみ呼び出すようにする。
	 * 
	 * @param contentId
	 */
	public void cancel(long contentId) {
		HttpFileDownloader downloader = getDownloader(contentId);
		if (downloader != null) {
			downloader.cancel(); // mapからのremoveはObserver経由で行う
		} else {
			removeDownload(contentId);
			//	ダウンロード中のコンテンツがないときに、DBを調べ、フラグが不正な場合、キャンセルに変更する。
			if (contentDao.getContent(contentId) != null && !contentDao.getContent(contentId).isDownloadSucceeded()) {
				//	フラグ変更
				contentDao.updateContent(contentId, DownloadStatusType.Canceled);
			}
		}
	}

	/**
	 * 現在ダウンロード中のものを停止し、待機中のものをキャンセルする
	 * 
	 */
	public void cancelExeptDownloading() {
		pauseAll();
		for (ContentDto contentDto : contentDao.getUnfinishedDownloadAll()) {
			if (!targetContentMap.containsKey(contentDto.contentId)) {
				cancel(contentDto.contentId);
			}
		}
	}
	
	/**
	 * debug用にマップの内容を記した文字列を返す
	 */
	public String getDownloadMapForDebug() {
		StringBuffer sb = new StringBuffer("--");
		for (ContentDto dto : targetContentMapValues()) {
			sb.append("[");
			sb.append("contentId=" + dto.contentId);
			sb.append(", contractContentId=" + dto.contractContentId);
			sb.append(", contentName=" + dto.contentName);
			sb.append(", status=" + dto.status);
			sb.append(", downloadProgress=" + dto.downloadProgress);
			if (dto.downloader != null) {
				sb.append(", fileSize=" + dto.downloader.getFileSize());
				sb.append(", dlRate=" + dto.downloader.getDownloadRate());
				sb.append(", state=" + dto.downloader.getState());
			}
			sb.append("]\n");
		}
		return sb.toString();
	}
	
	
	/// ダウンロード実行処理  ///

	/**
	 * ダウンロードを開始する
	 * 
	 * @param contentDto
	 * @throws AcmsException
	 * @throws NetworkDisconnectedException
	 */
	private void startDownload(ContentDto contentDto) throws AcmsException, NetworkDisconnectedException {
		contentDto.status = DownloadStatusType.Downloading.type();
		contentDto.resourcePath = ABVEnvironment.getInstance().getContentResourcesDirectoryPath(contentDto.contentId, false);
		contentDto.downloadStartDate = DateTimeUtil.getCurrentDate();
		contentDao.updateContent(contentDto, false);

		//	DL開始

		// 開始のログ送信
		try {
			sendDownloadStartLog(contentDto);
		} catch (Exception e) {
			Logger.e(TAG, "sendDownloadLogStart failed.", e);
		}

		GetContentParameters param = getContentParameter(contentDto);
		String downloadUrl = getDownloadUrl(param);
		Logger.d(TAG, "[url : %s], [param : %s]", downloadUrl, param.toHttpParameterString());

		HttpFileDownloader downloader = executeDownload(contentDto, downloadUrl);

		contentDto.downloader = downloader;
	}

	HttpFileDownloader executeDownload(ContentDto contentDto, String downloadUrl) {
		String fileName = ABVEnvironment.getInstance().getContentDownloadPath(contentDto.contentId);
		long startByte = modifyStartByte(contentDto.downloadedBytes, fileName);

		// ダウンロード実行
		HttpFileDownloader downloader = new HttpFileDownloader(downloadUrl, fileName, startByte, downloadObserver, contentDto.contentId);
		downloader.downloadAsync();
		new DownloadingPercentageWorker(downloader).start();
		return downloader;
	}

	private long modifyStartByte(long startByte, String fileName) {
		//	startByteが0より大きい場合、つまり前回に一時停止していたファイルのダウンロードを再開する場合、前回のファイルが存在しないと最初からダウンロードする。
		if (startByte > 0 && !(new File(fileName).exists())) {
			startByte = 0L;
			Logger.d(TAG, "paused file not found.%s", fileName);
		} else if (startByte > 0) {
			Logger.d(TAG, "paused file size : %s, startByte : %s", new File(fileName).length(), startByte);
		}
		return startByte;
	}


	private void sendDownloadStartLog(ContentDto contentDto) throws NetworkDisconnectedException, AcmsException {
		ContentDownloadLogParameters downloadParam = new ContentDownloadLogParameters(cache.getMemberInfo().sid, contentDto.contentId, DateTimeUtil.getCurrentTimestamp()
				, contentDto.resourceVersion, ABVEnvironment.DeviceTypeId, DownloadStatusType.Downloading);
		AcmsClient acmsClient = AcmsClient.getInstance(cache.getUrlPath(), ABVEnvironment.getInstance().networkAdapter);
		acmsClient.contentDownloadLog(downloadParam);
	}

	private String getDownloadUrl(GetContentParameters param) {
		String downloadUrl = AcmsApis.getDownloadUrl(ABVEnvironment.getInstance().downloadServerAddress, cache.getUrlPath(), param.getContentId(), param.getSid(), param.getContentType());
		if (!StringUtil.isNullOrEmpty(param.getProductId())) {
			downloadUrl = downloadUrl + "?productId=" + param.getProductId() + "&purchaseToken=" + param.getPurchaseToken();
		}
		else if (!StringUtil.isNullOrEmpty(param.getAccount())) {
			downloadUrl = downloadUrl + "?account=" + param.getAccount();
		}
		if (!StringUtil.isNullOrEmpty(param.loginId)) {
			String key = SecurityUtil.getEncryptString(param.loginId.substring(0,16), "" + System.currentTimeMillis(), true); // TODO: Ad以外で行う場合１６バイト0埋めが必要
			downloadUrl = downloadUrl + (downloadUrl.contains("?")?"&":"?") + "key=" + key;
		}
		return downloadUrl;
	}

	private GetContentParameters getContentParameter(ContentDto contentDto) {
		GetContentParameters param;
		param = new GetContentParameters(contentDto.contentId, cache.getMemberInfo().sid, ContentZipType.ContentDownload);
		return param;
	}

	/**
	 * コンテンツDLログの送信
	 * 
	 * @param dto
	 * @param statusType
	 * @throws NetworkDisconnectedException
	 * @throws AcmsException
	 */
	private void sendDownloadLog(ContentDto dto, DownloadStatusType statusType) {
		ContentDownloadLogParameters downloadParam = new ContentDownloadLogParameters(cache.getMemberInfo().sid, dto.contentId
				, new Timestamp(dto.downloadStartDate.getTime()), dto.resourceVersion, ABVEnvironment.DeviceTypeId, statusType);
		downloadParam.setDownloadEndtime(DateTimeUtil.getCurrentTimestamp());
		downloadParam.setDownloadSize(dto.downloadedBytes);

		try {
			if (AcmsClient.getInstance(cache.getUrlPath(), ABVEnvironment.getInstance().networkAdapter).contentDownloadLog(downloadParam)) {
				dto.logSendedFlg = true;
				contentDao.updateLogSendFlg(dto);
			}
		} catch (Exception e) { // ignore
			Logger.e(TAG, "send contentDownloadLog failed.", e);
		}
	}


	//// ダウンロードObserver ////

	/**
	 * ダウンロードのステータスの変更を監視する
	 * （プログレスの変更はDownloadingPercentageWorkerで監視する）
	 *
	 */
	private class DownloadObserver implements Observer {
		@Override
		public void update(Observable o, Object arg) {
			HttpDownloadNotification notification = (HttpDownloadNotification) arg;
			Object customInfo = notification.getCustomInformation();
			if (customInfo == null) {
				Logger.e(TAG, "Invalid content id. detail : " + notification.toString());
				return;
			}

			long contentId = (Long) customInfo;
			switch (notification.getDownloadState()) {
			case downloading:
				changeDownloadStatus(contentId, notification, DownloadStatusType.Downloading);
				break;
			case paused:
				changeDownloadStatus(contentId, notification, DownloadStatusType.Paused);
				break;
			case failed:
			case canceled:
				changeDownloadStatus(contentId, notification, notification.getDownloadState()==HttpDownloadState.failed?
						DownloadStatusType.Failed: DownloadStatusType.Canceled);
				FileUtil.delete(notification.getDownloadFileName());
				break;
			case finished:
				changeDownloadStatus(contentId, notification, DownloadStatusType.Initializing);
				//	ZIP解凍
				try {
					ContentDto contentDto = targetContentMap.get(contentId);
					if (contentDto != null) {
						contentFileExtractor.initializeContent(contentId, notification.getDownloadFileName(), notification.getOutputFileName(), contentDto); // infoDtoのzipパスがnullの場合があるので修正
					}
					changeDownloadStatus(contentId, notification, DownloadStatusType.Succeeded);
					contentFileExtractor.configureContent(contentId);
				} catch (Exception e) {
					Logger.e(TAG, "Can't extract zip file. detail : " + notification.toString(), e);
					notification.setError(e);
					notification.setDownloadState(HttpDownloadState.failed);
					changeDownloadStatus(contentId, notification, DownloadStatusType.Failed);
				} finally {
					//	ダウンロードしていたファイルの削除
					FileUtil.delete(notification.getDownloadFileName());
				}
				break;
			default: //	resumeは通知しない。
				break;
			}
		}
	}

	@SuppressWarnings("incomplete-switch")
	private void changeDownloadStatus(long contentId, HttpDownloadNotification notification, DownloadStatusType statusType) {
		ContentDto dto = targetContentMap.get(contentId); 
		if (dto == null) {
			Logger.e(TAG, "download job not found. contentId : " + contentId);
			return;
		}
		
		// ログ出力
		Logger.i(TAG, "[changeDownloadStatus]:content download status: %s. (%s)", statusType.display(), dto.contentId);

		// DBステータス(コンテンツテーブル）更新
		switch (statusType) {
			case Waiting:
				break;
			case Paused:
				break;
			case AutoPaused:
				break;
			case Initializing:
				break;
			case None:
				break;
			case Downloading: //	ダウンロード開始。
				dto.downloadingFlg = true; // ここでセットしなくてもすでにtrueになっているはずだが念のため
				break;
			case Succeeded:        //	正常終了
				dto.downloadedFlg = true;
				dto.updatedFlg = false;// breakせず続けて適用
			case Canceled:        //	キャンセル
			case Failed:            //	失敗
				dto.downloadEndDate = DateTimeUtil.getCurrentDate();
				dto.downloadingFlg = false;
				break;
			default:
				break;
		}
		dto.status = statusType.type();
		if (notification != null) {
			dto.downloadedBytes = notification.getDownloadedSize();
			dto.downloadProgress = (int)(notification.getDownloadRate());
		}
		contentDao.updateDownload(dto);

		// 失敗と完了のときログを送信し、ダウンローダを削除し、再度KickTaskを呼ぶ
		if (ArrayUtil.equalsAny(statusType, DownloadStatusType.Succeeded, DownloadStatusType.Canceled, DownloadStatusType.Failed)) {
			sendDownloadLog(dto, statusType);
			removeDownload(dto.contentId);
			if (!autoPaused) {
				kickTask();
			}
		}

		// Activityへ通知
		if (notification != null) {
			onDownloadingContentZip(notification, statusType);
		}
	}

	private void onDownloadingContentZip(HttpDownloadNotification notification, DownloadStatusType downloadStatus) {
		ContentZipDownloadNotification dlNotification = new ContentZipDownloadNotification(notification, downloadStatus);
		for (ContentDownloadListener listener : contentDownloadListenerSet) {
			listener.onDownloadingContentZip(dlNotification);
		}
	}

	/// Progress /// 

	/**
	 * ダウンロードのプログレスを通知するスレッド
	 * HttpFileDownloaderから通知されるのではなく、メンバーに持っておいてポーリングして、
	 * ステータスをチェックして、DL中の場合、リスナーに通知する。
	 * 
	 */
	private class DownloadingPercentageWorker extends Thread {
		private HttpFileDownloader downloader;
		private int maxNoChange;
		private int noChangeCount;
		private long prevDownloadedSize;

		public DownloadingPercentageWorker(HttpFileDownloader downloader) {
			this.downloader = downloader;
			maxNoChange = 60 * 1000 / downloadingPercentageNotifyInterval; // 1分間変化がない場合にpause
		}

		@Override
		public void run() {
			if (downloader == null) {
				return;
			}
			
			do {
				HttpDownloadNotification notification = downloader.getNotification();
				long downloadedSize = notification.getDownloadedSize();
				if (downloadedSize == prevDownloadedSize) {
					if (notification.getDownloadRate() > 100) { // 100%超なら不正状態なのでcancelになるようにする
						Logger.e(TAG, "content download seems invalid. cancel download the content. file=%s rate=%s",notification.getDownloadFileName(), notification.getDownloadRate());
						cancel((Long)notification.getCustomInformation());
					}
					else if (notification.getDownloadRate() < 100) { // cancelだとやり直しになるのでpauseにする
						noChangeCount++;
						if (noChangeCount > maxNoChange) {
							Logger.e(TAG, "content download seems to freeze. pause downloading the content. file=%s rate=%s",notification.getDownloadFileName(), notification.getDownloadRate());
							pause((Long)notification.getCustomInformation(), true);
							resuchedule();
							break;
						}
					}
				}
				else {
					prevDownloadedSize = downloadedSize;
					onDownloadingContentZip(notification, DownloadStatusType.Downloading);
				}
				try {
					Thread.sleep(downloadingPercentageNotifyInterval);
				} catch (InterruptedException e) {
				}
			} while (downloader.getState() == HttpDownloadState.downloading);
		}
	}


	//// Content Info ////

	public void downloadUnAuthorizedContentInfo(long contentId) throws MalformedURLException {
		GetContentParameters param = getContentInforParameters(contentId);
		String filename = getContentInfoZipFilePath(param);
		ContentRefresher.getInstance().addRefreshingContentId(contentId);
		try {
			Thread thread = AcmsClient.getInstance(cache.getUrlPath(), networkAdapter).getContentInfoAsync(param,filename, contentInforDownloadObserver, contentId);
			thread.join();
		} catch (InterruptedException e) {
			Logger.e(TAG,e);
		}
	}

	private GetContentParameters getContentInforParameters(long contentId) {
		return new GetContentParameters(contentId, cache.getMemberInfo().sid, ContentZipType.ContentInfo);
	}

	private String getContentInfoZipFilePath(GetContentParameters param) {
		return String.format("%s/%s_%s.zip", ABVEnvironment.getInstance().getContentDownloadsDirectoryPath(), param.getContentType().name(), param.getContentId());
	}

	private Observer contentInforDownloadObserver = new Observer() {
		@Override
		public void update(Observable o, Object arg) {
			HttpDownloadNotification notification = (HttpDownloadNotification) arg;
			Object customInfo = notification.getCustomInformation();

			if (customInfo != null) {
				long contentId = (Long) customInfo;
				switch (notification.getDownloadState()) {
					case downloading:
						break;
					case finished:
						//	ZIP解凍
						try {
							String contentPath = ABVEnvironment.getInstance().getContentDirectoryPath(contentId, false) + File.separator;
                            Logger.w(TAG, "[update]: contentPath=" + contentPath);
                            FileUtil.deleteFileOnly(contentPath);
							contentFileExtractor.removeContentCash(contentId);
							contentFileExtractor.extractZipFile(contentId, notification.getOutputFileName(), contentPath);
							contentLogic.saveContentInfo(contentPath);

							updateRefreshContentListState(HttpDownloadState.finished, contentId, null);
						} catch (Exception e) {
							downloadSucceededFlag = false;
							Logger.e("Can't extract zip file. detail : " + notification.toString(), e);
							updateRefreshContentListState(HttpDownloadState.failed, contentId, new ABVException(ABVExceptionCode.C_E_CONTENT_1003));
						} finally {
							//	ダウンロードしていたファイルの削除
							FileUtil.delete(notification.getDownloadFileName());
						}
						break;
					case paused: // pause(自動停止のみ)は失敗とみなす
					case canceled: // 発生はしないが念のため
					case failed:
						downloadSucceededFlag = false;
						Logger.w("contentInforDownload is failed :" + notification.getDownloadState());
						//	ダウンロードしていたファイルの削除
						FileUtil.delete(notification.getDownloadFileName());
						updateRefreshContentListState(HttpDownloadState.failed, contentId, notification.getError());

						// ダウンロード失敗時ネットワーク非接続の場合リフレッシュを止める
						try {
							if (!networkAdapter.isNetworkConnected()) {
								ContentRefresher.getInstance().stopRefresh();
							}
						} catch (Exception e) {
							Logger.d(TAG, "can not stop refresh worker."); // ignore
						}
						break;
					default:
						break;
				}
			} else {
				FileUtil.delete(notification.getDownloadFileName());

				String msg = "Invalid content id. detail : " + notification.toString();
				updateRefreshContentListState(HttpDownloadState.failed, null, new Exception(msg));
			}
		}
		private void updateRefreshContentListState(HttpDownloadState state, Long contentId, Exception e) {
			ContentRefresher.getInstance().removeRefreshingContentId(contentId);
			ContentRefresher.getInstance().updateRefreshContentListState(contentId, e);
		}
	};

	/**
	 * 新着コンテンツのバージョン情報をダウンロードします。
	 * @param contentId ダウンロードするコンテンツのIDです。
	 * @return コンテンツのバージョン情報のJSONファイルのパスを返します。
	 * @since 1.0.0
	 */
	public void downloadContentInfo(long contentId) throws Exception {
		GetContentParameters param = new GetContentParameters(contentId, cache.getMemberInfo().sid, ContentZipType.ContentInfo);
		String fileName = String.format("%s/%s_%s.zip", ABVEnvironment.getInstance().getContentDownloadsDirectoryPath(), param.getContentType().name(), param.getContentId());
		ContentRefresher.getInstance().addRefreshingContentId(contentId);

		AcmsClient.getInstance(cache.getUrlPath(), networkAdapter).getContentInfoAsync(param,fileName, contentInforDownloadObserver, contentId);
	}

    public void setDownloadSucceededFlag(boolean flag) {
        this.downloadSucceededFlag = flag;
    }

    public boolean isDownloadSucceeded() {
        return this.downloadSucceededFlag;
    }
}
