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>> availableModules; + private final AtomicBoolean refreshComplete = new AtomicBoolean(); + + @Inject + public RefreshManager(DownloadActivity activity) { + activity.inject(this); + refreshModules(); + } + + /** + * Do the work of kicking off the AsyncTask to refresh books, and make sure we know + * when it's done. + * TODO: Should I have a better way of scheduling than Schedulers.io()? + */ + private void refreshModules() { + if (availableModules == null) { + availableModules = Observable.from(new InstallManager().getInstallers().values()) + .map(new Func1>>() { + @Override + public Map> call(Installer installer) { + Map> map = new HashMap>(); + map.put(installer, installer.getBooks()); + return map; + } + }).subscribeOn(Schedulers.io()) + .cache(); + + // Set refresh complete when it is. + availableModules.observeOn(Schedulers.io()) + .subscribe(new Action1>>() { + @Override + public void call(Map> onNext) {} + }, new Action1() { + @Override + public void call(Throwable onError) {} + }, new Action0() { + @Override + public void call() { + refreshComplete.set(true); + } + }); + } + } + + public Observable>> getAvailableModules() { + return availableModules; + } + + public Observable getAvailableModulesFlattened() { + return availableModules + // First flatten the Map to its lists + .flatMap(new Func1>, Observable>>() { + @Override + public Observable> call(Map> books) { + return Observable.from(books.values()); + } + }) + // Then flatten the lists + .flatMap(new Func1, Observable>() { + @Override + public Observable call(List t1) { + return Observable.from(t1); + } + }); + } + + /** + * Get the cached book list + * @return The cached book list, or null + */ + public List getBookList() { + List availableList = new ArrayList(); + availableModules.reduce(availableList, + new Func2, Map>, List>() { + @Override + public List call(List books, Map> installerListMap) { + for (List l : installerListMap.values()) { + books.addAll(l); + } + return books; + } + }); + return availableList; + } + + /** + * Find the installer that a Book comes from. + * @param b The book to search for + * @return The Installer that should be used for this book. + */ + public Observable installerFromBook(final Book b) { + return availableModules.filter(new Func1>, Boolean>() { + @Override + public Boolean call(Map> installerListMap) { + for (List element : installerListMap.values()) { + if (element.contains(b)) { + return true; + } + } + return false; + } + }) + .first() + .map(new Func1>, Installer>() { + @Override + public Installer call(Map> element) { + return element.entrySet().iterator().next().getKey(); + } + }); + } + + public boolean isRefreshComplete() { + return refreshComplete.get(); + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_action_cancel.png b/app/src/main/res/drawable-hdpi/ic_action_cancel.png new file mode 100644 index 0000000..f889617 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_cancel.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_download.png b/app/src/main/res/drawable-hdpi/ic_action_download.png new file mode 100644 index 0000000..1f3d065 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_download.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_action_search.png b/app/src/main/res/drawable-hdpi/ic_action_search.png new file mode 100644 index 0000000..f594b4e Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_launcher.png b/app/src/main/res/drawable-hdpi/ic_launcher.png index 96a442e..53aa005 100644 Binary files a/app/src/main/res/drawable-hdpi/ic_launcher.png and b/app/src/main/res/drawable-hdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_cancel.png b/app/src/main/res/drawable-mdpi/ic_action_cancel.png new file mode 100644 index 0000000..e84853e Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_cancel.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_download.png b/app/src/main/res/drawable-mdpi/ic_action_download.png new file mode 100644 index 0000000..c2ead0c Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_download.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_action_search.png b/app/src/main/res/drawable-mdpi/ic_action_search.png new file mode 100644 index 0000000..2e446ec Binary files /dev/null and b/app/src/main/res/drawable-mdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-mdpi/ic_launcher.png b/app/src/main/res/drawable-mdpi/ic_launcher.png index 359047d..33dae9a 100644 Binary files a/app/src/main/res/drawable-mdpi/ic_launcher.png and b/app/src/main/res/drawable-mdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_cancel.png b/app/src/main/res/drawable-xhdpi/ic_action_cancel.png new file mode 100644 index 0000000..58e2e3b Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_cancel.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_download.png b/app/src/main/res/drawable-xhdpi/ic_action_download.png new file mode 100644 index 0000000..38a3aee Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_download.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_action_search.png b/app/src/main/res/drawable-xhdpi/ic_action_search.png new file mode 100644 index 0000000..aad535e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-xhdpi/ic_launcher.png b/app/src/main/res/drawable-xhdpi/ic_launcher.png index 71c6d76..9b0565f 100644 Binary files a/app/src/main/res/drawable-xhdpi/ic_launcher.png and b/app/src/main/res/drawable-xhdpi/ic_launcher.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_cancel.png b/app/src/main/res/drawable-xxhdpi/ic_action_cancel.png new file mode 100644 index 0000000..a9bbcde Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_cancel.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_download.png b/app/src/main/res/drawable-xxhdpi/ic_action_download.png new file mode 100644 index 0000000..ef7785a Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_download.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_action_search.png b/app/src/main/res/drawable-xxhdpi/ic_action_search.png new file mode 100644 index 0000000..9c0ea3c Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/ic_action_search.png differ diff --git a/app/src/main/res/drawable-xxhdpi/ic_launcher.png b/app/src/main/res/drawable-xxhdpi/ic_launcher.png index 4df1894..9b8def8 100644 Binary files a/app/src/main/res/drawable-xxhdpi/ic_launcher.png and b/app/src/main/res/drawable-xxhdpi/ic_launcher.png differ diff --git a/app/src/main/res/layout/activity_download.xml b/app/src/main/res/layout/activity_download.xml index 27fabd6..ca79697 100644 --- a/app/src/main/res/layout/activity_download.xml +++ b/app/src/main/res/layout/activity_download.xml @@ -1,31 +1,38 @@ - + tools:context="org.bspeice.minimalbible.activities.downloader.DownloadActivity" > + + - - - - + + + + android:layout_gravity="start" /> diff --git a/app/src/main/res/layout/fragment_download.xml b/app/src/main/res/layout/fragment_download.xml index d0d3abd..ab2dd9f 100644 --- a/app/src/main/res/layout/fragment_download.xml +++ b/app/src/main/res/layout/fragment_download.xml @@ -2,15 +2,14 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" - android:paddingLeft="@dimen/activity_horizontal_margin" - android:paddingRight="@dimen/activity_horizontal_margin" - android:paddingTop="@dimen/activity_vertical_margin" - android:paddingBottom="@dimen/activity_vertical_margin" - tools:context=".DownloadActivity$PlaceholderFragment"> - - + tools:context="org.bspeice.minimalbible.DownloadActivity$PlaceholderFragment" > + diff --git a/app/src/main/res/layout/fragment_navigation_drawer.xml b/app/src/main/res/layout/fragment_navigation_drawer.xml index 5250946..c7a1d25 100644 --- a/app/src/main/res/layout/fragment_navigation_drawer.xml +++ b/app/src/main/res/layout/fragment_navigation_drawer.xml @@ -1,9 +1,11 @@ + tools:context="org.bspeice.minimalbible.activities.BaseNavigationDrawerFragment" /> + diff --git a/app/src/main/res/layout/list_download_items.xml b/app/src/main/res/layout/list_download_items.xml new file mode 100644 index 0000000..4e93430 --- /dev/null +++ b/app/src/main/res/layout/list_download_items.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-v19/styles.xml b/app/src/main/res/values-v19/styles.xml new file mode 100644 index 0000000..cc0a9e8 --- /dev/null +++ b/app/src/main/res/values-v19/styles.xml @@ -0,0 +1,20 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml new file mode 100644 index 0000000..e8555f4 --- /dev/null +++ b/app/src/main/res/values/attrs.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..ab5b77e --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,5 @@ + + + #dddddd + #dddddd + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 92795d1..d42c3f0 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,8 +1,22 @@ - + + + diff --git a/app/src/testConfig/java/org/bspeice/minimalbible/TestModules.java b/app/src/testConfig/java/org/bspeice/minimalbible/TestModules.java index 53bf3a0..c2a0e18 100644 --- a/app/src/testConfig/java/org/bspeice/minimalbible/TestModules.java +++ b/app/src/testConfig/java/org/bspeice/minimalbible/TestModules.java @@ -1,6 +1,7 @@ package org.bspeice.minimalbible; -import org.bspeice.minimalbible.activity.download.DownloadActivity; + +import org.bspeice.minimalbible.activity.downloader.DownloadActivity; import dagger.Module; import dagger.Provides;