diff --git a/app/build.gradle b/app/build.gradle index 9d688f8dd..83426d580 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -59,6 +59,7 @@ dependencies { compile 'com.opencsv:opencsv:3.7' compile 'com.michaelpardo:activeandroid:3.1.0-SNAPSHOT' compile 'org.jetbrains:annotations-java5:15.0' + compile 'com.getpebble:pebblekit:3.0.0' compile 'com.jakewharton:butterknife:8.0.1' apt 'com.jakewharton:butterknife-compiler:8.0.1' diff --git a/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java b/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java index 585d4f49a..d3eb68490 100644 --- a/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java +++ b/app/src/androidTest/java/org/isoron/uhabits/BaseAndroidTest.java @@ -132,4 +132,9 @@ public class BaseAndroidTest BaseTask.waitForTasks(10000); } + + protected void awaitLatch() throws InterruptedException + { + assertTrue(latch.await(5, TimeUnit.SECONDS)); + } } diff --git a/app/src/androidTest/java/org/isoron/uhabits/pebble/PebbleReceiverTest.java b/app/src/androidTest/java/org/isoron/uhabits/pebble/PebbleReceiverTest.java new file mode 100644 index 000000000..0a97dcf63 --- /dev/null +++ b/app/src/androidTest/java/org/isoron/uhabits/pebble/PebbleReceiverTest.java @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.pebble; + +import android.content.*; +import android.support.annotation.*; +import android.support.test.runner.*; +import android.test.suitebuilder.annotation.*; + +import com.getpebble.android.kit.*; +import com.getpebble.android.kit.util.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.models.*; +import org.json.*; +import org.junit.*; +import org.junit.runner.*; + +import static com.getpebble.android.kit.Constants.*; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.core.IsEqual.*; + +@RunWith(AndroidJUnit4.class) +@MediumTest +public class PebbleReceiverTest extends BaseAndroidTest +{ + private BroadcastReceiver pebbleReceiver; + + private Habit habit1; + + private Habit habit2; + + @Override + public void setUp() + { + super.setUp(); + + fixtures.purgeHabits(habitList); + + habit1 = fixtures.createLongHabit(); + habit1.setName("Exercise"); + + habit2 = fixtures.createEmptyHabit(); + habit2.setName("Meditate"); + } + + @Test + public void testCount() throws Exception + { + onPebbleReceived((dict) -> { + assertThat(dict.getString(0), equalTo("COUNT")); + assertThat(dict.getInteger(1), equalTo(2L)); + }); + + PebbleDictionary dict = buildCountRequest(); + sendFromPebbleToAndroid(dict); + awaitLatch(); + } + + @Test + public void testFetch() throws Exception + { + onPebbleReceived((dict) -> { + assertThat(dict.getString(0), equalTo("HABIT")); + assertThat(dict.getInteger(1), equalTo(habit2.getId())); + assertThat(dict.getString(2), equalTo(habit2.getName())); + assertThat(dict.getInteger(3), equalTo(0L)); + }); + + PebbleDictionary dict = buildFetchRequest(1); + sendFromPebbleToAndroid(dict); + awaitLatch(); + } + + @Test + public void testToggle() throws Exception + { + onPebbleReceived((dict) -> { + assertThat(dict.getString(0), equalTo("OK")); + int value = habit1.getCheckmarks().getTodayValue(); + assertThat(value, equalTo(Checkmark.CHECKED_EXPLICITLY)); + }); + + PebbleDictionary dict = buildToggleRequest(habit1.getId()); + sendFromPebbleToAndroid(dict); + awaitLatch(); + } + + @NonNull + protected PebbleDictionary buildCountRequest() + { + PebbleDictionary dict = new PebbleDictionary(); + dict.addString(0, "COUNT"); + return dict; + } + + @NonNull + protected PebbleDictionary buildFetchRequest(int position) + { + PebbleDictionary dict = new PebbleDictionary(); + dict.addString(0, "FETCH"); + dict.addInt32(1, position); + return dict; + } + + protected void onPebbleReceived(PebbleProcessor processor) + { + pebbleReceiver = new BroadcastReceiver() + { + @Override + public void onReceive(Context context, Intent intent) + { + try + { + String jsonData = intent.getStringExtra(MSG_DATA); + PebbleDictionary dict = PebbleDictionary.fromJson(jsonData); + processor.process(dict); + latch.countDown(); + targetContext.unregisterReceiver(this); + } + catch (JSONException e) + { + throw new RuntimeException(e); + } + } + }; + + IntentFilter filter = new IntentFilter(Constants.INTENT_APP_SEND); + targetContext.registerReceiver(pebbleReceiver, filter); + } + + protected void sendFromPebbleToAndroid(PebbleDictionary dict) + { + Intent intent = new Intent(Constants.INTENT_APP_RECEIVE); + intent.putExtra(Constants.APP_UUID, PebbleReceiver.WATCHAPP_UUID); + intent.putExtra(Constants.TRANSACTION_ID, 0); + intent.putExtra(Constants.MSG_DATA, dict.toJsonString()); + targetContext.sendBroadcast(intent); + } + + private PebbleDictionary buildToggleRequest(long habitId) + { + PebbleDictionary dict = new PebbleDictionary(); + dict.addString(0, "TOGGLE"); + dict.addInt32(1, (int) habitId); + return dict; + } + + interface PebbleProcessor + { + void process(PebbleDictionary dict); + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b2a07390a..df468bb5e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -162,6 +162,12 @@ + + + + + + diff --git a/app/src/main/java/org/isoron/uhabits/BaseComponent.java b/app/src/main/java/org/isoron/uhabits/BaseComponent.java index 86ca692ef..faf381e30 100644 --- a/app/src/main/java/org/isoron/uhabits/BaseComponent.java +++ b/app/src/main/java/org/isoron/uhabits/BaseComponent.java @@ -22,6 +22,7 @@ package org.isoron.uhabits; import org.isoron.uhabits.commands.*; import org.isoron.uhabits.io.*; import org.isoron.uhabits.models.*; +import org.isoron.uhabits.pebble.*; import org.isoron.uhabits.tasks.*; import org.isoron.uhabits.ui.*; import org.isoron.uhabits.ui.habits.edit.*; @@ -97,4 +98,6 @@ public interface BaseComponent void inject(WidgetUpdater widgetManager); void inject(ListHabitsMenu listHabitsMenu); + + void inject(PebbleReceiver receiver); } diff --git a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java index 778ef02b4..1648e22c2 100644 --- a/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java +++ b/app/src/main/java/org/isoron/uhabits/models/sqlite/SQLiteHabitList.java @@ -109,11 +109,7 @@ public class SQLiteHabitList extends HabitList @Nullable public Habit getByPosition(int position) { - String query = buildSelectQuery() + "limit 1 offset ?"; - String params[] = { Integer.toString(position) }; - HabitRecord record = sqlite.querySingle(query, params); - if (record != null) return getById(record.getId()); - return null; + return toList().get(position); } @NonNull diff --git a/app/src/main/java/org/isoron/uhabits/pebble/PebbleReceiver.java b/app/src/main/java/org/isoron/uhabits/pebble/PebbleReceiver.java new file mode 100644 index 000000000..9ab60c712 --- /dev/null +++ b/app/src/main/java/org/isoron/uhabits/pebble/PebbleReceiver.java @@ -0,0 +1,156 @@ +/* + * Copyright (C) 2016 Álinson Santos Xavier + * + * This file is part of Loop Habit Tracker. + * + * Loop Habit Tracker is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by the + * Free Software Foundation, either version 3 of the License, or (at your + * option) any later version. + * + * Loop Habit Tracker is distributed in the hope that it will be useful, but + * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +package org.isoron.uhabits.pebble; + +import android.content.*; +import android.support.annotation.*; +import android.util.*; + +import com.getpebble.android.kit.*; +import com.getpebble.android.kit.PebbleKit.*; +import com.getpebble.android.kit.util.*; + +import org.isoron.uhabits.*; +import org.isoron.uhabits.commands.*; +import org.isoron.uhabits.models.*; +import org.isoron.uhabits.tasks.*; +import org.isoron.uhabits.utils.*; + +import java.util.*; + +import javax.inject.*; + +public class PebbleReceiver extends PebbleDataReceiver +{ + public static final UUID WATCHAPP_UUID = + UUID.fromString("82629d99-8ea6-4631-a022-9ca77a12a058"); + + @Inject + protected HabitList allHabits; + + @Inject + protected CommandRunner runner; + + protected HabitList filteredHabits; + + public PebbleReceiver() + { + super(WATCHAPP_UUID); + HabitsApplication.getComponent().inject(this); + + HabitMatcher build = new HabitMatcherBuilder() + .setArchivedAllowed(false) + .setCompletedAllowed(false) + .build(); + + filteredHabits = allHabits.getFiltered(build); + } + + @Override + public void receiveData(@Nullable Context context, + int transactionId, + @Nullable PebbleDictionary data) + { + if (context == null) throw new RuntimeException("context is null"); + if (data == null) throw new RuntimeException("data is null"); + + PebbleKit.sendAckToPebble(context, transactionId); + Log.d("PebbleReceiver", "<-- " + data.getString(0)); + + new SimpleTask(() -> { + switch (data.getString(0)) + { + case "COUNT": + sendCount(); + break; + + case "FETCH": + processFetch(data); + break; + + case "TOGGLE": + processToggle(data); + break; + } + }).execute(); + } + + private void processFetch(@NonNull PebbleDictionary dict) + { + Long position = dict.getInteger(1); + if (position == null) return; + if (position < 0 || position >= filteredHabits.size()) return; + + Habit habit = filteredHabits.getByPosition(position.intValue()); + if (habit == null) return; + + sendHabit(habit); + } + + private void processToggle(@NonNull PebbleDictionary dict) + { + Long habitId = dict.getInteger(1); + if (habitId == null) return; + + Habit habit = allHabits.getById(habitId); + if (habit == null) return; + + long today = DateUtils.getStartOfToday(); + runner.execute(new ToggleRepetitionCommand(habit, today), null); + + sendOK(); + } + + private void sendCount() + { + PebbleDictionary dict = new PebbleDictionary(); + dict.addString(0, "COUNT"); + dict.addInt32(1, filteredHabits.size()); + sendDict(dict); + + Log.d("PebbleReceiver", + String.format("--> COUNT %d", filteredHabits.size())); + } + + private void sendDict(@NonNull PebbleDictionary dict) + { + PebbleKit.sendDataToPebble(HabitsApplication.getContext(), + PebbleReceiver.WATCHAPP_UUID, dict); + } + + private void sendHabit(@NonNull Habit habit) + { + if (habit.getId() == null) return; + + PebbleDictionary response = new PebbleDictionary(); + response.addString(0, "HABIT"); + response.addInt32(1, habit.getId().intValue()); + response.addString(2, habit.getName()); + response.addInt32(3, habit.getCheckmarks().getTodayValue()); + sendDict(response); + } + + private void sendOK() + { + PebbleDictionary dict = new PebbleDictionary(); + dict.addString(0, "OK"); + sendDict(dict); + } +}