/** * Copyright (C) 2013 Gundog Studios LLC. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.godsandtowers.util; import java.io.File; import java.io.FileOutputStream; import java.net.URL; import java.net.URLConnection; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; import java.util.ArrayList; import java.util.HashMap; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicLong; import com.gundogstudios.modules.Modules; public class DownloadManager { private static final int FETCHER_THREADS = 2; private static final String TAG = "DownloadManager"; private static final int SUCCESSFUL = Integer.MIN_VALUE; private static final String HOST_NAME = "http://godsandtowers.appspot.com/"; private static final String[][] ASSETS = { { "boards/", "basic.csv" }, { "boards/", "basic_icon.jpg" }, { "boards/", "close_call.csv" }, { "boards/", "close_call_icon.jpg" }, { "boards/", "confusion.csv" }, { "boards/", "confusion_icon.jpg" }, { "boards/", "decisions.csv" }, { "boards/", "decisions_icon.jpg" }, { "boards/", "four_square.csv" }, { "boards/", "four_square_icon.jpg" }, { "boards/", "oasis.csv" }, { "boards/", "oasis_icon.jpg" }, { "boards/", "open_field.csv" }, { "boards/", "open_field_icon.jpg" }, { "boards/", "path_to_freedom.csv" }, { "boards/", "path_to_freedom_icon.jpg" }, { "boards/", "rocky_mountain.csv" }, { "boards/", "rocky_mountain_icon.jpg" }, { "boards/", "rush.csv" }, { "boards/", "rush_icon.jpg" }, { "boards/", "simple.csv" }, { "boards/", "simple_icon.jpg" }, { "boards/", "three_way.csv" }, { "boards/", "three_way_icon.jpg" }, { "models/", "ArcherHorsemen.gs1" }, { "models/", "Archers.gs1" }, { "models/", "ArrowProjectiles.gs1" }, { "models/", "Ballistas.gs1" }, { "models/", "BallProjectiles.gs1" }, { "models/", "Base.gs1" }, { "models/", "Bears.gs1" }, { "models/", "Birds.gs1" }, { "models/", "Blade.gs1" }, { "models/", "Cactus.gs1" }, { "models/", "Cannons.gs1" }, { "models/", "Catapults.gs1" }, { "models/", "Crystallizers.gs1" }, { "models/", "DeadHorsemen.gs1" }, { "models/", "DeadSoldiers.gs1" }, { "models/", "Dragons.gs1" }, { "models/", "Emitters.gs1" }, { "models/", "FemaleAngels.gs1" }, { "models/", "FemaleMages.gs1" }, { "models/", "Golems.gs1" }, { "models/", "Horsemen.gs1" }, { "models/", "Horses.gs1" }, { "models/", "HumanoidBeasts.gs1" }, { "models/", "Humanoids.gs1" }, { "models/", "Mages.gs1" }, { "models/", "MaleAngels.gs1" }, { "models/", "Pillars.gs1" }, { "models/", "Rock.gs1" }, { "models/", "Rubble.gs1" }, { "models/", "Soldiers.gs1" }, { "models/", "Statues.gs1" }, { "models/", "Tree.gs1" }, { "models/", "WallCorner.gs1" }, { "models/", "WallSide.gs1" }, { "models/", "Whip.gs1" }, { "music/", "10_intro.mp3" }, { "music/", "10_main.mp3" }, { "music/", "1_intro.mp3" }, { "music/", "1_main.mp3" }, { "music/", "2_intro.mp3" }, { "music/", "2_main.mp3" }, { "music/", "3_intro.mp3" }, { "music/", "3_main.mp3" }, { "music/", "4_intro.mp3" }, { "music/", "4_main.mp3" }, { "music/", "5_intro.mp3" }, { "music/", "5_main.mp3" }, { "music/", "6_intro.mp3" }, { "music/", "6_main.mp3" }, { "music/", "7_intro.mp3" }, { "music/", "7_main.mp3" }, { "music/", "8_intro.mp3" }, { "music/", "8_main.mp3" }, { "music/", "9_intro.mp3" }, { "music/", "9_main.mp3" }, { "music/", "theme_intro.mp3" }, { "music/", "theme_main.mp3" }, { "textures/", "angel.png" }, { "textures/", "angel_statue.png" }, { "textures/", "background_death.jpg" }, { "textures/", "background_earth.jpg" }, { "textures/", "background_fire.jpg" }, { "textures/", "background_ice.jpg" }, { "textures/", "background_life.jpg" }, { "textures/", "background_wind.jpg" }, { "textures/", "dead_horse.png" }, { "textures/", "desert_ground.jpg" }, { "textures/", "desert_props.png" }, { "textures/", "desert_wall.jpg" }, { "textures/", "dirt_emitter.png" }, { "textures/", "dragon.png" }, { "textures/", "drake.png" }, { "textures/", "dwarf.png" }, { "textures/", "eagle.png" }, { "textures/", "earth_ballista.png" }, { "textures/", "earth_ball_projectile.png" }, { "textures/", "earth_crystallizer.jpg" }, { "textures/", "earth_golem.jpg" }, { "textures/", "earth_pillar.jpg" }, { "textures/", "earth_rubble.jpg" }, { "textures/", "elf_wizard.png" }, { "textures/", "elvish_archer.png" }, { "textures/", "energy_cannon.jpg" }, { "textures/", "falcon.png" }, { "textures/", "fallen_angel.png" }, { "textures/", "female_elf_mage.png" }, { "textures/", "female_necromancer.png" }, { "textures/", "fiery_bear.jpg" }, { "textures/", "fire_ball_projectile.png" }, { "textures/", "fire_emitter.png" }, { "textures/", "fire_golem.jpg" }, { "textures/", "fire_pillar.jpg" }, { "textures/", "fire_rubble.jpg" }, { "textures/", "flame_ballista.png" }, { "textures/", "flame_blade.png" }, { "textures/", "flame_cannon.png" }, { "textures/", "flame_catapult.png" }, { "textures/", "flame_crystallizer.jpg" }, { "textures/", "flame_whip.png" }, { "textures/", "forest_ground.jpg" }, { "textures/", "forest_props.png" }, { "textures/", "forest_wall.jpg" }, { "textures/", "frozen_horse.png" }, { "textures/", "frozen_soldier.png" }, { "textures/", "gargoyle_statue.png" }, { "textures/", "ghoul.png" }, { "textures/", "gnome.png" }, { "textures/", "griffin_statue.png" }, { "textures/", "grizzly_bear.jpg" }, { "textures/", "halfling.png" }, { "textures/", "holy_water_emitter.jpg" }, { "textures/", "horse.png" }, { "textures/", "human_archer.png" }, { "textures/", "human_mage.png" }, { "textures/", "human_necromancer.png" }, { "textures/", "human_witch.png" }, { "textures/", "ice_ballista.png" }, { "textures/", "ice_ball_projectile.png" }, { "textures/", "ice_blade.png" }, { "textures/", "ice_crystallizer.jpg" }, { "textures/", "ice_golem.jpg" }, { "textures/", "kobold.png" }, { "textures/", "life_ball_projectile.png" }, { "textures/", "life_golem.jpg" }, { "textures/", "life_pillar.jpg" }, { "textures/", "life_rubble.jpg" }, { "textures/", "lightning_blade.png" }, { "textures/", "lightning_cannon.png" }, { "textures/", "lightning_emitter.png" }, { "textures/", "lightning_golem.jpg" }, { "textures/", "lightning_pillar.jpg" }, { "textures/", "mummy.png" }, { "textures/", "panda_bear.jpg" }, { "textures/", "poison_cannon.png" }, { "textures/", "polar_bear.jpg" }, { "textures/", "rock_cannon.png" }, { "textures/", "seraphim.png" }, { "textures/", "sky_day.jpg" }, { "textures/", "sky_night.jpg" }, { "textures/", "stone_catapult.png" }, { "textures/", "succubus.png" }, { "textures/", "tentical_whip.png" }, { "textures/", "toxic_gas_emitter.jpg" }, { "textures/", "troll.png" }, { "textures/", "undead_ball_projectile.jpg" }, { "textures/", "undead_golem.jpg" }, { "textures/", "undead_pillar.jpg" }, { "textures/", "undead_rubble.jpg" }, { "textures/", "wall_side.jpg" }, { "textures/", "water_cannon.png" }, { "textures/", "water_catapult.png" }, { "textures/", "water_emitter.png" }, { "textures/", "water_pillar.jpg" }, { "textures/", "water_rubble.jpg" }, { "textures/", "water_whip.png" }, { "textures/", "wind_ball_projectile.jpg" }, { "textures/", "wind_rubble.jpg" }, { "textures/", "winter_forest_ground.jpg" }, { "textures/", "winter_forest_props.png" }, { "textures/", "winter_forest_wall.jpg" }, { "textures/", "zealot.png" }, { "textures/", "zombie.png" }, }; private static final long[] ASSET_SIZES = { 189, 9897, 136, 9101, 275, 8669, 212, 9159, 275, 8833, 228, 8323, 180, 9158, 292, 10505, 182, 8178, 196, 9906, 182, 7992, 273, 10512, 292528, 292528, 438, 85224, 2114, 2564, 208592, 269906, 70310, 2810, 44418, 85962, 91780, 258630, 227362, 297496, 10640, 252398, 287756, 171784, 257452, 262152, 317646, 343788, 292116, 212582, 7458, 2372, 65786, 286086, 81678, 13002, 3566, 966, 151566, 387072, 1120256, 387072, 1120256, 247808, 1476608, 247808, 1968128, 131072, 1282048, 131072, 1282048, 124928, 1476608, 315392, 1433600, 282624, 1120256, 247808, 1476608, 32768, 739328, 336169, 445412, 128340, 70188, 73455, 73468, 131514, 70193, 404764, 57403, 436573, 66573, 371145, 445306, 431645, 324589, 389047, 378028, 138379, 50864, 36448, 53786, 17790, 329652, 383968, 56098, 415637, 308224, 336794, 320578, 49483, 126134, 335807, 44910, 65285, 14589, 359339, 403766, 345040, 390106, 40720, 374329, 163731, 431092, 68905, 501918, 553702, 352448, 284317, 352186, 406563, 66215, 304283, 58381, 378399, 383731, 380555, 290630, 307534, 408192, 144920, 437823, 44299, 56876, 352539, 147845, 54001, 66898, 18190, 455979, 426567, 381167, 48491, 64668, 501662, 72751, 341532, 90012, 376659, 284352, 17896, 13656, 391301, 275637, 452414, 47541, 248888, 18507, 37970, 49837, 14389, 119803, 412638, 444927, 411892, 65070, 18712, 399156, 23419, 17995, 100299, 425440, 67205, 420137, 391808, }; public static final long TOTAL_SIZE = 46975565; private HashMap<String, Integer> assets; private AtomicLong currentAssetSize; private ExecutorService executor; private ArrayList<Future<Integer>> futures; private String FILE_SYSTEM_PATH; public DownloadManager(String systemPath) { // TODO use "/Android/data/com.godsandtowers/" not "TDW" for the release version of the game so that all data // is deleted after uninstall FILE_SYSTEM_PATH = systemPath + "/Gods And Towers/assets/"; currentAssetSize = new AtomicLong(0); executor = Executors.newFixedThreadPool(FETCHER_THREADS); assets = new HashMap<String, Integer>(); futures = new ArrayList<Future<Integer>>(); } private long verifyAsset(File file) { long fileSize = 0; File[] files = file.listFiles(); if (files != null) { for (File f : files) { fileSize += verifyAsset(f); } } else { Integer loc = assets.remove(file.getName()); if (loc != null) { fileSize = file.length(); if (fileSize != ASSET_SIZES[loc]) { assets.put(file.getName(), loc); Modules.LOG.info(TAG, "Asset: " + file.getName() + " has incorrect filesize, it is " + fileSize + " and should be " + ASSET_SIZES[loc]); file.delete(); return 0; } } else { Modules.LOG.info(TAG, "Unknown asset file: " + file.getName()); } } return fileSize; } public void shutdown() { executor.shutdownNow(); } public synchronized boolean verifyAllAssets() { assets.clear(); for (int i = 0; i < ASSETS.length; i++) { assets.put(ASSETS[i][1], i); } currentAssetSize.set(verifyAsset(new File(FILE_SYSTEM_PATH))); if (assets.size() > 0) { Modules.LOG.info(TAG, "Still waiting to download " + assets.size() + " assets out of " + ASSETS.length); return false; } if (currentAssetSize.get() != TOTAL_SIZE) { Modules.LOG.error(TAG, "CurrentAssetSize " + currentAssetSize + " does not match total size " + TOTAL_SIZE); return false; } return true; } public synchronized void downloadMissingAssets() { futures.clear(); for (String asset : assets.keySet()) { Integer loc = assets.get(asset); Future<Integer> submit = executor.submit(new Downloader(loc, HOST_NAME)); futures.add(submit); } } public synchronized boolean downloadComplete() { for (Future<Integer> future : futures) { if (!future.isDone()) return false; } return true; } public synchronized boolean downloadFailed() { for (Future<Integer> future : futures) { try { int result = future.get(); if (result != SUCCESSFUL) return true; } catch (InterruptedException e) { Modules.LOG.error(TAG, e.toString()); return true; } catch (ExecutionException e) { Modules.LOG.error(TAG, e.toString()); return true; } } return false; } public synchronized long getCurrentSize() { return currentAssetSize.get(); } public synchronized long getTotalSize() { return TOTAL_SIZE; } private class Downloader implements Callable<Integer> { private int loc; private String hostname; public Downloader(int loc, String hostname) { this.loc = loc; this.hostname = hostname; } @Override public Integer call() { try { long startTime = System.currentTimeMillis(); String basePath = ASSETS[loc][0]; String fileName = ASSETS[loc][1]; String urlName = hostname + "assets/" + basePath + fileName; Modules.LOG.info(TAG, "Downloading " + urlName); String path = FILE_SYSTEM_PATH + basePath; new File(path).mkdirs(); path += fileName; Modules.LOG.info(TAG, "Storing " + fileName + " at " + path); FileOutputStream stream = new FileOutputStream(path, false); FileChannel fileChannel = stream.getChannel(); URL url = new URL(urlName); URLConnection openConnection = url.openConnection(); openConnection.setUseCaches(false); openConnection.setDoOutput(false); ReadableByteChannel inChannel = Channels.newChannel(openConnection.getInputStream()); long read = 0; long pos = 0; while ((read = fileChannel.transferFrom(inChannel, pos, ASSET_SIZES[loc])) >= 0 && pos < ASSET_SIZES[loc]) { pos += read; currentAssetSize.addAndGet(read); } inChannel.close(); fileChannel.close(); stream.close(); File file = new File(path); if (ASSET_SIZES[loc] != file.length()) { file.delete(); Modules.LOG.error(TAG, "Failed to download " + fileName + " correctly"); return loc; } Modules.LOG.info(TAG, "wrote file in " + (System.currentTimeMillis() - startTime) + " msec"); return SUCCESSFUL; } catch (Exception e) { Modules.LOG.error(TAG, "Error downloading file: " + e.toString()); return 0; } } } }