public class Constants {

	// public static final String BaseURL = "http://www.baka-tsuki.org/project/";
	public static final String BASE_URL_HTTPS = "http://fiction.icoolxue.com/index.php";
	public static final String BASE_URL = "http://fiction.icoolxue.com/index.php";
	public static final String BASE_IMAGE_URL = "http://fiction.icoolxue.com";
	// Intent parameter list
	public static final String EXTRA_NOVEL = "com.erakk.lnreader.NOVEL";
	public static final String EXTRA_PAGE = "com.erakk.lnreader.page";
	public static final String EXTRA_ID = "com.erakk.lnreader.id";
	public static final String EXTRA_SUMMARY = "com.erakk.lnreader.summary";
	public static final String EXTRA_TITLE = "com.erakk.lnreader.title";
	public static final String EXTRA_VOLUME = "com.erakk.lnreader.volume";
	public static final String EXTRA_ONLY_WATCHED = "com.erakk.lnreader.ONLY_WATCHED";
	public static final String EXTRA_IMAGE_URL = "com.erakk.lnreader.IMAGE_URL";
	public static final String EXTRA_SCROLL_X = "com.erakk.lnreader.SCROLL_X";
	public static final String EXTRA_SCROLL_Y = "com.erakk.lnreader.SCROLL_Y";
	public static final String EXTRA_P_INDEX = "pIndex";
	public static final String EXTRA_CALLER_ACTIVITY = "caller_activity";

	public static final String NOVEL_BOOK_DIVIDER = "%NOVEL_BOOK_DIVIDER%";
	public static final int CHECK_INTERVAL = 7;

	//小說類型id
	public static final String EXTRA_CATEGORY_ID="com.dotcool.category_id";
	public static final int EXTRA_HISTORY_CATEGORY_ID = 1;
	public static final int EXTRA_XUANHUAN_CATEGORY_ID = 3;
	public static final int EXTRA_KEHUAN_CATEGORY_ID = 7;
	public static final int EXTRA_QITA_CATEGORY_ID = 10;
	public static final int EXTRA_UPDATE_CATEGORY_ID = 11;
	public static final int EXTRA_MOOD_CATEGORY_ID = 12;
	
	// Shared Preferences keys
	public static final String PREF_FIRST_RUN = "first_run";
	public static final String PREF_DOWNLOAD_TOUCH = "touch_download_chapter";
	public static final String PREF_INVERT_COLOR = "invert_colors";
	public static final String PREF_LANGUAGE = "language_selection";
	public static final String PREF_LOCK_HORIZONTAL = "lock_horizontal";
	public static final String PREF_LAST_READ = "last_read";
	public static final String PREF_UPDATE_INTERVAL = "updates_interval";
	public static final String PREF_RUN_UPDATES = "run_update";
	public static final String PREF_RUN_UPDATES_STATUS = "run_update_status";
	public static final String PREF_DOWLOAD_BIG_IMAGE = "download_big_image";
	public static final String PREF_ZOOM_ENABLED = "enable_zoom";
	public static final String PREF_SHOW_IMAGE = "show_images";
	public static final String PREF_SHOW_ZOOM_CONTROL = "show_zoom_control";
	public static final String PREF_UPDATE_RING = "update_use_sound";
	public static final String PREF_UPDATE_VIBRATE = "update_use_vibration";
	public static final String PREF_PERSIST_NOTIFICATION = "persist_notification";
	public static final String PREF_USE_VOLUME_FOR_SCROLL = "use_volume_to_scroll";
	public static final String PREF_SCROLL_SIZE = "scroll_size";
	public static final String PREF_IS_NOVEL_ONLY = "novel_only";
	public static final String PREF_INVERT_SCROLL = "invert_scroll";
	public static final String PREF_ALPH_ORDER = "alphabet_order";
	public static final String PREF_SHOW_MISSING = "show_missing";
	public static final String PREF_SHOW_EXTERNAL = "show_external";
	public static final String PREF_KEEP_AWAKE = "keep_awake";
	public static final String PREF_FULSCREEN = "fullscreen";
	public static final String PREF_ENABLE_BOOKMARK = "enable_bookmark";
	public static final String PREF_ENABLE_WEBVIEW_BUTTONS = "enable_webview_buttons";
	public static final String PREF_USE_INTERNAL_WEBVIEW = "use_internal_webview";
	public static final String PREF_CONSOLIDATE_NOTIFICATION = "consolidate_notification";
	public static final String PREF_FORCE_JUSTIFIED = "force_justified";
	public static final String PREF_STRETCH_COVER = "strech_detail_image";
	public static final String PREF_LINESPACING = "line_spacing";
	public static final String PREF_USE_CUSTOM_CSS = "use_custom_css";
	public static final String PREF_CUSTOM_CSS_PATH = "custom_css_path";
	public static final String PREF_MARGINS = "margin_space";
	public static final String PREF_UI_SELECTION = "ui_selection";
	public static final String PREF_ORIENTATION = "orientation_lock";
	public static final String PREF_LAST_UPDATE = "last_update";
	public static final String PREF_TIMEOUT = "timeout";
	public static final String PREF_RETRY = "retry";
	public static final String PREF_INCREASE_RETRY = "increase_retry";
	public static final String PREF_IMAGE_SAVE_LOC = "save_location";
	public static final String PREF_AGGRESIVE_TITLE_CLEAN_UP = "aggresive_title_clean_up";
	public static final String PREF_USE_HTTPS = "use_https";
	public static final String PREF_HIDE_EMPTY_VOLUME = "hide_empty_volume";
	public static final String PREF_TTS_PITCH = "tts_pitch";
	public static final String PREF_TTS_SPEECH_RATE = "tts_reading_speed";
	public static final String PREF_TTS_DELAY = "tts_whitespace_delay";
	public static final String PREF_TTS_ENGINE = "tts_engine";
	public static final String PREF_TTS_TTS_STOP_ON_LOST_FOCUS = "tts_stop_on_lost_focus";
	public static final String PREF_AUTO_DOWNLOAD_UPDATED_CHAPTER = "auto_download_updated_chapter";
	public static final String PREF_BACKUP_THUMB_IMAGES = "backup_thumb_images";
	public static final String PREF_BACKUP_DB = "backup_database";
	public static final String PREF_RESTORE_DB = "restore_database";
	public static final String PREF_RESTORE_THUMB_IMAGES = "restore_thumb_images";
	public static final String PREF_RELINK_THUMB_IMAGES = "relink_images";
	public static final String PREF_BOOKMARK_ORDER = "bookmark_order";
	public static final String PREF_PROCESS_ALL_IMAGES = "process_all_images";
	public static final String PREF_SHOW_REDLINK = "show_redlink";

	public static final float DISPLAY_SCALE = LNReaderApplication.getInstance().getResources().getDisplayMetrics().density;

	@SuppressWarnings("deprecation")
	public static final int NOTIFIER_ID = (int) (new Date().getTime() - new Date(2012, 1, 1).getTime());
	public static final int CONSOLIDATED_NOTIFIER_ID = 20130210;

	public static final String STATUS_TEASER = "teaser";
	public static final String STATUS_STALLED = "stalled";
	public static final String STATUS_ABANDONED = "abandoned";
	public static final String STATUS_PENDING = "pending";
	public static final String STATUS_ORIGINAL = "original";
	public static final String STATUS_BAHASA_INDONESIA = "indonesian";

	public static final int COLOR_READ = Color.parseColor("#888888");
	public static final int COLOR_UNREAD = Color.parseColor("#110d0d");
	public static final int COLOR_UNREAD_DARK = Color.parseColor("#222222");
	public static final int COLOR_MISSING = Color.parseColor("#ff0000");
	public static final int COLOR_EXTERNAL = Color.parseColor("#3333ff");

	public static final String ROOT_NOVEL = "Main_Page";

	/* Section of Task Key */
	public static final String KEY_LOAD_CHAPTER = ":LoadChapter:";
	public static final String KEY_DOWNLOAD_CHAPTER = ":DownloadChapters:";
	public static final String KEY_DOWNLOAD_ALL_CHAPTER = ":DownloadChaptersAll:";

	/* Section of Language for novel parser */
	public static final String LANG_ENGLISH = "中文";

