package com.robert.maps.applib.downloader; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.InputStream; import java.io.OutputStream; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.HttpURLConnection; import java.net.URL; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.andnav.osm.views.util.StreamUtils; import org.andnav.osm.views.util.Util; import android.annotation.TargetApi; import android.app.Notification; import android.app.NotificationManager; import android.app.PendingIntent; import android.app.Service; import android.content.Intent; import android.os.Build; import android.os.Handler; import android.os.IBinder; import android.os.Message; import android.os.RemoteCallbackList; import android.os.RemoteException; import android.widget.Toast; import com.robert.maps.applib.R; import com.robert.maps.applib.tileprovider.TileProviderFileBase; import com.robert.maps.applib.tileprovider.TileSource; import com.robert.maps.applib.utils.SQLiteMapDatabase; import com.robert.maps.applib.utils.SimpleThreadFactory; import com.robert.maps.applib.utils.Ut; public class MapDownloaderService extends Service { private final int THREADCOUNT = 1; private NotificationManager mNM; private Notification mNotification; private PendingIntent mContentIntent = null; private int mZoomArr[]; private int mCoordArr[]; private String mMapID; private int mZoom; private String mOfflineMapName; private boolean mOverwriteFile; private boolean mOverwriteTiles; private boolean mLoadToOnlineCache; private TileIterator mTileIterator; private SQLiteMapDatabase mMapDatabase; private TileSource mTileSource; private ExecutorService mThreadPool = Executors.newFixedThreadPool(THREADCOUNT, new SimpleThreadFactory( "MapDownloaderService")); private Handler mHandler = new DownloaderHanler(); final RemoteCallbackList<IDownloaderCallback> mCallbacks = new RemoteCallbackList<IDownloaderCallback>(); private int mTileCntTotal = 0, mTileCnt = 0, mErrorCnt = 0; private long mStartTime = 0; private String mLogFileName; private final IRemoteService.Stub mBinder = new IRemoteService.Stub() { public void registerCallback(IDownloaderCallback cb) { if (cb != null) { mCallbacks.register(cb); if (mStartTime > 0) try { cb.downloadStart(mTileCntTotal, mStartTime, mLoadToOnlineCache ? "" : mOfflineMapName, mMapID, mZoom, mCoordArr[0], mCoordArr[1], mCoordArr[2], mCoordArr[3]); } catch (RemoteException e) { } } } public void unregisterCallback(IDownloaderCallback cb) { if (cb != null) mCallbacks.unregister(cb); } }; @Override public void onCreate() { super.onCreate(); mLogFileName = Ut.getRMapsMainDir(this, "").getAbsolutePath()+"/cache/mapdownloaderlog.txt"; final File file = new File(mLogFileName); if(file.exists()) file.delete(); mNM = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); try { mStartForeground = getClass().getMethod("startForeground", mStartForegroundSignature); mStopForeground = getClass().getMethod("stopForeground", mStopForegroundSignature); return; } catch (NoSuchMethodException e) { // Running on an older platform. mStartForeground = mStopForeground = null; } try { mSetForeground = getClass().getMethod("setForeground", mSetForegroundSignature); } catch (NoSuchMethodException e) { throw new IllegalStateException( "OS doesn't have Service.startForeground OR Service.setForeground!"); } } @Override public void onStart(Intent intent, int startId) { if(intent != null) handleCommand(intent); } @Override public int onStartCommand(Intent intent, int flags, int startId) { if(intent != null) handleCommand(intent); return START_STICKY; } private void handleCommand(Intent intent) { if(mStartTime > 0) { Toast.makeText(this, R.string.downloader_notif_text, Toast.LENGTH_LONG).show(); return; } checkLimitation(); mZoomArr = intent.getIntArrayExtra("ZOOM"); mCoordArr = intent.getIntArrayExtra("COORD"); mMapID = intent.getStringExtra("MAPID"); mZoom = intent.getIntExtra("ZOOMCUR", 0); mOfflineMapName = intent.getStringExtra("OFFLINEMAPNAME"); mOverwriteFile = intent.getBooleanExtra("overwritefile", true); mOverwriteTiles = intent.getBooleanExtra("overwritetiles", false); mLoadToOnlineCache = intent.getBooleanExtra("online_cache", false); mContentIntent = PendingIntent.getActivity( this, 0, new Intent(this, DownloaderActivity.class) .putExtra("MAPID", mMapID) .putExtra("Latitude", mCoordArr[2] - mCoordArr[0]) .putExtra("Longitude", mCoordArr[3] - mCoordArr[1]) .putExtra("ZoomLevel", mZoomArr[0]) .putExtra("OFFLINEMAPNAME", mOfflineMapName), 0); showNotification(); try { mTileSource = new TileSource(this, mMapID, true, false); } catch (Exception e1) { e1.printStackTrace(); return; } final SQLiteMapDatabase cacheDatabase = new SQLiteMapDatabase(); if(mLoadToOnlineCache) { if(mTileSource.CACHE.trim().equalsIgnoreCase("")) mOfflineMapName = mTileSource.ID; else mOfflineMapName = mTileSource.CACHE; } final File folder = mLoadToOnlineCache ? Ut.getRMapsMainDir(this, "cache") : Ut.getRMapsMapsDir(this); final File file = new File(folder.getAbsolutePath() + "/" + mOfflineMapName + ".sqlitedb"); if (mOverwriteFile && !mLoadToOnlineCache) { File[] files = folder.listFiles(); if (files != null) { for (int i = 0; i < files.length; i++) { if (files[i].getName().startsWith(file.getName())) files[i].delete(); } } } try { cacheDatabase.setFile(file.getAbsolutePath()); } catch (Exception e) { e.printStackTrace(); } mMapDatabase = cacheDatabase; mTileCnt = 0; mTileCntTotal = getTileCount(mZoomArr, mCoordArr); mTileIterator = new TileIterator(mZoomArr, mCoordArr); mStartTime = System.currentTimeMillis(); final int N = mCallbacks.beginBroadcast(); for (int i = 0; i < N; i++) { try { mCallbacks.getBroadcastItem(i).downloadStart(mTileCntTotal, mStartTime, mOfflineMapName, mMapID, mZoom, mCoordArr[0], mCoordArr[1], mCoordArr[2], mCoordArr[3]); } catch (RemoteException e) { } } mCallbacks.finishBroadcast(); for (int i = 0; i < THREADCOUNT; i++) mThreadPool.execute(new Downloader()); } private void checkLimitation() { // InputStream in = null; // OutputStream out = null; // // try { // in = new BufferedInputStream(new URL("https://sites.google.com/site/robertk506/limits.txt").openStream(), StreamUtils.IO_BUFFER_SIZE); // // final ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); // out = new BufferedOutputStream(dataStream, StreamUtils.IO_BUFFER_SIZE); // StreamUtils.copy(in, out); // out.flush(); // // String str = dataStream.toString(); // Ut.w("checkLimitation: "+str); // //JSONObject json = new JSONObject(str.replace("YMaps.TrafficLoader.onLoad(\"stat\",", "").replace("});", "}")); // // } catch (Exception e) { // e.printStackTrace(); // } finally { // StreamUtils.closeStream(in); // StreamUtils.closeStream(out); // } } @Override public void onDestroy() { if (mThreadPool != null) { mThreadPool.shutdown(); try { if (!mThreadPool.awaitTermination(5L, TimeUnit.SECONDS)) { mThreadPool.shutdownNow(); } } catch (InterruptedException e) { } } try { TileProviderFileBase provider = new TileProviderFileBase(this); provider.CommitIndex(mMapDatabase.getID("usermap_"), 0, 0, mMapDatabase.getMinZoom(), mMapDatabase.getMaxZoom()); provider.Free(); } catch (Exception e1) { } final int N = mCallbacks.beginBroadcast(); for (int i = 0; i < N; i++) { try { mCallbacks.getBroadcastItem(i).downloadDone(); } catch (RemoteException e) { } } mCallbacks.finishBroadcast(); if (mMapDatabase != null) { mMapDatabase.setParams(mMapID, mTileSource.NAME, mCoordArr, mZoomArr, mZoom); mMapDatabase.Free(); } if (mTileSource != null) mTileSource.Free(); //mNM.cancel(R.id.downloader_service); stopForegroundCompat(R.id.downloader_service); mNM = null; mStartTime = 0; super.onDestroy(); } @TargetApi(Build.VERSION_CODES.ECLAIR) private void showNotification() { // In this sample, we'll use the same text for the ticker and the // expanded notification CharSequence text = getText(R.string.downloader_notif_ticket); // Set the icon, scrolling text and timestamp mNotification = new Notification(R.drawable.r_download, text, System.currentTimeMillis()); mNotification.flags = mNotification.flags | Notification.FLAG_NO_CLEAR; // The PendingIntent to launch our activity if the user selects this // notification // mContentIntent = PendingIntent.getActivity(this, 0, new Intent(this, // MainActivity.class), 0); // Set the info for the views that show in the notification panel. mNotification.setLatestEventInfo(this, getText(R.string.downloader_notif_title), getText(R.string.downloader_notif_text), mContentIntent); // Send the notification. // We use a string id because it is a unique number. We use it later to // cancel. //mNM.notify(R.id.downloader_service, mNotification); startForegroundCompat(R.id.downloader_service, mNotification); } private void downloadDone() { stopSelf(); // ((NotificationManager)getSystemService(NOTIFICATION_SERVICE)).cancel(R.string.remote_service_started); // // CharSequence text = getText(R.string.auto_follow_enabled); // // // Set the icon, scrolling text and timestamp // Notification notification = new // Notification(R.drawable.track_writer_service, text, // System.currentTimeMillis()); // //notification.flags = notification.flags | // Notification.FLAG_NO_CLEAR; // // // The PendingIntent to launch our activity if the user selects this // notification // PendingIntent contentIntent = PendingIntent.getActivity(this, 0, // new Intent(this, AreaSelectorActivity.class), 0); // // // Set the info for the views that show in the notification panel. // notification.setLatestEventInfo(this, // getText(R.string.auto_follow_enabled), text, contentIntent); // // // Send the notification. // // We use a string id because it is a unique number. We use it later // to cancel. // ((NotificationManager)getSystemService(NOTIFICATION_SERVICE)).notify(R.string.auto_follow_enabled, // notification); } @Override public IBinder onBind(Intent intent) { return mBinder; } private class DownloaderHanler extends Handler { private int doneCounter = 0; @Override public void handleMessage(Message msg) { if(msg.what == R.id.done) { doneCounter++; if (doneCounter >= THREADCOUNT) downloadDone(); } else if(msg.what == R.id.tile_done || msg.what == R.id.tile_error) { mTileCnt++; if (msg.what == R.id.tile_error) mErrorCnt++; mNotification.setLatestEventInfo( MapDownloaderService.this, getText(R.string.downloader_notif_title), getText(R.string.downloader_notif_text) + String.format(": %d%% (%d/%d)", (mTileCnt * 100 / mTileCntTotal), mTileCnt, mTileCntTotal), mContentIntent); if (mNM != null) mNM.notify(R.id.downloader_service, mNotification); final int N = mCallbacks.beginBroadcast(); final XYZ tileParam = (XYZ) msg.obj; for (int i = 0; i < N; i++) { try { if (tileParam == null) mCallbacks.getBroadcastItem(i).downloadTileDone(mTileCnt, mErrorCnt, -1, -1, -1); else mCallbacks.getBroadcastItem(i).downloadTileDone(mTileCnt, mErrorCnt, tileParam.X, tileParam.Y, tileParam.Z); } catch (RemoteException e) { } } mCallbacks.finishBroadcast(); } } } private class Downloader implements Runnable { public void run() { XYZ tileParam = null; boolean continueExecute = true; while (continueExecute && !mThreadPool.isShutdown()) { synchronized (mTileIterator) { if (mTileIterator.hasNext()) { tileParam = mTileIterator.next(); } else { continueExecute = false; tileParam = null; } } if (tileParam != null) { tileParam.TILEURL = mTileSource.getTileURLGenerator().Get(tileParam.X, tileParam.Y, tileParam.Z); InputStream in = null; OutputStream out = null; try { if (mOverwriteFile || mOverwriteTiles || !mOverwriteTiles && !mMapDatabase.existsTile(tileParam.X, tileParam.Y, tileParam.Z)) { byte[] data = null; final URL url = new URL(tileParam.TILEURL); final HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.connect(); if(connection.getResponseCode() != 200) Ut.appendLog(mLogFileName, String.format("%tc %s\n Response: %d %s", System.currentTimeMillis(), tileParam.TILEURL, connection.getResponseCode(), connection.getResponseMessage())); in = new BufferedInputStream(url.openStream(), StreamUtils.IO_BUFFER_SIZE); final ByteArrayOutputStream dataStream = new ByteArrayOutputStream(); out = new BufferedOutputStream(dataStream, StreamUtils.IO_BUFFER_SIZE); StreamUtils.copy(in, out); out.flush(); data = dataStream.toByteArray(); if (data != null) { if (mOverwriteTiles) mMapDatabase.deleteTile(tileParam.TILEURL, tileParam.X, tileParam.Y, tileParam.Z); mMapDatabase.putTile(tileParam.X, tileParam.Y, tileParam.Z, data); } } if (mHandler != null) Message.obtain(mHandler, R.id.tile_done, tileParam).sendToTarget(); } catch (Exception e) { Ut.appendLog(mLogFileName, String.format("%tc %s\n Error: %s", System.currentTimeMillis(), tileParam.TILEURL, e.getMessage())); if (mHandler != null) Message.obtain(mHandler, R.id.tile_error, tileParam).sendToTarget(); } catch (OutOfMemoryError e) { Ut.appendLog(mLogFileName, String.format("%tc %s\n Error: %s", System.currentTimeMillis(), tileParam.TILEURL, e.getMessage())); if (mHandler != null) Message.obtain(mHandler, R.id.tile_error, tileParam).sendToTarget(); System.gc(); } finally { StreamUtils.closeStream(in); StreamUtils.closeStream(out); } } // try { // Thread.sleep(400); // } catch (InterruptedException e) { // } } if (mHandler != null) Message.obtain(mHandler, R.id.done).sendToTarget(); } } private int getTileCount(int[] zArr, int[] coordArr) { int xMin = 0, xMax = 0; int yMin = 0, yMax = 0; int cnt = 0; for (int i = 0; i < zArr.length; i++) { final int c0[] = Util.getMapTileFromCoordinates(coordArr[0], coordArr[1], zArr[i], null, mTileSource.PROJECTION); final int c1[] = Util.getMapTileFromCoordinates(coordArr[2], coordArr[3], zArr[i], null, mTileSource.PROJECTION); xMin = Math.min(c0[0], c1[0]); xMax = Math.max(c0[0], c1[0]); yMin = Math.min(c0[1], c1[1]); yMax = Math.max(c0[1], c1[1]); cnt += (xMax - xMin + 1) * (yMax - yMin + 1); } return cnt; } private class TileIterator { private int zInd = -1, zArr[]; private int x, xMin = 0, xMax = 0; private int y, yMin = 0, yMax = 0; private int coordArr[]; public TileIterator(int zarr[], int coordarr[]) { zArr = zarr; zInd = -1; x = 1; y = 1; coordArr = coordarr; } public boolean hasNext() { x++; if (x > xMax) { y++; x = xMin; if (y > yMax) { zInd++; y = yMin; if (zInd > zArr.length - 1) { return false; } final int c0[] = Util.getMapTileFromCoordinates(coordArr[0], coordArr[1], zArr[zInd], null, mTileSource.PROJECTION); final int c1[] = Util.getMapTileFromCoordinates(coordArr[2], coordArr[3], zArr[zInd], null, mTileSource.PROJECTION); yMin = Math.min(c0[0], c1[0]); yMax = Math.max(c0[0], c1[0]); xMin = Math.min(c0[1], c1[1]); xMax = Math.max(c0[1], c1[1]); x = xMin; y = yMin; } } return true; } public XYZ next() { try { return new XYZ(null, x, y, zArr[zInd]); } catch (Exception e) { return null; } } } private class XYZ { public String TILEURL; public int X; public int Y; public int Z; public XYZ(final String tileurl, final int x, final int y, final int z) { TILEURL = tileurl; X = x; Y = y; Z = z; } } private static final Class<?>[] mSetForegroundSignature = new Class[] { boolean.class}; private static final Class<?>[] mStartForegroundSignature = new Class[] { int.class, Notification.class}; private static final Class<?>[] mStopForegroundSignature = new Class[] { boolean.class}; private Method mSetForeground; private Method mStartForeground; private Method mStopForeground; private Object[] mSetForegroundArgs = new Object[1]; private Object[] mStartForegroundArgs = new Object[2]; private Object[] mStopForegroundArgs = new Object[1]; void invokeMethod(Method method, Object[] args) { try { method.invoke(this, args); } catch (InvocationTargetException e) { } catch (IllegalAccessException e) { } } /** * This is a wrapper around the new startForeground method, using the older * APIs if it is not available. */ void startForegroundCompat(int id, Notification notification) { // If we have the new startForeground API, then use it. if (mStartForeground != null) { mStartForegroundArgs[0] = Integer.valueOf(id); mStartForegroundArgs[1] = notification; invokeMethod(mStartForeground, mStartForegroundArgs); return; } // Fall back on the old API. mSetForegroundArgs[0] = Boolean.TRUE; invokeMethod(mSetForeground, mSetForegroundArgs); mNM.notify(id, notification); } /** * This is a wrapper around the new stopForeground method, using the older * APIs if it is not available. */ void stopForegroundCompat(int id) { // If we have the new stopForeground API, then use it. if (mStopForeground != null) { mStopForegroundArgs[0] = Boolean.TRUE; invokeMethod(mStopForeground, mStopForegroundArgs); return; } // Fall back on the old API. Note to cancel BEFORE changing the // foreground state, since we could be killed at that point. mNM.cancel(id); mSetForegroundArgs[0] = Boolean.FALSE; invokeMethod(mSetForeground, mSetForegroundArgs); } }