Start refactoring widgets

pull/145/head
Alinson S. Xavier 9 years ago
parent a90e26691f
commit 52701666bc

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 8.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 KiB

After

Width:  |  Height:  |  Size: 9.6 KiB

@ -19,21 +19,25 @@
package org.isoron.uhabits; package org.isoron.uhabits;
import android.content.Context; import android.appwidget.*;
import android.os.Build; import android.content.*;
import android.os.Looper; import android.os.*;
import android.support.test.InstrumentationRegistry; import android.support.test.*;
import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.tasks.BaseTask; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.InterfaceUtils; import org.isoron.uhabits.utils.*;
import org.isoron.uhabits.utils.Preferences; import org.junit.*;
import org.junit.Before;
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 public class BaseAndroidTest
{ {
@ -52,6 +56,9 @@ public class BaseAndroidTest
@Inject @Inject
protected HabitList habitList; protected HabitList habitList;
@Inject
protected CommandRunner commandRunner;
protected AndroidTestComponent androidTestComponent; protected AndroidTestComponent androidTestComponent;
protected HabitFixtures fixtures; protected HabitFixtures fixtures;
@ -78,6 +85,18 @@ public class BaseAndroidTest
fixtures = new HabitFixtures(habitList); fixtures = new HabitFixtures(habitList);
} }
protected void sleep(int time)
{
try
{
Thread.sleep(time);
}
catch (InterruptedException e)
{
fail();
}
}
protected void waitForAsyncTasks() protected void waitForAsyncTasks()
throws InterruptedException, TimeoutException throws InterruptedException, TimeoutException
{ {
@ -89,4 +108,19 @@ public class BaseAndroidTest
BaseTask.waitForTasks(10000); BaseTask.waitForTasks(10000);
} }
protected void assertWidgetProviderIsInstalled(ComponentName desiredProvider)
{
AppWidgetManager manager = AppWidgetManager.getInstance(targetContext);
List<AppWidgetProviderInfo> providerInfoList;
List<ComponentName> installedProviders;
providerInfoList = manager.getInstalledProviders();
installedProviders = new LinkedList<>();
for (AppWidgetProviderInfo info : providerInfoList)
installedProviders.add(info.provider);
assertThat(installedProviders, hasItems(desiredProvider));
}
} }

@ -21,10 +21,11 @@ package org.isoron.uhabits;
import android.graphics.*; import android.graphics.*;
import android.os.*; import android.os.*;
import android.support.annotation.*;
import android.view.*; import android.view.*;
import android.widget.*;
import org.isoron.uhabits.tasks.*; import org.isoron.uhabits.ui.widgets.*;
import org.isoron.uhabits.ui.common.views.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
import java.io.*; import java.io.*;
@ -34,7 +35,9 @@ import static junit.framework.Assert.*;
public class BaseViewTest extends BaseAndroidTest public class BaseViewTest extends BaseAndroidTest
{ {
protected static final double DEFAULT_SIMILARITY_CUTOFF = 0.09; protected static final double DEFAULT_SIMILARITY_CUTOFF = 0.09;
public static final int HISTOGRAM_BIN_SIZE = 8; public static final int HISTOGRAM_BIN_SIZE = 8;
private double similarityCutoff; private double similarityCutoff;
@Override @Override
@ -44,21 +47,8 @@ public class BaseViewTest extends BaseAndroidTest
similarityCutoff = DEFAULT_SIMILARITY_CUTOFF; similarityCutoff = DEFAULT_SIMILARITY_CUTOFF;
} }
protected void setSimilarityCutoff(double similarityCutoff) protected void assertRenders(View view, String expectedImagePath)
{ throws IOException
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
{ {
StringBuilder errorMessage = new StringBuilder(); StringBuilder errorMessage = new StringBuilder();
expectedImagePath = getVersionedViewAssetPath(expectedImagePath); expectedImagePath = getVersionedViewAssetPath(expectedImagePath);
@ -70,13 +60,14 @@ public class BaseViewTest extends BaseAndroidTest
int width = actual.getWidth(); int width = actual.getWidth();
int height = actual.getHeight(); int height = actual.getHeight();
Bitmap scaledExpected = Bitmap.createScaledBitmap(expected, width, height, true); Bitmap scaledExpected =
Bitmap.createScaledBitmap(expected, width, height, true);
double distance; double distance;
boolean similarEnough = true; boolean similarEnough = true;
if ((distance = compareHistograms(getHistogram(actual), getHistogram(scaledExpected))) > if ((distance = compareHistograms(getHistogram(actual),
similarityCutoff) getHistogram(scaledExpected))) > similarityCutoff)
{ {
similarEnough = false; similarEnough = false;
errorMessage.append(String.format( errorMessage.append(String.format(
@ -84,84 +75,107 @@ public class BaseViewTest extends BaseAndroidTest
distance)); distance));
} }
if(!similarEnough) if (!similarEnough)
{ {
saveBitmap(expectedImagePath, ".expected", scaledExpected); saveBitmap(expectedImagePath, ".expected", scaledExpected);
String path = saveBitmap(expectedImagePath, "", actual); 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()); fail(errorMessage.toString());
} }
actual.recycle();
expected.recycle(); expected.recycle();
scaledExpected.recycle(); scaledExpected.recycle();
} }
private Bitmap getBitmapFromAssets(String path) throws IOException protected int dpToPixels(int dp)
{ {
InputStream stream = testContext.getAssets().open(path); return (int) InterfaceUtils.dpToPixels(targetContext, dp);
return BitmapFactory.decodeStream(stream);
} }
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) view.measure(specWidth, specHeight);
{ view.layout(0, 0, view.getMeasuredWidth(), view.getMeasuredHeight());
try
{
String vpath = "views-v21/" + path;
testContext.getAssets().open(vpath);
result = vpath;
} }
catch (IOException e)
protected void setSimilarityCutoff(double similarityCutoff)
{ {
// ignored this.similarityCutoff = similarityCutoff;
}
} }
if(result == null) protected void tap(GestureDetector.OnGestureListener view, int x, int y)
result = "views/" + path; throws InterruptedException
{
return result; 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) private double compareHistograms(int[][] actualHistogram,
throws IOException int[][] expectedHistogram)
{ {
File dir = FileUtils.getSDCardDir("test-screenshots"); long diff = 0;
if(dir == null) dir = FileUtils.getFilesDir("test-screenshots"); long total = 0;
if(dir == null) throw new RuntimeException("Could not find suitable dir for screenshots");
filename = filename.replaceAll("\\.png$", suffix + ".png"); for (int i = 0; i < 256 / HISTOGRAM_BIN_SIZE; i++)
String absolutePath = String.format("%s/%s", dir.getAbsolutePath(), filename); {
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(); total += actualHistogram[0][i];
if(!parent.exists() && !parent.mkdirs()) total += actualHistogram[1][i];
throw new RuntimeException(String.format("Could not create dir: %s", total += actualHistogram[2][i];
parent.getAbsolutePath())); total += actualHistogram[3][i];
}
FileOutputStream out = new FileOutputStream(absolutePath); return (double) diff / total / 2;
bitmap.compress(Bitmap.CompressFormat.PNG, 100, out); }
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) private int[][] getHistogram(Bitmap bitmap)
{ {
int histogram[][] = new int[4][256 / HISTOGRAM_BIN_SIZE]; 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 color = bitmap.getPixel(x, y);
int[] argb = new int[]{ int[] argb = new int[]{
(color >> 24) & 0xff, //alpha (color >> 24) & 0xff, //alpha
(color >> 16) & 0xff, //red (color >> 16) & 0xff, //red
(color >> 8) & 0xff, //green (color >> 8) & 0xff, //green
(color ) & 0xff //blue (color) & 0xff //blue
}; };
histogram[0][argb[0] / HISTOGRAM_BIN_SIZE]++; histogram[0][argb[0] / HISTOGRAM_BIN_SIZE]++;
@ -174,59 +188,49 @@ public class BaseViewTest extends BaseAndroidTest
return histogram; return histogram;
} }
private double compareHistograms(int[][] actualHistogram, int[][] expectedHistogram) private String getVersionedViewAssetPath(String path)
{ {
long diff = 0; String result = null;
long total = 0;
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]); try
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];
}
return (double) diff / total / 2;
}
protected int dpToPixels(int dp)
{ {
return (int) InterfaceUtils.dpToPixels(targetContext, dp); String vpath = "views-v21/" + path;
testContext.getAssets().open(vpath);
result = vpath;
} }
catch (IOException e)
protected void tap(GestureDetector.OnGestureListener view, int x, int y) throws InterruptedException
{ {
long now = SystemClock.uptimeMillis(); // ignored
MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_UP, dpToPixels(x),
dpToPixels(y), 0);
view.onSingleTapUp(e);
e.recycle();
} }
protected void refreshData(final HabitChart view)
{
new BaseTask()
{
@Override
protected void doInBackground()
{
view.refreshData();
} }
}.execute();
try if (result == null) result = "views/" + path;
{
waitForAsyncTasks(); return result;
} }
catch (Exception e)
private String saveBitmap(String filename, String suffix, Bitmap bitmap)
throws IOException
{ {
throw new RuntimeException("Time out"); 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");
filename = filename.replaceAll("\\.png$", suffix + ".png");
String absolutePath =
String.format("%s/%s", dir.getAbsolutePath(), filename);
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;
} }
} }