	/* You just need to add a new alternative language here and in AlternativeLanguageInfo -> initSingleton */
	public static final String[] languageList = { Constants.LANG_ENGLISH};
	public static final int BUFFER = 1024;

} public class LNReaderApplication extends FOCApplication {
	private static final String TAG = LNReaderApplication.class.toString();
	private static NovelsDao novelsDao = null;
	private static DownloadListActivity downloadListActivity = null;
	private static UpdateService service = null;
	private static LNReaderApplication instance;
	private static Hashtable<String, AsyncTask<?, ?, ?>> runningTasks;
	private static ArrayList<DownloadModel> downloadList;

	private static Object lock = new Object();

	
	public void onCreate() {
		super.onCreate();

		initSingletons();
		instance = this;

		doBindService();
		Log.d(TAG, "Application created.");
	}

	public static LNReaderApplication getInstance() {
		return instance;
	}

	protected void initSingletons() {
		if (novelsDao == null)
			novelsDao = NovelsDao.getInstance(this);
		if (downloadListActivity == null)
			downloadListActivity = DownloadListActivity.getInstance();
		if (runningTasks == null)
			runningTasks = new Hashtable<String, AsyncTask<?, ?, ?>>();
		if (downloadList == null)
			downloadList = new ArrayList<DownloadModel>();
	}

	/*
	 * AsyncTask listing method
	 */
	public static Hashtable<String, AsyncTask<?, ?, ?>> getTaskList() {
		return runningTasks;
	}

	public AsyncTask<?, ?, ?> getTask(String key) {
		return runningTasks.get(key);
	}

	public boolean addTask(String key, AsyncTask<?, ?, ?> task) {
		synchronized (lock) {
			if (runningTasks.containsKey(key)) {
				AsyncTask<?, ?, ?> tempTask = runningTasks.get(key);
				if (tempTask != null && tempTask.getStatus() != Status.FINISHED)
					return false;
			}
			runningTasks.put(key, task);
			return true;
		}
	}

	public boolean removeTask(String key) {
		synchronized (lock) {
			if (!runningTasks.containsKey(key))
				return false;
			runningTasks.remove(key);
			return true;
		}
	}

	public boolean isOnline() {
		ConnectivityManager cm = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE);
		NetworkInfo netInfo = cm.getActiveNetworkInfo();
		if (netInfo != null && netInfo.isConnectedOrConnecting()) {
			return true;
		}
		return false;
	}

	/*
	 * DownloadActivity method
	 */
	public int addDownload(String id, String name) {
		synchronized (lock) {
			downloadList.add(new DownloadModel(id, name, 0));
			if (DownloadListActivity.getInstance() != null)
				DownloadListActivity.getInstance().updateContent();
			return downloadList.size();
		}
	}

	public void removeDownload(String id) {
		synchronized (lock) {
			for (int i = 0; i < downloadList.size(); i++) {
				if (downloadList.get(i).getDownloadId() == id) {
					downloadList.remove(i);
					break;
				}
			}
		}
		if (DownloadListActivity.getInstance() != null)
			DownloadListActivity.getInstance().updateContent();
	}

	public String getDownloadDescription(String id) {
		String name = "";
		for (int i = 0; i < downloadList.size(); i++) {
			if (downloadList.get(i).getDownloadId() == id) {
				name = downloadList.get(i).getDownloadName();
				break;
			}
		}
		return name;
	}

	public boolean checkIfDownloadExists(String name) {
		synchronized (lock) {
			boolean exists = false;
			for (int i = 0; i < downloadList.size(); i++) {
				if (downloadList.get(i).getDownloadName().equals(name)) {
					exists = true;
				}
			}
			return exists;
		}
	}

	public ArrayList<DownloadModel> getDownloadList() {
		return downloadList;
	}

	

	public void updateDownload(String id, Integer progress, String message) {

		/*
		 * Although this may seem an attempt at a fake incremental download bar
		 * its actually a progressbar smoother.
		 */

		int index = 0;
		Integer oldProgress;
		final Integer Increment;
		int smoothTime = 1000;
		int tickTime = 25;
		int tempIncrease = 0;
		for (int i = 0; i < downloadList.size(); i++) {
			if (downloadList.get(i).getDownloadId() == id) {
				index = i;
			}
		}
		final int idx = index;

		// Download status message
		if (downloadList.get(idx) != null) {
			downloadList.get(idx).setDownloadMessage(message);
		}
		if (DownloadListActivity.getInstance() != null) {
			DownloadListActivity.getInstance().updateContent();
		}

		oldProgress = downloadList.get(index).getDownloadProgress();
		tempIncrease = (progress - oldProgress);
		if (tempIncrease < smoothTime / tickTime) {
			smoothTime = tickTime * tempIncrease;
			tempIncrease = 1;
		} else
			tempIncrease /= (smoothTime / tickTime);

		Increment = tempIncrease;
		new CountDownTimer(smoothTime, tickTime) {
			@Override
			public void onTick(long millisUntilFinished) {
				if (downloadList.size() > idx) {
					DownloadModel temp = downloadList.get(idx);
					if (temp != null) {
						temp.setDownloadProgress(temp.getDownloadProgress() + Increment);
					}
				}
				if (DownloadListActivity.getInstance() != null) {
					DownloadListActivity.getInstance().updateContent();
				}
			}

			@Override
			public void onFinish() {
			}
		}.start();
	}

	/*
	 * UpdateService method
	 */
	private final ServiceConnection mConnection = new ServiceConnection() {

		public void onServiceConnected(ComponentName className, IBinder binder) {
			service = ((UpdateService.MyBinder) binder).getService();
			Log.d(UpdateService.TAG, "onServiceConnected");
			Toast.makeText(getApplicationContext(), "Connected", Toast.LENGTH_SHORT).show();
		}

		public void onServiceDisconnected(ComponentName className) {
			service = null;
			Log.d(UpdateService.TAG, "onServiceDisconnected");
		}
	};

	private void doBindService() {
		bindService(new Intent(this, UpdateService.class), mConnection, Context.BIND_AUTO_CREATE);
		Log.d(UpdateService.TAG, "doBindService");
	}

	public void setUpdateServiceListener(ICallbackNotifier notifier) {
		if (service != null) {
			service.notifier = notifier;
		}
	}

	public void runUpdateService(boolean force, ICallbackNotifier notifier) {
		if (service == null) {
			doBindService();
			service.force = force;
			service.notifier = notifier;
		} else
			service.force = force;
		service.notifier = notifier;
		service.onStartCommand(null, BIND_AUTO_CREATE, (int) (new Date().getTime() / 1000));
	}

	@Override
	public void onLowMemory() {

		/*
		 * java.lang.IllegalArgumentException
		 * in android.app.LoadedApk.forgetServiceDispatcher
		 * 
		 * probable crash: service is not checked if it exists after onLowMemory.
		 * Technically fixed. needs checking.
		 */
		if (mConnection != null) {
			Log.w(TAG, "Low Memory, Trying to unbind service...");
			try {
				unbindService(mConnection);
				Log.i(TAG, "Unbind service done.");
			} catch (Exception ex) {
				Log.e(TAG, "Failed to unbind.", ex);
			}
		}
		super.onLowMemory();
	}

	/*
	 * CSS caching method.
	 * Also used for caching javascript.
	 */
	private Hashtable<Integer, String> cssCache = null;

	public String ReadCss(int styleId) {
		if (cssCache == null)
			cssCache = new Hashtable<Integer, String>();
		if (!cssCache.containsKey(styleId)) {
			cssCache.put(styleId, UIHelper.readRawStringResources(getApplicationContext(), styleId));
		}
		return cssCache.get(styleId);
	}

	public void resetFirstRun() {
		SharedPreferences.Editor edit = PreferenceManager.getDefaultSharedPreferences(this).edit();
		edit.remove(Constants.PREF_FIRST_RUN);
		edit.commit();
	}

	public void restartApplication() {
		Intent i = getBaseContext().getPackageManager().getLaunchIntentForPackage(getBaseContext().getPackageName());
		i.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
		startActivity(i);
	}
} public class UIHelper {

	private static final String TAG = UIHelper.class.toString();

	public static void CheckScreenRotation(Activity activity) {
		switch (getIntFromPreferences(Constants.PREF_ORIENTATION, 0)) {
		case 0:
			activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
			break;
		case 1:
			activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
			break;
		case 2:
			activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
			break;
		default:
			activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
			break;
		}
	}

	/**
	 * Set action bar behaviour, only for API Level 11 and up.
	 * 
	 * @param activity
	 *            target activity
	 * @param enable
	 *            enable up behaviour
	 */
	@SuppressLint("NewApi")
	public static void SetActionBarDisplayHomeAsUp(Activity activity, boolean enable) {
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
			ActionBar actionBar = activity.getActionBar();
			if (actionBar != null)
				actionBar.setDisplayHomeAsUpEnabled(enable);
		}
		// CheckScreenRotation(activity);
		CheckKeepAwake(activity);
	}

	/**
	 * Recreate the activity
	 * 
	 * @param activity
	 *            target activity
	 */
	@SuppressLint("NewApi")
	public static void Recreate(Activity activity) {
		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
			activity.recreate();
		else {
			activity.finish();
			activity.startActivity(activity.getIntent());
		}
		CheckScreenRotation(activity);
		CheckKeepAwake(activity);
	}

	/**
	 * Set up the application theme based on Preferences:Constants.PREF_INVERT_COLOR
	 * 
	 * @param activity
	 *            target activity
	 * @param layoutId
	 *            layout to use
	 */
	public static void SetTheme(Activity activity, Integer layoutId) {
		CheckScreenRotation(activity);
		if (PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(Constants.PREF_INVERT_COLOR, false)) {
			activity.setTheme(R.style.AppTheme2);
		} else {
			activity.setTheme(R.style.AppTheme);
		}
		if (layoutId != null) {
			activity.setContentView(layoutId);
		}
	}

	public static boolean CheckKeepAwake(Activity activity) {
		boolean keep = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(Constants.PREF_KEEP_AWAKE, false);
		if (keep) {
			activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
		}
		return keep;

	}

	/**
	 * Check whether the screen width is less than 600dp
	 * 
	 * @param activity
	 *            target activity
	 * @return true if less than 600dp
	 */
	@SuppressWarnings("deprecation")
	public static boolean IsSmallScreen(Activity activity) {
		DisplayMetrics metrics = new DisplayMetrics();
		activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
		Display display = activity.getWindowManager().getDefaultDisplay();
		if (display.getWidth() < (600 * metrics.density)) {
			return true;
		}
		return false;
	}

	@SuppressWarnings("deprecation")
	public static int getScreenHeight(Activity activity) {
		DisplayMetrics metrics = new DisplayMetrics();
		activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
		Display display = activity.getWindowManager().getDefaultDisplay();
		return display.getWidth();
	}

	@SuppressLint("NewApi")
	public static void ToggleFullscreen(Activity activity, boolean fullscreen) {
		if (fullscreen) {
			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
				ActionBar actionBar = activity.getActionBar();
				if (actionBar != null)
					actionBar.hide();
			} else {
				activity.requestWindowFeature(Window.FEATURE_NO_TITLE);
			}

			activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
			activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
		} else {
			activity.getWindow().addFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN);
			activity.getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
		}
	}

	@SuppressLint("NewApi")
	public static void ToggleActionBar(Activity activity, boolean show) {
		if (!show) {
			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
				ActionBar actionBar = activity.getActionBar();
				if (actionBar != null)
					actionBar.hide();
			} else {
				activity.requestWindowFeature(Window.FEATURE_NO_TITLE);
			}
		}
	}

	/**
	 * Toggle the Preferences:Constants.PREF_INVERT_COLOR
	 * 
	 * @param activity
	 *            target activity
	 */
	public static void ToggleColorPref(Activity activity) {
		SharedPreferences sharedPrefs = PreferenceManager.getDefaultSharedPreferences(activity);
		SharedPreferences.Editor editor = sharedPrefs.edit();
		if (sharedPrefs.getBoolean(Constants.PREF_INVERT_COLOR, true)) {
			editor.putBoolean(Constants.PREF_INVERT_COLOR, false);
		} else {
			editor.putBoolean(Constants.PREF_INVERT_COLOR, true);
		}
		editor.commit();
	}

	public static int getIntFromPreferences(String key, int defaultValue) {
		String value = PreferenceManager.getDefaultSharedPreferences(LNReaderApplication.getInstance().getApplicationContext()).getString(key, "");
		try {
			return Integer.parseInt(value);
		} catch (NumberFormatException ex) {
			return defaultValue;
		}
	}

	public static float getFloatFromPreferences(String key, float defaultValue) {
		String value = PreferenceManager.getDefaultSharedPreferences(LNReaderApplication.getInstance().getApplicationContext()).getString(key, "");
		try {
			return Float.parseFloat(value);
		} catch (NumberFormatException ex) {
			return defaultValue;
		}
	}

	/**
	 * Create Yes/No Alert Dialog
	 * 
	 * @param context
	 * @param message
	 * @param caption
	 * @param listener
	 * @return
	 */
	public static AlertDialog createYesNoDialog(Context context, String message, String caption, DialogInterface.OnClickListener listener) {
		AlertDialog.Builder builder = new AlertDialog.Builder(context);
		builder.setMessage(message);
		builder.setTitle(caption);
		builder.setPositiveButton("Yes", listener);
		builder.setNegativeButton("No", listener);
		return builder.create();
	}

	/**
	 * Change the color of image in ImageView, works nicely with single coloured images.
	 * 
	 * @param targetIv
	 */
	public static ImageView setColorFilter(ImageView targetIv) {
		if (PreferenceManager.getDefaultSharedPreferences(targetIv.getContext()).getBoolean(Constants.PREF_INVERT_COLOR, true)) {
			targetIv.setColorFilter(Constants.COLOR_UNREAD);
		} else {
			targetIv.setColorFilter(Constants.COLOR_UNREAD_DARK);
		}
		return targetIv;
	}

	public static Drawable setColorFilter(Drawable targetIv) {
		if (PreferenceManager.getDefaultSharedPreferences(LNReaderApplication.getInstance().getApplicationContext()).getBoolean(Constants.PREF_INVERT_COLOR, true)) {
			targetIv.setColorFilter(Constants.COLOR_UNREAD, Mode.SRC_ATOP);
		} else {
			targetIv.setColorFilter(Constants.COLOR_UNREAD_DARK, Mode.SRC_ATOP);
		}
		return targetIv;
	}

	public static void setLanguage(Context activity, String key) {
		try {
			/* Changing configuration to user's choice */
			Locale myLocale = new Locale(key);
			Log.d(TAG, "Locale: " + key);
			Resources res = activity.getResources();
			DisplayMetrics dm = res.getDisplayMetrics();
			Configuration conf = res.getConfiguration();
			conf.locale = myLocale;
			/* update resources */
			res.updateConfiguration(conf, dm);
		} catch (Exception ex) {
			Log.e(TAG, "Failed to set language: " + key, ex);
			setLanguage(activity, "en");
		}
	}

	public static void setLanguage(Context activity) {
		/* Set starting language */
		String locale = PreferenceManager.getDefaultSharedPreferences(activity).getString(Constants.PREF_LANGUAGE, "en");
		setLanguage(activity, locale);
	}

	public static void setAlternativeLanguagePreferences(Context activity, String lang, boolean val) {
		/* Set Alternative Language Novels preferences */
		SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(activity);

		// write
		SharedPreferences.Editor editor = sharedPref.edit();
		editor.putBoolean(lang, val);
		editor.commit(); // save change
	}

	/**
	 * Get image root path, remove '/' from the last character.
	 * 
	 * @param activity
	 * @return
	 */
	public static String getImageRoot(Context activity) {
		String loc = PreferenceManager.getDefaultSharedPreferences(activity).getString(Constants.PREF_IMAGE_SAVE_LOC, "");
		if (Util.isStringNullOrEmpty(loc)) {
			Log.w(TAG, "Empty Path, use default path for image storage.");
			loc = Environment.getExternalStorageDirectory().getAbsolutePath().toString() + "/Android/data/" + Constants.class.getPackage().getName() + "/files";
		}
		if (loc.endsWith("/"))
			loc = loc.substring(0, loc.length() - 1);
		return loc;
	}

	/**
	 * Return HTTP or HTTPS based on pref.
	 * 
	 * @param activity
	 * @return
	 */
	public static String getBaseUrl(Context activity) {
		boolean useHttps = PreferenceManager.getDefaultSharedPreferences(activity).getBoolean(Constants.PREF_USE_HTTPS, false);
		if (useHttps)
			return Constants.BASE_URL_HTTPS;
		else
			return Constants.BASE_URL;
	}

	/**
	 * Read raw resources and return it as string
	 * 
	 * @param ctx
	 * @param resourceId
	 * @return
	 */
	public static String readRawStringResources(Context ctx, int resourceId) {
		InputStream in = ctx.getResources().openRawResource(resourceId);
		ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
		String temp = null;

		int i;
		try {
			i = in.read();
			while (i != -1)
			{
				byteArrayOutputStream.write(i);
				i = in.read();
			}
			in.close();
			temp = byteArrayOutputStream.toString();
			byteArrayOutputStream.close();
		} catch (Exception e) {
			Log.e(TAG, "Failed to Read Asset: " + resourceId, e);
		}
		return temp;
	}

	/* PREFERENCES HELPER */
	public static boolean getColorPreferences(Context ctx) {
		return PreferenceManager.getDefaultSharedPreferences(ctx).getBoolean(Constants.PREF_INVERT_COLOR, true);
	}

	public static boolean getDownloadTouchPreference(Context ctx) {
		return PreferenceManager.getDefaultSharedPreferences(ctx).getBoolean(Constants.PREF_DOWNLOAD_TOUCH, false);
	}

	public static boolean getStrechCoverPreference(Context ctx) {
		return PreferenceManager.getDefaultSharedPreferences(ctx).getBoolean(Constants.PREF_STRETCH_COVER, false);
	}

	public static boolean getZoomPreferences(Context ctx) {
		return PreferenceManager.getDefaultSharedPreferences(ctx).getBoolean(Constants.PREF_ZOOM_ENABLED, false);
	}

	public static boolean getZoomControlPreferences(Context ctx) {
		return PreferenceManager.getDefaultSharedPreferences(ctx).getBoolean(Constants.PREF_SHOW_ZOOM_CONTROL, false);
	}

	public static boolean getDynamicButtonsPreferences(Context ctx) {
		return PreferenceManager.getDefaultSharedPreferences(ctx).getBoolean(Constants.PREF_ENABLE_WEBVIEW_BUTTONS, false);
	}

	public static boolean getAllBookmarkOrder(Context ctx) {
		return PreferenceManager.getDefaultSharedPreferences(ctx).getBoolean(Constants.PREF_BOOKMARK_ORDER, false);
	}
} public class DisplayLightNovelDetailsActivity extends SherlockActivity implements IAsyncTaskOwner {
	public static final String TAG = DisplayLightNovelDetailsActivity.class.toString();
	private PageModel page;
	private NovelCollectionModel novelCol;
	private final NovelsDao dao = NovelsDao.getInstance(this);

	private BookModelAdapter bookModelAdapter;
	private ExpandableListView expandList;

	private DownloadNovelContentTask downloadTask = null;
	private LoadNovelDetailsTask task = null;

	private TextView loadingText;
	private ProgressBar loadingBar;
	private boolean isInverted;
	private String touchedForDownload;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		UIHelper.SetTheme(this, R.layout.activity_display_light_novel_details);
		UIHelper.SetActionBarDisplayHomeAsUp(this, true);

		// Get intent and message
		Intent intent = getIntent();
		page = new PageModel();
		page.setPage(intent.getStringExtra(Constants.EXTRA_PAGE));
		try {
			page = NovelsDao.getInstance(this).getPageModel(page, null);
		} catch (Exception e) {
			Log.e(TAG, "Error when getting Page Model for " + page.getPage(), e);
		}

		// setup listener
		expandList = (ExpandableListView) findViewById(R.id.chapter_list);
		registerForContextMenu(expandList);
		expandList.setOnChildClickListener(new OnChildClickListener() {
			public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) {
				if (novelCol != null) {
					PageModel chapter = bookModelAdapter.getChild(groupPosition, childPosition);
					String bookName = novelCol.getBookCollections().get(groupPosition).getTitle();
					touchedForDownload = bookName ;
					//loadChapter(chapter);
				}
				return false;
			}
		});
		expandList.setOnGroupClickListener(new OnGroupClickListener(){

            public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition,
                    long id) {
                if (novelCol != null) {
                    BookModel chapter = bookModelAdapter.getGroup(groupPosition);
                    loadCotent(chapter);
                  
                }
                    
                return false;
            }
		    
		});
		setTitle(page.getTitle());
		isInverted = UIHelper.getColorPreferences(this);

		loadingText = (TextView) findViewById(R.id.emptyList);
		loadingBar = (ProgressBar) findViewById(R.id.empttListProgress);

		executeTask(page, false);
	}
	private void loadCotent(BookModel chapter) {

        downloadTask = new DownloadNovelContentTask(chapter, DisplayLightNovelDetailsActivity.this);
        downloadTask.execute();
  
	}


	@Override
	protected void onRestart() {
		super.onRestart();
		if (isInverted != UIHelper.getColorPreferences(this)) {
			UIHelper.Recreate(this);
		}
		if (bookModelAdapter != null) {
			bookModelAdapter.notifyDataSetChanged();
		}
	}

	@Override
	protected void onResume() {
		super.onResume();
		Log.d(TAG, "OnResume: " + task.getStatus().toString());
	}

	@Override
	public void onStop() {

		super.onStop();
	}

	@Override
	public boolean onCreateOptionsMenu(Menu menu) {
		getSupportMenuInflater().inflate(R.menu.activity_display_light_novel_details, menu);
		return true;
	}

	@Override
	public boolean onOptionsItemSelected(MenuItem item) {
		switch (item.getItemId()) {
		case android.R.id.home:
			super.onBackPressed();
			return true;
		default:
			return super.onOptionsItemSelected(item);
		}
	}

	@Override
	public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
		super.onCreateContextMenu(menu, v, menuInfo);
		ExpandableListView.ExpandableListContextMenuInfo info = (ExpandableListView.ExpandableListContextMenuInfo) menuInfo;

		MenuInflater inflater = getMenuInflater();
		int type = ExpandableListView.getPackedPositionType(info.packedPosition);
		if (type == ExpandableListView.PACKED_POSITION_TYPE_GROUP) {
			inflater.inflate(R.menu.novel_details_volume_context_menu, menu);
		} else if (type == ExpandableListView.PACKED_POSITION_TYPE_CHILD) {
			inflater.inflate(R.menu.novel_details_chapter_context_menu, menu);
		}
	}

	
	@SuppressLint("NewApi")
	private void executeTask(PageModel pageModel, boolean willRefresh) {
		task = new LoadNovelDetailsTask(willRefresh, this);
		String key = TAG + Constants.KEY_LOAD_CHAPTER + pageModel.getPage();
		boolean isAdded = LNReaderApplication.getInstance().addTask(key, task);

		if (isAdded) {
			if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
				task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, new PageModel[] { pageModel });
			else
				task.execute(new PageModel[] { pageModel });
		} else {
			LoadNovelDetailsTask tempTask = (LoadNovelDetailsTask) LNReaderApplication.getInstance().getTask(key);
			if (tempTask != null) {
				task = tempTask;
				task.owner = this;
				toggleProgressBar(true);
			}
		}
	}

	@SuppressLint("NewApi")
	private void executeDownloadTask(BookModel chapters, boolean isAll) {
		if (page != null) {
			downloadTask = new DownloadNovelContentTask(chapters, this);
			String key = TAG + Constants.KEY_DOWNLOAD_CHAPTER + page.getPage();
			if (isAll) {
				key = TAG + Constants.KEY_DOWNLOAD_ALL_CHAPTER + page.getPage();
			}
			boolean isAdded = LNReaderApplication.getInstance().addTask(key, task);

			if (isAdded) {
				if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
					downloadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
				else
					downloadTask.execute();
			} else {
				DownloadNovelContentTask tempTask = (DownloadNovelContentTask) LNReaderApplication.getInstance().getTask(key);
				if (tempTask != null) {
					downloadTask = tempTask;
					downloadTask.owner = this;
				}
			}
		}
	}

	public boolean downloadListSetup(String id, String toastText, int type, boolean hasError) {
		boolean exists = false;
		String name = page.getTitle() + " " + touchedForDownload;
		if (type == 0) {
			if (LNReaderApplication.getInstance().checkIfDownloadExists(name)) {
				exists = true;
				Toast.makeText(this, getResources().getString(R.string.download_on_queue), Toast.LENGTH_SHORT).show();
			} else {
				Toast.makeText(this, getResources().getString(R.string.toast_downloading, name), Toast.LENGTH_SHORT).show();
				LNReaderApplication.getInstance().addDownload(id, name);
			}
		} else if (type == 1) {
			Toast.makeText(this, toastText, Toast.LENGTH_SHORT).show();
		} else if (type == 2) {
			String message = getResources().getString(R.string.toast_download_finish, page.getTitle(), LNReaderApplication.getInstance().getDownloadDescription(id));
			if (hasError)
				message = getResources().getString(R.string.toast_download_finish_with_error, page.getTitle(), LNReaderApplication.getInstance().getDownloadDescription(id));
			Toast.makeText(this, message, Toast.LENGTH_SHORT).show();
			LNReaderApplication.getInstance().removeDownload(id);
		}
		return exists;
	}

	public void updateProgress(String id, int current, int total, String message) {
		double cur = current;
		double tot = total;
		double result = (cur / tot) * 100;
		LNReaderApplication.getInstance().updateDownload(id, (int) result, message);
		if (loadingBar != null && loadingBar.getVisibility() == View.VISIBLE) {
			loadingBar.setIndeterminate(false);
			loadingBar.setMax(total);
			loadingBar.setProgress(current);
			loadingBar.setProgress(0);
			loadingBar.setProgress(current);
			loadingBar.setMax(total);
		}
	}

	public void toggleProgressBar(boolean show) {
		if (show) {
			loadingText.setText("加载中，请稍后...");
			loadingText.setVisibility(TextView.VISIBLE);
			loadingBar.setVisibility(ProgressBar.VISIBLE);
			loadingBar.setIndeterminate(true);
			expandList.setVisibility(ListView.GONE);
		} else {
			loadingText.setVisibility(TextView.GONE);
			loadingBar.setVisibility(ProgressBar.GONE);
			expandList.setVisibility(ListView.VISIBLE);
		}
	}

	public void setMessageDialog(ICallbackEventData message) {
		if (loadingText.getVisibility() == View.VISIBLE) {
			loadingText.setText(message.getMessage());
		}
	}

	@SuppressLint("NewApi")
	public void onGetResult(AsyncTaskResult<?> result, Class<?> t) {
		Exception e = result.getError();

		if (e == null) {
			// from DownloadNovelContentTask
			if (t == NovelContentModel.class) {
				NovelContentModel content = (NovelContentModel) result.getResult();
				if (content != null) {
				    Intent intent = new Intent(DisplayLightNovelDetailsActivity.this ,BookReadActivity.class);
			        intent.putExtra("net", 1);
			        intent.putExtra("net_content", content.getContent());
			        intent.putExtra("net_book_id", content.getId());
			        startActivity(intent);
					bookModelAdapter.notifyDataSetChanged();
				}
			}
			// from LoadNovelDetailsTask
			else if (t == NovelCollectionModel.class) {
				novelCol = (NovelCollectionModel) result.getResult();
				if(novelCol==null){
				      return;
				}
				expandList = (ExpandableListView) findViewById(R.id.chapter_list);
				// now add the volume and chapter list.
				try {
					// Prepare header
					if (expandList.getHeaderViewsCount() == 0) {
						page = novelCol.getPageModel();
						LayoutInflater layoutInflater = getLayoutInflater();
						View synopsis = layoutInflater.inflate(R.layout.activity_display_synopsis, null);
						TextView textViewTitle = (TextView) synopsis.findViewById(R.id.title);
						TextView textViewSynopsis = (TextView) synopsis.findViewById(R.id.synopsys);
						textViewTitle.setTextSize(20);
						textViewSynopsis.setTextSize(16);
						String title = page.getTitle();
						if (page.isTeaser()) {
							title += " (" + getResources().getString(R.string.teaser_project) + ")";
						}
						if (page.isStalled()) {
							title += "\nStatus: " + getResources().getString(R.string.project_stalled);
						}
						if (page.isAbandoned()) {
							title += "\nStatus: " + getResources().getString(R.string.project_abandonded);
						}
						if (page.isPending()) {
							title += "\nStatus: " + getResources().getString(R.string.project_pending_authorization);
						}

						textViewTitle.setText(title);
						textViewSynopsis.setText(novelCol.getPageModel().getSummary());

						CheckBox isWatched = (CheckBox) synopsis.findViewById(R.id.isWatched);
						isWatched.setChecked(page.isWatched());
						isWatched.setOnCheckedChangeListener(new OnCheckedChangeListener() {

							public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
								updateWatchStatus(isChecked, page);
							}
						});

						ImageView ImageViewCover = (ImageView) synopsis.findViewById(R.id.cover);
						if (novelCol.getCoverBitmap() == null) {
							// IN app test, is returning empty bitmap
							Toast.makeText(this, getResources().getString(R.string.toast_err_bitmap_empty), Toast.LENGTH_LONG).show();
						} else {

							if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) && UIHelper.getStrechCoverPreference(this)) {
								Drawable coverDrawable = new BitmapDrawable(getResources(), novelCol.getCoverBitmap());
								int coverHeight = novelCol.getCoverBitmap().getHeight();
								int coverWidth = novelCol.getCoverBitmap().getWidth();
								int screenWidth = (int) (UIHelper.getScreenHeight(this) * 0.9);
								int finalHeight = coverHeight * (screenWidth / coverWidth);
								ImageViewCover.setBackground(coverDrawable);
								ImageViewCover.getLayoutParams().height = finalHeight;
								ImageViewCover.getLayoutParams().width = screenWidth;
							} else {
								Log.d(TAG, "Non Stretch");
								ImageViewCover.setImageBitmap(novelCol.getCoverBitmap());
								ImageViewCover.getLayoutParams().height = novelCol.getCoverBitmap().getHeight();
								ImageViewCover.getLayoutParams().width = novelCol.getCoverBitmap().getWidth();
							}
						}

						expandList.addHeaderView(synopsis);
					}
					bookModelAdapter = new BookModelAdapter(DisplayLightNovelDetailsActivity.this, novelCol.getBookCollections());
					expandList.setAdapter(bookModelAdapter);
					Log.d(TAG, "Loaded: " + novelCol.getPage());
				} catch (Exception e2) {
					Log.e(TAG, "Error when setting up chapter list: " + e2.getMessage(), e2);
					Toast.makeText(DisplayLightNovelDetailsActivity.this, getResources().getString(R.string.error_setting_chapter_list, e2.getMessage()), Toast.LENGTH_SHORT).show();
				}

				if (novelCol == null) {
					Log.e(TAG, "Empty Novel Collection: " + getIntent().getStringExtra(Constants.EXTRA_PAGE));
				}
			}
		} else {
			Log.e(TAG, e.getClass().toString() + ": " + e.getMessage(), e);
			Toast.makeText(this, e.getClass().toString() + ": " + e.getMessage(), Toast.LENGTH_SHORT).show();
		}

		toggleProgressBar(false);
	}

	private void updateWatchStatus(boolean isChecked, PageModel page) {
		if (isChecked) {
			Toast.makeText(this, getResources().getString(R.string.toast_add_watch, page.getTitle()), Toast.LENGTH_SHORT).show();
		} else {
			Toast.makeText(this, getResources().getString(R.string.toast_remove_watch, page.getTitle()), Toast.LENGTH_SHORT).show();
		}
		// update the db!
		page.setWatched(isChecked);
		NovelsDao dao = NovelsDao.getInstance(this);
		dao.updatePageModel(page);
	}


	public Context getContext() {
		return this;
	}
} public interface INovelListHelper {
	public void manualAdd();
	public void downloadAllNovelInfo();
	public void refreshList();
	
} public class PageModelAdapter extends ArrayAdapter<PageModel> {
	private static final String TAG = PageModelAdapter.class.toString();
	private final Context context;
	private int layoutResourceId;
	public List<PageModel> data;

	public PageModelAdapter(Context context, int resourceId, List<PageModel> objects) {
		super(context, resourceId, objects);
		this.layoutResourceId = resourceId;
		this.context = context;
		this.data = objects;
		Log.d(TAG, "created with " + objects.size() + " items");
	}

	public void setLayout(int resourceId) {
		this.layoutResourceId = resourceId;
	}

	@Override
	public void addAll(PageModel... objects) {
		synchronized (this) {
			if (data == null) {
				data = new ArrayList<PageModel>();
			}
			for (PageModel pageModel : objects) {
				data.add(pageModel);
			}
		}

		this.notifyDataSetChanged();
	}

	@Override
	public void addAll(Collection<? extends PageModel> objects) {
		synchronized (this) {
			if (data == null) {
				data = new ArrayList<PageModel>();
			}
			data.addAll(objects);
		}

		this.notifyDataSetChanged();
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		View row = convertView;
		PageModelHolder holder = null;

		final PageModel page = data.get(position);

		LayoutInflater inflater = ((Activity) context).getLayoutInflater();
		row = inflater.inflate(layoutResourceId, parent, false);
		holder = new PageModelHolder();
		holder.txtNovel = (TextView) row.findViewById(R.id.novel_name);
		if (holder.txtNovel != null) {
			holder.txtNovel.setText(page.getTitle());
			if (page.isHighlighted()) {
				holder.txtNovel.setTypeface(null, Typeface.BOLD);
				holder.txtNovel.setTextSize(20);
				holder.txtNovel.setText("⇒" + holder.txtNovel.getText());
			}

			if (PreferenceManager.getDefaultSharedPreferences(context).getBoolean(Constants.PREF_INVERT_COLOR, true)) {
				holder.txtNovel.setTextColor(Constants.COLOR_UNREAD);
			}
			else {
				holder.txtNovel.setTextColor(Constants.COLOR_UNREAD_DARK);
			}
			if (page.isMissing()) {
				holder.txtNovel.setTextColor(Constants.COLOR_MISSING);
			}
			if (page.isExternal()) {
				holder.txtNovel.setTextColor(Constants.COLOR_EXTERNAL);
			}
			ImageView ivExternal = (ImageView) row.findViewById(R.id.is_external);
			if (ivExternal != null) {
				if (page.isExternal()) {
					ivExternal.setVisibility(View.VISIBLE);
					UIHelper.setColorFilter(ivExternal);
				}
				else {
					ivExternal.setVisibility(View.GONE);
				}
			}

			ImageView ivHasUpdates = (ImageView) row.findViewById(R.id.novel_has_updates);
			if (ivHasUpdates != null) {
				if (page.getUpdateCount() > 0) {
					ivHasUpdates.setVisibility(View.VISIBLE);
					UIHelper.setColorFilter(ivHasUpdates);
				}
				else {
					ivHasUpdates.setVisibility(View.GONE);
				}
			}
		}

		holder.txtLastUpdate = (TextView) row.findViewById(R.id.novel_last_update);
		if (holder.txtLastUpdate != null) {
			holder.txtLastUpdate.setText(context.getResources().getString(R.string.last_update) + ": " + Util.formatDateForDisplay(page.getLastUpdate()));
		}

		holder.txtLastCheck = (TextView) row.findViewById(R.id.novel_last_check);
		if (holder.txtLastCheck != null) {
			holder.txtLastCheck.setText(context.getResources().getString(R.string.last_check) + ": " + Util.formatDateForDisplay(page.getLastCheck()));
		}

		holder.chkIsWatched = (CheckBox) row.findViewById(R.id.novel_is_watched);
		if (holder.chkIsWatched != null) {
			// Log.d(TAG, page.getId() + " " + page.getTitle() + " isWatched: " + page.isWatched());
			holder.chkIsWatched.setChecked(page.isWatched());
			holder.chkIsWatched.setOnCheckedChangeListener(new OnCheckedChangeListener() {

				public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
					if (isChecked) {
						Toast.makeText(context, "添加到星标列表: " + page.getTitle(), Toast.LENGTH_SHORT).show();
					}
					else {
						Toast.makeText(context, "从星标列表中移除: " + page.getTitle(), Toast.LENGTH_SHORT).show();
					}
					// update the db!
					page.setWatched(isChecked);
					NovelsDao dao = NovelsDao.getInstance(context); // use the cached instance
					dao.updatePageModel(page);
				}
			});
		}

		row.setTag(holder);
		return row;
	}

	// somehow if enabled, will trigger the db 2x (first load and after load)
	@Override
	public void notifyDataSetChanged() {
		synchronized (this) {
			// refresh the data
			Log.d(TAG, "Refreshing data: " + data.size() + " items");
			for (int i = 0; i < data.size(); ++i) {
				try {
					PageModel temp = NovelsDao.getInstance(context).getPageModel(data.get(i), null);
					temp.setUpdateCount(data.get(i).getUpdateCount());
					data.set(i, temp);
				} catch (Exception e) {
					Log.e(TAG, "Error when refreshing PageModel: " + data.get(i).getPage(), e);
				}
			}
			super.notifyDataSetChanged();
		}
	}

	static class PageModelHolder
	{
		TextView txtNovel;
		TextView txtLastUpdate;
		TextView txtLastCheck;
		CheckBox chkIsWatched;
	}

	public void setResourceId(int id) {
		this.layoutResourceId = id;
	}
} public class CallbackEventData implements ICallbackEventData {
	protected String message;
	protected String source;

	public CallbackEventData() {};

	public CallbackEventData(String message) {
		this.message = message;
	}

	public CallbackEventData(String message, String source) {
		this.message = message;
		this.source = source;
	}

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}

	public String getSource() {
		return source;
	}
} public interface ICallbackEventData {

	public abstract String getMessage();
	public abstract void setMessage(String message);
} public class NovelsDao {
	private static final String TAG = NovelsDao.class.toString();
	private static DBHelper dbh;
	private static Context context;

	private static NovelsDao instance;
	private static Object lock = new Object();

	public static NovelsDao getInstance(Context applicationContext) {
		synchronized (lock) {
			if (instance == null) {
				instance = new NovelsDao(applicationContext);
				context = applicationContext;
			}
		}
		return instance;
	}

	public static NovelsDao getInstance() {
		synchronized (lock) {
			if (instance == null) {
				try {
					instance = new NovelsDao(LNReaderApplication.getInstance().getApplicationContext());
				} catch (Exception ex) {
					Log.e(TAG, "Failed to get context for NovelsDao", ex);
					// throw new BakaReaderException("NovelsDao is not Initialized!", BakaReaderException.NULL_NOVELDAO,
					// ex);
					throw new NullPointerException("NovelsDao is not Initialized!");
				}
			}
		}
		return instance;
	}

	private NovelsDao(Context context) {
		if (dbh == null) {
			dbh = new DBHelper(context);
		}
	}

	public DBHelper getDBHelper() {
		if (dbh == null) {
			dbh = new DBHelper(context);
		}
		return dbh;
	}

	public void deleteDB() {
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			try {
				dbh.deletePagesDB(db);
			} finally {
				db.close();
			}
		}
	}

	public String copyDB(boolean makeBackup) throws IOException {
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			String filePath;
			try {
				filePath = dbh.copyDB(db, context, makeBackup);
			} finally {
				db.close();
			}
			return filePath;
		}
	}

	public ArrayList<PageModel> getNovels(ICallbackNotifier notifier, boolean alphOrder,String api) throws Exception {
		ArrayList<PageModel> list = null;
		PageModel page = null;
		SQLiteDatabase db = null;

		// check if main page exist
		synchronized (dbh) {
			try {
				db = dbh.getReadableDatabase();
				
				if(api.equals("/api/leixin/cat/1")){
				    page = PageModelHelper.getHistoryPage(db);
				}else if(api.equals("/api/leixin/cat/3")){
                    page = PageModelHelper.getKehuanPage(db);
                }else if(api.equals("/api/leixin/cat/7")){
                    page = PageModelHelper.getXuanhuanPage(db);
                }else if(api.equals("/api/leixin/cat/10")){
                    page = PageModelHelper.getQitaPage(db);
                }else if(api.equals("/api/book/update/11")){
                    page = PageModelHelper.getUpdatePage(db);
                }else if(api.equals("/api/book/update/12")){
                    page = PageModelHelper.getMoodPage(db);
                }else {
                    page = PageModelHelper.getMainPage(db);  
                }
				
			} finally {
				db.close();
			}
		}
		if (page == null) {
			Log.d(TAG, "No Main_Page data!");
			list = getNovelsFromInternet(notifier,api);
		} else {
			// get from db
			synchronized (dbh) {
				try {
					db = dbh.getReadableDatabase();
					list = dbh.getAllNovels(db, alphOrder);// dbh.selectAllByColumn(db,
					// DBHelper.COLUMN_TYPE,
					// PageModel.TYPE_NOVEL);
				} finally {
					db.close();
				}
			}
			Log.d(TAG, "Found: " + list.size());
		}

		return list;
	}

	public ArrayList<PageModel> getNovelsFromInternet(ICallbackNotifier notifier,String api) throws Exception {
		if (!LNReaderApplication.getInstance().isOnline())
			throw new BakaReaderException("没有网络，请稍后...", BakaReaderException.NO_NETWORK_CONNECTIFITY);
		if (notifier != null) {
			notifier.onCallback(new CallbackEventData("正在加载列表请稍后..."));
		}
		// get last updated main page revision from internet

		PageModel mainPage = new PageModel();
		if(api.equals("/api/leixin/cat/1")){
		    mainPage.setPage("Category:history");
		    mainPage.setTitle(LNReaderApplication.getInstance().getResources().getString(R.string.str_xuanhuan));
        }else if(api.equals("/api/leixin/cat/3")){
            mainPage.setPage("Category:kehuan");
            mainPage.setTitle(LNReaderApplication.getInstance().getResources().getString(R.string.str_xuanhuan));
        }else if(api.equals("/api/leixin/cat/7")){
            mainPage.setPage("Category:xuanhuan");
            mainPage.setTitle(LNReaderApplication.getInstance().getResources().getString(R.string.str_xuanhuan));
        }else if(api.equals("/api/leixin/cat/10")){
            mainPage.setPage("Category:qita");
            mainPage.setTitle(LNReaderApplication.getInstance().getResources().getString(R.string.str_xuanhuan));
        }else if(api.equals("/api/book/update/11")){
            mainPage.setPage("Category:update");
            mainPage.setTitle(LNReaderApplication.getInstance().getResources().getString(R.string.light_novels));
        }else if(api.equals("/api/book/mood/12")){
            mainPage.setPage("Category:mood");
            mainPage.setTitle(LNReaderApplication.getInstance().getResources().getString(R.string.originals));
        }
		mainPage.setLanguage(Constants.LANG_ENGLISH);
		mainPage = getPageModel(mainPage, notifier);
		mainPage.setType(PageModel.TYPE_OTHER);
		mainPage.setParent("");

		ArrayList<PageModel> list = null;
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			try {
				// db.beginTransaction();
				mainPage = PageModelHelper.insertOrUpdatePageModel(db, mainPage, false);
				Log.d(TAG, "Updated Main_Page");

				// now get the novel list
				list = new ArrayList<PageModel>();

				String url = UIHelper.getBaseUrl(LNReaderApplication.getInstance().getApplicationContext()) + api;
				Log.d(TAG, "--"+url);
				int retry = 0;
				while (retry < getRetry()) {
					try {
						Response response = Jsoup.connect(url).timeout(getTimeout(retry)).execute();
						//Document doc = response.parse();
						String result =response.body();
						//String result =  AppUtil.getContent(url);
						
						Log.d(TAG, "Found from internet: "+ result + " Novels");
						list = BakaTsukiParser.ParseNovelList(result);
						Log.d(TAG, "Found from internet: " + list.size() + " Novels");

						// saved to db and get saved value
						list = PageModelHelper.insertAllNovel(db, list);

						// db.setTransactionSuccessful();

						if (notifier != null) {
							notifier.onCallback(new CallbackEventData("Found: " + list.size() + " novels."));
						}
						break;
					} catch (EOFException eof) {
						++retry;
						if (notifier != null) {
							notifier.onCallback(new CallbackEventData("Retrying: Main_Page (" + retry + " of " + getRetry() + ")\n" + eof.getMessage()));
						}
						if (retry > getRetry())
							throw eof;
					} catch (IOException eof) {
						++retry;
						String message = "Retrying: Main_Page (" + retry + " of " + getRetry() + ")\n" + eof.getMessage();
						if (notifier != null) {
							notifier.onCallback(new CallbackEventData(message));
						}
						Log.d(TAG, message, eof);
						if (retry > getRetry())
							throw eof;
					}
				}
			} finally {
				// db.endTransaction();
				db.close();
			}
		}
		return list;
	}

	public ArrayList<PageModel> getWatchedNovel() {
		ArrayList<PageModel> watchedNovel = null;
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getReadableDatabase();
			try {
				// watchedNovel = dbh.selectAllByColumn(db,
				// DBHelper.COLUMN_IS_WATCHED + " = ? and ("
				// + DBHelper.COLUMN_PARENT + " = ? or "
				// + DBHelper.COLUMN_PARENT + " = ? )"
				// , new String[] { "1", "Main_Page", "Category:Teasers" }
				// , DBHelper.COLUMN_TITLE );
				watchedNovel = dbh.getAllWatchedNovel(db, true);
			} finally {
				db.close();
			}
		}
		return watchedNovel;
	}

	public ArrayList<PageModel> getTeaser(ICallbackNotifier notifier, boolean alphOrder) throws Exception {
		SQLiteDatabase db = null;
		PageModel page = null;
		ArrayList<PageModel> list = null;
		// check if main page exist
		synchronized (dbh) {
			try {
				db = dbh.getReadableDatabase();
				page = PageModelHelper.getTeaserPage(db);
			} finally {
				db.close();
			}
		}

		if (page == null) {
			return getTeaserFromInternet(notifier);
		} else {
			// get from db
			synchronized (dbh) {
				try {
					db = dbh.getReadableDatabase();
					list = dbh.getAllTeaser(db, alphOrder);
				} finally {
					db.close();
				}
			}
			Log.d(TAG, "Found: " + list.size());
		}

		return list;
	}

	public ArrayList<PageModel> getTeaserFromInternet(ICallbackNotifier notifier) throws Exception {
		if (!LNReaderApplication.getInstance().isOnline())
			throw new BakaReaderException("No Network Connectifity", BakaReaderException.NO_NETWORK_CONNECTIFITY);
		if (notifier != null) {
			notifier.onCallback(new CallbackEventData("Downloading Teaser Novels list..."));
		}

		// parse Category:Teasers information
		PageModel teaserPage = new PageModel();
		teaserPage.setPage("Category:Teasers");
		teaserPage.setLanguage(Constants.LANG_ENGLISH);
		teaserPage.setTitle("Teasers");
		teaserPage = getPageModel(teaserPage, notifier);
		teaserPage.setType(PageModel.TYPE_OTHER);

		// update page model
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			teaserPage = PageModelHelper.insertOrUpdatePageModel(db, teaserPage, true);
			Log.d(TAG, "Updated Category:Teasers");
		}

		// get teaser list
		ArrayList<PageModel> list = null;
		String url = UIHelper.getBaseUrl(LNReaderApplication.getInstance().getApplicationContext()) + "/project/index.php?title=Category:Teasers";
		int retry = 0;
		while (retry < getRetry()) {
			try {
				Response response = Jsoup.connect(url).timeout(getTimeout(retry)).execute();
				String doc = response.body();

				list = BakaTsukiParser.ParseTeaserList(doc);
				Log.d(TAG, "Found from internet: " + list.size() + " Teaser");

				if (notifier != null) {
					notifier.onCallback(new CallbackEventData("Found: " + list.size() + " teaser."));
				}
				break;
			} catch (EOFException eof) {
				++retry;
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData("Retrying: Category:Teasers (" + retry + " of " + getRetry() + ")\n" + eof.getMessage()));
				}
				if (retry > getRetry())
					throw eof;
			} catch (IOException eof) {
				++retry;
				String message = "Retrying: Category:Teasers (" + retry + " of " + getRetry() + ")\n" + eof.getMessage();
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData(message));
				}
				Log.d(TAG, message, eof);
				if (retry > getRetry())
					throw eof;
			}
		}

		// save teaser list
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			for (PageModel pageModel : list) {
				pageModel = PageModelHelper.insertOrUpdatePageModel(db, pageModel, true);
				Log.d(TAG, "Updated teaser: " + pageModel.getPage());
			}
		}

		return list;
	}

	// Originals, copied from teaser
	public ArrayList<PageModel> getOriginal(ICallbackNotifier notifier, boolean alphOrder) throws Exception {
		SQLiteDatabase db = null;
		PageModel page = null;
		ArrayList<PageModel> list = null;
		// check if main page exist
		synchronized (dbh) {
			try {
				db = dbh.getReadableDatabase();
				page = PageModelHelper.getOriginalPage(db);
			} finally {
				db.close();
			}
		}

		if (page == null) {
			return getOriginalFromInternet(notifier);
		} else {
			// get from db
			synchronized (dbh) {
				try {
					db = dbh.getReadableDatabase();
					list = dbh.getAllOriginal(db, alphOrder);
				} finally {
					db.close();
				}
			}
			Log.d(TAG, "Found: " + list.size());
		}

		return list;
	}

	public ArrayList<PageModel> getOriginalFromInternet(ICallbackNotifier notifier) throws Exception {
		if (!LNReaderApplication.getInstance().isOnline())
			throw new Exception("No Network Connectifity");
		if (notifier != null) {
			notifier.onCallback(new CallbackEventData("Downloading Original Novels list..."));
		}

		// parse Category:Teasers information
		PageModel teaserPage = new PageModel();
		teaserPage.setPage("Category:Original");
		teaserPage.setLanguage(Constants.LANG_ENGLISH);
		teaserPage.setTitle("Original Novels");
		teaserPage = getPageModel(teaserPage, notifier);
		teaserPage.setType(PageModel.TYPE_OTHER);

		// update page model
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			teaserPage = PageModelHelper.insertOrUpdatePageModel(db, teaserPage, true);
			Log.d(TAG, "Updated Category:Original");
		}

		// get teaser list
		ArrayList<PageModel> list = null;
		String url = UIHelper.getBaseUrl(LNReaderApplication.getInstance().getApplicationContext()) + "/project/index.php?title=Category:Original";
		int retry = 0;
		while (retry < getRetry()) {
			try {
				Response response = Jsoup.connect(url).timeout(getTimeout(retry)).execute();
				String doc = response.body();

				list = BakaTsukiParser.ParseOriginalList(doc);
				Log.d(TAG, "Found from internet: " + list.size() + " Teaser");

				if (notifier != null) {
					notifier.onCallback(new CallbackEventData("Found: " + list.size() + " original."));
				}
				break;
			} catch (EOFException eof) {
				++retry;
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData("Retrying: Category:Original (" + retry + " of " + getRetry() + ")\n" + eof.getMessage()));
				}
				if (retry > getRetry())
					throw eof;
			} catch (IOException eof) {
				++retry;
				String message = "Retrying: Category:Original (" + retry + " of " + getRetry() + ")\n" + eof.getMessage();
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData(message));
				}
				Log.d(TAG, message, eof);
				if (retry > getRetry())
					throw eof;
			}
		}

		// save original list
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			for (PageModel pageModel : list) {
				pageModel = PageModelHelper.insertOrUpdatePageModel(db, pageModel, true);
				Log.d(TAG, "Updated original: " + pageModel.getPage());
			}
		}

		return list;
	}

	// Alternative Language, copied from Original
	public ArrayList<PageModel> getAlternative(ICallbackNotifier notifier, boolean alphOrder, String language) throws Exception {
		SQLiteDatabase db = null;
		PageModel page = null;
		ArrayList<PageModel> list = null;
		// check if main page exist
		synchronized (dbh) {
			try {
				db = dbh.getReadableDatabase();
				page = PageModelHelper.getAlternativePage(db, language);
			} finally {
				db.close();
			}
		}

		if (page == null) {
			return getAlternativeFromInternet(notifier, language);
		} else {
			// get from db
			synchronized (dbh) {
				try {
					db = dbh.getReadableDatabase();
					list = dbh.getAllAlternative(db, alphOrder, language);
				} finally {
					db.close();
				}
			}
			Log.d(TAG, "Found: " + list.size());
		}

		return list;
	}

	public ArrayList<PageModel> getAlternativeFromInternet(ICallbackNotifier notifier, String language) throws Exception {
		if (!LNReaderApplication.getInstance().isOnline())
			throw new BakaReaderException("No Network Connectifity", BakaReaderException.NO_NETWORK_CONNECTIFITY);
		if (notifier != null) {
			notifier.onCallback(new CallbackEventData("Downloading " + language + " Novels list..."));
		}

		// parse information
		PageModel teaserPage = new PageModel();

		if (language != null) {
			teaserPage.setPage(AlternativeLanguageInfo.getAlternativeLanguageInfo().get(language).getCategoryInfo());
			teaserPage.setTitle(language + " Novels");
			teaserPage.setLanguage(language);
		}

		teaserPage = getPageModel(teaserPage, notifier);
		teaserPage.setType(PageModel.TYPE_NOVEL);

		// update page model
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			teaserPage = PageModelHelper.insertOrUpdatePageModel(db, teaserPage, true);
			Log.d(TAG, "Updated " + language);
		}

		// get alternative list
		ArrayList<PageModel> list = null;
		String url = null;
		if (language != null)
			url = UIHelper.getBaseUrl(LNReaderApplication.getInstance().getApplicationContext()) + "/project/index.php?title=" + AlternativeLanguageInfo.getAlternativeLanguageInfo().get(language).getCategoryInfo();
		int retry = 0;
		while (retry < getRetry()) {
			try {
				Response response = Jsoup.connect(url).timeout(getTimeout(retry)).execute();
				Document doc = response.parse();
				list = BakaTsukiParserAlternative.ParseAlternativeList(doc, language);
				Log.d(TAG, "Found from internet: " + list.size() + " " + language + " Novel");

				if (notifier != null) {
					notifier.onCallback(new CallbackEventData("Found: " + list.size() + " " + language + " ."));
				}
				break;
			} catch (EOFException eof) {
				++retry;
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData("Retrying: " + language + " (" + retry + " of " + getRetry() + ")\n" + eof.getMessage()));
				}
				if (retry > getRetry())
					throw eof;
			} catch (IOException eof) {
				++retry;
				String message = "Retrying: " + language + " (" + retry + " of " + getRetry() + ")\n" + eof.getMessage();
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData(message));
				}
				Log.d(TAG, message, eof);
				if (retry > getRetry())
					throw eof;
			}
		}

		// save teaser list
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			for (PageModel pageModel : list) {
				pageModel = PageModelHelper.insertOrUpdatePageModel(db, pageModel, true);
				Log.d(TAG, "Updated " + language + " novel: " + pageModel.getPage());
			}
		}

		return list;
	}

	/**
	 * Get page model from db. If autoDownload = true, get the pageModel from
	 * internet if not exists.
	 * 
	 * @param page
	 * @param notifier
	 * @param autoDownload
	 * @return
	 * @throws Exception
	 */
	public PageModel getPageModel(PageModel page, ICallbackNotifier notifier, boolean autoDownload) throws Exception {
		PageModel pageModel = null;
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getReadableDatabase();
			try {
				pageModel = PageModelHelper.getPageModel(db, page.getPage());
			} finally {
				db.close();
			}
		}
		if (pageModel == null && autoDownload) {
			pageModel = getPageModelFromInternet(page, notifier);
		}
		return pageModel;
	}

	/**
	 * Get page model from db. Get the pageModel from internet if not exists.
	 * 
	 * @param page
	 * @param notifier
	 * @return
	 * @throws Exception
	 */
	public PageModel getPageModel(PageModel page, ICallbackNotifier notifier) throws Exception {
		return getPageModel(page, notifier, true);
	}

	/**
	 * Return pageModel, null if not exist.
	 * 
	 * @param page
	 * @param notifier
	 * @return
	 * @throws Exception
	 */
	public PageModel getExistingPageModel(PageModel page, ICallbackNotifier notifier) throws Exception {
		PageModel pageModel = null;
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getReadableDatabase();
			try {
				pageModel = PageModelHelper.getPageModel(db, page.getPage());
			} finally {
				db.close();
			}
		}

		return pageModel;
	}

	public PageModel getPageModelFromInternet(PageModel page, ICallbackNotifier notifier) throws Exception {
		if (!LNReaderApplication.getInstance().isOnline())
			throw new BakaReaderException("没有网络连接", BakaReaderException.NO_NETWORK_CONNECTIFITY);
		Log.d(TAG, "PageModel = " + page.getPage());

		int retry = 0;
		while (retry < getRetry()) {
			try {
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData("正在加载 "  + " 请稍后"));
				}
				String encodedTitle = Util.UrlEncode(page.getPage());
				String fullUrl = "http://www.baka-tsuki.org/project/api.php?action=query&prop=info&format=xml&redirects=yes&titles=" + encodedTitle;
				Response response = Jsoup.connect(fullUrl).timeout(getTimeout(retry)).execute();
				PageModel pageModel = null;
				String lang = page.getLanguage();
				if (lang != null)
					pageModel = CommonParser.parsePageAPI(page, response.parse(), fullUrl);
				pageModel.setFinishedRead(page.isFinishedRead());
				pageModel.setWatched(page.isWatched());

				synchronized (dbh) {
					// save to db and get saved value
					SQLiteDatabase db = dbh.getWritableDatabase();
					try {
						pageModel = PageModelHelper.insertOrUpdatePageModel(db, pageModel, false);
					} finally {
						db.close();
					}
				}
				return pageModel;
			} catch (EOFException eof) {
				++retry;
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData("重新尝试: " + page.getPage() + " (" + retry + " of " + getRetry() + ")\n" + eof.getMessage()));
				}
				if (retry > getRetry())
					throw eof;
			} catch (IOException eof) {
				++retry;
				String message = "重新尝试: " + page.getPage() + " (" + retry + " of " + getRetry() + ")\n" + eof.getMessage();
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData(message));
				}
				Log.d(TAG, message, eof);
				if (retry > getRetry())
					throw eof;
			}
		}
		return null;
	}

	public PageModel updatePageModel(PageModel page) {
		PageModel pageModel = null;
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			try {
				pageModel = PageModelHelper.insertOrUpdatePageModel(db, page, false);
			} finally {
				db.close();
			}
		}
		return pageModel;
	}

	/*
	 * NovelCollectionModel
	 */

	public NovelCollectionModel getNovelDetails(PageModel page, ICallbackNotifier notifier) throws Exception {
		NovelCollectionModel novel = null;
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getReadableDatabase();
			try {
				novel = NovelCollectionModelHelper.getNovelDetails(db, page.getPage());
			} finally {
				db.close();
			}
		}
		if (novel == null) {
			novel = getNovelDetailsFromInternet(page, notifier);
		}
		return novel;
	}

	public NovelCollectionModel getNovelDetailsFromInternet(PageModel page, ICallbackNotifier notifier) throws Exception {
		if (!LNReaderApplication.getInstance().isOnline())
			throw new BakaReaderException("没有网络连接", BakaReaderException.NO_NETWORK_CONNECTIFITY);
		Log.d(TAG, "Getting Novel Details from internet: " + page.getPage());
		NovelCollectionModel novel = null;

		int retry = 0;
		while (retry < getRetry()) {
			try {
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData("正在加载小说详细信息: " + page.getPage()));
				}
				//String encodedTitle = Util.UrlEncode();
				String fullUrl = UIHelper.getBaseUrl(LNReaderApplication.getInstance().getApplicationContext()) + "/api/article/" + page.getId()+"/1";
				Response response = Jsoup.connect(fullUrl).timeout(getTimeout(retry)).execute();
				String doc = response.body();
				/*
				 * Add your section of alternative language here, create own
				 * parser for each language for modularity reason
				 */
				if (!page.getLanguage().equals(Constants.LANG_ENGLISH))
					novel = BakaTsukiParserAlternative.ParseNovelDetails(doc, page);
				else
					novel = BakaTsukiParser.ParseNovelDetails(doc, page);
			} catch (EOFException eof) {
				++retry;
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData("重新尝试: " + page.getPage() + " (" + retry + " of " + getRetry() + ")\n" + eof.getMessage()));
				}
				if (retry > getRetry())
					throw eof;
			} catch (IOException eof) {
				++retry;
				String message = "重新尝试: " + page.getPage() + " (" + retry + " of " + getRetry() + ")\n" + eof.getMessage();
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData(message));
				}
				Log.d(TAG, message, eof);
				if (retry > getRetry())
					throw eof;
			}

			// Novel details' Page Model
			if (novel != null) {
				page = updateNovelDetailsPageModel(page, notifier, novel);

				synchronized (dbh) {
					// insert to DB and get saved value
					SQLiteDatabase db = dbh.getWritableDatabase();
					try {
						db.beginTransaction();
						novel = NovelCollectionModelHelper.insertNovelDetails(db, novel);
						db.setTransactionSuccessful();
						//db.endTransaction();
					} finally {
						db.endTransaction();
						db.close();
					}
				}

				// update info for each chapters
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData("获取章节信息: " + page.getPage()));
				}
				Log.d(TAG, "Cover size: " + novel.getFlattedChapterList().size());
				ArrayList<PageModel> chapters = getUpdateInfo(novel.getFlattedChapterList(), notifier);
				
				for (PageModel pageModel : chapters) {
					pageModel = updatePageModel(pageModel);
				}

				// download cover image
				Log.d(TAG, "Cover imgurl: " + page.getImgurl());
				novel.setCoverUrl(new URL(Constants.BASE_IMAGE_URL+ page.getImgurl()));
				if (novel.getCoverUrl() != null) {
					if (notifier != null) {
						notifier.onCallback(new CallbackEventData("获取封面图片."));
					}
					DownloadFileTask task = new DownloadFileTask(notifier);
					ImageModel image = task.downloadImage(novel.getCoverUrl());
					// TODO: need to save to db?
					Log.d(TAG, "Cover Image: " + image.toString());
					novel.setCover(image.getDedepath());
					
					synchronized (dbh) {
	                    // insert to DB and get saved value
	                    SQLiteDatabase db = dbh.getWritableDatabase();
	                    try {
	                        db.beginTransaction();
	                        NovelCollectionModelHelper.insertNovelDetailsImg(db, novel);
	                        db.setTransactionSuccessful();
	                        //db.endTransaction();
	                    } finally {
	                        db.endTransaction();
	                        db.close();
	                    }
	                }
					
					
				}

				Log.d(TAG, "Complete getting Novel Details from internet: " + page.getPage());
				break;
			}
		}
		return novel;
	}

	public PageModel updateNovelDetailsPageModel(PageModel page, ICallbackNotifier notifier, NovelCollectionModel novel) throws Exception {
		// comment out because have teaser now...
		// page.setParent("Main_Page"); // insurance

		// get the last update time from internet
		if (notifier != null) {
			notifier.onCallback(new CallbackEventData("获取小说信息: " + page.getPage()));
		}
		/*PageModel novelPageTemp = getPageModelFromInternet(page, notifier);
		if (novelPageTemp != null) {
			page.setLastUpdate(novelPageTemp.getLastUpdate());
			page.setLastCheck(new Date());
			novel.setLastUpdate(novelPageTemp.getLastUpdate());
			novel.setLastCheck(new Date());
		} else {
			page.setLastUpdate(new Date(0));
			page.setLastCheck(new Date());
			novel.setLastUpdate(new Date(0));
			novel.setLastCheck(new Date());
		}*/
		// save the changes
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			try {
				page = PageModelHelper.insertOrUpdatePageModel(db, page, true);
			} finally {
				db.close();
			}
		}
		return page;
	}

	public PageModel getUpdateInfo(PageModel pageModel, ICallbackNotifier notifier) throws Exception {
		ArrayList<PageModel> pageModels = new ArrayList<PageModel>();
		pageModels.add(pageModel);
		pageModels = getUpdateInfo(pageModels, notifier);
		return pageModels.get(0);
	}

	/***
	 * Bulk update page info through wiki API - LastUpdateInfo. - Redirected. -
	 * Missing flag.
	 * 
	 * @param pageModels
	 * @param notifier
	 * @return
	 * @throws Exception
	 */
	public ArrayList<PageModel> getUpdateInfo(ArrayList<PageModel> pageModels, ICallbackNotifier notifier) throws Exception {
		ArrayList<PageModel> resultPageModel = new ArrayList<PageModel>();
		ArrayList<PageModel> noInfoPageModel = new ArrayList<PageModel>();
		ArrayList<PageModel> externalPageModel = new ArrayList<PageModel>();

		String baseUrl = UIHelper.getBaseUrl(LNReaderApplication.getInstance().getApplicationContext()) + "/project/api.php?action=query&prop=info&format=xml&redirects=yes&titles=";
		int i = 0;
		int retry = 0;
		while (i < pageModels.size()) {
			int apiPageCount = 1;
			ArrayList<PageModel> checkedPageModel = new ArrayList<PageModel>();
			String titles = "";

			while (i < pageModels.size() && apiPageCount < 50) {
				if (pageModels.get(i).isExternal()) {
					pageModels.get(i).setMissing(false);
					externalPageModel.add(pageModels.get(i));
					++i;
					continue;
				}
				if (pageModels.get(i).isMissing() || pageModels.get(i).getPage().endsWith("&action=edit&redlink=1")) {
					pageModels.get(i).setMissing(true);
					noInfoPageModel.add(pageModels.get(i));
					++i;
					continue;
				}
				if (titles.length() + pageModels.get(i).getPage().length() < 2000) {
					titles += "|" + Util.UrlEncode(pageModels.get(i).getPage());
					checkedPageModel.add(pageModels.get(i));
					++i;
					++apiPageCount;
				} else {
					break;
				}
			}

			// request the page
			while (retry < getRetry()) {
				try {
					// Log.d(TAG, "Trying to get: " + baseUrl + titles);
					String url = baseUrl + titles;
					Response response = Jsoup.connect(url).timeout(getTimeout(retry)).execute();
					Document doc = response.parse();
					ArrayList<PageModel> updatedPageModels = CommonParser.parsePageAPI(checkedPageModel, doc, url);
					resultPageModel.addAll(updatedPageModels);
					break;
				} catch (EOFException eof) {
					++retry;
					if (notifier != null) {
						notifier.onCallback(new CallbackEventData("Retrying: Get Pages Info (" + retry + " of " + getRetry() + ")\n" + eof.getMessage()));
					}
					if (retry > getRetry())
						throw eof;
				} catch (IOException eof) {
					++retry;
					String message = "Retrying: Get Pages Info (" + retry + " of " + getRetry() + ")\n" + eof.getMessage();
					if (notifier != null) {
						notifier.onCallback(new CallbackEventData(message));
					}
					Log.d(TAG, message, eof);
					if (retry > getRetry())
						throw eof;
				}
			}
		}

		for (PageModel page : externalPageModel) {
			getExternalUpdateInfo(page);
		}

		resultPageModel.addAll(noInfoPageModel);
		resultPageModel.addAll(externalPageModel);

		return resultPageModel;
	}

	public void getExternalUpdateInfo(PageModel page) {
		int retry;
		Map<String, String> headers = null;
		// Date: Wed, 13 Nov 2013 13:08:35 GMT
		DateFormat df = new SimpleDateFormat("EEE, dd MMM yyyy kk:mm:ss z", Locale.US);
		retry = 0;
		while (retry < getRetry()) {
			try {
				headers = Jsoup.connect(page.getPage()).timeout(getTimeout(retry)).maxBodySize(1).execute().headers();
				break;
			} catch (Exception e) {
				Log.e(TAG, "Error when getting updated date for: " + page.getPage(), e);
				++retry;
			}
		}
		if (headers != null) {
			String dateStr = null;
			if (headers.containsKey("Last-Modified")) {
				dateStr = headers.get("Last-Modified");
			}
			else if (headers.containsKey("Date")) {
				dateStr = headers.get("Date");
			}
			if (!Util.isStringNullOrEmpty(dateStr)) {
				try {
					Log.d(TAG, "External Novel last update: " + dateStr);
					page.setLastUpdate(df.parse(dateStr));
				} catch (Exception e) {
					Log.e(TAG, "Failed to parse date for: " + page.getPage(), e);
				}
			}
		}
		page.setLastCheck(new Date());
	}

	public void deleteBooks(BookModel bookDel) {
		synchronized (dbh) {
			// get from db
			SQLiteDatabase db = dbh.getReadableDatabase();
			try {
				BookModel tempBook = BookModelHelper.getBookModel(db, bookDel.getId());
				if (tempBook != null) {
					ArrayList<PageModel> chapters = tempBook.getChapterCollection();
					for (PageModel chapter : chapters) {
						NovelContentModelHelper.deleteNovelContent(db, chapter);
					}
					BookModelHelper.deleteBookModel(db, tempBook);
				}
			} finally {
				db.close();
			}
		}
	}

	public void deletePage(PageModel page) {
		synchronized (dbh) {
			// get from db
			SQLiteDatabase db = dbh.getReadableDatabase();
			try {
				PageModel tempPage = PageModelHelper.getPageModel(db, page.getId());
				if (tempPage != null) {
					PageModelHelper.deletePageModel(db, tempPage);
				}
			} finally {
				db.close();
			}
		}
	}

	public ArrayList<PageModel> getChapterCollection(String page, String title, BookModel book) {
		synchronized (dbh) {
			// get from db
			SQLiteDatabase db = dbh.getReadableDatabase();
			try {
				return NovelCollectionModelHelper.getChapterCollection(db, page + Constants.NOVEL_BOOK_DIVIDER + title, book);
			} finally {
				db.close();
			}
		}
	}

	public ArrayList<PageModel> getAllContentPageModel() {
		ArrayList<PageModel> result = null;
		synchronized (dbh) {
			// get from db
			SQLiteDatabase db = dbh.getReadableDatabase();
			try {
				result = PageModelHelper.getAllContentPageModel(db);
			} finally {
				db.close();
			}
		}
		return result;
	}

	/*
	 * NovelContentModel
	 */

	public NovelContentModel getNovelContent(BookModel page, boolean download, ICallbackNotifier notifier) throws Exception {
		NovelContentModel content = null;

		synchronized (dbh) {
			// get from db
			SQLiteDatabase db = dbh.getReadableDatabase();
			try {
				content = NovelContentModelHelper.getNovelContent(db, page.getPage());
			} finally {
				db.close();
			}
		}
		// get from Internet;
		if (download && content == null) {
			Log.d("getNovelContent", "Get from Internet: " + page.getPage());
			content = getNovelContentFromInternet(page, notifier);
		}
		Log.d("getNovelContent", "Text: " + content.getContent());
		return content;
	}

	public NovelContentModel getNovelContentFromInternet(BookModel page, ICallbackNotifier notifier) throws Exception {
		if (!LNReaderApplication.getInstance().isOnline())
			throw new BakaReaderException("没有网络连接", BakaReaderException.NO_NETWORK_CONNECTIFITY);

		String oldTitle = page.getTitle();

		NovelContentModel content = new NovelContentModel();
		int retry = 0;
		String doc = null;
		while (retry < getRetry()) {
			try {
				String encodedUrl = Constants.BASE_URL + "/api/article/"+page.getBookId()+"/ch/"+page.getChapter();
				Response response = Jsoup.connect(encodedUrl).timeout(getTimeout(retry)).execute();
				doc = response.body();
				
				content = BakaTsukiParser.ParseNovelContent(doc, page);
				content.setUpdatingFromInternet(true);
				break;
			} catch (EOFException eof) {
				++retry;
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData("Retrying: " + page.getPage() + " (" + retry + " of " + getRetry() + ")\n" + eof.getMessage()));
				}
				if (retry > getRetry())
					throw eof;
			} catch (IOException eof) {
				++retry;
				String message = "Retrying: " + page.getPage() + " (" + retry + " of " + getRetry() + ")\n" + eof.getMessage();
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData(message));
				}
				Log.d(TAG, message, eof);
				if (retry > getRetry())
					throw eof;
			}
		}
		/*if (doc != null) {
			// download all attached images
			DownloadFileTask task = new DownloadFileTask(notifier);
			for (ImageModel image : content.getImages()) {
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData("Start downloading: " + image.getUrl()));
				}
				image = task.downloadImage(image.getUrl());
				// TODO: need to save image to db? mostly thumbnail only
			}

			// download linked big images
			boolean isDownloadBigImage = PreferenceManager.getDefaultSharedPreferences(LNReaderApplication.getInstance()).getBoolean(Constants.PREF_DOWLOAD_BIG_IMAGE, false);
			if (isDownloadBigImage) {
				Document imageDoc = Jsoup.parse(content.getContent());
				ArrayList<String> images = CommonParser.parseImagesFromContentPage(imageDoc);
				for (String imageUrl : images) {
					// ImageModel bigImage = getImageModelFromInternet(image, notifier);
					ImageModel bigImage = new ImageModel();
					bigImage.setBigImage(true);
					bigImage.setName(imageUrl);
					bigImage.setReferer(imageUrl);
					bigImage = getImageModel(bigImage, notifier);
				}
			}

			// get last updated info

			PageModel contentPageModelTemp = getPageModelFromInternet(content.getPageModel(), notifier);
			if (contentPageModelTemp != null) {
				// overwrite the old title
				content.getPageModel().setTitle(oldTitle);
				// syncronize the date
				content.getPageModel().setLastUpdate(contentPageModelTemp.getLastUpdate());
				content.getPageModel().setLastCheck(new Date());
				content.setLastUpdate(contentPageModelTemp.getLastUpdate());
				content.setLastCheck(new Date());
			}

			// page model will be also saved in insertNovelContent()
			synchronized (dbh) {
				// save to DB, and get the saved value
				SQLiteDatabase db = dbh.getWritableDatabase();
				try {
					// TODO: somehow using transaction cause problem...
					db.beginTransaction();
					content = NovelContentModelHelper.insertNovelContent(db, content);
					db.setTransactionSuccessful();
				} finally {
					db.endTransaction();
					db.close();
				}
			}
		}*/
		return content;
	}

	public NovelContentModel updateNovelContent(NovelContentModel content) throws Exception {
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			try {
				content = NovelContentModelHelper.insertNovelContent(db, content);
			} finally {
				db.close();
			}
		}
		return content;
	}

	public boolean deleteNovelContent(PageModel ref) {
		boolean result = false;
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			try {
				result = NovelContentModelHelper.deleteNovelContent(db, ref);
			} finally {
				db.close();
			}
		}
		return result;
	}

	/*
	 * ImageModel
	 */

	/**
	 * Get image from db, if not exist will try to download from internet
	 * 
	 * @param image
	 * @param notifier
	 * @return
	 * @throws Exception
	 */
	public ImageModel getImageModel(ImageModel image, ICallbackNotifier notifier) throws Exception {
		if (image == null || image.getName() == null)
			throw new BakaReaderException("Empty Image!", BakaReaderException.EMPTY_IMAGE);
		ImageModel imageTemp = null;
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getReadableDatabase();
			try {
				imageTemp = ImageModelHelper.getImage(db, image);

				if (imageTemp == null) {
					if (image.getReferer() == null)
						image.setReferer(image.getName());
					Log.d(TAG, "Image not found, might need to check by referer: " + image.getName() + ", referer: " + image.getReferer());
					imageTemp = ImageModelHelper.getImageByReferer(db, image);
				}
			} finally {
				db.close();
			}
		}
		boolean downloadBigImage = false;
		if (imageTemp == null) {
			Log.i(TAG, "Image not found in DB, getting data from internet: " + image.getName());
			downloadBigImage = true;
		} else if (!new File(imageTemp.getPath()).exists()) {
			try{
				Log.i(TAG, "Image found in DB, but doesn't exist in path: " + imageTemp.getPath()
						+"\nAttempting URLDecoding method with default charset:"+java.nio.charset.Charset.defaultCharset().displayName());
				if(!new File(java.net.URLDecoder.decode(imageTemp.getPath(), java.nio.charset.Charset.defaultCharset().displayName())).exists()){
					Log.i(TAG, "Image found in DB, but doesn't exist in URL decoded path: " + java.net.URLDecoder.decode(imageTemp.getPath(), java.nio.charset.Charset.defaultCharset().displayName()));
					downloadBigImage = true;					
				} //else Log.i(TAG, "Image found in DB with URL decoded path: " + java.net.URLDecoder.decode(imageTemp.getPath(), java.nio.charset.Charset.defaultCharset().displayName()));
				
			}catch(Exception e){
				Log.i(TAG, "Image found in DB, but path string seems to be broken: " + imageTemp.getPath()
						+ " Charset:" + java.nio.charset.Charset.defaultCharset().displayName());
				downloadBigImage = true;
			}
		}
		if (downloadBigImage) {
			Log.d(TAG, "Downloading big image from internet: " + image.getName());
			imageTemp = getImageModelFromInternet(image, notifier);
		}

		return imageTemp;
	}

	/**
	 * Get image from internet from File:xxx
	 * 
	 * @param page
	 * @param notifier
	 * @return
	 * @throws Exception
	 */
	public ImageModel getImageModelFromInternet(ImageModel image, ICallbackNotifier notifier) throws Exception {
		if (!LNReaderApplication.getInstance().isOnline())
			throw new BakaReaderException("No Network Connectifity", BakaReaderException.NO_NETWORK_CONNECTIFITY);
		String url = image.getName();
		if (!url.startsWith("http"))
			url = UIHelper.getBaseUrl(LNReaderApplication.getInstance().getApplicationContext()) + url;

		if (notifier != null) {
			notifier.onCallback(new CallbackEventData("Parsing File Page: " + url));
		}

		int retry = 0;
		while (retry < getRetry()) {
			try {
				Response response = Jsoup.connect(url).timeout(getTimeout(retry)).execute();
				Document doc = response.parse();

				// only return the full image url
				image = CommonParser.parseImagePage(doc);

				DownloadFileTask downloader = new DownloadFileTask(notifier);
				image = downloader.downloadImage(image.getUrl());
				image.setReferer(url);

				image = insertImage(image);
				break;
			} catch (EOFException eof) {
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData("Retrying: " + url + " (" + retry + " of " + getRetry() + ")\n" + eof.getMessage()));
				}
				++retry;
				if (retry > getRetry())
					throw eof;
			} catch (IOException eof) {
				++retry;
				String message = "Retrying: " + url + " (" + retry + " of " + getRetry() + ")\n" + eof.getMessage();
				if (notifier != null) {
					notifier.onCallback(new CallbackEventData(message));
				}
				Log.d(TAG, message, eof);
				if (retry > getRetry())
					throw eof;
			}
		}
		return image;
	}

	public ImageModel insertImage(ImageModel image) {
		synchronized (dbh) {
			// save to db and get the saved value
			SQLiteDatabase db = dbh.getWritableDatabase();
			try {
				image = ImageModelHelper.insertImage(db, image);
			} finally {
				db.close();
			}
		}
		return image;
	}

	public ArrayList<ImageModel> getAllImages() {
		ArrayList<ImageModel> result;
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getReadableDatabase();
			result = ImageModelHelper.getAllImages(db);
		}
		return result;
	}

	public ArrayList<PageModel> doSearch(String searchStr, boolean isNovelOnly, ArrayList<String> languageList) {
		if (searchStr == null || searchStr.length() < 3)
			return null;

		ArrayList<PageModel> result;
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getReadableDatabase();
			result = dbh.doSearch(db, searchStr, isNovelOnly, languageList);
		}
		return result;
	}

	public boolean deleteNovel(PageModel novel) {
		boolean result = false;
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			result = NovelCollectionModelHelper.deleteNovel(db, novel);
		}
		return result;
	}

	public ArrayList<BookmarkModel> getBookmarks(PageModel novel) {
		ArrayList<BookmarkModel> bookmarks = new ArrayList<BookmarkModel>();
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getReadableDatabase();
			bookmarks = BookmarkModelHelper.getBookmarks(db, novel);
		}
		return bookmarks;
	}

	public int addBookmark(BookmarkModel bookmark) {
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			return BookmarkModelHelper.insertBookmark(db, bookmark);
		}
	}

	public int deleteBookmark(BookmarkModel bookmark) {
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			return BookmarkModelHelper.deleteBookmark(db, bookmark);
		}
	}

	public ArrayList<BookmarkModel> getAllBookmarks(boolean isOrderByDate) {
		ArrayList<BookmarkModel> bookmarks = new ArrayList<BookmarkModel>();
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getReadableDatabase();
			bookmarks = BookmarkModelHelper.getAllBookmarks(db, isOrderByDate);
		}
		return bookmarks;
	}

	public boolean isContentUpdated(PageModel page) {
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getReadableDatabase();
			return dbh.isContentUpdated(db, page);
		}
	}

	public int isNovelUpdated(PageModel page) {
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getReadableDatabase();
			return dbh.isNovelUpdated(db, page);
		}
	}

	public ArrayList<UpdateInfoModel> getAllUpdateHistory() {
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getReadableDatabase();
			return UpdateInfoModelHelper.getAllUpdateHistory(db);
		}
	}

	public void deleteAllUpdateHistory() {
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			UpdateInfoModelHelper.deleteAllUpdateHistory(db);
		}
	}

	public void deleteUpdateHistory(UpdateInfoModel updateInfo) {
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			UpdateInfoModelHelper.deleteUpdateHistory(db, updateInfo);
		}
	}

	public void insertUpdateHistory(UpdateInfoModel update) {
		synchronized (dbh) {
			SQLiteDatabase db = dbh.getWritableDatabase();
			UpdateInfoModelHelper.insertUpdateHistory(db, update);
		}
	}

	public void deleteChapterCache(PageModel chapter) {
		deleteNovelContent(chapter);

		// Set isDownloaded to false
		chapter.setDownloaded(false);
		updatePageModel(chapter);
	}

	public void deleteBookCache(BookModel bookDel) {
		for (PageModel p : bookDel.getChapterCollection()) {
			deleteChapterCache(p);
		}
	}

	private int getRetry() {
		return UIHelper.getIntFromPreferences(Constants.PREF_RETRY, 3);
	}

	private int getTimeout(int retry) {
		boolean increaseRetry = PreferenceManager.getDefaultSharedPreferences(LNReaderApplication.getInstance().getApplicationContext()).getBoolean(Constants.PREF_INCREASE_RETRY, false);
		int timeout = UIHelper.getIntFromPreferences(Constants.PREF_TIMEOUT, 60) * 1000;
		if (increaseRetry) {
			timeout = timeout * (retry + 1);
		}
		return timeout;
	}
	// public void temp() {
	// synchronized (dbh) {
	// SQLiteDatabase db = dbh.getWritableDatabase();
	// db.execSQL("DROP TABLE IF EXISTS " + dbh.TABLE_NOVEL_BOOKMARK);
	// db.execSQL(dbh.DATABASE_CREATE_NOVEL_BOOKMARK);
	// }
	// }
} public class AsyncTaskResult<T> {
	private T result;
	private Exception error;


	public T getResult() {
	    return result;
	}
	public Exception getError() {
	    return error;
	}


	public AsyncTaskResult(T result) {
	    super();
	    this.result = result;
	}


	public AsyncTaskResult(Exception error) {
	    super();
	    this.error = error;
	}

} public class NovelCollectionModel {
	private static final String TAG = NovelCollectionModel.class.toString();
	private int id = -1;
	private PageModel pageModel;
	private String page;
	private String cover;
	private URL coverUrl;
	private Bitmap coverBitmap;
	private String synopsis;
	private ArrayList<BookModel> bookCollections;

	private String redirectTo;

	private Date lastUpdate;
	private Date lastCheck;

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public PageModel getPageModel() throws Exception {
		if (this.pageModel == null) {
			NovelsDao dao = NovelsDao.getInstance();
			PageModel tempPage = new PageModel();
			tempPage.setPage(this.page);
			this.pageModel = dao.getPageModel(tempPage, null);
		}
		return pageModel;
	}

	public void setPageModel(PageModel pageModel) {
		this.pageModel = pageModel;
	}

	public String getCover() {
		return cover;
	}

	public void setCover(String cover) {
		this.cover = cover;
	}

	public String getSynopsis() {
		return synopsis;
	}

	public void setSynopsis(String synopsis) {
		this.synopsis = synopsis;
	}

	public ArrayList<BookModel> getBookCollections() {
		return bookCollections;
	}

	public void setBookCollections(ArrayList<BookModel> bookCollections) {
		this.bookCollections = bookCollections;
	}

	public URL getCoverUrl() {
		if (this.coverUrl == null && this.cover != null && this.cover.length() > 0) {
			try {
				this.coverUrl = new URL(this.cover);
			} catch (MalformedURLException e) {
				Log.e(TAG, "Invalid url: " + this.cover, e);
			}
		}
		return coverUrl;
	}

	public void setCoverUrl(URL coverUri) {
		this.coverUrl = coverUri;
	}

	public Bitmap getCoverBitmap() {
		if (coverBitmap == null) {
			try {
				// TODO: maybe it is better to use ImageModel\
			    if(this.cover!=null){
			        this.coverBitmap = BitmapFactory.decodeFile(this.cover); 
			    }else if (getCoverUrl() != null) {
					@SuppressWarnings("deprecation")
					String filepath = UIHelper.getImageRoot(LNReaderApplication.getInstance().getApplicationContext()) + Util.sanitizeFilename(URLDecoder.decode(getCoverUrl().getFile()));
					Log.d("GetCover", filepath);
					this.coverBitmap = BitmapFactory.decodeFile(filepath);
				}
			} catch (Exception e) {
				Log.e("GetCover", e.getClass().toString() + ": " + e.getMessage(), e);
			}
		}
		// Redimension image so they all have a constant size
		// coverBitmap = Bitmap.createScaledBitmap(coverBitmap, 200, 300, true);
		return coverBitmap;
	}

	public Date getLastUpdate() {
		return lastUpdate;
	}

	public void setLastUpdate(Date lastUpdate) {
		this.lastUpdate = lastUpdate;
	}

	public Date getLastCheck() {
		return lastCheck;
	}

	public void setLastCheck(Date lastCheck) {
		this.lastCheck = lastCheck;
	}

	public void setPage(String page) {
		this.page = page;
	}

	public String getPage() {
		return this.page;
	}

	@Override
	public String toString() {
		return page;
	}

	private ArrayList<PageModel> _FlattedChapterList;

	public ArrayList<PageModel> getFlattedChapterList() {
		if (_FlattedChapterList == null) {
			_FlattedChapterList = new ArrayList<PageModel>();
			for (BookModel b : bookCollections) {
				for (PageModel temp : b.getChapterCollection()) {
					_FlattedChapterList.add(temp);
				}
			}
		}
		return _FlattedChapterList;
	}

	public PageModel getNext(String page) {
		int index = -1;
		for (Iterator<PageModel> i = getFlattedChapterList().iterator(); i.hasNext();) {
			PageModel temp = i.next();
			if (temp.getPage().contentEquals(page)) {
				index = getFlattedChapterList().indexOf(temp);
				break;
			}
		}
		if (index != -1 && index + 1 < getFlattedChapterList().size())
			return getFlattedChapterList().get(index + 1);
		else
			return null;
	}

	public PageModel getPrev(String page) {
		int index = -1;
		for (Iterator<PageModel> i = getFlattedChapterList().iterator(); i.hasNext();) {
			PageModel temp = i.next();
			if (temp.getPage().contentEquals(page)) {
				index = getFlattedChapterList().indexOf(temp);
				break;
			}
		}
		if (index != -1 && index - 1 >= 0)
			return getFlattedChapterList().get(index - 1);
		else
			return null;
	}

	public String getRedirectTo() {
		return redirectTo;
	}

	public void setRedirectTo(String redirectTo) {
		this.redirectTo = redirectTo;
	}

} public class PageModel {
	public static final String TYPE_NOVEL = "Novel";
	public static final String TYPE_OTHER = "Other";
	public static final String TYPE_CONTENT = "Content";
	public static final String TYPE_TOS = "Copyrights";
	private static final String TAG = PageModel.class.toString();
	private int id = -1;
	private String page;
	private String language; /* Attribute for language marker */
	private String title;
	private String type;
	private Date lastUpdate;
	private String parent;
	private PageModel parentPageModel;
	private PageModel pageModel;
	private Date lastCheck;
	private boolean isWatched;
	private boolean isFinishedRead = false;
	private boolean isDownloaded;
	private BookModel book;
	private int order;
	private String status;
	private boolean isHighlighted = false;
	private boolean isMissing = false;
	private boolean isExternal = false;
    private String summary;
    private String imgurl;
	private String redirectedTo;

	private int updateCount;

	// not saved to db
	private boolean isUpdated = false;

	public PageModel() {
	}

	public PageModel(String pageModel) {
		this.page = pageModel;
	}

	public int getId() {
		return id;
	}

	public void setId(int id) {
		this.id = id;
	}

	public String getPage() {
		return page;
	}

	public void setPage(String page) {
		this.page = page;
	}

	public String getLanguage() {
		if (language == null)
			language = Constants.LANG_ENGLISH;
		return language;
	}

	public void setLanguage(String language) {
		this.language = language;
	}

	public String getTitle() {
		return title;
	}

	public void setTitle(String title) {
		this.title = title;
	}

	public Date getLastUpdate() {
		return lastUpdate;
	}

	public void setLastUpdate(Date lastUpdate) {
		this.lastUpdate = lastUpdate;
	}

	public String getType() {
		return type;
	}

	public void setType(String type) {
		this.type = type;
	}

	public String getParent() {
		return parent;
	}

	public void setParent(String parent) {
		this.parent = parent;
	}

	public Date getLastCheck() {
		return lastCheck;
	}

	public void setLastCheck(Date lastCheck) {
		this.lastCheck = lastCheck;
	}

	public boolean isWatched() {
		return isWatched;
	}

	public void setWatched(boolean isWatched) {
		this.isWatched = isWatched;
	}

	public boolean isFinishedRead() {
		return isFinishedRead;
	}

	public void setFinishedRead(boolean isFinishedRead) {
		this.isFinishedRead = isFinishedRead;
	}

	public int getOrder() {
		return order;
	}

	public void setOrder(int order) {
		this.order = order;
	}

	public PageModel getParentPageModel() throws Exception {
		if (this.parentPageModel == null) {
			NovelsDao dao = NovelsDao.getInstance();
			PageModel tempPage = new PageModel();
			if (this.type.contentEquals(TYPE_CONTENT)) {
				String tempParent = parent.substring(0, parent.indexOf(Constants.NOVEL_BOOK_DIVIDER));
				tempPage.setPage(tempParent);
			}
			else {
				tempPage.setPage(this.parent);
			}
			this.parentPageModel = dao.getPageModel(tempPage, null);
		}
		return parentPageModel;
	}

	public void setParentPageModel(PageModel parentPageModel) {
		this.parentPageModel = parentPageModel;
	}

	public PageModel getPageModel() throws Exception {
		if (this.pageModel == null) {
			NovelsDao dao = NovelsDao.getInstance();
			PageModel tempPage = new PageModel();
			tempPage.setPage(this.page);
			this.pageModel = dao.getPageModel(tempPage, null);
		}
		return pageModel;
	}

	public void setPageModel(PageModel pageModel) {
		this.pageModel = pageModel;
	}

	public boolean isDownloaded() {
		return isDownloaded;
	}

	public void setDownloaded(boolean isDownloaded) {
		this.isDownloaded = isDownloaded;
	}

	public BookModel getBook() {
		if (this.getType() != null && this.getType().equals(TYPE_CONTENT)) {
			if (this.book == null) {
				NovelsDao dao = NovelsDao.getInstance();
				try {
					String bookTitle = parent.substring(parent.indexOf(Constants.NOVEL_BOOK_DIVIDER) + Constants.NOVEL_BOOK_DIVIDER.length());
					NovelCollectionModel details = dao.getNovelDetails(getParentPageModel(), null);
					for (Iterator<BookModel> iBook = details.getBookCollections().iterator(); iBook.hasNext();) {
						BookModel tempBook = iBook.next();
						if (tempBook.getTitle().equals(bookTitle)) {
							this.book = tempBook;
							break;
						}
					}
				} catch (Exception e) {
					Log.e(TAG, "Unable to get book for: " + getPage(), e);
				}
			}
			return this.book;
		}
		return null;
	}

	public void setBook(BookModel book) {
		this.book = book;
	}

	public boolean isUpdated() {
		return isUpdated;
	}

	public void setUpdated(boolean isUpdated) {
		this.isUpdated = isUpdated;
	}

	public String getRedirectedTo() {
		return redirectedTo;
	}

	public void setRedirectedTo(String redirectedTo) {
		this.redirectedTo = redirectedTo;
	}

	public boolean isHighlighted() {
		return isHighlighted;
	}

	public void setHighlighted(boolean isHighlighted) {
		this.isHighlighted = isHighlighted;
	}

	public boolean isMissing() {
		return isMissing;
	}

	public void setMissing(boolean isMissing) {
		this.isMissing = isMissing;
	}

	public String getStatus() {
		return status;
	}

	public void setStatus(String status) {
		this.status = status;
	}

	public boolean isTeaser() {
		if (status != null && status.length() > 0) {
			return status.contains(Constants.STATUS_TEASER);
		}
		return false;
	}

	public boolean isStalled() {
		if (status != null && status.length() > 0) {
			return status.contains(Constants.STATUS_STALLED);
		}
		return false;
	}

	public boolean isAbandoned() {
		if (status != null && status.length() > 0) {
			return status.contains(Constants.STATUS_ABANDONED);
		}
		return false;
	}

	public boolean isPending() {
		if (status != null && status.length() > 0) {
			return status.contains(Constants.STATUS_PENDING);
		}
		return false;
	}

	public boolean isExternal() {
		return isExternal;
	}

	public void setExternal(boolean isExternal) {
		this.isExternal = isExternal;
	}

	public int getUpdateCount() {
		return updateCount;
	}

	public void setUpdateCount(int updateCount) {
		this.updateCount = updateCount;
	}

	public boolean isRedlink() {
		if (Util.isStringNullOrEmpty(this.page))
			return false;
		return this.page.endsWith("&action=edit&redlink=1");
	}

    public String getSummary() {
        return summary;
    }

    public void setSummary(String summary) {
        this.summary = summary;
    }

    public String getImgurl() {
        return imgurl;
    }

    public void setImgurl(String imgurl) {
        this.imgurl = imgurl;
    }


} public class AddNovelTask extends AsyncTask<PageModel, ICallbackEventData, AsyncTaskResult<NovelCollectionModel>> implements ICallbackNotifier {
	public volatile IAsyncTaskOwner owner;

	public AddNovelTask(IAsyncTaskOwner displayLightNovelListActivity) {
		this.owner = displayLightNovelListActivity;
	}

	public void onCallback(ICallbackEventData message) {
		onProgressUpdate(message);
	}

	@Override
	protected AsyncTaskResult<NovelCollectionModel> doInBackground(PageModel... params) {
		Context ctx = owner.getContext();
		PageModel page = params[0];
		try {
			publishProgress(new CallbackEventData(ctx.getResources().getString(R.string.add_novel_task_check, page.getPage())));
			page = NovelsDao.getInstance().getUpdateInfo(page, this);
			if (page.isMissing()) {
				return new AsyncTaskResult<NovelCollectionModel>(new Exception(ctx.getResources().getString(R.string.add_novel_task_missing, page.getPage())));
			}

			NovelCollectionModel novelCol = NovelsDao.getInstance().getNovelDetailsFromInternet(page, this);
			Log.d("AddNovelTask", "Downloaded: " + novelCol.getPage());
			return new AsyncTaskResult<NovelCollectionModel>(novelCol);
		} catch (Exception e) {
			Log.e("AddNovelTask", e.getClass().toString() + ": " + e.getMessage(), e);
			publishProgress(new CallbackEventData(ctx.getResources().getString(R.string.add_novel_task_error, page.getPage(), e.getMessage())));
			return new AsyncTaskResult<NovelCollectionModel>(e);
		}
	}

	@Override
	protected void onProgressUpdate(ICallbackEventData... values) {
		// executed on UI thread.
		owner.setMessageDialog(values[0]);
	}

	@Override
	protected void onPostExecute(AsyncTaskResult<NovelCollectionModel> result) {
		owner.setMessageDialog(new CallbackEventData(owner.getContext().getResources().getString(R.string.add_novel_task_complete)));
		owner.onGetResult(result, NovelCollectionModel.class);
	}
} public class DownloadNovelDetailsTask extends AsyncTask<PageModel, ICallbackEventData, AsyncTaskResult<NovelCollectionModel[]>> implements ICallbackNotifier {
	public volatile IAsyncTaskOwner owner;
	private int currentPart = 0;
	private int totalParts = 0;
	private final String taskId;

	public DownloadNovelDetailsTask(IAsyncTaskOwner owner) {
		this.owner = owner;
		this.taskId = this.toString();
	}

	@Override
	protected void onPreExecute() {
		// executed on UI thread.
		// owner.toggleProgressBar(true);
		boolean exists = false;
		exists = owner.downloadListSetup(this.taskId, null, 0, false);
		if (exists)
			this.cancel(true);
	}

	public void onCallback(ICallbackEventData message) {
		publishProgress(message);
	}

	@Override
	protected AsyncTaskResult<NovelCollectionModel[]> doInBackground(PageModel... params) {
		Context ctx = owner.getContext();
		ArrayList<NovelCollectionModel> result = new ArrayList<NovelCollectionModel>();
		totalParts = params.length;
		for (PageModel pageModel : params) {
			currentPart++;
			try {
				publishProgress(new CallbackEventData(ctx.getResources().getString(R.string.download_novel_details_task_progress, pageModel.getTitle())));
				NovelCollectionModel novelCol = NovelsDao.getInstance().getNovelDetailsFromInternet(pageModel, this);
				Log.d("DownloadNovelDetailsTask", "Downloaded: " + novelCol.getPage());
				result.add(novelCol);
			} catch (Exception e) {
				Log.e("DownloadNovelDetailsTask", "Failed to download novel details for " + pageModel.getPage() + ": " + e.getMessage(), e);
				publishProgress(new CallbackEventData(ctx.getResources().getString(R.string.download_novel_details_task_error, pageModel.getPage(), e.getMessage())));
				return new AsyncTaskResult<NovelCollectionModel[]>(e);
			}
		}
		return new AsyncTaskResult<NovelCollectionModel[]>(result.toArray(new NovelCollectionModel[result.size()]));
	}

	@Override
	protected void onProgressUpdate(ICallbackEventData... values) {
		// executed on UI thread.
		owner.setMessageDialog(values[0]);
		owner.updateProgress(this.taskId, currentPart, totalParts, values[0].getMessage());
	}

	@Override
	protected void onPostExecute(AsyncTaskResult<NovelCollectionModel[]> result) {
		owner.setMessageDialog(new CallbackEventData(owner.getContext().getResources().getString(R.string.download_novel_details_task_complete)));
		owner.onGetResult(result, NovelCollectionModel[].class);
		owner.downloadListSetup(this.taskId, null, 2, result.getError() != null ? true : false);
	}
} public interface IAsyncTaskOwner {
	void updateProgress(String id, int current, int total, String message);

	boolean downloadListSetup(String id, String toastText, int type, boolean hasError);

	void toggleProgressBar(boolean show);

	void setMessageDialog(ICallbackEventData message);

	void onGetResult(AsyncTaskResult<?> result, Class<?> type);

	Context getContext();
} public class LoadNovelsTask extends AsyncTask<Void, ICallbackEventData, AsyncTaskResult<PageModel[]>> implements ICallbackNotifier {
	private static final String TAG = LoadNovelsTask.class.toString();
	private boolean refreshOnly = false;
	private boolean onlyWatched = false;
	private boolean alphOrder = false;
	private String api="";
	public volatile IAsyncTaskOwner owner;

	public LoadNovelsTask(IAsyncTaskOwner owner, boolean refreshOnly, boolean onlyWatched, boolean alphOrder,String api) {
		this.refreshOnly = refreshOnly;
		this.onlyWatched = onlyWatched;
		this.alphOrder = alphOrder;
		this.owner = owner;
		this.api=api;
	}

	public void onCallback(ICallbackEventData message) {
		publishProgress(message);
	}

	@Override
	protected void onPreExecute() {
		// executed on UI thread.
		owner.toggleProgressBar(true);
	}

	@Override
	protected AsyncTaskResult<PageModel[]> doInBackground(Void... arg0) {
		Context ctx = LNReaderApplication.getInstance().getApplicationContext();
		// different thread from UI
		try {
			ArrayList<PageModel> novels = new ArrayList<PageModel>();
			if (onlyWatched) {
				publishProgress(new CallbackEventData(ctx.getResources().getString(R.string.load_novels_task_watched)));
				novels = NovelsDao.getInstance().getWatchedNovel();
			}
			else {
				if (refreshOnly) {
					publishProgress(new CallbackEventData(ctx.getResources().getString(R.string.load_novels_task_refreshing)));
					novels = NovelsDao.getInstance().getNovelsFromInternet(this,api);
				}
				else {
					publishProgress(new CallbackEventData(ctx.getResources().getString(R.string.load_novels_task_loading)));
					novels = NovelsDao.getInstance().getNovels(this, alphOrder,api);
				}
			}
			return new AsyncTaskResult<PageModel[]>(novels.toArray(new PageModel[novels.size()]));
		} catch (Exception e) {
			Log.e(TAG, "Error when getting novel list: " + e.getMessage(), e);
			publishProgress(new CallbackEventData(ctx.getResources().getString(R.string.load_novels_task_error, e.getMessage())));
			return new AsyncTaskResult<PageModel[]>(e);
		}
	}

	@Override
	protected void onProgressUpdate(ICallbackEventData... values) {
		owner.setMessageDialog(values[0]);
	}

	@Override
	protected void onPostExecute(AsyncTaskResult<PageModel[]> result) {
		// executed on UI thread.
		if (onlyWatched) {
			owner.setMessageDialog(new CallbackEventData(owner.getContext().getResources().getString(R.string.load_novels_task_watched_complete)));
		} else {
			owner.setMessageDialog(new CallbackEventData(owner.getContext().getResources().getString(R.string.load_novels_task_complete)));
		}
		owner.onGetResult(result, PageModel[].class);
	}
}
import java.util.ArrayList;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.ContextMenu;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.LayoutInflater;
import android.view.MenuInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView.AdapterContextMenuInfo;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import android.widget.Toast;
import com.actionbarsherlock.app.SherlockListFragment;
import com.dotcool.reader.Constants;
import com.dotcool.reader.LNReaderApplication;
import com.dotcool.R;
import com.dotcool.reader.UIHelper;
import com.dotcool.reader.activity.DisplayLightNovelDetailsActivity;
import com.dotcool.reader.activity.INovelListHelper;
import com.dotcool.reader.adapter.PageModelAdapter;
import com.dotcool.reader.callback.CallbackEventData;
import com.dotcool.reader.callback.ICallbackEventData;
import com.dotcool.reader.dao.NovelsDao;
import com.dotcool.reader.helper.AsyncTaskResult;
import com.dotcool.reader.model.NovelCollectionModel;
import com.dotcool.reader.model.PageModel;
import com.dotcool.reader.task.AddNovelTask;
import com.dotcool.reader.task.DownloadNovelDetailsTask;
import com.dotcool.reader.task.IAsyncTaskOwner;
import com.dotcool.reader.task.LoadNovelsTask;
package com.dotcool.reader.fragment;




/*
 * Author: Nandaka
 * Copy from: NovelsActivity.java
 */

public class DisplayLightNovelListFragment extends SherlockListFragment implements IAsyncTaskOwner, INovelListHelper {
	private static final String TAG = DisplayLightNovelListFragment.class.toString();
	private final ArrayList<PageModel> listItems = new ArrayList<PageModel>();
	private PageModelAdapter adapter;
	private LoadNovelsTask task = null;