/** * 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.activity; import android.annotation.TargetApi; import android.app.ActionBar; import android.os.Build; import android.provider.Settings; import android.support.annotation.NonNull; import android.support.v4.widget.SwipeRefreshLayout; import android.support.v7.app.ActionBarActivity; import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; import android.graphics.Color; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.webkit.WebChromeClient; import android.webkit.WebView; import android.webkit.WebViewClient; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.ImageButton; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.Toast; import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; import com.bakerframework.baker.BakerApplication; import com.bakerframework.baker.R; import com.bakerframework.baker.adapter.IssueAdapter; import com.bakerframework.baker.events.DownloadIssueErrorEvent; import com.bakerframework.baker.events.IssueCollectionLoadedEvent; import com.bakerframework.baker.events.ParseBookJsonCompleteEvent; import com.bakerframework.baker.events.ParseBookJsonErrorEvent; import com.bakerframework.baker.model.*; import com.bakerframework.baker.model.RemoteIssueCollection; import com.bakerframework.baker.settings.Configuration; import com.bakerframework.baker.settings.SettingsActivity; import com.bakerframework.baker.view.IssueCardView; import com.bakerframework.baker.view.ShelfView; import com.path.android.jobqueue.JobManager; import org.json.JSONException; import org.solovyev.android.checkout.ActivityCheckout; import org.solovyev.android.checkout.BillingRequests; import org.solovyev.android.checkout.Checkout; import org.solovyev.android.checkout.Purchase; import org.solovyev.android.checkout.RequestListener; import org.solovyev.android.checkout.ResponseCodes; import org.solovyev.android.checkout.Sku; import java.util.ArrayList; import java.util.List; import de.greenrobot.event.EventBus; public class ShelfActivity extends ActionBarActivity implements SwipeRefreshLayout.OnRefreshListener { public static final int STANDALONE_MAGAZINE_ACTIVITY_FINISH = 1; static final int SHELF_CHECKOUT_REQUEST_CODE = 0XCAFE; // Issues private ShelfView shelfView; private IssueAdapter issueAdapter; private IssueCollection issueCollection; // Features private SwipeRefreshLayout swipeRefreshLayout; private DrawerLayout drawerLayout; private ListView drawerList; // Billing private ActivityCheckout shelfCheckout; public ActivityCheckout getShelfCheckout() { return shelfCheckout; } // Jobs JobManager jobManager; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Initialize preferences PreferenceManager.setDefaultValues(this, R.xml.preferences, false); // Initialize tutorial if(Configuration.getPrefFirstTimeRun()) { Log.d(this.getClass().getName(), "First time app running, launching tutorial."); showAppUsage(); } // Initialize jobs jobManager = BakerApplication.getInstance().getJobManager(); EventBus.getDefault().register(this); // Initialize issue collection issueCollection = BakerApplication.getInstance().getIssueCollection(); // Initialize issue adapter for shelf view issueAdapter = new IssueAdapter(this, issueCollection); // Render View this.setContentView(R.layout.shelf_activity); // Initialize Features setupHeader(); loadBackground(); setupSwipeLayout(); setupActionBar(); setupCategoryDrawer(); // Fade in animation View view = findViewById(android.R.id.content); Animation mLoadAnimation = AnimationUtils.loadAnimation(getApplicationContext(), android.R.anim.fade_in); mLoadAnimation.setDuration(2000); view.startAnimation(mLoadAnimation); // Initialize shelf view shelfView = (ShelfView) findViewById(R.id.shelf_view); shelfView.setAdapter(issueAdapter); issueAdapter.updateIssues(); // Update category drawer updateCategoryDrawer(issueCollection.getCategories(), issueAdapter.getCategoryIndex()); // Continue downloads unzipPendingPackages(); // Checkout if(!Configuration.isStandaloneMode()) { shelfCheckout = Checkout.forActivity(this, BakerApplication.getInstance().getCheckout()); shelfCheckout.start(); shelfCheckout.createPurchaseFlow(SHELF_CHECKOUT_REQUEST_CODE, new PurchaseListener()); } // Plugin Callback BakerApplication.getInstance().getPluginManager().onShelfActivityCreated(this); } public void openConnectionDialog() { new AlertDialog.Builder(this) .setTitle(getString(R.string.msg_no_connection_title)) .setMessage(getString(R.string.msg_no_connection_message)) .setPositiveButton(getString(R.string.msg_yes), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { ShelfActivity.this.startActivity(new Intent(Settings.ACTION_WIFI_SETTINGS)); } }) .setNegativeButton(getString(R.string.msg_no), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { swipeRefreshLayout.setRefreshing(false); dialog.cancel(); } }).show(); } private class PurchaseListener extends BaseRequestListener<Purchase> { @Override public void onSuccess(@NonNull Purchase purchase) { onPurchased(); } private void onPurchased() { // let's update purchase information in local inventory Toast.makeText(getApplicationContext(), getString(R.string.msg_purchase_complete), Toast.LENGTH_SHORT).show(); ShelfActivity.this.onRefresh(); } @Override public void onError(int response, @NonNull Exception e) { // it is possible that our data is not synchronized with data on Google Play => need to handle some errors if (response == ResponseCodes.ITEM_ALREADY_OWNED) { onPurchased(); } else { super.onError(response, e); } } } private abstract class BaseRequestListener<Request> implements RequestListener<Request> { @Override public void onError(int response, @NonNull Exception e) { // @TODO: add alert dialog or logging Log.e("ShelfActivity", e.getMessage()); } } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) protected void setupSwipeLayout() { swipeRefreshLayout = (SwipeRefreshLayout) findViewById(R.id.swipe_container); swipeRefreshLayout.setOnRefreshListener(this); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.shelf, menu); return super.onCreateOptionsMenu(menu); } @Override public boolean onPrepareOptionsMenu(Menu menu) { // Show / Hide subscription menu if(getResources().getStringArray(R.array.google_play_subscription_ids).length > 0) { menu.getItem(0).setVisible(true); } return super.onPrepareOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { final int itemId = item.getItemId(); if (itemId == R.id.action_info) { Intent intent = new Intent(this, InfoActivity.class); intent.putExtra(IssueActivity.MODAL_URL, getString(R.string.asset_url_info)); startActivity(intent); return true; } else if (itemId == R.id.action_settings) { Intent settingsIntent = new Intent(this, SettingsActivity.class); startActivity(settingsIntent); return true; } else if (itemId == R.id.action_refresh) { swipeRefreshLayout.setRefreshing(true); this.onRefresh(); return true; } else if (itemId == R.id.action_subscribe) { if (BakerApplication.getInstance().isNetworkConnected()) { final List<Sku> subscriptionSkus = ((RemoteIssueCollection) issueCollection).getSubscriptionSkus(); if(subscriptionSkus == null || subscriptionSkus.size() == 0) { return false; }else if(subscriptionSkus.size() > 1) { final String[] subscriptionItems = new String[subscriptionSkus.size()]; for(int i = 0; i < subscriptionSkus.size(); i++) { subscriptionItems[i] = subscriptionSkus.get(i).title; } new AlertDialog.Builder(this) .setTitle("Choose a subscription") .setItems(subscriptionItems, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, final int which) { shelfCheckout.whenReady(new Checkout.ListenerAdapter() { @Override public void onReady(@NonNull BillingRequests requests) { onSubscriptionClicked(requests, subscriptionSkus.get(which)); } }); } }).show(); }else{ shelfCheckout.whenReady(new Checkout.ListenerAdapter() { @Override public void onReady(@NonNull BillingRequests requests) { onSubscriptionClicked(requests, subscriptionSkus.get(0)); } }); } }else { this.openConnectionDialog(); } return true; } else { return super.onContextItemSelected(item); } } private void onSubscriptionClicked(BillingRequests requests, Sku subscriptionSku) { requests.purchase(subscriptionSku, Configuration.getUserId(), shelfCheckout.getPurchaseFlow()); BakerApplication.getInstance().getPluginManager().onSubscribeClicked(subscriptionSku); } public void presentIssues() { // Update Shelf Spinner swipeRefreshLayout.setRefreshing(false); // Update category drawer updateCategoryDrawer(issueCollection.getCategories(), issueAdapter.getCategoryIndex()); // Update shelf view adapter issueAdapter.updateIssues(); } private void loadBackground() { WebView webview = (WebView) findViewById(R.id.backgroundWebView); webview.getSettings().setJavaScriptEnabled(true); webview.setBackgroundColor(Color.WHITE); webview.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } }); webview.loadUrl(getString(R.string.asset_url_background)); } @TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH) private void setupCategoryDrawer() { // Create layout drawerLayout = (DrawerLayout) findViewById(R.id.drawer_layout); drawerLayout.setDrawerShadow(R.drawable.drawer_shadow, GravityCompat.START); // Create list drawerList = (ListView) findViewById(R.id.left_drawer); drawerList.setOnItemClickListener(new DrawerItemClickListener()); // Set up drawer toggle Button drawerToggle = (Button) findViewById(R.id.category_toggle); drawerToggle.setOnClickListener(new CategoryToggleClickListener()); } private void updateCategoryDrawer(List<String> categories, int position) { if(categories == null || categories.size() == 0) { findViewById(R.id.category_toggle).setVisibility(View.GONE); }else{ drawerList.setAdapter(new ArrayAdapter<>(this, R.layout.drawer_list_item, categories)); drawerList.setItemChecked(position, true); } } private void setupHeader() { WebView webview = (WebView) findViewById(R.id.headerView); webview.getSettings().setJavaScriptEnabled(true); webview.getSettings().setUseWideViewPort(true); webview.setOnTouchListener(new View.OnTouchListener() { @Override public boolean onTouch(View view, MotionEvent motionEvent) { return true; } }); webview.setBackgroundColor(Color.TRANSPARENT); webview.setWebChromeClient(new WebChromeClient()); webview.loadUrl(getString(R.string.asset_url_header)); } private void setupActionBar() { android.support.v7.app.ActionBar actionBar = getSupportActionBar(); actionBar.setDisplayOptions(ActionBar.DISPLAY_SHOW_CUSTOM); actionBar.setCustomView(R.layout.shelf_actionbar); } public void viewIssue(final BookJson book) { Intent intent = new Intent(ShelfActivity.this, IssueActivity.class); try { intent.putExtra(Configuration.BOOK_JSON_KEY, book.toJSON().toString()); intent.putExtra(Configuration.ISSUE_NAME, book.getMagazineName()); startActivityForResult(intent, STANDALONE_MAGAZINE_ACTIVITY_FINISH); } catch (JSONException e) { Toast.makeText(this, "The book.json is invalid.", Toast.LENGTH_LONG).show(); } } public void showAppUsage() { BookJson book = new BookJson(); book.setIssueName(this.getString(R.string.path_tutorial_directory)); List<String> contents = new ArrayList<>(); String[] pages = this.getResources().getStringArray(R.array.list_tutorial_pages); for (String page : pages) { page = page.trim(); contents.add(page); } book.setContents(contents); book.setOrientation("portrait"); Intent intent = new Intent(this, IssueActivity.class); try { intent.putExtra(Configuration.BOOK_JSON_KEY, book.toJSON().toString()); intent.putExtra(Configuration.ISSUE_NAME, book.getMagazineName()); intent.putExtra(Configuration.ISSUE_RETURN_TO_SHELF, true); intent.putExtra(Configuration.ISSUE_ENABLE_DOUBLE_TAP, false); intent.putExtra(Configuration.ISSUE_ENABLE_BACK_NEXT_BUTTONS, true); intent.putExtra(Configuration.ISSUE_ENABLE_TUTORIAL, true); startActivityForResult(intent, STANDALONE_MAGAZINE_ACTIVITY_FINISH); } catch (JSONException e) { Log.e(this.getClass().getName(), "Error parsing book json", e); } } private void unzipPendingPackages() { if(shelfView != null) { for (int i = 0; i < shelfView.getChildCount(); i++) { IssueCardView issueCardView = (IssueCardView) shelfView.getChildAt(i); if(issueCardView.getIssue().isDownloaded() && !issueCardView.getIssue().isDownloading() && !issueCardView.getIssue().isExtracted() && !issueCardView.getIssue().isExtracting()) { // Continue issue extraction Log.d(this.getClass().toString(), "Continue extract of " + issueCardView.getIssue().getName()); issueCardView.extractZip(); } } } } @Override public void onBackPressed() { final List<Issue> downloadingIssues = issueCollection.getDownloadingIssues(); if (downloadingIssues.size() > 0) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder .setTitle(this.getString(R.string.msg_exit)) .setMessage(this.getString(R.string.msg_closing_app)) .setPositiveButton(this.getString(R.string.msg_yes), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { issueCollection.cancelDownloadingIssues(downloadingIssues); ShelfActivity.super.onBackPressed(); } }) .setNegativeButton(this.getString(R.string.msg_no), null) .show(); } else { super.onBackPressed(); } } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d(this.getClass().getName(), "MagazineActivity finished, resultCode: " + resultCode); if(requestCode == SHELF_CHECKOUT_REQUEST_CODE) { if(resultCode == RESULT_OK) { onRefresh(); }else{ Toast.makeText(getApplicationContext(), getString(R.string.err_purchase_not_possible), Toast.LENGTH_SHORT).show(); onRefresh(); } }else if (resultCode == STANDALONE_MAGAZINE_ACTIVITY_FINISH) { this.finish(); } } @Override public void onRefresh() { if (BakerApplication.getInstance().isNetworkConnected()) { issueCollection.load(); }else { this.openConnectionDialog(); } } @Override public void onDestroy() { super.onDestroy(); } // Categories /* The click listener for ListView in the navigation drawer */ private class DrawerItemClickListener implements ListView.OnItemClickListener { @Override public void onItemClick(AdapterView<?> parent, View view, int index, long id) { onCategorySelected(index); } } /* The click listener for the category toggle */ private class CategoryToggleClickListener implements ImageButton.OnClickListener { @Override public void onClick(View view) { if(drawerLayout.isDrawerOpen(Gravity.START)) { drawerLayout.closeDrawer(Gravity.START); }else{ drawerLayout.openDrawer(Gravity.START); } } } private void onCategorySelected(int index) { String category = issueCollection.getCategories().get(index); issueAdapter.setCategory(category); // Update category drawer UI drawerList.setItemChecked(index, true); ((Button) findViewById(R.id.category_toggle)).setText(category); // Close category drawer drawerLayout.closeDrawer(drawerList); } @SuppressWarnings("UnusedDeclaration") public void onEventMainThread(DownloadIssueErrorEvent event) { // Restore purchases this.onRefresh(); Toast.makeText(getApplicationContext(), getString(R.string.err_download_task_io), Toast.LENGTH_SHORT).show(); } @SuppressWarnings("UnusedDeclaration") public void onEventMainThread(ParseBookJsonCompleteEvent event) { // View magazine viewIssue(event.getBookJson()); } @SuppressWarnings("UnusedDeclaration") public void onEventMainThread(ParseBookJsonErrorEvent event) { Toast.makeText(this, "The book.json was not found for issue " + event.getIssue().getName(), Toast.LENGTH_LONG).show(); } @SuppressWarnings("UnusedDeclaration") public void onEventMainThread(IssueCollectionLoadedEvent event) { presentIssues(); } }