@ -48,7 +48,7 @@ public class FrequencyChartTest extends BaseViewTest
view = new FrequencyChart(targetContext); view = new FrequencyChart(targetContext);
view.setFrequency(habit.getRepetitions().getWeekdayFrequency()); view.setFrequency(habit.getRepetitions().getWeekdayFrequency());
view.setColor(ColorUtils.getAndroidTestColor(habit.getColor())); view.setColor(ColorUtils.getAndroidTestColor(habit.getColor()));
measureView(dpToPixels(300), dpToPixels(100), view); measureView(view, dpToPixels(300), dpToPixels(100));
} }
@Test @Test
@ -69,7 +69,7 @@ public class FrequencyChartTest extends BaseViewTest
@Test @Test
public void testRender_withDifferentSize() throws Throwable public void testRender_withDifferentSize() throws Throwable
{ {
measureView(dpToPixels(200), dpToPixels(200), view); measureView(view, dpToPixels(200), dpToPixels(200));
assertRenders(view, BASE_PATH + "renderDifferentSize.png"); assertRenders(view, BASE_PATH + "renderDifferentSize.png");
} }

@ -48,7 +48,7 @@ public class HistoryChartTest extends BaseViewTest
chart = new HistoryChart(targetContext); chart = new HistoryChart(targetContext);
chart.setCheckmarks(habit.getCheckmarks().getAllValues()); chart.setCheckmarks(habit.getCheckmarks().getAllValues());
chart.setColor(ColorUtils.getAndroidTestColor(habit.getColor())); chart.setColor(ColorUtils.getAndroidTestColor(habit.getColor()));
measureView(dpToPixels(400), dpToPixels(200), chart); measureView(chart, dpToPixels(400), dpToPixels(200));
} }
// @Test // @Test
@ -106,7 +106,7 @@ public class HistoryChartTest extends BaseViewTest
@Test @Test
public void testRender_withDifferentSize() throws Throwable public void testRender_withDifferentSize() throws Throwable
{ {
measureView(dpToPixels(200), dpToPixels(200), chart); measureView(chart, dpToPixels(200), dpToPixels(200));
assertRenders(chart, BASE_PATH + "renderDifferentSize.png"); assertRenders(chart, BASE_PATH + "renderDifferentSize.png");
} }

@ -55,7 +55,7 @@ public class RingViewTest extends BaseViewTest
@Test @Test
public void testRender_base() throws IOException public void testRender_base() throws IOException
{ {
measureView(dpToPixels(100), dpToPixels(100), view); measureView(view, dpToPixels(100), dpToPixels(100));
assertRenders(view, BASE_PATH + "render.png"); assertRenders(view, BASE_PATH + "render.png");
} }
@ -65,7 +65,7 @@ public class RingViewTest extends BaseViewTest
view.setPercentage(0.25f); view.setPercentage(0.25f);
view.setColor(ColorUtils.getAndroidTestColor(5)); view.setColor(ColorUtils.getAndroidTestColor(5));
measureView(dpToPixels(200), dpToPixels(200), view); measureView(view, dpToPixels(200), dpToPixels(200));
assertRenders(view, BASE_PATH + "renderDifferentParams.png"); assertRenders(view, BASE_PATH + "renderDifferentParams.png");
} }
} }

@ -50,9 +50,9 @@ public class ScoreChartTest extends BaseViewTest
view = new ScoreChart(targetContext); view = new ScoreChart(targetContext);
view.setScores(habit.getScores().getAll()); view.setScores(habit.getScores().getAll());
view.setPrimaryColor(ColorUtils.getColor(targetContext, habit.getColor())); view.setColor(ColorUtils.getColor(targetContext, habit.getColor()));
view.setBucketSize(7); view.setBucketSize(7);
measureView(dpToPixels(300), dpToPixels(200), view); measureView(view, dpToPixels(300), dpToPixels(200));
} }
@Test @Test
@ -75,7 +75,7 @@ public class ScoreChartTest extends BaseViewTest
@Test @Test
public void testRender_withDifferentSize() throws Throwable public void testRender_withDifferentSize() throws Throwable
{ {
measureView(dpToPixels(200), dpToPixels(200), view); measureView(view, dpToPixels(200), dpToPixels(200));
assertRenders(view, BASE_PATH + "renderDifferentSize.png"); assertRenders(view, BASE_PATH + "renderDifferentSize.png");
} }

@ -48,7 +48,7 @@ public class StreakChartTest extends BaseViewTest
view = new StreakChart(targetContext); view = new StreakChart(targetContext);
view.setColor(ColorUtils.getAndroidTestColor(habit.getColor())); view.setColor(ColorUtils.getAndroidTestColor(habit.getColor()));
view.setStreaks(habit.getStreaks().getBest(5)); view.setStreaks(habit.getStreaks().getBest(5));
measureView(dpToPixels(300), dpToPixels(100), view); measureView(view, dpToPixels(300), dpToPixels(100));
} }
@Test @Test
@ -60,7 +60,7 @@ public class StreakChartTest extends BaseViewTest
@Test @Test
public void testRender_withSmallSize() throws Throwable public void testRender_withSmallSize() throws Throwable
{ {
measureView(dpToPixels(100), dpToPixels(100), view); measureView(view, dpToPixels(100), dpToPixels(100));
assertRenders(view, BASE_PATH + "renderSmallSize.png"); assertRenders(view, BASE_PATH + "renderSmallSize.png");
} }

@ -54,7 +54,7 @@ public class CheckmarkButtonViewTest extends BaseViewTest
view.setValue(Checkmark.UNCHECKED); view.setValue(Checkmark.UNCHECKED);
view.setColor(ColorUtils.getAndroidTestColor(7)); view.setColor(ColorUtils.getAndroidTestColor(7));
measureView(dpToPixels(40), dpToPixels(40), view); measureView(view, dpToPixels(40), dpToPixels(40));
} }
@Test @Test

@ -62,7 +62,7 @@ public class CheckmarkPanelViewTest extends BaseViewTest
view.setCheckmarkValues(checkmarks); view.setCheckmarkValues(checkmarks);
view.setColor(ColorUtils.getAndroidTestColor(7)); view.setColor(ColorUtils.getAndroidTestColor(7));
measureView(dpToPixels(200), dpToPixels(200), view); measureView(view, dpToPixels(200), dpToPixels(200));
} }
// protected void waitForLatch() throws InterruptedException // protected void waitForLatch() throws InterruptedException

@ -0,0 +1,90 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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");
}
}

@ -17,20 +17,18 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
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 android.test.suitebuilder.annotation.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.utils.DateUtils; import org.isoron.uhabits.utils.*;
import org.isoron.uhabits.utils.InterfaceUtils; import org.junit.*;
import org.junit.Before; import org.junit.runner.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.io.IOException; import java.io.*;
@RunWith(AndroidJUnit4.class) @RunWith(AndroidJUnit4.class)
@MediumTest @MediumTest
@ -51,9 +49,16 @@ public class CheckmarkWidgetViewTest extends BaseViewTest
habit = fixtures.createShortHabit(); habit = fixtures.createShortHabit();
view = new CheckmarkWidgetView(targetContext); view = new CheckmarkWidgetView(targetContext);
view.setHabit(habit); int color = ColorUtils.getAndroidTestColor(habit.getColor());
refreshData(view); int score = habit.getScores().getTodayValue();
measureView(dpToPixels(100), dpToPixels(200), view); 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 @Test
@ -65,29 +70,23 @@ public class CheckmarkWidgetViewTest extends BaseViewTest
@Test @Test
public void testRender_implicitlyChecked() throws IOException public void testRender_implicitlyChecked() throws IOException
{ {
long today = DateUtils.getStartOfToday(); view.setCheckmarkValue(Checkmark.CHECKED_IMPLICITLY);
long day = DateUtils.millisecondsInOneDay; view.refresh();
habit.getRepetitions().toggleTimestamp(today);
habit.getRepetitions().toggleTimestamp(today - day);
habit.getRepetitions().toggleTimestamp(today - 2 * day);
view.refreshData();
assertRenders(view, PATH + "implicitly_checked.png"); assertRenders(view, PATH + "implicitly_checked.png");
} }
@Test @Test
public void testRender_largeSize() throws IOException public void testRender_largeSize() throws IOException
{ {
measureView(dpToPixels(300), dpToPixels(300), view); measureView(view, dpToPixels(300), dpToPixels(300));
assertRenders(view, PATH + "large_size.png"); assertRenders(view, PATH + "large_size.png");
} }
@Test @Test
public void testRender_unchecked() throws IOException public void testRender_unchecked() throws IOException
{ {
habit.getRepetitions().toggleTimestamp(DateUtils.getStartOfToday()); view.setCheckmarkValue(Checkmark.UNCHECKED);
view.refreshData(); view.refresh();
assertRenders(view, PATH + "unchecked.png"); assertRenders(view, PATH + "unchecked.png");
} }
} }

