File Download Service
/*
Copyright (c) 2010, Sungjin Han <meinside@gmail.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of meinside nor the names of its contributors may be
used to endorse or promote products derived from this software without
specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
//package org.andlib.helpers;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.logging.Logger;
import android.app.Notification;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Intent;
import android.os.AsyncTask;
import android.os.Binder;
import android.os.IBinder;
import android.widget.RemoteViews;
/**
* service class for downloading multiple files from web
* <br>
* <br>
* - to start: startService(new Intent(context, SomeOverriddenClass.class));
* <br>
* - to stop: stopService(new Intent(context, SomeOverriddenClass.class));
* <br>
*
* @author meinside@gmail.com
* @since 10.11.05.
*
* last update 11.03.13.
*
*/
abstract class FileDownloadService extends Service
{
public static final int SERVICE_ID = 0x101104;
public static final int BYTES_BUFFER_SIZE = 32 * 1024;
private NotificationManager notificationManager;
private final IBinder binder = new FileDownloadBinder();
private AsyncDownloadTask task = null;
protected static boolean isRunning = false;
public class FileDownloadBinder extends Binder
{
FileDownloadService getService()
{
return FileDownloadService.this;
}
}
/**
*
* @return if service is running or not
*/
public static boolean isRunning()
{
return isRunning;
}
@Override
public void onCreate()
{
if(isRunning)
return;
else
isRunning = true;
notificationManager = (NotificationManager)getSystemService(NOTIFICATION_SERVICE);
//start downloading immediately
task = new AsyncDownloadTask();
task.execute();
// Logger.v("service created");
}
@Override
public int onStartCommand(Intent intent, int flags, int startId)
{
return START_STICKY;
}
@Override
public void onDestroy()
{
if(task != null)
{
if(!task.isCancelled())
task.cancel(true);
}
isRunning = false;
// Logger.v("service destroyed");
}
/* (non-Javadoc)
* @see android.app.Service#onBind(android.content.Intent)
*/
@Override
public IBinder onBind(Intent intent)
{
return binder;
}
/**
* implement this function to decide what intent to be called when user clicks download notification
* <br>
* <br>
* ex)
* <br>
* <pre>
* protected Class<?> getIntentForLatestInfo()
* {
* return SomeActivity.class;
* }
* </pre>
*
* @return
*/
abstract protected Class<?> getIntentForLatestInfo();
/**
* implement this function to customize notification flag
* <br>
* <br>
* ex)
* <br>
* <pre>
* protected int getNotificationFlag()
* {
* return Notification.FLAG_AUTO_CANCEL | Notification.DEFAULT_LIGHTS;
* }
* </pre>
*
* @return
*/
abstract protected int getNotificationFlag();
/**
* implement this function to provide target files
* <br>
* <br>
* (HashMap's key = remote file path, value = local file path)
*
* @return
*/
abstract protected HashMap<String, String> getTargetFiles();
/**
* called when all downloads are finished
*
* @param successCount
* @param failedFiles
*/
abstract protected void onFinishDownload(int successCount, HashMap<String, String> failedFiles);
/**
*
* @return
*/
abstract protected int getNotificationIcon();
/**
* override this function to customize progress view on notification
* <br>
* <br>
* ex)
* <br>
* <pre>
* <?xml version="1.0" encoding="utf-8"?>
* <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
* android:orientation="horizontal" android:layout_width="fill_parent"
* android:layout_height="fill_parent" android:padding="3dp">
* <ImageView android:id="@+id/image" android:layout_width="30dip"
* android:layout_height="30dip" android:layout_marginRight="10dp" />
* <LinearLayout android:orientation="vertical"
* android:layout_width="fill_parent" android:layout_height="fill_parent"
* android:padding="3dp">
* <ProgressBar android:id="@+id/progress"
* android:layout_width="200dip" android:layout_height="20dip"
* style="?android:attr/progressBarStyleHorizontal" android:max="100"
* android:progress="0" />
* <TextView android:id="@+id/text" android:layout_width="wrap_content"
* android:layout_height="20dip" android:textColor="#000" />
* </LinearLayout>
* </LinearLayout>
* </pre>
* and
* <br>
* <br>
* <pre>
* protected RemoteViews getProgressView(int currentNumFile, int totalNumFiles, int currentReceivedBytes, int totalNumBytes)
* {
* RemoteViews contentView = new RemoteViews(getPackageName(), R.layout.progress);
* contentView.setImageViewResource(R.id.image, R.drawable.icon);
* contentView.setTextViewText(R.id.text, String.format("Progress (%d / %d)", currentNumFile, totalNumFiles));
* contentView.setProgressBar(R.id.progress, 100, 100 * currentReceivedBytes / totalNumBytes, false);
* return contentView;
* }
* </pre>
*
* @param currentNumFile
* @param totalNumFiles
* @param currentReceivedBytes
* @param totalNumBytes
* @return
*/
protected RemoteViews getProgressView(int currentNumFile, int totalNumFiles, int currentReceivedBytes, int totalNumBytes)
{
return null;
}
/**
*
* @param title
* @param content
*/
protected void showNotification(String ticker, String title, String content)
{
Notification notification = new Notification(getNotificationIcon(), ticker, System.currentTimeMillis());
PendingIntent contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, getIntentForLatestInfo()), Intent.FLAG_ACTIVITY_CLEAR_TOP);
notification.setLatestEventInfo(getApplicationContext(), title, content, contentIntent);
notification.flags = getNotificationFlag();
notificationManager.notify(SERVICE_ID, notification);
}
/**
*
* @param remoteView
* @param ticker
*/
protected void showNotification(RemoteViews remoteView, String ticker)
{
Notification notification = new Notification(getNotificationIcon(), ticker, System.currentTimeMillis());
notification.contentView = remoteView;
notification.contentIntent = PendingIntent.getActivity(this, 0, new Intent(this, getIntentForLatestInfo()), Intent.FLAG_ACTIVITY_CLEAR_TOP);
notification.flags = getNotificationFlag();
notificationManager.notify(SERVICE_ID, notification);
}
/**
* override this function to alter socket connect timeout value
* @return
*/
protected int getConnectTimeout()
{
return 1000;
}
/**
* override this function to alter socket read timeout value
* @return
*/
protected int getReadTimeout()
{
return 1000;
}
/**
*
* AsyncTask for downloading multiple files
*
* @author meinside@gmail.com
* @since 10.11.05.
*
* last update 11.03.13.
*
*/
private class AsyncDownloadTask extends AsyncTask<Void, Void, Void>
{
private int successCount;
private int numTotalFiles;
private HashMap<String, String> targetFiles = null;
private HashMap<String, String> failedFiles = null;
@Override
protected void onPreExecute()
{
super.onPreExecute();
successCount = 0;
targetFiles = getTargetFiles();
numTotalFiles = targetFiles.size();
failedFiles = new HashMap<String, String>();
}
/**
* get file's size at given url (using http header)
*
* @param url
* @return -1 when failed
*/
public int getFileSizeAtURL(URL url)
{
int filesize = -1;
try
{
HttpURLConnection http = (HttpURLConnection)url.openConnection();
filesize = http.getContentLength();
http.disconnect();
}
catch(Exception e)
{
//Logger.e(e.toString());
}
return filesize;
}
/* (non-Javadoc)
* @see android.os.AsyncTask#doInBackground(Params[])
*/
@Override
protected Void doInBackground(Void... params)
{
String remoteFilepath, localFilepath;
for(Entry<String, String> entry: targetFiles.entrySet())
{
remoteFilepath = entry.getKey();
localFilepath = entry.getValue();
// Logger.v("downloading: '" + remoteFilepath + "' => '" + localFilepath + "'");
try
{
if(isCancelled())
return null;
URL url = new URL(remoteFilepath);
int filesize = getFileSizeAtURL(url);
int loopCount = 0;
if(filesize > 0)
{
URLConnection connection = url.openConnection();
connection.setConnectTimeout(getConnectTimeout());
connection.setReadTimeout(getReadTimeout());
BufferedInputStream bis = new BufferedInputStream(connection.getInputStream());
FileOutputStream fos = new FileOutputStream(new File(localFilepath));
int bytesRead, totalBytesRead = 0;
byte[] bytes = new byte[BYTES_BUFFER_SIZE];
String progress, kbytes;
while(!isCancelled() && (bytesRead = bis.read(bytes)) != -1)
{
totalBytesRead += bytesRead;
fos.write(bytes, 0, bytesRead);
//don't show notification too often
if(!isCancelled() && loopCount++ % 20 == 0)
{
RemoteViews progressView = getProgressView(successCount + 1, numTotalFiles, totalBytesRead, filesize);
if(progressView == null)
{
progress = String.format("Download Progress (%d / %d)", successCount + 1, numTotalFiles);
kbytes = String.format("%s / %s", getStringByteSize(totalBytesRead), getStringByteSize(filesize));
if(!isCancelled())
showNotification("Downloading File(s)", progress , kbytes);
}
else
{
if(!isCancelled())
showNotification(progressView, "Downloading File(s)");
}
}
}
fos.close();
bis.close();
if(isCancelled())
return null;
successCount ++;
}
else
{
// Logger.i("file size unknown for remote file: " + remoteFilepath);
failedFiles.put(remoteFilepath, localFilepath);
}
}
catch(Exception e)
{
// Logger.e(e.toString());
showNotification("Download Failed", "Download Progress", "Failed: " + (new File(remoteFilepath)).getName());
failedFiles.put(remoteFilepath, localFilepath);
}
}
return null;
}
@Override
protected void onCancelled()
{
super.onCancelled();
// Logger.v("download task cancelled");
showNotification("Download Cancelled", "Download Progress", "Cancelled");
}
@Override
protected void onPostExecute(Void result)
{
super.onPostExecute(result);
onFinishDownload(successCount, failedFiles);
String finished;
if(successCount != numTotalFiles)
finished = String.format("Finished (%d download(s) failed)", numTotalFiles - successCount);
else
finished = "Finished";
showNotification("Download Finished", "Download Progress", finished);
// Logger.v("download task finished");
}
}
/**
*
* @param size
* @return
*/
protected String getStringByteSize(int size)
{
if(size > 1024 * 1024) //mega
{
return String.format("%.1f MB", size / (float)(1024 * 1024));
}
else if(size > 1024) //kilo
{
return String.format("%.1f KB", size / 1024.0f);
}
else
{
return String.format("%d B");
}
}
}
Related examples in the same category