package edu.vanderbilt.cs282.feisele.assignment7;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.SocketTimeoutException;
import java.net.URL;
import java.net.UnknownHostException;
import org.jsoup.Connection;
import org.jsoup.HttpStatusException;
import org.jsoup.Jsoup;
import org.jsoup.UnsupportedMimeTypeException;
import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element;
import org.jsoup.select.Elements;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import android.content.ContentProviderClient;
import android.content.ContentValues;
import android.content.Intent;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import edu.vanderbilt.cs282.feisele.assignment7.DownloadContentProviderSchema.ImageTable;
import edu.vanderbilt.cs282.feisele.assignment7.DownloadContentProviderSchema.Selection;
/**
* The parent class for performing the work. The child classes implement the
* specific communication mechanism.
* <p>
*
* Given that, only one content provider is accessed and in the same process,
* two fast mechanisms are available for communicating with the content
* provider. The local binder and the content provider client.
*
*
* @author "Fred Eisele" <phreed@gmail.com>
*/
public class DownloadService extends LLService {
static private final Logger logger = LoggerFactory
.getLogger("class.service.download");
static private final int MINIMUM_WIDTH = 100;
static private final int MINIMUM_HEIGHT = 100;
/** this intent action indicates that the bind request is for a local binder */
public static final boolean ACTION_LOCAL = false;
/**
* The content provider client object is obtained.
*/
private ContentProviderClient cpc = null;
@Override
public void onCreate() {
super.onCreate();
this.cpc = this.getContentResolver().acquireContentProviderClient(
DownloadContentProviderSchema.AUTHORITY);
}
/**
* AIDL Stub
*/
final private DownloadRequest.Stub stub = new DownloadRequest.Stub() {
final DownloadService master = DownloadService.this;
/**
* The download image method acquires the images and loads them into the
* content provider.
*/
public void downloadImage(Uri uri, DownloadCallback callback)
throws RemoteException {
logger.info("download request received {}", uri);
try {
final String url = master.downloadImages(master, uri);
callback.sendPath(url);
} catch (FileNotFoundException ex) {
logger.info("file not found ", ex);
callback.sendFault("file not found :");
} catch (FailedDownload ex) {
logger.info("download failed ", ex);
callback.sendFault("download failed");
} catch (IOException ex) {
logger.info("io failed ", ex);
callback.sendFault("not implemented by this service");
}
}
};
/**
* The onBind() is called after construction and onCreate().
*/
@Override
public IBinder onBind(Intent intent) {
logger.debug("sync service on bind");
if (DownloadService.ACTION_LOCAL) {
return this.localBinder;
}
return this.stub;
}
/**
* Used when making local connections to the service.
*/
private final IBinder localBinder = new LocalBinder();
public class LocalBinder extends Binder {
DownloadService getService() {
return DownloadService.this;
}
}
/**
* The workhorse for the class. Download the provided image uri. If there is
* a problem an exception is raised and the calling method is expected to
* handle it in an appropriate manner.
* <p>
* The uri is presumed to be an http page containing elements like the
* following: <code>
* <img
* src="http://somethingscrawlinginmyhair.com/wp-content/uploads/2012/10/Wasp.spider.and_.nearby.male_.jpg"
* alt="" title="Wasp.spider.and.nearby.male"
* width="600"
* height="334"
* class="alignnone size-full wp-image-7431" />
* </code>
*
* <p>
* The url is indicated by the
* <p>
* An allowance is made for a uri of a content provider, which have a shema
* of "content".
*
* @param master
*
* @param uri
* the thing to download
*/
protected String downloadImages(DownloadService master, Uri uri)
throws FailedDownload, FileNotFoundException, IOException {
logger.debug("download images:");
master.clearImages(uri);
final String mainUrl = uri.toString();
final String scheme = uri.getScheme();
if ("http".equals(scheme)) {
try {
final Connection channel = Jsoup.connect(mainUrl);
final Document doc = channel.get();
final String title = doc.title();
final Elements imageUrlSet = doc.select("img[src]");
int ordinal = 1;
for (Element imageUrlElement : imageUrlSet) {
final String imageUrlStr = imageUrlElement.attr("src");
try {
final URL imageUrl = new URL(imageUrlStr);
final InputStream is = imageUrl.openStream();
final Bitmap bitmap = BitmapFactory.decodeStream(is);
logger.debug("bitmap size [{}:{}]", bitmap.getWidth(),
bitmap.getHeight());
if (bitmap.getWidth() < MINIMUM_WIDTH) {
logger.warn("bitmap too narrow {} px", bitmap.getWidth());
continue;
}
if (bitmap.getHeight() < MINIMUM_HEIGHT) {
logger.warn("bitmap too short {} px", bitmap.getHeight());
continue;
}
master.storeBitmap(uri, ordinal, bitmap);
ordinal++;
} catch (UnknownHostException ex) {
logger.warn("download failed bad host {}", imageUrlStr, ex);
} catch (MalformedURLException ex) {
logger.warn(
"the request URL is not a HTTP or HTTPS URL, or is otherwise malformed {}",
imageUrlStr, ex);
} catch (HttpStatusException ex) {
logger.warn(
"the response is not OK and HTTP response errors are not ignored {}",
imageUrlStr, ex);
} catch (UnsupportedMimeTypeException ex) {
logger.warn(
"the response mime type is not supported and those errors are not ignored {}",
imageUrlStr, ex);
} catch (SocketTimeoutException ex) {
logger.warn("the connection times out {}", imageUrlStr, ex);
} catch (IOException ex) {
logger.warn("download failed {}", imageUrlStr, ex);
}
}
logger.info("loaded page=<{}>", title);
return mainUrl;
} catch (UnknownHostException ex) {
logger.warn("download failed bad host {}", uri, ex);
throw new FailedDownload(uri, this.getResources().getText(
R.string.error_downloading_url));
} catch (MalformedURLException ex) {
logger.warn(
"the request URL is not a HTTP or HTTPS URL, or is otherwise malformed {}",
uri, ex);
throw new FailedDownload(uri, this.getResources().getText(
R.string.error_downloading_url));
} catch (HttpStatusException ex) {
logger.warn(
"the response is not OK and HTTP response errors are not ignored {}",
uri, ex);
throw new FailedDownload(uri, this.getResources().getText(
R.string.error_downloading_url));
} catch (UnsupportedMimeTypeException ex) {
logger.warn(
"the response mime type is not supported and those errors are not ignored {}",
uri, ex);
throw new FailedDownload(uri, this.getResources().getText(
R.string.error_downloading_url));
} catch (SocketTimeoutException ex) {
logger.warn("the connection times out {}", uri, ex);
throw new FailedDownload(uri, this.getResources().getText(
R.string.error_downloading_url));
} catch (IOException ex) {
logger.warn("download failed {}", uri, ex);
throw new FailedDownload(uri, this.getResources().getText(
R.string.error_downloading_url));
}
} else {
}
return mainUrl;
}
/**
* clear out any prior images having the same uri.
*
* @param uri
*/
private void clearImages(Uri uri) {
try {
this.cpc.delete(ImageTable.CONTENT_URI,
Selection.BY_URI.code,
new String[] { uri.toString() });
} catch (RemoteException ex) {
logger.error("could not expunge the old values {}", ex);
}
}
protected void storeBitmap(final Uri uri, final int ordinal, final Bitmap bitmap) {
try {
final ByteArrayOutputStream outBytes = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 40, outBytes);
final ContentValues cv = new ContentValues();
cv.put(ImageTable.ORDINAL.title, ordinal);
cv.put(ImageTable.URI.title, uri.toString());
final Uri tupleUri = this.cpc.insert(ImageTable.CONTENT_URI, cv);
final ParcelFileDescriptor pfd = this.cpc.openFile(tupleUri, "w");
final FileOutputStream fileOutputStream = new FileOutputStream(
pfd.getFileDescriptor());
fileOutputStream.write(outBytes.toByteArray());
fileOutputStream.close();
outBytes.close();
} catch (IOException ex) {
logger.error("could not write bitmap file {}", uri, ex);
} catch (RemoteException ex) {
logger.error("remote exception write bitmap file {}", uri, ex);
}
}
/**
* An exception class used when there is a problem with the download.
*/
public static class FailedDownload extends Exception {
private static final long serialVersionUID = 6673968049922918951L;
final public Uri uri;
final public CharSequence msg;
@Override
public String getMessage() {
return new StringBuilder().append("uri=<").append(this.uri)
.append(">").append(" msg=<").append(this.msg).append(">")
.toString();
}
public FailedDownload(final Uri uri, final CharSequence msg) {
super();
this.uri = uri;
this.msg = msg;
}
}
}