@ -81,7 +81,7 @@
android:theme="@style/Theme.AppCompat.Light.NoActionBar"/> android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>
<activity <activity
android:name=".widgets.HabitPickerDialog" android:name=".ui.widgets.HabitPickerDialog"
android:theme="@style/Theme.AppCompat.Light.Dialog"> android:theme="@style/Theme.AppCompat.Light.Dialog">
<intent-filter> <intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/> <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
@ -108,53 +108,53 @@
android:resource="@xml/widget_checkmark_info"/> android:resource="@xml/widget_checkmark_info"/>
</receiver> </receiver>
<receiver <!--<receiver-->
android:name=".widgets.HistoryWidgetProvider" <!--android:name=".widgets.HistoryWidgetProvider"-->
android:label="@string/history"> <!--android:label="@string/history">-->
<intent-filter> <!--<intent-filter>-->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/> <!--<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>-->
</intent-filter> <!--</intent-filter>-->
<meta-data <!--<meta-data-->
android:name="android.appwidget.provider" <!--android:name="android.appwidget.provider"-->
android:resource="@xml/widget_history_info"/> <!--android:resource="@xml/widget_history_info"/>-->
</receiver> <!--</receiver>-->
<receiver <!--<receiver-->
android:name=".widgets.ScoreWidgetProvider" <!--android:name=".widgets.ScoreWidgetProvider"-->
android:label="@string/habit_strength"> <!--android:label="@string/habit_strength">-->
<intent-filter> <!--<intent-filter>-->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/> <!--<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>-->
</intent-filter> <!--</intent-filter>-->
<meta-data <!--<meta-data-->
android:name="android.appwidget.provider" <!--android:name="android.appwidget.provider"-->
android:resource="@xml/widget_score_info"/> <!--android:resource="@xml/widget_score_info"/>-->
</receiver> <!--</receiver>-->
<receiver <!--<receiver-->
android:name=".widgets.StreakWidgetProvider" <!--android:name=".widgets.StreakWidgetProvider"-->
android:label="@string/streaks"> <!--android:label="@string/streaks">-->
<intent-filter> <!--<intent-filter>-->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/> <!--<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>-->
</intent-filter> <!--</intent-filter>-->
<meta-data <!--<meta-data-->
android:name="android.appwidget.provider" <!--android:name="android.appwidget.provider"-->
android:resource="@xml/widget_streak_info"/> <!--android:resource="@xml/widget_streak_info"/>-->
</receiver> <!--</receiver>-->
<receiver <!--<receiver-->
android:name=".widgets.FrequencyWidgetProvider" <!--android:name=".widgets.FrequencyWidgetProvider"-->
android:label="@string/frequency"> <!--android:label="@string/frequency">-->
<intent-filter> <!--<intent-filter>-->
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/> <!--<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>-->
</intent-filter> <!--</intent-filter>-->
<meta-data <!--<meta-data-->
android:name="android.appwidget.provider" <!--android:name="android.appwidget.provider"-->
android:resource="@xml/widget_frequency_info"/> <!--android:resource="@xml/widget_frequency_info"/>-->
</receiver> <!--</receiver>-->
<receiver android:name=".HabitBroadcastReceiver"> <receiver android:name=".HabitBroadcastReceiver">
<intent-filter> <intent-filter>

@ -63,4 +63,11 @@ public class AndroidModule
{ {
return new Preferences(); return new Preferences();
} }
@Provides
@Singleton
WidgetPreferences provideWidgetPreferences()
{
return new WidgetPreferences();
}
} }

@ -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.model.*;
import org.isoron.uhabits.ui.habits.list.views.*; import org.isoron.uhabits.ui.habits.list.views.*;
import org.isoron.uhabits.ui.habits.show.*; import org.isoron.uhabits.ui.habits.show.*;
import org.isoron.uhabits.ui.widgets.*;
import org.isoron.uhabits.widgets.*; import org.isoron.uhabits.widgets.*;
/** /**
@ -90,4 +91,8 @@ public interface BaseComponent
void inject(BaseDialogFragment baseDialogFragment); void inject(BaseDialogFragment baseDialogFragment);
void inject(ShowHabitController showHabitController); void inject(ShowHabitController showHabitController);
void inject(BaseWidget baseWidget);
void inject(WidgetUpdater widgetManager);
} }

@ -27,14 +27,12 @@ import android.os.*;
import android.preference.*; import android.preference.*;
import android.support.v4.app.*; import android.support.v4.app.*;
import android.support.v4.app.TaskStackBuilder; import android.support.v4.app.TaskStackBuilder;
import android.support.v4.content.*;
import org.isoron.uhabits.commands.*; import org.isoron.uhabits.commands.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*; import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.ui.habits.show.*; import org.isoron.uhabits.ui.habits.show.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
import org.isoron.uhabits.widgets.*;
import java.util.*; import java.util.*;
@ -124,16 +122,6 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
notificationManager.cancel(notificationId); 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 @Override
public void onReceive(final Context context, Intent intent) public void onReceive(final Context context, Intent intent)
{ {
@ -178,7 +166,6 @@ public class HabitBroadcastReceiver extends BroadcastReceiver
} }
dismissNotification(context, habitId); dismissNotification(context, habitId);
sendRefreshBroadcast(context);
} }
private boolean checkWeekday(Intent intent, Habit habit) private boolean checkWeekday(Intent intent, Habit habit)

@ -25,21 +25,16 @@ import android.support.annotation.*;
import com.activeandroid.*; import com.activeandroid.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.ui.widgets.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
import java.io.*; import java.io.*;
import javax.inject.*;
/** /**
* The Android application for Loop Habit Tracker. * The Android application for Loop Habit Tracker.
*/ */
public class HabitsApplication extends Application 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_BUG_REPORT = 4;
public static final int RESULT_EXPORT_CSV = 2; public static final int RESULT_EXPORT_CSV = 2;
@ -56,19 +51,13 @@ public class HabitsApplication extends Application
@Nullable @Nullable
private static Context context; private static Context context;
@Inject private static WidgetUpdater widgetManager;
HabitList habitList;
public static BaseComponent getComponent() public static BaseComponent getComponent()
{ {
return component; return component;
} }
public HabitList getHabitList()
{
return habitList;
}
public static void setComponent(BaseComponent component) public static void setComponent(BaseComponent component)
{ {
HabitsApplication.component = component; HabitsApplication.component = component;
@ -86,6 +75,15 @@ public class HabitsApplication extends Application
return application; return application;
} }
@NonNull
public static WidgetUpdater getWidgetManager()
{
if (widgetManager == null)
throw new RuntimeException("widgetManager is null");
return widgetManager;
}
public static boolean isTestMode() public static boolean isTestMode()
{ {
try try
@ -108,6 +106,7 @@ public class HabitsApplication extends Application
HabitsApplication.context = this; HabitsApplication.context = this;
HabitsApplication.application = this; HabitsApplication.application = this;
component = DaggerAndroidComponent.builder().build(); component = DaggerAndroidComponent.builder().build();
component.inject(this);
if (isTestMode()) if (isTestMode())
{ {
@ -115,7 +114,9 @@ public class HabitsApplication extends Application
if (db.exists()) db.delete(); if (db.exists()) db.delete();
} }
component.inject(this); widgetManager = new WidgetUpdater(this);
widgetManager.startListening();
DatabaseUtils.initializeActiveAndroid(); DatabaseUtils.initializeActiveAndroid();
} }
@ -124,6 +125,7 @@ public class HabitsApplication extends Application
{ {
HabitsApplication.context = null; HabitsApplication.context = null;
ActiveAndroid.dispose(); ActiveAndroid.dispose();
widgetManager.stopListening();
super.onTerminate(); super.onTerminate();
} }
} }

@ -160,7 +160,7 @@ public abstract class BaseScreen
* *
* @param rootView the root view for this screen. * @param rootView the root view for this screen.
*/ */
public void setRootView(@Nullable BaseRootView rootView) protected void setRootView(@Nullable BaseRootView rootView)
{ {
this.rootView = rootView; this.rootView = rootView;
activity.setContentView(rootView); activity.setContentView(rootView);

@ -28,7 +28,6 @@ import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.tasks.*; import org.isoron.uhabits.tasks.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
import org.isoron.uhabits.widgets.*;
import java.io.*; import java.io.*;
import java.lang.Process; import java.lang.Process;
@ -119,22 +118,6 @@ public class BaseSystem
}.execute(); }.execute();
} }
/**
* Refreshes all application widgets.
*/
public void updateWidgets()
{
new BaseTask()
{
@Override
protected void doInBackground()
{
WidgetManager.updateWidgets(context);
}
}.execute();
}
private String getDeviceInfo() private String getDeviceInfo()
{ {
if (context == null) return "null context\n"; if (context == null) return "null context\n";

@ -137,7 +137,7 @@ public class ScoreChart extends ScrollableChart
requestLayout(); requestLayout();
} }
public void setPrimaryColor(int primaryColor) public void setColor(int primaryColor)
{ {
this.primaryColor = primaryColor; this.primaryColor = primaryColor;
postInvalidate(); postInvalidate();

@ -19,14 +19,14 @@
package org.isoron.uhabits.ui.habits.list; package org.isoron.uhabits.ui.habits.list;
import android.os.Bundle; import android.os.*;
import org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.*;
import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.ui.BaseActivity; import org.isoron.uhabits.ui.*;
import org.isoron.uhabits.ui.BaseSystem; 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. * Activity that allows the user to see and modify the list of habits.
@ -36,19 +36,55 @@ public class ListHabitsActivity extends BaseActivity
@Inject @Inject
HabitList habitList; HabitList habitList;
private HabitCardListAdapter adapter;
private ListHabitsRootView rootView;
private ListHabitsScreen screen;
private ListHabitsMenu menu;
private ListHabitsSelectionMenu selectionMenu;
private ListHabitsController controller;
private BaseSystem system;
@Override @Override
protected void onCreate(Bundle savedInstanceState) protected void onCreate(Bundle savedInstanceState)
{ {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
HabitsApplication.getComponent().inject(this); HabitsApplication.getComponent().inject(this);
BaseSystem system = new BaseSystem(this); int checkmarkCount = ListHabitsRootView.MAX_CHECKMARK_COUNT;
ListHabitsScreen screen = new ListHabitsScreen(this);
ListHabitsController controller = system = new BaseSystem(this);
new ListHabitsController(screen, system, habitList); 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); setScreen(screen);
controller.onStartup(); controller.onStartup();
} }
@Override
protected void onPause()
{
adapter.cancelRefresh();
super.onPause();
}
@Override
protected void onResume()
{
super.onResume();
adapter.refresh();
}
} }

