diff --git a/app/build.gradle b/app/build.gradle
index 300ff19..8ba7bec 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -48,8 +48,21 @@ android {
}
dependencies {
+ compile project(path: ':jsword-minimalbible', configuration: 'buildJSword')
+
compile 'com.squareup.dagger:dagger:1.2.1'
provided 'com.squareup.dagger:dagger-compiler:1.2.1'
+ compile 'com.jakewharton:butterknife:5.0.1'
+
+ compile 'de.devland.esperandro:esperandro-api:1.1.2'
+ provided 'de.devland.esperandro:esperandro:1.1.2'
+
+ compile 'com.readystatesoftware.systembartint:systembartint:1.0.3'
+ compile 'com.netflix.rxjava:rxjava-android:0.19.0'
+
+ androidTestCompile 'com.jayway.awaitility:awaitility:1.6.0'
+ androidTestProvided 'com.squareup.dagger:dagger-compiler:1.2.0'
+
compile 'com.android.support:appcompat-v7:20.+'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3292541..57cab64 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,7 +9,7 @@
android:theme="@style/AppTheme"
android:name=".MinimalBible" >
@@ -19,4 +19,6 @@
+
+
diff --git a/app/src/main/java/com/todddavies/components/progressbar/ProgressWheel.java b/app/src/main/java/com/todddavies/components/progressbar/ProgressWheel.java
new file mode 100644
index 0000000..224d212
--- /dev/null
+++ b/app/src/main/java/com/todddavies/components/progressbar/ProgressWheel.java
@@ -0,0 +1,530 @@
+package com.todddavies.components.progressbar;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Style;
+import android.graphics.RectF;
+import android.graphics.Shader;
+import android.os.Handler;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.view.View;
+
+import org.bspeice.minimalbible.R;
+
+
+/**
+ * An indicator of progress, similar to Android's ProgressBar.
+ * Can be used in 'spin mode' or 'increment mode'
+ *
+ * @author Todd Davies
+ *
+ * Licensed under the Creative Commons Attribution 3.0 license see:
+ * http://creativecommons.org/licenses/by/3.0/
+ */
+public class ProgressWheel extends View {
+
+ //Sizes (with defaults)
+ private int layout_height = 0;
+ private int layout_width = 0;
+ private int fullRadius = 100;
+ private int circleRadius = 80;
+ private int barLength = 60;
+ private int barWidth = 20;
+ private int rimWidth = 20;
+ private int textSize = 20;
+ private float contourSize = 0;
+
+ //Padding (with defaults)
+ private int paddingTop = 5;
+ private int paddingBottom = 5;
+ private int paddingLeft = 5;
+ private int paddingRight = 5;
+
+ //Colors (with defaults)
+ private int barColor = 0xAA000000;
+ private int contourColor = 0xAA000000;
+ private int circleColor = 0x00000000;
+ private int rimColor = 0xAADDDDDD;
+ private int textColor = 0xFF000000;
+
+ //Paints
+ private Paint barPaint = new Paint();
+ private Paint circlePaint = new Paint();
+ private Paint rimPaint = new Paint();
+ private Paint textPaint = new Paint();
+ private Paint contourPaint = new Paint();
+
+ //Rectangles
+ @SuppressWarnings("unused")
+ private RectF rectBounds = new RectF();
+ private RectF circleBounds = new RectF();
+ private RectF circleOuterContour = new RectF();
+ private RectF circleInnerContour = new RectF();
+
+ //Animation
+ //The amount of pixels to move the bar by on each draw
+ private int spinSpeed = 2;
+ //The number of milliseconds to wait inbetween each draw
+ private int delayMillis = 0;
+ private Handler spinHandler = new Handler() {
+ /**
+ * This is the code that will increment the progress variable
+ * and so spin the wheel
+ */
+ @Override
+ public void handleMessage(Message msg) {
+ invalidate();
+ if (isSpinning) {
+ progress += spinSpeed;
+ if (progress > 360) {
+ progress = 0;
+ }
+ spinHandler.sendEmptyMessageDelayed(0, delayMillis);
+ }
+ //super.handleMessage(msg);
+ }
+ };
+ int progress = 0;
+ boolean isSpinning = false;
+
+ //Other
+ private String text = "";
+ private String[] splitText = {};
+
+ /**
+ * The constructor for the ProgressWheel
+ *
+ * @param context
+ * @param attrs
+ */
+ public ProgressWheel(Context context, AttributeSet attrs) {
+ super(context, attrs);
+
+ parseAttributes(context.obtainStyledAttributes(attrs,
+ R.styleable.ProgressWheel));
+ }
+
+ //----------------------------------
+ //Setting up stuff
+ //----------------------------------
+
+ /*
+ * When this is called, make the view square.
+ * From: http://www.jayway.com/2012/12/12/creating-custom-android-views-part-4-measuring-and-how-to-force-a-view-to-be-square/
+ *
+ */
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // The first thing that happen is that we call the superclass
+ // implementation of onMeasure. The reason for that is that measuring
+ // can be quite a complex process and calling the super method is a
+ // convenient way to get most of this complexity handled.
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+ // We can’t use getWidth() or getHight() here. During the measuring
+ // pass the view has not gotten its final size yet (this happens first
+ // at the start of the layout pass) so we have to use getMeasuredWidth()
+ // and getMeasuredHeight().
+ int size = 0;
+ int width = getMeasuredWidth();
+ int height = getMeasuredHeight();
+ int widthWithoutPadding = width - getPaddingLeft() - getPaddingRight();
+ int heigthWithoutPadding = height - getPaddingTop() - getPaddingBottom();
+
+ // Finally we have some simple logic that calculates the size of the view
+ // and calls setMeasuredDimension() to set that size.
+ // Before we compare the width and height of the view, we remove the padding,
+ // and when we set the dimension we add it back again. Now the actual content
+ // of the view will be square, but, depending on the padding, the total dimensions
+ // of the view might not be.
+ if (widthWithoutPadding > heigthWithoutPadding) {
+ size = heigthWithoutPadding;
+ } else {
+ size = widthWithoutPadding;
+ }
+
+ // If you override onMeasure() you have to call setMeasuredDimension().
+ // This is how you report back the measured size. If you don’t call
+ // setMeasuredDimension() the parent will throw an exception and your
+ // application will crash.
+ // We are calling the onMeasure() method of the superclass so we don’t
+ // actually need to call setMeasuredDimension() since that takes care
+ // of that. However, the purpose with overriding onMeasure() was to
+ // change the default behaviour and to do that we need to call
+ // setMeasuredDimension() with our own values.
+ setMeasuredDimension(size + getPaddingLeft() + getPaddingRight(), size + getPaddingTop() + getPaddingBottom());
+ }
+
+ /**
+ * Use onSizeChanged instead of onAttachedToWindow to get the dimensions of the view,
+ * because this method is called after measuring the dimensions of MATCH_PARENT & WRAP_CONTENT.
+ * Use this dimensions to setup the bounds and paints.
+ */
+ @Override
+ protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+ super.onSizeChanged(w, h, oldw, oldh);
+
+ // Share the dimensions
+ layout_width = w;
+ layout_height = h;
+
+ setupBounds();
+ setupPaints();
+ invalidate();
+ }
+
+ /**
+ * Set the properties of the paints we're using to
+ * draw the progress wheel
+ */
+ private void setupPaints() {
+ barPaint.setColor(barColor);
+ barPaint.setAntiAlias(true);
+ barPaint.setStyle(Style.STROKE);
+ barPaint.setStrokeWidth(barWidth);
+
+ rimPaint.setColor(rimColor);
+ rimPaint.setAntiAlias(true);
+ rimPaint.setStyle(Style.STROKE);
+ rimPaint.setStrokeWidth(rimWidth);
+
+ circlePaint.setColor(circleColor);
+ circlePaint.setAntiAlias(true);
+ circlePaint.setStyle(Style.FILL);
+
+ textPaint.setColor(textColor);
+ textPaint.setStyle(Style.FILL);
+ textPaint.setAntiAlias(true);
+ textPaint.setTextSize(textSize);
+
+ contourPaint.setColor(contourColor);
+ contourPaint.setAntiAlias(true);
+ contourPaint.setStyle(Style.STROKE);
+ contourPaint.setStrokeWidth(contourSize);
+ }
+
+ /**
+ * Set the bounds of the component
+ */
+ private void setupBounds() {
+ // Width should equal to Height, find the min value to steup the circle
+ int minValue = Math.min(layout_width, layout_height);
+
+ // Calc the Offset if needed
+ int xOffset = layout_width - minValue;
+ int yOffset = layout_height - minValue;
+
+ // Add the offset
+ paddingTop = this.getPaddingTop() + (yOffset / 2);
+ paddingBottom = this.getPaddingBottom() + (yOffset / 2);
+ paddingLeft = this.getPaddingLeft() + (xOffset / 2);
+ paddingRight = this.getPaddingRight() + (xOffset / 2);
+
+ int width = getWidth(); //this.getLayoutParams().width;
+ int height = getHeight(); //this.getLayoutParams().height;
+
+ rectBounds = new RectF(paddingLeft,
+ paddingTop,
+ width - paddingRight,
+ height - paddingBottom);
+
+ circleBounds = new RectF(paddingLeft + barWidth,
+ paddingTop + barWidth,
+ width - paddingRight - barWidth,
+ height - paddingBottom - barWidth);
+ circleInnerContour = new RectF(circleBounds.left + (rimWidth / 2.0f) + (contourSize / 2.0f), circleBounds.top + (rimWidth / 2.0f) + (contourSize / 2.0f), circleBounds.right - (rimWidth / 2.0f) - (contourSize / 2.0f), circleBounds.bottom - (rimWidth / 2.0f) - (contourSize / 2.0f));
+ circleOuterContour = new RectF(circleBounds.left - (rimWidth / 2.0f) - (contourSize / 2.0f), circleBounds.top - (rimWidth / 2.0f) - (contourSize / 2.0f), circleBounds.right + (rimWidth / 2.0f) + (contourSize / 2.0f), circleBounds.bottom + (rimWidth / 2.0f) + (contourSize / 2.0f));
+
+ fullRadius = (width - paddingRight - barWidth) / 2;
+ circleRadius = (fullRadius - barWidth) + 1;
+ }
+
+ /**
+ * Parse the attributes passed to the view from the XML
+ *
+ * @param a the attributes to parse
+ */
+ private void parseAttributes(TypedArray a) {
+ barWidth = (int) a.getDimension(R.styleable.ProgressWheel_barWidth,
+ barWidth);
+
+ rimWidth = (int) a.getDimension(R.styleable.ProgressWheel_rimWidth,
+ rimWidth);
+
+ spinSpeed = (int) a.getDimension(R.styleable.ProgressWheel_spinSpeed,
+ spinSpeed);
+
+ delayMillis = a.getInteger(R.styleable.ProgressWheel_delayMillis,
+ delayMillis);
+ if (delayMillis < 0) {
+ delayMillis = 0;
+ }
+
+ barColor = a.getColor(R.styleable.ProgressWheel_barColor, barColor);
+
+ barLength = (int) a.getDimension(R.styleable.ProgressWheel_barLength,
+ barLength);
+
+ textSize = (int) a.getDimension(R.styleable.ProgressWheel_textSize,
+ textSize);
+
+ textColor = (int) a.getColor(R.styleable.ProgressWheel_textColor,
+ textColor);
+
+ //if the text is empty , so ignore it
+ if (a.hasValue(R.styleable.ProgressWheel_text)) {
+ setText(a.getString(R.styleable.ProgressWheel_text));
+ }
+
+ rimColor = (int) a.getColor(R.styleable.ProgressWheel_rimColor,
+ rimColor);
+
+ circleColor = (int) a.getColor(R.styleable.ProgressWheel_circleColor,
+ circleColor);
+
+ contourColor = a.getColor(R.styleable.ProgressWheel_contourColor, contourColor);
+ contourSize = a.getDimension(R.styleable.ProgressWheel_contourSize, contourSize);
+
+
+ // Recycle
+ a.recycle();
+ }
+
+ //----------------------------------
+ //Animation stuff
+ //----------------------------------
+
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+ //Draw the inner circle
+ canvas.drawArc(circleBounds, 360, 360, false, circlePaint);
+ //Draw the rim
+ canvas.drawArc(circleBounds, 360, 360, false, rimPaint);
+ canvas.drawArc(circleOuterContour, 360, 360, false, contourPaint);
+ canvas.drawArc(circleInnerContour, 360, 360, false, contourPaint);
+ //Draw the bar
+ if (isSpinning) {
+ canvas.drawArc(circleBounds, progress - 90, barLength, false,
+ barPaint);
+ } else {
+ canvas.drawArc(circleBounds, -90, progress, false, barPaint);
+ }
+ //Draw the text (attempts to center it horizontally and vertically)
+ float textHeight = textPaint.descent() - textPaint.ascent();
+ float verticalTextOffset = (textHeight / 2) - textPaint.descent();
+
+ for (String s : splitText) {
+ float horizontalTextOffset = textPaint.measureText(s) / 2;
+ canvas.drawText(s, this.getWidth() / 2 - horizontalTextOffset,
+ this.getHeight() / 2 + verticalTextOffset, textPaint);
+ }
+ }
+
+ /**
+ * Check if the wheel is currently spinning
+ */
+
+ public boolean isSpinning() {
+ if(isSpinning){
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Reset the count (in increment mode)
+ */
+ public void resetCount() {
+ progress = 0;
+ setText("0%");
+ invalidate();
+ }
+
+ /**
+ * Turn off spin mode
+ */
+ public void stopSpinning() {
+ isSpinning = false;
+ progress = 0;
+ spinHandler.removeMessages(0);
+ }
+
+
+ /**
+ * Puts the view on spin mode
+ */
+ public void spin() {
+ isSpinning = true;
+ spinHandler.sendEmptyMessage(0);
+ }
+
+ /**
+ * Increment the progress by 1 (of 360)
+ */
+ public void incrementProgress() {
+ isSpinning = false;
+ progress++;
+ if (progress > 360)
+ progress = 0;
+// setText(Math.round(((float) progress / 360) * 100) + "%");
+ spinHandler.sendEmptyMessage(0);
+ }
+
+
+ /**
+ * Set the progress to a specific value
+ */
+ public void setProgress(int i) {
+ isSpinning = false;
+ progress = i;
+ spinHandler.sendEmptyMessage(0);
+ }
+
+ //----------------------------------
+ //Getters + setters
+ //----------------------------------
+
+ /**
+ * Set the text in the progress bar
+ * Doesn't invalidate the view
+ *
+ * @param text the text to show ('\n' constitutes a new line)
+ */
+ public void setText(String text) {
+ this.text = text;
+ splitText = this.text.split("\n");
+ }
+
+ public int getCircleRadius() {
+ return circleRadius;
+ }
+
+ public void setCircleRadius(int circleRadius) {
+ this.circleRadius = circleRadius;
+ }
+
+ public int getBarLength() {
+ return barLength;
+ }
+
+ public void setBarLength(int barLength) {
+ this.barLength = barLength;
+ }
+
+ public int getBarWidth() {
+ return barWidth;
+ }
+
+ public void setBarWidth(int barWidth) {
+ this.barWidth = barWidth;
+ }
+
+ public int getTextSize() {
+ return textSize;
+ }
+
+ public void setTextSize(int textSize) {
+ this.textSize = textSize;
+ }
+
+ public int getPaddingTop() {
+ return paddingTop;
+ }
+
+ public void setPaddingTop(int paddingTop) {
+ this.paddingTop = paddingTop;
+ }
+
+ public int getPaddingBottom() {
+ return paddingBottom;
+ }
+
+ public void setPaddingBottom(int paddingBottom) {
+ this.paddingBottom = paddingBottom;
+ }
+
+ public int getPaddingLeft() {
+ return paddingLeft;
+ }
+
+ public void setPaddingLeft(int paddingLeft) {
+ this.paddingLeft = paddingLeft;
+ }
+
+ public int getPaddingRight() {
+ return paddingRight;
+ }
+
+ public void setPaddingRight(int paddingRight) {
+ this.paddingRight = paddingRight;
+ }
+
+ public int getBarColor() {
+ return barColor;
+ }
+
+ public void setBarColor(int barColor) {
+ this.barColor = barColor;
+ }
+
+ public int getCircleColor() {
+ return circleColor;
+ }
+
+ public void setCircleColor(int circleColor) {
+ this.circleColor = circleColor;
+ }
+
+ public int getRimColor() {
+ return rimColor;
+ }
+
+ public void setRimColor(int rimColor) {
+ this.rimColor = rimColor;
+ }
+
+
+ public Shader getRimShader() {
+ return rimPaint.getShader();
+ }
+
+ public void setRimShader(Shader shader) {
+ this.rimPaint.setShader(shader);
+ }
+
+ public int getTextColor() {
+ return textColor;
+ }
+
+ public void setTextColor(int textColor) {
+ this.textColor = textColor;
+ }
+
+ public int getSpinSpeed() {
+ return spinSpeed;
+ }
+
+ public void setSpinSpeed(int spinSpeed) {
+ this.spinSpeed = spinSpeed;
+ }
+
+ public int getRimWidth() {
+ return rimWidth;
+ }
+
+ public void setRimWidth(int rimWidth) {
+ this.rimWidth = rimWidth;
+ }
+
+ public int getDelayMillis() {
+ return delayMillis;
+ }
+
+ public void setDelayMillis(int delayMillis) {
+ this.delayMillis = delayMillis;
+ }
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/Injector.java b/app/src/main/java/org/bspeice/minimalbible/Injector.java
new file mode 100644
index 0000000..a8e0584
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/Injector.java
@@ -0,0 +1,8 @@
+package org.bspeice.minimalbible;
+
+/**
+ * Created by bspeice on 7/2/14.
+ */
+public interface Injector {
+ public void inject(Object o);
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/MinimalBible.java b/app/src/main/java/org/bspeice/minimalbible/MinimalBible.java
index 3a73912..3fcca64 100644
--- a/app/src/main/java/org/bspeice/minimalbible/MinimalBible.java
+++ b/app/src/main/java/org/bspeice/minimalbible/MinimalBible.java
@@ -2,19 +2,26 @@ package org.bspeice.minimalbible;
import android.app.Application;
import android.content.Context;
+import android.util.Log;
+
+import org.crosswire.jsword.book.sword.SwordBookPath;
+
+import java.io.File;
import dagger.ObjectGraph;
/**
* Created by Bradlee Speice on 7/5/2014.
*/
-public class MinimalBible extends Application {
+public class MinimalBible extends Application implements Injector {
+ private String TAG = "MinimalBible";
private ObjectGraph mObjectGraph;
@Override
public void onCreate() {
super.onCreate();
buildObjGraph();
+ setJswordHome();
}
public void buildObjGraph() {
@@ -25,7 +32,27 @@ public class MinimalBible extends Application {
mObjectGraph.inject(o);
}
+ public ObjectGraph plus(Object... modules) {
+ return mObjectGraph.plus(modules);
+ }
+
public static MinimalBible get(Context ctx) {
return (MinimalBible)ctx.getApplicationContext();
}
+
+ /**
+ * Notify jSword that it needs to store files in the Android internal directory
+ * NOTE: Android will uninstall these files if you uninstall MinimalBible.
+ */
+ @SuppressWarnings("null")
+ private void setJswordHome() {
+ // We need to set the download directory for jSword to stick with
+ // Android.
+ String home = this.getFilesDir().toString();
+ Log.d(TAG, "Setting jsword.home to: " + home);
+ System.setProperty("jsword.home", home);
+ System.setProperty("sword.home", home);
+ SwordBookPath.setDownloadDir(new File(home));
+ Log.d(TAG, "Sword download path: " + SwordBookPath.getSwordDownloadDir());
+ }
}
diff --git a/app/src/main/java/org/bspeice/minimalbible/MinimalBibleModules.java b/app/src/main/java/org/bspeice/minimalbible/MinimalBibleModules.java
index 50a3694..2a711e8 100644
--- a/app/src/main/java/org/bspeice/minimalbible/MinimalBibleModules.java
+++ b/app/src/main/java/org/bspeice/minimalbible/MinimalBibleModules.java
@@ -2,7 +2,7 @@ package org.bspeice.minimalbible;
import android.app.Application;
-import org.bspeice.minimalbible.activity.download.DownloadActivity;
+import org.bspeice.minimalbible.activity.downloader.DownloadActivity;
import javax.inject.Singleton;
@@ -12,8 +12,7 @@ import dagger.Provides;
/**
* Entry point for the default modules used by MinimalBible
*/
-@Module(injects = DownloadActivity.class,
- library = true)
+@Module(library = true)
public class MinimalBibleModules {
MinimalBible app;
@@ -25,8 +24,4 @@ public class MinimalBibleModules {
Application provideApplication() {
return app;
}
-
- @Provides CharSequence provideString() {
- return "Main";
- }
}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/BaseActivity.java b/app/src/main/java/org/bspeice/minimalbible/activity/BaseActivity.java
new file mode 100644
index 0000000..f3f59ed
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/BaseActivity.java
@@ -0,0 +1,30 @@
+package org.bspeice.minimalbible.activity;
+
+import android.os.Build;
+import android.os.Bundle;
+import android.support.v7.app.ActionBarActivity;
+
+import com.readystatesoftware.systembartint.SystemBarTintManager;
+
+import org.bspeice.minimalbible.R;
+
+/**
+ * Wrapper for activities in MinimalBible to make sure we can support
+ * common functionality between them all.
+ */
+public class BaseActivity extends ActionBarActivity {
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Only set the tint if the device is running KitKat or above
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+ SystemBarTintManager tintManager = new SystemBarTintManager(this);
+ tintManager.setStatusBarTintEnabled(true);
+ tintManager.setStatusBarTintColor(getResources().getColor(
+ R.color.statusbar));
+ }
+ }
+
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/BaseFragment.java b/app/src/main/java/org/bspeice/minimalbible/activity/BaseFragment.java
new file mode 100644
index 0000000..582711a
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/BaseFragment.java
@@ -0,0 +1,26 @@
+package org.bspeice.minimalbible.activity;
+
+import android.app.Activity;
+import android.os.Build;
+import android.support.v4.app.Fragment;
+import android.view.View;
+
+import com.readystatesoftware.systembartint.SystemBarTintManager;
+
+/**
+ * Base class that defines all behavior common to Fragments in MinimalBible
+ */
+public class BaseFragment extends Fragment {
+
+ /**
+ * Calculate the offset we need to display properly if the System bar is translucent
+ * @param context The {@link android.app.Activity} we are displaying in
+ * @param view The {@link android.view.View} we need to calculate the offset for.
+ */
+ protected static void setInsets(Activity context, View view) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) return;
+ SystemBarTintManager tintManager = new SystemBarTintManager(context);
+ SystemBarTintManager.SystemBarConfig config = tintManager.getConfig();
+ view.setPadding(0, config.getPixelInsetTop(true), config.getPixelInsetRight(), config.getPixelInsetBottom());
+ }
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/BaseNavigationDrawerFragment.java b/app/src/main/java/org/bspeice/minimalbible/activity/BaseNavigationDrawerFragment.java
new file mode 100644
index 0000000..a795607
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/BaseNavigationDrawerFragment.java
@@ -0,0 +1,302 @@
+package org.bspeice.minimalbible.activity;
+
+import android.app.Activity;
+import android.content.SharedPreferences;
+import android.content.res.Configuration;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.support.v4.app.ActionBarDrawerToggle;
+import android.support.v4.app.Fragment;
+import android.support.v4.view.GravityCompat;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.ActionBarActivity;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.ListView;
+
+import com.readystatesoftware.systembartint.SystemBarTintManager;
+
+import org.bspeice.minimalbible.R;
+
+/**
+ * Fragment used for managing interactions for and presentation of a navigation
+ * drawer. See the design guidelines for a complete explanation of the behaviors
+ * implemented here.
+ */
+public class BaseNavigationDrawerFragment extends Fragment {
+
+ /**
+ * Remember the position of the selected item.
+ */
+ private static final String STATE_SELECTED_POSITION = "selected_navigation_drawer_position";
+
+ /**
+ * Per the design guidelines, you should show the drawer on launch until the
+ * user manually expands it. This shared preference tracks this.
+ */
+ private static final String PREF_USER_LEARNED_DRAWER = "navigation_drawer_learned";
+
+ /**
+ * A pointer to the current callbacks instance (the Activity).
+ */
+ private NavigationDrawerCallbacks mCallbacks;
+
+ /**
+ * Helper component that ties the action bar to the navigation drawer.
+ */
+ private ActionBarDrawerToggle mDrawerToggle;
+
+ private DrawerLayout mDrawerLayout;
+ protected ListView mDrawerListView;
+ private View mFragmentContainerView;
+
+ protected int mCurrentSelectedPosition = 0;
+ private boolean mFromSavedInstanceState;
+ private boolean mUserLearnedDrawer;
+
+ public BaseNavigationDrawerFragment() {
+ }
+
+ @Override
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+
+ // Read in the flag indicating whether or not the user has demonstrated
+ // awareness of the
+ // drawer. See PREF_USER_LEARNED_DRAWER for details.
+ SharedPreferences sp = PreferenceManager
+ .getDefaultSharedPreferences(getActivity());
+ mUserLearnedDrawer = sp.getBoolean(PREF_USER_LEARNED_DRAWER, false);
+
+ if (savedInstanceState != null) {
+ mCurrentSelectedPosition = savedInstanceState
+ .getInt(STATE_SELECTED_POSITION);
+ mFromSavedInstanceState = true;
+ }
+
+ // Select either the default item (0) or the last selected item.
+ selectItem(mCurrentSelectedPosition);
+ }
+
+ @Override
+ public void onActivityCreated(Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ // Indicate that this fragment would like to influence the set of
+ // actions in the action bar.
+ setHasOptionsMenu(true);
+ }
+
+ public boolean isDrawerOpen() {
+ return mDrawerLayout != null
+ && mDrawerLayout.isDrawerOpen(mFragmentContainerView);
+ }
+
+ /**
+ * Users of this fragment must call this method to set up the navigation
+ * drawer interactions.
+ *
+ * @param fragmentId
+ * The android:id of this fragment in its activity's layout.
+ * @param drawerLayout
+ * The DrawerLayout containing this fragment's UI.
+ */
+ public void setUp(int fragmentId, DrawerLayout drawerLayout) {
+ mFragmentContainerView = getActivity().findViewById(fragmentId);
+ mDrawerLayout = drawerLayout;
+
+ // set a custom shadow that overlays the main content when the drawer
+ // opens
+ mDrawerLayout.setDrawerShadow(R.drawable.drawer_shadow,
+ GravityCompat.START);
+ // set up the drawer's list view with items and click listener
+
+ ActionBar actionBar = getActionBar();
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ actionBar.setHomeButtonEnabled(true);
+
+ // ActionBarDrawerToggle ties together the the proper interactions
+ // between the navigation drawer and the action bar app icon.
+ mDrawerToggle = new ActionBarDrawerToggle(getActivity(), /* host Activity */
+ mDrawerLayout, /* DrawerLayout object */
+ R.drawable.ic_drawer, /* nav drawer image to replace 'Up' caret */
+ R.string.navigation_drawer_open, /*
+ * "open drawer" description for
+ * accessibility
+ */
+ R.string.navigation_drawer_close /*
+ * "close drawer" description for
+ * accessibility
+ */
+ ) {
+ @Override
+ public void onDrawerClosed(View drawerView) {
+ super.onDrawerClosed(drawerView);
+ if (!isAdded()) {
+ return;
+ }
+
+ getActivity().supportInvalidateOptionsMenu(); // calls
+ // onPrepareOptionsMenu()
+ }
+
+ @Override
+ public void onDrawerOpened(View drawerView) {
+ super.onDrawerOpened(drawerView);
+ if (!isAdded()) {
+ return;
+ }
+
+ if (!mUserLearnedDrawer) {
+ // The user manually opened the drawer; store this flag to
+ // prevent auto-showing
+ // the navigation drawer automatically in the future.
+ mUserLearnedDrawer = true;
+ SharedPreferences sp = PreferenceManager
+ .getDefaultSharedPreferences(getActivity());
+ sp.edit().putBoolean(PREF_USER_LEARNED_DRAWER, true)
+ .commit();
+ }
+
+ getActivity().supportInvalidateOptionsMenu(); // calls
+ // onPrepareOptionsMenu()
+ }
+ };
+
+ // If the user hasn't 'learned' about the drawer, open it to introduce
+ // them to the drawer,
+ // per the navigation drawer design guidelines.
+ if (!mUserLearnedDrawer && !mFromSavedInstanceState) {
+ mDrawerLayout.openDrawer(mFragmentContainerView);
+ }
+
+ // Defer code dependent on restoration of previous instance state.
+ mDrawerLayout.post(new Runnable() {
+ @Override
+ public void run() {
+ mDrawerToggle.syncState();
+ }
+ });
+
+ mDrawerLayout.setDrawerListener(mDrawerToggle);
+ }
+
+ public void selectItem(int position) {
+ mCurrentSelectedPosition = position;
+ if (mDrawerListView != null) {
+ mDrawerListView.setItemChecked(position, true);
+ }
+ if (mDrawerLayout != null) {
+ mDrawerLayout.closeDrawer(mFragmentContainerView);
+ }
+ if (mCallbacks != null) {
+ mCallbacks.onNavigationDrawerItemSelected(position);
+ }
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ try {
+ mCallbacks = (NavigationDrawerCallbacks) activity;
+ } catch (ClassCastException e) {
+ throw new ClassCastException(
+ "Activity must implement NavigationDrawerCallbacks.");
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mCallbacks = null;
+ }
+
+ @Override
+ public void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ outState.putInt(STATE_SELECTED_POSITION, mCurrentSelectedPosition);
+ }
+
+ @Override
+ public void onConfigurationChanged(Configuration newConfig) {
+ super.onConfigurationChanged(newConfig);
+ // Forward the new configuration the drawer toggle component.
+ mDrawerToggle.onConfigurationChanged(newConfig);
+ }
+
+ @Override
+ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+ // If the drawer is open, show the global app actions in the action bar.
+ // See also
+ // showGlobalContextActionBar, which controls the top-left area of the
+ // action bar.
+ if (mDrawerLayout != null && isDrawerOpen()) {
+ inflater.inflate(R.menu.global, menu);
+ showGlobalContextActionBar();
+ }
+ super.onCreateOptionsMenu(menu, inflater);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (mDrawerToggle.onOptionsItemSelected(item)) {
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ /**
+ * Per the navigation drawer design guidelines, updates the action bar to
+ * show the global app 'context', rather than just what's in the current
+ * screen.
+ */
+ private void showGlobalContextActionBar() {
+ ActionBar actionBar = getActionBar();
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ // actionBar.setTitle(R.string.app_name);
+ }
+
+ protected ActionBar getActionBar() {
+ return ((ActionBarActivity) getActivity()).getSupportActionBar();
+ }
+
+ public void setInsets(View view) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
+ return;
+ SystemBarTintManager tintManager = new SystemBarTintManager(getActivity());
+ SystemBarTintManager.SystemBarConfig config = tintManager.getConfig();
+ view.setPadding(0, config.getPixelInsetTop(true),
+ config.getPixelInsetRight(), config.getPixelInsetBottom());
+ }
+
+ @Override
+ public void onViewCreated(View view, Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+
+ // This could also be a ScrollView
+ ListView list = (ListView) view.findViewById(R.id.list_nav_drawer);
+ // This could also be set in your layout, allows the list items to
+ // scroll through the bottom padded area (navigation bar)
+ list.setClipToPadding(false);
+ // Sets the padding to the insets (include action bar and navigation bar
+ // padding for the current device and orientation)
+ setInsets(list);
+ }
+
+ /**
+ * Callbacks interface that all activities using this fragment must
+ * implement.
+ */
+ public static interface NavigationDrawerCallbacks {
+ /**
+ * Called when an item in the navigation drawer is selected.
+ */
+ void onNavigationDrawerItemSelected(int position);
+ }
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/download/DownloadActivity.java b/app/src/main/java/org/bspeice/minimalbible/activity/download/DownloadActivity.java
deleted file mode 100644
index ddd11aa..0000000
--- a/app/src/main/java/org/bspeice/minimalbible/activity/download/DownloadActivity.java
+++ /dev/null
@@ -1,156 +0,0 @@
-package org.bspeice.minimalbible.activity.download;
-
-import android.app.Activity;
-import android.os.Bundle;
-import android.support.v4.app.Fragment;
-import android.support.v4.app.FragmentManager;
-import android.support.v4.widget.DrawerLayout;
-import android.support.v7.app.ActionBar;
-import android.support.v7.app.ActionBarActivity;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.Menu;
-import android.view.MenuItem;
-import android.view.View;
-import android.view.ViewGroup;
-
-import org.bspeice.minimalbible.MinimalBible;
-import org.bspeice.minimalbible.NavigationDrawerFragment;
-import org.bspeice.minimalbible.R;
-
-import javax.inject.Inject;
-
-
-public class DownloadActivity extends ActionBarActivity
- implements NavigationDrawerFragment.NavigationDrawerCallbacks {
-
- /**
- * Fragment managing the behaviors, interactions and presentation of the navigation drawer.
- */
- private NavigationDrawerFragment mNavigationDrawerFragment;
-
- /**
- * Used to store the last screen title. For use in {@link #restoreActionBar()}.
- */
- protected CharSequence mTitle;
-
- //TODO: This will need to be refactored out later, for now it's a proof of concept.
- @Inject CharSequence testInject;
-
- @Override
- protected void onCreate(Bundle savedInstanceState) {
- setTheme(R.style.AppTheme);
- super.onCreate(savedInstanceState);
-
- MinimalBible.get(this).inject(this);
-
- setContentView(R.layout.activity_download);
-
- mNavigationDrawerFragment = (NavigationDrawerFragment)
- getSupportFragmentManager().findFragmentById(R.id.navigation_drawer);
- mTitle = getTitle();
-
- // Set up the drawer.
- mNavigationDrawerFragment.setUp(
- R.id.navigation_drawer,
- (DrawerLayout) findViewById(R.id.drawer_layout));
-
- }
-
- @Override
- public void onNavigationDrawerItemSelected(int position) {
- // update the main content by replacing fragments
- FragmentManager fragmentManager = getSupportFragmentManager();
- fragmentManager.beginTransaction()
- .replace(R.id.container, PlaceholderFragment.newInstance(position + 1))
- .commit();
- }
-
- public void onSectionAttached(int number) {
- switch (number) {
- case 1:
- mTitle = getString(R.string.title_section1);
- break;
- case 2:
- mTitle = getString(R.string.title_section2);
- break;
- case 3:
- mTitle = getString(R.string.title_section3);
- break;
- }
- }
-
- public void restoreActionBar() {
- ActionBar actionBar = getSupportActionBar();
- actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
- actionBar.setDisplayShowTitleEnabled(true);
- actionBar.setTitle(mTitle);
- }
-
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- if (!mNavigationDrawerFragment.isDrawerOpen()) {
- // Only show items in the action bar relevant to this screen
- // if the drawer is not showing. Otherwise, let the drawer
- // decide what to show in the action bar.
- getMenuInflater().inflate(R.menu.download, menu);
- restoreActionBar();
- return true;
- }
- return super.onCreateOptionsMenu(menu);
- }
-
- @Override
- public boolean onOptionsItemSelected(MenuItem item) {
- // Handle action bar item clicks here. The action bar will
- // automatically handle clicks on the Home/Up button, so long
- // as you specify a parent activity in AndroidManifest.xml.
- int id = item.getItemId();
- if (id == R.id.action_settings) {
- return true;
- }
- return super.onOptionsItemSelected(item);
- }
-
- /**
- * A placeholder fragment containing a simple view.
- */
- public static class PlaceholderFragment extends Fragment {
- /**
- * The fragment argument representing the section number for this
- * fragment.
- */
- private static final String ARG_SECTION_NUMBER = "section_number";
-
- /**
- * Returns a new instance of this fragment for the given section
- * number.
- */
- public static PlaceholderFragment newInstance(int sectionNumber) {
- PlaceholderFragment fragment = new PlaceholderFragment();
- Bundle args = new Bundle();
- args.putInt(ARG_SECTION_NUMBER, sectionNumber);
- fragment.setArguments(args);
- return fragment;
- }
-
- public PlaceholderFragment() {
- }
-
- @Override
- public View onCreateView(LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.fragment_download, container, false);
- return rootView;
- }
-
- @Override
- public void onAttach(Activity activity) {
- super.onAttach(activity);
- ((DownloadActivity) activity).onSectionAttached(
- getArguments().getInt(ARG_SECTION_NUMBER));
- }
- }
-
-}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/downloader/BookItemHolder.java b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/BookItemHolder.java
new file mode 100644
index 0000000..b851525
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/BookItemHolder.java
@@ -0,0 +1,156 @@
+package org.bspeice.minimalbible.activity.downloader;
+
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+import com.todddavies.components.progressbar.ProgressWheel;
+
+import org.bspeice.minimalbible.R;
+import org.bspeice.minimalbible.activity.downloader.manager.BookDownloadManager;
+import org.bspeice.minimalbible.activity.downloader.manager.DLProgressEvent;
+import org.bspeice.minimalbible.activity.downloader.manager.InstalledManager;
+import org.crosswire.jsword.book.Book;
+
+import javax.inject.Inject;
+
+import butterknife.ButterKnife;
+import butterknife.InjectView;
+import butterknife.OnClick;
+import rx.Subscription;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.functions.Action1;
+import rx.functions.Func1;
+
+/**
+* Created by bspeice on 5/20/14.
+*/
+public class BookItemHolder {
+
+ // TODO: The holder should register and unregister itself for DownloadProgress events
+ // so that we can display live updates.
+
+ @InjectView(R.id.download_txt_item_acronym)
+ TextView acronym;
+ @InjectView(R.id.txt_download_item_name)
+ TextView itemName;
+ @InjectView(R.id.download_ibtn_download)
+ ImageButton isDownloaded;
+ @InjectView(R.id.download_prg_download)
+ ProgressWheel downloadProgress;
+
+ @Inject
+ BookDownloadManager bookDownloadManager;
+ @Inject
+ InstalledManager installedManager;
+
+ private final Book b;
+ private Subscription subscription;
+
+ // TODO: Factory style?
+ public BookItemHolder(View v, Book b, DownloadActivity activity) {
+ ButterKnife.inject(this, v);
+ activity.inject(this);
+ this.b = b;
+ }
+
+ public void bindHolder() {
+ acronym.setText(b.getInitials());
+ itemName.setText(b.getName());
+ DLProgressEvent dlProgressEvent = bookDownloadManager.getInProgressDownloadProgress(b);
+ if (dlProgressEvent != null) {
+ displayProgress((int) dlProgressEvent.toCircular());
+ } else if (installedManager.isInstalled(b)) {
+ displayInstalled();
+ }
+ //TODO: Refactor
+ subscription = bookDownloadManager.getDownloadEvents()
+ .observeOn(AndroidSchedulers.mainThread())
+ .filter(new Func1() {
+ @Override
+ public Boolean call(DLProgressEvent event) {
+ return event.getB().getInitials().equals(b.getInitials());
+ }
+ })
+ .subscribe(new Action1() {
+ @Override
+ public void call(DLProgressEvent event) {
+ BookItemHolder.this.displayProgress((int) event.toCircular());
+ }
+ });
+ }
+
+ private void displayInstalled() {
+ isDownloaded.setImageResource(R.drawable.ic_action_cancel);
+ }
+
+ @OnClick(R.id.download_ibtn_download)
+ public void onDownloadItem(View v) {
+ if (installedManager.isInstalled(b)) {
+ // Remove the book
+ installedManager.removeBook(b);
+ isDownloaded.setImageResource(R.drawable.ic_action_download);
+ } else {
+ bookDownloadManager.installBook(this.b);
+ }
+ }
+
+ /**
+ * Display the current progress of this download
+ * TODO: Clean up this logic if at all possible...
+ * @param progress The progress out of 360 (degrees of a circle)
+ */
+ private void displayProgress(int progress) {
+
+
+ if (progress == DLProgressEvent.PROGRESS_BEGINNING) {
+ // Download starting
+ RelativeLayout.LayoutParams acronymParams =
+ (RelativeLayout.LayoutParams)acronym.getLayoutParams();
+ acronymParams.addRule(RelativeLayout.LEFT_OF, downloadProgress.getId());
+
+ RelativeLayout.LayoutParams nameParams =
+ (RelativeLayout.LayoutParams)itemName.getLayoutParams();
+ nameParams.addRule(RelativeLayout.LEFT_OF, downloadProgress.getId());
+
+ isDownloaded.setVisibility(View.GONE);
+ downloadProgress.setVisibility(View.VISIBLE);
+
+ downloadProgress.spin();
+ } else if (progress < 360) {
+ // Download in progress
+ RelativeLayout.LayoutParams acronymParams =
+ (RelativeLayout.LayoutParams)acronym.getLayoutParams();
+ acronymParams.addRule(RelativeLayout.LEFT_OF, downloadProgress.getId());
+
+ RelativeLayout.LayoutParams nameParams =
+ (RelativeLayout.LayoutParams)itemName.getLayoutParams();
+ nameParams.addRule(RelativeLayout.LEFT_OF, downloadProgress.getId());
+
+ isDownloaded.setVisibility(View.GONE);
+ downloadProgress.setVisibility(View.VISIBLE);
+
+ downloadProgress.stopSpinning();
+ downloadProgress.setProgress(progress);
+ } else {
+ // Download complete
+ subscription.unsubscribe();
+ RelativeLayout.LayoutParams acronymParams =
+ (RelativeLayout.LayoutParams)acronym.getLayoutParams();
+ acronymParams.addRule(RelativeLayout.LEFT_OF, isDownloaded.getId());
+
+ RelativeLayout.LayoutParams nameParams =
+ (RelativeLayout.LayoutParams)itemName.getLayoutParams();
+ nameParams.addRule(RelativeLayout.LEFT_OF, isDownloaded.getId());
+
+ isDownloaded.setVisibility(View.VISIBLE);
+ downloadProgress.setVisibility(View.GONE);
+ displayInstalled();
+ }
+ }
+
+ public void onScrollOffscreen() {
+ subscription.unsubscribe();
+ }
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/downloader/BookListAdapter.java b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/BookListAdapter.java
new file mode 100644
index 0000000..7277dd7
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/BookListAdapter.java
@@ -0,0 +1,65 @@
+package org.bspeice.minimalbible.activity.downloader;
+
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AbsListView;
+import android.widget.BaseAdapter;
+
+import org.bspeice.minimalbible.R;
+import org.crosswire.jsword.book.Book;
+
+import java.util.List;
+
+/**
+ * Adapter to inflate list_download_items.xml
+ */
+public class BookListAdapter extends BaseAdapter implements AbsListView.RecyclerListener {
+ private final List bookList;
+ private final LayoutInflater inflater;
+ private final DownloadActivity activity;
+
+ public BookListAdapter(LayoutInflater inflater, List bookList, DownloadActivity activity) {
+ this.bookList = bookList;
+ this.inflater = inflater;
+ this.activity = activity;
+ }
+
+ @Override
+ public int getCount() {
+ return bookList.size();
+ }
+
+ @Override
+ public Book getItem(int position) {
+ return bookList.get(position);
+ }
+
+ @Override
+ public long getItemId(int i) {
+ return i;
+ }
+
+ @Override
+ public View getView(int position, View convertView, ViewGroup parent) {
+ BookItemHolder viewHolder;
+ // Nasty Android issue - if you don't check the getTag(), Android will start recycling,
+ // and you'll get some really strange issues
+ if (convertView == null || convertView.getTag() == null) {
+ convertView = inflater.inflate(R.layout.list_download_items, parent, false);
+ viewHolder = new BookItemHolder(convertView, getItem(position), activity);
+ } else {
+ viewHolder = (BookItemHolder) convertView.getTag();
+ }
+
+ viewHolder.bindHolder();
+ return convertView;
+ }
+
+ @Override
+ public void onMovedToScrapHeap(View view) {
+ BookItemHolder holder = (BookItemHolder) view.getTag();
+ holder.onScrollOffscreen();
+ }
+
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/downloader/BookListFragment.java b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/BookListFragment.java
new file mode 100644
index 0000000..c1fc750
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/BookListFragment.java
@@ -0,0 +1,193 @@
+package org.bspeice.minimalbible.activity.downloader;
+
+import android.app.Activity;
+import android.app.AlertDialog;
+import android.app.ProgressDialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ListView;
+import android.widget.Toast;
+
+import org.bspeice.minimalbible.Injector;
+import org.bspeice.minimalbible.MinimalBible;
+import org.bspeice.minimalbible.R;
+import org.bspeice.minimalbible.activity.BaseFragment;
+import org.bspeice.minimalbible.activity.downloader.manager.RefreshManager;
+import org.crosswire.jsword.book.Book;
+import org.crosswire.jsword.book.BookCategory;
+import org.crosswire.jsword.book.BookComparators;
+
+import java.util.List;
+
+import javax.inject.Inject;
+
+import butterknife.ButterKnife;
+import butterknife.InjectView;
+import rx.android.schedulers.AndroidSchedulers;
+import rx.functions.Action1;
+import rx.functions.Func1;
+import rx.functions.Func2;
+
+/**
+ * A placeholder fragment containing a simple view.
+ */
+
+public class BookListFragment extends BaseFragment {
+ /**
+ * The fragment argument representing the section number for this fragment.
+ * Not a candidate for Dart (yet) because I would have to write a Parcelable around it.
+ */
+ protected static final String ARG_BOOK_CATEGORY = "book_category";
+
+ private final String TAG = "BookListFragment";
+
+ @InjectView(R.id.lst_download_available)
+ ListView downloadsAvailable;
+
+ @Inject RefreshManager refreshManager;
+ @Inject protected DownloadPrefs downloadPrefs;
+
+ protected ProgressDialog refreshDialog;
+ private LayoutInflater inflater;
+
+ /**
+ * Returns a new instance of this fragment for the given section number.
+ * TODO: Switch to AutoFactory/@Provides rather than inline creation.
+ */
+ public static BookListFragment newInstance(BookCategory c) {
+ BookListFragment fragment = new BookListFragment();
+ Bundle args = new Bundle();
+ args.putString(ARG_BOOK_CATEGORY, c.toString());
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onCreate(Bundle state) {
+ super.onCreate(state);
+ ((Injector)getActivity()).inject(this);
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ this.inflater = inflater;
+ View rootView = inflater.inflate(R.layout.fragment_download, container,
+ false);
+ ButterKnife.inject(this, rootView);
+ displayModules();
+ return rootView;
+ }
+
+ @Override
+ public void onAttach(Activity activity) {
+ super.onAttach(activity);
+ ((DownloadActivity) activity).onSectionAttached(getArguments()
+ .getString(ARG_BOOK_CATEGORY));
+ }
+
+ /**
+ * Trigger the functionality to display a list of modules. Prompts user if downloading
+ * from the internet is allowable.
+ */
+ protected void displayModules() {
+ boolean dialogDisplayed = downloadPrefs.hasShownDownloadDialog();
+
+ if (!dialogDisplayed) {
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ DownloadDialogListener dialogListener = new DownloadDialogListener();
+ builder.setMessage(
+ "About to contact servers to download content. Continue?")
+ .setPositiveButton("Yes", dialogListener)
+ .setNegativeButton("No", dialogListener)
+ .setCancelable(false).show();
+ } else {
+ refreshModules();
+ }
+ }
+
+ /**
+ * Do the work of refreshing modules (download manager handles using cached copy vs. actual
+ * refresh), and then displaying them when ready.
+ */
+ private void refreshModules() {
+ // Check if the downloadManager has already refreshed everything
+ if (!refreshManager.isRefreshComplete()) {
+ // downloadManager is in progress of refreshing
+ refreshDialog = new ProgressDialog(getActivity());
+ refreshDialog.setMessage("Refreshing available modules...");
+ refreshDialog.setCancelable(false);
+ refreshDialog.show();
+ }
+
+ // Listen for the books!
+ refreshManager.getAvailableModulesFlattened()
+ .filter(new Func1() {
+ @Override
+ public Boolean call(Book book) {
+ return book.getBookCategory() ==
+ BookCategory.fromString(BookListFragment.this.getArguments()
+ .getString(ARG_BOOK_CATEGORY));
+ }
+ })
+ // Repack all the books
+ .toSortedList(new Func2() {
+ @Override
+ public Integer call(Book book1, Book book2) {
+ return BookComparators.getInitialComparator().compare(book1, book2);
+ }
+ })
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(new Action1>() {
+ @Override
+ public void call(List books) {
+ downloadsAvailable.setAdapter(
+ new BookListAdapter(inflater, books, (DownloadActivity)getActivity()));
+ if (BookListFragment.this.getActivity() != null) {
+ // On a screen rotate, getActivity() will be null. But, the activity
+ // will already have been set up correctly, so we don't need to worry
+ // about it.
+ // If not null, we need to set it up now.
+ setInsets(BookListFragment.this.getActivity(), downloadsAvailable);
+ }
+ if (refreshDialog != null) {
+ refreshDialog.cancel();
+ }
+ }
+ });
+ }
+
+ private class DownloadDialogListener implements
+ DialogInterface.OnClickListener {
+ @Override
+ public void onClick(DialogInterface dialog, int which) {
+ downloadPrefs.hasShownDownloadDialog(true);
+
+ switch (which) {
+ case DialogInterface.BUTTON_POSITIVE:
+ // Clicked ready to continue - allow downloading in the future
+ downloadPrefs.hasEnabledDownload(true);
+
+ // And warn them that it has been enabled in the future.
+ Toast.makeText(getActivity(),
+ "Downloading now enabled. Disable in settings.",
+ Toast.LENGTH_SHORT).show();
+ refreshModules();
+ break;
+
+ case DialogInterface.BUTTON_NEGATIVE:
+ // Clicked to not download - Permanently disable downloading
+ downloadPrefs.hasEnabledDownload(false);
+ Toast.makeText(getActivity(),
+ "Disabling downloading. Re-enable it in settings.",
+ Toast.LENGTH_SHORT).show();
+ refreshModules();
+ break;
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/downloader/DownloadActivity.java b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/DownloadActivity.java
new file mode 100644
index 0000000..3397914
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/DownloadActivity.java
@@ -0,0 +1,115 @@
+package org.bspeice.minimalbible.activity.downloader;
+
+import android.os.Bundle;
+import android.support.v4.app.FragmentManager;
+import android.support.v4.widget.DrawerLayout;
+import android.support.v7.app.ActionBar;
+import android.view.Menu;
+import android.view.MenuItem;
+
+import org.bspeice.minimalbible.Injector;
+import org.bspeice.minimalbible.MinimalBible;
+import org.bspeice.minimalbible.R;
+import org.bspeice.minimalbible.activity.BaseActivity;
+import org.bspeice.minimalbible.activity.BaseNavigationDrawerFragment;
+import org.bspeice.minimalbible.activity.downloader.manager.DownloadManager;
+
+import dagger.ObjectGraph;
+
+public class DownloadActivity extends BaseActivity implements
+ BaseNavigationDrawerFragment.NavigationDrawerCallbacks,
+ Injector {
+
+ /**
+ * Fragment managing the behaviors, interactions and presentation of the
+ * navigation drawer.
+ */
+ private DownloadNavDrawerFragment mNavigationDrawerFragment;
+
+ /**
+ * Used to store the last screen title. For use in
+ * {@link #restoreActionBar()}.
+ */
+ private CharSequence mTitle;
+
+ private ObjectGraph daObjectGraph;
+
+ /**
+ * Build a scoped object graph for anything used by the DownloadActivity
+ */
+ private void buildObjGraph() {
+ if (daObjectGraph == null) {
+ daObjectGraph = MinimalBible.get(this)
+ .plus(new DownloadActivityModules(this));
+ }
+ }
+
+ @Override
+ public void inject(Object o) {
+ buildObjGraph();
+ daObjectGraph.inject(o);
+ }
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_download);
+
+ mNavigationDrawerFragment = (DownloadNavDrawerFragment) getSupportFragmentManager()
+ .findFragmentById(R.id.navigation_drawer);
+ mTitle = getTitle();
+
+ // Set up the drawer.
+ mNavigationDrawerFragment.setUp(R.id.navigation_drawer,
+ (DrawerLayout) findViewById(R.id.drawer_layout));
+ }
+
+ @Override
+ public void onNavigationDrawerItemSelected(int position) {
+ // update the main content by replacing fragments
+ //TODO: Switch to AutoFactory pattern, rather than newInstance()
+ FragmentManager fragmentManager = getSupportFragmentManager();
+ fragmentManager
+ .beginTransaction()
+ .replace(R.id.container,
+ BookListFragment.newInstance(DownloadManager.VALID_CATEGORIES[position])).commit();
+ }
+
+ public void onSectionAttached(String category) {
+ mTitle = category;
+ }
+
+ public void restoreActionBar() {
+ ActionBar actionBar = getSupportActionBar();
+ actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_STANDARD);
+ actionBar.setDisplayShowTitleEnabled(true);
+ actionBar.setTitle(mTitle);
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ if (!mNavigationDrawerFragment.isDrawerOpen()) {
+ // Only show items in the action bar relevant to this screen
+ // if the drawer is not showing. Otherwise, let the drawer
+ // decide what to show in the action bar.
+ getMenuInflater().inflate(R.menu.download, menu);
+ restoreActionBar();
+ return true;
+ }
+ return super.onCreateOptionsMenu(menu);
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ // Handle action bar item clicks here. The action bar will
+ // automatically handle clicks on the Home/Up button, so long
+ // as you specify a parent activity in AndroidManifest.xml.
+ int id = item.getItemId();
+ if (id == R.id.action_settings) {
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/downloader/DownloadActivityModules.java b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/DownloadActivityModules.java
new file mode 100644
index 0000000..08a80d1
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/DownloadActivityModules.java
@@ -0,0 +1,48 @@
+package org.bspeice.minimalbible.activity.downloader;
+
+import org.bspeice.minimalbible.MinimalBibleModules;
+import org.bspeice.minimalbible.activity.downloader.manager.BookDownloadManager;
+import org.bspeice.minimalbible.activity.downloader.manager.BookDownloadThread;
+import org.bspeice.minimalbible.activity.downloader.manager.RefreshManager;
+
+import javax.inject.Singleton;
+
+import dagger.Module;
+import dagger.Provides;
+import de.devland.esperandro.Esperandro;
+
+/**
+ * Module mappings for the classes under the Download Activity
+ */
+@Module(
+ injects = {
+ BookListFragment.class,
+ BookItemHolder.class,
+ BookDownloadManager.class,
+ BookDownloadThread.class,
+ RefreshManager.class
+ },
+ addsTo = MinimalBibleModules.class
+)
+public class DownloadActivityModules {
+ DownloadActivity activity;
+
+ DownloadActivityModules(DownloadActivity activity) {
+ this.activity = activity;
+ }
+
+ @Provides @Singleton
+ DownloadPrefs provideDownloadPrefs() {
+ return Esperandro.getPreferences(DownloadPrefs.class, activity);
+ }
+
+ @Provides @Singleton
+ DownloadActivity provideDownloadActivity() {
+ return activity;
+ }
+
+ @Provides @Singleton
+ BookDownloadManager provideBookDownloadManager() {
+ return new BookDownloadManager(activity);
+ }
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/downloader/DownloadNavDrawerFragment.java b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/DownloadNavDrawerFragment.java
new file mode 100644
index 0000000..45a60d4
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/DownloadNavDrawerFragment.java
@@ -0,0 +1,43 @@
+package org.bspeice.minimalbible.activity.downloader;
+
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.AdapterView;
+import android.widget.ArrayAdapter;
+import android.widget.ListView;
+
+import org.bspeice.minimalbible.R;
+import org.bspeice.minimalbible.activity.BaseNavigationDrawerFragment;
+import org.bspeice.minimalbible.activity.downloader.manager.DownloadManager;
+
+public class DownloadNavDrawerFragment extends BaseNavigationDrawerFragment {
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container,
+ Bundle savedInstanceState) {
+ mDrawerListView = (ListView) inflater.inflate(
+ R.layout.fragment_navigation_drawer, container, false);
+ mDrawerListView
+ .setOnItemClickListener(new AdapterView.OnItemClickListener() {
+ @Override
+ public void onItemClick(AdapterView> parent, View view,
+ int position, long id) {
+ selectItem(position);
+ }
+ });
+
+ String[] sCategories = new String[DownloadManager.VALID_CATEGORIES.length];
+ for (int i = 0; i < DownloadManager.VALID_CATEGORIES.length; i++) {
+ sCategories[i] = DownloadManager.VALID_CATEGORIES[i].toString();
+ }
+
+ mDrawerListView.setAdapter(new ArrayAdapter(getActionBar()
+ .getThemedContext(), android.R.layout.simple_list_item_1,
+ android.R.id.text1, sCategories));
+ mDrawerListView.setItemChecked(mCurrentSelectedPosition, true);
+ return mDrawerListView;
+ }
+
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/downloader/DownloadPrefs.java b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/DownloadPrefs.java
new file mode 100644
index 0000000..d5ba978
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/DownloadPrefs.java
@@ -0,0 +1,20 @@
+package org.bspeice.minimalbible.activity.downloader;
+
+import de.devland.esperandro.annotations.SharedPreferences;
+
+/**
+ * SharedPreferences interface to be built by Esperandro
+ */
+@SharedPreferences(name="DownloadPrefs")
+public interface DownloadPrefs {
+
+ boolean hasEnabledDownload();
+ void hasEnabledDownload(boolean hasEnabledDownload);
+
+ boolean hasShownDownloadDialog();
+ void hasShownDownloadDialog(boolean hasShownDownloadDialog);
+
+ long downloadRefreshedOn();
+ void downloadRefreshedOn(long downloadRefreshedOn);
+
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/BookDownloadManager.java b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/BookDownloadManager.java
new file mode 100644
index 0000000..fd40f7a
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/BookDownloadManager.java
@@ -0,0 +1,132 @@
+package org.bspeice.minimalbible.activity.downloader.manager;
+
+import android.content.Context;
+import android.util.Log;
+
+import org.bspeice.minimalbible.MinimalBible;
+import org.bspeice.minimalbible.activity.downloader.DownloadActivity;
+import org.crosswire.common.progress.JobManager;
+import org.crosswire.common.progress.Progress;
+import org.crosswire.common.progress.WorkEvent;
+import org.crosswire.common.progress.WorkListener;
+import org.crosswire.jsword.book.Book;
+import org.crosswire.jsword.book.Books;
+import org.crosswire.jsword.book.BooksEvent;
+import org.crosswire.jsword.book.BooksListener;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.inject.Inject;
+import javax.inject.Provider;
+import javax.inject.Singleton;
+
+import rx.subjects.PublishSubject;
+
+/**
+ * Wrapper to convert JSword progress events to MinimalBible EventBus-based
+ */
+//TODO: Make sure that jobs have the correct name
+//TODO: Install indexes for Bibles
+@Singleton
+public class BookDownloadManager implements WorkListener, BooksListener {
+
+ /**
+ * Mapping of Job ID to the EventBus we should trigger progress on
+ */
+ private final Map bookMappings;
+
+ /**
+ * Cached copy of downloads in progress so views displaying this info can get it quickly.
+ */
+ private final Map inProgressDownloads;
+
+ private final PublishSubject downloadEvents = PublishSubject.create();
+
+ @Inject
+ Provider dlThreadProvider;
+
+ @Inject
+ public BookDownloadManager(DownloadActivity activity) {
+ bookMappings = new HashMap();
+ inProgressDownloads = new HashMap();
+ JobManager.addWorkListener(this);
+ activity.inject(this);
+ Books.installed().addBooksListener(this);
+ }
+
+ public void installBook(Book b) {
+ BookDownloadThread dlThread = dlThreadProvider.get();
+ dlThread.downloadBook(b);
+ addJob(BookDownloadThread.getJobId(b), b);
+ downloadEvents.onNext(new DLProgressEvent(DLProgressEvent.PROGRESS_BEGINNING, b));
+ }
+
+ public void addJob(String jobId, Book b) {
+ bookMappings.put(jobId, b);
+ }
+
+ @Override
+ public void workProgressed(WorkEvent ev) {
+ Progress job = ev.getJob();
+ Log.d("BookDownloadManager", "Download in progress: " + job.getJobID() + " - " + job.getJobName() + " " + job.getWorkDone() + "/" + job.getTotalWork());
+ if (bookMappings.containsKey(job.getJobID())) {
+ Book b = bookMappings.get(job.getJobID());
+
+ if (job.getWorkDone() == job.getTotalWork()) {
+ // Download is complete
+ inProgressDownloads.remove(bookMappings.get(job.getJobID()));
+ bookMappings.remove(job.getJobID());
+ downloadEvents.onNext(new DLProgressEvent(DLProgressEvent.PROGRESS_COMPLETE, b));
+ } else {
+ // Track the ongoing download
+ DLProgressEvent event = new DLProgressEvent(job.getWorkDone(),
+ job.getTotalWork(), b);
+ inProgressDownloads.put(b, event);
+ downloadEvents.onNext(event);
+ }
+ }
+ }
+
+ /**
+ * Check the status of a book download in progress.
+ * @param b The book to get the current progress of
+ * @return The most recent DownloadProgressEvent for the book, or null if not downloading
+ */
+ public DLProgressEvent getInProgressDownloadProgress(Book b) {
+ if (inProgressDownloads.containsKey(b)) {
+ return inProgressDownloads.get(b);
+ } else {
+ return null;
+ }
+ }
+
+ public PublishSubject getDownloadEvents() {
+ return downloadEvents;
+ }
+
+ @Override
+ public void workStateChanged(WorkEvent ev) {
+ Log.d("BookDownloadManager", ev.toString());
+ }
+
+ @Override
+ public void bookAdded(BooksEvent booksEvent) {
+ // It's possible the install finished before we received a progress event for it,
+ // we handle that case here.
+ Book b = booksEvent.getBook();
+ Log.d("BookDownloadManager", "Book added: " + b.getName());
+ if (inProgressDownloads.containsKey(b)) {
+ inProgressDownloads.remove(b);
+ }
+ // Not sure why, but the inProgressDownloads might not have our book,
+ // so we always trigger the PROGRESS_COMPLETE event.
+ // TODO: Make sure all books get to the inProgressDownloads
+ downloadEvents.onNext(new DLProgressEvent(DLProgressEvent.PROGRESS_COMPLETE, b));
+ }
+
+ @Override
+ public void bookRemoved(BooksEvent booksEvent) {
+ // Not too worried about this just yet.
+ }
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/BookDownloadThread.java b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/BookDownloadThread.java
new file mode 100644
index 0000000..e03ef7e
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/BookDownloadThread.java
@@ -0,0 +1,68 @@
+package org.bspeice.minimalbible.activity.downloader.manager;
+
+import android.content.Context;
+import android.util.Log;
+
+import org.bspeice.minimalbible.MinimalBible;
+import org.bspeice.minimalbible.activity.downloader.DownloadActivity;
+import org.crosswire.jsword.book.Book;
+import org.crosswire.jsword.book.install.InstallException;
+import org.crosswire.jsword.book.install.Installer;
+
+import javax.inject.Inject;
+
+import rx.functions.Action1;
+import rx.schedulers.Schedulers;
+
+/**
+ * Thread that handles downloading a book
+ */
+//TODO: Refactor to BookDownloadManager, downloadBook() creates its own thread
+public class BookDownloadThread {
+
+ private final String TAG = "BookDownloadThread";
+
+ @Inject
+ BookDownloadManager bookDownloadManager;
+ @Inject
+ RefreshManager refreshManager;
+
+ @Inject
+ public BookDownloadThread(DownloadActivity activity) {
+ activity.inject(this);
+ }
+
+ public void downloadBook(final Book b) {
+ // So, the JobManager can't be injected, but we'll make do
+
+ // First, look up where the Book came from
+ refreshManager.installerFromBook(b)
+ .subscribeOn(Schedulers.io())
+ .subscribe(new Action1() {
+ @Override
+ public void call(Installer installer) {
+ try {
+ installer.install(b);
+ } catch (InstallException e) {
+ Log.d(TAG, e.getMessage());
+ }
+
+ bookDownloadManager.getDownloadEvents()
+ .onNext(new DLProgressEvent(DLProgressEvent.PROGRESS_BEGINNING, b));
+ }
+ });
+ }
+
+ /**
+ * Build what the installer creates the job name as.
+ * Likely prone to be brittle.
+ * TODO: Make sure to test that this is an accurate job name
+ *
+ * @param b The book to predict the download job name of
+ * @return The name of the job that will/is download/ing this book
+ */
+
+ public static String getJobId(Book b) {
+ return "INSTALL_BOOK-" + b.getInitials();
+ }
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/DLProgressEvent.java b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/DLProgressEvent.java
new file mode 100644
index 0000000..b664b12
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/DLProgressEvent.java
@@ -0,0 +1,40 @@
+package org.bspeice.minimalbible.activity.downloader.manager;
+
+import org.crosswire.jsword.book.Book;
+
+/**
+ * Used for notifying that a book's download progress is ongoing
+ */
+public class DLProgressEvent {
+ private final int progress;
+ private final Book b;
+
+ public static final int PROGRESS_COMPLETE = 100;
+ public static final int PROGRESS_BEGINNING = 0;
+
+ public DLProgressEvent(int workDone, int totalWork, Book b) {
+ if (totalWork == 0) {
+ this.progress = 0;
+ } else {
+ this.progress = (int)((float) workDone / totalWork * 100);
+ }
+ this.b = b;
+ }
+
+ public DLProgressEvent(int workDone, Book b) {
+ this.progress = workDone;
+ this.b = b;
+ }
+
+ public int getProgress() {
+ return progress;
+ }
+
+ public float toCircular() {
+ return ((float)progress) * 360 / 100;
+ }
+
+ public Book getB() {
+ return this.b;
+ }
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/DownloadManager.java b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/DownloadManager.java
new file mode 100644
index 0000000..db08f48
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/DownloadManager.java
@@ -0,0 +1,14 @@
+package org.bspeice.minimalbible.activity.downloader.manager;
+
+import org.crosswire.jsword.book.BookCategory;
+
+// TODO: Listen to BookInstall events?
+public class DownloadManager {
+
+ private final String TAG = "DownloadManager";
+
+ // TODO: Inject this, don't have any static references
+ public static final BookCategory[] VALID_CATEGORIES = { BookCategory.BIBLE,
+ BookCategory.COMMENTARY, BookCategory.DICTIONARY,
+ BookCategory.MAPS };
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/InstalledManager.java b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/InstalledManager.java
new file mode 100644
index 0000000..16a1224
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/InstalledManager.java
@@ -0,0 +1,81 @@
+package org.bspeice.minimalbible.activity.downloader.manager;
+
+import android.util.Log;
+
+import org.crosswire.jsword.book.Book;
+import org.crosswire.jsword.book.BookException;
+import org.crosswire.jsword.book.Books;
+import org.crosswire.jsword.book.BooksEvent;
+import org.crosswire.jsword.book.BooksListener;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Manager to keep track of which books have been installed
+ */
+@Singleton
+public class InstalledManager implements BooksListener {
+
+ private Books installedBooks;
+ private List installedBooksList;
+ private String TAG = "InstalledManager";
+
+ @Inject InstalledManager() {}
+
+ /**
+ * Register our manager to receive events on Book install
+ * This is a relatively expensive operation,
+ * so we don't put it in the constructor.
+ */
+ public void initialize() {
+ //TODO: Move this to a true async, rather than separate initialize() function
+ installedBooks = Books.installed();
+ installedBooksList = installedBooks.getBooks();
+ installedBooks.addBooksListener(this);
+ }
+
+ public boolean isInstalled(Book b) {
+ if (installedBooks == null) {
+ initialize();
+ }
+ return installedBooksList.contains(b);
+ }
+
+ @Override
+ public void bookAdded(BooksEvent booksEvent) {
+ Log.d(TAG, "Book added: " + booksEvent.getBook().toString());
+ Book b = booksEvent.getBook();
+ if (!installedBooksList.contains(b)) {
+ installedBooksList.add(b);
+ }
+ }
+
+ @Override
+ public void bookRemoved(BooksEvent booksEvent) {
+ Log.d(TAG, "Book removed: " + booksEvent.getBook().toString());
+ Book b = booksEvent.getBook();
+ if (installedBooksList.contains(b)) {
+ installedBooksList.remove(b);
+ }
+ }
+
+ public void removeBook(Book b) {
+ if (installedBooks == null) {
+ initialize();
+ }
+ // Not sure why we need to call this multiple times, but...
+ while (Books.installed().getBooks().contains(b)) {
+ try {
+ // This worked in the past, but isn't now...
+ // installedBooks.remove(b);
+ Book realBook = installedBooks.getBook(b.getInitials());
+ b.getDriver().delete(realBook);
+ } catch (BookException e) {
+ Log.e("InstalledManager", "Unable to remove book (already uninstalled?): " + e.getLocalizedMessage());
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/RefreshManager.java b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/RefreshManager.java
new file mode 100644
index 0000000..ce5dcf2
--- /dev/null
+++ b/app/src/main/java/org/bspeice/minimalbible/activity/downloader/manager/RefreshManager.java
@@ -0,0 +1,152 @@
+package org.bspeice.minimalbible.activity.downloader.manager;
+
+import android.content.Context;
+
+import org.bspeice.minimalbible.MinimalBible;
+import org.bspeice.minimalbible.activity.downloader.DownloadActivity;
+import org.crosswire.jsword.book.Book;
+import org.crosswire.jsword.book.install.InstallManager;
+import org.crosswire.jsword.book.install.Installer;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+import rx.Observable;
+import rx.functions.Action0;
+import rx.functions.Action1;
+import rx.functions.Func1;
+import rx.functions.Func2;
+import rx.schedulers.Schedulers;
+
+/**
+ * Handle refreshing the list of books available as needed
+ */
+@Singleton
+public class RefreshManager {
+
+ @Inject InstalledManager installedManager;
+
+ /**
+ * Cached copy of modules that are available so we don't refresh for everyone who requests it.
+ */
+ private Observable