/**
* Copyright (c) 2013-2014. Francisco Contreras, Holland Salazar.
* Copyright (c) 2015. Tobias Strebitzer, Francisco Contreras, Holland Salazar.
* 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 the Baker Framework 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 HOLDER 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 com.bakerframework.baker.model;
import android.support.annotation.NonNull;
import android.util.Log;
import com.bakerframework.baker.BakerApplication;
import com.bakerframework.baker.R;
import com.bakerframework.baker.events.DownloadManifestCompleteEvent;
import com.bakerframework.baker.events.DownloadManifestErrorEvent;
import com.bakerframework.baker.events.FetchPurchasesCompleteEvent;
import com.bakerframework.baker.events.FetchPurchasesErrorEvent;
import com.bakerframework.baker.events.IssueCollectionErrorEvent;
import com.bakerframework.baker.events.IssueCollectionLoadedEvent;
import com.bakerframework.baker.helper.FileHelper;
import com.bakerframework.baker.jobs.DownloadManifestJob;
import com.bakerframework.baker.jobs.FetchPurchasesJob;
import com.bakerframework.baker.settings.Configuration;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.solovyev.android.checkout.Inventory;
import org.solovyev.android.checkout.Sku;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import de.greenrobot.event.EventBus;
import static org.solovyev.android.checkout.ProductTypes.IN_APP;
import static org.solovyev.android.checkout.ProductTypes.SUBSCRIPTION;
public class RemoteIssueCollection implements IssueCollection {
private final HashMap<String, Issue> issueMap;
private List<String> categories;
private List<Sku> subscriptionSkus;
// Tasks management
private DownloadManifestJob downloadManifestJob;
private FetchPurchasesJob fetchPurchasesJob;
// Data Processing
final String JSON_ENCODING = "utf-8";
final SimpleDateFormat SDF_INPUT = new SimpleDateFormat(BakerApplication.getInstance().getString(R.string.format_input_date), Locale.US);
final SimpleDateFormat SDF_OUTPUT = new SimpleDateFormat(BakerApplication.getInstance().getString(R.string.format_output_date), Locale.US);
// Categories
public static final String ALL_CATEGORIES_STRING = "All Categories";
// Billing
@NonNull
private Inventory inventory;
public RemoteIssueCollection() {
// Initialize issue map
issueMap = new HashMap<>();
EventBus.getDefault().register(this);
}
public List<String> getCategories() {
return categories;
}
public List<Sku> getSubscriptionSkus() {
return subscriptionSkus;
}
public List<String> getIssueProductIds() {
List<String> issueProductIdList = new ArrayList<>();
for(Issue issue : getIssues()) {
if(issue.getProductId() != null && !issue.getProductId().equals("")) {
issueProductIdList.add(issue.getProductId());
}
}
return issueProductIdList;
}
public List<Issue> getIssues() {
if(isLoading() || issueMap == null) {
return new ArrayList<>();
}else{
return new ArrayList<>(issueMap.values());
}
}
public Issue getIssueBySku(Sku sku) {
return getIssueByProductId(sku.id);
}
public Issue getIssueByProductId(String productId) {
for(Issue issue : getIssues()) {
if(issue.getProductId() != null && issue.getProductId().equals(productId)) {
return issue;
}
}
return null;
}
public boolean isLoading() {
return (downloadManifestJob != null && !downloadManifestJob.isCompleted()) || (fetchPurchasesJob != null && !fetchPurchasesJob.isCompleted());
}
// Reload data from backend
public void load() {
if (!isLoading()) {
if (BakerApplication.getInstance().isNetworkConnected()) {
// Online Mode: Reload issue collection
downloadManifestJob = new DownloadManifestJob(Configuration.getManifestUrl(), getCachedFile());
BakerApplication.getInstance().getJobManager().addJobInBackground(downloadManifestJob);
}else if(isCacheAvailable()) {
processManifestFile(getCachedFile());
}else{
EventBus.getDefault().post(new IssueCollectionErrorEvent(new Exception("No cached file available")));
}
}
}
public void processManifestFileFromCache() {
}
private void processManifestFile(File file) {
try {
// Create issues
processJson(FileHelper.getJsonArrayFromFile(file));
// Process categories
categories = extractAllCategories();
// you only need this if this activity needs information about purchases/SKUs
if(BakerApplication.getInstance().isNetworkConnected()) {
inventory = BakerApplication.getInstance().getCheckout().loadInventory();
inventory.whenLoaded(new InventoryLoadedListener());
inventory.load();
}
// Instantly trigger load event
EventBus.getDefault().post(new IssueCollectionLoadedEvent());
} catch (JSONException e) {
Log.e(this.getClass().getName(), "processing error (invalid json): " + e);
} catch (IOException e) {
Log.e(this.getClass().getName(), "processing error (buffer error): " + e);
} catch (ParseException e) {
Log.e(this.getClass().getName(), "processing error (parse error): " + e);
}
}
private void processJson(final JSONArray jsonArray) throws JSONException, ParseException, UnsupportedEncodingException {
JSONObject json;
JSONArray jsonCategories;
List<String> categories;
List<String> issueNameList = new ArrayList<>();
// Loop through issues
int length = jsonArray.length();
for (int i = 0; i < length; i++) {
json = new JSONObject(jsonArray.getString(i));
// Get issue data from json
String issueName = jsonString(json.getString("name"));
String issueProductId = json.isNull("product_id") ? null : jsonString(json.getString("product_id"));
String issueTitle = jsonString(json.getString("title"));
String issueInfo = jsonString(json.getString("info"));
String issueDate = jsonDate(json.getString("date"));
Date issueObjDate = jsonObjDate(json.getString("date"));
String issueCover = jsonString(json.getString("cover"));
String issueUrl = jsonString(json.getString("url"));
int issueSize = json.has("size") ? json.getInt("size") : 0;
Issue issue;
if(issueMap.containsKey(issueName)) {
// Get issue from issue map
issue = issueMap.get(issueName);
// Flag fields for update
if(!issue.getCover().equals(issueCover)) {
issue.setCoverChanged(true);
}
if(!issue.getUrl().equals(issueUrl)) {
issue.setUrlChanged(true);
}
}else{
// Create new issue and store in issue map
issue = new Issue(issueName);
issueMap.put(issueName, issue);
}
// Set issue data
issue.setTitle(issueTitle);
issue.setProductId(issueProductId);
issue.setInfo(issueInfo);
issue.setDate(issueDate);
issue.setObjDate(issueObjDate);
issue.setCover(issueCover);
issue.setUrl(issueUrl);
issue.setSize(issueSize);
// Set categories
if(json.has("categories")) {
jsonCategories = json.getJSONArray("categories");
categories = new ArrayList<>();
for (int j = 0; j < jsonCategories.length(); j++) {
categories.add(jsonCategories.get(j).toString());
}
issue.setCategories(categories);
}else{
issue.setCategories(new ArrayList<String>());
}
// Add name to issue name list
issueNameList.add(issueName);
}
// Get rid of old issues that are no longer in the manifest
for(Issue issue : issueMap.values()) {
if(!issueNameList.contains(issue.getName())) {
issueMap.remove(issue);
}
}
}
// Helpers
private String jsonDate(String value) throws ParseException {
return SDF_OUTPUT.format(SDF_INPUT.parse(value));
}
private Date jsonObjDate(String value) throws ParseException {
return SDF_INPUT.parse(value);
}
private String jsonString(String value) throws UnsupportedEncodingException {
if(value != null) {
return new String(value.getBytes(JSON_ENCODING), JSON_ENCODING);
}else{
return null;
}
}
private String getCachedPath() {
return Configuration.getCacheDirectory() + File.separator + BakerApplication.getInstance().getString(R.string.path_shelf);
}
private File getCachedFile() {
return new File(getCachedPath());
}
public boolean isCacheAvailable() {
return getCachedFile().exists() && getCachedFile().isFile();
}
public void updatePrices(Inventory.Products inventoryProducts, List<String> productIds) {
// Update google-play subscriptions
if(inventoryProducts != null) {
boolean hasSubscription = false;
subscriptionSkus = new ArrayList<>();
final Inventory.Product subscriptionProductCollection = inventoryProducts.get(SUBSCRIPTION);
if (subscriptionProductCollection.supported) {
for (Sku sku : subscriptionProductCollection.getSkus()) {
subscriptionSkus.add(sku);
}
}
// Update google-play purchased issues
final Inventory.Product inAppProductCollection = inventoryProducts.get(IN_APP);
if (inAppProductCollection.supported) {
// Update issue prices
for (Sku sku : inAppProductCollection.getSkus()) {
Issue issue = getIssueBySku(sku);
if(issue != null) {
// Check for subscription
issue.setPurchased(inAppProductCollection.isPurchased(sku));
issue.setSku(sku);
}
}
} else {
Log.e(getClass().getName(), "Error: " + R.string.err_purchase_not_possible);
}
}
// Update backend-purchased issues
if(productIds != null) {
for (String productId : productIds) {
Issue issue = getIssueByProductId(productId);
if(issue != null) {
issue.setPurchased(true);
}
}
}
}
public List<String> extractAllCategories() {
// Collect all categories from issues
List<String> allCategories = new ArrayList<>();
for(Issue issue : issueMap.values()) {
for(String category : issue.getCategories()) {
if(allCategories.indexOf(category) == -1) {
allCategories.add(category);
}
}
}
// Sort categories
Collections.sort(allCategories);
// Append all categories item
allCategories.add(0, ALL_CATEGORIES_STRING);
return allCategories;
}
public List<Issue> getDownloadingIssues() {
List<Issue> downloadingIssues = new ArrayList<>();
for (Issue issue : issueMap.values()) {
if(issue.isDownloading()) {
downloadingIssues.add(issue);
}
}
return downloadingIssues;
}
public void cancelDownloadingIssues(final List<Issue> downloadingIssues) {
for (Issue issue : downloadingIssues) {
if(issue.isDownloading()) {
issue.cancelDownloadJob();
}
}
}
public Issue getIssueByName(String issueName) {
return issueMap.get(issueName);
}
private class InventoryLoadedListener implements Inventory.Listener {
@Override
public void onLoaded(@NonNull Inventory.Products inventoryProducts) {
// Load existing purchases from backend
fetchPurchasesJob = new FetchPurchasesJob(Configuration.getManifestUrl());
BakerApplication.getInstance().getJobManager().addJobInBackground(fetchPurchasesJob);
}
}
// @SuppressWarnings("UnusedDeclaration")
public void onEventMainThread(DownloadManifestCompleteEvent event) {
processManifestFile(getCachedFile());
}
// @SuppressWarnings("UnusedDeclaration")
public void onEventMainThread(DownloadManifestErrorEvent event) {
Log.i("IssueCollection", "DownloadManifestErrorEvent");
if(isCacheAvailable()) {
processManifestFile(getCachedFile());
}else{
EventBus.getDefault().post(new IssueCollectionErrorEvent(new Exception("No cached file available")));
}
}
// @SuppressWarnings("UnusedDeclaration")
public void onEventMainThread(FetchPurchasesCompleteEvent event) {
// Set purchased issues
updatePrices(inventory.getProducts(), event.getFetchPurchasesResponse().issues);
// Trigger issues loaded event
EventBus.getDefault().post(new IssueCollectionLoadedEvent());
}
// @SuppressWarnings("UnusedDeclaration")
public void onEventMainThread(FetchPurchasesErrorEvent event) {
// Set purchased issues
updatePrices(inventory.getProducts(), null);
// Trigger issues loaded event
EventBus.getDefault().post(new IssueCollectionLoadedEvent());
}
public Inventory getInventory() {
return inventory;
}
}