@ -95,7 +95,7 @@ public class ListHabitsController
habitList.reorder(from, to); habitList.reorder(from, to);
} }
public void onImportData(File file) public void onImportData(@NonNull File file)
{ {
ImportDataTask task = new ImportDataTask(file, screen.getProgressBar()); ImportDataTask task = new ImportDataTask(file, screen.getProgressBar());
task.setListener(this); task.setListener(this);
@ -163,7 +163,6 @@ public class ListHabitsController
if (prefs.isFirstRun()) onFirstRun(); if (prefs.isFirstRun()) onFirstRun();
new Handler().postDelayed(() -> { new Handler().postDelayed(() -> {
system.updateWidgets();
system.scheduleReminders(); system.scheduleReminders();
}, 1000); }, 1000);
} }

@ -26,6 +26,7 @@ import android.view.MenuItem;
import org.isoron.uhabits.R; import org.isoron.uhabits.R;
import org.isoron.uhabits.ui.BaseActivity; import org.isoron.uhabits.ui.BaseActivity;
import org.isoron.uhabits.ui.BaseMenu; import org.isoron.uhabits.ui.BaseMenu;
import org.isoron.uhabits.ui.habits.list.model.*;
import org.isoron.uhabits.utils.InterfaceUtils; import org.isoron.uhabits.utils.InterfaceUtils;
public class ListHabitsMenu extends BaseMenu public class ListHabitsMenu extends BaseMenu
@ -35,11 +36,15 @@ public class ListHabitsMenu extends BaseMenu
private boolean showArchived; private boolean showArchived;
private final HabitCardListAdapter adapter;
public ListHabitsMenu(@NonNull BaseActivity activity, public ListHabitsMenu(@NonNull BaseActivity activity,
@NonNull ListHabitsScreen screen) @NonNull ListHabitsScreen screen,
@NonNull HabitCardListAdapter adapter)
{ {
super(activity); super(activity);
this.screen = screen; this.screen = screen;
this.adapter = adapter;
} }
@Override @Override
@ -79,7 +84,7 @@ public class ListHabitsMenu extends BaseMenu
case R.id.action_show_archived: case R.id.action_show_archived:
showArchived = !showArchived; showArchived = !showArchived;
screen.getRootView().setShowArchived(showArchived); adapter.setShowArchived(showArchived);
invalidate(); invalidate();
return true; return true;

