Network Android

/*
 Copyright (c) 2010, Sungjin Han 
 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
 * 

 * 

 * - to start: startService(new Intent(context, SomeOverriddenClass.class));
 * 

 * - to stop: stopService(new Intent(context, SomeOverriddenClass.class));
 * 

 * 
 * @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
   * 

   * 

   * ex)
   * 

   * 

   *   protected Class getIntentForLatestInfo()
   *   {
   *     return SomeActivity.class;
   *   }
   * 

   * 
   * @return
   */
  abstract protected Class getIntentForLatestInfo();
  
  /**
   * implement this function to customize notification flag
   * 

   * 

   * ex)
   * 

   * 

   *   protected int getNotificationFlag()
   *   {
   *     return Notification.FLAG_AUTO_CANCEL | Notification.DEFAULT_LIGHTS;
   *   }
   * 

   * 
   * @return
   */
  abstract protected int getNotificationFlag();
  /**
   * implement this function to provide target files
   * 

   * 

   * (HashMap's key = remote file path, value = local file path)
   * 
   * @return
   */
  abstract protected HashMap getTargetFiles();
  /**
   * called when all downloads are finished
   * 
   * @param successCount
   * @param failedFiles
   */
  abstract protected void onFinishDownload(int successCount, HashMap failedFiles);
  /**
   * 
   * @return
   */
  abstract protected int getNotificationIcon();
  /**
   * override this function to customize progress view on notification
   * 

   * 

   * ex)
   * 

   * 

   * <?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>
   * 

   * and
   * 

   * 

   * 

   * 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;
   * }
   * 

   * 
   * @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
  {
    private int successCount;
    private int numTotalFiles;
    private HashMap targetFiles = null;
    private HashMap failedFiles = null;
    @Override
    protected void onPreExecute()
    {
      super.onPreExecute();
      successCount = 0;
      targetFiles = getTargetFiles();
      numTotalFiles = targetFiles.size();
      failedFiles = new HashMap();
    }
    /**
     * 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 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");
    }
  }
}