package au.com.newint.newinternationalist; import android.net.Uri; import android.os.AsyncTask; import android.os.Parcel; import android.os.Parcelable; import android.util.Log; import android.util.SparseArray; import android.util.SparseIntArray; import com.google.gson.Gson; import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonNull; import com.google.gson.JsonObject; import com.google.gson.JsonParser; import org.apache.http.HttpResponse; import org.apache.http.client.ClientProtocolException; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.protocol.ClientContext; import org.apache.http.entity.StringEntity; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.message.BasicHeader; import org.apache.http.protocol.BasicHttpContext; import org.apache.http.protocol.HTTP; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.FileWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStream; import java.io.OutputStreamWriter; import java.io.StreamCorruptedException; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.net.MalformedURLException; import java.net.URL; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashMap; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; import au.com.newint.newinternationalist.util.Purchase; /** * Created by New Internationalist on 4/02/15. */ public class Issue implements Parcelable { /* String title; int id; int number; Date release; String editors_name; String editors_letter_html; URL editors_photo; URL cover; */ ArrayList<Article> articles; JsonObject issueJson; CacheStreamFactory coverCacheStreamFactory; CacheStreamFactory editorsImageCacheStreamFactory; // issues/<issueID>.json CacheStreamFactory articlesJSONCacheStreamFactory; SparseArray<ThumbnailCacheStreamFactory> thumbnailCacheStreamFactorySparseArray; public Issue(File jsonFile) throws StreamCorruptedException { this(Publisher.parseJsonFile(jsonFile).getAsJsonObject()); } public Issue(int issueID) { this(Publisher.getIssueJsonForId(issueID)); } public Issue(JsonObject issueJson) { this.issueJson = issueJson; coverCacheStreamFactory = FileCacheStreamFactory.createIfNecessary(getCoverLocationOnFilesystem(), new URLCacheStreamFactory(getCoverURL())); editorsImageCacheStreamFactory = FileCacheStreamFactory.createIfNecessary(getEditorsLetterLocationOnFilesystem(), new URLCacheStreamFactory(getEditorsPhotoURL())); thumbnailCacheStreamFactorySparseArray = new SparseArray<>(); // Get SITE_URL String siteURLString = (String) Helpers.getSiteURL(); URL articlesURL = null; try { articlesURL = new URL(siteURLString + "issues/" + this.getID() + ".json"); } catch (MalformedURLException e) { e.printStackTrace(); } File cacheDir = Helpers.getStorageDirectory(); File cacheFile = new File(cacheDir, this.getID() + ".json"); articlesJSONCacheStreamFactory = FileCacheStreamFactory.createIfNecessary(cacheFile, new URLCacheStreamFactory(articlesURL)); } public ArrayList<Article> buildArticlesFromDir (File dir) { Helpers.debugLog("Issue","buildArticlesFromDir("+dir+")"); ArrayList<Article> articlesArray = new ArrayList<Article>(); if (dir.exists()) { File[] files = dir.listFiles(); for (File file : files) { if (file.isDirectory()) { articlesArray.addAll(buildArticlesFromDir(file)); } else { // do something here with the file if (file.getName().equals("article.json")) { // Add to array try { articlesArray.add(new Article(file, this)); } catch (StreamCorruptedException e) { e.printStackTrace(); // we skip this file, it will be reloaded next time } } } } } return articlesArray; } public String getTitle() { return issueJson.get("title").getAsString(); } public int getID() { return issueJson.get("id").getAsInt(); } public int getNumber() { return issueJson.get("number").getAsInt(); } public Date getRelease() { return Publisher.parseDateFromString(issueJson.get("release").getAsString()); } public String getEditorsName() { return issueJson.get("editors_name").getAsString(); } public String getEditorsLetterHtml() { return issueJson.get("editors_letter_html").getAsString(); } public URL getEditorsPhotoURL() { try { String photoString = issueJson.get("editors_photo").getAsJsonObject().get("url").getAsString(); if (BuildConfig.DEBUG && Helpers.getSiteURL().contains("3000")) { // For running from a local Rails dev site photoString = Helpers.getSiteURL() + photoString; } return new URL(photoString); } catch (MalformedURLException e) { return null; } } private URL getCoverURL() { try { String coverString = issueJson.get("cover").getAsJsonObject().get("url").getAsString(); if (BuildConfig.DEBUG && Helpers.getSiteURL().contains("3000")) { // For running from a local Rails dev site coverString = Helpers.getSiteURL() + coverString; } return new URL(coverString); } catch (MalformedURLException e) { return null; } } public URL getWebURL() { try { return new URL(Helpers.getSiteURL() + "issues/" + getID()); } catch (MalformedURLException e) { return null; } } public File getCoverLocationOnFilesystem() { File issueDir = new File(Helpers.getStorageDirectory(), Integer.toString(getID())); String[] pathComponents = getCoverURL().getPath().split("/"); String coverFilename = pathComponents[pathComponents.length - 1]; return new File(issueDir, coverFilename); } public File getEditorsLetterLocationOnFilesystem() { File issueDir = new File(Helpers.getStorageDirectory(), Integer.toString(getID())); String[] pathComponents = getEditorsPhotoURL().getPath().split("/"); String editorsPhotoFilename = pathComponents[pathComponents.length - 1]; return new File(issueDir, editorsPhotoFilename); } public Uri getCoverUriOnFilesystem() { String issueDir = Helpers.getStorageDirectory() + Integer.toString(getID()); String[] pathComponents = getCoverURL().getPath().split("/"); String coverFilename = pathComponents[pathComponents.length - 1]; Uri.Builder uri = new Uri.Builder(); uri.appendPath(issueDir); uri.appendPath(coverFilename); return uri.build(); } public void preloadArticles() { preloadArticles(null); } public void preloadArticles(final CacheStreamFactory.CachePreloadCallback callback) { articlesJSONCacheStreamFactory.preload(new CacheStreamFactory.CachePreloadCallback() { @Override public void onLoad(byte[] payload) { if (callback != null) { callback.onLoad(payload); } } @Override public void onLoadBackground(byte[] payload) { JsonArray rootArray = null; ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(payload); InputStreamReader inputStreamReader = new InputStreamReader(byteArrayInputStream); JsonElement root = null; try { root = new JsonParser().parse(inputStreamReader); } catch (Exception e) { Log.e("Issue", "Preload articles got error: " + e); } if (root == null || root.isJsonNull()) { //TODO: got null json, now what? // doing nothing seems to work... Log.e("Issue", "Preload articles returned a JsonNull root."); } else { rootArray = root.getAsJsonObject().get("articles").getAsJsonArray(); // Save article.json for each article to the filesystem if (rootArray != null) { for (JsonElement aRootArray : rootArray) { JsonObject jsonObject = aRootArray.getAsJsonObject(); int id = jsonObject.get("id").getAsInt(); ArticleJsonCacheStreamFactory articleJsonCacheStreamFactory = new ArticleJsonCacheStreamFactory(id, Issue.this); OutputStream outputStream = articleJsonCacheStreamFactory.createCacheOutputStream(); //FIXME: this doesn't seem to create an actual output file... OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream); new Gson().toJson(jsonObject, outputStreamWriter); try { // paranoia... outputStreamWriter.flush(); outputStreamWriter.close(); outputStream.flush(); outputStream.close(); } catch (IOException e) { Log.e("Issue", "error closing articleJsonCacheStreamFactory output stream: " + e); } } } //TODO: articles should inform their issue when they are updated // make issue reload articles from disk Issue.this.articles = null; } if (callback != null) { callback.onLoadBackground(payload); } } }); } public ArrayList<Article> getArticles() { // articles is nulled by the DownloadArticlesJSONTask.onPostExecute in Publisher if (articles == null || articles.size() == 0) { // assumes that all articles have been downloaded.. File dir = new File(Helpers.getStorageDirectory() + "/" + Integer.toString(getID()) + "/"); articles = buildArticlesFromDir(dir); // TODO: Sort into sections by category Collections.sort(articles, new Comparator<Article>() { @Override public int compare(Article lhs, Article rhs) { return lhs.getPublication().compareTo(rhs.getPublication()); } }); } return articles; } public Article getArticleWithID(int articleID) { return new Article(articleID,this); } // public Article getArticleWithID(int articleID) { // Article articleMatched = null; // if (articles == null || articles.size() == 0) { // articles = getArticles(); // } // if (articles != null && articles.size() > 0) { // for (Article article : articles) { // if (article.getID() == articleID) { // articleMatched = article; // } // } // } // return articleMatched; // } public ThumbnailCacheStreamFactory getCoverCacheStreamFactoryForSize(int width) { //store in a hash, only make once for each width ThumbnailCacheStreamFactory tcsf = thumbnailCacheStreamFactorySparseArray.get(width); if (tcsf==null) { tcsf = new ThumbnailCacheStreamFactory(width, getCoverLocationOnFilesystem(), coverCacheStreamFactory); }/* // Register for DownloadComplete listener Publisher.ArticlesDownloadCompleteListener listener = new Publisher.ArticlesDownloadCompleteListener() { @Override public void onArticlesDownloadComplete(JsonArray articles) { Helpers.debugLog("ArticlesReady", "Received listener, refreshing articles view."); // Refresh adapter data adapter.notifyDataSetChanged(); Publisher.articleListeners.clear(); } }; Publisher.INSTANCE.setOnArticlesDownloadCompleteListener(listener); // Register for editors photo complete listener. // Register for DownloadComplete listener Publisher.UpdateListener editorImageListener = new Publisher.UpdateListener() { @Override public void onUpdate(Object object) { // Tell the adapter to update the footer view so it loads the editor image adapter.notifyItemChanged(adapter.getItemCount() - 1); } }; Publisher.INSTANCE.setOnDownloadCompleteListener(editorImageListener); */ return tcsf; } public ThumbnailCacheStreamFactory getEditorsImageCacheStreamFactoryForSize(int width, int height) { return new ThumbnailCacheStreamFactory(width, height, getEditorsLetterLocationOnFilesystem(), editorsImageCacheStreamFactory); } public boolean deleteCache() { File dir = new File(Helpers.getStorageDirectory().getPath() + "/" + this.getID()); boolean success = false; if (dir.isDirectory()) { String[] children = dir.list(); for (String child : children) { File childFile = new File(dir, child); if (childFile.isDirectory()) { String[] childDirChildren = childFile.list(); for (String childDirChild : childDirChildren) { // Delete everything! success = new File(childFile, childDirChild).delete(); } } else { // We used to delete everything apart from the .json // But then we can't get updates to the editor's letter // So deleting everything now // if (!child.contains("json")) { // // Don't delete the .json // success = new File(dir, child).delete(); // } // Delete all files, including issue.json success = new File(dir, child).delete(); } } } if (success) { // Recreate issue.json from issues.json Publisher.INSTANCE.recreateIssueJsonForId(this.getID()); } return success; } public URL getIssueJsonURL() { try { return new URL(Helpers.getSiteURL() + "issues/" + getID() + ".json"); } catch (MalformedURLException e) { return null; } } public void downloadZip(ArrayList<Purchase> purchases) { // Attempt to get zip URL from rails new DownloadZipTask().execute(purchases); } // Download body async task private class DownloadZipTask extends AsyncTask<Object, Integer, ArrayList> { @Override protected ArrayList doInBackground(Object... objects) { // Pass in purchases ArrayList<Purchase> purchases = (ArrayList<Purchase>) objects[0]; // Get the zip url from rails DefaultHttpClient httpclient = new DefaultHttpClient(); // Setup post request HttpContext ctx = new BasicHttpContext(); ctx.setAttribute(ClientContext.COOKIE_STORE, Publisher.INSTANCE.cookieStore); HttpPost post = new HttpPost(getIssueJsonURL().toString()); post.setHeader("Content-Type", "application/json"); // Add in-app purchase JSON data if (purchases != null && purchases.size() > 0) { JsonArray purchasesJsonArray = new JsonArray(); for (Purchase purchase : purchases) { // Send each purchase JSON data to rails to validate with Google Play JsonParser parser = new JsonParser(); JsonObject purchaseJsonObject = (JsonObject)parser.parse(purchase.getOriginalJson()); purchasesJsonArray.add(purchaseJsonObject); } StringEntity stringEntity = null; try { stringEntity = new StringEntity(purchasesJsonArray.toString()); stringEntity.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE, "application/json")); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } post.setEntity(stringEntity); } HttpResponse zipURLresponse = null; try { // Execute HTTP Post Request zipURLresponse = httpclient.execute(post, ctx); } catch (ClientProtocolException e) { Helpers.debugLog("TOC", "Zip URL download: ClientProtocolException: " + e); } catch (IOException e) { Helpers.debugLog("TOC", "Zip URL download: IOException: " + e); } int zipURLresponseStatusCode; boolean zipURLsuccess = false; ArrayList<Object> responseList = new ArrayList<>(); if (zipURLresponse != null) { // Add it to the response list responseList.add(zipURLresponse); zipURLresponseStatusCode = zipURLresponse.getStatusLine().getStatusCode(); if (zipURLresponseStatusCode >= 200 && zipURLresponseStatusCode < 300) { // We have the ZipURL JSON Helpers.debugLog("TOC", "Zip URL success: " + zipURLresponse.getStatusLine()); zipURLsuccess = true; } else if (zipURLresponseStatusCode > 400 && zipURLresponseStatusCode < 500) { // Zip request failed Helpers.debugLog("TOC", "Zip URL request failed with code: " + zipURLresponseStatusCode); } else { // Server error. Helpers.debugLog("TOC", "Zip URL request failed with code: " + zipURLresponseStatusCode + " and response: " + zipURLresponse.getStatusLine()); } } else { // Error getting zipURL Helpers.debugLog("TOC", "Zip URL request failed! Response is null"); } if (zipURLsuccess) { Helpers.debugLog("TOC", "Success! ZipURL found."); // Download the zip file String zipURLresponseJSONstring = ""; JsonObject zipURLJson; String zipURL = ""; HttpResponse zipFileResponse = null; try { zipURLresponseJSONstring = EntityUtils.toString(zipURLresponse.getEntity()); } catch (IOException e) { e.printStackTrace(); } if (zipURLresponseJSONstring != null && zipURLresponseJSONstring.length() > 0) { zipURLJson = new JsonParser().parse(zipURLresponseJSONstring).getAsJsonObject(); zipURL = zipURLJson.get("zipURL").getAsString(); } if (zipURL != null && zipURL.length() > 0) { // Setup post request HttpContext zipContext = new BasicHttpContext(); HttpGet zipGet = new HttpGet(zipURL); try { // Execute HTTP Post Request zipFileResponse = httpclient.execute(zipGet, zipContext); } catch (ClientProtocolException e) { Helpers.debugLog("TOC", "Zip file download: ClientProtocolException: " + e); } catch (IOException e) { Helpers.debugLog("TOC", "Zip file download: IOException: " + e); } } int zipFileResponseStatusCode; boolean zipFileSuccess = false; if (zipFileResponse != null) { // Add it to the response list responseList.add(zipFileResponse); zipFileResponseStatusCode = zipFileResponse.getStatusLine().getStatusCode(); if (zipFileResponseStatusCode >= 200 && zipFileResponseStatusCode < 300) { // We have the ZipURL JSON Helpers.debugLog("TOC", "Zip file success: " + zipFileResponse.getStatusLine()); zipFileSuccess = true; } else if (zipFileResponseStatusCode > 400 && zipFileResponseStatusCode < 500) { // Zip request failed Helpers.debugLog("TOC", "Zip file request failed with code: " + zipFileResponseStatusCode); } else { // Server error. Helpers.debugLog("TOC", "Zip file request failed with code: " + zipFileResponseStatusCode + " and response: " + zipFileResponse.getStatusLine()); } } else { Helpers.debugLog("TOC", "Zip file download response is null, sorry."); } if (zipFileSuccess) { // Unzip the zip and move it into place Helpers.debugLog("TOC", "Zip file: " + zipFileResponse); ZipInputStream zipInputStream = null; try { zipInputStream = new ZipInputStream(new BufferedInputStream(zipFileResponse.getEntity().getContent())); } catch (IOException e) { e.printStackTrace(); } File saveDir = new File(Helpers.getStorageDirectory() + "/" + Integer.toString(getID())); if (zipInputStream != null) { try { ZipEntry entry; while ((entry = zipInputStream.getNextEntry()) != null) { Helpers.debugLog("TOC","Zip file entry: " + entry.getName() + ", " + entry.getSize()); // If it's a directory, make it. String filename = entry.getName(); File file = new File(saveDir, filename); if (file.isDirectory()) { file.mkdir(); } else { // Read the bytes for this entry ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int count; while ((count = zipInputStream.read(buffer)) != -1) { baos.write(buffer, 0, count); } byte[] bytes = baos.toByteArray(); // Write the bytes to the filesystem FileOutputStream fos = new FileOutputStream(file); fos.write(bytes); fos.close(); } } } catch (IOException e) { e.printStackTrace(); } finally { try { zipInputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } } return responseList; } @Override protected void onPostExecute(ArrayList responseList) { super.onPostExecute(responseList); // Post listener Publisher.IssueZipDownloadCompleteListener listener = Publisher.INSTANCE.issueZipDownloadCompleteListener; if (listener != null) { listener.onIssueZipDownloadComplete(responseList); } } } // PARCELABLE delegate methods private Issue(Parcel in) { this(in.readInt()); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel dest, int flags) { dest.writeInt(this.getID()); } public static final Parcelable.Creator<Issue> CREATOR = new Parcelable.Creator<Issue>() { public Issue createFromParcel(Parcel in) { return new Issue(in); } public Issue[] newArray(int size) { return new Issue[size]; } }; }