@ -59,13 +59,27 @@ public class ListHabitsRootView extends BaseRootView
@BindView(R.id.hintView) @BindView(R.id.hintView)
HintView hintView; HintView hintView;
@Nullable @NonNull
private HabitCardListAdapter listAdapter; private final HabitCardListAdapter listAdapter;
public ListHabitsRootView(@NonNull Context context) public ListHabitsRootView(@NonNull Context context,
@NonNull HabitCardListAdapter listAdapter)
{ {
super(context); 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) public static int getCheckmarkCount(View v)
@ -84,12 +98,6 @@ public class ListHabitsRootView extends BaseRootView
return progressBar; return progressBar;
} }
public void setShowArchived(boolean showArchived)
{
if (listAdapter == null) return;
listAdapter.setShowArchived(showArchived);
}
@NonNull @NonNull
@Override @Override
public Toolbar getToolbar() public Toolbar getToolbar()
@ -103,12 +111,9 @@ public class ListHabitsRootView extends BaseRootView
updateEmptyView(); updateEmptyView();
} }
public void setController(@Nullable ListHabitsController controller, public void setController(@NonNull ListHabitsController controller,
@Nullable ListHabitsSelectionMenu menu) @NonNull ListHabitsSelectionMenu menu)
{ {
listView.setController(null);
if (controller == null || menu == null || listAdapter == null) return;
HabitCardListController listController = HabitCardListController listController =
new HabitCardListController(listAdapter, listView); new HabitCardListController(listAdapter, listView);
@ -118,49 +123,23 @@ public class ListHabitsRootView extends BaseRootView
menu.setListController(listController); 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 @Override
protected void onAttachedToWindow() protected void onAttachedToWindow()
{ {
super.onAttachedToWindow(); super.onAttachedToWindow();
if (listAdapter != null) listAdapter.getObservable().addListener(this); listAdapter.getObservable().addListener(this);
} }
@Override @Override
protected void onDetachedFromWindow() protected void onDetachedFromWindow()
{ {
if (listAdapter != null)
listAdapter.getObservable().removeListener(this); listAdapter.getObservable().removeListener(this);
super.onDetachedFromWindow(); 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() private void updateEmptyView()
{ {
if (listAdapter == null) return;
llEmpty.setVisibility( llEmpty.setVisibility(
listAdapter.getCount() > 0 ? View.GONE : View.VISIBLE); listAdapter.getCount() > 0 ? View.GONE : View.VISIBLE);
} }

@ -32,7 +32,6 @@ import org.isoron.uhabits.models.*;
import org.isoron.uhabits.ui.*; import org.isoron.uhabits.ui.*;
import org.isoron.uhabits.ui.about.*; import org.isoron.uhabits.ui.about.*;
import org.isoron.uhabits.ui.habits.edit.*; 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.habits.show.*;
import org.isoron.uhabits.ui.intro.*; import org.isoron.uhabits.ui.intro.*;
import org.isoron.uhabits.ui.settings.*; import org.isoron.uhabits.ui.settings.*;
@ -42,37 +41,14 @@ import java.io.*;
public class ListHabitsScreen extends BaseScreen public class ListHabitsScreen extends BaseScreen
{ {
@Nullable @Nullable
ListHabitsController controller; ListHabitsController controller;
@NonNull public ListHabitsScreen(@NonNull BaseActivity activity,
private final ListHabitsRootView rootView; ListHabitsRootView rootView)
@NonNull
private final ListHabitsSelectionMenu selectionMenu;
public ListHabitsScreen(@NonNull BaseActivity activity)
{ {
super(activity); super(activity);
rootView = new ListHabitsRootView(activity);
setRootView(rootView); 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 @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() public void showAboutScreen()
{ {
Intent intent = new Intent(activity, AboutActivity.class); Intent intent = new Intent(activity, AboutActivity.class);

@ -42,16 +42,18 @@ public class ListHabitsSelectionMenu extends BaseSelectionMenu
@Inject @Inject
CommandRunner commandRunner; CommandRunner commandRunner;
@Nullable @NonNull
private HabitCardListAdapter listAdapter; private final HabitCardListAdapter listAdapter;
@Nullable @Nullable
private HabitCardListController listController; private HabitCardListController listController;
public ListHabitsSelectionMenu(@NonNull ListHabitsScreen screen) public ListHabitsSelectionMenu(@NonNull ListHabitsScreen screen,
HabitCardListAdapter listAdapter)
{ {
this.screen = screen; this.screen = screen;
HabitsApplication.getComponent().inject(this); HabitsApplication.getComponent().inject(this);
this.listAdapter = listAdapter;
} }
@Override @Override
@ -64,8 +66,6 @@ public class ListHabitsSelectionMenu extends BaseSelectionMenu
@Override @Override
public boolean onItemClicked(@NonNull MenuItem item) public boolean onItemClicked(@NonNull MenuItem item)
{ {
if (listAdapter == null) return false;
List<Habit> selected = listAdapter.getSelected(); List<Habit> selected = listAdapter.getSelected();
if (selected.isEmpty()) return false; if (selected.isEmpty()) return false;
@ -104,7 +104,6 @@ public class ListHabitsSelectionMenu extends BaseSelectionMenu
@Override @Override
public boolean onPrepare(@NonNull Menu menu) public boolean onPrepare(@NonNull Menu menu)
{ {
if (listAdapter == null) return false;
List<Habit> selected = listAdapter.getSelected(); List<Habit> selected = listAdapter.getSelected();
boolean showEdit = (selected.size() == 1); boolean showEdit = (selected.size() == 1);
@ -149,12 +148,6 @@ public class ListHabitsSelectionMenu extends BaseSelectionMenu
screen.startSelection(); screen.startSelection();
} }
public void setListAdapter(@Nullable HabitCardListAdapter listAdapter)
{
if (listAdapter == null) return;
this.listAdapter = listAdapter;
}
public void setListController(HabitCardListController listController) public void setListController(HabitCardListController listController)
{ {
this.listController = listController; this.listController = listController;

@ -93,6 +93,6 @@ public class CheckmarkButtonController
void onInvalidToggle(); void onInvalidToggle();
void onToggle(Habit habit, long timestamp); void onToggle(@NonNull Habit habit, long timestamp);
} }
} }

@ -19,7 +19,7 @@
package org.isoron.uhabits.ui.habits.list.controllers; 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.models.Habit;
import org.isoron.uhabits.ui.habits.list.views.HabitCardView; import org.isoron.uhabits.ui.habits.list.views.HabitCardView;
@ -39,7 +39,7 @@ public class HabitCardController implements HabitCardView.Controller
} }
@Override @Override
public void onToggle(Habit habit, long timestamp) public void onToggle(@NonNull Habit habit, long timestamp)
{ {
if (view != null) view.triggerRipple(timestamp); if (view != null) view.triggerRipple(timestamp);
if (listener != null) listener.onToggle(habit, timestamp); if (listener != null) listener.onToggle(habit, timestamp);

@ -147,7 +147,7 @@ public class HabitCardListController implements DragSortListView.DropListener,
* @param timestamp the timestamps of the checkmark * @param timestamp the timestamps of the checkmark
*/ */
@Override @Override
public void onToggle(Habit habit, long timestamp) public void onToggle(@NonNull Habit habit, long timestamp)
{ {
if (habitListener != null) habitListener.onToggle(habit, timestamp); if (habitListener != null) habitListener.onToggle(habit, timestamp);
} }
@ -203,7 +203,7 @@ public class HabitCardListController implements DragSortListView.DropListener,
* *
* @param habit the habit clicked * @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 * 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 from habit to be moved
* @param to habit that currently occupies the desired position * @param to habit that currently occupies the desired position
*/ */
void onHabitReorder(Habit from, Habit to); void onHabitReorder(@NonNull Habit from, @NonNull Habit to);
} }
/** /**

@ -62,6 +62,11 @@ public class HabitCardListAdapter extends BaseAdapter
cache.setCheckmarkCount(checkmarkCount); cache.setCheckmarkCount(checkmarkCount);
} }
public void cancelRefresh()
{
cache.cancelTasks();
}
/** /**
* Sets all items as not selected. * Sets all items as not selected.
*/ */
@ -77,11 +82,6 @@ public class HabitCardListAdapter extends BaseAdapter
return cache.getHabitCount(); return cache.getHabitCount();
} }
public boolean getIncludeArchived()
{
return cache.getIncludeArchived();
}
/** /**
* Returns the item that occupies a certain position on the list * Returns the item that occupies a certain position on the list
* *
@ -163,6 +163,11 @@ public class HabitCardListAdapter extends BaseAdapter
cache.onDetached(); cache.onDetached();
} }
public void refresh()
{
cache.refreshAllHabits(true);
}
/** /**
* Changes the order of habits on the adapter. * Changes the order of habits on the adapter.
* <p> * <p>

@ -69,6 +69,12 @@ public class HabitCardListCache implements CommandRunner.Listener
HabitsApplication.getComponent().inject(this); HabitsApplication.getComponent().inject(this);
} }
public void cancelTasks()
{
if(currentFetchTask != null)
currentFetchTask.cancel(true);
}
public int[] getCheckmarks(long habitId) public int[] getCheckmarks(long habitId)
{ {
return data.checkmarks.get(habitId); return data.checkmarks.get(habitId);
@ -256,6 +262,7 @@ public class HabitCardListCache implements CommandRunner.Listener
newData.copyScoresFrom(data); newData.copyScoresFrom(data);
newData.copyCheckmarksFrom(data); newData.copyCheckmarksFrom(data);
// sleep(1000);
commit(); commit();
if (!refreshScoresAndCheckmarks) return; if (!refreshScoresAndCheckmarks) return;
@ -274,10 +281,23 @@ public class HabitCardListCache implements CommandRunner.Listener
newData.checkmarks.put(id, newData.checkmarks.put(id,
h.getCheckmarks().getValues(dateFrom, dateTo)); h.getCheckmarks().getValues(dateFrom, dateTo));
// sleep(1000);
publishProgress(current++, newData.habits.size()); publishProgress(current++, newData.habits.size());
} }
} }
private void sleep(int time)
{
try
{
Thread.sleep(time);
}
catch (InterruptedException e)
{
e.printStackTrace();
}
}
@Override @Override
protected void onPostExecute(Void aVoid) protected void onPostExecute(Void aVoid)
{ {

@ -40,6 +40,14 @@ public class ShowHabitActivity extends BaseActivity
@Inject @Inject
HabitList habitList; HabitList habitList;
private ShowHabitController controller;
private ShowHabitRootView rootView;
private ShowHabitScreen screen;
private ShowHabitsMenu menu;
@Override @Override
protected void onCreate(Bundle savedInstanceState) protected void onCreate(Bundle savedInstanceState)
{ {
@ -47,15 +55,15 @@ public class ShowHabitActivity extends BaseActivity
HabitsApplication.getComponent().inject(this); HabitsApplication.getComponent().inject(this);
Habit habit = getHabitFromIntent(); Habit habit = getHabitFromIntent();
ShowHabitScreen screen = new ShowHabitScreen(this, habit); rootView = new ShowHabitRootView(this, habit);
ShowHabitRootView view = new ShowHabitRootView(this, habit); screen = new ShowHabitScreen(this, habit, rootView);
screen.setRootView(view); setScreen(screen);
this.setScreen(screen);
ShowHabitsMenu menu = new ShowHabitsMenu(this, screen); menu = new ShowHabitsMenu(this, screen);
ShowHabitController controller = new ShowHabitController(screen, habit);
screen.setMenu(menu); screen.setMenu(menu);
view.setController(controller);
controller = new ShowHabitController(screen, habit);
rootView.setController(controller);
} }
@NonNull @NonNull

@ -31,10 +31,13 @@ public class ShowHabitScreen extends BaseScreen
@NonNull @NonNull
private final Habit habit; private final Habit habit;
public ShowHabitScreen(@NonNull BaseActivity activity, @NonNull Habit habit) public ShowHabitScreen(@NonNull BaseActivity activity,
@NonNull Habit habit,
ShowHabitRootView view)
{ {
super(activity); super(activity);
this.habit = habit; this.habit = habit;
setRootView(view);
} }
public void showEditHabitDialog() public void showEditHabitDialog()

@ -58,7 +58,6 @@ public class HistoryCard extends HabitCard
@OnClick(R.id.edit) @OnClick(R.id.edit)
public void onClickEditButton() public void onClickEditButton()
{ {
Log.d("HistoryCard", "onClickEditButton");
controller.onEditHistoryButtonClick(); controller.onEditHistoryButtonClick();
} }

@ -107,7 +107,7 @@ public class ScoreCard extends HabitCard
{ {
spinner.setVisibility(GONE); spinner.setVisibility(GONE);
title.setTextColor(ColorUtils.getAndroidTestColor(1)); title.setTextColor(ColorUtils.getAndroidTestColor(1));
chart.setPrimaryColor(ColorUtils.getAndroidTestColor(1)); chart.setColor(ColorUtils.getAndroidTestColor(1));
chart.populateWithRandomData(); chart.populateWithRandomData();
} }
} }
@ -141,7 +141,7 @@ public class ScoreCard extends HabitCard
int color = int color =
ColorUtils.getColor(getContext(), getHabit().getColor()); ColorUtils.getColor(getContext(), getHabit().getColor());
title.setTextColor(color); title.setTextColor(color);
chart.setPrimaryColor(color); chart.setColor(color);
} }
} }
} }

@ -0,0 +1,196 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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());
}
}

@ -0,0 +1,96 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

@ -17,38 +17,53 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.widgets; package org.isoron.uhabits.ui.widgets;
import android.app.Activity; import android.app.*;
import android.appwidget.AppWidgetManager; import android.content.*;
import android.content.Intent; import android.os.*;
import android.content.SharedPreferences; import android.view.*;
import android.os.Bundle; import android.widget.*;
import android.preference.PreferenceManager;
import android.view.View; import org.isoron.uhabits.*;
import android.widget.AdapterView; import org.isoron.uhabits.models.*;
import android.widget.ArrayAdapter; import org.isoron.uhabits.utils.*;
import android.widget.ListView;
import java.util.*;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R; import javax.inject.*;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.models.HabitList; import static android.appwidget.AppWidgetManager.*;
import java.util.ArrayList; public class HabitPickerDialog extends Activity
import java.util.List; implements AdapterView.OnItemClickListener
import javax.inject.Inject;
public class HabitPickerDialog extends Activity implements AdapterView.OnItemClickListener
{ {
@Inject @Inject
HabitList habitList; HabitList habitList;
@Inject
WidgetPreferences preferences;
private Integer widgetId; private Integer widgetId;
private ArrayList<Long> habitIds; private ArrayList<Long> 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 @Override
protected void onCreate(Bundle savedInstanceState) protected void onCreate(Bundle savedInstanceState)
{ {
@ -59,8 +74,8 @@ public class HabitPickerDialog extends Activity implements AdapterView.OnItemCli
Intent intent = getIntent(); Intent intent = getIntent();
Bundle extras = intent.getExtras(); Bundle extras = intent.getExtras();
if (extras != null) widgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, if (extras != null)
AppWidgetManager.INVALID_APPWIDGET_ID); widgetId = extras.getInt(EXTRA_APPWIDGET_ID, INVALID_APPWIDGET_ID);
ListView listView = (ListView) findViewById(R.id.listView); ListView listView = (ListView) findViewById(R.id.listView);
@ -74,29 +89,11 @@ public class HabitPickerDialog extends Activity implements AdapterView.OnItemCli
habitNames.add(h.getName()); habitNames.add(h.getName());
} }
ArrayAdapter<String> adapter = new ArrayAdapter<>(this, android.R.layout.simple_list_item_1, ArrayAdapter<String> adapter =
new ArrayAdapter<>(this, android.R.layout.simple_list_item_1,
habitNames); habitNames);
listView.setAdapter(adapter); listView.setAdapter(adapter);
listView.setOnItemClickListener(this); 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();
}
} }

@ -0,0 +1,62 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

@ -0,0 +1,98 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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.
* <p>
* 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);
}
}

@ -17,26 +17,19 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.widgets.views; package org.isoron.uhabits.ui.widgets.views;
import android.content.Context; import android.content.*;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.support.annotation.Nullable; import android.util.*;
import android.util.AttributeSet; import android.widget.*;
import android.util.TypedValue;
import android.widget.TextView; import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.R; import org.isoron.uhabits.ui.common.views.*;
import org.isoron.uhabits.models.Checkmark; import org.isoron.uhabits.utils.*;
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;
public class CheckmarkWidgetView extends HabitWidgetView public class CheckmarkWidgetView extends HabitWidgetView
implements HabitChart
{ {
private int activeColor; private int activeColor;
@ -94,6 +87,10 @@ public class CheckmarkWidgetView extends HabitWidgetView
R.attr.cardBackgroundColor); R.attr.cardBackgroundColor);
foregroundColor = InterfaceUtils.getStyledColor(context, foregroundColor = InterfaceUtils.getStyledColor(context,
R.attr.mediumContrastTextColor); R.attr.mediumContrastTextColor);
setShadowAlpha(0x00);
rebuildBackground();
break; break;
case Checkmark.UNCHECKED: case Checkmark.UNCHECKED:
@ -103,6 +100,10 @@ public class CheckmarkWidgetView extends HabitWidgetView
R.attr.cardBackgroundColor); R.attr.cardBackgroundColor);
foregroundColor = InterfaceUtils.getStyledColor(context, foregroundColor = InterfaceUtils.getStyledColor(context,
R.attr.mediumContrastTextColor); R.attr.mediumContrastTextColor);
setShadowAlpha(0x00);
rebuildBackground();
break; break;
} }
@ -118,47 +119,31 @@ public class CheckmarkWidgetView extends HabitWidgetView
postInvalidate(); postInvalidate();
} }
@Override public void setCheckmarkValue(int checkmarkValue)
public void refreshData()
{ {
if (habit == null) return; this.checkmarkValue = checkmarkValue;
this.percentage =
(float) habit.getScores().getTodayValue() / Score.MAX_VALUE;
this.checkmarkValue = habit.getCheckmarks().getTodayValue();
refresh();
} }
@Override public void setName(@NonNull String name)
public void setHabit(@NonNull Habit habit)
{ {
super.setHabit(habit); this.name = name;
this.name = habit.getName();
this.activeColor = ColorUtils.getColor(getContext(), habit.getColor());
refresh();
} }
@Override public void setPercentage(float percentage)
@NonNull
protected Integer getInnerLayoutId()
{ {
return R.layout.widget_checkmark; this.percentage = percentage;
} }
private void init() public void setActiveColor(int activeColor)
{ {
ring = (RingView) findViewById(R.id.scoreRing); this.activeColor = activeColor;
label = (TextView) findViewById(R.id.label); }
if (ring != null) ring.setIsTransparencyEnabled(true);
if (isInEditMode()) @Override
@NonNull
protected Integer getInnerLayoutId()
{ {
percentage = 0.75f; return R.layout.widget_checkmark;
name = "Wake up early";
activeColor = ColorUtils.getAndroidTestColor(6);
checkmarkValue = Checkmark.CHECKED_EXPLICITLY;
refresh();
}
} }
@Override @Override
@ -194,4 +179,21 @@ public class CheckmarkWidgetView extends HabitWidgetView
super.onMeasure(widthMeasureSpec, heightMeasureSpec); 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();
}
}
} }

@ -17,63 +17,57 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.widgets.views; package org.isoron.uhabits.ui.widgets.views;
import android.content.Context; import android.content.*;
import android.support.annotation.NonNull; import android.support.annotation.*;
import android.view.View; import android.view.*;
import android.view.ViewGroup; import android.widget.*;
import android.widget.TextView;
import org.isoron.uhabits.R; import org.isoron.uhabits.*;
import org.isoron.uhabits.models.Habit;
import org.isoron.uhabits.ui.common.views.HabitChart;
public class GraphWidgetView extends HabitWidgetView implements HabitChart public class GraphWidgetView extends HabitWidgetView
{ {
private final HabitChart dataView; private final View dataView;
private TextView title; private TextView title;
public GraphWidgetView(Context context, HabitChart dataView) public GraphWidgetView(Context context, View dataView)
{ {
super(context); super(context);
this.dataView = dataView; this.dataView = dataView;
init(); init();
} }
private void init() public View getDataView()
{ {
ViewGroup.LayoutParams params = return dataView;
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);
} }
@Override public void setTitle(String text)
public void setHabit(@NonNull Habit habit)
{ {
super.setHabit(habit); title.setText(text);
dataView.setHabit(habit);
title.setText(habit.getName());
} }
@Override @Override
public void refreshData()
{
if(habit == null) return;
dataView.refreshData();
}
@NonNull @NonNull
protected Integer getInnerLayoutId() protected Integer getInnerLayoutId()
{ {
return R.layout.widget_graph; 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);
}
} }

@ -17,7 +17,7 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package org.isoron.uhabits.widgets.views; package org.isoron.uhabits.ui.widgets.views;
import android.content.*; import android.content.*;
import android.graphics.*; import android.graphics.*;
@ -29,14 +29,11 @@ import android.view.*;
import android.widget.*; import android.widget.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.ui.common.views.*;
import org.isoron.uhabits.utils.*; import org.isoron.uhabits.utils.*;
import java.util.*; import java.util.*;
public abstract class HabitWidgetView extends FrameLayout public abstract class HabitWidgetView extends FrameLayout
implements HabitChart
{ {
@Nullable @Nullable
protected InsetDrawable background; protected InsetDrawable background;
@ -44,9 +41,6 @@ public abstract class HabitWidgetView extends FrameLayout
@Nullable @Nullable
protected Paint backgroundPaint; protected Paint backgroundPaint;
@Nullable
protected Habit habit;
protected ViewGroup frame; protected ViewGroup frame;
private int shadowAlpha; private int shadowAlpha;
@ -63,12 +57,6 @@ public abstract class HabitWidgetView extends FrameLayout
init(); init();
} }
@Override
public void setHabit(@NonNull Habit habit)
{
this.habit = habit;
}
public void setShadowAlpha(int shadowAlpha) public void setShadowAlpha(int shadowAlpha)
{ {
this.shadowAlpha = shadowAlpha; this.shadowAlpha = shadowAlpha;

@ -20,4 +20,4 @@
/** /**
* Provides views that are specific for the home-screen widgets. * Provides views that are specific for the home-screen widgets.
*/ */
package org.isoron.uhabits.widgets.views; package org.isoron.uhabits.ui.widgets.views;

@ -19,13 +19,10 @@
package org.isoron.uhabits.utils; package org.isoron.uhabits.utils;
import android.content.Context; import android.content.*;
import android.content.SharedPreferences; import android.preference.*;
import android.preference.PreferenceManager;
import org.isoron.uhabits.BuildConfig; import org.isoron.uhabits.*;
import org.isoron.uhabits.HabitsApplication;
import org.isoron.uhabits.R;
public class Preferences public class Preferences
{ {
@ -132,6 +129,4 @@ public class Preferences
.putLong("last_hint_timestamp", timestamp) .putLong("last_hint_timestamp", timestamp)
.apply(); .apply();
} }
} }

@ -0,0 +1,65 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

@ -0,0 +1,70 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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);
}
}
}

