From 52701666bcf49ae078558539b2a5144c64f7ba8f Mon Sep 17 00:00:00 2001 From: Alinson Xavier Date: Tue, 28 Jun 2016 12:11:34 -0400 Subject: [PATCH] Start refactoring widgets --- .../widgets/CheckmarkWidgetView/checked.png | Bin 8776 -> 8687 bytes .../implicitly_checked.png | Bin 8684 -> 9651 bytes .../CheckmarkWidgetView/large_size.png | Bin 17291 -> 17039 bytes .../widgets/CheckmarkWidgetView/unchecked.png | Bin 8767 -> 9837 bytes .../org/isoron/uhabits/BaseAndroidTest.java | 58 ++- .../java/org/isoron/uhabits/BaseViewTest.java | 222 ++++++------ .../ui/common/views/FrequencyChartTest.java | 4 +- .../ui/common/views/HistoryChartTest.java | 4 +- .../uhabits/ui/common/views/RingViewTest.java | 4 +- .../ui/common/views/ScoreChartTest.java | 6 +- .../ui/common/views/StreakChartTest.java | 4 +- .../list/views/CheckmarkButtonViewTest.java | 2 +- .../list/views/CheckmarkPanelViewTest.java | 2 +- .../ui/widgets/CheckmarkWidgetTest.java | 90 +++++ .../views/CheckmarkWidgetViewTest.java | 45 ++- app/src/main/AndroidManifest.xml | 96 ++--- .../org/isoron/uhabits/AndroidModule.java | 7 + .../org/isoron/uhabits/BaseComponent.java | 5 + .../uhabits/HabitBroadcastReceiver.java | 13 - .../org/isoron/uhabits/HabitsApplication.java | 30 +- .../org/isoron/uhabits/ui/BaseScreen.java | 2 +- .../org/isoron/uhabits/ui/BaseSystem.java | 17 - .../uhabits/ui/common/views/ScoreChart.java | 2 +- .../ui/habits/list/ListHabitsActivity.java | 58 ++- .../ui/habits/list/ListHabitsController.java | 3 +- .../ui/habits/list/ListHabitsMenu.java | 9 +- .../ui/habits/list/ListHabitsRootView.java | 65 ++-- .../ui/habits/list/ListHabitsScreen.java | 34 +- .../habits/list/ListHabitsSelectionMenu.java | 17 +- .../CheckmarkButtonController.java | 2 +- .../list/controllers/HabitCardController.java | 4 +- .../controllers/HabitCardListController.java | 6 +- .../list/model/HabitCardListAdapter.java | 15 +- .../habits/list/model/HabitCardListCache.java | 20 ++ .../ui/habits/show/ShowHabitActivity.java | 22 +- .../ui/habits/show/ShowHabitScreen.java | 5 +- .../ui/habits/show/views/HistoryCard.java | 1 - .../ui/habits/show/views/ScoreCard.java | 4 +- .../isoron/uhabits/ui/widgets/BaseWidget.java | 196 ++++++++++ .../uhabits/ui/widgets/CheckmarkWidget.java | 96 +++++ .../{ => ui}/widgets/HabitPickerDialog.java | 91 +++-- .../uhabits/ui/widgets/WidgetDimensions.java | 62 ++++ .../uhabits/ui/widgets/WidgetUpdater.java | 98 +++++ .../widgets/views/CheckmarkWidgetView.java | 98 ++--- .../widgets/views/GraphWidgetView.java | 62 ++-- .../widgets/views/HabitWidgetView.java | 14 +- .../{ => ui}/widgets/views/package-info.java | 2 +- .../org/isoron/uhabits/utils/Preferences.java | 25 +- .../uhabits/utils/WidgetPreferences.java | 65 ++++ .../org/isoron/uhabits/utils/WidgetUtils.java | 70 ++++ .../uhabits/widgets/BaseWidgetProvider.java | 335 +++++------------- .../widgets/CheckmarkWidgetProvider.java | 48 +-- .../widgets/FrequencyWidgetProvider.java | 97 +++-- .../widgets/HistoryWidgetProvider.java | 100 +++--- .../uhabits/widgets/ScoreWidgetProvider.java | 106 +++--- .../uhabits/widgets/StreakWidgetProvider.java | 95 ++--- .../isoron/uhabits/widgets/WidgetManager.java | 47 --- .../main/res/xml/widget_checkmark_info.xml | 2 +- .../main/res/xml/widget_frequency_info.xml | 2 +- app/src/main/res/xml/widget_history_info.xml | 2 +- app/src/main/res/xml/widget_score_info.xml | 2 +- app/src/main/res/xml/widget_streak_info.xml | 2 +- .../java/org/isoron/uhabits/TestModule.java | 25 +- 63 files changed, 1561 insertions(+), 1059 deletions(-) create mode 100644 app/src/androidTest/java/org/isoron/uhabits/ui/widgets/CheckmarkWidgetTest.java rename app/src/androidTest/java/org/isoron/uhabits/{ => ui}/widgets/views/CheckmarkWidgetViewTest.java (67%) create mode 100644 app/src/main/java/org/isoron/uhabits/ui/widgets/BaseWidget.java create mode 100644 app/src/main/java/org/isoron/uhabits/ui/widgets/CheckmarkWidget.java rename app/src/main/java/org/isoron/uhabits/{ => ui}/widgets/HabitPickerDialog.java (56%) create mode 100644 app/src/main/java/org/isoron/uhabits/ui/widgets/WidgetDimensions.java create mode 100644 app/src/main/java/org/isoron/uhabits/ui/widgets/WidgetUpdater.java rename app/src/main/java/org/isoron/uhabits/{ => ui}/widgets/views/CheckmarkWidgetView.java (81%) rename app/src/main/java/org/isoron/uhabits/{ => ui}/widgets/views/GraphWidgetView.java (56%) rename app/src/main/java/org/isoron/uhabits/{ => ui}/widgets/views/HabitWidgetView.java (92%) rename app/src/main/java/org/isoron/uhabits/{ => ui}/widgets/views/package-info.java (95%) create mode 100644 app/src/main/java/org/isoron/uhabits/utils/WidgetPreferences.java create mode 100644 app/src/main/java/org/isoron/uhabits/utils/WidgetUtils.java delete mode 100644 app/src/main/java/org/isoron/uhabits/widgets/WidgetManager.java diff --git a/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/checked.png b/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/checked.png index 69f650a0baa7c0440109697305bacab9119353af..1437a510bc0bb1607b533b955f923402bf6a4dc0 100644 GIT binary patch literal 8687 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 literal 8776 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 diff --git a/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/implicitly_checked.png b/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/implicitly_checked.png index 992a9e5b2639e94a13104593fa338858e37861a5..97fdcbd198e850737320a8aa7ef82fdbbaf42078 100644 GIT binary patch literal 9651 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 Q5cpA3)_Pc>Xz}|009tm3XaE2J diff --git a/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/unchecked.png b/app/src/androidTest/assets/views/widgets/CheckmarkWidgetView/unchecked.png index 576d369ec4ccb8cda7aa1cadeb1875ed0bde3ff8..2f64db223355b1d2276d48453835f2dbb320750b 100644 GIT binary patch literal 9837 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 literal 8767 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 M%0RtJ)h_gZ0P*B7E&u=k diff --git a/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java b/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java index 5b94835e2..0123be685 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java @@ -19,21 +19,25 @@ package org.isoron.uhabits; -import android.content.Context; -import android.os.Build; -import android.os.Looper; -import android.support.test.InstrumentationRegistry; +import android.appwidget.*; +import android.content.*; +import android.os.*; +import android.support.test.*; -import org.isoron.uhabits.models.HabitList; -import org.isoron.uhabits.tasks.BaseTask; -import org.isoron.uhabits.utils.DateUtils; -import org.isoron.uhabits.utils.InterfaceUtils; -import org.isoron.uhabits.utils.Preferences; -import org.junit.Before; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; -import java.util.concurrent.TimeoutException; +import java.util.*; +import java.util.concurrent.*; -import javax.inject.Inject; +import javax.inject.*; + +import static junit.framework.Assert.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; public class BaseAndroidTest { @@ -52,6 +56,9 @@ public class BaseAndroidTest @Inject protected HabitList habitList; + @Inject + protected CommandRunner commandRunner; + protected AndroidTestComponent androidTestComponent; protected HabitFixtures fixtures; @@ -78,6 +85,18 @@ public class BaseAndroidTest fixtures = new HabitFixtures(habitList); } + protected void sleep(int time) + { + try + { + Thread.sleep(time); + } + catch (InterruptedException e) + { + fail(); + } + } + protected void waitForAsyncTasks() throws InterruptedException, TimeoutException { @@ -89,4 +108,19 @@ public class BaseAndroidTest BaseTask.waitForTasks(10000); } + + protected void assertWidgetProviderIsInstalled(ComponentName desiredProvider) + { + AppWidgetManager manager = AppWidgetManager.getInstance(targetContext); + List providerInfoList; + List installedProviders; + + providerInfoList = manager.getInstalledProviders(); + installedProviders = new LinkedList<>(); + + for (AppWidgetProviderInfo info : providerInfoList) + installedProviders.add(info.provider); + + assertThat(installedProviders, hasItems(desiredProvider)); + } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java index 32dac96cb..b7d2d1af5 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/BaseViewTest.java @@ -21,10 +21,11 @@ package org.isoron.uhabits; import android.graphics.*; import android.os.*; +import android.support.annotation.*; import android.view.*; +import android.widget.*; -import org.isoron.uhabits.tasks.*; -import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.ui.widgets.*; import org.isoron.uhabits.utils.*; import java.io.*; @@ -34,7 +35,9 @@ import static junit.framework.Assert.*; public class BaseViewTest extends BaseAndroidTest { protected static final double DEFAULT_SIMILARITY_CUTOFF = 0.09; + public static final int HISTOGRAM_BIN_SIZE = 8; + private double similarityCutoff; @Override @@ -44,21 +47,8 @@ public class BaseViewTest extends BaseAndroidTest similarityCutoff = DEFAULT_SIMILARITY_CUTOFF; } - protected void setSimilarityCutoff(double similarityCutoff) - { - this.similarityCutoff = similarityCutoff; - } - - protected void measureView(int width, int height, View view) - { - int specWidth = View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); - int specHeight = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY); - - view.measure(specWidth, specHeight); - view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); - } - - protected void assertRenders(View view, String expectedImagePath) throws IOException + protected void assertRenders(View view, String expectedImagePath) + throws IOException { StringBuilder errorMessage = new StringBuilder(); expectedImagePath = getVersionedViewAssetPath(expectedImagePath); @@ -70,98 +60,122 @@ public class BaseViewTest extends BaseAndroidTest int width = actual.getWidth(); int height = actual.getHeight(); - Bitmap scaledExpected = Bitmap.createScaledBitmap(expected, width, height, true); + Bitmap scaledExpected = + Bitmap.createScaledBitmap(expected, width, height, true); double distance; boolean similarEnough = true; - if ((distance = compareHistograms(getHistogram(actual), getHistogram(scaledExpected))) > - similarityCutoff) + if ((distance = compareHistograms(getHistogram(actual), + getHistogram(scaledExpected))) > similarityCutoff) { similarEnough = false; errorMessage.append(String.format( - "Rendered image has wrong histogram (distance=%f). ", - distance)); + "Rendered image has wrong histogram (distance=%f). ", + distance)); } - if(!similarEnough) + if (!similarEnough) { saveBitmap(expectedImagePath, ".expected", scaledExpected); String path = saveBitmap(expectedImagePath, "", actual); - errorMessage.append(String.format("Actual rendered image " + "saved to %s", path)); + errorMessage.append( + String.format("Actual rendered image " + "saved to %s", path)); fail(errorMessage.toString()); } - actual.recycle(); expected.recycle(); scaledExpected.recycle(); } - private Bitmap getBitmapFromAssets(String path) throws IOException + protected int dpToPixels(int dp) { - InputStream stream = testContext.getAssets().open(path); - return BitmapFactory.decodeStream(stream); + return (int) InterfaceUtils.dpToPixels(targetContext, dp); } - private String getVersionedViewAssetPath(String path) + protected void measureView(View view, int width, int height) { - String result = null; + int specWidth = + View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY); + int specHeight = + View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.EXACTLY); - if (android.os.Build.VERSION.SDK_INT >= 21) - { - try - { - String vpath = "views-v21/" + path; - testContext.getAssets().open(vpath); - result = vpath; - } - catch (IOException e) - { - // ignored - } - } + view.measure(specWidth, specHeight); + view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); + } - if(result == null) - result = "views/" + path; + protected void setSimilarityCutoff(double similarityCutoff) + { + this.similarityCutoff = similarityCutoff; + } - return result; + protected void tap(GestureDetector.OnGestureListener view, int x, int y) + throws InterruptedException + { + long now = SystemClock.uptimeMillis(); + MotionEvent e = + MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, dpToPixels(x), + dpToPixels(y), 0); + view.onSingleTapUp(e); + e.recycle(); } - private String saveBitmap(String filename, String suffix, Bitmap bitmap) - throws IOException + private double compareHistograms(int[][] actualHistogram, + int[][] expectedHistogram) { - File dir = FileUtils.getSDCardDir("test-screenshots"); - if(dir == null) dir = FileUtils.getFilesDir("test-screenshots"); - if(dir == null) throw new RuntimeException("Could not find suitable dir for screenshots"); + long diff = 0; + long total = 0; - filename = filename.replaceAll("\\.png$", suffix + ".png"); - String absolutePath = String.format("%s/%s", dir.getAbsolutePath(), filename); + for (int i = 0; i < 256 / HISTOGRAM_BIN_SIZE; i++) + { + diff += Math.abs(actualHistogram[0][i] - expectedHistogram[0][i]); + diff += Math.abs(actualHistogram[1][i] - expectedHistogram[1][i]); + diff += Math.abs(actualHistogram[2][i] - expectedHistogram[2][i]); + diff += Math.abs(actualHistogram[3][i] - expectedHistogram[3][i]); - File parent = new File(absolutePath).getParentFile(); - if(!parent.exists() && !parent.mkdirs()) - throw new RuntimeException(String.format("Could not create dir: %s", - parent.getAbsolutePath())); + total += actualHistogram[0][i]; + total += actualHistogram[1][i]; + total += actualHistogram[2][i]; + total += actualHistogram[3][i]; + } - FileOutputStream out = new FileOutputStream(absolutePath); - bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + return (double) diff / total / 2; + } - return absolutePath; + @NonNull + protected FrameLayout convertToView(BaseWidget widget, + int width, + int height) + { + widget.setDimensions( + new WidgetDimensions(width, height, width, height)); + FrameLayout view = new FrameLayout(targetContext); + RemoteViews remoteViews = widget.getPortraitRemoteViews(); + view.addView(remoteViews.apply(targetContext, view)); + measureView(view, width, height); + return view; + } + + private Bitmap getBitmapFromAssets(String path) throws IOException + { + InputStream stream = testContext.getAssets().open(path); + return BitmapFactory.decodeStream(stream); } private int[][] getHistogram(Bitmap bitmap) { int histogram[][] = new int[4][256 / HISTOGRAM_BIN_SIZE]; - for(int x = 0; x < bitmap.getWidth(); x++) + for (int x = 0; x < bitmap.getWidth(); x++) { - for(int y = 0; y < bitmap.getHeight(); y++) + for (int y = 0; y < bitmap.getHeight(); y++) { int color = bitmap.getPixel(x, y); int[] argb = new int[]{ - (color >> 24) & 0xff, //alpha - (color >> 16) & 0xff, //red - (color >> 8) & 0xff, //green - (color ) & 0xff //blue + (color >> 24) & 0xff, //alpha + (color >> 16) & 0xff, //red + (color >> 8) & 0xff, //green + (color) & 0xff //blue }; histogram[0][argb[0] / HISTOGRAM_BIN_SIZE]++; @@ -174,59 +188,49 @@ public class BaseViewTest extends BaseAndroidTest return histogram; } - private double compareHistograms(int[][] actualHistogram, int[][] expectedHistogram) + private String getVersionedViewAssetPath(String path) { - long diff = 0; - long total = 0; + String result = null; - for(int i = 0; i < 256 / HISTOGRAM_BIN_SIZE; i ++) + if (android.os.Build.VERSION.SDK_INT >= 21) { - diff += Math.abs(actualHistogram[0][i] - expectedHistogram[0][i]); - diff += Math.abs(actualHistogram[1][i] - expectedHistogram[1][i]); - diff += Math.abs(actualHistogram[2][i] - expectedHistogram[2][i]); - diff += Math.abs(actualHistogram[3][i] - expectedHistogram[3][i]); - - total += actualHistogram[0][i]; - total += actualHistogram[1][i]; - total += actualHistogram[2][i]; - total += actualHistogram[3][i]; + try + { + String vpath = "views-v21/" + path; + testContext.getAssets().open(vpath); + result = vpath; + } + catch (IOException e) + { + // ignored + } } - return (double) diff / total / 2; - } + if (result == null) result = "views/" + path; - protected int dpToPixels(int dp) - { - return (int) InterfaceUtils.dpToPixels(targetContext, dp); + return result; } - protected void tap(GestureDetector.OnGestureListener view, int x, int y) throws InterruptedException + private String saveBitmap(String filename, String suffix, Bitmap bitmap) + throws IOException { - long now = SystemClock.uptimeMillis(); - MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, dpToPixels(x), - dpToPixels(y), 0); - view.onSingleTapUp(e); - e.recycle(); - } + File dir = FileUtils.getSDCardDir("test-screenshots"); + if (dir == null) dir = FileUtils.getFilesDir("test-screenshots"); + if (dir == null) throw new RuntimeException( + "Could not find suitable dir for screenshots"); - protected void refreshData(final HabitChart view) - { - new BaseTask() - { - @Override - protected void doInBackground() - { - view.refreshData(); - } - }.execute(); + filename = filename.replaceAll("\\.png$", suffix + ".png"); + String absolutePath = + String.format("%s/%s", dir.getAbsolutePath(), filename); - try - { - waitForAsyncTasks(); - } - catch (Exception e) - { - throw new RuntimeException("Time out"); - } + File parent = new File(absolutePath).getParentFile(); + if (!parent.exists() && !parent.mkdirs()) throw new RuntimeException( + String.format("Could not create dir: %s", + parent.getAbsolutePath())); + + FileOutputStream out = new FileOutputStream(absolutePath); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); + + return absolutePath; } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/FrequencyChartTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/FrequencyChartTest.java index 2240254f3..2d21a6dd1 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/FrequencyChartTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/FrequencyChartTest.java @@ -48,7 +48,7 @@ public class FrequencyChartTest extends BaseViewTest view = new FrequencyChart(targetContext); view.setFrequency(habit.getRepetitions().getWeekdayFrequency()); view.setColor(ColorUtils.getAndroidTestColor(habit.getColor())); - measureView(dpToPixels(300), dpToPixels(100), view); + measureView(view, dpToPixels(300), dpToPixels(100)); } @Test @@ -69,7 +69,7 @@ public class FrequencyChartTest extends BaseViewTest @Test public void testRender_withDifferentSize() throws Throwable { - measureView(dpToPixels(200), dpToPixels(200), view); + measureView(view, dpToPixels(200), dpToPixels(200)); assertRenders(view, BASE_PATH + "renderDifferentSize.png"); } diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/HistoryChartTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/HistoryChartTest.java index af5b3f3f5..9a6c99640 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/HistoryChartTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/HistoryChartTest.java @@ -48,7 +48,7 @@ public class HistoryChartTest extends BaseViewTest chart = new HistoryChart(targetContext); chart.setCheckmarks(habit.getCheckmarks().getAllValues()); chart.setColor(ColorUtils.getAndroidTestColor(habit.getColor())); - measureView(dpToPixels(400), dpToPixels(200), chart); + measureView(chart, dpToPixels(400), dpToPixels(200)); } // @Test @@ -106,7 +106,7 @@ public class HistoryChartTest extends BaseViewTest @Test public void testRender_withDifferentSize() throws Throwable { - measureView(dpToPixels(200), dpToPixels(200), chart); + measureView(chart, dpToPixels(200), dpToPixels(200)); assertRenders(chart, BASE_PATH + "renderDifferentSize.png"); } diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/RingViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/RingViewTest.java index 7512bc107..771e1d17c 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/RingViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/RingViewTest.java @@ -55,7 +55,7 @@ public class RingViewTest extends BaseViewTest @Test public void testRender_base() throws IOException { - measureView(dpToPixels(100), dpToPixels(100), view); + measureView(view, dpToPixels(100), dpToPixels(100)); assertRenders(view, BASE_PATH + "render.png"); } @@ -65,7 +65,7 @@ public class RingViewTest extends BaseViewTest view.setPercentage(0.25f); view.setColor(ColorUtils.getAndroidTestColor(5)); - measureView(dpToPixels(200), dpToPixels(200), view); + measureView(view, dpToPixels(200), dpToPixels(200)); assertRenders(view, BASE_PATH + "renderDifferentParams.png"); } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/ScoreChartTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/ScoreChartTest.java index 50346aa17..b56bd5704 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/ScoreChartTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/ScoreChartTest.java @@ -50,9 +50,9 @@ public class ScoreChartTest extends BaseViewTest view = new ScoreChart(targetContext); view.setScores(habit.getScores().getAll()); - view.setPrimaryColor(ColorUtils.getColor(targetContext, habit.getColor())); + view.setColor(ColorUtils.getColor(targetContext, habit.getColor())); view.setBucketSize(7); - measureView(dpToPixels(300), dpToPixels(200), view); + measureView(view, dpToPixels(300), dpToPixels(200)); } @Test @@ -75,7 +75,7 @@ public class ScoreChartTest extends BaseViewTest @Test public void testRender_withDifferentSize() throws Throwable { - measureView(dpToPixels(200), dpToPixels(200), view); + measureView(view, dpToPixels(200), dpToPixels(200)); assertRenders(view, BASE_PATH + "renderDifferentSize.png"); } diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/StreakChartTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/StreakChartTest.java index 44f7c7280..3ca002117 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/StreakChartTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/common/views/StreakChartTest.java @@ -48,7 +48,7 @@ public class StreakChartTest extends BaseViewTest view = new StreakChart(targetContext); view.setColor(ColorUtils.getAndroidTestColor(habit.getColor())); view.setStreaks(habit.getStreaks().getBest(5)); - measureView(dpToPixels(300), dpToPixels(100), view); + measureView(view, dpToPixels(300), dpToPixels(100)); } @Test @@ -60,7 +60,7 @@ public class StreakChartTest extends BaseViewTest @Test public void testRender_withSmallSize() throws Throwable { - measureView(dpToPixels(100), dpToPixels(100), view); + measureView(view, dpToPixels(100), dpToPixels(100)); assertRenders(view, BASE_PATH + "renderSmallSize.png"); } diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkButtonViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkButtonViewTest.java index e1c474d25..b55eefce1 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkButtonViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkButtonViewTest.java @@ -54,7 +54,7 @@ public class CheckmarkButtonViewTest extends BaseViewTest view.setValue(Checkmark.UNCHECKED); view.setColor(ColorUtils.getAndroidTestColor(7)); - measureView(dpToPixels(40), dpToPixels(40), view); + measureView(view, dpToPixels(40), dpToPixels(40)); } @Test diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkPanelViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkPanelViewTest.java index 684f82746..878dc5c9f 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkPanelViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/habits/list/views/CheckmarkPanelViewTest.java @@ -62,7 +62,7 @@ public class CheckmarkPanelViewTest extends BaseViewTest view.setCheckmarkValues(checkmarks); view.setColor(ColorUtils.getAndroidTestColor(7)); - measureView(dpToPixels(200), dpToPixels(200), view); + measureView(view, dpToPixels(200), dpToPixels(200)); } // protected void waitForLatch() throws InterruptedException diff --git a/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/CheckmarkWidgetTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/CheckmarkWidgetTest.java new file mode 100644 index 000000000..20a25bb07 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/CheckmarkWidgetTest.java @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.ui.widgets; + +import android.content.*; +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.widgets.*; +import org.junit.*; +import org.junit.runner.*; + +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.Matchers.*; +import static org.isoron.uhabits.models.Checkmark.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class CheckmarkWidgetTest extends BaseViewTest +{ + + private static final String PATH = "widgets/CheckmarkWidgetView/"; + + private Habit habit; + + private CheckmarkList checkmarks; + + private FrameLayout view; + + @Override + public void setUp() + { + super.setUp(); + habit = fixtures.createShortHabit(); + checkmarks = habit.getCheckmarks(); + CheckmarkWidget widget = new CheckmarkWidget(targetContext, 0, habit); + view = convertToView(widget, 200, 250); + + assertThat(checkmarks.getTodayValue(), equalTo(CHECKED_EXPLICITLY)); + } + + @Test + public void testClick() throws Exception + { + Button button = (Button) view.findViewById(R.id.button); + assertThat(button, is(not(nullValue()))); + + // A better test would be to capture the intent, but it doesn't seem + // possible to capture intents sent to BroadcastReceivers. + button.performClick(); + sleep(1000); + + assertThat(checkmarks.getTodayValue(), equalTo(CHECKED_IMPLICITLY)); + } + + @Test + public void testIsInstalled() + { + ComponentName provider = + new ComponentName(targetContext, CheckmarkWidgetProvider.class); + + assertWidgetProviderIsInstalled(provider); + } + + @Test + public void testRender() throws Exception + { + assertRenders(view, PATH + "checked.png"); + } +} diff --git a/app/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/views/CheckmarkWidgetViewTest.java similarity index 67% rename from app/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java rename to app/src/androidTest/java/org/isoron/uhabits/ui/widgets/views/CheckmarkWidgetViewTest.java index 9b7fc14d3..fe64291af 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetViewTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/ui/widgets/views/CheckmarkWidgetViewTest.java @@ -17,20 +17,18 @@ * with this program. If not, see . */ -package org.isoron.uhabits.widgets.views; +package org.isoron.uhabits.ui.widgets.views; -import android.support.test.runner.AndroidJUnit4; +import android.support.test.runner.*; import android.test.suitebuilder.annotation.*; import org.isoron.uhabits.*; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.utils.DateUtils; -import org.isoron.uhabits.utils.InterfaceUtils; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; +import org.junit.*; +import org.junit.runner.*; -import java.io.IOException; +import java.io.*; @RunWith(AndroidJUnit4.class) @MediumTest @@ -51,9 +49,16 @@ public class CheckmarkWidgetViewTest extends BaseViewTest habit = fixtures.createShortHabit(); view = new CheckmarkWidgetView(targetContext); - view.setHabit(habit); - refreshData(view); - measureView(dpToPixels(100), dpToPixels(200), view); + int color = ColorUtils.getAndroidTestColor(habit.getColor()); + int score = habit.getScores().getTodayValue(); + float percentage = (float) score / Score.MAX_VALUE; + + view.setActiveColor(color); + view.setCheckmarkValue(habit.getCheckmarks().getTodayValue()); + view.setPercentage(percentage); + view.setName(habit.getName()); + view.refresh(); + measureView(view, dpToPixels(100), dpToPixels(200)); } @Test @@ -65,29 +70,23 @@ public class CheckmarkWidgetViewTest extends BaseViewTest @Test public void testRender_implicitlyChecked() throws IOException { - long today = DateUtils.getStartOfToday(); - long day = DateUtils.millisecondsInOneDay; - habit.getRepetitions().toggleTimestamp(today); - habit.getRepetitions().toggleTimestamp(today - day); - habit.getRepetitions().toggleTimestamp(today - 2 * day); - view.refreshData(); - + view.setCheckmarkValue(Checkmark.CHECKED_IMPLICITLY); + view.refresh(); assertRenders(view, PATH + "implicitly_checked.png"); } @Test public void testRender_largeSize() throws IOException { - measureView(dpToPixels(300), dpToPixels(300), view); + measureView(view, dpToPixels(300), dpToPixels(300)); assertRenders(view, PATH + "large_size.png"); } @Test public void testRender_unchecked() throws IOException { - habit.getRepetitions().toggleTimestamp(DateUtils.getStartOfToday()); - view.refreshData(); - + view.setCheckmarkValue(Checkmark.UNCHECKED); + view.refresh(); assertRenders(view, PATH + "unchecked.png"); } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index fe419041b..3382151ec 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -81,7 +81,7 @@ android:theme="@style/Theme.AppCompat.Light.NoActionBar"/> @@ -108,53 +108,53 @@ android:resource="@xml/widget_checkmark_info"/> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/org/isoron/uhabits/AndroidModule.java b/app/src/main/java/org/isoron/uhabits/AndroidModule.java index 21f5bb008..3a371d2e8 100644 --- a/app/src/main/java/org/isoron/uhabits/AndroidModule.java +++ b/app/src/main/java/org/isoron/uhabits/AndroidModule.java @@ -63,4 +63,11 @@ public class AndroidModule { return new Preferences(); } + + @Provides + @Singleton + WidgetPreferences provideWidgetPreferences() + { + return new WidgetPreferences(); + } } diff --git a/app/src/main/java/org/isoron/uhabits/BaseComponent.java b/app/src/main/java/org/isoron/uhabits/BaseComponent.java index 28f39fe9a..feb93f511 100644 --- a/app/src/main/java/org/isoron/uhabits/BaseComponent.java +++ b/app/src/main/java/org/isoron/uhabits/BaseComponent.java @@ -30,6 +30,7 @@ import org.isoron.uhabits.ui.habits.list.controllers.*; import org.isoron.uhabits.ui.habits.list.model.*; import org.isoron.uhabits.ui.habits.list.views.*; import org.isoron.uhabits.ui.habits.show.*; +import org.isoron.uhabits.ui.widgets.*; import org.isoron.uhabits.widgets.*; /** @@ -90,4 +91,8 @@ public interface BaseComponent void inject(BaseDialogFragment baseDialogFragment); void inject(ShowHabitController showHabitController); + + void inject(BaseWidget baseWidget); + + void inject(WidgetUpdater widgetManager); } diff --git a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java index 5cbdb5bfe..6a0d1f431 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java +++ b/app/src/main/java/org/isoron/uhabits/HabitBroadcastReceiver.java @@ -27,14 +27,12 @@ import android.os.*; import android.preference.*; import android.support.v4.app.*; import android.support.v4.app.TaskStackBuilder; -import android.support.v4.content.*; import org.isoron.uhabits.commands.*; import org.isoron.uhabits.models.*; import org.isoron.uhabits.tasks.*; import org.isoron.uhabits.ui.habits.show.*; import org.isoron.uhabits.utils.*; -import org.isoron.uhabits.widgets.*; import java.util.*; @@ -124,16 +122,6 @@ public class HabitBroadcastReceiver extends BroadcastReceiver notificationManager.cancel(notificationId); } - public static void sendRefreshBroadcast(Context context) - { - LocalBroadcastManager manager = - LocalBroadcastManager.getInstance(context); - Intent refreshIntent = new Intent(HabitsApplication.ACTION_REFRESH); - manager.sendBroadcast(refreshIntent); - - WidgetManager.updateWidgets(context); - } - @Override public void onReceive(final Context context, Intent intent) { @@ -178,7 +166,6 @@ public class HabitBroadcastReceiver extends BroadcastReceiver } dismissNotification(context, habitId); - sendRefreshBroadcast(context); } private boolean checkWeekday(Intent intent, Habit habit) diff --git a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java index 72e3c38c3..53fb23d24 100644 --- a/app/src/main/java/org/isoron/uhabits/HabitsApplication.java +++ b/app/src/main/java/org/isoron/uhabits/HabitsApplication.java @@ -25,21 +25,16 @@ import android.support.annotation.*; import com.activeandroid.*; -import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.widgets.*; import org.isoron.uhabits.utils.*; import java.io.*; -import javax.inject.*; - /** * The Android application for Loop Habit Tracker. */ public class HabitsApplication extends Application { - public static final String ACTION_REFRESH = - "org.isoron.uhabits.ACTION_REFRESH"; - public static final int RESULT_BUG_REPORT = 4; public static final int RESULT_EXPORT_CSV = 2; @@ -56,19 +51,13 @@ public class HabitsApplication extends Application @Nullable private static Context context; - @Inject - HabitList habitList; + private static WidgetUpdater widgetManager; public static BaseComponent getComponent() { return component; } - public HabitList getHabitList() - { - return habitList; - } - public static void setComponent(BaseComponent component) { HabitsApplication.component = component; @@ -86,6 +75,15 @@ public class HabitsApplication extends Application return application; } + @NonNull + public static WidgetUpdater getWidgetManager() + { + if (widgetManager == null) + throw new RuntimeException("widgetManager is null"); + + return widgetManager; + } + public static boolean isTestMode() { try @@ -108,6 +106,7 @@ public class HabitsApplication extends Application HabitsApplication.context = this; HabitsApplication.application = this; component = DaggerAndroidComponent.builder().build(); + component.inject(this); if (isTestMode()) { @@ -115,7 +114,9 @@ public class HabitsApplication extends Application if (db.exists()) db.delete(); } - component.inject(this); + widgetManager = new WidgetUpdater(this); + widgetManager.startListening(); + DatabaseUtils.initializeActiveAndroid(); } @@ -124,6 +125,7 @@ public class HabitsApplication extends Application { HabitsApplication.context = null; ActiveAndroid.dispose(); + widgetManager.stopListening(); super.onTerminate(); } } diff --git a/app/src/main/java/org/isoron/uhabits/ui/BaseScreen.java b/app/src/main/java/org/isoron/uhabits/ui/BaseScreen.java index 910896284..08b30526b 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/BaseScreen.java +++ b/app/src/main/java/org/isoron/uhabits/ui/BaseScreen.java @@ -160,7 +160,7 @@ public abstract class BaseScreen * * @param rootView the root view for this screen. */ - public void setRootView(@Nullable BaseRootView rootView) + protected void setRootView(@Nullable BaseRootView rootView) { this.rootView = rootView; activity.setContentView(rootView); diff --git a/app/src/main/java/org/isoron/uhabits/ui/BaseSystem.java b/app/src/main/java/org/isoron/uhabits/ui/BaseSystem.java index e1c7e106c..ec39c0270 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/BaseSystem.java +++ b/app/src/main/java/org/isoron/uhabits/ui/BaseSystem.java @@ -28,7 +28,6 @@ import org.isoron.uhabits.*; import org.isoron.uhabits.models.*; import org.isoron.uhabits.tasks.*; import org.isoron.uhabits.utils.*; -import org.isoron.uhabits.widgets.*; import java.io.*; import java.lang.Process; @@ -119,22 +118,6 @@ public class BaseSystem }.execute(); } - /** - * Refreshes all application widgets. - */ - public void updateWidgets() - { - new BaseTask() - { - - @Override - protected void doInBackground() - { - WidgetManager.updateWidgets(context); - } - }.execute(); - } - private String getDeviceInfo() { if (context == null) return "null context\n"; diff --git a/app/src/main/java/org/isoron/uhabits/ui/common/views/ScoreChart.java b/app/src/main/java/org/isoron/uhabits/ui/common/views/ScoreChart.java index 9afad1595..969be04de 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/common/views/ScoreChart.java +++ b/app/src/main/java/org/isoron/uhabits/ui/common/views/ScoreChart.java @@ -137,7 +137,7 @@ public class ScoreChart extends ScrollableChart requestLayout(); } - public void setPrimaryColor(int primaryColor) + public void setColor(int primaryColor) { this.primaryColor = primaryColor; postInvalidate(); diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsActivity.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsActivity.java index 9ca8d433e..2a93436bb 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsActivity.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsActivity.java @@ -19,14 +19,14 @@ package org.isoron.uhabits.ui.habits.list; -import android.os.Bundle; +import android.os.*; -import org.isoron.uhabits.HabitsApplication; -import org.isoron.uhabits.models.HabitList; -import org.isoron.uhabits.ui.BaseActivity; -import org.isoron.uhabits.ui.BaseSystem; +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.*; +import org.isoron.uhabits.ui.habits.list.model.*; -import javax.inject.Inject; +import javax.inject.*; /** * Activity that allows the user to see and modify the list of habits. @@ -36,19 +36,55 @@ public class ListHabitsActivity extends BaseActivity @Inject HabitList habitList; + private HabitCardListAdapter adapter; + + private ListHabitsRootView rootView; + + private ListHabitsScreen screen; + + private ListHabitsMenu menu; + + private ListHabitsSelectionMenu selectionMenu; + + private ListHabitsController controller; + + private BaseSystem system; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); HabitsApplication.getComponent().inject(this); - BaseSystem system = new BaseSystem(this); - ListHabitsScreen screen = new ListHabitsScreen(this); - ListHabitsController controller = - new ListHabitsController(screen, system, habitList); + int checkmarkCount = ListHabitsRootView.MAX_CHECKMARK_COUNT; + + system = new BaseSystem(this); + adapter = new HabitCardListAdapter(checkmarkCount); + rootView = new ListHabitsRootView(this, adapter); + screen = new ListHabitsScreen(this, rootView); + menu = new ListHabitsMenu(this, screen, adapter); + selectionMenu = new ListHabitsSelectionMenu(screen, adapter); + controller = new ListHabitsController(screen, system, habitList); + + screen.setMenu(menu); + screen.setSelectionMenu(selectionMenu); + rootView.setController(controller, selectionMenu); - screen.setController(controller); setScreen(screen); controller.onStartup(); } + + @Override + protected void onPause() + { + adapter.cancelRefresh(); + super.onPause(); + } + + @Override + protected void onResume() + { + super.onResume(); + adapter.refresh(); + } } diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsController.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsController.java index 6af715be0..f36dc42f8 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsController.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsController.java @@ -95,7 +95,7 @@ public class ListHabitsController habitList.reorder(from, to); } - public void onImportData(File file) + public void onImportData(@NonNull File file) { ImportDataTask task = new ImportDataTask(file, screen.getProgressBar()); task.setListener(this); @@ -163,7 +163,6 @@ public class ListHabitsController if (prefs.isFirstRun()) onFirstRun(); new Handler().postDelayed(() -> { - system.updateWidgets(); system.scheduleReminders(); }, 1000); } diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsMenu.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsMenu.java index 8349499f9..b518a33be 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsMenu.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsMenu.java @@ -26,6 +26,7 @@ import android.view.MenuItem; import org.isoron.uhabits.R; import org.isoron.uhabits.ui.BaseActivity; import org.isoron.uhabits.ui.BaseMenu; +import org.isoron.uhabits.ui.habits.list.model.*; import org.isoron.uhabits.utils.InterfaceUtils; public class ListHabitsMenu extends BaseMenu @@ -35,11 +36,15 @@ public class ListHabitsMenu extends BaseMenu private boolean showArchived; + private final HabitCardListAdapter adapter; + public ListHabitsMenu(@NonNull BaseActivity activity, - @NonNull ListHabitsScreen screen) + @NonNull ListHabitsScreen screen, + @NonNull HabitCardListAdapter adapter) { super(activity); this.screen = screen; + this.adapter = adapter; } @Override @@ -79,7 +84,7 @@ public class ListHabitsMenu extends BaseMenu case R.id.action_show_archived: showArchived = !showArchived; - screen.getRootView().setShowArchived(showArchived); + adapter.setShowArchived(showArchived); invalidate(); return true; diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsRootView.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsRootView.java index 65c3b1b47..d534098f7 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsRootView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsRootView.java @@ -59,13 +59,27 @@ public class ListHabitsRootView extends BaseRootView @BindView(R.id.hintView) HintView hintView; - @Nullable - private HabitCardListAdapter listAdapter; + @NonNull + private final HabitCardListAdapter listAdapter; - public ListHabitsRootView(@NonNull Context context) + public ListHabitsRootView(@NonNull Context context, + @NonNull HabitCardListAdapter listAdapter) { super(context); - init(); + addView(inflate(getContext(), R.layout.list_habits, null)); + ButterKnife.bind(this); + + this.listAdapter = listAdapter; + listView.setAdapter(listAdapter); + listAdapter.setListView(listView); + + tvStarEmpty.setTypeface(InterfaceUtils.getFontAwesome(getContext())); + initToolbar(); + + String hints[] = + getContext().getResources().getStringArray(R.array.hints); + HintList hintList = new HintList(hints); + hintView.setHints(hintList); } public static int getCheckmarkCount(View v) @@ -84,12 +98,6 @@ public class ListHabitsRootView extends BaseRootView return progressBar; } - public void setShowArchived(boolean showArchived) - { - if (listAdapter == null) return; - listAdapter.setShowArchived(showArchived); - } - @NonNull @Override public Toolbar getToolbar() @@ -103,12 +111,9 @@ public class ListHabitsRootView extends BaseRootView updateEmptyView(); } - public void setController(@Nullable ListHabitsController controller, - @Nullable ListHabitsSelectionMenu menu) + public void setController(@NonNull ListHabitsController controller, + @NonNull ListHabitsSelectionMenu menu) { - listView.setController(null); - if (controller == null || menu == null || listAdapter == null) return; - HabitCardListController listController = new HabitCardListController(listAdapter, listView); @@ -118,49 +123,23 @@ public class ListHabitsRootView extends BaseRootView menu.setListController(listController); } - public void setListAdapter(@NonNull HabitCardListAdapter listAdapter) - { - if (this.listAdapter != null) - listAdapter.getObservable().removeListener(this); - - this.listAdapter = listAdapter; - listView.setAdapter(listAdapter); - listAdapter.setListView(listView); - } - @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); - if (listAdapter != null) listAdapter.getObservable().addListener(this); + listAdapter.getObservable().addListener(this); } @Override protected void onDetachedFromWindow() { - if (listAdapter != null) - listAdapter.getObservable().removeListener(this); + listAdapter.getObservable().removeListener(this); super.onDetachedFromWindow(); } - private void init() - { - addView(inflate(getContext(), R.layout.list_habits, null)); - ButterKnife.bind(this); - - tvStarEmpty.setTypeface(InterfaceUtils.getFontAwesome(getContext())); - initToolbar(); - - String hints[] = - getContext().getResources().getStringArray(R.array.hints); - HintList hintList = new HintList(hints); - hintView.setHints(hintList); - } private void updateEmptyView() { - if (listAdapter == null) return; - llEmpty.setVisibility( listAdapter.getCount() > 0 ? View.GONE : View.VISIBLE); } diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsScreen.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsScreen.java index 794574b1a..8d22d5eb0 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsScreen.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsScreen.java @@ -32,7 +32,6 @@ import org.isoron.uhabits.models.*; import org.isoron.uhabits.ui.*; import org.isoron.uhabits.ui.about.*; import org.isoron.uhabits.ui.habits.edit.*; -import org.isoron.uhabits.ui.habits.list.model.*; import org.isoron.uhabits.ui.habits.show.*; import org.isoron.uhabits.ui.intro.*; import org.isoron.uhabits.ui.settings.*; @@ -42,37 +41,14 @@ import java.io.*; public class ListHabitsScreen extends BaseScreen { - @Nullable ListHabitsController controller; - @NonNull - private final ListHabitsRootView rootView; - - @NonNull - private final ListHabitsSelectionMenu selectionMenu; - - public ListHabitsScreen(@NonNull BaseActivity activity) + public ListHabitsScreen(@NonNull BaseActivity activity, + ListHabitsRootView rootView) { super(activity); - rootView = new ListHabitsRootView(activity); setRootView(rootView); - - ListHabitsMenu menu = new ListHabitsMenu(activity, this); - selectionMenu = new ListHabitsSelectionMenu(this); - setMenu(menu); - setSelectionMenu(selectionMenu); - - HabitCardListAdapter adapter = - new HabitCardListAdapter(ListHabitsRootView.MAX_CHECKMARK_COUNT); - rootView.setListAdapter(adapter); - selectionMenu.setListAdapter(adapter); - } - - @NonNull - public ListHabitsRootView getRootView() - { - return rootView; } @Override @@ -100,12 +76,6 @@ public class ListHabitsScreen extends BaseScreen } } - public void setController(@Nullable ListHabitsController controller) - { - this.controller = controller; - rootView.setController(controller, selectionMenu); - } - public void showAboutScreen() { Intent intent = new Intent(activity, AboutActivity.class); diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsSelectionMenu.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsSelectionMenu.java index 540f71e90..07980edca 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsSelectionMenu.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/ListHabitsSelectionMenu.java @@ -42,16 +42,18 @@ public class ListHabitsSelectionMenu extends BaseSelectionMenu @Inject CommandRunner commandRunner; - @Nullable - private HabitCardListAdapter listAdapter; + @NonNull + private final HabitCardListAdapter listAdapter; @Nullable private HabitCardListController listController; - public ListHabitsSelectionMenu(@NonNull ListHabitsScreen screen) + public ListHabitsSelectionMenu(@NonNull ListHabitsScreen screen, + HabitCardListAdapter listAdapter) { this.screen = screen; HabitsApplication.getComponent().inject(this); + this.listAdapter = listAdapter; } @Override @@ -64,8 +66,6 @@ public class ListHabitsSelectionMenu extends BaseSelectionMenu @Override public boolean onItemClicked(@NonNull MenuItem item) { - if (listAdapter == null) return false; - List selected = listAdapter.getSelected(); if (selected.isEmpty()) return false; @@ -104,7 +104,6 @@ public class ListHabitsSelectionMenu extends BaseSelectionMenu @Override public boolean onPrepare(@NonNull Menu menu) { - if (listAdapter == null) return false; List selected = listAdapter.getSelected(); boolean showEdit = (selected.size() == 1); @@ -149,12 +148,6 @@ public class ListHabitsSelectionMenu extends BaseSelectionMenu screen.startSelection(); } - public void setListAdapter(@Nullable HabitCardListAdapter listAdapter) - { - if (listAdapter == null) return; - this.listAdapter = listAdapter; - } - public void setListController(HabitCardListController listController) { this.listController = listController; diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/CheckmarkButtonController.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/CheckmarkButtonController.java index 234d87f94..d5c5dc078 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/CheckmarkButtonController.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/CheckmarkButtonController.java @@ -93,6 +93,6 @@ public class CheckmarkButtonController void onInvalidToggle(); - void onToggle(Habit habit, long timestamp); + void onToggle(@NonNull Habit habit, long timestamp); } } diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/HabitCardController.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/HabitCardController.java index f2f2b662b..f62fca8a2 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/HabitCardController.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/HabitCardController.java @@ -19,7 +19,7 @@ package org.isoron.uhabits.ui.habits.list.controllers; -import android.support.annotation.Nullable; +import android.support.annotation.*; import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.ui.habits.list.views.HabitCardView; @@ -39,7 +39,7 @@ public class HabitCardController implements HabitCardView.Controller } @Override - public void onToggle(Habit habit, long timestamp) + public void onToggle(@NonNull Habit habit, long timestamp) { if (view != null) view.triggerRipple(timestamp); if (listener != null) listener.onToggle(habit, timestamp); diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/HabitCardListController.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/HabitCardListController.java index 192d92f21..b1137b6ad 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/HabitCardListController.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/controllers/HabitCardListController.java @@ -147,7 +147,7 @@ public class HabitCardListController implements DragSortListView.DropListener, * @param timestamp the timestamps of the checkmark */ @Override - public void onToggle(Habit habit, long timestamp) + public void onToggle(@NonNull Habit habit, long timestamp) { if (habitListener != null) habitListener.onToggle(habit, timestamp); } @@ -203,7 +203,7 @@ public class HabitCardListController implements DragSortListView.DropListener, * * @param habit the habit clicked */ - void onHabitClick(Habit habit); + void onHabitClick(@NonNull Habit habit); /** * Called when the user wants to change the position of a habit on the @@ -212,7 +212,7 @@ public class HabitCardListController implements DragSortListView.DropListener, * @param from habit to be moved * @param to habit that currently occupies the desired position */ - void onHabitReorder(Habit from, Habit to); + void onHabitReorder(@NonNull Habit from, @NonNull Habit to); } /** diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListAdapter.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListAdapter.java index 1d2750316..0f9dab743 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListAdapter.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListAdapter.java @@ -62,6 +62,11 @@ public class HabitCardListAdapter extends BaseAdapter cache.setCheckmarkCount(checkmarkCount); } + public void cancelRefresh() + { + cache.cancelTasks(); + } + /** * Sets all items as not selected. */ @@ -77,11 +82,6 @@ public class HabitCardListAdapter extends BaseAdapter return cache.getHabitCount(); } - public boolean getIncludeArchived() - { - return cache.getIncludeArchived(); - } - /** * Returns the item that occupies a certain position on the list * @@ -163,6 +163,11 @@ public class HabitCardListAdapter extends BaseAdapter cache.onDetached(); } + public void refresh() + { + cache.refreshAllHabits(true); + } + /** * Changes the order of habits on the adapter. *

diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListCache.java b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListCache.java index 2cc1a108b..65a3fb9ed 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListCache.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/list/model/HabitCardListCache.java @@ -69,6 +69,12 @@ public class HabitCardListCache implements CommandRunner.Listener HabitsApplication.getComponent().inject(this); } + public void cancelTasks() + { + if(currentFetchTask != null) + currentFetchTask.cancel(true); + } + public int[] getCheckmarks(long habitId) { return data.checkmarks.get(habitId); @@ -256,6 +262,7 @@ public class HabitCardListCache implements CommandRunner.Listener newData.copyScoresFrom(data); newData.copyCheckmarksFrom(data); +// sleep(1000); commit(); if (!refreshScoresAndCheckmarks) return; @@ -274,10 +281,23 @@ public class HabitCardListCache implements CommandRunner.Listener newData.checkmarks.put(id, h.getCheckmarks().getValues(dateFrom, dateTo)); +// sleep(1000); publishProgress(current++, newData.habits.size()); } } + private void sleep(int time) + { + try + { + Thread.sleep(time); + } + catch (InterruptedException e) + { + e.printStackTrace(); + } + } + @Override protected void onPostExecute(Void aVoid) { diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitActivity.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitActivity.java index ebff34915..55e95d4e8 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitActivity.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitActivity.java @@ -40,6 +40,14 @@ public class ShowHabitActivity extends BaseActivity @Inject HabitList habitList; + private ShowHabitController controller; + + private ShowHabitRootView rootView; + + private ShowHabitScreen screen; + + private ShowHabitsMenu menu; + @Override protected void onCreate(Bundle savedInstanceState) { @@ -47,15 +55,15 @@ public class ShowHabitActivity extends BaseActivity HabitsApplication.getComponent().inject(this); Habit habit = getHabitFromIntent(); - ShowHabitScreen screen = new ShowHabitScreen(this, habit); - ShowHabitRootView view = new ShowHabitRootView(this, habit); - screen.setRootView(view); - this.setScreen(screen); + rootView = new ShowHabitRootView(this, habit); + screen = new ShowHabitScreen(this, habit, rootView); + setScreen(screen); - ShowHabitsMenu menu = new ShowHabitsMenu(this, screen); - ShowHabitController controller = new ShowHabitController(screen, habit); + menu = new ShowHabitsMenu(this, screen); screen.setMenu(menu); - view.setController(controller); + + controller = new ShowHabitController(screen, habit); + rootView.setController(controller); } @NonNull diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitScreen.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitScreen.java index 947a95a35..d5f7cf21b 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitScreen.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/ShowHabitScreen.java @@ -31,10 +31,13 @@ public class ShowHabitScreen extends BaseScreen @NonNull private final Habit habit; - public ShowHabitScreen(@NonNull BaseActivity activity, @NonNull Habit habit) + public ShowHabitScreen(@NonNull BaseActivity activity, + @NonNull Habit habit, + ShowHabitRootView view) { super(activity); this.habit = habit; + setRootView(view); } public void showEditHabitDialog() diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HistoryCard.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HistoryCard.java index 03b663715..c901595ed 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HistoryCard.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/HistoryCard.java @@ -58,7 +58,6 @@ public class HistoryCard extends HabitCard @OnClick(R.id.edit) public void onClickEditButton() { - Log.d("HistoryCard", "onClickEditButton"); controller.onEditHistoryButtonClick(); } diff --git a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/ScoreCard.java b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/ScoreCard.java index 95ff4b351..ea82cdaff 100644 --- a/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/ScoreCard.java +++ b/app/src/main/java/org/isoron/uhabits/ui/habits/show/views/ScoreCard.java @@ -107,7 +107,7 @@ public class ScoreCard extends HabitCard { spinner.setVisibility(GONE); title.setTextColor(ColorUtils.getAndroidTestColor(1)); - chart.setPrimaryColor(ColorUtils.getAndroidTestColor(1)); + chart.setColor(ColorUtils.getAndroidTestColor(1)); chart.populateWithRandomData(); } } @@ -141,7 +141,7 @@ public class ScoreCard extends HabitCard int color = ColorUtils.getColor(getContext(), getHabit().getColor()); title.setTextColor(color); - chart.setPrimaryColor(color); + chart.setColor(color); } } } diff --git a/app/src/main/java/org/isoron/uhabits/ui/widgets/BaseWidget.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/BaseWidget.java new file mode 100644 index 000000000..830922833 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/BaseWidget.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.ui.widgets; + +import android.app.*; +import android.content.*; +import android.graphics.*; +import android.support.annotation.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.utils.*; + +import javax.inject.*; + +import static android.os.Build.VERSION.*; +import static android.os.Build.VERSION_CODES.*; +import static android.view.View.MeasureSpec.*; + +public abstract class BaseWidget +{ + @Inject + WidgetPreferences preferences; + + private final int id; + + @NonNull + private WidgetDimensions dimensions; + + @NonNull + private final Context context; + + public BaseWidget(@NonNull Context context, int id) + { + this.id = id; + this.context = context; + HabitsApplication.getComponent().inject(this); + dimensions = new WidgetDimensions(0, 0, 0, 0); + } + + public void delete() + { + preferences.removeWidget(id); + } + + @NonNull + public Context getContext() + { + return context; + } + + public int getId() + { + return id; + } + + @NonNull + public RemoteViews getLandscapeRemoteViews() + { + return getRemoteViews(dimensions.getLandscapeWidth(), + dimensions.getLandscapeHeight()); + } + + public abstract int getLayoutId(); + + public abstract PendingIntent getOnClickPendingIntent(Context context); + + @NonNull + public RemoteViews getPortraitRemoteViews() + { + return getRemoteViews(dimensions.getPortraitWidth(), + dimensions.getPortraitHeight()); + } + + public abstract void refreshData(View widgetView); + + public void setDimensions(@NonNull WidgetDimensions dimensions) + { + this.dimensions = dimensions; + } + + protected abstract View buildView(); + + protected abstract int getDefaultHeight(); + + protected abstract int getDefaultWidth(); + + protected abstract String getTitle(); + + private void adjustRemoteViewsPadding(RemoteViews remoteViews, + View view, + int width, + int height) + { + int imageWidth = view.getMeasuredWidth(); + int imageHeight = view.getMeasuredHeight(); + int p[] = calculatePadding(width, height, imageWidth, imageHeight); + remoteViews.setViewPadding(R.id.buttonOverlay, p[0], p[1], p[2], p[3]); + } + + private void buildRemoteViews(View view, + RemoteViews remoteViews, + int width, + int height) + { + Bitmap bitmap = getBitmapFromView(view); + remoteViews.setTextViewText(R.id.label, getTitle()); + remoteViews.setImageViewBitmap(R.id.imageView, bitmap); + + if (SDK_INT >= JELLY_BEAN) + adjustRemoteViewsPadding(remoteViews, view, width, height); + + PendingIntent onClickIntent = getOnClickPendingIntent(context); + if (onClickIntent != null) + remoteViews.setOnClickPendingIntent(R.id.button, onClickIntent); + } + + private int[] calculatePadding(int entireWidth, + int entireHeight, + int imageWidth, + int imageHeight) + { + int w = (int) (((float) entireWidth - imageWidth) / 2); + int h = (int) (((float) entireHeight - imageHeight) / 2); + + return new int[]{ w, h, w, h }; + } + + private Bitmap getBitmapFromView(View view) + { + view.invalidate(); + view.setDrawingCacheEnabled(true); + view.buildDrawingCache(true); + return view.getDrawingCache(); + } + + @NonNull + private RemoteViews getRemoteViews(int width, int height) + { + View view = buildView(); + measureView(view, width, height); + + refreshData(view); + + if(view.isLayoutRequested()) + measureView(view, width, height); + + RemoteViews remoteViews = + new RemoteViews(context.getPackageName(), getLayoutId()); + + buildRemoteViews(view, remoteViews, width, height); + + return remoteViews; + } + + private void measureView(View view, int width, int height) + { + LayoutInflater inflater = LayoutInflater.from(context); + View entireView = inflater.inflate(getLayoutId(), null); + + int specWidth = makeMeasureSpec(width, View.MeasureSpec.EXACTLY); + int specHeight = makeMeasureSpec(height, View.MeasureSpec.EXACTLY); + + entireView.measure(specWidth, specHeight); + entireView.layout(0, 0, entireView.getMeasuredWidth(), + entireView.getMeasuredHeight()); + + View imageView = entireView.findViewById(R.id.imageView); + width = imageView.getMeasuredWidth(); + height = imageView.getMeasuredHeight(); + + specWidth = makeMeasureSpec(width, View.MeasureSpec.EXACTLY); + specHeight = makeMeasureSpec(height, View.MeasureSpec.EXACTLY); + + view.measure(specWidth, specHeight); + view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/ui/widgets/CheckmarkWidget.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/CheckmarkWidget.java new file mode 100644 index 000000000..fa7013b1f --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/CheckmarkWidget.java @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.ui.widgets; + +import android.app.*; +import android.content.*; +import android.support.annotation.*; +import android.view.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.widgets.views.*; +import org.isoron.uhabits.utils.*; + +public class CheckmarkWidget extends BaseWidget +{ + @NonNull + private final Habit habit; + + public CheckmarkWidget(@NonNull Context context, + int widgetId, + @NonNull Habit habit) + { + super(context, widgetId); + this.habit = habit; + } + + @Override + public int getLayoutId() + { + return R.layout.widget_wrapper; + } + + @Override + public PendingIntent getOnClickPendingIntent(Context context) + { + return HabitBroadcastReceiver.buildCheckIntent(context, habit, null); + } + + @Override + public void refreshData(View v) + { + CheckmarkWidgetView view = (CheckmarkWidgetView) v; + int color = ColorUtils.getColor(getContext(), habit.getColor()); + int score = habit.getScores().getTodayValue(); + float percentage = (float) score / Score.MAX_VALUE; + int checkmark = habit.getCheckmarks().getTodayValue(); + + view.setPercentage(percentage); + view.setActiveColor(color); + view.setName(habit.getName()); + view.setCheckmarkValue(checkmark); + view.refresh(); + } + + @Override + protected View buildView() + { + return new CheckmarkWidgetView(getContext()); + } + + @Override + protected int getDefaultHeight() + { + return 125; + } + + @Override + protected int getDefaultWidth() + { + return 125; + } + + @Override + protected String getTitle() + { + return habit.getName(); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/widgets/HabitPickerDialog.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/HabitPickerDialog.java similarity index 56% rename from app/src/main/java/org/isoron/uhabits/widgets/HabitPickerDialog.java rename to app/src/main/java/org/isoron/uhabits/ui/widgets/HabitPickerDialog.java index ffe682065..0126bc2f8 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/HabitPickerDialog.java +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/HabitPickerDialog.java @@ -17,38 +17,53 @@ * with this program. If not, see . */ -package org.isoron.uhabits.widgets; - -import android.app.Activity; -import android.appwidget.AppWidgetManager; -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.view.View; -import android.widget.AdapterView; -import android.widget.ArrayAdapter; -import android.widget.ListView; - -import org.isoron.uhabits.HabitsApplication; -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.HabitList; - -import java.util.ArrayList; -import java.util.List; - -import javax.inject.Inject; - -public class HabitPickerDialog extends Activity implements AdapterView.OnItemClickListener +package org.isoron.uhabits.ui.widgets; + +import android.app.*; +import android.content.*; +import android.os.*; +import android.view.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import javax.inject.*; + +import static android.appwidget.AppWidgetManager.*; + +public class HabitPickerDialog extends Activity + implements AdapterView.OnItemClickListener { @Inject HabitList habitList; + @Inject + WidgetPreferences preferences; + private Integer widgetId; private ArrayList habitIds; + @Override + public void onItemClick(AdapterView parent, + View view, + int position, + long id) + { + Long habitId = habitIds.get(position); + preferences.addWidget(widgetId, habitId); + HabitsApplication.getWidgetManager().updateWidgets(this); + + Intent resultValue = new Intent(); + resultValue.putExtra(EXTRA_APPWIDGET_ID, widgetId); + setResult(RESULT_OK, resultValue); + finish(); + } + @Override protected void onCreate(Bundle savedInstanceState) { @@ -59,8 +74,8 @@ public class HabitPickerDialog extends Activity implements AdapterView.OnItemCli Intent intent = getIntent(); Bundle extras = intent.getExtras(); - if (extras != null) widgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, - AppWidgetManager.INVALID_APPWIDGET_ID); + if (extras != null) + widgetId = extras.getInt(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID); ListView listView = (ListView) findViewById(R.id.listView); @@ -74,29 +89,11 @@ public class HabitPickerDialog extends Activity implements AdapterView.OnItemCli habitNames.add(h.getName()); } - ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, - habitNames); + ArrayAdapter adapter = + new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, + habitNames); listView.setAdapter(adapter); listView.setOnItemClickListener(this); } - @Override - public void onItemClick(AdapterView parent, View view, int position, long id) - { - Long habitId = habitIds.get(position); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences( - getApplicationContext()); - prefs - .edit() - .putLong(BaseWidgetProvider.getHabitIdKey(widgetId), habitId) - .commit(); - - WidgetManager.updateWidgets(this); - - Intent resultValue = new Intent(); - resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId); - setResult(RESULT_OK, resultValue); - finish(); - } - } diff --git a/app/src/main/java/org/isoron/uhabits/ui/widgets/WidgetDimensions.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/WidgetDimensions.java new file mode 100644 index 000000000..28285e34b --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/WidgetDimensions.java @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.ui.widgets; + +public class WidgetDimensions +{ + private final int portraitWidth; + + private final int portraitHeight; + + private final int landscapeWidth; + + private final int landscapeHeight; + + public WidgetDimensions(int portraitWidth, + int portraitHeight, + int landscapeWidth, + int landscapeHeight) + { + this.portraitWidth = portraitWidth; + this.portraitHeight = portraitHeight; + this.landscapeWidth = landscapeWidth; + this.landscapeHeight = landscapeHeight; + } + + public int getLandscapeHeight() + { + return landscapeHeight; + } + + public int getLandscapeWidth() + { + return landscapeWidth; + } + + public int getPortraitHeight() + { + return portraitHeight; + } + + public int getPortraitWidth() + { + return portraitWidth; + } +} \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/ui/widgets/WidgetUpdater.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/WidgetUpdater.java new file mode 100644 index 000000000..f2a3d3cac --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/WidgetUpdater.java @@ -0,0 +1,98 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.ui.widgets; + +import android.appwidget.*; +import android.content.*; +import android.support.annotation.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.widgets.*; + +import javax.inject.*; + +/** + * A WidgetUpdater listens to the commands being executed by the application and + * updates the home-screen widgets accordingly. + *

+ * There should be only one instance of this class, created when the application + * starts. To access it, call HabitApplication.getWidgetUpdater(). + */ +public class WidgetUpdater implements CommandRunner.Listener +{ + @Inject + CommandRunner commandRunner; + + @NonNull + private final Context context; + + public WidgetUpdater(@NonNull Context context) + { + this.context = context; + HabitsApplication.getComponent().inject(this); + } + + @Override + public void onCommandExecuted(@NonNull Command command, + @Nullable Long refreshKey) + { + updateWidgets(context); + } + + /** + * Instructs the updater to start listening to commands. If any relevant + * commands are executed after this method is called, the corresponding + * widgets will get updated. + */ + public void startListening() + { + commandRunner.addListener(this); + } + + /** + * Instructs the updater to stop listening to commands. Every command + * executed after this method is called will be ignored by the updater. + */ + public void stopListening() + { + commandRunner.removeListener(this); + } + + void updateWidgets(Context context) + { + updateWidgets(context, CheckmarkWidgetProvider.class); + updateWidgets(context, HistoryWidgetProvider.class); + updateWidgets(context, ScoreWidgetProvider.class); + updateWidgets(context, StreakWidgetProvider.class); + updateWidgets(context, FrequencyWidgetProvider.class); + } + + private void updateWidgets(Context context, Class providerClass) + { + ComponentName provider = new ComponentName(context, providerClass); + Intent intent = new Intent(context, providerClass); + intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); + int ids[] = + AppWidgetManager.getInstance(context).getAppWidgetIds(provider); + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); + context.sendBroadcast(intent); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/CheckmarkWidgetView.java similarity index 81% rename from app/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.java rename to app/src/main/java/org/isoron/uhabits/ui/widgets/views/CheckmarkWidgetView.java index b4d8fc6ba..3d3bc3a8c 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/views/CheckmarkWidgetView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/CheckmarkWidgetView.java @@ -17,26 +17,19 @@ * with this program. If not, see . */ -package org.isoron.uhabits.widgets.views; - -import android.content.Context; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.util.AttributeSet; -import android.util.TypedValue; -import android.widget.TextView; - -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Checkmark; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.Score; -import org.isoron.uhabits.ui.common.views.HabitChart; -import org.isoron.uhabits.ui.common.views.RingView; -import org.isoron.uhabits.utils.ColorUtils; -import org.isoron.uhabits.utils.InterfaceUtils; +package org.isoron.uhabits.ui.widgets.views; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; +import android.widget.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.utils.*; public class CheckmarkWidgetView extends HabitWidgetView - implements HabitChart { private int activeColor; @@ -94,6 +87,10 @@ public class CheckmarkWidgetView extends HabitWidgetView R.attr.cardBackgroundColor); foregroundColor = InterfaceUtils.getStyledColor(context, R.attr.mediumContrastTextColor); + + setShadowAlpha(0x00); + rebuildBackground(); + break; case Checkmark.UNCHECKED: @@ -103,6 +100,10 @@ public class CheckmarkWidgetView extends HabitWidgetView R.attr.cardBackgroundColor); foregroundColor = InterfaceUtils.getStyledColor(context, R.attr.mediumContrastTextColor); + + setShadowAlpha(0x00); + rebuildBackground(); + break; } @@ -118,47 +119,31 @@ public class CheckmarkWidgetView extends HabitWidgetView postInvalidate(); } - @Override - public void refreshData() + public void setCheckmarkValue(int checkmarkValue) { - if (habit == null) return; - this.percentage = - (float) habit.getScores().getTodayValue() / Score.MAX_VALUE; - this.checkmarkValue = habit.getCheckmarks().getTodayValue(); - refresh(); + this.checkmarkValue = checkmarkValue; } - @Override - public void setHabit(@NonNull Habit habit) + public void setName(@NonNull String name) { - super.setHabit(habit); - this.name = habit.getName(); - this.activeColor = ColorUtils.getColor(getContext(), habit.getColor()); - refresh(); + this.name = name; } - @Override - @NonNull - protected Integer getInnerLayoutId() + public void setPercentage(float percentage) { - return R.layout.widget_checkmark; + this.percentage = percentage; } - private void init() + public void setActiveColor(int activeColor) { - ring = (RingView) findViewById(R.id.scoreRing); - label = (TextView) findViewById(R.id.label); - - if (ring != null) ring.setIsTransparencyEnabled(true); + this.activeColor = activeColor; + } - if (isInEditMode()) - { - percentage = 0.75f; - name = "Wake up early"; - activeColor = ColorUtils.getAndroidTestColor(6); - checkmarkValue = Checkmark.CHECKED_EXPLICITLY; - refresh(); - } + @Override + @NonNull + protected Integer getInnerLayoutId() + { + return R.layout.widget_checkmark; } @Override @@ -194,4 +179,21 @@ public class CheckmarkWidgetView extends HabitWidgetView super.onMeasure(widthMeasureSpec, heightMeasureSpec); } + + private void init() + { + ring = (RingView) findViewById(R.id.scoreRing); + label = (TextView) findViewById(R.id.label); + + if (ring != null) ring.setIsTransparencyEnabled(true); + + if (isInEditMode()) + { + percentage = 0.75f; + name = "Wake up early"; + activeColor = ColorUtils.getAndroidTestColor(6); + checkmarkValue = Checkmark.CHECKED_EXPLICITLY; + refresh(); + } + } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/views/GraphWidgetView.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/GraphWidgetView.java similarity index 56% rename from app/src/main/java/org/isoron/uhabits/widgets/views/GraphWidgetView.java rename to app/src/main/java/org/isoron/uhabits/ui/widgets/views/GraphWidgetView.java index 96b25dcc8..56ab7b05b 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/views/GraphWidgetView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/GraphWidgetView.java @@ -17,63 +17,57 @@ * with this program. If not, see . */ -package org.isoron.uhabits.widgets.views; +package org.isoron.uhabits.ui.widgets.views; -import android.content.Context; -import android.support.annotation.NonNull; -import android.view.View; -import android.view.ViewGroup; -import android.widget.TextView; +import android.content.*; +import android.support.annotation.*; +import android.view.*; +import android.widget.*; -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.ui.common.views.HabitChart; +import org.isoron.uhabits.*; -public class GraphWidgetView extends HabitWidgetView implements HabitChart +public class GraphWidgetView extends HabitWidgetView { - private final HabitChart dataView; + private final View dataView; + private TextView title; - public GraphWidgetView(Context context, HabitChart dataView) + public GraphWidgetView(Context context, View dataView) { super(context); this.dataView = dataView; init(); } - private void init() + public View getDataView() { - ViewGroup.LayoutParams params = - new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT); - ((View) dataView).setLayoutParams(params); - - ViewGroup innerFrame = (ViewGroup) findViewById(R.id.innerFrame); - innerFrame.addView(((View) dataView)); - - title = (TextView) findViewById(R.id.title); - title.setVisibility(VISIBLE); + return dataView; } - @Override - public void setHabit(@NonNull Habit habit) + public void setTitle(String text) { - super.setHabit(habit); - dataView.setHabit(habit); - title.setText(habit.getName()); + title.setText(text); } @Override - public void refreshData() - { - if(habit == null) return; - dataView.refreshData(); - } - @NonNull protected Integer getInnerLayoutId() { return R.layout.widget_graph; } + + private void init() + { + ViewGroup.LayoutParams params = + new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + dataView.setLayoutParams(params); + + ViewGroup innerFrame = (ViewGroup) findViewById(R.id.innerFrame); + innerFrame.addView(dataView); + + title = (TextView) findViewById(R.id.title); + title.setVisibility(VISIBLE); + } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/views/HabitWidgetView.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/HabitWidgetView.java similarity index 92% rename from app/src/main/java/org/isoron/uhabits/widgets/views/HabitWidgetView.java rename to app/src/main/java/org/isoron/uhabits/ui/widgets/views/HabitWidgetView.java index 0c4266ec1..b4e6dafea 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/views/HabitWidgetView.java +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/HabitWidgetView.java @@ -17,7 +17,7 @@ * with this program. If not, see . */ -package org.isoron.uhabits.widgets.views; +package org.isoron.uhabits.ui.widgets.views; import android.content.*; import android.graphics.*; @@ -29,14 +29,11 @@ import android.view.*; import android.widget.*; import org.isoron.uhabits.*; -import org.isoron.uhabits.models.*; -import org.isoron.uhabits.ui.common.views.*; import org.isoron.uhabits.utils.*; import java.util.*; public abstract class HabitWidgetView extends FrameLayout - implements HabitChart { @Nullable protected InsetDrawable background; @@ -44,9 +41,6 @@ public abstract class HabitWidgetView extends FrameLayout @Nullable protected Paint backgroundPaint; - @Nullable - protected Habit habit; - protected ViewGroup frame; private int shadowAlpha; @@ -63,12 +57,6 @@ public abstract class HabitWidgetView extends FrameLayout init(); } - @Override - public void setHabit(@NonNull Habit habit) - { - this.habit = habit; - } - public void setShadowAlpha(int shadowAlpha) { this.shadowAlpha = shadowAlpha; diff --git a/app/src/main/java/org/isoron/uhabits/widgets/views/package-info.java b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/package-info.java similarity index 95% rename from app/src/main/java/org/isoron/uhabits/widgets/views/package-info.java rename to app/src/main/java/org/isoron/uhabits/ui/widgets/views/package-info.java index 2fbf2799c..7e26dce73 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/views/package-info.java +++ b/app/src/main/java/org/isoron/uhabits/ui/widgets/views/package-info.java @@ -20,4 +20,4 @@ /** * Provides views that are specific for the home-screen widgets. */ -package org.isoron.uhabits.widgets.views; \ No newline at end of file +package org.isoron.uhabits.ui.widgets.views; \ No newline at end of file diff --git a/app/src/main/java/org/isoron/uhabits/utils/Preferences.java b/app/src/main/java/org/isoron/uhabits/utils/Preferences.java index adb0c6086..73a9e729b 100644 --- a/app/src/main/java/org/isoron/uhabits/utils/Preferences.java +++ b/app/src/main/java/org/isoron/uhabits/utils/Preferences.java @@ -19,13 +19,10 @@ package org.isoron.uhabits.utils; -import android.content.Context; -import android.content.SharedPreferences; -import android.preference.PreferenceManager; +import android.content.*; +import android.preference.*; -import org.isoron.uhabits.BuildConfig; -import org.isoron.uhabits.HabitsApplication; -import org.isoron.uhabits.R; +import org.isoron.uhabits.*; public class Preferences { @@ -103,9 +100,9 @@ public class Preferences public void setShouldReverseCheckmarks(boolean shouldReverse) { prefs - .edit() - .putBoolean("pref_checkmark_reverse_order", shouldReverse) - .apply(); + .edit() + .putBoolean("pref_checkmark_reverse_order", shouldReverse) + .apply(); } public boolean shouldReverseCheckmarks() @@ -127,11 +124,9 @@ public class Preferences public void updateLastHint(int number, long timestamp) { prefs - .edit() - .putInt("last_hint_number", number) - .putLong("last_hint_timestamp", timestamp) - .apply(); + .edit() + .putInt("last_hint_number", number) + .putLong("last_hint_timestamp", timestamp) + .apply(); } - - } diff --git a/app/src/main/java/org/isoron/uhabits/utils/WidgetPreferences.java b/app/src/main/java/org/isoron/uhabits/utils/WidgetPreferences.java new file mode 100644 index 000000000..6fac955b5 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/WidgetPreferences.java @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.utils; + +import android.content.*; +import android.preference.*; + +import org.isoron.uhabits.*; + +public class WidgetPreferences +{ + private Context context; + + private SharedPreferences prefs; + + public WidgetPreferences() + { + this.context = HabitsApplication.getContext(); + prefs = PreferenceManager.getDefaultSharedPreferences(context); + } + + public void addWidget(int widgetId, long habitId) + { + prefs + .edit() + .putLong(getHabitIdKey(widgetId), habitId) + .commit(); + } + + public long getHabitIdFromWidgetId(int widgetId) + { + Long habitId = prefs.getLong(getHabitIdKey(widgetId), -1); + if (habitId < 0) throw new RuntimeException("widget not found"); + + return habitId; + } + + public void removeWidget(int id) + { + String habitIdKey = getHabitIdKey(id); + prefs.edit().remove(habitIdKey).apply(); + } + + private String getHabitIdKey(int id) + { + return String.format("widget-%06d-habit", id); + } +} diff --git a/app/src/main/java/org/isoron/uhabits/utils/WidgetUtils.java b/app/src/main/java/org/isoron/uhabits/utils/WidgetUtils.java new file mode 100644 index 000000000..44e8238e7 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/utils/WidgetUtils.java @@ -0,0 +1,70 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.utils; + +import android.appwidget.*; +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.widget.*; + +import org.isoron.uhabits.ui.widgets.*; + +import static android.os.Build.VERSION.*; +import static android.os.Build.VERSION_CODES.*; + +public abstract class WidgetUtils +{ + @NonNull + public static WidgetDimensions getDimensionsFromOptions( + @NonNull Context context, @NonNull Bundle options) + { + if (SDK_INT < JELLY_BEAN) + throw new AssertionError("method requires jelly-bean"); + + int maxWidth = (int) InterfaceUtils.dpToPixels(context, + options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH)); + int maxHeight = (int) InterfaceUtils.dpToPixels(context, + options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)); + int minWidth = (int) InterfaceUtils.dpToPixels(context, + options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)); + int minHeight = (int) InterfaceUtils.dpToPixels(context, + options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)); + + return new WidgetDimensions(minWidth, maxHeight, maxWidth, minHeight); + } + + public static void updateAppWidget(@NonNull AppWidgetManager manager, + @NonNull BaseWidget widget) + { + if (SDK_INT < JELLY_BEAN) + { + RemoteViews portrait = widget.getPortraitRemoteViews(); + manager.updateAppWidget(widget.getId(), portrait); + } + else + { + RemoteViews landscape = widget.getLandscapeRemoteViews(); + RemoteViews portrait = widget.getPortraitRemoteViews(); + RemoteViews views = new RemoteViews(landscape, portrait); + manager.updateAppWidget(widget.getId(), views); + } + } +} diff --git a/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java index a94809ff4..89d91e2f4 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/BaseWidgetProvider.java @@ -19,308 +19,131 @@ package org.isoron.uhabits.widgets; -import android.app.PendingIntent; -import android.appwidget.AppWidgetManager; -import android.appwidget.AppWidgetProvider; -import android.content.Context; -import android.content.SharedPreferences; -import android.graphics.Bitmap; -import android.os.Build; -import android.os.Bundle; -import android.preference.PreferenceManager; -import android.util.Log; -import android.view.LayoutInflater; -import android.view.View; -import android.widget.ImageView; -import android.widget.RemoteViews; -import android.widget.TextView; +import android.appwidget.*; +import android.content.*; +import android.os.*; +import android.support.annotation.*; +import android.widget.*; -import org.isoron.uhabits.HabitsApplication; -import org.isoron.uhabits.R; -import org.isoron.uhabits.models.Habit; -import org.isoron.uhabits.models.HabitList; -import org.isoron.uhabits.tasks.BaseTask; -import org.isoron.uhabits.utils.InterfaceUtils; +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.ui.widgets.*; +import org.isoron.uhabits.utils.*; -import java.io.FileOutputStream; -import java.io.IOException; +import javax.inject.*; -import javax.inject.Inject; +import static android.os.Build.VERSION.*; +import static android.os.Build.VERSION_CODES.*; +import static org.isoron.uhabits.utils.WidgetUtils.*; public abstract class BaseWidgetProvider extends AppWidgetProvider { @Inject HabitList habitList; - private class WidgetDimensions - { - public int portraitWidth, portraitHeight; - public int landscapeWidth, landscapeHeight; - } - - protected abstract int getDefaultHeight(); - - protected abstract int getDefaultWidth(); - - protected abstract PendingIntent getOnClickPendingIntent(Context context, Habit habit); - - protected abstract int getLayoutId(); - - protected abstract View buildCustomView(Context context, Habit habit); - - public static String getHabitIdKey(long widgetId) - { - return String.format("widget-%06d-habit", widgetId); - } - - @Override - public void onDeleted(Context context, int[] appWidgetIds) - { - Context appContext = context.getApplicationContext(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext); - - for(Integer id : appWidgetIds) - prefs.edit().remove(getHabitIdKey(id)).apply(); - } + @Inject + WidgetPreferences widgetPrefs; - @Override - public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, - int appWidgetId, Bundle newOptions) + public BaseWidgetProvider() { - updateWidget(context, appWidgetManager, appWidgetId, newOptions); + HabitsApplication.getComponent().inject(this); } @Override - public void onUpdate(Context context, AppWidgetManager manager, int[] appWidgetIds) + public void onAppWidgetOptionsChanged(@Nullable Context context, + @Nullable AppWidgetManager manager, + int widgetId, + @Nullable Bundle options) { - for(int id : appWidgetIds) + try { - Bundle options = null; - - if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) - options = manager.getAppWidgetOptions(id); + if (context == null) throw new RuntimeException("context is null"); + if (manager == null) throw new RuntimeException("manager is null"); + if (options == null) throw new RuntimeException("options is null"); + context.setTheme(R.style.TransparentWidgetTheme); - updateWidget(context, manager, id, options); + BaseWidget widget = getWidgetFromId(context, widgetId); + WidgetDimensions dims = getDimensionsFromOptions(context, options); + widget.setDimensions(dims); + updateAppWidget(manager, widget); } - } - - private void updateWidget(Context context, AppWidgetManager manager, - int widgetId, Bundle options) - { - HabitsApplication.getComponent().inject(this); - WidgetDimensions dim = getWidgetDimensions(context, options); - - Context appContext = context.getApplicationContext(); - SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(appContext); - - Long habitId = prefs.getLong(getHabitIdKey(widgetId), -1L); - if(habitId < 0) return; - - Habit habit = habitList.getById(habitId); - if(habit == null) + catch (RuntimeException e) { drawErrorWidget(context, manager, widgetId); - return; + e.printStackTrace(); } - - new RenderWidgetTask(widgetId, context, habit, dim, manager).execute(); } - private void drawErrorWidget(Context context, AppWidgetManager manager, int widgetId) - { - RemoteViews errorView = new RemoteViews(context.getPackageName(), R.layout.widget_error); - manager.updateAppWidget(widgetId, errorView); - } - - protected abstract void refreshCustomViewData(View widgetView); - - private void savePreview(Context context, int widgetId, Bitmap widgetCache, int width, - int height, String label) + @Override + public void onDeleted(@Nullable Context context, @Nullable int[] ids) { - try - { - LayoutInflater inflater = LayoutInflater.from(context); - View view = inflater.inflate(getLayoutId(), null); - - TextView tvLabel = (TextView) view.findViewById(R.id.label); - if(tvLabel != null) tvLabel.setText(label); - - ImageView iv = (ImageView) view.findViewById(R.id.imageView); - if(iv != null) iv.setImageBitmap(widgetCache); - - view.measure(width, height); - view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight()); - view.setDrawingCacheEnabled(true); - view.buildDrawingCache(); - Bitmap previewCache = view.getDrawingCache(); - - String filename = String.format("%s/%d_%d.png", context.getExternalCacheDir(), widgetId, width); - Log.d("BaseWidgetProvider", String.format("Writing %s", filename)); - FileOutputStream out = new FileOutputStream(filename); + if (context == null) throw new RuntimeException("context is null"); + if (ids == null) throw new RuntimeException("ids is null"); - if(previewCache != null) - previewCache.compress(Bitmap.CompressFormat.PNG, 100, out); - - out.close(); - } - catch (IOException e) + for (int id : ids) { - e.printStackTrace(); + BaseWidget widget = getWidgetFromId(context, id); + widget.delete(); } } - private WidgetDimensions getWidgetDimensions(Context context, Bundle options) + @Override + public void onUpdate(@Nullable Context context, + @Nullable AppWidgetManager manager, + @Nullable int[] widgetIds) { - int maxWidth = getDefaultWidth(); - int minWidth = getDefaultWidth(); - int maxHeight = getDefaultHeight(); - int minHeight = getDefaultHeight(); + if (context == null) throw new RuntimeException("context is null"); + if (manager == null) throw new RuntimeException("manager is null"); + if (widgetIds == null) throw new RuntimeException("widgetIds is null"); + context.setTheme(R.style.TransparentWidgetTheme); - if (options != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) - { - maxWidth = (int) InterfaceUtils.dpToPixels(context, - options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH)); - maxHeight = (int) InterfaceUtils.dpToPixels(context, - options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_HEIGHT)); - minWidth = (int) InterfaceUtils.dpToPixels(context, - options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)); - minHeight = (int) InterfaceUtils.dpToPixels(context, - options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)); - } - - WidgetDimensions ws = new WidgetDimensions(); - ws.portraitWidth = minWidth; - ws.portraitHeight = maxHeight; - ws.landscapeWidth = maxWidth; - ws.landscapeHeight = minHeight; - return ws; + for (int id : widgetIds) + update(context, manager, id); } - private void measureCustomView(Context context, int w, int h, View customView) + @NonNull + protected Habit getHabitFromWidgetId(int widgetId) { - LayoutInflater inflater = LayoutInflater.from(context); - View entireView = inflater.inflate(getLayoutId(), null); - - int specWidth = View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY); - int specHeight = View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY); - - entireView.measure(specWidth, specHeight); - entireView.layout(0, 0, entireView.getMeasuredWidth(), entireView.getMeasuredHeight()); + long habitId = widgetPrefs.getHabitIdFromWidgetId(widgetId); + Habit habit = habitList.getById(habitId); + if (habit == null) throw new RuntimeException("habit not found"); + return habit; + } - View imageView = entireView.findViewById(R.id.imageView); - w = imageView.getMeasuredWidth(); - h = imageView.getMeasuredHeight(); + @NonNull + protected abstract BaseWidget getWidgetFromId(@NonNull Context context, + int id); - specWidth = View.MeasureSpec.makeMeasureSpec(w, View.MeasureSpec.EXACTLY); - specHeight = View.MeasureSpec.makeMeasureSpec(h, View.MeasureSpec.EXACTLY); - customView.measure(specWidth, specHeight); - customView.layout(0, 0, customView.getMeasuredWidth(), customView.getMeasuredHeight()); + private void drawErrorWidget(Context context, + AppWidgetManager manager, + int widgetId) + { + RemoteViews errorView = + new RemoteViews(context.getPackageName(), R.layout.widget_error); + manager.updateAppWidget(widgetId, errorView); } - private class RenderWidgetTask extends BaseTask + private void update(@NonNull Context context, + @NonNull AppWidgetManager manager, + int widgetId) { - private final int widgetId; - private final Context context; - private final Habit habit; - private final AppWidgetManager manager; - private RemoteViews portraitRemoteViews, landscapeRemoteViews; - private View portraitWidgetView, landscapeWidgetView; - private WidgetDimensions dim; - - public RenderWidgetTask(int widgetId, Context context, Habit habit, WidgetDimensions ws, - AppWidgetManager manager) - { - this.widgetId = widgetId; - this.context = context; - this.habit = habit; - this.manager = manager; - this.dim = ws; - } - - @Override - protected void onPreExecute() - { - super.onPreExecute(); - context.setTheme(R.style.TransparentWidgetTheme); - - portraitRemoteViews = new RemoteViews(context.getPackageName(), getLayoutId()); - portraitWidgetView = buildCustomView(context, habit); - measureCustomView(context, dim.portraitWidth, dim.portraitHeight, portraitWidgetView); - - landscapeRemoteViews = new RemoteViews(context.getPackageName(), getLayoutId()); - landscapeWidgetView = buildCustomView(context, habit); - measureCustomView(context, dim.landscapeWidth, dim.landscapeHeight, - landscapeWidgetView); - } - - private void updateAppWidget() - { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) - manager.updateAppWidget(widgetId, new RemoteViews(landscapeRemoteViews, - portraitRemoteViews)); - else - manager.updateAppWidget(widgetId, portraitRemoteViews); - } - - @Override - protected void doInBackground() + try { - refreshCustomViewData(portraitWidgetView); - refreshCustomViewData(landscapeWidgetView); - } + BaseWidget widget = getWidgetFromId(context, widgetId); - @Override - protected void onPostExecute(Void aVoid) - { - try - { - buildRemoteViews(portraitWidgetView, portraitRemoteViews, - dim.portraitWidth, dim.portraitHeight); - buildRemoteViews(landscapeWidgetView, landscapeRemoteViews, - dim.landscapeWidth, dim.landscapeHeight); - updateAppWidget(); - } - catch (Exception e) + if (SDK_INT > JELLY_BEAN) { - drawErrorWidget(context, manager, widgetId); - e.printStackTrace(); + Bundle options = manager.getAppWidgetOptions(widgetId); + widget.setDimensions( + getDimensionsFromOptions(context, options)); } - super.onPostExecute(aVoid); + updateAppWidget(manager, widget); } - - private void buildRemoteViews(View widgetView, RemoteViews remoteViews, int width, - int height) + catch (RuntimeException e) { - widgetView.invalidate(); - widgetView.setDrawingCacheEnabled(true); - widgetView.buildDrawingCache(true); - Bitmap drawingCache = widgetView.getDrawingCache(); - remoteViews.setTextViewText(R.id.label, habit.getName()); - remoteViews.setImageViewBitmap(R.id.imageView, drawingCache); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) - { - int imageWidth = widgetView.getMeasuredWidth(); - int imageHeight = widgetView.getMeasuredHeight(); - int p[] = getPadding(width, height, imageWidth, imageHeight); - remoteViews.setViewPadding(R.id.buttonOverlay, p[0], p[1], p[2], p[3]); - } - - //savePreview(context, widgetId, drawingCache, width, height, habit.name); - - PendingIntent onClickIntent = getOnClickPendingIntent(context, habit); - if (onClickIntent != null) remoteViews.setOnClickPendingIntent(R.id.button, - onClickIntent); + drawErrorWidget(context, manager, widgetId); + e.printStackTrace(); } } - - private int[] getPadding(int entireWidth, int entireHeight, int imageWidth, - int imageHeight) - { - int w = (int) (((float) entireWidth - imageWidth) / 2); - int h = (int) (((float) entireHeight - imageHeight) / 2); - - return new int[]{ w, h, w, h }; - } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidgetProvider.java index 42ddada52..c574d9c2e 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/CheckmarkWidgetProvider.java @@ -18,55 +18,19 @@ */ package org.isoron.uhabits.widgets; -import android.app.*; import android.content.*; -import android.view.*; +import android.support.annotation.*; -import org.isoron.uhabits.*; import org.isoron.uhabits.models.*; -import org.isoron.uhabits.ui.common.views.*; -import org.isoron.uhabits.widgets.views.*; +import org.isoron.uhabits.ui.widgets.*; public class CheckmarkWidgetProvider extends BaseWidgetProvider { + @NonNull @Override - protected View buildCustomView(Context context, Habit habit) + protected CheckmarkWidget getWidgetFromId(@NonNull Context context, int id) { - CheckmarkWidgetView view = new CheckmarkWidgetView(context); - view.setHabit(habit); - return view; + Habit habit = getHabitFromWidgetId(id); + return new CheckmarkWidget(context, id, habit); } - - @Override - protected int getDefaultHeight() - { - return 125; - } - - @Override - protected int getDefaultWidth() - { - return 125; - } - - @Override - protected int getLayoutId() - { - return R.layout.widget_wrapper; - } - - @Override - protected PendingIntent getOnClickPendingIntent(Context context, - Habit habit) - { - return HabitBroadcastReceiver.buildCheckIntent(context, habit, null); - } - - @Override - protected void refreshCustomViewData(View view) - { - ((HabitChart) view).refreshData(); - } - - } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java index 1eed0d708..3758e1b0f 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/FrequencyWidgetProvider.java @@ -19,54 +19,73 @@ package org.isoron.uhabits.widgets; -import android.app.*; import android.content.*; -import android.view.*; +import android.support.annotation.*; import org.apache.commons.lang3.*; -import org.isoron.uhabits.*; -import org.isoron.uhabits.models.*; -import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.ui.widgets.*; public class FrequencyWidgetProvider extends BaseWidgetProvider { + @NonNull @Override - protected View buildCustomView(Context context, Habit habit) + protected BaseWidget getWidgetFromId(@NonNull Context context, int id) { - FrequencyChart dataView = new FrequencyChart(context); throw new NotImplementedException(""); -// GraphWidgetView view = new GraphWidgetView(context, dataView); -// view.setHabit(habit); -// return view; - } - - @Override - protected void refreshCustomViewData(View view) - { - ((HabitChart) view).refreshData(); } - @Override - protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) - { - return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); - } - - @Override - protected int getDefaultHeight() - { - return 200; - } - - @Override - protected int getDefaultWidth() - { - return 200; - } - - @Override - protected int getLayoutId() - { - return R.layout.widget_wrapper; - } +// @NonNull +// @Override +// protected BaseWidget getWidgetFromId(int id) +// { +// throw new NotImplementedException(""); +// } +// +// @Override +// protected View buildCustomView(Context context, Habit habit) +// { +// FrequencyChart chart = new FrequencyChart(context); +// GraphWidgetView view = new GraphWidgetView(context, chart); +// view.setTitle(habit.getName()); +// return view; +// } +// +// @Override +// protected int getDefaultHeight() +// { +// return 200; +// } +// +// @Override +// protected int getDefaultWidth() +// { +// return 200; +// } +// +// @Override +// protected int getLayoutId() +// { +// return R.layout.widget_wrapper; +// } +// +// @Override +// protected PendingIntent getOnClickPendingIntent(Context context, +// Habit habit) +// { +// return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); +// } +// +// @Override +// protected void refreshCustomViewData(Context context, +// View view, +// Habit habit) +// { +// GraphWidgetView widgetView = (GraphWidgetView) view; +// FrequencyChart chart = (FrequencyChart) widgetView.getDataView(); +// +// int color = ColorUtils.getColor(context, habit.getColor()); +// +// chart.setColor(color); +// chart.setFrequency(habit.getRepetitions().getWeekdayFrequency()); +// } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidgetProvider.java index e30a6f71c..a5451a647 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/HistoryWidgetProvider.java @@ -18,54 +18,74 @@ */ package org.isoron.uhabits.widgets; -import android.app.*; import android.content.*; -import android.view.*; +import android.support.annotation.*; import org.apache.commons.lang3.*; -import org.isoron.uhabits.*; -import org.isoron.uhabits.models.*; -import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.ui.widgets.*; -public class HistoryWidgetProvider extends BaseWidgetProvider +public class HistoryWidgetProvider extends BaseWidgetProvider { + @NonNull @Override - protected View buildCustomView(Context context, Habit habit) + protected BaseWidget getWidgetFromId(@NonNull Context context, int id) { throw new NotImplementedException(""); -// HistoryChart dataView = new HistoryChart(context); -// GraphWidgetView view = new GraphWidgetView(context, dataView); -// view.setHabit(habit); -// return view; - } - - @Override - protected void refreshCustomViewData(View view) - { - ((HabitChart) view).refreshData(); } - @Override - protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) - { - return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); - } - - @Override - protected int getDefaultHeight() - { - return 250; - } - - @Override - protected int getDefaultWidth() - { - return 250; - } - - @Override - protected int getLayoutId() - { - return R.layout.widget_wrapper; - } +// @NonNull +// @Override +// protected BaseWidget getWidgetFromId(int id) +// { +// throw new NotImplementedException(""); +// } +// +// @Override +// protected View buildCustomView(Context context, Habit habit) +// { +// HistoryChart dataView = new HistoryChart(context); +// GraphWidgetView widgetView = new GraphWidgetView(context, dataView); +// widgetView.setTitle(habit.getName()); +// return widgetView; +// } +// +// @Override +// protected int getDefaultHeight() +// { +// return 250; +// } +// +// @Override +// protected int getDefaultWidth() +// { +// return 250; +// } +// +// @Override +// protected int getLayoutId() +// { +// return R.layout.widget_wrapper; +// } +// +// @Override +// protected PendingIntent getOnClickPendingIntent(Context context, +// Habit habit) +// { +// return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); +// } +// +// @Override +// protected void refreshCustomViewData(Context context, +// View view, +// Habit habit) +// { +// GraphWidgetView widgetView = (GraphWidgetView) view; +// HistoryChart chart = (HistoryChart) widgetView.getDataView(); +// +// int color = ColorUtils.getColor(context, habit.getColor()); +// int[] values = habit.getCheckmarks().getAllValues(); +// +// chart.setColor(color); +// chart.setCheckmarks(values); +// } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.java index f01b7be20..0b250d07d 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/ScoreWidgetProvider.java @@ -18,63 +18,73 @@ */ package org.isoron.uhabits.widgets; -import android.app.*; import android.content.*; -import android.view.*; +import android.support.annotation.*; import org.apache.commons.lang3.*; -import org.isoron.uhabits.*; -import org.isoron.uhabits.models.*; -import org.isoron.uhabits.ui.common.views.*; -import org.isoron.uhabits.ui.habits.show.views.*; -import org.isoron.uhabits.utils.*; +import org.isoron.uhabits.ui.widgets.*; public class ScoreWidgetProvider extends BaseWidgetProvider { + @NonNull @Override - protected View buildCustomView(Context context, Habit habit) + protected BaseWidget getWidgetFromId(@NonNull Context context, int id) { - int defaultScoreInterval = InterfaceUtils.getDefaultScoreSpinnerPosition(context); - int size = ScoreCard.BUCKET_SIZES[defaultScoreInterval]; - - ScoreChart dataView = new ScoreChart(context); - dataView.setIsTransparencyEnabled(true); - dataView.setBucketSize(size); - -// GraphWidgetView view = new GraphWidgetView(context, dataView); -// view.setHabit(habit); -// return view; - throw new NotImplementedException(""); } - @Override - protected void refreshCustomViewData(View view) - { - ((HabitChart) view).refreshData(); - } - - @Override - protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) - { - return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); - } - - @Override - protected int getDefaultHeight() - { - return 300; - } - - @Override - protected int getDefaultWidth() - { - return 300; - } - - @Override - protected int getLayoutId() - { - return R.layout.widget_wrapper; - } +// @Override +// protected View buildCustomView(Context context, Habit habit) +// { +// ScoreChart dataView = new ScoreChart(context); +// GraphWidgetView view = new GraphWidgetView(context, dataView); +// view.setTitle(habit.getName()); +// return view; +// } +// +// @Override +// protected int getDefaultHeight() +// { +// return 300; +// } +// +// @Override +// protected int getDefaultWidth() +// { +// return 300; +// } +// +// @Override +// protected int getLayoutId() +// { +// return R.layout.widget_wrapper; +// } +// +// @Override +// protected PendingIntent getOnClickPendingIntent(Context context, +// Habit habit) +// { +// return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); +// } +// +// @Override +// protected void refreshCustomViewData(Context context, +// View view, +// Habit habit) +// { +// int defaultScoreInterval = +// InterfaceUtils.getDefaultScoreSpinnerPosition(context); +// int size = ScoreCard.BUCKET_SIZES[defaultScoreInterval]; +// +// GraphWidgetView widgetView = (GraphWidgetView) view; +// ScoreChart chart = (ScoreChart) widgetView.getDataView(); +// +// int color = ColorUtils.getColor(context, habit.getColor()); +// List scores = habit.getScores().getAll(); +// +// chart.setIsTransparencyEnabled(true); +// chart.setBucketSize(size); +// chart.setColor(color); +// chart.setScores(scores); +// } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/StreakWidgetProvider.java b/app/src/main/java/org/isoron/uhabits/widgets/StreakWidgetProvider.java index 9b0052f53..71862edef 100644 --- a/app/src/main/java/org/isoron/uhabits/widgets/StreakWidgetProvider.java +++ b/app/src/main/java/org/isoron/uhabits/widgets/StreakWidgetProvider.java @@ -18,54 +18,69 @@ */ package org.isoron.uhabits.widgets; -import android.app.*; import android.content.*; -import android.view.*; +import android.support.annotation.*; import org.apache.commons.lang3.*; -import org.isoron.uhabits.*; -import org.isoron.uhabits.models.*; -import org.isoron.uhabits.ui.common.views.*; +import org.isoron.uhabits.ui.widgets.*; -public class StreakWidgetProvider extends BaseWidgetProvider +public class StreakWidgetProvider extends BaseWidgetProvider { + @NonNull @Override - protected View buildCustomView(Context context, Habit habit) + protected BaseWidget getWidgetFromId(@NonNull Context context, int id) { - StreakChart dataView = new StreakChart(context); throw new NotImplementedException(""); -// GraphWidgetView view = new GraphWidgetView(context, dataView); -// view.setHabit(habit); -// return view; - } - - @Override - protected void refreshCustomViewData(View view) - { - ((HabitChart) view).refreshData(); } - @Override - protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) - { - return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); - } - - @Override - protected int getDefaultHeight() - { - return 200; - } - - @Override - protected int getDefaultWidth() - { - return 200; - } - - @Override - protected int getLayoutId() - { - return R.layout.widget_wrapper; - } +// @Override +// protected View buildCustomView(Context context, Habit habit) +// { +// StreakChart dataView = new StreakChart(context); +// GraphWidgetView view = new GraphWidgetView(context, dataView); +// view.setTitle(habit.getName()); +// return view; +// } +// +// @Override +// protected int getDefaultHeight() +// { +// return 200; +// } +// +// @Override +// protected int getDefaultWidth() +// { +// return 200; +// } +// +// @Override +// protected int getLayoutId() +// { +// return R.layout.widget_wrapper; +// } +// +// @Override +// protected PendingIntent getOnClickPendingIntent(Context context, +// Habit habit) +// { +// return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); +// } +// +// @Override +// protected void refreshCustomViewData(Context context, +// View view, +// Habit habit) +// { +// GraphWidgetView widgetView = (GraphWidgetView) view; +// StreakChart chart = (StreakChart) widgetView.getDataView(); +// +// int color = ColorUtils.getColor(context, habit.getColor()); +// +// // TODO: make this dynamic +// List streaks = habit.getStreaks().getBest(10); +// +// chart.setColor(color); +// chart.setStreaks(streaks); +// } } diff --git a/app/src/main/java/org/isoron/uhabits/widgets/WidgetManager.java b/app/src/main/java/org/isoron/uhabits/widgets/WidgetManager.java deleted file mode 100644 index 69643059c..000000000 --- a/app/src/main/java/org/isoron/uhabits/widgets/WidgetManager.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright (C) 2016 Álinson Santos Xavier - * - * This file is part of Loop Habit Tracker. - * - * Loop Habit Tracker is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by the - * Free Software Foundation, either version 3 of the License, or (at your - * option) any later version. - * - * Loop Habit Tracker is distributed in the hope that it will be useful, but - * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY - * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for - * more details. - * - * You should have received a copy of the GNU General Public License along - * with this program. If not, see . - */ - -package org.isoron.uhabits.widgets; - -import android.appwidget.AppWidgetManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; - -public class WidgetManager -{ - public static void updateWidgets(Context context) - { - updateWidgets(context, CheckmarkWidgetProvider.class); - updateWidgets(context, HistoryWidgetProvider.class); - updateWidgets(context, ScoreWidgetProvider.class); - updateWidgets(context, StreakWidgetProvider.class); - updateWidgets(context, FrequencyWidgetProvider.class); - } - - private static void updateWidgets(Context context, Class providerClass) - { - ComponentName provider = new ComponentName(context, providerClass); - Intent intent = new Intent(context, providerClass); - intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); - int ids[] = AppWidgetManager.getInstance(context).getAppWidgetIds(provider); - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); - context.sendBroadcast(intent); - } -} diff --git a/app/src/main/res/xml/widget_checkmark_info.xml b/app/src/main/res/xml/widget_checkmark_info.xml index 3474f56bf..729170884 100644 --- a/app/src/main/res/xml/widget_checkmark_info.xml +++ b/app/src/main/res/xml/widget_checkmark_info.xml @@ -25,7 +25,7 @@ android:previewImage="@drawable/widget_preview_checkmark" android:resizeMode="none" android:updatePeriodMillis="3600000" - android:configure="org.isoron.uhabits.widgets.HabitPickerDialog" + android:configure="org.isoron.uhabits.ui.widgets.HabitPickerDialog" android:widgetCategory="home_screen"> diff --git a/app/src/main/res/xml/widget_frequency_info.xml b/app/src/main/res/xml/widget_frequency_info.xml index 32a25906b..2cb07f9a6 100644 --- a/app/src/main/res/xml/widget_frequency_info.xml +++ b/app/src/main/res/xml/widget_frequency_info.xml @@ -27,7 +27,7 @@ android:previewImage="@drawable/widget_preview_frequency" android:resizeMode="vertical|horizontal" android:updatePeriodMillis="3600000" - android:configure="org.isoron.uhabits.widgets.HabitPickerDialog" + android:configure="org.isoron.uhabits.ui.widgets.HabitPickerDialog" android:widgetCategory="home_screen"> \ No newline at end of file diff --git a/app/src/main/res/xml/widget_history_info.xml b/app/src/main/res/xml/widget_history_info.xml index 5b8d501af..392fa0892 100644 --- a/app/src/main/res/xml/widget_history_info.xml +++ b/app/src/main/res/xml/widget_history_info.xml @@ -27,7 +27,7 @@ android:previewImage="@drawable/widget_preview_history" android:resizeMode="vertical|horizontal" android:updatePeriodMillis="3600000" - android:configure="org.isoron.uhabits.widgets.HabitPickerDialog" + android:configure="org.isoron.uhabits.ui.widgets.HabitPickerDialog" android:widgetCategory="home_screen"> \ No newline at end of file diff --git a/app/src/main/res/xml/widget_score_info.xml b/app/src/main/res/xml/widget_score_info.xml index e2be33252..e69902047 100644 --- a/app/src/main/res/xml/widget_score_info.xml +++ b/app/src/main/res/xml/widget_score_info.xml @@ -27,7 +27,7 @@ android:previewImage="@drawable/widget_preview_score" android:resizeMode="vertical|horizontal" android:updatePeriodMillis="3600000" - android:configure="org.isoron.uhabits.widgets.HabitPickerDialog" + android:configure="org.isoron.uhabits.ui.widgets.HabitPickerDialog" android:widgetCategory="home_screen"> \ No newline at end of file diff --git a/app/src/main/res/xml/widget_streak_info.xml b/app/src/main/res/xml/widget_streak_info.xml index 4ca86447f..e201161dd 100644 --- a/app/src/main/res/xml/widget_streak_info.xml +++ b/app/src/main/res/xml/widget_streak_info.xml @@ -27,7 +27,7 @@ android:previewImage="@drawable/widget_preview_streaks" android:resizeMode="vertical|horizontal" android:updatePeriodMillis="3600000" - android:configure="org.isoron.uhabits.widgets.HabitPickerDialog" + android:configure="org.isoron.uhabits.ui.widgets.HabitPickerDialog" android:widgetCategory="home_screen"> \ No newline at end of file diff --git a/app/src/test/java/org/isoron/uhabits/TestModule.java b/app/src/test/java/org/isoron/uhabits/TestModule.java index f49c96318..f93845f89 100644 --- a/app/src/test/java/org/isoron/uhabits/TestModule.java +++ b/app/src/test/java/org/isoron/uhabits/TestModule.java @@ -35,16 +35,15 @@ public class TestModule { @Singleton @Provides - Preferences providePreferences() + CommandRunner provideCommandRunner() { - return mock(Preferences.class); + return mock(CommandRunner.class); } - @Singleton @Provides - CommandRunner provideCommandRunner() + Habit provideHabit() { - return mock(CommandRunner.class); + return mock(Habit.class); } @Singleton @@ -55,15 +54,23 @@ public class TestModule } @Provides - Habit provideHabit() + @Singleton + ModelFactory provideModelFactory() { - return mock(Habit.class); + return new MemoryModelFactory(); + } + + @Singleton + @Provides + Preferences providePreferences() + { + return mock(Preferences.class); } @Provides @Singleton - ModelFactory provideModelFactory() + WidgetPreferences provideWidgetPreferences() { - return new MemoryModelFactory(); + return mock(WidgetPreferences.class); } }