From a052107bd71a49fa263cd2cec461200b4465057f Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 13 Feb 2017 16:17:05 +0100 Subject: [PATCH 01/91] add recipe cooler component: - singleton - persistence on disk of recipe log and latest recipe timestamp --- .../it/near/sdk/Recipes/RecipeCooler.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/main/java/it/near/sdk/Recipes/RecipeCooler.java diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java new file mode 100644 index 00000000..7de0c2cb --- /dev/null +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -0,0 +1,123 @@ +package it.near.sdk.Recipes; + +import android.content.Context; +import android.content.SharedPreferences; + +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import it.near.sdk.Recipes.Models.Recipe; + +/** + * Created by cattaneostefano on 13/02/2017. + */ + +public class RecipeCooler { + + private static RecipeCooler INSTANCE; + private Context mContext; + private SharedPreferences mSharedPreferences; + + private static final String NEAR_COOLDOWN_HISTORY = "NearCooldownHistory"; + private static final String LOG_MAP = "LOG_MAP"; + private static final String LATEST_LOG = "LATEST_LOG"; + private Map mRecipeLogMap; + private Long latestLogEntry; + + private RecipeCooler(Context context) { + mContext = context; + mRecipeLogMap = loadMap(); + latestLogEntry = loadLatestEntry(); + } + + private SharedPreferences getSharedPreferences(){ + if (mSharedPreferences == null){ + mSharedPreferences = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); + } + return mSharedPreferences; + } + + public static RecipeCooler getInstance(Context context){ + if (INSTANCE == null){ + INSTANCE = new RecipeCooler(context); + } + return INSTANCE; + } + + public void markRecipeAsShown(String recipeId){ + if (mRecipeLogMap == null) mRecipeLogMap = loadMap(); + long timeStamp = System.currentTimeMillis(); + mRecipeLogMap.put(recipeId, new Long(timeStamp)); + saveMap(mRecipeLogMap); + + } + + public boolean canShowRecipe(Recipe recipe){ + long timestamp = System.currentTimeMillis(); + long latestNotificationTimestamp = loadLatestEntry(); + return false; + } + + public void filterRecipe(List recipes){ + for (Iterator it = recipes.iterator(); it.hasNext();) { + Recipe recipe = it.next(); + if (!canShowRecipe(recipe)){ + it.remove(); + } + } + } + + private void saveMap(Map inputMap){ + SharedPreferences pSharedPref = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); + if (pSharedPref != null){ + JSONObject jsonObject = new JSONObject(inputMap); + String jsonString = jsonObject.toString(); + SharedPreferences.Editor editor = pSharedPref.edit(); + editor.remove(LOG_MAP).commit(); + editor.putString(LOG_MAP, jsonString); + editor.commit(); + } + } + + private Map loadMap(){ + Map outputMap = new HashMap(); + SharedPreferences pSharedPref = getSharedPreferences(); + try{ + if (pSharedPref != null){ + String jsonString = pSharedPref.getString(LOG_MAP, (new JSONObject()).toString()); + JSONObject jsonObject = new JSONObject(jsonString); + Iterator keysItr = jsonObject.keys(); + while(keysItr.hasNext()) { + String key = keysItr.next(); + Long value = (Long) jsonObject.get(key); + outputMap.put(key, value); + } + } + }catch(Exception e){ + e.printStackTrace(); + } + return outputMap; + } + + private Long loadLatestEntry() { + SharedPreferences pSharedPref = getSharedPreferences(); + if (pSharedPref != null) { + return pSharedPref.getLong(LATEST_LOG, 0L); + } + return 0L; + } + + private void saveLatestEntry() { + SharedPreferences pSharedPref = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); + if (pSharedPref!=null){ + SharedPreferences.Editor editor = pSharedPref.edit(); + editor.remove(LATEST_LOG).commit(); + editor.putLong(LATEST_LOG, System.currentTimeMillis()); + editor.commit(); + } + } +} From 7a47fddbc1ccb915e30b0a0d903e0c32bdb29930 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 13 Feb 2017 18:14:29 +0100 Subject: [PATCH 02/91] Add recipe cooler functionalities: - check for global and self cool down - lazy loading of cached recipe history --- .../it/near/sdk/Recipes/RecipeCooler.java | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index 7de0c2cb..45f8ba9d 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -9,6 +9,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import it.near.sdk.Recipes.Models.Recipe; @@ -25,13 +26,15 @@ public class RecipeCooler { private static final String NEAR_COOLDOWN_HISTORY = "NearCooldownHistory"; private static final String LOG_MAP = "LOG_MAP"; private static final String LATEST_LOG = "LATEST_LOG"; + + private static final String GLOBAL_COOLDOWN = "global_cooldown"; + private static final String SELF_COOLDOWN = "self_cooldown"; + private Map mRecipeLogMap; - private Long latestLogEntry; + private Long mLatestLogEntry; private RecipeCooler(Context context) { mContext = context; - mRecipeLogMap = loadMap(); - latestLogEntry = loadLatestEntry(); } private SharedPreferences getSharedPreferences(){ @@ -49,17 +52,33 @@ public static RecipeCooler getInstance(Context context){ } public void markRecipeAsShown(String recipeId){ - if (mRecipeLogMap == null) mRecipeLogMap = loadMap(); long timeStamp = System.currentTimeMillis(); - mRecipeLogMap.put(recipeId, new Long(timeStamp)); + getMap().put(recipeId, new Long(timeStamp)); saveMap(mRecipeLogMap); } - public boolean canShowRecipe(Recipe recipe){ - long timestamp = System.currentTimeMillis(); - long latestNotificationTimestamp = loadLatestEntry(); - return false; + private boolean canShowRecipe(Recipe recipe){ + Map cooldown = recipe.getCooldown(); + return cooldown != null && + globalCooldownCheck(cooldown) && + selfCooldownCheck(recipe, cooldown); + } + + private boolean globalCooldownCheck(Map cooldown) { + // TODO cosa fare se non c'è? + if (!cooldown.containsKey(GLOBAL_COOLDOWN)) return true; + long expiredSeconds = (System.currentTimeMillis() - getLatestLogEntry()) / 1000; + return expiredSeconds >= (Long)cooldown.get(GLOBAL_COOLDOWN); + } + + private boolean selfCooldownCheck(Recipe recipe, Map cooldown){ + // TODO cosa fare se non c'è? + if (!cooldown.containsKey(SELF_COOLDOWN)) return true; + if (!getMap().containsKey(recipe.getId())) return true; + long recipeLatestEntry = getMap().get(recipe.getId()); + long expiredSeconds = (System.currentTimeMillis() - recipeLatestEntry) / 1000; + return expiredSeconds >= (Long)cooldown.get(SELF_COOLDOWN); } public void filterRecipe(List recipes){ @@ -71,6 +90,13 @@ public void filterRecipe(List recipes){ } } + private Map getMap() { + if (mRecipeLogMap == null){ + mRecipeLogMap = loadMap(); + } + return mRecipeLogMap; + } + private void saveMap(Map inputMap){ SharedPreferences pSharedPref = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); if (pSharedPref != null){ @@ -103,6 +129,13 @@ private Map loadMap(){ return outputMap; } + private Long getLatestLogEntry(){ + if (mLatestLogEntry == null) { + mLatestLogEntry = loadLatestEntry(); + } + return mLatestLogEntry; + } + private Long loadLatestEntry() { SharedPreferences pSharedPref = getSharedPreferences(); if (pSharedPref != null) { From c57c1bbfb16e90daff35dbda9eb07a5a80f55307 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 13 Feb 2017 19:04:16 +0100 Subject: [PATCH 03/91] update recipe model to contain cool down structure --- src/main/java/it/near/sdk/Recipes/Models/Recipe.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java index 5bc993ac..e38f7d8a 100644 --- a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java +++ b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java @@ -41,6 +41,8 @@ public class Recipe extends Resource { HashMap labels; @SerializedName("scheduling") HashMap scheduling; + @SerializedName("cooldown") + HashMap cooldown; @SerializedName("pulse_plugin_id") String pulse_plugin_id; @Relationship("pulse_bundle") @@ -169,6 +171,10 @@ public void setReaction_action(ReactionAction reaction_action) { this.reaction_action = reaction_action; } + public HashMap getCooldown() { + return cooldown; + } + public String getNotificationTitle() { if (getNotification().containsKey("title")){ return getNotification().get("title").toString(); From 3983fdc6251f8c06beaf3c7f35aad658f44d4ecf Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 14 Feb 2017 15:28:24 +0100 Subject: [PATCH 04/91] add dependencies to testCompile configuration: - mockito - guava - ham crest - json --- build.gradle | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index fa60e635..ae21d4d9 100644 --- a/build.gradle +++ b/build.gradle @@ -140,10 +140,7 @@ task uploadDocs { dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') - testCompile 'junit:junit:4.12' compile 'com.android.support:support-annotations:24.2.1' - javadocDeps 'com.android.support:support-annotations:24.0.0' - javadocDeps 'org.altbeacon:android-beacon-library:2.9.2' compile 'org.apache.commons:commons-lang3:3.4' compile 'com.loopj.android:android-async-http:1.4.9' compile 'com.google.code.gson:gson:2.6.2' @@ -152,8 +149,15 @@ dependencies { compile 'com.google.firebase:firebase-core:10.0.1' compile 'com.google.android.gms:play-services-location:10.0.1' - // remove this - compile 'com.jraska:console:0.4.3' + testCompile 'junit:junit:4.12' + testCompile "org.mockito:mockito-core:2.7.6" + testCompile "com.google.guava:guava:21.0" + testCompile 'org.hamcrest:hamcrest-core:1.3' + testCompile 'org.json:json:20160212' + + javadocDeps 'com.android.support:support-annotations:24.0.0' + javadocDeps 'org.altbeacon:android-beacon-library:2.9.2' + } From 6b2ca5e96b4dcfb4f0217c96d6fbc441b3d2c4ac Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 14 Feb 2017 15:28:44 +0100 Subject: [PATCH 05/91] add setter for cool down object in recipe model --- src/main/java/it/near/sdk/Recipes/Models/Recipe.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java index e38f7d8a..9bc55014 100644 --- a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java +++ b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java @@ -171,10 +171,14 @@ public void setReaction_action(ReactionAction reaction_action) { this.reaction_action = reaction_action; } - public HashMap getCooldown() { + public Map getCooldown() { return cooldown; } + public void setCooldown(HashMap cooldown) { + this.cooldown = cooldown; + } + public String getNotificationTitle() { if (getNotification().containsKey("title")){ return getNotification().get("title").toString(); From 039e4ce2108df5138a67b4afa7487bfad2a13774 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 14 Feb 2017 15:29:10 +0100 Subject: [PATCH 06/91] change visibility of constants --- src/main/java/it/near/sdk/Recipes/RecipeCooler.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index 45f8ba9d..ae8c1aa3 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -23,12 +23,12 @@ public class RecipeCooler { private Context mContext; private SharedPreferences mSharedPreferences; - private static final String NEAR_COOLDOWN_HISTORY = "NearCooldownHistory"; - private static final String LOG_MAP = "LOG_MAP"; - private static final String LATEST_LOG = "LATEST_LOG"; + public static final String NEAR_COOLDOWN_HISTORY = "NearCooldownHistory"; + public static final String LOG_MAP = "LOG_MAP"; + public static final String LATEST_LOG = "LATEST_LOG"; - private static final String GLOBAL_COOLDOWN = "global_cooldown"; - private static final String SELF_COOLDOWN = "self_cooldown"; + public static final String GLOBAL_COOLDOWN = "global_cooldown"; + public static final String SELF_COOLDOWN = "self_cooldown"; private Map mRecipeLogMap; private Long mLatestLogEntry; @@ -55,6 +55,7 @@ public void markRecipeAsShown(String recipeId){ long timeStamp = System.currentTimeMillis(); getMap().put(recipeId, new Long(timeStamp)); saveMap(mRecipeLogMap); + saveLatestEntry(); } @@ -124,7 +125,7 @@ private Map loadMap(){ } } }catch(Exception e){ - e.printStackTrace(); + // e.printStackTrace(); } return outputMap; } From c056d4485e75fa218aa012a81ce3a9fdb7487e8f Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 14 Feb 2017 15:30:02 +0100 Subject: [PATCH 07/91] Add unit test class for recipe cooler. --- .../java/it/near/sdk/RecipeCoolerTest.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/test/java/it/near/sdk/RecipeCoolerTest.java diff --git a/src/test/java/it/near/sdk/RecipeCoolerTest.java b/src/test/java/it/near/sdk/RecipeCoolerTest.java new file mode 100644 index 00000000..622a0fd8 --- /dev/null +++ b/src/test/java/it/near/sdk/RecipeCoolerTest.java @@ -0,0 +1,99 @@ +package it.near.sdk; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.google.common.collect.Maps; + +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import it.near.sdk.Recipes.Models.Recipe; +import it.near.sdk.Recipes.RecipeCooler; + +import static it.near.sdk.Recipes.RecipeCooler.*; +import static junit.framework.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Created by cattaneostefano on 14/02/2017. + */ + +@RunWith(MockitoJUnitRunner.class) +public class RecipeCoolerTest { + + @Mock + Context mockContext; + @Mock + SharedPreferences mockSharedPreferences; + @Mock + SharedPreferences.Editor mockEditor; + + Recipe criticalRecipe; + + @Before + public void init() { + + when(mockContext.getSharedPreferences(anyString(), anyInt())).thenReturn(mockSharedPreferences); + when(mockSharedPreferences.edit()).thenReturn(mockEditor); + when(mockEditor.remove(anyString())).thenReturn(mockEditor); + when(mockEditor.commit()).thenReturn(true); + + + criticalRecipe = new Recipe(); + criticalRecipe.setId("recipe_id"); + HashMap cooldown = Maps.newHashMap(); + cooldown.put(GLOBAL_COOLDOWN, 0L); + cooldown.put(SELF_COOLDOWN, 0L); + criticalRecipe.setCooldown(cooldown); + } + + @Before + public void resetSingleton() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { + Field instance = RecipeCooler.class.getDeclaredField("INSTANCE"); + instance.setAccessible(true); + instance.set(null, null); + } + + @Test + public void whenLogIsEmpty_enableRecipe() { + when(mockSharedPreferences.getString(eq(LOG_MAP), anyString())).thenReturn("{}"); + + List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); + assertEquals(1, recipeList.size()); + RecipeCooler.getInstance(mockContext).filterRecipe(recipeList); + assertEquals(1, recipeList.size()); + + } + + @Test + public void whenRecipeShown_historyUpdated() { + + + RecipeCooler.getInstance(mockContext).markRecipeAsShown("recipe_id"); + Map logMap = Maps.newHashMap(); + logMap.put("recipe_id", System.currentTimeMillis()); + JSONObject jsonObject = new JSONObject(logMap); + String jsonString = jsonObject.toString(); + verify(mockEditor).putString(eq(LOG_MAP), contains("recipe_id")); + + } + + @Test + public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { + assertTrue(true); + RecipeCooler.getInstance(mockContext).markRecipeAsShown("recipe_id"); + } + +} From 3d7938aacfb0e16d2574722f137dc10b09b8bf55 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 14 Feb 2017 18:35:21 +0100 Subject: [PATCH 08/91] minor refactor --- src/main/java/it/near/sdk/Recipes/RecipeCooler.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index ae8c1aa3..9475f56b 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -55,7 +55,7 @@ public void markRecipeAsShown(String recipeId){ long timeStamp = System.currentTimeMillis(); getMap().put(recipeId, new Long(timeStamp)); saveMap(mRecipeLogMap); - saveLatestEntry(); + saveLatestEntry(System.currentTimeMillis()); } @@ -130,7 +130,7 @@ private Map loadMap(){ return outputMap; } - private Long getLatestLogEntry(){ + public Long getLatestLogEntry(){ if (mLatestLogEntry == null) { mLatestLogEntry = loadLatestEntry(); } @@ -145,12 +145,13 @@ private Long loadLatestEntry() { return 0L; } - private void saveLatestEntry() { + private void saveLatestEntry(long timestamp) { + mLatestLogEntry = timestamp; SharedPreferences pSharedPref = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); if (pSharedPref!=null){ SharedPreferences.Editor editor = pSharedPref.edit(); editor.remove(LATEST_LOG).commit(); - editor.putLong(LATEST_LOG, System.currentTimeMillis()); + editor.putLong(LATEST_LOG, timestamp); editor.commit(); } } From 6757f5bd1ef9a4c687ee243a27f1950b410139ea Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 14 Feb 2017 18:36:26 +0100 Subject: [PATCH 09/91] recipecooler testing: - refactoring initialisation - more test cases --- .../java/it/near/sdk/RecipeCoolerTest.java | 83 +++++++++++++++---- 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/src/test/java/it/near/sdk/RecipeCoolerTest.java b/src/test/java/it/near/sdk/RecipeCoolerTest.java index 622a0fd8..2775b9ef 100644 --- a/src/test/java/it/near/sdk/RecipeCoolerTest.java +++ b/src/test/java/it/near/sdk/RecipeCoolerTest.java @@ -7,6 +7,7 @@ import org.json.JSONObject; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -40,23 +41,22 @@ public class RecipeCoolerTest { @Mock SharedPreferences.Editor mockEditor; - Recipe criticalRecipe; + private Recipe criticalRecipe; + private Recipe nonCriticalRecipe; @Before - public void init() { + public void initRecipes() { + criticalRecipe = buildRecipe("critical", 0L, 0L); + nonCriticalRecipe = buildRecipe("pedestrian", 24 * 60 * 60L, 24 * 60 * 60L); + } + @Before + public void initMocks() { when(mockContext.getSharedPreferences(anyString(), anyInt())).thenReturn(mockSharedPreferences); when(mockSharedPreferences.edit()).thenReturn(mockEditor); when(mockEditor.remove(anyString())).thenReturn(mockEditor); when(mockEditor.commit()).thenReturn(true); - - criticalRecipe = new Recipe(); - criticalRecipe.setId("recipe_id"); - HashMap cooldown = Maps.newHashMap(); - cooldown.put(GLOBAL_COOLDOWN, 0L); - cooldown.put(SELF_COOLDOWN, 0L); - criticalRecipe.setCooldown(cooldown); } @Before @@ -79,21 +79,68 @@ public void whenLogIsEmpty_enableRecipe() { @Test public void whenRecipeShown_historyUpdated() { + RecipeCooler.getInstance(mockContext).markRecipeAsShown(criticalRecipe.getId()); + verify(mockEditor).putString(eq(LOG_MAP), contains(criticalRecipe.getId())); + } + @Test + public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + recipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); + List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); + recipeCooler.filterRecipe(recipeList); + assertEquals(0, recipeList.size()); + } - RecipeCooler.getInstance(mockContext).markRecipeAsShown("recipe_id"); - Map logMap = Maps.newHashMap(); - logMap.put("recipe_id", System.currentTimeMillis()); - JSONObject jsonObject = new JSONObject(logMap); - String jsonString = jsonObject.toString(); - verify(mockEditor).putString(eq(LOG_MAP), contains("recipe_id")); + @Test + public void whenRecipeHasNoCooldown_canBeShownAgain() { + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); + recipeCooler.filterRecipe(recipeList); + assertEquals(1, recipeList.size()); + recipeList.add(criticalRecipe); + recipeCooler.filterRecipe(recipeList); + assertEquals(2, recipeList.size()); + recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + } + @Test + public void whenRecipeIsShown_globalCooldownApplies() { + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); + recipeCooler.filterRecipe(recipeList); + assertEquals(0, recipeList.size()); + recipeList.add(criticalRecipe); + recipeCooler.filterRecipe(recipeList); + assertEquals(1, recipeList.size()); + recipeList.add(criticalRecipe); + recipeList.add(nonCriticalRecipe); + recipeList.add(nonCriticalRecipe); + recipeCooler.filterRecipe(recipeList); + assertEquals(2, recipeList.size()); } @Test - public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { - assertTrue(true); - RecipeCooler.getInstance(mockContext).markRecipeAsShown("recipe_id"); + public void whenRecipeIsShown_updateLastLogEntry() { + long beforeTimestamp = System.currentTimeMillis(); + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + long afterTimestamp = System.currentTimeMillis(); + long actualTimestamp = recipeCooler.getLatestLogEntry(); + assertTrue(beforeTimestamp <= actualTimestamp); + assertTrue(actualTimestamp <= afterTimestamp); + } + + private Recipe buildRecipe(String id, long globalCD, long selfCD) { + Recipe criticalRecipe = new Recipe(); + criticalRecipe.setId(id); + HashMap cooldown = Maps.newHashMap(); + cooldown.put(GLOBAL_COOLDOWN, globalCD); + cooldown.put(SELF_COOLDOWN, selfCD); + criticalRecipe.setCooldown(cooldown); + return criticalRecipe; } } From bea2b98c836fb45368bf913c34769ed27a7d392a Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Wed, 15 Feb 2017 11:45:54 +0100 Subject: [PATCH 10/91] DI improvement: inject shared preferences instead of context; healthier testing, but weirder initialisation. --- .../it/near/sdk/Recipes/RecipeCooler.java | 43 ++++++++----------- .../java/it/near/sdk/RecipeCoolerTest.java | 15 +++---- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index 9475f56b..ea7ed92e 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -2,6 +2,7 @@ import android.content.Context; import android.content.SharedPreferences; +import android.support.annotation.NonNull; import org.json.JSONObject; @@ -9,10 +10,11 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import it.near.sdk.Recipes.Models.Recipe; +import static com.google.gson.internal.$Gson$Preconditions.checkNotNull; + /** * Created by cattaneostefano on 13/02/2017. */ @@ -20,10 +22,9 @@ public class RecipeCooler { private static RecipeCooler INSTANCE; - private Context mContext; private SharedPreferences mSharedPreferences; - public static final String NEAR_COOLDOWN_HISTORY = "NearCooldownHistory"; + public static final String NEAR_RECIPECOOLER_PREFSNAME = "NearRecipeCoolerPrefsName"; public static final String LOG_MAP = "LOG_MAP"; public static final String LATEST_LOG = "LATEST_LOG"; @@ -33,20 +34,14 @@ public class RecipeCooler { private Map mRecipeLogMap; private Long mLatestLogEntry; - private RecipeCooler(Context context) { - mContext = context; - } - - private SharedPreferences getSharedPreferences(){ - if (mSharedPreferences == null){ - mSharedPreferences = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); - } - return mSharedPreferences; + private RecipeCooler(SharedPreferences sharedPreferences) { + mSharedPreferences = sharedPreferences; } - public static RecipeCooler getInstance(Context context){ + public static RecipeCooler getInstance(@NonNull SharedPreferences sharedPreferences){ + checkNotNull(sharedPreferences); if (INSTANCE == null){ - INSTANCE = new RecipeCooler(context); + INSTANCE = new RecipeCooler(sharedPreferences); } return INSTANCE; } @@ -99,11 +94,10 @@ private Map getMap() { } private void saveMap(Map inputMap){ - SharedPreferences pSharedPref = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); - if (pSharedPref != null){ + if (mSharedPreferences != null){ JSONObject jsonObject = new JSONObject(inputMap); String jsonString = jsonObject.toString(); - SharedPreferences.Editor editor = pSharedPref.edit(); + SharedPreferences.Editor editor = mSharedPreferences.edit(); editor.remove(LOG_MAP).commit(); editor.putString(LOG_MAP, jsonString); editor.commit(); @@ -112,10 +106,9 @@ private void saveMap(Map inputMap){ private Map loadMap(){ Map outputMap = new HashMap(); - SharedPreferences pSharedPref = getSharedPreferences(); try{ - if (pSharedPref != null){ - String jsonString = pSharedPref.getString(LOG_MAP, (new JSONObject()).toString()); + if (mSharedPreferences != null){ + String jsonString = mSharedPreferences.getString(LOG_MAP, (new JSONObject()).toString()); JSONObject jsonObject = new JSONObject(jsonString); Iterator keysItr = jsonObject.keys(); while(keysItr.hasNext()) { @@ -138,18 +131,16 @@ public Long getLatestLogEntry(){ } private Long loadLatestEntry() { - SharedPreferences pSharedPref = getSharedPreferences(); - if (pSharedPref != null) { - return pSharedPref.getLong(LATEST_LOG, 0L); + if (mSharedPreferences != null) { + return mSharedPreferences.getLong(LATEST_LOG, 0L); } return 0L; } private void saveLatestEntry(long timestamp) { mLatestLogEntry = timestamp; - SharedPreferences pSharedPref = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); - if (pSharedPref!=null){ - SharedPreferences.Editor editor = pSharedPref.edit(); + if (mSharedPreferences!=null){ + SharedPreferences.Editor editor = mSharedPreferences.edit(); editor.remove(LATEST_LOG).commit(); editor.putLong(LATEST_LOG, timestamp); editor.commit(); diff --git a/src/test/java/it/near/sdk/RecipeCoolerTest.java b/src/test/java/it/near/sdk/RecipeCoolerTest.java index 2775b9ef..d7f0f80f 100644 --- a/src/test/java/it/near/sdk/RecipeCoolerTest.java +++ b/src/test/java/it/near/sdk/RecipeCoolerTest.java @@ -34,8 +34,6 @@ @RunWith(MockitoJUnitRunner.class) public class RecipeCoolerTest { - @Mock - Context mockContext; @Mock SharedPreferences mockSharedPreferences; @Mock @@ -52,7 +50,6 @@ public void initRecipes() { @Before public void initMocks() { - when(mockContext.getSharedPreferences(anyString(), anyInt())).thenReturn(mockSharedPreferences); when(mockSharedPreferences.edit()).thenReturn(mockEditor); when(mockEditor.remove(anyString())).thenReturn(mockEditor); when(mockEditor.commit()).thenReturn(true); @@ -72,20 +69,20 @@ public void whenLogIsEmpty_enableRecipe() { List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); assertEquals(1, recipeList.size()); - RecipeCooler.getInstance(mockContext).filterRecipe(recipeList); + RecipeCooler.getInstance(mockSharedPreferences).filterRecipe(recipeList); assertEquals(1, recipeList.size()); } @Test public void whenRecipeShown_historyUpdated() { - RecipeCooler.getInstance(mockContext).markRecipeAsShown(criticalRecipe.getId()); + RecipeCooler.getInstance(mockSharedPreferences).markRecipeAsShown(criticalRecipe.getId()); verify(mockEditor).putString(eq(LOG_MAP), contains(criticalRecipe.getId())); } @Test public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); recipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); recipeCooler.filterRecipe(recipeList); @@ -94,7 +91,7 @@ public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { @Test public void whenRecipeHasNoCooldown_canBeShownAgain() { - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); recipeCooler.markRecipeAsShown(criticalRecipe.getId()); List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); recipeCooler.filterRecipe(recipeList); @@ -107,7 +104,7 @@ public void whenRecipeHasNoCooldown_canBeShownAgain() { @Test public void whenRecipeIsShown_globalCooldownApplies() { - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); recipeCooler.markRecipeAsShown(criticalRecipe.getId()); List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); recipeCooler.filterRecipe(recipeList); @@ -125,7 +122,7 @@ public void whenRecipeIsShown_globalCooldownApplies() { @Test public void whenRecipeIsShown_updateLastLogEntry() { long beforeTimestamp = System.currentTimeMillis(); - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); recipeCooler.markRecipeAsShown(criticalRecipe.getId()); long afterTimestamp = System.currentTimeMillis(); long actualTimestamp = recipeCooler.getLatestLogEntry(); From ad95527d7c5bc27a817ad99bda18e7fda4db9130 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Wed, 15 Feb 2017 12:31:11 +0100 Subject: [PATCH 11/91] better historyUpdated test for recipe cooler --- src/test/java/it/near/sdk/RecipeCoolerTest.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/test/java/it/near/sdk/RecipeCoolerTest.java b/src/test/java/it/near/sdk/RecipeCoolerTest.java index d7f0f80f..3420ad28 100644 --- a/src/test/java/it/near/sdk/RecipeCoolerTest.java +++ b/src/test/java/it/near/sdk/RecipeCoolerTest.java @@ -25,6 +25,7 @@ import static it.near.sdk.Recipes.RecipeCooler.*; import static junit.framework.Assert.*; +import static org.mockito.AdditionalMatchers.*; import static org.mockito.Mockito.*; /** @@ -76,8 +77,14 @@ public void whenLogIsEmpty_enableRecipe() { @Test public void whenRecipeShown_historyUpdated() { - RecipeCooler.getInstance(mockSharedPreferences).markRecipeAsShown(criticalRecipe.getId()); - verify(mockEditor).putString(eq(LOG_MAP), contains(criticalRecipe.getId())); + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); + recipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); + verify(mockEditor).putString(eq(LOG_MAP), and(contains(nonCriticalRecipe.getId()), + not(contains(criticalRecipe.getId())))); + recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + verify(mockEditor, atLeastOnce()) + .putString(eq(LOG_MAP), and(contains(nonCriticalRecipe.getId()), + contains(criticalRecipe.getId()))); } @Test From ea5e77fe6c2aebe380149eab16cea5fb6ca24caf Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Wed, 15 Feb 2017 13:08:31 +0100 Subject: [PATCH 12/91] As OOP design for testability suggests, refactored useless singleton pattern. Better testability as a result. --- .../it/near/sdk/Recipes/RecipeCooler.java | 12 +---- .../java/it/near/sdk/RecipeCoolerTest.java | 49 +++++++------------ 2 files changed, 20 insertions(+), 41 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index ea7ed92e..98d0feb9 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -21,7 +21,6 @@ public class RecipeCooler { - private static RecipeCooler INSTANCE; private SharedPreferences mSharedPreferences; public static final String NEAR_RECIPECOOLER_PREFSNAME = "NearRecipeCoolerPrefsName"; @@ -34,16 +33,9 @@ public class RecipeCooler { private Map mRecipeLogMap; private Long mLatestLogEntry; - private RecipeCooler(SharedPreferences sharedPreferences) { - mSharedPreferences = sharedPreferences; - } - - public static RecipeCooler getInstance(@NonNull SharedPreferences sharedPreferences){ + public RecipeCooler(@NonNull SharedPreferences sharedPreferences) { checkNotNull(sharedPreferences); - if (INSTANCE == null){ - INSTANCE = new RecipeCooler(sharedPreferences); - } - return INSTANCE; + mSharedPreferences = sharedPreferences; } public void markRecipeAsShown(String recipeId){ diff --git a/src/test/java/it/near/sdk/RecipeCoolerTest.java b/src/test/java/it/near/sdk/RecipeCoolerTest.java index 3420ad28..13dab338 100644 --- a/src/test/java/it/near/sdk/RecipeCoolerTest.java +++ b/src/test/java/it/near/sdk/RecipeCoolerTest.java @@ -42,26 +42,18 @@ public class RecipeCoolerTest { private Recipe criticalRecipe; private Recipe nonCriticalRecipe; + private RecipeCooler mRecipeCooler; @Before - public void initRecipes() { + public void setUp() { criticalRecipe = buildRecipe("critical", 0L, 0L); nonCriticalRecipe = buildRecipe("pedestrian", 24 * 60 * 60L, 24 * 60 * 60L); - } - @Before - public void initMocks() { when(mockSharedPreferences.edit()).thenReturn(mockEditor); when(mockEditor.remove(anyString())).thenReturn(mockEditor); when(mockEditor.commit()).thenReturn(true); - } - - @Before - public void resetSingleton() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { - Field instance = RecipeCooler.class.getDeclaredField("INSTANCE"); - instance.setAccessible(true); - instance.set(null, null); + mRecipeCooler = new RecipeCooler(mockSharedPreferences); } @Test @@ -70,18 +62,17 @@ public void whenLogIsEmpty_enableRecipe() { List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); assertEquals(1, recipeList.size()); - RecipeCooler.getInstance(mockSharedPreferences).filterRecipe(recipeList); + mRecipeCooler.filterRecipe(recipeList); assertEquals(1, recipeList.size()); } @Test public void whenRecipeShown_historyUpdated() { - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); - recipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); + mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); verify(mockEditor).putString(eq(LOG_MAP), and(contains(nonCriticalRecipe.getId()), not(contains(criticalRecipe.getId())))); - recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); verify(mockEditor, atLeastOnce()) .putString(eq(LOG_MAP), and(contains(nonCriticalRecipe.getId()), contains(criticalRecipe.getId()))); @@ -89,50 +80,46 @@ public void whenRecipeShown_historyUpdated() { @Test public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); - recipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); + mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); - recipeCooler.filterRecipe(recipeList); + mRecipeCooler.filterRecipe(recipeList); assertEquals(0, recipeList.size()); } @Test public void whenRecipeHasNoCooldown_canBeShownAgain() { - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); - recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); - recipeCooler.filterRecipe(recipeList); + mRecipeCooler.filterRecipe(recipeList); assertEquals(1, recipeList.size()); recipeList.add(criticalRecipe); - recipeCooler.filterRecipe(recipeList); + mRecipeCooler.filterRecipe(recipeList); assertEquals(2, recipeList.size()); - recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); } @Test public void whenRecipeIsShown_globalCooldownApplies() { - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); - recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); - recipeCooler.filterRecipe(recipeList); + mRecipeCooler.filterRecipe(recipeList); assertEquals(0, recipeList.size()); recipeList.add(criticalRecipe); - recipeCooler.filterRecipe(recipeList); + mRecipeCooler.filterRecipe(recipeList); assertEquals(1, recipeList.size()); recipeList.add(criticalRecipe); recipeList.add(nonCriticalRecipe); recipeList.add(nonCriticalRecipe); - recipeCooler.filterRecipe(recipeList); + mRecipeCooler.filterRecipe(recipeList); assertEquals(2, recipeList.size()); } @Test public void whenRecipeIsShown_updateLastLogEntry() { long beforeTimestamp = System.currentTimeMillis(); - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); - recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); long afterTimestamp = System.currentTimeMillis(); - long actualTimestamp = recipeCooler.getLatestLogEntry(); + long actualTimestamp = mRecipeCooler.getLatestLogEntry(); assertTrue(beforeTimestamp <= actualTimestamp); assertTrue(actualTimestamp <= afterTimestamp); } From ec2759c0091d817c6de4ceed4b3b413e5a95648a Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Wed, 15 Feb 2017 16:12:09 +0100 Subject: [PATCH 13/91] Handle cool down null cases (both on the whole cool down structure and on the single cool down) as a critical scenario. Create tests for those cases. --- .../it/near/sdk/Recipes/RecipeCooler.java | 17 +++---- .../java/it/near/sdk/RecipeCoolerTest.java | 48 +++++++++++++++++-- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index 98d0feb9..ca5c3adc 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -48,22 +48,23 @@ public void markRecipeAsShown(String recipeId){ private boolean canShowRecipe(Recipe recipe){ Map cooldown = recipe.getCooldown(); - return cooldown != null && - globalCooldownCheck(cooldown) && - selfCooldownCheck(recipe, cooldown); + return cooldown == null || + ( globalCooldownCheck(cooldown) && selfCooldownCheck(recipe, cooldown) ); } private boolean globalCooldownCheck(Map cooldown) { - // TODO cosa fare se non c'è? - if (!cooldown.containsKey(GLOBAL_COOLDOWN)) return true; + if (!cooldown.containsKey(GLOBAL_COOLDOWN) || + cooldown.get(GLOBAL_COOLDOWN) == null) return true; + long expiredSeconds = (System.currentTimeMillis() - getLatestLogEntry()) / 1000; return expiredSeconds >= (Long)cooldown.get(GLOBAL_COOLDOWN); } private boolean selfCooldownCheck(Recipe recipe, Map cooldown){ - // TODO cosa fare se non c'è? - if (!cooldown.containsKey(SELF_COOLDOWN)) return true; - if (!getMap().containsKey(recipe.getId())) return true; + if (!cooldown.containsKey(SELF_COOLDOWN) || + cooldown.get(SELF_COOLDOWN) == null || + !getMap().containsKey(recipe.getId())) return true; + long recipeLatestEntry = getMap().get(recipe.getId()); long expiredSeconds = (System.currentTimeMillis() - recipeLatestEntry) / 1000; return expiredSeconds >= (Long)cooldown.get(SELF_COOLDOWN); diff --git a/src/test/java/it/near/sdk/RecipeCoolerTest.java b/src/test/java/it/near/sdk/RecipeCoolerTest.java index 13dab338..a7286532 100644 --- a/src/test/java/it/near/sdk/RecipeCoolerTest.java +++ b/src/test/java/it/near/sdk/RecipeCoolerTest.java @@ -4,6 +4,7 @@ import android.content.SharedPreferences; import com.google.common.collect.Maps; +import com.google.common.collect.ObjectArrays; import org.json.JSONObject; import org.junit.Before; @@ -46,8 +47,9 @@ public class RecipeCoolerTest { @Before public void setUp() { - criticalRecipe = buildRecipe("critical", 0L, 0L); - nonCriticalRecipe = buildRecipe("pedestrian", 24 * 60 * 60L, 24 * 60 * 60L); + criticalRecipe = buildRecipe("critical", buildCooldown(0L, 0L)); + nonCriticalRecipe = buildRecipe("pedestrian", buildCooldown(24 * 60 * 60L, + 24 * 60 * 60L)); when(mockSharedPreferences.edit()).thenReturn(mockEditor); when(mockEditor.remove(anyString())).thenReturn(mockEditor); @@ -124,14 +126,50 @@ public void whenRecipeIsShown_updateLastLogEntry() { assertTrue(actualTimestamp <= afterTimestamp); } - private Recipe buildRecipe(String id, long globalCD, long selfCD) { + @Test + public void whenCooldownMissing_showRecipe() { + mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); + List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); + mRecipeCooler.filterRecipe(recipeList); + assertEquals(0, recipeList.size()); + nonCriticalRecipe.setCooldown(null); + recipeList.add(nonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + assertEquals(1, recipeList.size()); + } + + @Test + public void whenMissingSelfCooldown_considerItZero() { + mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); + // force the check on the selfcooldown + nonCriticalRecipe.setCooldown(buildCooldown(0L, null)); + List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); + mRecipeCooler.filterRecipe(recipeList); + assertEquals(1, recipeList.size()); + } + + @Test + public void whenMissingGlobalCoolDown_considerItZero() { + mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); + // force the check on globalcooldown + nonCriticalRecipe.setCooldown(buildCooldown(null, 0L)); + List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); + mRecipeCooler.filterRecipe(recipeList); + assertEquals(1, recipeList.size()); + } + + private Recipe buildRecipe(String id, HashMap cooldown) { Recipe criticalRecipe = new Recipe(); criticalRecipe.setId(id); + criticalRecipe.setCooldown(cooldown); + return criticalRecipe; + } + + private HashMap buildCooldown(Long globalCD, Long selfCD) { HashMap cooldown = Maps.newHashMap(); cooldown.put(GLOBAL_COOLDOWN, globalCD); cooldown.put(SELF_COOLDOWN, selfCD); - criticalRecipe.setCooldown(cooldown); - return criticalRecipe; + return cooldown; } } From b9374a29b717c01d97a666649e920ba602e16ade Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Thu, 16 Feb 2017 15:14:57 +0100 Subject: [PATCH 14/91] new matchers in testing, some comments --- .../java/it/near/sdk/RecipeCoolerTest.java | 175 ---------------- .../it/near/sdk/Recipes/RecipeCoolerTest.java | 197 ++++++++++++++++++ 2 files changed, 197 insertions(+), 175 deletions(-) delete mode 100644 src/test/java/it/near/sdk/RecipeCoolerTest.java create mode 100644 src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java diff --git a/src/test/java/it/near/sdk/RecipeCoolerTest.java b/src/test/java/it/near/sdk/RecipeCoolerTest.java deleted file mode 100644 index a7286532..00000000 --- a/src/test/java/it/near/sdk/RecipeCoolerTest.java +++ /dev/null @@ -1,175 +0,0 @@ -package it.near.sdk; - -import android.content.Context; -import android.content.SharedPreferences; - -import com.google.common.collect.Maps; -import com.google.common.collect.ObjectArrays; - -import org.json.JSONObject; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import it.near.sdk.Recipes.Models.Recipe; -import it.near.sdk.Recipes.RecipeCooler; - -import static it.near.sdk.Recipes.RecipeCooler.*; -import static junit.framework.Assert.*; -import static org.mockito.AdditionalMatchers.*; -import static org.mockito.Mockito.*; - -/** - * Created by cattaneostefano on 14/02/2017. - */ - -@RunWith(MockitoJUnitRunner.class) -public class RecipeCoolerTest { - - @Mock - SharedPreferences mockSharedPreferences; - @Mock - SharedPreferences.Editor mockEditor; - - private Recipe criticalRecipe; - private Recipe nonCriticalRecipe; - private RecipeCooler mRecipeCooler; - - @Before - public void setUp() { - criticalRecipe = buildRecipe("critical", buildCooldown(0L, 0L)); - nonCriticalRecipe = buildRecipe("pedestrian", buildCooldown(24 * 60 * 60L, - 24 * 60 * 60L)); - - when(mockSharedPreferences.edit()).thenReturn(mockEditor); - when(mockEditor.remove(anyString())).thenReturn(mockEditor); - when(mockEditor.commit()).thenReturn(true); - - mRecipeCooler = new RecipeCooler(mockSharedPreferences); - } - - @Test - public void whenLogIsEmpty_enableRecipe() { - when(mockSharedPreferences.getString(eq(LOG_MAP), anyString())).thenReturn("{}"); - - List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); - assertEquals(1, recipeList.size()); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(1, recipeList.size()); - - } - - @Test - public void whenRecipeShown_historyUpdated() { - mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); - verify(mockEditor).putString(eq(LOG_MAP), and(contains(nonCriticalRecipe.getId()), - not(contains(criticalRecipe.getId())))); - mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); - verify(mockEditor, atLeastOnce()) - .putString(eq(LOG_MAP), and(contains(nonCriticalRecipe.getId()), - contains(criticalRecipe.getId()))); - } - - @Test - public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { - mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); - List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(0, recipeList.size()); - } - - @Test - public void whenRecipeHasNoCooldown_canBeShownAgain() { - mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); - List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(1, recipeList.size()); - recipeList.add(criticalRecipe); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(2, recipeList.size()); - mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); - } - - @Test - public void whenRecipeIsShown_globalCooldownApplies() { - mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); - List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(0, recipeList.size()); - recipeList.add(criticalRecipe); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(1, recipeList.size()); - recipeList.add(criticalRecipe); - recipeList.add(nonCriticalRecipe); - recipeList.add(nonCriticalRecipe); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(2, recipeList.size()); - } - - @Test - public void whenRecipeIsShown_updateLastLogEntry() { - long beforeTimestamp = System.currentTimeMillis(); - mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); - long afterTimestamp = System.currentTimeMillis(); - long actualTimestamp = mRecipeCooler.getLatestLogEntry(); - assertTrue(beforeTimestamp <= actualTimestamp); - assertTrue(actualTimestamp <= afterTimestamp); - } - - @Test - public void whenCooldownMissing_showRecipe() { - mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); - List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(0, recipeList.size()); - nonCriticalRecipe.setCooldown(null); - recipeList.add(nonCriticalRecipe); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(1, recipeList.size()); - } - - @Test - public void whenMissingSelfCooldown_considerItZero() { - mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); - // force the check on the selfcooldown - nonCriticalRecipe.setCooldown(buildCooldown(0L, null)); - List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(1, recipeList.size()); - } - - @Test - public void whenMissingGlobalCoolDown_considerItZero() { - mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); - // force the check on globalcooldown - nonCriticalRecipe.setCooldown(buildCooldown(null, 0L)); - List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(1, recipeList.size()); - } - - private Recipe buildRecipe(String id, HashMap cooldown) { - Recipe criticalRecipe = new Recipe(); - criticalRecipe.setId(id); - criticalRecipe.setCooldown(cooldown); - return criticalRecipe; - } - - private HashMap buildCooldown(Long globalCD, Long selfCD) { - HashMap cooldown = Maps.newHashMap(); - cooldown.put(GLOBAL_COOLDOWN, globalCD); - cooldown.put(SELF_COOLDOWN, selfCD); - return cooldown; - } - -} diff --git a/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java b/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java new file mode 100644 index 00000000..ccc46201 --- /dev/null +++ b/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java @@ -0,0 +1,197 @@ +package it.near.sdk.Recipes; + +import android.content.SharedPreferences; + +import com.google.common.collect.Maps; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import it.near.sdk.Recipes.Models.Recipe; +import it.near.sdk.Recipes.RecipeCooler; + +import static com.google.common.collect.Lists.newArrayList; +import static it.near.sdk.Recipes.RecipeCooler.*; +import static junit.framework.Assert.*; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.AdditionalMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Created by cattaneostefano on 14/02/2017. + */ + +@RunWith(MockitoJUnitRunner.class) +public class RecipeCoolerTest { + + @Mock + SharedPreferences mMockSharedPreferences; + @Mock + SharedPreferences.Editor mMockEditor; + + private Recipe mCriticalRecipe; + private Recipe mNonCriticalRecipe; + private RecipeCooler mRecipeCooler; + + @Before + public void setUp() { + mCriticalRecipe = buildRecipe("critical", buildCooldown(0L, 0L)); + mNonCriticalRecipe = buildRecipe("pedestrian", buildCooldown(24 * 60 * 60L, + 24 * 60 * 60L)); + + when(mMockSharedPreferences.edit()).thenReturn(mMockEditor); + when(mMockEditor.remove(anyString())).thenReturn(mMockEditor); + when(mMockEditor.commit()).thenReturn(true); + + mRecipeCooler = new RecipeCooler(mMockSharedPreferences); + } + + @Test + public void whenLogIsEmpty_enableRecipe() { + // when we filter recipes with a fresh history + List recipeList = newArrayList(mCriticalRecipe, + mCriticalRecipe, + mNonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // then they all pass the filter + assertEquals(3, recipeList.size()); + assertThat(recipeList, hasItems(mCriticalRecipe, mNonCriticalRecipe)); + + } + + @Test + public void whenRecipeShown_historyUpdated() { + // when we mark a recipe as shown + mRecipeCooler.markRecipeAsShown(mNonCriticalRecipe.getId()); + // then its timestamp is put in history + verify(mMockEditor).putString(eq(LOG_MAP), and(contains(mNonCriticalRecipe.getId()), + not(contains(mCriticalRecipe.getId())))); + // when we mark another recipe as shown + mRecipeCooler.markRecipeAsShown(mCriticalRecipe.getId()); + // then its timestamp is added to history + verify(mMockEditor, atLeastOnce()) + .putString(eq(LOG_MAP), and(contains(mNonCriticalRecipe.getId()), + contains(mCriticalRecipe.getId()))); + } + + @Test + public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { + // when a non critical recipe is shown + mRecipeCooler.markRecipeAsShown(mNonCriticalRecipe.getId()); + List recipeList = newArrayList(mNonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // then it can't be displayed again + assertEquals(0, recipeList.size()); + assertThat(recipeList, org.hamcrest.core.IsNot.not(hasItem(mNonCriticalRecipe))); + } + + @Test + public void whenRecipeHasNoCooldown_canBeShownAgain() { + // when a recipe is shown + mRecipeCooler.markRecipeAsShown(mCriticalRecipe.getId()); + List recipeList = newArrayList(mCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // then a critical recipe can still be shown shortly afterwards + assertEquals(1, recipeList.size()); + recipeList.add(mCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + assertEquals(2, recipeList.size()); + } + + @Test + public void whenRecipeIsShown_globalCooldownApplies() { + // when a recipe is shown + mRecipeCooler.markRecipeAsShown(mCriticalRecipe.getId()); + List recipeList = newArrayList(mNonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // then a non critical recipe won't be shown + assertEquals(0, recipeList.size()); + assertThat(recipeList, org.hamcrest.core.IsNot.not(hasItem(mNonCriticalRecipe))); + recipeList.add(mCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // but a critical recipe will + assertEquals(1, recipeList.size()); + recipeList.add(mCriticalRecipe); + recipeList.add(mNonCriticalRecipe); + recipeList.add(mNonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + assertEquals(2, recipeList.size()); + assertThat(recipeList, hasItem(mCriticalRecipe)); + assertThat(recipeList, org.hamcrest.core.IsNot.not(hasItem(mNonCriticalRecipe))); + } + + @Test + public void whenRecipeIsShown_updateLastLogEntry() { + long beforeTimestamp = System.currentTimeMillis(); + // when we mark a recipe as shown + mRecipeCooler.markRecipeAsShown(mCriticalRecipe.getId()); + long afterTimestamp = System.currentTimeMillis(); + long actualTimestamp = mRecipeCooler.getLatestLogEntry(); + // then the latest log entry is updated + assertTrue(beforeTimestamp <= actualTimestamp); + assertTrue(actualTimestamp <= afterTimestamp); + } + + @Test + public void whenCooldownMissing_showRecipe() { + // when there's a recent entry log + mRecipeCooler.markRecipeAsShown(mNonCriticalRecipe.getId()); + // and a recipe without the cooldown section + mNonCriticalRecipe.setCooldown(null); + List recipeList = newArrayList(mNonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // then it gets treated as a critical recipe + assertEquals(1, recipeList.size()); + assertThat(recipeList, hasItem(mNonCriticalRecipe)); + } + + @Test + public void whenMissingSelfCooldown_considerItZero() { + // when a recipe has no selfcooldown + mNonCriticalRecipe.setCooldown(buildCooldown(0L, null)); + mRecipeCooler.markRecipeAsShown(mNonCriticalRecipe.getId()); + List recipeList = newArrayList(mNonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // then it gets treaded as critical + assertEquals(1, recipeList.size()); + assertThat(recipeList, hasItem(mNonCriticalRecipe)); + } + + @Test + public void whenMissingGlobalCoolDown_considerItZero() { + // when a recipe has no globalcooldown + mRecipeCooler.markRecipeAsShown(mCriticalRecipe.getId()); + mNonCriticalRecipe.setCooldown(buildCooldown(null, 0L)); + List recipeList = newArrayList(mNonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // then its get treaded as critical + assertEquals(1, recipeList.size()); + assertThat(recipeList, hasItem(mNonCriticalRecipe)); + } + + private Recipe buildRecipe(String id, HashMap cooldown) { + Recipe criticalRecipe = new Recipe(); + criticalRecipe.setId(id); + criticalRecipe.setCooldown(cooldown); + return criticalRecipe; + } + + private HashMap buildCooldown(Long globalCD, Long selfCD) { + HashMap cooldown = Maps.newHashMap(); + cooldown.put(GLOBAL_COOLDOWN, globalCD); + cooldown.put(SELF_COOLDOWN, selfCD); + return cooldown; + } + +} From c98bb0a438be1088ed9b58dd8524b614887a9c21 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Thu, 16 Feb 2017 15:24:34 +0100 Subject: [PATCH 15/91] recipe cooler filter pipelined in recipe evaluation for trigger defined as pulse triple. Still missing from online evaluation, refactor needed --- src/main/java/it/near/sdk/Recipes/RecipesManager.java | 9 +++++++++ src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipesManager.java b/src/main/java/it/near/sdk/Recipes/RecipesManager.java index 64aaa004..2f5073e2 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipesManager.java +++ b/src/main/java/it/near/sdk/Recipes/RecipesManager.java @@ -55,6 +55,7 @@ public class RecipesManager { private HashMap reactions = new HashMap<>(); SharedPreferences.Editor editor; private NearAsyncHttpClient httpClient; + private RecipeCooler mRecipeCooler; public RecipesManager(Context context) { this.mContext = context; @@ -71,6 +72,7 @@ public RecipesManager(Context context) { } setUpMorpheusParser(); refreshConfig(); + setUpRecipeCooler(); } /** @@ -89,6 +91,11 @@ private void setUpMorpheusParser() { morpheus.getFactory().getDeserializer().registerResourceClass("reaction_bundles", ReactionBundle.class); } + private void setUpRecipeCooler() { + SharedPreferences recipeCoolerSP = mContext.getSharedPreferences(RecipeCooler.NEAR_RECIPECOOLER_PREFSNAME,0); + mRecipeCooler = new RecipeCooler(recipeCoolerSP); + } + public void addReaction(Reaction reaction){ reactions.put(reaction.getPluginName(), reaction); } @@ -215,6 +222,8 @@ public void gotPulse(String pulse_plugin, String pulse_action, String pulse_bund } } + mRecipeCooler.filterRecipe(validRecipes); + if (validRecipes.isEmpty()){ // if no recipe is found the the online fallback onlinePulseEvaluation(pulse_plugin, pulse_action, pulse_bundle); diff --git a/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java b/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java index ccc46201..82e89985 100644 --- a/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java +++ b/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java @@ -179,7 +179,7 @@ public void whenMissingGlobalCoolDown_considerItZero() { assertEquals(1, recipeList.size()); assertThat(recipeList, hasItem(mNonCriticalRecipe)); } - + private Recipe buildRecipe(String id, HashMap cooldown) { Recipe criticalRecipe = new Recipe(); criticalRecipe.setId(id); From e4305475c9939f8f264421e56ad0f5f9553cc160 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Fri, 17 Feb 2017 11:41:25 +0100 Subject: [PATCH 16/91] dependency as a final field --- src/main/java/it/near/sdk/Recipes/RecipeCooler.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index ca5c3adc..f2df17b7 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -21,7 +21,7 @@ public class RecipeCooler { - private SharedPreferences mSharedPreferences; + private final SharedPreferences mSharedPreferences; public static final String NEAR_RECIPECOOLER_PREFSNAME = "NearRecipeCoolerPrefsName"; public static final String LOG_MAP = "LOG_MAP"; @@ -34,8 +34,7 @@ public class RecipeCooler { private Long mLatestLogEntry; public RecipeCooler(@NonNull SharedPreferences sharedPreferences) { - checkNotNull(sharedPreferences); - mSharedPreferences = sharedPreferences; + mSharedPreferences = checkNotNull(sharedPreferences);; } public void markRecipeAsShown(String recipeId){ From b698d9873ad314feb1e25df87ecd0798c9185956 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 13 Feb 2017 16:17:05 +0100 Subject: [PATCH 17/91] add recipe cooler component: - singleton - persistence on disk of recipe log and latest recipe timestamp --- .../it/near/sdk/Recipes/RecipeCooler.java | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/main/java/it/near/sdk/Recipes/RecipeCooler.java diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java new file mode 100644 index 00000000..7de0c2cb --- /dev/null +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -0,0 +1,123 @@ +package it.near.sdk.Recipes; + +import android.content.Context; +import android.content.SharedPreferences; + +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; + +import it.near.sdk.Recipes.Models.Recipe; + +/** + * Created by cattaneostefano on 13/02/2017. + */ + +public class RecipeCooler { + + private static RecipeCooler INSTANCE; + private Context mContext; + private SharedPreferences mSharedPreferences; + + private static final String NEAR_COOLDOWN_HISTORY = "NearCooldownHistory"; + private static final String LOG_MAP = "LOG_MAP"; + private static final String LATEST_LOG = "LATEST_LOG"; + private Map mRecipeLogMap; + private Long latestLogEntry; + + private RecipeCooler(Context context) { + mContext = context; + mRecipeLogMap = loadMap(); + latestLogEntry = loadLatestEntry(); + } + + private SharedPreferences getSharedPreferences(){ + if (mSharedPreferences == null){ + mSharedPreferences = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); + } + return mSharedPreferences; + } + + public static RecipeCooler getInstance(Context context){ + if (INSTANCE == null){ + INSTANCE = new RecipeCooler(context); + } + return INSTANCE; + } + + public void markRecipeAsShown(String recipeId){ + if (mRecipeLogMap == null) mRecipeLogMap = loadMap(); + long timeStamp = System.currentTimeMillis(); + mRecipeLogMap.put(recipeId, new Long(timeStamp)); + saveMap(mRecipeLogMap); + + } + + public boolean canShowRecipe(Recipe recipe){ + long timestamp = System.currentTimeMillis(); + long latestNotificationTimestamp = loadLatestEntry(); + return false; + } + + public void filterRecipe(List recipes){ + for (Iterator it = recipes.iterator(); it.hasNext();) { + Recipe recipe = it.next(); + if (!canShowRecipe(recipe)){ + it.remove(); + } + } + } + + private void saveMap(Map inputMap){ + SharedPreferences pSharedPref = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); + if (pSharedPref != null){ + JSONObject jsonObject = new JSONObject(inputMap); + String jsonString = jsonObject.toString(); + SharedPreferences.Editor editor = pSharedPref.edit(); + editor.remove(LOG_MAP).commit(); + editor.putString(LOG_MAP, jsonString); + editor.commit(); + } + } + + private Map loadMap(){ + Map outputMap = new HashMap(); + SharedPreferences pSharedPref = getSharedPreferences(); + try{ + if (pSharedPref != null){ + String jsonString = pSharedPref.getString(LOG_MAP, (new JSONObject()).toString()); + JSONObject jsonObject = new JSONObject(jsonString); + Iterator keysItr = jsonObject.keys(); + while(keysItr.hasNext()) { + String key = keysItr.next(); + Long value = (Long) jsonObject.get(key); + outputMap.put(key, value); + } + } + }catch(Exception e){ + e.printStackTrace(); + } + return outputMap; + } + + private Long loadLatestEntry() { + SharedPreferences pSharedPref = getSharedPreferences(); + if (pSharedPref != null) { + return pSharedPref.getLong(LATEST_LOG, 0L); + } + return 0L; + } + + private void saveLatestEntry() { + SharedPreferences pSharedPref = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); + if (pSharedPref!=null){ + SharedPreferences.Editor editor = pSharedPref.edit(); + editor.remove(LATEST_LOG).commit(); + editor.putLong(LATEST_LOG, System.currentTimeMillis()); + editor.commit(); + } + } +} From 8a8fde842c4f64bf90be8a0e39c9af3a04152559 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 13 Feb 2017 18:14:29 +0100 Subject: [PATCH 18/91] Add recipe cooler functionalities: - check for global and self cool down - lazy loading of cached recipe history --- .../it/near/sdk/Recipes/RecipeCooler.java | 51 +++++++++++++++---- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index 7de0c2cb..45f8ba9d 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -9,6 +9,7 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import it.near.sdk.Recipes.Models.Recipe; @@ -25,13 +26,15 @@ public class RecipeCooler { private static final String NEAR_COOLDOWN_HISTORY = "NearCooldownHistory"; private static final String LOG_MAP = "LOG_MAP"; private static final String LATEST_LOG = "LATEST_LOG"; + + private static final String GLOBAL_COOLDOWN = "global_cooldown"; + private static final String SELF_COOLDOWN = "self_cooldown"; + private Map mRecipeLogMap; - private Long latestLogEntry; + private Long mLatestLogEntry; private RecipeCooler(Context context) { mContext = context; - mRecipeLogMap = loadMap(); - latestLogEntry = loadLatestEntry(); } private SharedPreferences getSharedPreferences(){ @@ -49,17 +52,33 @@ public static RecipeCooler getInstance(Context context){ } public void markRecipeAsShown(String recipeId){ - if (mRecipeLogMap == null) mRecipeLogMap = loadMap(); long timeStamp = System.currentTimeMillis(); - mRecipeLogMap.put(recipeId, new Long(timeStamp)); + getMap().put(recipeId, new Long(timeStamp)); saveMap(mRecipeLogMap); } - public boolean canShowRecipe(Recipe recipe){ - long timestamp = System.currentTimeMillis(); - long latestNotificationTimestamp = loadLatestEntry(); - return false; + private boolean canShowRecipe(Recipe recipe){ + Map cooldown = recipe.getCooldown(); + return cooldown != null && + globalCooldownCheck(cooldown) && + selfCooldownCheck(recipe, cooldown); + } + + private boolean globalCooldownCheck(Map cooldown) { + // TODO cosa fare se non c'è? + if (!cooldown.containsKey(GLOBAL_COOLDOWN)) return true; + long expiredSeconds = (System.currentTimeMillis() - getLatestLogEntry()) / 1000; + return expiredSeconds >= (Long)cooldown.get(GLOBAL_COOLDOWN); + } + + private boolean selfCooldownCheck(Recipe recipe, Map cooldown){ + // TODO cosa fare se non c'è? + if (!cooldown.containsKey(SELF_COOLDOWN)) return true; + if (!getMap().containsKey(recipe.getId())) return true; + long recipeLatestEntry = getMap().get(recipe.getId()); + long expiredSeconds = (System.currentTimeMillis() - recipeLatestEntry) / 1000; + return expiredSeconds >= (Long)cooldown.get(SELF_COOLDOWN); } public void filterRecipe(List recipes){ @@ -71,6 +90,13 @@ public void filterRecipe(List recipes){ } } + private Map getMap() { + if (mRecipeLogMap == null){ + mRecipeLogMap = loadMap(); + } + return mRecipeLogMap; + } + private void saveMap(Map inputMap){ SharedPreferences pSharedPref = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); if (pSharedPref != null){ @@ -103,6 +129,13 @@ private Map loadMap(){ return outputMap; } + private Long getLatestLogEntry(){ + if (mLatestLogEntry == null) { + mLatestLogEntry = loadLatestEntry(); + } + return mLatestLogEntry; + } + private Long loadLatestEntry() { SharedPreferences pSharedPref = getSharedPreferences(); if (pSharedPref != null) { From b2c4b6f0118c4795c48bc0f0ceae15de593fe378 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 13 Feb 2017 19:04:16 +0100 Subject: [PATCH 19/91] update recipe model to contain cool down structure --- src/main/java/it/near/sdk/Recipes/Models/Recipe.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java index 1c1435ba..fe2a162b 100644 --- a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java +++ b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java @@ -41,6 +41,8 @@ public class Recipe extends Resource { HashMap labels; @SerializedName("scheduling") HashMap scheduling; + @SerializedName("cooldown") + HashMap cooldown; @SerializedName("pulse_plugin_id") String pulse_plugin_id; @Relationship("pulse_bundle") @@ -169,8 +171,12 @@ public void setReaction_action(ReactionAction reaction_action) { this.reaction_action = reaction_action; } - public void setScheduling(HashMap scheduling) { +public void setScheduling(HashMap scheduling) { this.scheduling = scheduling; + } + +public HashMap getCooldown() { + return cooldown; } public String getNotificationTitle() { From 6c32259bce4e1dd68528479c4436b387e79d27ba Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 14 Feb 2017 15:28:44 +0100 Subject: [PATCH 20/91] add setter for cool down object in recipe model --- src/main/java/it/near/sdk/Recipes/Models/Recipe.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java index fe2a162b..7b90360c 100644 --- a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java +++ b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java @@ -173,12 +173,15 @@ public void setReaction_action(ReactionAction reaction_action) { public void setScheduling(HashMap scheduling) { this.scheduling = scheduling; - } - -public HashMap getCooldown() { + } + public HashMap getCooldown() { return cooldown; } + public void setCooldown(HashMap cooldown) { + this.cooldown = cooldown; + } + public String getNotificationTitle() { if (getNotification().containsKey("title")){ return getNotification().get("title").toString(); From 157db19a636ab483de158e1caa6c92cfccf3a37f Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 14 Feb 2017 15:29:10 +0100 Subject: [PATCH 21/91] change visibility of constants --- src/main/java/it/near/sdk/Recipes/RecipeCooler.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index 45f8ba9d..ae8c1aa3 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -23,12 +23,12 @@ public class RecipeCooler { private Context mContext; private SharedPreferences mSharedPreferences; - private static final String NEAR_COOLDOWN_HISTORY = "NearCooldownHistory"; - private static final String LOG_MAP = "LOG_MAP"; - private static final String LATEST_LOG = "LATEST_LOG"; + public static final String NEAR_COOLDOWN_HISTORY = "NearCooldownHistory"; + public static final String LOG_MAP = "LOG_MAP"; + public static final String LATEST_LOG = "LATEST_LOG"; - private static final String GLOBAL_COOLDOWN = "global_cooldown"; - private static final String SELF_COOLDOWN = "self_cooldown"; + public static final String GLOBAL_COOLDOWN = "global_cooldown"; + public static final String SELF_COOLDOWN = "self_cooldown"; private Map mRecipeLogMap; private Long mLatestLogEntry; @@ -55,6 +55,7 @@ public void markRecipeAsShown(String recipeId){ long timeStamp = System.currentTimeMillis(); getMap().put(recipeId, new Long(timeStamp)); saveMap(mRecipeLogMap); + saveLatestEntry(); } @@ -124,7 +125,7 @@ private Map loadMap(){ } } }catch(Exception e){ - e.printStackTrace(); + // e.printStackTrace(); } return outputMap; } From 6a6fa4138812b6fb87c4f6066f5a73f5ead36920 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 14 Feb 2017 15:30:02 +0100 Subject: [PATCH 22/91] Add unit test class for recipe cooler. --- .../java/it/near/sdk/RecipeCoolerTest.java | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 src/test/java/it/near/sdk/RecipeCoolerTest.java diff --git a/src/test/java/it/near/sdk/RecipeCoolerTest.java b/src/test/java/it/near/sdk/RecipeCoolerTest.java new file mode 100644 index 00000000..622a0fd8 --- /dev/null +++ b/src/test/java/it/near/sdk/RecipeCoolerTest.java @@ -0,0 +1,99 @@ +package it.near.sdk; + +import android.content.Context; +import android.content.SharedPreferences; + +import com.google.common.collect.Maps; + +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import it.near.sdk.Recipes.Models.Recipe; +import it.near.sdk.Recipes.RecipeCooler; + +import static it.near.sdk.Recipes.RecipeCooler.*; +import static junit.framework.Assert.*; +import static org.mockito.Mockito.*; + +/** + * Created by cattaneostefano on 14/02/2017. + */ + +@RunWith(MockitoJUnitRunner.class) +public class RecipeCoolerTest { + + @Mock + Context mockContext; + @Mock + SharedPreferences mockSharedPreferences; + @Mock + SharedPreferences.Editor mockEditor; + + Recipe criticalRecipe; + + @Before + public void init() { + + when(mockContext.getSharedPreferences(anyString(), anyInt())).thenReturn(mockSharedPreferences); + when(mockSharedPreferences.edit()).thenReturn(mockEditor); + when(mockEditor.remove(anyString())).thenReturn(mockEditor); + when(mockEditor.commit()).thenReturn(true); + + + criticalRecipe = new Recipe(); + criticalRecipe.setId("recipe_id"); + HashMap cooldown = Maps.newHashMap(); + cooldown.put(GLOBAL_COOLDOWN, 0L); + cooldown.put(SELF_COOLDOWN, 0L); + criticalRecipe.setCooldown(cooldown); + } + + @Before + public void resetSingleton() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { + Field instance = RecipeCooler.class.getDeclaredField("INSTANCE"); + instance.setAccessible(true); + instance.set(null, null); + } + + @Test + public void whenLogIsEmpty_enableRecipe() { + when(mockSharedPreferences.getString(eq(LOG_MAP), anyString())).thenReturn("{}"); + + List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); + assertEquals(1, recipeList.size()); + RecipeCooler.getInstance(mockContext).filterRecipe(recipeList); + assertEquals(1, recipeList.size()); + + } + + @Test + public void whenRecipeShown_historyUpdated() { + + + RecipeCooler.getInstance(mockContext).markRecipeAsShown("recipe_id"); + Map logMap = Maps.newHashMap(); + logMap.put("recipe_id", System.currentTimeMillis()); + JSONObject jsonObject = new JSONObject(logMap); + String jsonString = jsonObject.toString(); + verify(mockEditor).putString(eq(LOG_MAP), contains("recipe_id")); + + } + + @Test + public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { + assertTrue(true); + RecipeCooler.getInstance(mockContext).markRecipeAsShown("recipe_id"); + } + +} From c182dfec4fee6e8d5fd5c977d0860b0146d42afa Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 14 Feb 2017 18:35:21 +0100 Subject: [PATCH 23/91] minor refactor --- src/main/java/it/near/sdk/Recipes/RecipeCooler.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index ae8c1aa3..9475f56b 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -55,7 +55,7 @@ public void markRecipeAsShown(String recipeId){ long timeStamp = System.currentTimeMillis(); getMap().put(recipeId, new Long(timeStamp)); saveMap(mRecipeLogMap); - saveLatestEntry(); + saveLatestEntry(System.currentTimeMillis()); } @@ -130,7 +130,7 @@ private Map loadMap(){ return outputMap; } - private Long getLatestLogEntry(){ + public Long getLatestLogEntry(){ if (mLatestLogEntry == null) { mLatestLogEntry = loadLatestEntry(); } @@ -145,12 +145,13 @@ private Long loadLatestEntry() { return 0L; } - private void saveLatestEntry() { + private void saveLatestEntry(long timestamp) { + mLatestLogEntry = timestamp; SharedPreferences pSharedPref = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); if (pSharedPref!=null){ SharedPreferences.Editor editor = pSharedPref.edit(); editor.remove(LATEST_LOG).commit(); - editor.putLong(LATEST_LOG, System.currentTimeMillis()); + editor.putLong(LATEST_LOG, timestamp); editor.commit(); } } From bf1dc6d1de40fc29586215555346624837b23833 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 14 Feb 2017 18:36:26 +0100 Subject: [PATCH 24/91] recipecooler testing: - refactoring initialisation - more test cases --- .../java/it/near/sdk/RecipeCoolerTest.java | 83 +++++++++++++++---- 1 file changed, 65 insertions(+), 18 deletions(-) diff --git a/src/test/java/it/near/sdk/RecipeCoolerTest.java b/src/test/java/it/near/sdk/RecipeCoolerTest.java index 622a0fd8..2775b9ef 100644 --- a/src/test/java/it/near/sdk/RecipeCoolerTest.java +++ b/src/test/java/it/near/sdk/RecipeCoolerTest.java @@ -7,6 +7,7 @@ import org.json.JSONObject; import org.junit.Before; +import org.junit.BeforeClass; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -40,23 +41,22 @@ public class RecipeCoolerTest { @Mock SharedPreferences.Editor mockEditor; - Recipe criticalRecipe; + private Recipe criticalRecipe; + private Recipe nonCriticalRecipe; @Before - public void init() { + public void initRecipes() { + criticalRecipe = buildRecipe("critical", 0L, 0L); + nonCriticalRecipe = buildRecipe("pedestrian", 24 * 60 * 60L, 24 * 60 * 60L); + } + @Before + public void initMocks() { when(mockContext.getSharedPreferences(anyString(), anyInt())).thenReturn(mockSharedPreferences); when(mockSharedPreferences.edit()).thenReturn(mockEditor); when(mockEditor.remove(anyString())).thenReturn(mockEditor); when(mockEditor.commit()).thenReturn(true); - - criticalRecipe = new Recipe(); - criticalRecipe.setId("recipe_id"); - HashMap cooldown = Maps.newHashMap(); - cooldown.put(GLOBAL_COOLDOWN, 0L); - cooldown.put(SELF_COOLDOWN, 0L); - criticalRecipe.setCooldown(cooldown); } @Before @@ -79,21 +79,68 @@ public void whenLogIsEmpty_enableRecipe() { @Test public void whenRecipeShown_historyUpdated() { + RecipeCooler.getInstance(mockContext).markRecipeAsShown(criticalRecipe.getId()); + verify(mockEditor).putString(eq(LOG_MAP), contains(criticalRecipe.getId())); + } + @Test + public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + recipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); + List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); + recipeCooler.filterRecipe(recipeList); + assertEquals(0, recipeList.size()); + } - RecipeCooler.getInstance(mockContext).markRecipeAsShown("recipe_id"); - Map logMap = Maps.newHashMap(); - logMap.put("recipe_id", System.currentTimeMillis()); - JSONObject jsonObject = new JSONObject(logMap); - String jsonString = jsonObject.toString(); - verify(mockEditor).putString(eq(LOG_MAP), contains("recipe_id")); + @Test + public void whenRecipeHasNoCooldown_canBeShownAgain() { + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); + recipeCooler.filterRecipe(recipeList); + assertEquals(1, recipeList.size()); + recipeList.add(criticalRecipe); + recipeCooler.filterRecipe(recipeList); + assertEquals(2, recipeList.size()); + recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + } + @Test + public void whenRecipeIsShown_globalCooldownApplies() { + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); + recipeCooler.filterRecipe(recipeList); + assertEquals(0, recipeList.size()); + recipeList.add(criticalRecipe); + recipeCooler.filterRecipe(recipeList); + assertEquals(1, recipeList.size()); + recipeList.add(criticalRecipe); + recipeList.add(nonCriticalRecipe); + recipeList.add(nonCriticalRecipe); + recipeCooler.filterRecipe(recipeList); + assertEquals(2, recipeList.size()); } @Test - public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { - assertTrue(true); - RecipeCooler.getInstance(mockContext).markRecipeAsShown("recipe_id"); + public void whenRecipeIsShown_updateLastLogEntry() { + long beforeTimestamp = System.currentTimeMillis(); + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + long afterTimestamp = System.currentTimeMillis(); + long actualTimestamp = recipeCooler.getLatestLogEntry(); + assertTrue(beforeTimestamp <= actualTimestamp); + assertTrue(actualTimestamp <= afterTimestamp); + } + + private Recipe buildRecipe(String id, long globalCD, long selfCD) { + Recipe criticalRecipe = new Recipe(); + criticalRecipe.setId(id); + HashMap cooldown = Maps.newHashMap(); + cooldown.put(GLOBAL_COOLDOWN, globalCD); + cooldown.put(SELF_COOLDOWN, selfCD); + criticalRecipe.setCooldown(cooldown); + return criticalRecipe; } } From 62201cb3b10d4f8cd246a4cba4efcb1545ccd047 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Wed, 15 Feb 2017 11:45:54 +0100 Subject: [PATCH 25/91] DI improvement: inject shared preferences instead of context; healthier testing, but weirder initialisation. --- .../it/near/sdk/Recipes/RecipeCooler.java | 43 ++++++++----------- .../java/it/near/sdk/RecipeCoolerTest.java | 15 +++---- 2 files changed, 23 insertions(+), 35 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index 9475f56b..ea7ed92e 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -2,6 +2,7 @@ import android.content.Context; import android.content.SharedPreferences; +import android.support.annotation.NonNull; import org.json.JSONObject; @@ -9,10 +10,11 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Objects; import it.near.sdk.Recipes.Models.Recipe; +import static com.google.gson.internal.$Gson$Preconditions.checkNotNull; + /** * Created by cattaneostefano on 13/02/2017. */ @@ -20,10 +22,9 @@ public class RecipeCooler { private static RecipeCooler INSTANCE; - private Context mContext; private SharedPreferences mSharedPreferences; - public static final String NEAR_COOLDOWN_HISTORY = "NearCooldownHistory"; + public static final String NEAR_RECIPECOOLER_PREFSNAME = "NearRecipeCoolerPrefsName"; public static final String LOG_MAP = "LOG_MAP"; public static final String LATEST_LOG = "LATEST_LOG"; @@ -33,20 +34,14 @@ public class RecipeCooler { private Map mRecipeLogMap; private Long mLatestLogEntry; - private RecipeCooler(Context context) { - mContext = context; - } - - private SharedPreferences getSharedPreferences(){ - if (mSharedPreferences == null){ - mSharedPreferences = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); - } - return mSharedPreferences; + private RecipeCooler(SharedPreferences sharedPreferences) { + mSharedPreferences = sharedPreferences; } - public static RecipeCooler getInstance(Context context){ + public static RecipeCooler getInstance(@NonNull SharedPreferences sharedPreferences){ + checkNotNull(sharedPreferences); if (INSTANCE == null){ - INSTANCE = new RecipeCooler(context); + INSTANCE = new RecipeCooler(sharedPreferences); } return INSTANCE; } @@ -99,11 +94,10 @@ private Map getMap() { } private void saveMap(Map inputMap){ - SharedPreferences pSharedPref = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); - if (pSharedPref != null){ + if (mSharedPreferences != null){ JSONObject jsonObject = new JSONObject(inputMap); String jsonString = jsonObject.toString(); - SharedPreferences.Editor editor = pSharedPref.edit(); + SharedPreferences.Editor editor = mSharedPreferences.edit(); editor.remove(LOG_MAP).commit(); editor.putString(LOG_MAP, jsonString); editor.commit(); @@ -112,10 +106,9 @@ private void saveMap(Map inputMap){ private Map loadMap(){ Map outputMap = new HashMap(); - SharedPreferences pSharedPref = getSharedPreferences(); try{ - if (pSharedPref != null){ - String jsonString = pSharedPref.getString(LOG_MAP, (new JSONObject()).toString()); + if (mSharedPreferences != null){ + String jsonString = mSharedPreferences.getString(LOG_MAP, (new JSONObject()).toString()); JSONObject jsonObject = new JSONObject(jsonString); Iterator keysItr = jsonObject.keys(); while(keysItr.hasNext()) { @@ -138,18 +131,16 @@ public Long getLatestLogEntry(){ } private Long loadLatestEntry() { - SharedPreferences pSharedPref = getSharedPreferences(); - if (pSharedPref != null) { - return pSharedPref.getLong(LATEST_LOG, 0L); + if (mSharedPreferences != null) { + return mSharedPreferences.getLong(LATEST_LOG, 0L); } return 0L; } private void saveLatestEntry(long timestamp) { mLatestLogEntry = timestamp; - SharedPreferences pSharedPref = mContext.getSharedPreferences(NEAR_COOLDOWN_HISTORY, Context.MODE_PRIVATE); - if (pSharedPref!=null){ - SharedPreferences.Editor editor = pSharedPref.edit(); + if (mSharedPreferences!=null){ + SharedPreferences.Editor editor = mSharedPreferences.edit(); editor.remove(LATEST_LOG).commit(); editor.putLong(LATEST_LOG, timestamp); editor.commit(); diff --git a/src/test/java/it/near/sdk/RecipeCoolerTest.java b/src/test/java/it/near/sdk/RecipeCoolerTest.java index 2775b9ef..d7f0f80f 100644 --- a/src/test/java/it/near/sdk/RecipeCoolerTest.java +++ b/src/test/java/it/near/sdk/RecipeCoolerTest.java @@ -34,8 +34,6 @@ @RunWith(MockitoJUnitRunner.class) public class RecipeCoolerTest { - @Mock - Context mockContext; @Mock SharedPreferences mockSharedPreferences; @Mock @@ -52,7 +50,6 @@ public void initRecipes() { @Before public void initMocks() { - when(mockContext.getSharedPreferences(anyString(), anyInt())).thenReturn(mockSharedPreferences); when(mockSharedPreferences.edit()).thenReturn(mockEditor); when(mockEditor.remove(anyString())).thenReturn(mockEditor); when(mockEditor.commit()).thenReturn(true); @@ -72,20 +69,20 @@ public void whenLogIsEmpty_enableRecipe() { List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); assertEquals(1, recipeList.size()); - RecipeCooler.getInstance(mockContext).filterRecipe(recipeList); + RecipeCooler.getInstance(mockSharedPreferences).filterRecipe(recipeList); assertEquals(1, recipeList.size()); } @Test public void whenRecipeShown_historyUpdated() { - RecipeCooler.getInstance(mockContext).markRecipeAsShown(criticalRecipe.getId()); + RecipeCooler.getInstance(mockSharedPreferences).markRecipeAsShown(criticalRecipe.getId()); verify(mockEditor).putString(eq(LOG_MAP), contains(criticalRecipe.getId())); } @Test public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); recipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); recipeCooler.filterRecipe(recipeList); @@ -94,7 +91,7 @@ public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { @Test public void whenRecipeHasNoCooldown_canBeShownAgain() { - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); recipeCooler.markRecipeAsShown(criticalRecipe.getId()); List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); recipeCooler.filterRecipe(recipeList); @@ -107,7 +104,7 @@ public void whenRecipeHasNoCooldown_canBeShownAgain() { @Test public void whenRecipeIsShown_globalCooldownApplies() { - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); recipeCooler.markRecipeAsShown(criticalRecipe.getId()); List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); recipeCooler.filterRecipe(recipeList); @@ -125,7 +122,7 @@ public void whenRecipeIsShown_globalCooldownApplies() { @Test public void whenRecipeIsShown_updateLastLogEntry() { long beforeTimestamp = System.currentTimeMillis(); - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockContext); + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); recipeCooler.markRecipeAsShown(criticalRecipe.getId()); long afterTimestamp = System.currentTimeMillis(); long actualTimestamp = recipeCooler.getLatestLogEntry(); From c17a40497f9408cb30b9e60beea61e396bbf6ea9 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Wed, 15 Feb 2017 12:31:11 +0100 Subject: [PATCH 26/91] better historyUpdated test for recipe cooler --- src/test/java/it/near/sdk/RecipeCoolerTest.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/test/java/it/near/sdk/RecipeCoolerTest.java b/src/test/java/it/near/sdk/RecipeCoolerTest.java index d7f0f80f..3420ad28 100644 --- a/src/test/java/it/near/sdk/RecipeCoolerTest.java +++ b/src/test/java/it/near/sdk/RecipeCoolerTest.java @@ -25,6 +25,7 @@ import static it.near.sdk.Recipes.RecipeCooler.*; import static junit.framework.Assert.*; +import static org.mockito.AdditionalMatchers.*; import static org.mockito.Mockito.*; /** @@ -76,8 +77,14 @@ public void whenLogIsEmpty_enableRecipe() { @Test public void whenRecipeShown_historyUpdated() { - RecipeCooler.getInstance(mockSharedPreferences).markRecipeAsShown(criticalRecipe.getId()); - verify(mockEditor).putString(eq(LOG_MAP), contains(criticalRecipe.getId())); + RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); + recipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); + verify(mockEditor).putString(eq(LOG_MAP), and(contains(nonCriticalRecipe.getId()), + not(contains(criticalRecipe.getId())))); + recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + verify(mockEditor, atLeastOnce()) + .putString(eq(LOG_MAP), and(contains(nonCriticalRecipe.getId()), + contains(criticalRecipe.getId()))); } @Test From b8eff9a6a2d26a0394de195929776df4b19d1393 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Wed, 15 Feb 2017 13:08:31 +0100 Subject: [PATCH 27/91] As OOP design for testability suggests, refactored useless singleton pattern. Better testability as a result. --- .../it/near/sdk/Recipes/RecipeCooler.java | 12 +---- .../java/it/near/sdk/RecipeCoolerTest.java | 49 +++++++------------ 2 files changed, 20 insertions(+), 41 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index ea7ed92e..98d0feb9 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -21,7 +21,6 @@ public class RecipeCooler { - private static RecipeCooler INSTANCE; private SharedPreferences mSharedPreferences; public static final String NEAR_RECIPECOOLER_PREFSNAME = "NearRecipeCoolerPrefsName"; @@ -34,16 +33,9 @@ public class RecipeCooler { private Map mRecipeLogMap; private Long mLatestLogEntry; - private RecipeCooler(SharedPreferences sharedPreferences) { - mSharedPreferences = sharedPreferences; - } - - public static RecipeCooler getInstance(@NonNull SharedPreferences sharedPreferences){ + public RecipeCooler(@NonNull SharedPreferences sharedPreferences) { checkNotNull(sharedPreferences); - if (INSTANCE == null){ - INSTANCE = new RecipeCooler(sharedPreferences); - } - return INSTANCE; + mSharedPreferences = sharedPreferences; } public void markRecipeAsShown(String recipeId){ diff --git a/src/test/java/it/near/sdk/RecipeCoolerTest.java b/src/test/java/it/near/sdk/RecipeCoolerTest.java index 3420ad28..13dab338 100644 --- a/src/test/java/it/near/sdk/RecipeCoolerTest.java +++ b/src/test/java/it/near/sdk/RecipeCoolerTest.java @@ -42,26 +42,18 @@ public class RecipeCoolerTest { private Recipe criticalRecipe; private Recipe nonCriticalRecipe; + private RecipeCooler mRecipeCooler; @Before - public void initRecipes() { + public void setUp() { criticalRecipe = buildRecipe("critical", 0L, 0L); nonCriticalRecipe = buildRecipe("pedestrian", 24 * 60 * 60L, 24 * 60 * 60L); - } - @Before - public void initMocks() { when(mockSharedPreferences.edit()).thenReturn(mockEditor); when(mockEditor.remove(anyString())).thenReturn(mockEditor); when(mockEditor.commit()).thenReturn(true); - } - - @Before - public void resetSingleton() throws SecurityException, NoSuchFieldException, IllegalArgumentException, IllegalAccessException { - Field instance = RecipeCooler.class.getDeclaredField("INSTANCE"); - instance.setAccessible(true); - instance.set(null, null); + mRecipeCooler = new RecipeCooler(mockSharedPreferences); } @Test @@ -70,18 +62,17 @@ public void whenLogIsEmpty_enableRecipe() { List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); assertEquals(1, recipeList.size()); - RecipeCooler.getInstance(mockSharedPreferences).filterRecipe(recipeList); + mRecipeCooler.filterRecipe(recipeList); assertEquals(1, recipeList.size()); } @Test public void whenRecipeShown_historyUpdated() { - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); - recipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); + mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); verify(mockEditor).putString(eq(LOG_MAP), and(contains(nonCriticalRecipe.getId()), not(contains(criticalRecipe.getId())))); - recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); verify(mockEditor, atLeastOnce()) .putString(eq(LOG_MAP), and(contains(nonCriticalRecipe.getId()), contains(criticalRecipe.getId()))); @@ -89,50 +80,46 @@ public void whenRecipeShown_historyUpdated() { @Test public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); - recipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); + mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); - recipeCooler.filterRecipe(recipeList); + mRecipeCooler.filterRecipe(recipeList); assertEquals(0, recipeList.size()); } @Test public void whenRecipeHasNoCooldown_canBeShownAgain() { - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); - recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); - recipeCooler.filterRecipe(recipeList); + mRecipeCooler.filterRecipe(recipeList); assertEquals(1, recipeList.size()); recipeList.add(criticalRecipe); - recipeCooler.filterRecipe(recipeList); + mRecipeCooler.filterRecipe(recipeList); assertEquals(2, recipeList.size()); - recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); } @Test public void whenRecipeIsShown_globalCooldownApplies() { - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); - recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); - recipeCooler.filterRecipe(recipeList); + mRecipeCooler.filterRecipe(recipeList); assertEquals(0, recipeList.size()); recipeList.add(criticalRecipe); - recipeCooler.filterRecipe(recipeList); + mRecipeCooler.filterRecipe(recipeList); assertEquals(1, recipeList.size()); recipeList.add(criticalRecipe); recipeList.add(nonCriticalRecipe); recipeList.add(nonCriticalRecipe); - recipeCooler.filterRecipe(recipeList); + mRecipeCooler.filterRecipe(recipeList); assertEquals(2, recipeList.size()); } @Test public void whenRecipeIsShown_updateLastLogEntry() { long beforeTimestamp = System.currentTimeMillis(); - RecipeCooler recipeCooler = RecipeCooler.getInstance(mockSharedPreferences); - recipeCooler.markRecipeAsShown(criticalRecipe.getId()); + mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); long afterTimestamp = System.currentTimeMillis(); - long actualTimestamp = recipeCooler.getLatestLogEntry(); + long actualTimestamp = mRecipeCooler.getLatestLogEntry(); assertTrue(beforeTimestamp <= actualTimestamp); assertTrue(actualTimestamp <= afterTimestamp); } From 0de6d81b3d6c207f4ab51d65be07162940ad8f9b Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Wed, 15 Feb 2017 16:12:09 +0100 Subject: [PATCH 28/91] Handle cool down null cases (both on the whole cool down structure and on the single cool down) as a critical scenario. Create tests for those cases. --- .../it/near/sdk/Recipes/RecipeCooler.java | 17 +++---- .../java/it/near/sdk/RecipeCoolerTest.java | 48 +++++++++++++++++-- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index 98d0feb9..ca5c3adc 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -48,22 +48,23 @@ public void markRecipeAsShown(String recipeId){ private boolean canShowRecipe(Recipe recipe){ Map cooldown = recipe.getCooldown(); - return cooldown != null && - globalCooldownCheck(cooldown) && - selfCooldownCheck(recipe, cooldown); + return cooldown == null || + ( globalCooldownCheck(cooldown) && selfCooldownCheck(recipe, cooldown) ); } private boolean globalCooldownCheck(Map cooldown) { - // TODO cosa fare se non c'è? - if (!cooldown.containsKey(GLOBAL_COOLDOWN)) return true; + if (!cooldown.containsKey(GLOBAL_COOLDOWN) || + cooldown.get(GLOBAL_COOLDOWN) == null) return true; + long expiredSeconds = (System.currentTimeMillis() - getLatestLogEntry()) / 1000; return expiredSeconds >= (Long)cooldown.get(GLOBAL_COOLDOWN); } private boolean selfCooldownCheck(Recipe recipe, Map cooldown){ - // TODO cosa fare se non c'è? - if (!cooldown.containsKey(SELF_COOLDOWN)) return true; - if (!getMap().containsKey(recipe.getId())) return true; + if (!cooldown.containsKey(SELF_COOLDOWN) || + cooldown.get(SELF_COOLDOWN) == null || + !getMap().containsKey(recipe.getId())) return true; + long recipeLatestEntry = getMap().get(recipe.getId()); long expiredSeconds = (System.currentTimeMillis() - recipeLatestEntry) / 1000; return expiredSeconds >= (Long)cooldown.get(SELF_COOLDOWN); diff --git a/src/test/java/it/near/sdk/RecipeCoolerTest.java b/src/test/java/it/near/sdk/RecipeCoolerTest.java index 13dab338..a7286532 100644 --- a/src/test/java/it/near/sdk/RecipeCoolerTest.java +++ b/src/test/java/it/near/sdk/RecipeCoolerTest.java @@ -4,6 +4,7 @@ import android.content.SharedPreferences; import com.google.common.collect.Maps; +import com.google.common.collect.ObjectArrays; import org.json.JSONObject; import org.junit.Before; @@ -46,8 +47,9 @@ public class RecipeCoolerTest { @Before public void setUp() { - criticalRecipe = buildRecipe("critical", 0L, 0L); - nonCriticalRecipe = buildRecipe("pedestrian", 24 * 60 * 60L, 24 * 60 * 60L); + criticalRecipe = buildRecipe("critical", buildCooldown(0L, 0L)); + nonCriticalRecipe = buildRecipe("pedestrian", buildCooldown(24 * 60 * 60L, + 24 * 60 * 60L)); when(mockSharedPreferences.edit()).thenReturn(mockEditor); when(mockEditor.remove(anyString())).thenReturn(mockEditor); @@ -124,14 +126,50 @@ public void whenRecipeIsShown_updateLastLogEntry() { assertTrue(actualTimestamp <= afterTimestamp); } - private Recipe buildRecipe(String id, long globalCD, long selfCD) { + @Test + public void whenCooldownMissing_showRecipe() { + mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); + List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); + mRecipeCooler.filterRecipe(recipeList); + assertEquals(0, recipeList.size()); + nonCriticalRecipe.setCooldown(null); + recipeList.add(nonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + assertEquals(1, recipeList.size()); + } + + @Test + public void whenMissingSelfCooldown_considerItZero() { + mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); + // force the check on the selfcooldown + nonCriticalRecipe.setCooldown(buildCooldown(0L, null)); + List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); + mRecipeCooler.filterRecipe(recipeList); + assertEquals(1, recipeList.size()); + } + + @Test + public void whenMissingGlobalCoolDown_considerItZero() { + mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); + // force the check on globalcooldown + nonCriticalRecipe.setCooldown(buildCooldown(null, 0L)); + List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); + mRecipeCooler.filterRecipe(recipeList); + assertEquals(1, recipeList.size()); + } + + private Recipe buildRecipe(String id, HashMap cooldown) { Recipe criticalRecipe = new Recipe(); criticalRecipe.setId(id); + criticalRecipe.setCooldown(cooldown); + return criticalRecipe; + } + + private HashMap buildCooldown(Long globalCD, Long selfCD) { HashMap cooldown = Maps.newHashMap(); cooldown.put(GLOBAL_COOLDOWN, globalCD); cooldown.put(SELF_COOLDOWN, selfCD); - criticalRecipe.setCooldown(cooldown); - return criticalRecipe; + return cooldown; } } From 6048a99335f6d0423809c80ca46a3b5d89abe7cd Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Thu, 16 Feb 2017 15:14:57 +0100 Subject: [PATCH 29/91] new matchers in testing, some comments --- .../java/it/near/sdk/RecipeCoolerTest.java | 175 ---------------- .../it/near/sdk/Recipes/RecipeCoolerTest.java | 197 ++++++++++++++++++ 2 files changed, 197 insertions(+), 175 deletions(-) delete mode 100644 src/test/java/it/near/sdk/RecipeCoolerTest.java create mode 100644 src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java diff --git a/src/test/java/it/near/sdk/RecipeCoolerTest.java b/src/test/java/it/near/sdk/RecipeCoolerTest.java deleted file mode 100644 index a7286532..00000000 --- a/src/test/java/it/near/sdk/RecipeCoolerTest.java +++ /dev/null @@ -1,175 +0,0 @@ -package it.near.sdk; - -import android.content.Context; -import android.content.SharedPreferences; - -import com.google.common.collect.Maps; -import com.google.common.collect.ObjectArrays; - -import org.json.JSONObject; -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; -import org.mockito.junit.MockitoJUnitRunner; - -import java.lang.reflect.Field; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -import it.near.sdk.Recipes.Models.Recipe; -import it.near.sdk.Recipes.RecipeCooler; - -import static it.near.sdk.Recipes.RecipeCooler.*; -import static junit.framework.Assert.*; -import static org.mockito.AdditionalMatchers.*; -import static org.mockito.Mockito.*; - -/** - * Created by cattaneostefano on 14/02/2017. - */ - -@RunWith(MockitoJUnitRunner.class) -public class RecipeCoolerTest { - - @Mock - SharedPreferences mockSharedPreferences; - @Mock - SharedPreferences.Editor mockEditor; - - private Recipe criticalRecipe; - private Recipe nonCriticalRecipe; - private RecipeCooler mRecipeCooler; - - @Before - public void setUp() { - criticalRecipe = buildRecipe("critical", buildCooldown(0L, 0L)); - nonCriticalRecipe = buildRecipe("pedestrian", buildCooldown(24 * 60 * 60L, - 24 * 60 * 60L)); - - when(mockSharedPreferences.edit()).thenReturn(mockEditor); - when(mockEditor.remove(anyString())).thenReturn(mockEditor); - when(mockEditor.commit()).thenReturn(true); - - mRecipeCooler = new RecipeCooler(mockSharedPreferences); - } - - @Test - public void whenLogIsEmpty_enableRecipe() { - when(mockSharedPreferences.getString(eq(LOG_MAP), anyString())).thenReturn("{}"); - - List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); - assertEquals(1, recipeList.size()); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(1, recipeList.size()); - - } - - @Test - public void whenRecipeShown_historyUpdated() { - mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); - verify(mockEditor).putString(eq(LOG_MAP), and(contains(nonCriticalRecipe.getId()), - not(contains(criticalRecipe.getId())))); - mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); - verify(mockEditor, atLeastOnce()) - .putString(eq(LOG_MAP), and(contains(nonCriticalRecipe.getId()), - contains(criticalRecipe.getId()))); - } - - @Test - public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { - mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); - List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(0, recipeList.size()); - } - - @Test - public void whenRecipeHasNoCooldown_canBeShownAgain() { - mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); - List recipeList = new ArrayList(Arrays.asList(criticalRecipe)); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(1, recipeList.size()); - recipeList.add(criticalRecipe); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(2, recipeList.size()); - mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); - } - - @Test - public void whenRecipeIsShown_globalCooldownApplies() { - mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); - List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(0, recipeList.size()); - recipeList.add(criticalRecipe); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(1, recipeList.size()); - recipeList.add(criticalRecipe); - recipeList.add(nonCriticalRecipe); - recipeList.add(nonCriticalRecipe); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(2, recipeList.size()); - } - - @Test - public void whenRecipeIsShown_updateLastLogEntry() { - long beforeTimestamp = System.currentTimeMillis(); - mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); - long afterTimestamp = System.currentTimeMillis(); - long actualTimestamp = mRecipeCooler.getLatestLogEntry(); - assertTrue(beforeTimestamp <= actualTimestamp); - assertTrue(actualTimestamp <= afterTimestamp); - } - - @Test - public void whenCooldownMissing_showRecipe() { - mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); - List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(0, recipeList.size()); - nonCriticalRecipe.setCooldown(null); - recipeList.add(nonCriticalRecipe); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(1, recipeList.size()); - } - - @Test - public void whenMissingSelfCooldown_considerItZero() { - mRecipeCooler.markRecipeAsShown(nonCriticalRecipe.getId()); - // force the check on the selfcooldown - nonCriticalRecipe.setCooldown(buildCooldown(0L, null)); - List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(1, recipeList.size()); - } - - @Test - public void whenMissingGlobalCoolDown_considerItZero() { - mRecipeCooler.markRecipeAsShown(criticalRecipe.getId()); - // force the check on globalcooldown - nonCriticalRecipe.setCooldown(buildCooldown(null, 0L)); - List recipeList = new ArrayList(Arrays.asList(nonCriticalRecipe)); - mRecipeCooler.filterRecipe(recipeList); - assertEquals(1, recipeList.size()); - } - - private Recipe buildRecipe(String id, HashMap cooldown) { - Recipe criticalRecipe = new Recipe(); - criticalRecipe.setId(id); - criticalRecipe.setCooldown(cooldown); - return criticalRecipe; - } - - private HashMap buildCooldown(Long globalCD, Long selfCD) { - HashMap cooldown = Maps.newHashMap(); - cooldown.put(GLOBAL_COOLDOWN, globalCD); - cooldown.put(SELF_COOLDOWN, selfCD); - return cooldown; - } - -} diff --git a/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java b/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java new file mode 100644 index 00000000..ccc46201 --- /dev/null +++ b/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java @@ -0,0 +1,197 @@ +package it.near.sdk.Recipes; + +import android.content.SharedPreferences; + +import com.google.common.collect.Maps; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Matchers; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import it.near.sdk.Recipes.Models.Recipe; +import it.near.sdk.Recipes.RecipeCooler; + +import static com.google.common.collect.Lists.newArrayList; +import static it.near.sdk.Recipes.RecipeCooler.*; +import static junit.framework.Assert.*; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.hasItems; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.mockito.AdditionalMatchers.*; +import static org.mockito.Mockito.*; + +/** + * Created by cattaneostefano on 14/02/2017. + */ + +@RunWith(MockitoJUnitRunner.class) +public class RecipeCoolerTest { + + @Mock + SharedPreferences mMockSharedPreferences; + @Mock + SharedPreferences.Editor mMockEditor; + + private Recipe mCriticalRecipe; + private Recipe mNonCriticalRecipe; + private RecipeCooler mRecipeCooler; + + @Before + public void setUp() { + mCriticalRecipe = buildRecipe("critical", buildCooldown(0L, 0L)); + mNonCriticalRecipe = buildRecipe("pedestrian", buildCooldown(24 * 60 * 60L, + 24 * 60 * 60L)); + + when(mMockSharedPreferences.edit()).thenReturn(mMockEditor); + when(mMockEditor.remove(anyString())).thenReturn(mMockEditor); + when(mMockEditor.commit()).thenReturn(true); + + mRecipeCooler = new RecipeCooler(mMockSharedPreferences); + } + + @Test + public void whenLogIsEmpty_enableRecipe() { + // when we filter recipes with a fresh history + List recipeList = newArrayList(mCriticalRecipe, + mCriticalRecipe, + mNonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // then they all pass the filter + assertEquals(3, recipeList.size()); + assertThat(recipeList, hasItems(mCriticalRecipe, mNonCriticalRecipe)); + + } + + @Test + public void whenRecipeShown_historyUpdated() { + // when we mark a recipe as shown + mRecipeCooler.markRecipeAsShown(mNonCriticalRecipe.getId()); + // then its timestamp is put in history + verify(mMockEditor).putString(eq(LOG_MAP), and(contains(mNonCriticalRecipe.getId()), + not(contains(mCriticalRecipe.getId())))); + // when we mark another recipe as shown + mRecipeCooler.markRecipeAsShown(mCriticalRecipe.getId()); + // then its timestamp is added to history + verify(mMockEditor, atLeastOnce()) + .putString(eq(LOG_MAP), and(contains(mNonCriticalRecipe.getId()), + contains(mCriticalRecipe.getId()))); + } + + @Test + public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { + // when a non critical recipe is shown + mRecipeCooler.markRecipeAsShown(mNonCriticalRecipe.getId()); + List recipeList = newArrayList(mNonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // then it can't be displayed again + assertEquals(0, recipeList.size()); + assertThat(recipeList, org.hamcrest.core.IsNot.not(hasItem(mNonCriticalRecipe))); + } + + @Test + public void whenRecipeHasNoCooldown_canBeShownAgain() { + // when a recipe is shown + mRecipeCooler.markRecipeAsShown(mCriticalRecipe.getId()); + List recipeList = newArrayList(mCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // then a critical recipe can still be shown shortly afterwards + assertEquals(1, recipeList.size()); + recipeList.add(mCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + assertEquals(2, recipeList.size()); + } + + @Test + public void whenRecipeIsShown_globalCooldownApplies() { + // when a recipe is shown + mRecipeCooler.markRecipeAsShown(mCriticalRecipe.getId()); + List recipeList = newArrayList(mNonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // then a non critical recipe won't be shown + assertEquals(0, recipeList.size()); + assertThat(recipeList, org.hamcrest.core.IsNot.not(hasItem(mNonCriticalRecipe))); + recipeList.add(mCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // but a critical recipe will + assertEquals(1, recipeList.size()); + recipeList.add(mCriticalRecipe); + recipeList.add(mNonCriticalRecipe); + recipeList.add(mNonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + assertEquals(2, recipeList.size()); + assertThat(recipeList, hasItem(mCriticalRecipe)); + assertThat(recipeList, org.hamcrest.core.IsNot.not(hasItem(mNonCriticalRecipe))); + } + + @Test + public void whenRecipeIsShown_updateLastLogEntry() { + long beforeTimestamp = System.currentTimeMillis(); + // when we mark a recipe as shown + mRecipeCooler.markRecipeAsShown(mCriticalRecipe.getId()); + long afterTimestamp = System.currentTimeMillis(); + long actualTimestamp = mRecipeCooler.getLatestLogEntry(); + // then the latest log entry is updated + assertTrue(beforeTimestamp <= actualTimestamp); + assertTrue(actualTimestamp <= afterTimestamp); + } + + @Test + public void whenCooldownMissing_showRecipe() { + // when there's a recent entry log + mRecipeCooler.markRecipeAsShown(mNonCriticalRecipe.getId()); + // and a recipe without the cooldown section + mNonCriticalRecipe.setCooldown(null); + List recipeList = newArrayList(mNonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // then it gets treated as a critical recipe + assertEquals(1, recipeList.size()); + assertThat(recipeList, hasItem(mNonCriticalRecipe)); + } + + @Test + public void whenMissingSelfCooldown_considerItZero() { + // when a recipe has no selfcooldown + mNonCriticalRecipe.setCooldown(buildCooldown(0L, null)); + mRecipeCooler.markRecipeAsShown(mNonCriticalRecipe.getId()); + List recipeList = newArrayList(mNonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // then it gets treaded as critical + assertEquals(1, recipeList.size()); + assertThat(recipeList, hasItem(mNonCriticalRecipe)); + } + + @Test + public void whenMissingGlobalCoolDown_considerItZero() { + // when a recipe has no globalcooldown + mRecipeCooler.markRecipeAsShown(mCriticalRecipe.getId()); + mNonCriticalRecipe.setCooldown(buildCooldown(null, 0L)); + List recipeList = newArrayList(mNonCriticalRecipe); + mRecipeCooler.filterRecipe(recipeList); + // then its get treaded as critical + assertEquals(1, recipeList.size()); + assertThat(recipeList, hasItem(mNonCriticalRecipe)); + } + + private Recipe buildRecipe(String id, HashMap cooldown) { + Recipe criticalRecipe = new Recipe(); + criticalRecipe.setId(id); + criticalRecipe.setCooldown(cooldown); + return criticalRecipe; + } + + private HashMap buildCooldown(Long globalCD, Long selfCD) { + HashMap cooldown = Maps.newHashMap(); + cooldown.put(GLOBAL_COOLDOWN, globalCD); + cooldown.put(SELF_COOLDOWN, selfCD); + return cooldown; + } + +} From 2d179917842970f40a7994124b3c2c3b828f6936 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Thu, 16 Feb 2017 15:24:34 +0100 Subject: [PATCH 30/91] recipe cooler filter pipelined in recipe evaluation for trigger defined as pulse triple. Still missing from online evaluation, refactor needed --- src/main/java/it/near/sdk/Recipes/RecipesManager.java | 9 +++++++++ src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipesManager.java b/src/main/java/it/near/sdk/Recipes/RecipesManager.java index db4ffb09..c662b86a 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipesManager.java +++ b/src/main/java/it/near/sdk/Recipes/RecipesManager.java @@ -56,6 +56,7 @@ public class RecipesManager { private HashMap reactions = new HashMap<>(); SharedPreferences.Editor editor; private NearAsyncHttpClient httpClient; + private RecipeCooler mRecipeCooler; public RecipesManager(Context context) { this.mContext = context; @@ -72,6 +73,7 @@ public RecipesManager(Context context) { } setUpMorpheusParser(); refreshConfig(); + setUpRecipeCooler(); } /** @@ -90,6 +92,11 @@ private void setUpMorpheusParser() { morpheus.getFactory().getDeserializer().registerResourceClass("reaction_bundles", ReactionBundle.class); } + private void setUpRecipeCooler() { + SharedPreferences recipeCoolerSP = mContext.getSharedPreferences(RecipeCooler.NEAR_RECIPECOOLER_PREFSNAME,0); + mRecipeCooler = new RecipeCooler(recipeCoolerSP); + } + public void addReaction(Reaction reaction){ reactions.put(reaction.getPluginName(), reaction); } @@ -217,6 +224,8 @@ public void gotPulse(String pulse_plugin, String pulse_action, String pulse_bund } } + mRecipeCooler.filterRecipe(validRecipes); + if (validRecipes.isEmpty()){ // if no recipe is found the the online fallback onlinePulseEvaluation(pulse_plugin, pulse_action, pulse_bundle); diff --git a/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java b/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java index ccc46201..82e89985 100644 --- a/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java +++ b/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java @@ -179,7 +179,7 @@ public void whenMissingGlobalCoolDown_considerItZero() { assertEquals(1, recipeList.size()); assertThat(recipeList, hasItem(mNonCriticalRecipe)); } - + private Recipe buildRecipe(String id, HashMap cooldown) { Recipe criticalRecipe = new Recipe(); criticalRecipe.setId(id); From 56ce8a1d223e04de1c1b5f337c68843a5daa50db Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Fri, 17 Feb 2017 11:41:25 +0100 Subject: [PATCH 31/91] dependency as a final field --- src/main/java/it/near/sdk/Recipes/RecipeCooler.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index ca5c3adc..f2df17b7 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -21,7 +21,7 @@ public class RecipeCooler { - private SharedPreferences mSharedPreferences; + private final SharedPreferences mSharedPreferences; public static final String NEAR_RECIPECOOLER_PREFSNAME = "NearRecipeCoolerPrefsName"; public static final String LOG_MAP = "LOG_MAP"; @@ -34,8 +34,7 @@ public class RecipeCooler { private Long mLatestLogEntry; public RecipeCooler(@NonNull SharedPreferences sharedPreferences) { - checkNotNull(sharedPreferences); - mSharedPreferences = sharedPreferences; + mSharedPreferences = checkNotNull(sharedPreferences);; } public void markRecipeAsShown(String recipeId){ From 2f8a102f64ca47bd283c0df5ba2517f3726f5980 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Fri, 17 Feb 2017 19:24:26 +0100 Subject: [PATCH 32/91] setter for scheduling block --- src/main/java/it/near/sdk/Recipes/Models/Recipe.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java index 7b90360c..50757bb7 100644 --- a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java +++ b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java @@ -178,6 +178,10 @@ public HashMap getCooldown() { return cooldown; } + public void setScheduling(HashMap scheduling) { + this.scheduling = scheduling; + } + public void setCooldown(HashMap cooldown) { this.cooldown = cooldown; } From 4ef7dcc8d84f7b64b6cedb407c31aaa0e730054c Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 20 Feb 2017 15:35:59 +0100 Subject: [PATCH 33/91] Recipe time-based scheduling refinement. plus more tests --- .../it/near/sdk/Recipes/Models/Recipe.java | 11 +- .../java/it/near/sdk/ExampleUnitTest.java | 15 -- .../it/near/sdk/Recipes/Model/RecipeTest.java | 131 ++++++++++++++++-- 3 files changed, 125 insertions(+), 32 deletions(-) delete mode 100644 src/test/java/it/near/sdk/ExampleUnitTest.java diff --git a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java index 50757bb7..f897a46d 100644 --- a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java +++ b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java @@ -171,9 +171,6 @@ public void setReaction_action(ReactionAction reaction_action) { this.reaction_action = reaction_action; } -public void setScheduling(HashMap scheduling) { - this.scheduling = scheduling; - } public HashMap getCooldown() { return cooldown; } @@ -292,11 +289,11 @@ private boolean isDateValid(Calendar now){ } /** - * Check it the time range is valid. + * Check if the time range is valid. * @return if the time range is respected. */ private boolean isTimetableValid(Calendar now) { - Map timetable = (LinkedTreeMap) scheduling.get("timetable"); + Map timetable = (Map) scheduling.get("timetable"); if (timetable == null) return true; String fromHour = (String) timetable.get("from"); String toHour = (String) timetable.get("to"); @@ -307,13 +304,13 @@ private boolean isTimetableValid(Calendar now) { Date fromHourDate = timeFormatter.parse(fromHour); Calendar fromHourCalendar = Calendar.getInstance(); fromHourCalendar.setTime(fromHourDate); - valid &= fromHourCalendar.before(now); + valid &= fromHourCalendar.before(now) || fromHourCalendar.equals(now); } if (toHour != null){ Date toHourDate = timeFormatter.parse(toHour); Calendar toHourCalendar = Calendar.getInstance(); toHourCalendar.setTime(toHourDate); - valid &= toHourCalendar.after(now); + valid &= toHourCalendar.after(now) || toHourCalendar.equals(now); } } catch (ParseException e) { e.printStackTrace(); diff --git a/src/test/java/it/near/sdk/ExampleUnitTest.java b/src/test/java/it/near/sdk/ExampleUnitTest.java deleted file mode 100644 index a358b16a..00000000 --- a/src/test/java/it/near/sdk/ExampleUnitTest.java +++ /dev/null @@ -1,15 +0,0 @@ -package it.near.sdk; - -import org.junit.Test; - -import static org.junit.Assert.*; - -/** - * To work on unit tests, switch the Test Artifact in the Build Variants view. - */ -public class ExampleUnitTest { - @Test - public void addition_isCorrect() throws Exception { - assertEquals(4, 2 + 2); - } -} \ No newline at end of file diff --git a/src/test/java/it/near/sdk/Recipes/Model/RecipeTest.java b/src/test/java/it/near/sdk/Recipes/Model/RecipeTest.java index dd678036..c42f43ba 100644 --- a/src/test/java/it/near/sdk/Recipes/Model/RecipeTest.java +++ b/src/test/java/it/near/sdk/Recipes/Model/RecipeTest.java @@ -2,11 +2,13 @@ import android.content.Context; import android.content.res.Resources; +import android.support.annotation.Nullable; import com.google.common.collect.ImmutableMap; import com.google.common.collect.Maps; import org.joda.time.DateTime; +import org.joda.time.LocalTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.DateTimeFormatterBuilder; @@ -15,10 +17,13 @@ import org.junit.Test; import java.io.InputStream; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CancellationException; import static junit.framework.Assert.*; import static org.mockito.ArgumentMatchers.anyInt; @@ -61,7 +66,7 @@ public void whenSchedulingIsForThisMonth_theRecipeIsValid() { // when a recipe is scheduled for the month of january 2017 DateTime startPeriod = new DateTime(2017, 1, 1, 0, 0, 0).withTimeAtStartOfDay(); DateTime endPeriod = new DateTime(2017, 1, 31, 0, 0, 0).withTimeAtStartOfDay(); - HashMap scheduling = buildSchedulingBlockForDate(startPeriod, endPeriod); + HashMap scheduling = buildScheduling(startPeriod, endPeriod, null, null); mRecipe.setScheduling(scheduling); Calendar lowerBound = buildCalendarFrom(startPeriod); // then it is valid on the start date @@ -79,7 +84,7 @@ public void whenNotScheduledForToday_theRecipeIsNotValid() { // when a recipe is scheduled for the month of january 2017 DateTime startPeriod = new DateTime(2017, 1, 1, 0, 0, 0).withTimeAtStartOfDay(); DateTime endPeriod = new DateTime(2017, 1, 31, 0, 0, 0).withTimeAtStartOfDay(); - HashMap scheduling = buildSchedulingBlockForDate(startPeriod, endPeriod); + HashMap scheduling = buildScheduling(startPeriod, endPeriod, null, null); mRecipe.setScheduling(scheduling); Calendar dayBefore = buildCalendarFrom(startPeriod.minusDays(1)); // then it is not valid the day before the start @@ -95,22 +100,128 @@ public void whenNotScheduledForToday_theRecipeIsNotValid() { assertFalse(mRecipe.isScheduledNow(yearBefore)); } - private HashMap buildSchedulingBlockForDate(DateTime start, DateTime end){ - DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd"); - Map date = Maps.newLinkedHashMap( - ImmutableMap.builder(). - put("from", start.toString(formatter)). - put("to", end.toString(formatter)). - build()); + @Test + public void whenRecipeOnlyHasLowerBound_thenValidyIsChecked() { + // when a recipe is scheduled from february 1st 2017 + DateTime startDate = new DateTime(2017, 2, 1, 0, 0, 0).withTimeAtStartOfDay(); + HashMap scheduling = buildScheduling(startDate, null, null, null); + mRecipe.setScheduling(scheduling); + + Calendar thatDay = buildCalendarFrom(startDate); + Calendar monthLater = buildCalendarFrom(startDate.plusMonths(1)); + Calendar yearLater = buildCalendarFrom(startDate.plusYears(1)); + // then is valid from that day on + assertTrue(mRecipe.isScheduledNow(thatDay)); + assertTrue(mRecipe.isScheduledNow(monthLater)); + assertTrue(mRecipe.isScheduledNow(yearLater)); + Calendar dayBefore = buildCalendarFrom(startDate.minusDays(1)); + Calendar monthBefore = buildCalendarFrom(startDate.minusMonths(1)); + Calendar yearBefore = buildCalendarFrom(startDate.minusYears(1)); + // then is not valid before that day + assertFalse(mRecipe.isScheduledNow(dayBefore)); + assertFalse(mRecipe.isScheduledNow(monthBefore)); + assertFalse(mRecipe.isScheduledNow(yearBefore)); + } + + @Test + public void whenRecipeOnlyHasUpperBound_thenValidityIsChecked() { + // when a recipe is scheduled until january 31st 2017 + DateTime endDate = new DateTime(2017, 1, 31, 0, 0, 0).withTimeAtStartOfDay(); + HashMap scheduling = buildScheduling(null, endDate, null, null); + mRecipe.setScheduling(scheduling); + + Calendar thatDay = buildCalendarFrom(endDate); + Calendar monthBefore = buildCalendarFrom(endDate.minusMonths(1)); + Calendar yearBefore = buildCalendarFrom(endDate.minusYears(1)); + // then is valid up until that day + assertTrue(mRecipe.isScheduledNow(thatDay)); + assertTrue(mRecipe.isScheduledNow(monthBefore)); + assertTrue(mRecipe.isScheduledNow(yearBefore)); + Calendar nextDay = buildCalendarFrom(endDate.plusDays(1)); + Calendar nextMonth = buildCalendarFrom(endDate.plusMonths(1)); + Calendar nextYear = buildCalendarFrom(endDate.plusYears(1)); + assertFalse(mRecipe.isScheduledNow(nextDay)); + assertFalse(mRecipe.isScheduledNow(nextMonth)); + assertFalse(mRecipe.isScheduledNow(nextYear)); + } + + @Test + public void whenRecipeIsScheduledATimeOfDay_isValidityChecked() throws ParseException { + // when a recipe is scheduled for this time of day + LocalTime startTime = new LocalTime(8, 0, 0); + LocalTime endTime = new LocalTime(20, 0, 0); + HashMap scheduling = buildScheduling(null, null, startTime, endTime); + mRecipe.setScheduling(scheduling); + + Calendar atTheStart = buildCalendarFrom(startTime); + Calendar atTheEnd = buildCalendarFrom(endTime); + Calendar inTheMiddle =buildCalendarFrom(startTime.plusHours(3)); + // then is valid during the period + assertTrue(mRecipe.isScheduledNow(atTheStart)); + assertTrue(mRecipe.isScheduledNow(atTheEnd)); + assertTrue(mRecipe.isScheduledNow(inTheMiddle)); + + Calendar justBefore = buildCalendarFrom(startTime.minusMillis(1)); + Calendar justAfter = buildCalendarFrom(endTime.plusMinutes(1)); + Calendar before = buildCalendarFrom(startTime.minusHours(2)); + Calendar after = buildCalendarFrom(endTime.plusHours(2)); + // then is not valid outside of the period + assertFalse(mRecipe.isScheduledNow(justBefore)); + assertFalse(mRecipe.isScheduledNow(justAfter)); + assertFalse(mRecipe.isScheduledNow(before)); + assertFalse(mRecipe.isScheduledNow(after)); + } + + @Test + public void whenRecipeIsNotScheduledForThisTimeOfDay_isNotValid() throws ParseException { + // when a recipe is not scheduled for this time of day + LocalTime startTime = new LocalTime(15, 0, 0); + LocalTime endTime = new LocalTime(18, 0, 0); + HashMap scheduling = buildScheduling(null, null, startTime, endTime); + mRecipe.setScheduling(scheduling); + + + } + + private HashMap buildScheduling(DateTime startDate, DateTime endDate, + LocalTime startTime, LocalTime endTime){ HashMap scheduling = Maps.newHashMap(); - scheduling.put("date", date); + if (startDate != null || endDate != null) + scheduling.put("date", buildSchedulingBlockForDate(startDate, endDate)); + if (startTime != null || endTime != null) + scheduling.put("timetable", buildSchedulingBlockForTimeOfDay(startTime, endTime)); return scheduling; } + private Map buildSchedulingBlockForDate(@Nullable DateTime startDate, @Nullable DateTime endDate){ + DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd"); + Map date = Maps.newLinkedHashMap(); + if (startDate != null) date.put("from", startDate.toString(formatter)); + if (endDate != null) date.put("to", endDate.toString(formatter)); + return date; + } + + private Map buildSchedulingBlockForTimeOfDay(@Nullable LocalTime startTime, @Nullable LocalTime endTime) { + DateTimeFormatter fmt = DateTimeFormat.forPattern("HH:mm:ss"); + Map timetable = Maps.newLinkedHashMap(); + if (startTime != null) timetable.put("from", startTime.toString(fmt)); + if (endTime != null) timetable.put("to", endTime.toString(fmt)); + return timetable; + } + + private Calendar buildCalendarFrom(DateTime dateTime){ Calendar calendar = Calendar.getInstance(); calendar.setTime(dateTime.toDate()); return calendar; } + private Calendar buildCalendarFrom(LocalTime localTime) throws ParseException { + SimpleDateFormat timeFormatter = new SimpleDateFormat("HH:mm:ss"); + Date fromHourDate = timeFormatter.parse(localTime.toString()); + Calendar calendar = Calendar.getInstance(); + calendar.setTime(fromHourDate); + return calendar; + } + } From 4309aca012c8c3433a06247e2b73f4418bee6bd2 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Thu, 23 Feb 2017 12:30:29 +0100 Subject: [PATCH 34/91] play services and firebase version bump --- build.gradle | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index 756a3d7d..7d2d4b6f 100644 --- a/build.gradle +++ b/build.gradle @@ -145,9 +145,9 @@ dependencies { compile 'com.loopj.android:android-async-http:1.4.9' compile 'com.google.code.gson:gson:2.6.2' compile 'org.altbeacon:android-beacon-library:2.9.1' - compile 'com.google.firebase:firebase-messaging:10.0.1' - compile 'com.google.firebase:firebase-core:10.0.1' - compile 'com.google.android.gms:play-services-location:10.0.1' + compile 'com.google.firebase:firebase-messaging:10.2.0' + compile 'com.google.firebase:firebase-core:10.2.0' + compile 'com.google.android.gms:play-services-location:10.2.0' testCompile 'junit:junit:4.12' testCompile "org.mockito:mockito-core:2.7.6" From 65ff9d294d7db70fa7ff9f5b7071eb1a59c0ae40 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 27 Feb 2017 09:52:39 +0100 Subject: [PATCH 35/91] endpoints --- src/main/res/values/paths.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/res/values/paths.xml b/src/main/res/values/paths.xml index 0ed9ef7e..aac1d75e 100644 --- a/src/main/res/values/paths.xml +++ b/src/main/res/values/paths.xml @@ -1,5 +1,5 @@ - https://api.nearit.com/ + https://dev-api.nearit.com/ \ No newline at end of file From 8f7f299c3d0ac3771f30f5ced0cfee1d357c8f49 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 27 Feb 2017 14:35:47 +0100 Subject: [PATCH 36/91] upgrade dependency to full hamcrest library --- build.gradle | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/build.gradle b/build.gradle index 7d2d4b6f..34cf5d5d 100644 --- a/build.gradle +++ b/build.gradle @@ -137,7 +137,6 @@ task uploadDocs { } } - dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') compile 'com.android.support:support-annotations:24.2.1' @@ -152,7 +151,7 @@ dependencies { testCompile 'junit:junit:4.12' testCompile "org.mockito:mockito-core:2.7.6" testCompile "com.google.guava:guava:21.0" - testCompile 'org.hamcrest:hamcrest-core:1.3' + testCompile 'org.hamcrest:hamcrest-library:1.3' testCompile 'org.json:json:20160212' testCompile 'joda-time:joda-time:2.9.3' From 241ac5c85c25033da35d55ab16c5d99f76920fcc Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 27 Feb 2017 14:37:12 +0100 Subject: [PATCH 37/91] remove unnecessary boxing --- src/main/java/it/near/sdk/Recipes/RecipeCooler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index f2df17b7..e932519c 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -39,7 +39,7 @@ public RecipeCooler(@NonNull SharedPreferences sharedPreferences) { public void markRecipeAsShown(String recipeId){ long timeStamp = System.currentTimeMillis(); - getMap().put(recipeId, new Long(timeStamp)); + getMap().put(recipeId, timeStamp); saveMap(mRecipeLogMap); saveLatestEntry(System.currentTimeMillis()); From a0ce26b1225126c90a13b36c8f55b953f8fd2525 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 27 Feb 2017 14:37:43 +0100 Subject: [PATCH 38/91] add initial trivial test for parser, test model --- .../Recipes/MorpheusNear/MorpheusTest.java | 101 ++++++++++++++++++ .../sdk/Recipes/MorpheusNear/TestModel.java | 21 ++++ 2 files changed, 122 insertions(+) create mode 100644 src/test/java/it/near/sdk/Recipes/MorpheusNear/MorpheusTest.java create mode 100644 src/test/java/it/near/sdk/Recipes/MorpheusNear/TestModel.java diff --git a/src/test/java/it/near/sdk/Recipes/MorpheusNear/MorpheusTest.java b/src/test/java/it/near/sdk/Recipes/MorpheusNear/MorpheusTest.java new file mode 100644 index 00000000..d889b02c --- /dev/null +++ b/src/test/java/it/near/sdk/Recipes/MorpheusNear/MorpheusTest.java @@ -0,0 +1,101 @@ +package it.near.sdk.Recipes.MorpheusNear; + +import android.os.Build; +import android.support.annotation.RequiresApi; + +import com.google.gson.Gson; +import com.google.gson.stream.JsonReader; + +import org.hamcrest.beans.HasProperty; +import org.hamcrest.core.IsInstanceOf; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.util.List; + +import it.near.sdk.MorpheusNear.Morpheus; +import it.near.sdk.Recipes.Models.Recipe; +import it.near.sdk.Recipes.Utils; +import it.near.sdk.Utils.NearJsonAPIUtils; + +import static junit.framework.Assert.*; +import static org.hamcrest.CoreMatchers.allOf; +import static org.hamcrest.CoreMatchers.containsString; +import static org.hamcrest.CoreMatchers.everyItem; +import static org.hamcrest.CoreMatchers.hasItem; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.CoreMatchers.notNullValue; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.core.Is.is; +import static org.mockito.AdditionalMatchers.not; + +/** + * Created by cattaneostefano on 27/02/2017. + */ + +public class MorpheusTest { + + Morpheus morpheus; + + @Before + public void setUP(){ + morpheus = new Morpheus(); + morpheus.getFactory().getDeserializer().registerResourceClass("test", TestModel.class); + } + + @Test + public void parsingElement() throws JSONException { + JSONObject jsonObject = new JSONObject("{\n" + + " \"data\": {\n" + + " \"type\": \"test\",\n" + + " \"id\": \"1\",\n" + + " \"attributes\": {\n" + + " \"content\" : \"contenuto\"\n" + + " }\n" + + " }\n" + + "}"); + TestModel object = NearJsonAPIUtils.parseElement(morpheus, jsonObject, TestModel.class); + assertNotNull(object); + assertEquals("1", object.getId()); + assertEquals("contenuto", object.getContent()); + + } + + @Test + public void parsingList() throws JSONException { + JSONObject jsonObject = new JSONObject("{\n" + + " \"data\" : [{\n" + + " \"type\" : \"test\",\n" + + " \"id\" : 1,\n" + + " \"attributes\" : {\n" + + " \"content\" : \"contenuto\"\n" + + " }\n" + + " }, {\n" + + " \"type\" : \"test\",\n" + + " \"id\" : 2,\n" + + " \"attributes\" : {\n" + + " \"content\" : \"contenuto2\"\n" + + " }\n" + + " } ]\n" + + "}"); + List objectList = NearJsonAPIUtils.parseList(morpheus, jsonObject, TestModel.class); + assertNotNull(objectList); + assertThat(objectList, hasSize(2)); + assertThat(objectList, everyItem( IsInstanceOf.instanceOf(TestModel.class))); + } + + + +} diff --git a/src/test/java/it/near/sdk/Recipes/MorpheusNear/TestModel.java b/src/test/java/it/near/sdk/Recipes/MorpheusNear/TestModel.java new file mode 100644 index 00000000..06debbc4 --- /dev/null +++ b/src/test/java/it/near/sdk/Recipes/MorpheusNear/TestModel.java @@ -0,0 +1,21 @@ +package it.near.sdk.Recipes.MorpheusNear; + +import com.google.gson.annotations.SerializedName; + +import it.near.sdk.MorpheusNear.Resource; + +/** + * Created by cattaneostefano on 27/02/2017. + */ + +public class TestModel extends Resource { + @SerializedName("content") + String content; + + public TestModel() { + } + + public String getContent() { + return content; + } +} From 65765fd40c4917d6141697153dae4e58e9bbd59e Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 27 Feb 2017 15:57:08 +0100 Subject: [PATCH 39/91] move test files in appropriate package --- .../sdk/{Recipes => }/MorpheusNear/MorpheusTest.java | 10 +++++++--- .../near/sdk/{Recipes => }/MorpheusNear/TestModel.java | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) rename src/test/java/it/near/sdk/{Recipes => }/MorpheusNear/MorpheusTest.java (91%) rename src/test/java/it/near/sdk/{Recipes => }/MorpheusNear/TestModel.java (89%) diff --git a/src/test/java/it/near/sdk/Recipes/MorpheusNear/MorpheusTest.java b/src/test/java/it/near/sdk/MorpheusNear/MorpheusTest.java similarity index 91% rename from src/test/java/it/near/sdk/Recipes/MorpheusNear/MorpheusTest.java rename to src/test/java/it/near/sdk/MorpheusNear/MorpheusTest.java index d889b02c..40bbd8b2 100644 --- a/src/test/java/it/near/sdk/Recipes/MorpheusNear/MorpheusTest.java +++ b/src/test/java/it/near/sdk/MorpheusNear/MorpheusTest.java @@ -1,4 +1,4 @@ -package it.near.sdk.Recipes.MorpheusNear; +package it.near.sdk.MorpheusNear; import android.os.Build; import android.support.annotation.RequiresApi; @@ -6,6 +6,8 @@ import com.google.gson.Gson; import com.google.gson.stream.JsonReader; +import org.hamcrest.CoreMatchers; +import org.hamcrest.Matchers; import org.hamcrest.beans.HasProperty; import org.hamcrest.core.IsInstanceOf; import org.json.JSONException; @@ -25,7 +27,6 @@ import it.near.sdk.MorpheusNear.Morpheus; import it.near.sdk.Recipes.Models.Recipe; -import it.near.sdk.Recipes.Utils; import it.near.sdk.Utils.NearJsonAPIUtils; import static junit.framework.Assert.*; @@ -36,10 +37,11 @@ import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.notNullValue; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.hasProperty; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.not; import static org.hamcrest.core.Is.is; -import static org.mockito.AdditionalMatchers.not; /** * Created by cattaneostefano on 27/02/2017. @@ -68,6 +70,7 @@ public void parsingElement() throws JSONException { "}"); TestModel object = NearJsonAPIUtils.parseElement(morpheus, jsonObject, TestModel.class); assertNotNull(object); + assertThat(object, instanceOf(TestModel.class)); assertEquals("1", object.getId()); assertEquals("contenuto", object.getContent()); @@ -92,6 +95,7 @@ public void parsingList() throws JSONException { "}"); List objectList = NearJsonAPIUtils.parseList(morpheus, jsonObject, TestModel.class); assertNotNull(objectList); + assertThat(objectList, not(empty())); assertThat(objectList, hasSize(2)); assertThat(objectList, everyItem( IsInstanceOf.instanceOf(TestModel.class))); } diff --git a/src/test/java/it/near/sdk/Recipes/MorpheusNear/TestModel.java b/src/test/java/it/near/sdk/MorpheusNear/TestModel.java similarity index 89% rename from src/test/java/it/near/sdk/Recipes/MorpheusNear/TestModel.java rename to src/test/java/it/near/sdk/MorpheusNear/TestModel.java index 06debbc4..08d149b5 100644 --- a/src/test/java/it/near/sdk/Recipes/MorpheusNear/TestModel.java +++ b/src/test/java/it/near/sdk/MorpheusNear/TestModel.java @@ -1,4 +1,4 @@ -package it.near.sdk.Recipes.MorpheusNear; +package it.near.sdk.MorpheusNear; import com.google.gson.annotations.SerializedName; From 62fd888369b685505dc2825c46e5f7580167aa44 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 27 Feb 2017 15:57:34 +0100 Subject: [PATCH 40/91] Add test for near json utils --- .../near/sdk/Utils/NearJsonAPIUtilsTest.java | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 src/test/java/it/near/sdk/Utils/NearJsonAPIUtilsTest.java diff --git a/src/test/java/it/near/sdk/Utils/NearJsonAPIUtilsTest.java b/src/test/java/it/near/sdk/Utils/NearJsonAPIUtilsTest.java new file mode 100644 index 00000000..55578c9e --- /dev/null +++ b/src/test/java/it/near/sdk/Utils/NearJsonAPIUtilsTest.java @@ -0,0 +1,106 @@ +package it.near.sdk.Utils; + +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Test; + +import java.util.HashMap; +import java.util.List; + +import static junit.framework.Assert.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.hasEntry; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; + +/** + * Created by cattaneostefano on 27/02/2017. + */ + +public class NearJsonAPIUtilsTest { + + @Test + public void toJsonApiSingleElement() throws JSONException { + HashMap attributes = Maps.newHashMap(); + attributes.put("name", "bonetti"); + attributes.put("color", "black"); + String type = "goalkeeper"; + String jsonApiOutput = NearJsonAPIUtils.toJsonAPI(type, attributes); + assertThat(jsonApiOutput, is(notNullValue())); + JSONObject actualJson = new JSONObject(jsonApiOutput); + assertThat(actualJson.has("data"), is(true)); + assertThat(actualJson.has("attributes"), is(false)); + actualJson = actualJson.getJSONObject("data"); + assertThat(actualJson, is(notNullValue())); + assertThat(actualJson.getString("type"), is(type)); + assertThat(actualJson.has("attributes"), is(true)); + assertThat(actualJson.has("id"), is(false)); + actualJson = actualJson.getJSONObject("attributes"); + assertThat(actualJson, is(notNullValue())); + assertThat(actualJson.getString("name"), is("bonetti")); + assertThat(actualJson.getString("color"), is("black")); + } + + @Test + public void toJsonApiMultiElement() throws JSONException { + HashMap firstMap = Maps.newHashMap(); + firstMap.put("name", "bonetti"); + firstMap.put("color", "black"); + HashMap secondMap = Maps.newHashMap(); + secondMap.put("name", "neuer"); + secondMap.put("color", "white"); + List> mapList = Lists.newArrayList(firstMap, secondMap); + String type = "goalkeeper"; + String jsonApiOutput = NearJsonAPIUtils.toJsonAPI(type, mapList); + assertThat(jsonApiOutput, is(notNullValue())); + JSONObject actualJson = new JSONObject(jsonApiOutput); + assertThat(actualJson.has("data"), is(true)); + JSONArray actualArray = actualJson.getJSONArray("data"); + assertThat(actualArray.length(), is(2)); + actualJson = actualArray.getJSONObject(0); + assertThat(actualJson, is(notNullValue())); + assertThat(actualJson.getString("type"), is(type)); + assertThat(actualJson.has("attributes"), is(true)); + actualJson = actualJson.getJSONObject("attributes"); + assertThat(actualJson.getString("name"), is("bonetti")); + assertThat(actualJson.getString("color"), is("black")); + actualJson = actualArray.getJSONObject(1); + assertThat(actualJson.getString("type"), is(type)); + assertThat(actualJson.has("attributes"), is(true)); + actualJson = actualJson.getJSONObject("attributes"); + assertThat(actualJson.getString("name"), is("neuer")); + assertThat(actualJson.getString("color"), is("white")); + + } + + @Test + public void toJsonApiWithId() throws JSONException { + HashMap attributes = Maps.newHashMap(); + attributes.put("name", "bonetti"); + attributes.put("color", "black"); + String type = "goalkeeper"; + String id = "1"; + String jsonApiOutput = NearJsonAPIUtils.toJsonAPI(type, id, attributes); + assertThat(jsonApiOutput, is(notNullValue())); + JSONObject actualJson = new JSONObject(jsonApiOutput); + assertThat(actualJson.has("data"), is(true)); + assertThat(actualJson.has("attributes"), is(false)); + actualJson = actualJson.getJSONObject("data"); + assertThat(actualJson, is(notNullValue())); + assertThat(actualJson.getString("type"), is(type)); + assertThat(actualJson.getString("id"), is("1")); + assertThat(actualJson.has("attributes"), is(true)); + actualJson = actualJson.getJSONObject("attributes"); + assertThat(actualJson, is(notNullValue())); + assertThat(actualJson.getString("name"), is("bonetti")); + assertThat(actualJson.getString("color"), is("black")); + } + +} From 75afa7acd5008424da2d5445e82f8978f95c79f3 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 27 Feb 2017 16:40:35 +0100 Subject: [PATCH 41/91] Remove random newlines --- src/main/java/it/near/sdk/Recipes/RecipeCooler.java | 1 - src/main/java/it/near/sdk/Recipes/RecipesManager.java | 5 ----- src/main/java/it/near/sdk/Utils/NearJsonAPIUtils.java | 4 ---- 3 files changed, 10 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index e932519c..9a5e5cbd 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -42,7 +42,6 @@ public void markRecipeAsShown(String recipeId){ getMap().put(recipeId, timeStamp); saveMap(mRecipeLogMap); saveLatestEntry(System.currentTimeMillis()); - } private boolean canShowRecipe(Recipe recipe){ diff --git a/src/main/java/it/near/sdk/Recipes/RecipesManager.java b/src/main/java/it/near/sdk/Recipes/RecipesManager.java index c662b86a..81281d7d 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipesManager.java +++ b/src/main/java/it/near/sdk/Recipes/RecipesManager.java @@ -177,7 +177,6 @@ public void onFailureUnique(int statusCode, Header[] headers, Throwable throwabl e.printStackTrace(); listener.onRecipesRefreshFail(-1); } - } private void persistList(List recipes) { @@ -238,8 +237,6 @@ public void gotPulse(String pulse_plugin, String pulse_action, String pulse_bund gotRecipe(winnerRecipe); } } - - } /** @@ -264,7 +261,6 @@ public void processRecipe(final String id) { .appendQueryParameter("filter[core][profile_id]", GlobalConfig.getInstance(mContext).getProfileId()) .appendQueryParameter("include", "reaction_bundle") .build(); - try { httpClient.nearGet(mContext, url.toString(), new NearJsonHttpResponseHandler(){ @@ -335,7 +331,6 @@ public void onFailureUnique(int statusCode, Header[] headers, Throwable throwabl } catch (NullPointerException e){ e.printStackTrace(); } - } /** diff --git a/src/main/java/it/near/sdk/Utils/NearJsonAPIUtils.java b/src/main/java/it/near/sdk/Utils/NearJsonAPIUtils.java index 1b027dc8..96fb0e92 100644 --- a/src/main/java/it/near/sdk/Utils/NearJsonAPIUtils.java +++ b/src/main/java/it/near/sdk/Utils/NearJsonAPIUtils.java @@ -57,10 +57,8 @@ public static String toJsonAPI(String type, List> maps) */ public static String toJsonAPI(String type, String id, HashMap map) throws JSONException { JSONObject dataObject = getResObj(type, id, map); - JSONObject outerObj = new JSONObject(); outerObj.put("data", dataObject); - return outerObj.toString(); } @@ -109,13 +107,11 @@ public static List parseList(Morpheus morpheus, JSONObject json, Class } List returnList = new ArrayList(); - if (jsonApiObject.getResources() == null) return returnList; for (Resource r : jsonApiObject.getResources()){ returnList.add((T) r); } - return returnList; } From 287676a26475ec78882ffe72ed5aa18851611952 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 27 Feb 2017 17:33:39 +0100 Subject: [PATCH 42/91] Remove whitespace --- src/main/java/it/near/sdk/Communication/NearNetworkUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/it/near/sdk/Communication/NearNetworkUtil.java b/src/main/java/it/near/sdk/Communication/NearNetworkUtil.java index 2fa61128..139f5a04 100644 --- a/src/main/java/it/near/sdk/Communication/NearNetworkUtil.java +++ b/src/main/java/it/near/sdk/Communication/NearNetworkUtil.java @@ -55,7 +55,7 @@ public void onFailureUnique(int statusCode, Header[] headers, Throwable throwabl * @param body the HHTP request body. * @param handler the response handler. */ - public static void sendTrack (Context context, String url, String body, NearJsonHttpResponseHandler handler) throws UnsupportedEncodingException, AuthenticationException { + public static void sendTrack(Context context, String url, String body, NearJsonHttpResponseHandler handler) throws UnsupportedEncodingException, AuthenticationException { NearAsyncHttpClient httpClient = new NearAsyncHttpClient(); httpClient.nearPost(context, url, body, handler); } From b8e29f74a9aedde164ff000f460874a5b59ec056 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Mon, 27 Feb 2017 17:55:26 +0100 Subject: [PATCH 43/91] remove unused file, create geopolis trackings manager with initial set of dependencies --- .../near/sdk/Geopolis/TestRegionCrafter.java | 44 ------------------- .../Trackings/GeopolisTrackingsManager.java | 23 ++++++++++ 2 files changed, 23 insertions(+), 44 deletions(-) delete mode 100644 src/main/java/it/near/sdk/Geopolis/TestRegionCrafter.java create mode 100644 src/main/java/it/near/sdk/Geopolis/Trackings/GeopolisTrackingsManager.java diff --git a/src/main/java/it/near/sdk/Geopolis/TestRegionCrafter.java b/src/main/java/it/near/sdk/Geopolis/TestRegionCrafter.java deleted file mode 100644 index e3e9bac9..00000000 --- a/src/main/java/it/near/sdk/Geopolis/TestRegionCrafter.java +++ /dev/null @@ -1,44 +0,0 @@ -package it.near.sdk.Geopolis; - -import android.content.Context; - -import org.altbeacon.beacon.Identifier; -import org.altbeacon.beacon.Region; - -import java.util.ArrayList; -import java.util.Arrays; - -import it.near.sdk.R; - -/** - * Craft a list of region from the test_beacon resource file - * @author cattaneostefano - */ -public class TestRegionCrafter { - - /** - * Return the test region crafted from the test file - * @param context - * @return - */ - public static ArrayList getTestRegions(Context context){ - - /*ArrayList testRegions = new ArrayList<>(); - testRegions.add(new Region("Region" , Identifier.parse("ACFD065E-C3C0-11E3-9BBE-1A514932AC01"), null, null)); - return testRegions;*/ - - ArrayList testRegions = new ArrayList<>(); - String[] unparsedStrings = context.getResources().getStringArray(R.array.test_beacons); - ArrayList list = new ArrayList(Arrays.asList(unparsedStrings)); - for (String beaconString : list){ - String[] identifiers = beaconString.split(";"); - Region testRegion; - int major = Integer.parseInt(identifiers[1]); - int minor = Integer.parseInt(identifiers[2]); - String uniqueId = "Region" + Integer.toString(major) + Integer.toString(minor); - testRegion = new Region(uniqueId, Identifier.parse(identifiers[0]), Identifier.fromInt(major), Identifier.fromInt(minor) ); - testRegions.add(testRegion); - } - return testRegions; - } -} diff --git a/src/main/java/it/near/sdk/Geopolis/Trackings/GeopolisTrackingsManager.java b/src/main/java/it/near/sdk/Geopolis/Trackings/GeopolisTrackingsManager.java new file mode 100644 index 00000000..710451c4 --- /dev/null +++ b/src/main/java/it/near/sdk/Geopolis/Trackings/GeopolisTrackingsManager.java @@ -0,0 +1,23 @@ +package it.near.sdk.Geopolis.Trackings; + +import android.content.SharedPreferences; + +import it.near.sdk.Communication.NearAsyncHttpClient; + +/** + * Created by cattaneostefano on 27/02/2017. + */ + +public class GeopolisTrackingsManager { + + private final NearAsyncHttpClient nearAsyncHttpClient; + private final SharedPreferences sp; + + public GeopolisTrackingsManager(NearAsyncHttpClient nearAsyncHttpClient, SharedPreferences sp) { + this.nearAsyncHttpClient = nearAsyncHttpClient; + this.sp = sp; + } + + + +} From 016faf86d2fd013c568f9f741225ea0b03ce4be3 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 28 Feb 2017 10:47:08 +0100 Subject: [PATCH 44/91] null check in DI --- .../Geopolis/Trackings/GeopolisTrackingsManager.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main/java/it/near/sdk/Geopolis/Trackings/GeopolisTrackingsManager.java b/src/main/java/it/near/sdk/Geopolis/Trackings/GeopolisTrackingsManager.java index 710451c4..33c4931b 100644 --- a/src/main/java/it/near/sdk/Geopolis/Trackings/GeopolisTrackingsManager.java +++ b/src/main/java/it/near/sdk/Geopolis/Trackings/GeopolisTrackingsManager.java @@ -1,9 +1,12 @@ package it.near.sdk.Geopolis.Trackings; import android.content.SharedPreferences; +import android.support.annotation.NonNull; import it.near.sdk.Communication.NearAsyncHttpClient; +import static com.google.gson.internal.$Gson$Preconditions.checkNotNull; + /** * Created by cattaneostefano on 27/02/2017. */ @@ -13,11 +16,10 @@ public class GeopolisTrackingsManager { private final NearAsyncHttpClient nearAsyncHttpClient; private final SharedPreferences sp; - public GeopolisTrackingsManager(NearAsyncHttpClient nearAsyncHttpClient, SharedPreferences sp) { - this.nearAsyncHttpClient = nearAsyncHttpClient; - this.sp = sp; + public GeopolisTrackingsManager(@NonNull NearAsyncHttpClient nearAsyncHttpClient, + @NonNull SharedPreferences sp) { + this.nearAsyncHttpClient = checkNotNull(nearAsyncHttpClient); + this.sp = checkNotNull(sp); } - - } From 59baa3c385077723aa3813de86fab2a1d361a8c3 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 28 Feb 2017 10:47:34 +0100 Subject: [PATCH 45/91] refactor strings to constants --- .../it/near/sdk/Recipes/Models/Recipe.java | 40 ++++--------------- 1 file changed, 8 insertions(+), 32 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java index f897a46d..0285601e 100644 --- a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java +++ b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java @@ -55,18 +55,16 @@ public class Recipe extends Resource { ReactionBundle reaction_bundle; @Relationship("reaction_action") ReactionAction reaction_action; - /*@SerializedName("operation_plugin_id") - String operation_plugin_id; - @SerializedName("operation_bundle_id") - String operation_bundle_id;*/ - /*@Relationship("operation_action") - OperationAction operation_action;*/ - private static final String ONLINE = "online"; private static final String TRACKINGS_PATH = "trackings"; public static final String NOTIFIED_STATUS = "notified"; public static final String ENGAGED_STATUS = "engaged"; + private static final String ONLINE = "online"; + public static final String DATE_SCHEDULING = "date"; + public static final String TIMETABLE_SCHEDULING = "timetable"; + public static final String DAYS_SCHEDULING = "days"; + public String getName() { return name; } @@ -115,21 +113,6 @@ public void setPulse_bundle(PulseBundle pulse_bundle) { this.pulse_bundle = pulse_bundle; } - /*public String getOperation_plugin_id() { - return operation_plugin_id; - } - - public void setOperation_plugin_id(String operation_plugin_id) { - this.operation_plugin_id = operation_plugin_id; - } - - public String getOperation_bundle_id() { - return operation_bundle_id; - } - - public void setOperation_bundle_id(String operation_bundle_id) { - this.operation_bundle_id = operation_bundle_id; - }*/ public String getReaction_plugin_id() { return reaction_plugin_id; @@ -155,13 +138,6 @@ public void setPulse_action(PulseAction pulse_action) { this.pulse_action = pulse_action; } - /*public OperationAction getOperation_action() { - return operation_action; - } - - public void setOperation_action(OperationAction operation_action) { - this.operation_action = operation_action; - }*/ public ReactionAction getReaction_action() { return reaction_action; @@ -260,7 +236,7 @@ public boolean isScheduledNow(Calendar now){ * @return if the date range is respected. */ private boolean isDateValid(Calendar now){ - Map date = (Map) scheduling.get("date"); + Map date = (Map) scheduling.get(DATE_SCHEDULING); if (date == null) return true; String fromDateString = (String) date.get("from"); String toDateString = (String) date.get("to"); @@ -293,7 +269,7 @@ private boolean isDateValid(Calendar now){ * @return if the time range is respected. */ private boolean isTimetableValid(Calendar now) { - Map timetable = (Map) scheduling.get("timetable"); + Map timetable = (Map) scheduling.get(TIMETABLE_SCHEDULING); if (timetable == null) return true; String fromHour = (String) timetable.get("from"); String toHour = (String) timetable.get("to"); @@ -324,7 +300,7 @@ private boolean isTimetableValid(Calendar now) { * @return if the days selection is respected. */ private boolean isDaysValid(Calendar now) { - List days = (List) scheduling.get("days"); + List days = (List) scheduling.get(DAYS_SCHEDULING); if (days == null) return true; String todaysDate = getTodaysDate(now); From e24f20b618156df8e999fcb18dbdd5195b24c927 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 28 Feb 2017 11:17:14 +0100 Subject: [PATCH 46/91] DI for sharedpreferences in NodesManager --- .../it/near/sdk/Geopolis/GeopolisManager.java | 6 ++---- .../java/it/near/sdk/Geopolis/NodesManager.java | 15 +++++++-------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/main/java/it/near/sdk/Geopolis/GeopolisManager.java b/src/main/java/it/near/sdk/Geopolis/GeopolisManager.java index ab8ef012..56049c18 100644 --- a/src/main/java/it/near/sdk/Geopolis/GeopolisManager.java +++ b/src/main/java/it/near/sdk/Geopolis/GeopolisManager.java @@ -87,7 +87,8 @@ public class GeopolisManager { public GeopolisManager(Application application, RecipesManager recipesManager) { this.mApplication = application; this.recipesManager = recipesManager; - this.nodesManager = new NodesManager(application); + SharedPreferences nodesManSP = application.getSharedPreferences(NodesManager.NODES_MANAGER_PREF_NAME, 0); + this.nodesManager = new NodesManager(nodesManSP); this.altBeaconMonitor = new AltBeaconMonitor(application, nodesManager); this.geofenceMonitor = new GeoFenceMonitor(application); @@ -123,9 +124,6 @@ private void registerResetReceiver() { mApplication.registerReceiver(resetEventReceiver, resetFilter); } - - - /** * Refresh the configuration of the component. The list of beacons to altBeaconMonitor will be downloaded from the APIs. * If there's an error in refreshing the configuration, a cached version will be used instead. diff --git a/src/main/java/it/near/sdk/Geopolis/NodesManager.java b/src/main/java/it/near/sdk/Geopolis/NodesManager.java index efe73e82..b08ec788 100644 --- a/src/main/java/it/near/sdk/Geopolis/NodesManager.java +++ b/src/main/java/it/near/sdk/Geopolis/NodesManager.java @@ -2,6 +2,7 @@ import android.content.Context; import android.content.SharedPreferences; +import android.support.annotation.NonNull; import org.json.JSONException; import org.json.JSONObject; @@ -13,6 +14,8 @@ import it.near.sdk.MorpheusNear.Morpheus; import it.near.sdk.Utils.NearJsonAPIUtils; +import static com.google.gson.internal.$Gson$Preconditions.checkNotNull; + /** * Manages the geopolis node structure. * Created by cattaneostefano on 10/10/2016. @@ -22,18 +25,14 @@ public class NodesManager { private static final String PREFS_SUFFIX = "NodesManager"; private static final String NODES_CONFIG = "nodes_config"; + public static final String NODES_MANAGER_PREF_NAME = "NearNodesManager"; private List nodes; - private Context mContext; private Morpheus morpheus; - private SharedPreferences sp; + private final SharedPreferences sp; - public NodesManager(Context mContext) { - this.mContext = mContext; + public NodesManager(@NonNull SharedPreferences sp) { + this.sp = checkNotNull(sp); setUpMorpheusParser(); - - String PACK_NAME = mContext.getApplicationContext().getPackageName(); - String PREFS_NAME = PACK_NAME + PREFS_SUFFIX; - sp = mContext.getSharedPreferences(PREFS_NAME, 0); } /** From 668ec7c60b0e90e849691afaa8a18b8390aedbcb Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 28 Feb 2017 11:17:45 +0100 Subject: [PATCH 47/91] remove useless object instantiation --- src/main/java/it/near/sdk/NearItManager.java | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/src/main/java/it/near/sdk/NearItManager.java b/src/main/java/it/near/sdk/NearItManager.java index 473ec1c2..2574bf7b 100644 --- a/src/main/java/it/near/sdk/NearItManager.java +++ b/src/main/java/it/near/sdk/NearItManager.java @@ -88,7 +88,6 @@ public class NearItManager { */ public NearItManager(final Application application, String apiKey) { this.application = application; - initLifecycleMonitor(); GlobalConfig.getInstance(application).setApiKey(apiKey); GlobalConfig.getInstance(application).setAppId(NearUtils.fetchAppIdFrom(apiKey)); @@ -186,20 +185,6 @@ public static boolean verifyBluetooth(Context context) throws RuntimeException{ return BeaconManager.getInstanceForApplication(context.getApplicationContext()).checkAvailability(); } - private void initLifecycleMonitor() { - new AppLifecycleMonitor(application, new OnLifecycleEventListener() { - @Override - public void onForeground() { - ULog.d(TAG, "onForeground" ); - } - - @Override - public void onBackground() { - ULog.d(TAG, "onBackground"); - } - }); - } - /** * Set a notification image. Refer to the Android guidelines to determine the best image for a notification * @param imgRes the resource int of the image From d2e4780ba51e03b13c928949e6b107467b22a53d Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 28 Feb 2017 12:25:43 +0100 Subject: [PATCH 48/91] Remove unused object --- .../it/near/sdk/Geopolis/GeopolisManager.java | 2 + src/main/java/it/near/sdk/NearItManager.java | 37 ------------------- .../it/near/sdk/Utils/NearSimpleLogger.java | 8 ---- 3 files changed, 2 insertions(+), 45 deletions(-) delete mode 100644 src/main/java/it/near/sdk/Utils/NearSimpleLogger.java diff --git a/src/main/java/it/near/sdk/Geopolis/GeopolisManager.java b/src/main/java/it/near/sdk/Geopolis/GeopolisManager.java index 56049c18..145c5b57 100644 --- a/src/main/java/it/near/sdk/Geopolis/GeopolisManager.java +++ b/src/main/java/it/near/sdk/Geopolis/GeopolisManager.java @@ -87,8 +87,10 @@ public class GeopolisManager { public GeopolisManager(Application application, RecipesManager recipesManager) { this.mApplication = application; this.recipesManager = recipesManager; + SharedPreferences nodesManSP = application.getSharedPreferences(NodesManager.NODES_MANAGER_PREF_NAME, 0); this.nodesManager = new NodesManager(nodesManSP); + this.altBeaconMonitor = new AltBeaconMonitor(application, nodesManager); this.geofenceMonitor = new GeoFenceMonitor(application); diff --git a/src/main/java/it/near/sdk/NearItManager.java b/src/main/java/it/near/sdk/NearItManager.java index 2574bf7b..d1b9b5f0 100644 --- a/src/main/java/it/near/sdk/NearItManager.java +++ b/src/main/java/it/near/sdk/NearItManager.java @@ -39,7 +39,6 @@ import it.near.sdk.Recipes.RecipesManager; import it.near.sdk.Utils.AppLifecycleMonitor; import it.near.sdk.Utils.NearItIntentConstants; -import it.near.sdk.Utils.NearSimpleLogger; import it.near.sdk.Utils.NearUtils; import it.near.sdk.Utils.OnLifecycleEventListener; import it.near.sdk.Utils.ULog; @@ -73,7 +72,6 @@ public class NearItManager { private CustomJSONReaction customJSONReaction; private FeedbackReaction feedbackReaction; private PushManager pushManager; - private NearSimpleLogger logger; private List proximityListenerList = new ArrayList<>(); private AltBeaconMonitor monitor; @@ -109,29 +107,11 @@ public void onProfileCreationError(String error) { } }); - registerLogReceiver(); - pushManager = new PushManager(application); GlobalState.getInstance(application).setPushManager(pushManager); } - /** - * Set logger for beacon distance information. - * @param logger logs beacon data. - */ - public void setLogger(NearSimpleLogger logger) { - this.logger = logger; - } - - /** - * Remove the beacon logger. - */ - public void removeLogger() { - this.logger = null; - } - - private void plugInSetup() { recipesManager = new RecipesManager(application); GlobalState.getInstance(application).setRecipesManager(recipesManager); @@ -158,13 +138,6 @@ private void plugInSetup() { } - - private void registerLogReceiver() { - String filter = application.getPackageName() + "log"; - IntentFilter intentFilter = new IntentFilter(filter); - application.registerReceiver(logReceiver, intentFilter); - } - /** * Return the recipes manager * @return the recipes manager @@ -273,16 +246,6 @@ public boolean sendEvent(Event event){ return false; } - private BroadcastReceiver logReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - String log = intent.getStringExtra("log"); - if (logger != null){ - logger.log(log); - } - } - }; - /** * Return a list of coupon claimed by the user and that are currently valid. * @param listener a listener for success or failure. If there are no coupons available the success method will be called with a null paramaeter. diff --git a/src/main/java/it/near/sdk/Utils/NearSimpleLogger.java b/src/main/java/it/near/sdk/Utils/NearSimpleLogger.java deleted file mode 100644 index e5da4001..00000000 --- a/src/main/java/it/near/sdk/Utils/NearSimpleLogger.java +++ /dev/null @@ -1,8 +0,0 @@ -package it.near.sdk.Utils; - -/** - * @author cattaneostefano. - */ -public interface NearSimpleLogger { - void log(String logLine); -} From e922a7c74d711dd8ab0b7e2fc9be3acdacfc1753 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 28 Feb 2017 18:00:49 +0100 Subject: [PATCH 49/91] dependencies and instrumentation test runner for android instrumented tests --- build.gradle | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/build.gradle b/build.gradle index f1fa9a9b..7079fbc8 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,7 @@ android { versionName version buildConfigField("int", "API_VERSION", "2") multiDexEnabled true + testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { @@ -155,6 +156,12 @@ dependencies { testCompile 'org.json:json:20160212' testCompile 'joda-time:joda-time:2.9.3' + androidTestCompile 'junit:junit:4.12' + androidTestCompile 'org.hamcrest:hamcrest-library:1.3' + androidTestCompile 'com.android.support.test:runner:0.5' + // using the latest java7-compatible guava version + androidTestCompile "com.google.guava:guava:20.0" + javadocDeps 'com.android.support:support-annotations:24.0.0' javadocDeps 'org.altbeacon:android-beacon-library:2.9.2' From 6b7f4c9c2547b0368084d9a3eeddef64faa6ca33 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 28 Feb 2017 18:01:51 +0100 Subject: [PATCH 50/91] Add type to Content creator from parcel --- src/main/java/it/near/sdk/Reactions/Content/Content.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/it/near/sdk/Reactions/Content/Content.java b/src/main/java/it/near/sdk/Reactions/Content/Content.java index f64f234b..52a0bd27 100644 --- a/src/main/java/it/near/sdk/Reactions/Content/Content.java +++ b/src/main/java/it/near/sdk/Reactions/Content/Content.java @@ -95,7 +95,7 @@ public void writeToParcel(Parcel dest, int flags) { } // Creator - public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { public Content createFromParcel(Parcel in) { return new Content(in); } From 61f980b2ee1c943e4b4e5d7d4866eebdc63e572e Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 28 Feb 2017 18:02:30 +0100 Subject: [PATCH 51/91] add generated equals and hash code methods to model classes --- .../near/sdk/Reactions/Content/ImageSet.java | 21 +++++++++++++++++ .../it/near/sdk/Reactions/Coupon/Claim.java | 23 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/main/java/it/near/sdk/Reactions/Content/ImageSet.java b/src/main/java/it/near/sdk/Reactions/Content/ImageSet.java index dfebc0a1..5cce6e48 100644 --- a/src/main/java/it/near/sdk/Reactions/Content/ImageSet.java +++ b/src/main/java/it/near/sdk/Reactions/Content/ImageSet.java @@ -56,4 +56,25 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeString(fullSize); dest.writeString(smallSize); } + + // generated + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ImageSet imageSet = (ImageSet) o; + + if (fullSize != null ? !fullSize.equals(imageSet.fullSize) : imageSet.fullSize != null) + return false; + return smallSize != null ? smallSize.equals(imageSet.smallSize) : imageSet.smallSize == null; + + } + + @Override + public int hashCode() { + int result = fullSize != null ? fullSize.hashCode() : 0; + result = 31 * result + (smallSize != null ? smallSize.hashCode() : 0); + return result; + } } diff --git a/src/main/java/it/near/sdk/Reactions/Coupon/Claim.java b/src/main/java/it/near/sdk/Reactions/Coupon/Claim.java index da533071..c394294e 100644 --- a/src/main/java/it/near/sdk/Reactions/Coupon/Claim.java +++ b/src/main/java/it/near/sdk/Reactions/Coupon/Claim.java @@ -87,4 +87,27 @@ protected Claim(Parcel in) { claimed_at = in.readString(); redeemed_at = in.readString(); } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Claim claim = (Claim) o; + + if (serial_number != null ? !serial_number.equals(claim.serial_number) : claim.serial_number != null) + return false; + if (claimed_at != null ? !claimed_at.equals(claim.claimed_at) : claim.claimed_at != null) + return false; + return redeemed_at != null ? redeemed_at.equals(claim.redeemed_at) : claim.redeemed_at == null; + + } + + @Override + public int hashCode() { + int result = serial_number != null ? serial_number.hashCode() : 0; + result = 31 * result + (claimed_at != null ? claimed_at.hashCode() : 0); + result = 31 * result + (redeemed_at != null ? redeemed_at.hashCode() : 0); + return result; + } } From 0fc94babc19fd44ae8323a020181eef998826bb6 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 28 Feb 2017 18:03:12 +0100 Subject: [PATCH 52/91] simplify string extra access --- src/main/java/it/near/sdk/Utils/NearUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/it/near/sdk/Utils/NearUtils.java b/src/main/java/it/near/sdk/Utils/NearUtils.java index 2951e3bf..dc7014f9 100644 --- a/src/main/java/it/near/sdk/Utils/NearUtils.java +++ b/src/main/java/it/near/sdk/Utils/NearUtils.java @@ -94,7 +94,7 @@ private static String substringBetween(String str, String open, String close) { * @return true if the content was recognized as core and passed to a callback method, false if it wasn't. */ public static boolean parseCoreContents(Intent intent, CoreContentsListener listener) { - String reaction_plugin = intent.getExtras().getString(NearItIntentConstants.REACTION_PLUGIN); + String reaction_plugin = intent.getStringExtra(NearItIntentConstants.REACTION_PLUGIN); String recipeId = intent.getStringExtra(NearItIntentConstants.RECIPE_ID); if (!intent.hasExtra(NearItIntentConstants.CONTENT)) return false; From 0deda4ac507951ac34c62371c84bddf99590ac7b Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 28 Feb 2017 18:04:13 +0100 Subject: [PATCH 53/91] add hamcrest sugar to list assertion --- .../it/near/sdk/Recipes/RecipeCoolerTest.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java b/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java index 82e89985..eea4b464 100644 --- a/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java +++ b/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java @@ -25,6 +25,7 @@ import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; import static org.mockito.AdditionalMatchers.*; import static org.mockito.Mockito.*; @@ -65,9 +66,8 @@ public void whenLogIsEmpty_enableRecipe() { mNonCriticalRecipe); mRecipeCooler.filterRecipe(recipeList); // then they all pass the filter - assertEquals(3, recipeList.size()); + assertThat(recipeList, hasSize(3)); assertThat(recipeList, hasItems(mCriticalRecipe, mNonCriticalRecipe)); - } @Test @@ -92,7 +92,7 @@ public void whenRecipeWithSelfCooldownShown_cantBeShownAgain() { List recipeList = newArrayList(mNonCriticalRecipe); mRecipeCooler.filterRecipe(recipeList); // then it can't be displayed again - assertEquals(0, recipeList.size()); + assertThat(recipeList, hasSize(0)); assertThat(recipeList, org.hamcrest.core.IsNot.not(hasItem(mNonCriticalRecipe))); } @@ -103,10 +103,10 @@ public void whenRecipeHasNoCooldown_canBeShownAgain() { List recipeList = newArrayList(mCriticalRecipe); mRecipeCooler.filterRecipe(recipeList); // then a critical recipe can still be shown shortly afterwards - assertEquals(1, recipeList.size()); + assertThat(recipeList, hasSize(1)); recipeList.add(mCriticalRecipe); mRecipeCooler.filterRecipe(recipeList); - assertEquals(2, recipeList.size()); + assertThat(recipeList, hasSize(2)); } @Test @@ -116,17 +116,17 @@ public void whenRecipeIsShown_globalCooldownApplies() { List recipeList = newArrayList(mNonCriticalRecipe); mRecipeCooler.filterRecipe(recipeList); // then a non critical recipe won't be shown - assertEquals(0, recipeList.size()); + assertThat(recipeList, hasSize(0)); assertThat(recipeList, org.hamcrest.core.IsNot.not(hasItem(mNonCriticalRecipe))); recipeList.add(mCriticalRecipe); mRecipeCooler.filterRecipe(recipeList); // but a critical recipe will - assertEquals(1, recipeList.size()); + assertThat(recipeList, hasSize(1)); recipeList.add(mCriticalRecipe); recipeList.add(mNonCriticalRecipe); recipeList.add(mNonCriticalRecipe); mRecipeCooler.filterRecipe(recipeList); - assertEquals(2, recipeList.size()); + assertThat(recipeList, hasSize(2)); assertThat(recipeList, hasItem(mCriticalRecipe)); assertThat(recipeList, org.hamcrest.core.IsNot.not(hasItem(mNonCriticalRecipe))); } @@ -152,7 +152,7 @@ public void whenCooldownMissing_showRecipe() { List recipeList = newArrayList(mNonCriticalRecipe); mRecipeCooler.filterRecipe(recipeList); // then it gets treated as a critical recipe - assertEquals(1, recipeList.size()); + assertThat(recipeList, hasSize(1)); assertThat(recipeList, hasItem(mNonCriticalRecipe)); } @@ -164,7 +164,7 @@ public void whenMissingSelfCooldown_considerItZero() { List recipeList = newArrayList(mNonCriticalRecipe); mRecipeCooler.filterRecipe(recipeList); // then it gets treaded as critical - assertEquals(1, recipeList.size()); + assertThat(recipeList, hasSize(1)); assertThat(recipeList, hasItem(mNonCriticalRecipe)); } @@ -176,7 +176,7 @@ public void whenMissingGlobalCoolDown_considerItZero() { List recipeList = newArrayList(mNonCriticalRecipe); mRecipeCooler.filterRecipe(recipeList); // then its get treaded as critical - assertEquals(1, recipeList.size()); + assertThat(recipeList, hasSize(1)); assertThat(recipeList, hasItem(mNonCriticalRecipe)); } From ce8f6addf81f60cbc592f86f84c98db1471b5016 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 28 Feb 2017 18:05:27 +0100 Subject: [PATCH 54/91] add unit test for utils parser: - all content types and unsupported type --- .../java/it/near/sdk/Utils/NearUtilsTest.java | 102 ++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/test/java/it/near/sdk/Utils/NearUtilsTest.java diff --git a/src/test/java/it/near/sdk/Utils/NearUtilsTest.java b/src/test/java/it/near/sdk/Utils/NearUtilsTest.java new file mode 100644 index 00000000..4e589b64 --- /dev/null +++ b/src/test/java/it/near/sdk/Utils/NearUtilsTest.java @@ -0,0 +1,102 @@ +package it.near.sdk.Utils; + +import android.content.Intent; +import android.os.Parcelable; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import it.near.sdk.Reactions.Content.Content; +import it.near.sdk.Reactions.Content.ContentReaction; +import it.near.sdk.Reactions.Coupon.Coupon; +import it.near.sdk.Reactions.Coupon.CouponReaction; +import it.near.sdk.Reactions.CustomJSON.CustomJSON; +import it.near.sdk.Reactions.CustomJSON.CustomJSONReaction; +import it.near.sdk.Reactions.Feedback.Feedback; +import it.near.sdk.Reactions.Feedback.FeedbackReaction; +import it.near.sdk.Reactions.SimpleNotification.SimpleNotification; +import it.near.sdk.Reactions.SimpleNotification.SimpleNotificationReaction; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + + +/** + * Created by cattaneostefano on 28/02/2017. + */ + +@RunWith(MockitoJUnitRunner.class) +public class NearUtilsTest { + + @Mock + private Intent mockIntent; + + @Mock + CoreContentsListener mockListener; + + @Test + public void parseContent_ofTypeContent(){ + Content content = new Content(); + configMockFor(ContentReaction.PLUGIN_NAME, content); + NearUtils.parseCoreContents(mockIntent, mockListener); + verify(mockListener).gotContentNotification(eq(mockIntent), eq(content), anyString()); + } + + @Test + public void parseContent_ofTypeSimpleContent() { + SimpleNotification simpleNotification = new SimpleNotification("",""); + configMockFor(SimpleNotificationReaction.PLUGIN_NAME, simpleNotification); + NearUtils.parseCoreContents(mockIntent, mockListener); + verify(mockListener).gotSimpleNotification(eq(mockIntent), eq(simpleNotification), anyString()); + } + + @Test + public void parseContent_ofTypeCoupon() { + Coupon coupon = new Coupon(); + configMockFor(CouponReaction.PLUGIN_NAME, coupon); + NearUtils.parseCoreContents(mockIntent, mockListener); + verify(mockListener).gotCouponNotification(eq(mockIntent), eq(coupon), anyString()); + } + + @Test + public void parseContent_ofTypeCustomJSON() { + CustomJSON customJson = new CustomJSON(); + configMockFor(CustomJSONReaction.PLUGIN_NAME, customJson); + NearUtils.parseCoreContents(mockIntent, mockListener); + verify(mockListener).gotCustomJSONNotification(eq(mockIntent), eq(customJson), anyString()); + } + + @Test + public void parseContent_ofTypeFeedback() { + Feedback feedback = new Feedback(); + configMockFor(FeedbackReaction.PLUGIN_NAME, feedback); + NearUtils.parseCoreContents(mockIntent, mockListener); + verify(mockListener).gotFeedbackNotification(eq(mockIntent), eq(feedback), anyString()); + } + + @Test + public void parseContent_ofUnsupportedType() { + Parcelable p = mock(Parcelable.class); + configMockFor("Unsupported type", p); + NearUtils.parseCoreContents(mockIntent, mockListener); + verifyZeroInteractions(mockListener); + + } + + private void configMockFor(String plugin_name, Parcelable reaction) { + when(mockIntent.getStringExtra(NearItIntentConstants.REACTION_PLUGIN)) + .thenReturn(plugin_name); + when(mockIntent.getStringExtra(NearItIntentConstants.RECIPE_ID)) + .thenReturn("my_test_id"); + when(mockIntent.hasExtra(NearItIntentConstants.CONTENT)).thenReturn(true); + when(mockIntent.getParcelableExtra(NearItIntentConstants.CONTENT)) + .thenReturn(reaction); + } + +} From 251dde10874540a71898d2fba86a57b9a127bbef Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 28 Feb 2017 18:06:18 +0100 Subject: [PATCH 55/91] add instrumented test classes for models: - test write and read to parcel --- .../sdk/Reaction/Content/ContentTest.java | 45 +++++++++++++++ .../near/sdk/Reaction/Coupon/CouponTest.java | 55 +++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/androidTest/java/it/near/sdk/Reaction/Content/ContentTest.java create mode 100644 src/androidTest/java/it/near/sdk/Reaction/Coupon/CouponTest.java diff --git a/src/androidTest/java/it/near/sdk/Reaction/Content/ContentTest.java b/src/androidTest/java/it/near/sdk/Reaction/Content/ContentTest.java new file mode 100644 index 00000000..870b88f6 --- /dev/null +++ b/src/androidTest/java/it/near/sdk/Reaction/Content/ContentTest.java @@ -0,0 +1,45 @@ +package it.near.sdk.Reaction.Content; + +import android.os.Parcel; +import android.support.test.runner.AndroidJUnit4; + +import com.google.common.collect.Lists; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import it.near.sdk.Reactions.Content.Content; +import it.near.sdk.Reactions.Content.Image; +import it.near.sdk.Reactions.Content.ImageSet; + +import static junit.framework.Assert.*; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertThat; + +/** + * Created by cattaneostefano on 28/02/2017. + */ + +@RunWith(AndroidJUnit4.class) +public class ContentTest { + + @Test + public void contentIsParcelable() { + Content content = new Content(); + content.setContent("content"); + content.setImages_links(Lists.newArrayList(new ImageSet(), new ImageSet())); + content.setVideo_link("video_link"); + content.setId("content_id"); + content.setUpdated_at("updated@whatever"); + Parcel parcel = Parcel.obtain(); + content.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + Content actual = Content.CREATOR.createFromParcel(parcel); + assertEquals(content.getContent(), actual.getContent()); + assertThat(content.getImages_links(), containsInAnyOrder(actual.getImages_links().toArray())); + assertEquals(content.getVideo_link(), actual.getVideo_link()); + assertEquals(content.getId(), actual.getId()); + assertEquals(content.getUpdated_at(), actual.getUpdated_at()); + } + +} diff --git a/src/androidTest/java/it/near/sdk/Reaction/Coupon/CouponTest.java b/src/androidTest/java/it/near/sdk/Reaction/Coupon/CouponTest.java new file mode 100644 index 00000000..0732a06a --- /dev/null +++ b/src/androidTest/java/it/near/sdk/Reaction/Coupon/CouponTest.java @@ -0,0 +1,55 @@ +package it.near.sdk.Reaction.Coupon; + +import android.os.Parcel; +import android.support.test.runner.AndroidJUnit4; + +import com.google.common.collect.Lists; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import it.near.sdk.Reactions.Content.Content; +import it.near.sdk.Reactions.Content.ImageSet; +import it.near.sdk.Reactions.Coupon.Claim; +import it.near.sdk.Reactions.Coupon.Coupon; + +import static junit.framework.Assert.*; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertThat; + +/** + * Created by cattaneostefano on 28/02/2017. + */ + +@RunWith(AndroidJUnit4.class) +public class CouponTest { + + @Test + public void couponIsParcelable() { + Coupon coupon = new Coupon(); + coupon.setId("coupon_id"); + coupon.setName("coupon_name"); + coupon.setDescription("coupon_description"); + coupon.setValue("coupon_value"); + coupon.setExpires_at("expiring_soon"); + coupon.setIcon_id("coupon_icon_id"); + coupon.setClaims(Lists.newArrayList(new Claim(), new Claim())); + coupon.setIconSet(new ImageSet()); + + Parcel parcel = Parcel.obtain(); + coupon.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + Coupon actual = Coupon.CREATOR.createFromParcel(parcel); + + assertEquals(coupon.getId(), actual.getId()); + assertEquals(coupon.getName(), actual.getName()); + assertEquals(coupon.getDescription(), actual.getDescription()); + assertEquals(coupon.getValue(), actual.getValue()); + assertEquals(coupon.getExpires_at(), actual.getExpires_at()); + assertEquals(coupon.getIcon_id(), actual.getIcon_id()); + assertThat(coupon.getClaims(), containsInAnyOrder(actual.getClaims().toArray())); + assertEquals(coupon.getIconSet(), actual.getIconSet()); + + } + +} From dde6471a54c8884fc0b5766ccbffea310626618d Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 28 Feb 2017 18:27:05 +0100 Subject: [PATCH 56/91] add nullable annotation --- .../sdk/Reactions/SimpleNotification/SimpleNotification.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/it/near/sdk/Reactions/SimpleNotification/SimpleNotification.java b/src/main/java/it/near/sdk/Reactions/SimpleNotification/SimpleNotification.java index 729eb034..ff41b45a 100644 --- a/src/main/java/it/near/sdk/Reactions/SimpleNotification/SimpleNotification.java +++ b/src/main/java/it/near/sdk/Reactions/SimpleNotification/SimpleNotification.java @@ -2,6 +2,7 @@ import android.os.Parcel; import android.os.Parcelable; +import android.support.annotation.Nullable; /** * Created by cattaneostefano on 07/10/2016. @@ -12,7 +13,7 @@ public class SimpleNotification implements Parcelable { String notificationMessage; String notificationTitle; - public SimpleNotification(String notificationMessage, String notificationTitle) { + public SimpleNotification(String notificationMessage, @Nullable String notificationTitle) { this.notificationMessage = notificationMessage; this.notificationTitle = notificationTitle; } From 136745c6000ac24918cc0aaed9cd04d029a7b5b7 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Tue, 28 Feb 2017 18:27:21 +0100 Subject: [PATCH 57/91] add tests for models --- .../Reaction/CustomJSON/CustomJSONTest.java | 45 +++++++++++++++++++ .../sdk/Reaction/Feedback/FeedbackTest.java | 37 +++++++++++++++ .../SimpleNotificationTest.java | 43 ++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 src/androidTest/java/it/near/sdk/Reaction/CustomJSON/CustomJSONTest.java create mode 100644 src/androidTest/java/it/near/sdk/Reaction/Feedback/FeedbackTest.java create mode 100644 src/androidTest/java/it/near/sdk/Reaction/SimpleNotification/SimpleNotificationTest.java diff --git a/src/androidTest/java/it/near/sdk/Reaction/CustomJSON/CustomJSONTest.java b/src/androidTest/java/it/near/sdk/Reaction/CustomJSON/CustomJSONTest.java new file mode 100644 index 00000000..707f4a72 --- /dev/null +++ b/src/androidTest/java/it/near/sdk/Reaction/CustomJSON/CustomJSONTest.java @@ -0,0 +1,45 @@ +package it.near.sdk.Reaction.CustomJSON; + +import android.os.Parcel; +import android.support.test.runner.AndroidJUnit4; + +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; +import java.util.HashMap; + +import it.near.sdk.Reactions.CustomJSON.CustomJSON; + +import static junit.framework.Assert.*; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertThat; + +/** + * Created by cattaneostefano on 28/02/2017. + */ + +@RunWith(AndroidJUnit4.class) +public class CustomJSONTest { + + @Test + public void custonJSONIsParcelable() { + CustomJSON customJSON = new CustomJSON(); + HashMap content = Maps.newHashMap(); + /*content.put("number", 1); + content.put("string", "2"); + content.put("boolean", true); + content.put("arrayInt", new int[]{3,2,5,4,1}); + content.put("arrayString", new String[]{"blah", "hey", "yo"});*/ + customJSON.setContent(content); + + Parcel parcel = Parcel.obtain(); + customJSON.writeToParcel(parcel, 0); + CustomJSON actual = CustomJSON.CREATOR.createFromParcel(parcel); + + assertTrue(true); + } +} diff --git a/src/androidTest/java/it/near/sdk/Reaction/Feedback/FeedbackTest.java b/src/androidTest/java/it/near/sdk/Reaction/Feedback/FeedbackTest.java new file mode 100644 index 00000000..7a2b16a6 --- /dev/null +++ b/src/androidTest/java/it/near/sdk/Reaction/Feedback/FeedbackTest.java @@ -0,0 +1,37 @@ +package it.near.sdk.Reaction.Feedback; + +import android.os.Parcel; +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import it.near.sdk.Reactions.Feedback.Feedback; + +import static junit.framework.Assert.*; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertThat; + +/** + * Created by cattaneostefano on 28/02/2017. + */ + +@RunWith(AndroidJUnit4.class) +public class FeedbackTest { + + @Test + public void feedbackIsParcelable() { + Feedback feedback = new Feedback(); + feedback.setId("feedback_id"); + feedback.setRecipeId("recipe_id"); + feedback.setQuestion("what comes after five?"); + Parcel parcel = Parcel.obtain(); + feedback.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + Feedback actual = Feedback.CREATOR.createFromParcel(parcel); + assertEquals(feedback.getId(), actual.getId()); + assertEquals(feedback.getRecipeId(), actual.getRecipeId()); + assertEquals(feedback.getQuestion(), actual.getQuestion()); + } + +} diff --git a/src/androidTest/java/it/near/sdk/Reaction/SimpleNotification/SimpleNotificationTest.java b/src/androidTest/java/it/near/sdk/Reaction/SimpleNotification/SimpleNotificationTest.java new file mode 100644 index 00000000..be0c5aea --- /dev/null +++ b/src/androidTest/java/it/near/sdk/Reaction/SimpleNotification/SimpleNotificationTest.java @@ -0,0 +1,43 @@ +package it.near.sdk.Reaction.SimpleNotification; + +import android.os.Parcel; +import android.support.test.runner.AndroidJUnit4; + +import com.loopj.android.http.PreemptiveAuthorizationHttpRequestInterceptor; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import it.near.sdk.Reactions.SimpleNotification.SimpleNotification; + +import static junit.framework.Assert.*; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.junit.Assert.assertThat; + +/** + * Created by cattaneostefano on 28/02/2017. + */ + +@RunWith(AndroidJUnit4.class) +public class SimpleNotificationTest { + + @Test + public void simpleNotificationIsParcelable() { + SimpleNotification simpleNotification = new SimpleNotification("message", "title"); + Parcel parcel = Parcel.obtain(); + simpleNotification.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + SimpleNotification actual = SimpleNotification.CREATOR.createFromParcel(parcel); + assertEquals(simpleNotification.getNotificationMessage(), actual.getNotificationMessage()); + assertEquals(simpleNotification.getNotificationTitle(), actual.getNotificationTitle()); + + + SimpleNotification simpleNotificationNoTitle = new SimpleNotification("message", null); + parcel = Parcel.obtain(); + simpleNotificationNoTitle.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + actual = SimpleNotification.CREATOR.createFromParcel(parcel); + assertEquals(simpleNotification.getNotificationMessage(), actual.getNotificationMessage()); + assertNull(actual.getNotificationTitle()); + } +} From 4d905292c266ff21a344366770ef1b1dd6795b7f Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Wed, 1 Mar 2017 10:53:42 +0100 Subject: [PATCH 58/91] add test for jet decoding method --- .../java/it/near/sdk/Utils/NearUtilsTest.java | 23 +++++++++++++ .../java/it/near/sdk/Utils/NearUtils.java | 33 +++++++++---------- 2 files changed, 39 insertions(+), 17 deletions(-) create mode 100644 src/androidTest/java/it/near/sdk/Utils/NearUtilsTest.java diff --git a/src/androidTest/java/it/near/sdk/Utils/NearUtilsTest.java b/src/androidTest/java/it/near/sdk/Utils/NearUtilsTest.java new file mode 100644 index 00000000..116911f0 --- /dev/null +++ b/src/androidTest/java/it/near/sdk/Utils/NearUtilsTest.java @@ -0,0 +1,23 @@ +package it.near.sdk.Utils; + +import android.support.test.runner.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; +import static junit.framework.Assert.*; + +/** + * Created by cattaneostefano on 01/03/2017. + */ + +@RunWith(AndroidJUnit4.class) +public class NearUtilsTest { + + @Test + public void testFetchAppIdFrom() { + String encoded = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0aGlzaXNub3RhdGFzZWNyZXRJc3dlYXJvbm15bGlmZSIsImlhdCI6MTQ4MDYxMTA2NSwiZXhwIjoxNjA2ODY3MTk5LCJkYXRhIjp7ImFjY291bnQiOnsiaWQiOiI2YTU2MTg0Yy0zZTliLTQ2MWQtODU4OS04ZDUxNDZmYWUyMzgiLCJyb2xlX2tleSI6ImFwcCJ9fX0.Ne4wDy0kwVe4jkNQ_PR9QskFo4kOgXfqlOsRQTLTJ1o"; + String expectedAppId = "6a56184c-3e9b-461d-8589-8d5146fae238"; + String actualAppId = NearUtils.fetchAppIdFrom(encoded); + assertEquals(expectedAppId, actualAppId); + } +} diff --git a/src/main/java/it/near/sdk/Utils/NearUtils.java b/src/main/java/it/near/sdk/Utils/NearUtils.java index dc7014f9..e8cc531d 100644 --- a/src/main/java/it/near/sdk/Utils/NearUtils.java +++ b/src/main/java/it/near/sdk/Utils/NearUtils.java @@ -29,23 +29,6 @@ */ public class NearUtils { - /** - * Decode base 64 string - * @param encoded encoded string - * @return decoded string - */ - public static String decodeString(String encoded) throws NullPointerException{ - byte[] dataDec = Base64.decode(encoded, Base64.DEFAULT); - String decodedString = ""; - try { - decodedString = new String(dataDec, "UTF-8"); - } catch (UnsupportedEncodingException e) { - e.printStackTrace(); - } - return decodedString; - } - - /** * Compute app Id from the Apptoken (apikey) * @param apiKey token @@ -66,6 +49,22 @@ public static String fetchAppIdFrom(String apiKey) { return appId; } + /** + * Decode base 64 string + * @param encoded encoded string + * @return decoded string + */ + private static String decodeString(String encoded) throws NullPointerException{ + byte[] dataDec = Base64.decode(encoded, Base64.DEFAULT); + String decodedString = ""; + try { + decodedString = new String(dataDec, "UTF-8"); + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return decodedString; + } + /** * String utility method * @param str initial string From c1b9b8437bb63e74a6fde758353bf6356fc76f83 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Wed, 1 Mar 2017 17:35:04 +0100 Subject: [PATCH 59/91] Expose new method in recipeCooler, add it to test cases --- .../it/near/sdk/Recipes/RecipeCooler.java | 70 ++++++++++++------- .../it/near/sdk/Recipes/RecipeCoolerTest.java | 26 ++++--- 2 files changed, 60 insertions(+), 36 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java index 9a5e5cbd..1ffebdbf 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipeCooler.java +++ b/src/main/java/it/near/sdk/Recipes/RecipeCooler.java @@ -1,6 +1,5 @@ package it.near.sdk.Recipes; -import android.content.Context; import android.content.SharedPreferences; import android.support.annotation.NonNull; @@ -37,13 +36,53 @@ public RecipeCooler(@NonNull SharedPreferences sharedPreferences) { mSharedPreferences = checkNotNull(sharedPreferences);; } + /** + * Register the recipe as shown for future cooldown evaluation. + * @param recipeId the recipe identifier. + */ public void markRecipeAsShown(String recipeId){ long timeStamp = System.currentTimeMillis(); - getMap().put(recipeId, timeStamp); + getRecipeLogMap().put(recipeId, timeStamp); saveMap(mRecipeLogMap); saveLatestEntry(System.currentTimeMillis()); } + /** + * Filters a recipe list against the log of recipes that have been marked as shown and its cooldown period. + * @param recipes the recipe list to filter. This object will be modified. + */ + public void filterRecipe(List recipes){ + for (Iterator it = recipes.iterator(); it.hasNext();) { + Recipe recipe = it.next(); + if (!canShowRecipe(recipe)){ + it.remove(); + } + } + } + + /** + * Get the latest recipe shown event timestamp. + * @return the timestamp for the last recipe shown event. + */ + public Long getLatestLogEntry(){ + if (mLatestLogEntry == null) { + mLatestLogEntry = loadLatestEntry(); + } + return mLatestLogEntry; + } + + /** + * Get the map of recipe shown event timestamps. + * @return the map of timestamps. + */ + public Map getRecipeLogMap() { + if (mRecipeLogMap == null){ + mRecipeLogMap = loadMap(); + } + return mRecipeLogMap; + } + + private boolean canShowRecipe(Recipe recipe){ Map cooldown = recipe.getCooldown(); return cooldown == null || @@ -61,29 +100,13 @@ private boolean globalCooldownCheck(Map cooldown) { private boolean selfCooldownCheck(Recipe recipe, Map cooldown){ if (!cooldown.containsKey(SELF_COOLDOWN) || cooldown.get(SELF_COOLDOWN) == null || - !getMap().containsKey(recipe.getId())) return true; + !getRecipeLogMap().containsKey(recipe.getId())) return true; - long recipeLatestEntry = getMap().get(recipe.getId()); + long recipeLatestEntry = getRecipeLogMap().get(recipe.getId()); long expiredSeconds = (System.currentTimeMillis() - recipeLatestEntry) / 1000; return expiredSeconds >= (Long)cooldown.get(SELF_COOLDOWN); } - public void filterRecipe(List recipes){ - for (Iterator it = recipes.iterator(); it.hasNext();) { - Recipe recipe = it.next(); - if (!canShowRecipe(recipe)){ - it.remove(); - } - } - } - - private Map getMap() { - if (mRecipeLogMap == null){ - mRecipeLogMap = loadMap(); - } - return mRecipeLogMap; - } - private void saveMap(Map inputMap){ if (mSharedPreferences != null){ JSONObject jsonObject = new JSONObject(inputMap); @@ -114,13 +137,6 @@ private Map loadMap(){ return outputMap; } - public Long getLatestLogEntry(){ - if (mLatestLogEntry == null) { - mLatestLogEntry = loadLatestEntry(); - } - return mLatestLogEntry; - } - private Long loadLatestEntry() { if (mSharedPreferences != null) { return mSharedPreferences.getLong(LATEST_LOG, 0L); diff --git a/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java b/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java index eea4b464..248a98d9 100644 --- a/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java +++ b/src/test/java/it/near/sdk/Recipes/RecipeCoolerTest.java @@ -2,22 +2,21 @@ import android.content.SharedPreferences; +import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import org.hamcrest.core.IsNot; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; -import org.mockito.Matchers; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import java.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; +import java.util.Map; import it.near.sdk.Recipes.Models.Recipe; -import it.near.sdk.Recipes.RecipeCooler; import static com.google.common.collect.Lists.newArrayList; import static it.near.sdk.Recipes.RecipeCooler.*; @@ -25,7 +24,11 @@ import static org.hamcrest.CoreMatchers.hasItem; import static org.hamcrest.CoreMatchers.hasItems; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.both; +import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.core.IsNot.not; import static org.mockito.AdditionalMatchers.*; import static org.mockito.Mockito.*; @@ -68,6 +71,7 @@ public void whenLogIsEmpty_enableRecipe() { // then they all pass the filter assertThat(recipeList, hasSize(3)); assertThat(recipeList, hasItems(mCriticalRecipe, mNonCriticalRecipe)); + assertThat(mRecipeCooler.getRecipeLogMap().values(), is(empty())); } @Test @@ -75,14 +79,13 @@ public void whenRecipeShown_historyUpdated() { // when we mark a recipe as shown mRecipeCooler.markRecipeAsShown(mNonCriticalRecipe.getId()); // then its timestamp is put in history - verify(mMockEditor).putString(eq(LOG_MAP), and(contains(mNonCriticalRecipe.getId()), - not(contains(mCriticalRecipe.getId())))); + assertThat(keySetOf(mRecipeCooler.getRecipeLogMap()), both(hasItem(mNonCriticalRecipe.getId())) + .and(not(hasItem(mCriticalRecipe.getId())))); // when we mark another recipe as shown mRecipeCooler.markRecipeAsShown(mCriticalRecipe.getId()); // then its timestamp is added to history - verify(mMockEditor, atLeastOnce()) - .putString(eq(LOG_MAP), and(contains(mNonCriticalRecipe.getId()), - contains(mCriticalRecipe.getId()))); + assertThat(keySetOf(mRecipeCooler.getRecipeLogMap()), both(hasItem(mNonCriticalRecipe.getId())) + .and(hasItem(mCriticalRecipe.getId()))); } @Test @@ -141,6 +144,7 @@ public void whenRecipeIsShown_updateLastLogEntry() { // then the latest log entry is updated assertTrue(beforeTimestamp <= actualTimestamp); assertTrue(actualTimestamp <= afterTimestamp); + assertThat(keySetOf(mRecipeCooler.getRecipeLogMap()), hasItem(mCriticalRecipe.getId())); } @Test @@ -180,6 +184,10 @@ public void whenMissingGlobalCoolDown_considerItZero() { assertThat(recipeList, hasItem(mNonCriticalRecipe)); } + private List keySetOf(Map map){ + return Lists.newArrayList(map.keySet()); + } + private Recipe buildRecipe(String id, HashMap cooldown) { Recipe criticalRecipe = new Recipe(); criticalRecipe.setId(id); From b0ccb19524f22e31f9054bf8a5abdc5ce57c2d55 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Wed, 1 Mar 2017 18:29:03 +0100 Subject: [PATCH 60/91] add method to build evaluation body for all different scenarios --- .../it/near/sdk/Recipes/RecipesManager.java | 44 +++++++++- .../near/sdk/Recipes/RecipesManagerTest.java | 84 +++++++++++++++++++ 2 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 src/test/java/it/near/sdk/Recipes/RecipesManagerTest.java diff --git a/src/main/java/it/near/sdk/Recipes/RecipesManager.java b/src/main/java/it/near/sdk/Recipes/RecipesManager.java index 81281d7d..35323824 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipesManager.java +++ b/src/main/java/it/near/sdk/Recipes/RecipesManager.java @@ -3,6 +3,8 @@ import android.content.Context; import android.content.SharedPreferences; import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import com.google.gson.Gson; @@ -45,9 +47,9 @@ public class RecipesManager { public static final String PREFS_SUFFIX = "NearRecipes"; private static final String PROCESS_PATH = "process"; private static final String EVALUATE = "evaluate"; - private static final String PULSE_PLUGIN_ID_KEY = "pulse_plugin_id"; - private static final String PULSE_ACTION_ID_KEY = "pulse_action_id"; - private static final String PULSE_BUNDLE_ID_KEY = "pulse_bundle_id"; + static final String PULSE_PLUGIN_ID_KEY = "pulse_plugin_id"; + static final String PULSE_ACTION_ID_KEY = "pulse_action_id"; + static final String PULSE_BUNDLE_ID_KEY = "pulse_bundle_id"; public final String PREFS_NAME; private final SharedPreferences sp; private Context mContext; @@ -394,4 +396,40 @@ public String buildEvaluateBody() throws JSONException { attributes.put("core" , coreObj); return NearJsonAPIUtils.toJsonAPI("evaluation", attributes); } + + public static String buildEvaluateBody(@NonNull GlobalConfig globalConfig, + @NonNull RecipeCooler recipeCooler, + @Nullable String pulse_plugin, + @Nullable String pulse_action, + @Nullable String pulse_bundle) throws JSONException { + if (globalConfig.getProfileId() == null || + globalConfig.getInstallationId() == null || + globalConfig.getAppId() == null){ + throw new JSONException("missing data"); + } + HashMap attributes = new HashMap<>(); + attributes.put("core", buildCoreObject(globalConfig, recipeCooler)); + if (pulse_plugin != null) attributes.put(PULSE_PLUGIN_ID_KEY, pulse_plugin); + if (pulse_action != null) attributes.put(PULSE_ACTION_ID_KEY, pulse_action); + if (pulse_bundle != null) attributes.put(PULSE_BUNDLE_ID_KEY, pulse_bundle); + return NearJsonAPIUtils.toJsonAPI("evaluation", attributes); + } + + private static HashMap buildCoreObject(@NonNull GlobalConfig globalConfig, + @NonNull RecipeCooler recipeCooler) { + HashMap coreObj = new HashMap<>(); + coreObj.put("profile_id", globalConfig.getProfileId()); + coreObj.put("installation_id", globalConfig.getInstallationId()); + coreObj.put("app_id", globalConfig.getAppId()); + HashMap cooldown = buildCooldownBlock(recipeCooler); + coreObj.put("cooldown", cooldown); + return coreObj; + } + + private static HashMap buildCooldownBlock(@NonNull RecipeCooler recipeCooler) { + HashMap block = new HashMap<>(); + block.put("last_notified_at", recipeCooler.getLatestLogEntry()); + block.put("recipes_notified_at", recipeCooler.getRecipeLogMap()); + return block; + } } diff --git a/src/test/java/it/near/sdk/Recipes/RecipesManagerTest.java b/src/test/java/it/near/sdk/Recipes/RecipesManagerTest.java new file mode 100644 index 00000000..7597ce8a --- /dev/null +++ b/src/test/java/it/near/sdk/Recipes/RecipesManagerTest.java @@ -0,0 +1,84 @@ +package it.near.sdk.Recipes; + +import android.content.SharedPreferences; + +import com.google.common.collect.Maps; + +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import it.near.sdk.GlobalConfig; +import it.near.sdk.Recipes.Models.Recipe; + +import static it.near.sdk.Recipes.RecipesManager.PULSE_ACTION_ID_KEY; +import static it.near.sdk.Recipes.RecipesManager.PULSE_BUNDLE_ID_KEY; +import static it.near.sdk.Recipes.RecipesManager.PULSE_PLUGIN_ID_KEY; +import static junit.framework.Assert.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.ArgumentMatchers.notNull; +import static org.mockito.Mockito.when; + +/** + * Created by cattaneostefano on 01/03/2017. + */ +@RunWith(MockitoJUnitRunner.class) +public class RecipesManagerTest { + + @Mock + GlobalConfig mockGlobalConfig; + @Mock + RecipeCooler recipeCooler; + + @Before + public void setUp() { + when(mockGlobalConfig.getAppId()).thenReturn("app_id"); + when(mockGlobalConfig.getInstallationId()).thenReturn("installation_id"); + when(mockGlobalConfig.getProfileId()).thenReturn("profile_id"); + } + + @Test + public void buildCorrectEvaluationBody_noCooldownNoPulse() throws JSONException { + String actual = RecipesManager.buildEvaluateBody(mockGlobalConfig, recipeCooler, null, null, null); + JSONObject actualObj = new JSONObject(actual); + assertThat(actualObj = actualObj.getJSONObject("data"), is(notNullValue())); + assertThat(actualObj = actualObj.getJSONObject("attributes"), is(notNullValue())); + assertThat(actualObj.has(PULSE_PLUGIN_ID_KEY), is(false)); + assertThat(actualObj.has(PULSE_ACTION_ID_KEY), is(false)); + assertThat(actualObj.has(PULSE_BUNDLE_ID_KEY), is(false)); + assertThat(actualObj = actualObj.getJSONObject("core"), is(notNullValue())); + assertThat((String) actualObj.get("profile_id"), is("profile_id")); + assertThat((String) actualObj.get("installation_id"), is("installation_id")); + assertThat((String) actualObj.get("app_id"), is("app_id")); + assertThat(actualObj = actualObj.getJSONObject("cooldown"), is(notNullValue())); + assertThat(Long.valueOf((Integer)actualObj.get("last_notified_at")), is(0L)); + assertThat(actualObj.getJSONObject("recipes_notified_at").length(), is(0)); + } + + @Test + public void buildCorrectEvaluationBody_withPulse() throws JSONException { + String actual = RecipesManager.buildEvaluateBody(mockGlobalConfig, recipeCooler, "plugin", "action", "bundle"); + JSONObject actualObj = new JSONObject(actual); + assertThat(actualObj = actualObj.getJSONObject("data"), is(notNullValue())); + assertThat(actualObj = actualObj.getJSONObject("attributes"), is(notNullValue())); + assertThat((String) actualObj.get(PULSE_PLUGIN_ID_KEY), is("plugin")); + assertThat((String) actualObj.get(PULSE_ACTION_ID_KEY), is("action")); + assertThat((String) actualObj.get(PULSE_BUNDLE_ID_KEY), is("bundle")); + } + + @Test + public void buildCorrectEvaluationBody_withCooldown() { + // TODO + } + + +} From 6e2b6afe83cf841bca02f5bf35e3cd171062b722 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Thu, 2 Mar 2017 12:12:14 +0100 Subject: [PATCH 61/91] Move recipe tracking method to RecipesManager --- .../Background/NearItIntentService.java | 3 +- .../it/near/sdk/Recipes/Models/Recipe.java | 16 +---- .../it/near/sdk/Recipes/RecipesManager.java | 58 +++++++++---------- 3 files changed, 30 insertions(+), 47 deletions(-) diff --git a/src/main/java/it/near/sdk/Recipes/Background/NearItIntentService.java b/src/main/java/it/near/sdk/Recipes/Background/NearItIntentService.java index eef73981..48c90add 100644 --- a/src/main/java/it/near/sdk/Recipes/Background/NearItIntentService.java +++ b/src/main/java/it/near/sdk/Recipes/Background/NearItIntentService.java @@ -11,6 +11,7 @@ import it.near.sdk.NearItManager; import it.near.sdk.R; import it.near.sdk.Recipes.Models.Recipe; +import it.near.sdk.Recipes.RecipesManager; import it.near.sdk.Utils.NearItIntentConstants; import it.near.sdk.Utils.NearNotification; @@ -56,7 +57,7 @@ protected void sendSimpleNotification(@NonNull Intent intent){ } String recipeId = intent.getStringExtra(NearItIntentConstants.RECIPE_ID); try { - Recipe.sendTracking(getApplicationContext(), recipeId, Recipe.NOTIFIED_STATUS); + RecipesManager.sendTracking(getApplicationContext(), recipeId, Recipe.NOTIFIED_STATUS); } catch (JSONException e) { e.printStackTrace(); } diff --git a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java index 0285601e..7ed6812b 100644 --- a/src/main/java/it/near/sdk/Recipes/Models/Recipe.java +++ b/src/main/java/it/near/sdk/Recipes/Models/Recipe.java @@ -56,7 +56,6 @@ public class Recipe extends Resource { @Relationship("reaction_action") ReactionAction reaction_action; - private static final String TRACKINGS_PATH = "trackings"; public static final String NOTIFIED_STATUS = "notified"; public static final String ENGAGED_STATUS = "engaged"; @@ -173,18 +172,7 @@ public String getNotificationBody() { return null; } - /** - * Sends tracking on a recipe. Lets choose the notified status. - * @param context the app context. - * @param recipeId the recipe identifier. - * @param trackingEvent notified status to send. Can either be NO - * @throws JSONException - */ - public static void sendTracking(Context context, String recipeId, String trackingEvent) throws JSONException { - String trackingBody = buildTrackingBody(context, recipeId, trackingEvent); - Uri url = Uri.parse(TRACKINGS_PATH).buildUpon().build(); - NearNetworkUtil.sendTrack(context, url.toString(), trackingBody); - } + /** * Builds the tracking send request body. @@ -194,7 +182,7 @@ public static void sendTracking(Context context, String recipeId, String trackin * @return the http body string. * @throws JSONException */ - private static String buildTrackingBody(Context context, String recipeId, String trackingEvent) throws JSONException { + public static String buildTrackingBody(Context context, String recipeId, String trackingEvent) throws JSONException { String profileId = GlobalConfig.getInstance(context).getProfileId(); String appId = GlobalConfig.getInstance(context).getAppId(); String installationId = GlobalConfig.getInstance(context).getInstallationId(); diff --git a/src/main/java/it/near/sdk/Recipes/RecipesManager.java b/src/main/java/it/near/sdk/Recipes/RecipesManager.java index 35323824..a7a7ad02 100644 --- a/src/main/java/it/near/sdk/Recipes/RecipesManager.java +++ b/src/main/java/it/near/sdk/Recipes/RecipesManager.java @@ -25,6 +25,7 @@ import it.near.sdk.Communication.Constants; import it.near.sdk.Communication.NearAsyncHttpClient; import it.near.sdk.Communication.NearJsonHttpResponseHandler; +import it.near.sdk.Communication.NearNetworkUtil; import it.near.sdk.GlobalConfig; import it.near.sdk.MorpheusNear.Morpheus; import it.near.sdk.Reactions.Reaction; @@ -47,6 +48,7 @@ public class RecipesManager { public static final String PREFS_SUFFIX = "NearRecipes"; private static final String PROCESS_PATH = "process"; private static final String EVALUATE = "evaluate"; + private static final String TRACKINGS_PATH = "trackings"; static final String PULSE_PLUGIN_ID_KEY = "pulse_plugin_id"; static final String PULSE_ACTION_ID_KEY = "pulse_action_id"; static final String PULSE_BUNDLE_ID_KEY = "pulse_bundle_id"; @@ -58,7 +60,7 @@ public class RecipesManager { private HashMap reactions = new HashMap<>(); SharedPreferences.Editor editor; private NearAsyncHttpClient httpClient; - private RecipeCooler mRecipeCooler; + private static RecipeCooler mRecipeCooler; public RecipesManager(Context context) { this.mContext = context; @@ -289,30 +291,18 @@ public void onFailureUnique(int statusCode, Header[] headers, Throwable throwabl private void onlinePulseEvaluation(String pulse_plugin, String pulse_action, String pulse_bundle) { Uri url = Uri.parse(Constants.API.RECIPES_PATH).buildUpon() .appendEncodedPath(EVALUATE).build(); - HashMap map = new HashMap<>(); - JSONObject evalCoreObject = new JSONObject(); - try { - evalCoreObject.put("installation_id", GlobalConfig.getInstance(mContext).getInstallationId()); - evalCoreObject.put("app_id", GlobalConfig.getInstance(mContext).getAppId()); - evalCoreObject.put("profile_id", GlobalConfig.getInstance(mContext).getProfileId()); - } catch (JSONException e) { - e.printStackTrace(); - ULog.d(TAG, "profileId not present"); - } - map.put("core", evalCoreObject); - map.put(PULSE_PLUGIN_ID_KEY, pulse_plugin); - map.put(PULSE_ACTION_ID_KEY, pulse_action); - map.put(PULSE_BUNDLE_ID_KEY, pulse_bundle); - String requestBody = null; + String evaluateBody = null; try { - requestBody = NearJsonAPIUtils.toJsonAPI("evaluation", map); + evaluateBody = buildEvaluateBody(GlobalConfig.getInstance(mContext), + mRecipeCooler, pulse_plugin, pulse_action, pulse_bundle); } catch (JSONException e) { e.printStackTrace(); - ULog.d(TAG, "Can't build request body"); + ULog.d(TAG, "body build error"); + return; } try { - httpClient.nearPost(mContext, url.toString(), requestBody, new NearJsonHttpResponseHandler(){ + httpClient.nearPost(mContext, url.toString(), evaluateBody, new NearJsonHttpResponseHandler(){ @Override public void onSuccess(int statusCode, Header[] headers, JSONObject response) { Recipe recipe = NearJsonAPIUtils.parseElement(morpheus, response, Recipe.class); @@ -347,7 +337,8 @@ public void evaluateRecipe(String recipeId){ .appendPath(EVALUATE).build(); String evaluateBody = null; try { - evaluateBody = buildEvaluateBody(); + evaluateBody = buildEvaluateBody(GlobalConfig.getInstance(mContext), + mRecipeCooler, null, null, null); } catch (JSONException e) { e.printStackTrace(); ULog.d(TAG, "body build error"); @@ -382,19 +373,22 @@ public void onFailureUnique(int statusCode, Header[] headers, Throwable throwabl } } - public String buildEvaluateBody() throws JSONException { - if (GlobalConfig.getInstance(mContext).getProfileId() == null || - GlobalConfig.getInstance(mContext).getInstallationId() == null || - GlobalConfig.getInstance(mContext).getAppId() == null){ - throw new JSONException("missing data"); + /** + * Sends tracking on a recipe. Lets choose the notified status. + * @param context the app context. + * @param recipeId the recipe identifier. + * @param trackingEvent notified status to send. Can either be NO + * @throws JSONException + */ + public static void sendTracking(Context context, String recipeId, String trackingEvent) throws JSONException { + if (trackingEvent.equals(Recipe.NOTIFIED_STATUS)){ + if (mRecipeCooler != null){ + mRecipeCooler.markRecipeAsShown(recipeId); + } } - HashMap coreObj = new HashMap<>(); - coreObj.put("profile_id", GlobalConfig.getInstance(mContext).getProfileId()); - coreObj.put("installation_id", GlobalConfig.getInstance(mContext).getInstallationId()); - coreObj.put("app_id", GlobalConfig.getInstance(mContext).getAppId()); - HashMap attributes = new HashMap<>(); - attributes.put("core" , coreObj); - return NearJsonAPIUtils.toJsonAPI("evaluation", attributes); + String trackingBody = Recipe.buildTrackingBody(context, recipeId, trackingEvent); + Uri url = Uri.parse(TRACKINGS_PATH).buildUpon().build(); + NearNetworkUtil.sendTrack(context, url.toString(), trackingBody); } public static String buildEvaluateBody(@NonNull GlobalConfig globalConfig, From c7555574146d476e5fef3aa8eb7ad2ebc001da16 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Thu, 2 Mar 2017 12:12:48 +0100 Subject: [PATCH 62/91] add test for building evaluation body with cool down information --- .../near/sdk/Recipes/RecipesManagerTest.java | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/test/java/it/near/sdk/Recipes/RecipesManagerTest.java b/src/test/java/it/near/sdk/Recipes/RecipesManagerTest.java index 7597ce8a..eb5fcbd5 100644 --- a/src/test/java/it/near/sdk/Recipes/RecipesManagerTest.java +++ b/src/test/java/it/near/sdk/Recipes/RecipesManagerTest.java @@ -1,7 +1,5 @@ package it.near.sdk.Recipes; -import android.content.SharedPreferences; - import com.google.common.collect.Maps; import org.json.JSONException; @@ -12,18 +10,17 @@ import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; +import java.util.Map; + import it.near.sdk.GlobalConfig; -import it.near.sdk.Recipes.Models.Recipe; import static it.near.sdk.Recipes.RecipesManager.PULSE_ACTION_ID_KEY; import static it.near.sdk.Recipes.RecipesManager.PULSE_BUNDLE_ID_KEY; import static it.near.sdk.Recipes.RecipesManager.PULSE_PLUGIN_ID_KEY; -import static junit.framework.Assert.*; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.notNull; import static org.mockito.Mockito.when; @@ -37,7 +34,7 @@ public class RecipesManagerTest { @Mock GlobalConfig mockGlobalConfig; @Mock - RecipeCooler recipeCooler; + RecipeCooler mockRecipeCooler; @Before public void setUp() { @@ -48,7 +45,7 @@ public void setUp() { @Test public void buildCorrectEvaluationBody_noCooldownNoPulse() throws JSONException { - String actual = RecipesManager.buildEvaluateBody(mockGlobalConfig, recipeCooler, null, null, null); + String actual = RecipesManager.buildEvaluateBody(mockGlobalConfig, mockRecipeCooler, null, null, null); JSONObject actualObj = new JSONObject(actual); assertThat(actualObj = actualObj.getJSONObject("data"), is(notNullValue())); assertThat(actualObj = actualObj.getJSONObject("attributes"), is(notNullValue())); @@ -66,7 +63,7 @@ public void buildCorrectEvaluationBody_noCooldownNoPulse() throws JSONException @Test public void buildCorrectEvaluationBody_withPulse() throws JSONException { - String actual = RecipesManager.buildEvaluateBody(mockGlobalConfig, recipeCooler, "plugin", "action", "bundle"); + String actual = RecipesManager.buildEvaluateBody(mockGlobalConfig, mockRecipeCooler, "plugin", "action", "bundle"); JSONObject actualObj = new JSONObject(actual); assertThat(actualObj = actualObj.getJSONObject("data"), is(notNullValue())); assertThat(actualObj = actualObj.getJSONObject("attributes"), is(notNullValue())); @@ -76,9 +73,21 @@ public void buildCorrectEvaluationBody_withPulse() throws JSONException { } @Test - public void buildCorrectEvaluationBody_withCooldown() { - // TODO + public void buildCorrectEvaluationBody_withCooldown() throws JSONException { + Map logMap = Maps.newHashMap(); + logMap.put("recipe_id_1", 0L); + logMap.put("recipe_id_2", 1000L); + when(mockRecipeCooler.getRecipeLogMap()).thenReturn(logMap); + String actual = RecipesManager.buildEvaluateBody(mockGlobalConfig, mockRecipeCooler, "plugin", "action", "bundle"); + JSONObject actualObj = new JSONObject(actual); + assertThat(actualObj = actualObj.getJSONObject("data"), is(notNullValue())); + assertThat(actualObj = actualObj.getJSONObject("attributes"), is(notNullValue())); + assertThat(actualObj = actualObj.getJSONObject("core"), is(notNullValue())); + assertThat(actualObj = actualObj.getJSONObject("cooldown"), is(notNullValue())); + assertThat(actualObj = actualObj.getJSONObject("recipes_notified_at"), is(notNullValue())); + assertThat(actualObj.length(), is(2)); + assertThat(Long.valueOf((Integer)actualObj.get("recipe_id_1")), is(0L)); + assertThat(Long.valueOf((Integer)actualObj.get("recipe_id_2")), is(1000L)); } - } From 2fb0f74e906b41c51dd36dcb81fdfcb2a5e77294 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Thu, 2 Mar 2017 12:17:33 +0100 Subject: [PATCH 63/91] Update docs --- docs/custom-bkg-notification.md | 2 +- docs/custom-receiver-sample.md | 1 - docs/handle-content.md | 6 +++--- 3 files changed, 4 insertions(+), 5 deletions(-) delete mode 100644 docs/custom-receiver-sample.md diff --git a/docs/custom-bkg-notification.md b/docs/custom-bkg-notification.md index ee977d48..78419848 100644 --- a/docs/custom-bkg-notification.md +++ b/docs/custom-bkg-notification.md @@ -47,7 +47,7 @@ protected void onHandleIntent(Intent intent) { Since you are overriding the default notification mechanism, remember to track the recipe as notified with: String recipeId = intent.getStringExtra(NearItIntentConstants.RECIPE_ID); try { - Recipe.sendTracking(getApplicationContext(), recipeId, Recipe.NOTIFIED_STATUS); + RecipesManager.sendTracking(getApplicationContext(), recipeId, Recipe.NOTIFIED_STATUS); } catch (JSONException e) { e.printStackTrace(); } diff --git a/docs/custom-receiver-sample.md b/docs/custom-receiver-sample.md deleted file mode 100644 index 8b137891..00000000 --- a/docs/custom-receiver-sample.md +++ /dev/null @@ -1 +0,0 @@ - diff --git a/docs/handle-content.md b/docs/handle-content.md index c93af159..ef78eb9b 100644 --- a/docs/handle-content.md +++ b/docs/handle-content.md @@ -49,15 +49,15 @@ NearIT analytics on recipes are built from trackings describing the status of us Background recipes track themselves as notified. To track the tap event, use this method: ```java -Recipe.sendTracking(getApplicationContext(), recipeId, Recipe.ENGAGED_STATUS); +RecipesManager.sendTracking(getApplicationContext(), recipeId, Recipe.ENGAGED_STATUS); ``` You should be able to catch the event inside the activity that is started after interacting with the notification. Foreground recipes don't have automatic tracking. You need to track both the "Notified" and the "Engaged" statuses when it's the best appropriate for you scenario. ```java -Recipe.sendTracking(getApplicationContext(), recipe.getId(), Recipe.NOTIFIED_STATUS); +RecipesManager.sendTracking(getApplicationContext(), recipe.getId(), Recipe.NOTIFIED_STATUS); // and -Recipe.sendTracking(getApplicationContext(), recipe.getId(), Recipe.ENGAGED_STATUS); +RecipesManager.sendTracking(getApplicationContext(), recipe.getId(), Recipe.ENGAGED_STATUS); ``` ## Content Objects From 5f805a30eadeabd64fe763f28897fddc49a5fc34 Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Thu, 2 Mar 2017 12:28:26 +0100 Subject: [PATCH 64/91] more elements in include request parameter --- .../java/it/near/sdk/Reactions/Content/ContentReaction.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/it/near/sdk/Reactions/Content/ContentReaction.java b/src/main/java/it/near/sdk/Reactions/Content/ContentReaction.java index 9dfe3d22..fdbeafd1 100644 --- a/src/main/java/it/near/sdk/Reactions/Content/ContentReaction.java +++ b/src/main/java/it/near/sdk/Reactions/Content/ContentReaction.java @@ -91,7 +91,7 @@ public void refreshConfig() { Uri url = Uri.parse(Constants.API.PLUGINS_ROOT).buildUpon() .appendPath(CONTENT_NOTIFICATION_PATH) .appendPath(CONTENT_NOTIFICATION_RESOURCE) - .appendQueryParameter("include", "images").build(); + .appendQueryParameter("include", "images,audio,upload").build(); try { httpClient.nearGet(mContext, url.toString(), new NearJsonHttpResponseHandler(){ @Override From f94844de6c450814a9528b76219551641995a3ff Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Thu, 2 Mar 2017 17:12:01 +0100 Subject: [PATCH 65/91] - add model for Upload object - add test for parcelable implementation --- .../near/sdk/Reaction/Content/UploadTest.java | 40 ++++++++++++ .../it/near/sdk/Reactions/Content/Upload.java | 63 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 src/androidTest/java/it/near/sdk/Reaction/Content/UploadTest.java create mode 100644 src/main/java/it/near/sdk/Reactions/Content/Upload.java diff --git a/src/androidTest/java/it/near/sdk/Reaction/Content/UploadTest.java b/src/androidTest/java/it/near/sdk/Reaction/Content/UploadTest.java new file mode 100644 index 00000000..07daf7c2 --- /dev/null +++ b/src/androidTest/java/it/near/sdk/Reaction/Content/UploadTest.java @@ -0,0 +1,40 @@ +package it.near.sdk.Reaction.Content; + +import android.os.Parcel; +import android.support.test.runner.AndroidJUnit4; + +import com.google.common.collect.Maps; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashMap; + +import it.near.sdk.Reactions.Content.Upload; + +import static junit.framework.Assert.*; + +/** + * Created by cattaneostefano on 02/03/2017. + */ + +@RunWith(AndroidJUnit4.class) +public class UploadTest { + + @Test + public void uploadIsParcelable() { + Upload upload = new Upload(); + upload.setId("upload_id"); + HashMap map = Maps.newHashMap(); + String pdfUrl = "http://jsnfijegfuien.pdf"; + map.put("url", pdfUrl); + upload.setUpload(map); + Parcel parcel = Parcel.obtain(); + upload.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + Upload actual = Upload.CREATOR.createFromParcel(parcel); + assertEquals(upload.getId(), actual.getId()); + assertEquals(upload.getUpload(), actual.getUpload()); + assertEquals(pdfUrl, actual.getUrl()); + } +} diff --git a/src/main/java/it/near/sdk/Reactions/Content/Upload.java b/src/main/java/it/near/sdk/Reactions/Content/Upload.java new file mode 100644 index 00000000..af549946 --- /dev/null +++ b/src/main/java/it/near/sdk/Reactions/Content/Upload.java @@ -0,0 +1,63 @@ +package it.near.sdk.Reactions.Content; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.google.gson.annotations.SerializedName; + +import java.util.HashMap; + +import it.near.sdk.MorpheusNear.Resource; + +/** + * Created by cattaneostefano on 02/03/2017. + */ + +public class Upload extends Resource implements Parcelable { + @SerializedName("upload") + HashMap upload; + + public Upload() { + } + + public HashMap getUpload() { + return upload; + } + + public void setUpload(HashMap upload) { + this.upload = upload; + } + + public String getUrl() { + return (String) upload.get("url"); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getId()); + dest.writeSerializable(upload); + } + + protected Upload(Parcel in) { + setId(in.readString()); + upload = (HashMap) in.readSerializable(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public Upload createFromParcel(Parcel in) { + return new Upload(in); + } + + @Override + public Upload[] newArray(int size) { + return new Upload[size]; + } + }; + +} From 016be1586dcecd07d39b9737c5ec63061f420c7b Mon Sep 17 00:00:00 2001 From: Cattaneo Stefano Date: Thu, 2 Mar 2017 17:12:38 +0100 Subject: [PATCH 66/91] - add model for audio object - add test for parcelable implementation --- .../near/sdk/Reaction/Content/AudioTest.java | 41 ++++++++++++ .../it/near/sdk/Reactions/Content/Audio.java | 64 +++++++++++++++++++ 2 files changed, 105 insertions(+) create mode 100644 src/androidTest/java/it/near/sdk/Reaction/Content/AudioTest.java create mode 100644 src/main/java/it/near/sdk/Reactions/Content/Audio.java diff --git a/src/androidTest/java/it/near/sdk/Reaction/Content/AudioTest.java b/src/androidTest/java/it/near/sdk/Reaction/Content/AudioTest.java new file mode 100644 index 00000000..85dce88e --- /dev/null +++ b/src/androidTest/java/it/near/sdk/Reaction/Content/AudioTest.java @@ -0,0 +1,41 @@ +package it.near.sdk.Reaction.Content; + +import android.os.Parcel; +import android.support.test.runner.AndroidJUnit4; + +import com.google.common.collect.Maps; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.HashMap; + +import it.near.sdk.Reactions.Content.Audio; + +import static junit.framework.Assert.*; + +/** + * Created by cattaneostefano on 02/03/2017. + */ + +@RunWith(AndroidJUnit4.class) +public class AudioTest { + + @Test + public void audioIsParcelable() { + Audio audio = new Audio(); + HashMap map = Maps.newHashMap(); + String audioUrl = "http://jsnfijegfuien.mp3"; + map.put("url", audioUrl); + audio.setAudio(map); + audio.setId("audio_id"); + Parcel parcel = Parcel.obtain(); + audio.writeToParcel(parcel, 0); + parcel.setDataPosition(0); + Audio actual = Audio.CREATOR.createFromParcel(parcel); + assertEquals(audio.getAudio(), actual.getAudio()); + assertEquals(audioUrl, actual.getUrl()); + assertEquals(audio.getId(), actual.getId()); + } + +} diff --git a/src/main/java/it/near/sdk/Reactions/Content/Audio.java b/src/main/java/it/near/sdk/Reactions/Content/Audio.java new file mode 100644 index 00000000..744efd4f --- /dev/null +++ b/src/main/java/it/near/sdk/Reactions/Content/Audio.java @@ -0,0 +1,64 @@ +package it.near.sdk.Reactions.Content; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.google.gson.annotations.SerializedName; + +import java.util.HashMap; +import java.util.Map; + +import it.near.sdk.MorpheusNear.Resource; + +/** + * Created by cattaneostefano on 02/03/2017. + */ + +public class Audio extends Resource implements Parcelable { + @SerializedName("audio") + HashMap audio; + + public Audio() { + } + + public HashMap getAudio() { + return audio; + } + + public void setAudio(HashMap audio) { + this.audio = audio; + } + + public String getUrl() { + return (String) audio.get("url"); + } + + protected Audio(Parcel in) { + setId(in.readString()); + audio = (HashMap) in.readSerializable(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(getId()); + dest.writeSerializable(audio); + } + + public static final Creator