@ -19,308 +19,131 @@
package org.isoron.uhabits.widgets; package org.isoron.uhabits.widgets;
import android.app.PendingIntent; import android.appwidget.*;
import android.appwidget.AppWidgetManager; import android.content.*;
import android.appwidget.AppWidgetProvider; import android.os.*;
import android.content.Context; import android.support.annotation.*;
import android.content.SharedPreferences; import android.widget.*;
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 org.isoron.uhabits.HabitsApplication; import org.isoron.uhabits.*;
import org.isoron.uhabits.R; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.models.Habit; import org.isoron.uhabits.ui.widgets.*;
import org.isoron.uhabits.models.HabitList; import org.isoron.uhabits.utils.*;
import org.isoron.uhabits.tasks.BaseTask;
import org.isoron.uhabits.utils.InterfaceUtils;
import java.io.FileOutputStream; import javax.inject.*;
import java.io.IOException;
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 public abstract class BaseWidgetProvider extends AppWidgetProvider
{ {
@Inject @Inject
HabitList habitList; HabitList habitList;
private class WidgetDimensions @Inject
{ WidgetPreferences widgetPrefs;
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();
}
@Override
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager,
int appWidgetId, Bundle newOptions)
{
updateWidget(context, appWidgetManager, appWidgetId, newOptions);
}
@Override
public void onUpdate(Context context, AppWidgetManager manager, int[] appWidgetIds)
{
for(int id : appWidgetIds)
{
Bundle options = null;
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN)
options = manager.getAppWidgetOptions(id);
updateWidget(context, manager, id, options);
}
}
private void updateWidget(Context context, AppWidgetManager manager, public BaseWidgetProvider()
int widgetId, Bundle options)
{ {
HabitsApplication.getComponent().inject(this); 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)
{
drawErrorWidget(context, manager, widgetId);
return;
}
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); @Override
public void onAppWidgetOptionsChanged(@Nullable Context context,
private void savePreview(Context context, int widgetId, Bitmap widgetCache, int width, @Nullable AppWidgetManager manager,
int height, String label) int widgetId,
@Nullable Bundle options)
{ {
try try
{ {
LayoutInflater inflater = LayoutInflater.from(context); if (context == null) throw new RuntimeException("context is null");
View view = inflater.inflate(getLayoutId(), null); if (manager == null) throw new RuntimeException("manager is null");
if (options == null) throw new RuntimeException("options is null");
TextView tvLabel = (TextView) view.findViewById(R.id.label); context.setTheme(R.style.TransparentWidgetTheme);
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(previewCache != null)
previewCache.compress(Bitmap.CompressFormat.PNG, 100, out);
out.close(); BaseWidget widget = getWidgetFromId(context, widgetId);
WidgetDimensions dims = getDimensionsFromOptions(context, options);
widget.setDimensions(dims);
updateAppWidget(manager, widget);
} }
catch (IOException e) catch (RuntimeException e)
{ {
drawErrorWidget(context, manager, widgetId);
e.printStackTrace(); e.printStackTrace();
} }
} }
private WidgetDimensions getWidgetDimensions(Context context, Bundle options) @Override
public void onDeleted(@Nullable Context context, @Nullable int[] ids)
{ {
int maxWidth = getDefaultWidth(); if (context == null) throw new RuntimeException("context is null");
int minWidth = getDefaultWidth(); if (ids == null) throw new RuntimeException("ids is null");
int maxHeight = getDefaultHeight();
int minHeight = getDefaultHeight();
if (options != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) for (int id : ids)
{ {
maxWidth = (int) InterfaceUtils.dpToPixels(context, BaseWidget widget = getWidgetFromId(context, id);
options.getInt(AppWidgetManager.OPTION_APPWIDGET_MAX_WIDTH)); widget.delete();
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;
}
private void measureCustomView(Context context, int w, int h, View customView)
{
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());
View imageView = entireView.findViewById(R.id.imageView);
w = imageView.getMeasuredWidth();
h = imageView.getMeasuredHeight();
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 class RenderWidgetTask extends BaseTask
{
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 @Override
protected void onPreExecute() public void onUpdate(@Nullable Context context,
@Nullable AppWidgetManager manager,
@Nullable int[] widgetIds)
{ {
super.onPreExecute(); 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); context.setTheme(R.style.TransparentWidgetTheme);
portraitRemoteViews = new RemoteViews(context.getPackageName(), getLayoutId()); for (int id : widgetIds)
portraitWidgetView = buildCustomView(context, habit); update(context, manager, id);
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() @NonNull
protected Habit getHabitFromWidgetId(int widgetId)
{ {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) long habitId = widgetPrefs.getHabitIdFromWidgetId(widgetId);
manager.updateAppWidget(widgetId, new RemoteViews(landscapeRemoteViews, Habit habit = habitList.getById(habitId);
portraitRemoteViews)); if (habit == null) throw new RuntimeException("habit not found");
else return habit;
manager.updateAppWidget(widgetId, portraitRemoteViews);
} }
@Override @NonNull
protected void doInBackground() protected abstract BaseWidget getWidgetFromId(@NonNull Context context,
int id);
private void drawErrorWidget(Context context,
AppWidgetManager manager,
int widgetId)
{ {
refreshCustomViewData(portraitWidgetView); RemoteViews errorView =
refreshCustomViewData(landscapeWidgetView); new RemoteViews(context.getPackageName(), R.layout.widget_error);
manager.updateAppWidget(widgetId, errorView);
} }
@Override private void update(@NonNull Context context,
protected void onPostExecute(Void aVoid) @NonNull AppWidgetManager manager,
int widgetId)
{ {
try try
{ {
buildRemoteViews(portraitWidgetView, portraitRemoteViews, BaseWidget widget = getWidgetFromId(context, widgetId);
dim.portraitWidth, dim.portraitHeight);
buildRemoteViews(landscapeWidgetView, landscapeRemoteViews,
dim.landscapeWidth, dim.landscapeHeight);
updateAppWidget();
}
catch (Exception e)
{
drawErrorWidget(context, manager, widgetId);
e.printStackTrace();
}
super.onPostExecute(aVoid); if (SDK_INT > JELLY_BEAN)
}
private void buildRemoteViews(View widgetView, RemoteViews remoteViews, int width,
int height)
{
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(); Bundle options = manager.getAppWidgetOptions(widgetId);
int imageHeight = widgetView.getMeasuredHeight(); widget.setDimensions(
int p[] = getPadding(width, height, imageWidth, imageHeight); getDimensionsFromOptions(context, options));
remoteViews.setViewPadding(R.id.buttonOverlay, p[0], p[1], p[2], p[3]);
} }
//savePreview(context, widgetId, drawingCache, width, height, habit.name); updateAppWidget(manager, widget);
PendingIntent onClickIntent = getOnClickPendingIntent(context, habit);
if (onClickIntent != null) remoteViews.setOnClickPendingIntent(R.id.button,
onClickIntent);
}
} }
catch (RuntimeException e)
private int[] getPadding(int entireWidth, int entireHeight, int imageWidth,
int imageHeight)
{ {
int w = (int) (((float) entireWidth - imageWidth) / 2); drawErrorWidget(context, manager, widgetId);
int h = (int) (((float) entireHeight - imageHeight) / 2); e.printStackTrace();
}
return new int[]{ w, h, w, h };
} }
} }

@ -18,55 +18,19 @@
*/ */
package org.isoron.uhabits.widgets; package org.isoron.uhabits.widgets;
import android.app.*;
import android.content.*; import android.content.*;
import android.view.*; import android.support.annotation.*;
import org.isoron.uhabits.*;
import org.isoron.uhabits.models.*; import org.isoron.uhabits.models.*;
import org.isoron.uhabits.ui.common.views.*; import org.isoron.uhabits.ui.widgets.*;
import org.isoron.uhabits.widgets.views.*;
public class CheckmarkWidgetProvider extends BaseWidgetProvider public class CheckmarkWidgetProvider extends BaseWidgetProvider
{ {
@NonNull
@Override @Override
protected View buildCustomView(Context context, Habit habit) protected CheckmarkWidget getWidgetFromId(@NonNull Context context, int id)
{ {
CheckmarkWidgetView view = new CheckmarkWidgetView(context); Habit habit = getHabitFromWidgetId(id);
view.setHabit(habit); return new CheckmarkWidget(context, id, habit);
return view;
} }
@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();
}
} }

@ -19,54 +19,73 @@
package org.isoron.uhabits.widgets; package org.isoron.uhabits.widgets;
import android.app.*;
import android.content.*; import android.content.*;
import android.view.*; import android.support.annotation.*;
import org.apache.commons.lang3.*; import org.apache.commons.lang3.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.ui.widgets.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.ui.common.views.*;
public class FrequencyWidgetProvider extends BaseWidgetProvider public class FrequencyWidgetProvider extends BaseWidgetProvider
{ {
@NonNull
@Override @Override
protected View buildCustomView(Context context, Habit habit) protected BaseWidget getWidgetFromId(@NonNull Context context, int id)
{ {
FrequencyChart dataView = new FrequencyChart(context);
throw new NotImplementedException(""); throw new NotImplementedException("");
// GraphWidgetView view = new GraphWidgetView(context, dataView);
// view.setHabit(habit);
// return view;
}
@Override
protected void refreshCustomViewData(View view)
{
((HabitChart) view).refreshData();
} }
@Override // @NonNull
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) // @Override
{ // protected BaseWidget getWidgetFromId(int id)
return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); // {
} // throw new NotImplementedException("");
// }
@Override //
protected int getDefaultHeight() // @Override
{ // protected View buildCustomView(Context context, Habit habit)
return 200; // {
} // FrequencyChart chart = new FrequencyChart(context);
// GraphWidgetView view = new GraphWidgetView(context, chart);
@Override // view.setTitle(habit.getName());
protected int getDefaultWidth() // return view;
{ // }
return 200; //
} // @Override
// protected int getDefaultHeight()
@Override // {
protected int getLayoutId() // return 200;
{ // }
return R.layout.widget_wrapper; //
} // @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());
// }
} }

@ -18,54 +18,74 @@
*/ */
package org.isoron.uhabits.widgets; package org.isoron.uhabits.widgets;
import android.app.*;
import android.content.*; import android.content.*;
import android.view.*; import android.support.annotation.*;
import org.apache.commons.lang3.*; import org.apache.commons.lang3.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.ui.widgets.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.ui.common.views.*;
public class HistoryWidgetProvider extends BaseWidgetProvider public class HistoryWidgetProvider extends BaseWidgetProvider
{ {
@NonNull
@Override @Override
protected View buildCustomView(Context context, Habit habit) protected BaseWidget getWidgetFromId(@NonNull Context context, int id)
{ {
throw new NotImplementedException(""); 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 // @NonNull
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) // @Override
{ // protected BaseWidget getWidgetFromId(int id)
return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); // {
} // throw new NotImplementedException("");
// }
@Override //
protected int getDefaultHeight() // @Override
{ // protected View buildCustomView(Context context, Habit habit)
return 250; // {
} // HistoryChart dataView = new HistoryChart(context);
// GraphWidgetView widgetView = new GraphWidgetView(context, dataView);
@Override // widgetView.setTitle(habit.getName());
protected int getDefaultWidth() // return widgetView;
{ // }
return 250; //
} // @Override
// protected int getDefaultHeight()
@Override // {
protected int getLayoutId() // return 250;
{ // }
return R.layout.widget_wrapper; //
} // @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);
// }
} }

@ -18,63 +18,73 @@
*/ */
package org.isoron.uhabits.widgets; package org.isoron.uhabits.widgets;
import android.app.*;
import android.content.*; import android.content.*;
import android.view.*; import android.support.annotation.*;
import org.apache.commons.lang3.*; import org.apache.commons.lang3.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.ui.widgets.*;
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.*;
public class ScoreWidgetProvider extends BaseWidgetProvider public class ScoreWidgetProvider extends BaseWidgetProvider
{ {
@NonNull
@Override @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(""); throw new NotImplementedException("");
} }
@Override // @Override
protected void refreshCustomViewData(View view) // protected View buildCustomView(Context context, Habit habit)
{ // {
((HabitChart) view).refreshData(); // ScoreChart dataView = new ScoreChart(context);
} // GraphWidgetView view = new GraphWidgetView(context, dataView);
// view.setTitle(habit.getName());
@Override // return view;
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) // }
{ //
return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); // @Override
} // protected int getDefaultHeight()
// {
@Override // return 300;
protected int getDefaultHeight() // }
{ //
return 300; // @Override
} // protected int getDefaultWidth()
// {
@Override // return 300;
protected int getDefaultWidth() // }
{ //
return 300; // @Override
} // protected int getLayoutId()
// {
@Override // return R.layout.widget_wrapper;
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<Score> scores = habit.getScores().getAll();
//
// chart.setIsTransparencyEnabled(true);
// chart.setBucketSize(size);
// chart.setColor(color);
// chart.setScores(scores);
// }
} }

@ -18,54 +18,69 @@
*/ */
package org.isoron.uhabits.widgets; package org.isoron.uhabits.widgets;
import android.app.*;
import android.content.*; import android.content.*;
import android.view.*; import android.support.annotation.*;
import org.apache.commons.lang3.*; import org.apache.commons.lang3.*;
import org.isoron.uhabits.*; import org.isoron.uhabits.ui.widgets.*;
import org.isoron.uhabits.models.*;
import org.isoron.uhabits.ui.common.views.*;
public class StreakWidgetProvider extends BaseWidgetProvider public class StreakWidgetProvider extends BaseWidgetProvider
{ {
@NonNull
@Override @Override
protected View buildCustomView(Context context, Habit habit) protected BaseWidget getWidgetFromId(@NonNull Context context, int id)
{ {
StreakChart dataView = new StreakChart(context);
throw new NotImplementedException(""); throw new NotImplementedException("");
// GraphWidgetView view = new GraphWidgetView(context, dataView);
// view.setHabit(habit);
// return view;
}
@Override
protected void refreshCustomViewData(View view)
{
((HabitChart) view).refreshData();
} }
@Override // @Override
protected PendingIntent getOnClickPendingIntent(Context context, Habit habit) // protected View buildCustomView(Context context, Habit habit)
{ // {
return HabitBroadcastReceiver.buildViewHabitIntent(context, habit); // StreakChart dataView = new StreakChart(context);
} // GraphWidgetView view = new GraphWidgetView(context, dataView);
// view.setTitle(habit.getName());
@Override // return view;
protected int getDefaultHeight() // }
{ //
return 200; // @Override
} // protected int getDefaultHeight()
// {
@Override // return 200;
protected int getDefaultWidth() // }
{ //
return 200; // @Override
} // protected int getDefaultWidth()
// {
@Override // return 200;
protected int getLayoutId() // }
{ //
return R.layout.widget_wrapper; // @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<Streak> streaks = habit.getStreaks().getBest(10);
//
// chart.setColor(color);
// chart.setStreaks(streaks);
// }
} }

@ -1,47 +0,0 @@
/*
* Copyright (C) 2016 Álinson Santos Xavier <isoron@gmail.com>
*
* 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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

@ -25,7 +25,7 @@
android:previewImage="@drawable/widget_preview_checkmark" android:previewImage="@drawable/widget_preview_checkmark"
android:resizeMode="none" android:resizeMode="none"
android:updatePeriodMillis="3600000" android:updatePeriodMillis="3600000"
android:configure="org.isoron.uhabits.widgets.HabitPickerDialog" android:configure="org.isoron.uhabits.ui.widgets.HabitPickerDialog"
android:widgetCategory="home_screen"> android:widgetCategory="home_screen">

@ -27,7 +27,7 @@
android:previewImage="@drawable/widget_preview_frequency" android:previewImage="@drawable/widget_preview_frequency"
android:resizeMode="vertical|horizontal" android:resizeMode="vertical|horizontal"
android:updatePeriodMillis="3600000" android:updatePeriodMillis="3600000"
android:configure="org.isoron.uhabits.widgets.HabitPickerDialog" android:configure="org.isoron.uhabits.ui.widgets.HabitPickerDialog"
android:widgetCategory="home_screen"> android:widgetCategory="home_screen">
</appwidget-provider> </appwidget-provider>

@ -27,7 +27,7 @@
android:previewImage="@drawable/widget_preview_history" android:previewImage="@drawable/widget_preview_history"
android:resizeMode="vertical|horizontal" android:resizeMode="vertical|horizontal"
android:updatePeriodMillis="3600000" android:updatePeriodMillis="3600000"
android:configure="org.isoron.uhabits.widgets.HabitPickerDialog" android:configure="org.isoron.uhabits.ui.widgets.HabitPickerDialog"
android:widgetCategory="home_screen"> android:widgetCategory="home_screen">
</appwidget-provider> </appwidget-provider>

@ -27,7 +27,7 @@
android:previewImage="@drawable/widget_preview_score" android:previewImage="@drawable/widget_preview_score"
android:resizeMode="vertical|horizontal" android:resizeMode="vertical|horizontal"
android:updatePeriodMillis="3600000" android:updatePeriodMillis="3600000"
android:configure="org.isoron.uhabits.widgets.HabitPickerDialog" android:configure="org.isoron.uhabits.ui.widgets.HabitPickerDialog"
android:widgetCategory="home_screen"> android:widgetCategory="home_screen">
</appwidget-provider> </appwidget-provider>

@ -27,7 +27,7 @@
android:previewImage="@drawable/widget_preview_streaks" android:previewImage="@drawable/widget_preview_streaks"
android:resizeMode="vertical|horizontal" android:resizeMode="vertical|horizontal"
android:updatePeriodMillis="3600000" android:updatePeriodMillis="3600000"
android:configure="org.isoron.uhabits.widgets.HabitPickerDialog" android:configure="org.isoron.uhabits.ui.widgets.HabitPickerDialog"
android:widgetCategory="home_screen"> android:widgetCategory="home_screen">
</appwidget-provider> </appwidget-provider>

@ -35,16 +35,15 @@ public class TestModule
{ {
@Singleton @Singleton
@Provides @Provides
Preferences providePreferences() CommandRunner provideCommandRunner()
{ {
return mock(Preferences.class); return mock(CommandRunner.class);
} }
@Singleton
@Provides @Provides
CommandRunner provideCommandRunner() Habit provideHabit()
{ {
return mock(CommandRunner.class); return mock(Habit.class);
} }
@Singleton @Singleton
@ -55,15 +54,23 @@ public class TestModule
} }
@Provides @Provides
Habit provideHabit() @Singleton
ModelFactory provideModelFactory()
{ {
return mock(Habit.class); return new MemoryModelFactory();
}
@Singleton
@Provides
Preferences providePreferences()
{
return mock(Preferences.class);
} }
@Provides @Provides
@Singleton @Singleton
ModelFactory provideModelFactory() WidgetPreferences provideWidgetPreferences()
{ {
return new MemoryModelFactory(); return mock(WidgetPreferences.class);
} }
} }

Loading…
Cancel
Save