├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── build.gradle ├── proguard-rules.pro └── src │ ├── androidTest │ └── java │ │ └── com │ │ └── designatednerd │ │ └── wino │ │ ├── activity │ │ └── WineTastingActivityTest.java │ │ ├── model │ │ ├── ParameterizedWineNameTests.java │ │ └── WineTastingTests.java │ │ └── ui │ │ ├── EspressoHelpers.java │ │ └── WineTastingDetailUITests.java │ └── main │ ├── AndroidManifest.xml │ ├── java │ └── com │ │ └── designatednerd │ │ └── wino │ │ ├── SharedPreferencesHelper.java │ │ ├── activity │ │ ├── WineTastingActivity.java │ │ └── WineTastingDetailActivity.java │ │ ├── adapter │ │ └── WineTastingAdapter.java │ │ ├── dialog │ │ ├── DatePickerDialogFragment.java │ │ └── RatingPickerDialogFragment.java │ │ ├── fragment │ │ ├── WineTastingDetailFragment.java │ │ └── WineTastingListFragment.java │ │ └── model │ │ ├── TastingDateFormatter.java │ │ └── WineTasting.java │ └── res │ ├── drawable-hdpi │ ├── ic_add_white_24dp.png │ └── ic_create_white_24dp.png │ ├── drawable-mdpi │ ├── ic_add_white_24dp.png │ └── ic_create_white_24dp.png │ ├── drawable-xhdpi │ ├── ic_add_white_24dp.png │ └── ic_create_white_24dp.png │ ├── drawable-xxhdpi │ ├── ic_add_white_24dp.png │ └── ic_create_white_24dp.png │ ├── layout │ ├── activity_wine_tasting_frame.xml │ ├── fragment_wine_tasting_detail.xml │ ├── fragment_wine_tasting_list.xml │ └── row_wine_tasting.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-mdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ └── values │ ├── colors.xml │ ├── dimens.xml │ ├── strings.xml │ └── styles.xml ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | # Built application files 2 | *.apk 3 | *.ap_ 4 | 5 | # Files for the Dalvik VM 6 | *.dex 7 | 8 | # Java class files 9 | *.class 10 | 11 | # Generated files 12 | bin/ 13 | gen/ 14 | 15 | # Gradle files 16 | .gradle/ 17 | build/ 18 | 19 | # Local configuration file (sdk path, etc) 20 | local.properties 21 | 22 | # Proguard folder generated by Eclipse 23 | proguard/ 24 | 25 | # Log Files 26 | *.log 27 | 28 | .idea/ 29 | *.iml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 designatednerd 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #Wino 2 | 3 | Android Wine-Tasting Note Taker / Example App for Testing Talk 4 | 5 | ##JUnit 4 Example 6 | 7 | [WineTastingTests](app/src/androidTest/java/com/designatednerd/wino/model/WineTastingTests.java) is a really basic example of setting up JUnit4-style tests. 8 | 9 | ##Parameterized Example 10 | 11 | [ParameterizedWineNameTests](app/src/androidTest/java/com/designatednerd/wino/model/ParameterizedWineNameTests.java) is an example of how to use parameterized tests to run the same test repeatedly using different parameters to make sure the same object can handle many types of input quickly. 12 | 13 | ##Espresso Examples 14 | 15 | [WineTastingDetailUITests](app/src/androidTest/java/com/designatednerd/wino/ui/WineTastingDetailUITests.java) is an example of how to set up individual UI tests with Espresso. 16 | 17 | [EspressoHelpers](app/src/androidTest/java/com/designatednerd/wino/ui/EspressoHelpers.java) is an example of how to compose pieces of Espresso functionality into custom actions which allow you to write more expressive code. -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 23 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | applicationId "com.designatednerd.wino" 9 | minSdkVersion 15 10 | targetSdkVersion 23 11 | versionCode 1 12 | versionName "1.0" 13 | testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" 14 | } 15 | 16 | buildTypes { 17 | release { 18 | minifyEnabled false 19 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 20 | } 21 | } 22 | 23 | lintOptions { 24 | //Required by butterknife 25 | disable 'InvalidPackage' 26 | } 27 | 28 | packagingOptions { 29 | //Prevent a warning when trying to do UI tests. 30 | exclude 'LICENSE.txt' 31 | } 32 | } 33 | 34 | 35 | dependencies { 36 | compile fileTree(dir: 'libs', include: ['*.jar']) 37 | 38 | //Android compat things 39 | compile 'com.android.support:appcompat-v7:23.4.0' 40 | compile 'com.android.support:support-v4:23.4.0' 41 | compile 'com.android.support:recyclerview-v7:23.4.0' 42 | 43 | 44 | //Design support library 45 | compile 'com.android.support:design:23.4.0' 46 | 47 | //Square/Jake Wharton Thingies 48 | compile 'com.jakewharton:butterknife:7.0.1' 49 | compile 'com.squareup:otto:1.3.5' 50 | 51 | //GSON for serializing 52 | compile 'com.google.code.gson:gson:2.4' 53 | 54 | //Testing 55 | androidTestCompile ('com.android.support.test.espresso:espresso-core:2.2.2') { 56 | exclude group: 'com.android.support', module: 'support-annotations' 57 | } 58 | 59 | androidTestCompile ('com.android.support.test:runner:0.5') { 60 | exclude group: 'com.android.support', module: 'support-annotations' 61 | } 62 | androidTestCompile ('com.android.support.test:rules:0.5') { 63 | exclude group: 'com.android.support', module: 'support-annotations' 64 | 65 | } 66 | androidTestCompile ('com.android.support.test.espresso:espresso-intents:2.2.2') { 67 | exclude group: 'com.android.support', module: 'support-annotations' 68 | } 69 | androidTestCompile ('com.android.support.test.espresso:espresso-contrib:2.2.2') { 70 | exclude group: 'com.android.support', module: 'appcompat' 71 | exclude group: 'com.android.support', module: 'support-v4' 72 | exclude group: 'com.android.support', module: 'support-annotations' 73 | exclude group: 'com.android.support', module: 'appcompat-v7' 74 | exclude group: 'com.android.support', module: 'design' 75 | exclude module: 'recyclerview-v7' 76 | } 77 | 78 | 79 | } 80 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in /Users/Shared/android-osx-sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/designatednerd/wino/activity/WineTastingActivityTest.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino.activity; 2 | 3 | 4 | import android.support.test.espresso.ViewInteraction; 5 | import android.support.test.rule.ActivityTestRule; 6 | import android.support.test.runner.AndroidJUnit4; 7 | import android.test.suitebuilder.annotation.LargeTest; 8 | import com.designatednerd.wino.R; 9 | import org.junit.Rule; 10 | import org.junit.Test; 11 | import org.junit.runner.RunWith; 12 | 13 | import static android.support.test.espresso.Espresso.onView; 14 | import static android.support.test.espresso.Espresso.pressBack; 15 | import static android.support.test.espresso.action.ViewActions.*; 16 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 17 | import static android.support.test.espresso.matcher.ViewMatchers.*; 18 | import static org.hamcrest.Matchers.allOf; 19 | 20 | /** 21 | * This test was recorded with Android Studio 2.2 Canary Preview 3. 22 | * 23 | * It...doesn't so much actually run, but it gives a good idea of what 24 | * kind of information you can record. 25 | * 26 | * Assuming this will be improved in later versions of 2.2. 27 | */ 28 | 29 | @LargeTest 30 | @RunWith(AndroidJUnit4.class) 31 | public class WineTastingActivityTest { 32 | 33 | @Rule 34 | public ActivityTestRule mActivityTestRule = new ActivityTestRule<>(WineTastingActivity.class); 35 | 36 | @Test 37 | public void wineTastingActivityTest() { 38 | ViewInteraction floatingActionButton = onView( 39 | allOf(withId(R.id.add_tasting_button), isDisplayed())); 40 | floatingActionButton.perform(click()); 41 | 42 | ViewInteraction editText = onView( 43 | allOf(withId(R.id.tasting_detail_wine_name_edittext), 44 | withParent(allOf(withText("Wine Name"), 45 | withParent(withId(R.id.winetasting_detail_layout)))), 46 | isDisplayed())); 47 | editText.check(matches(isDisplayed())); 48 | 49 | ViewInteraction appCompatEditText = onView( 50 | allOf(withId(R.id.tasting_detail_vineyard_name_edittext), isDisplayed())); 51 | appCompatEditText.perform(replaceText("wine")); 52 | 53 | ViewInteraction appCompatEditText2 = onView( 54 | allOf(withId(R.id.tasting_detail_wine_name_edittext), isDisplayed())); 55 | appCompatEditText2.perform(replaceText("it is s")); 56 | 57 | ViewInteraction appCompatEditText3 = onView( 58 | allOf(withId(R.id.tasting_detail_varietal_edittext), isDisplayed())); 59 | appCompatEditText3.perform(replaceText("merlot")); 60 | 61 | appCompatEditText3.perform(pressImeActionButton()); 62 | 63 | ViewInteraction appCompatSpinner = onView( 64 | allOf(withId(R.id.tasting_detail_rating_spinner), 65 | withParent(withId(R.id.winetasting_detail_layout)), 66 | isDisplayed())); 67 | appCompatSpinner.perform(click()); 68 | 69 | ViewInteraction appCompatSpinner2 = onView( 70 | allOf(withId(R.id.tasting_detail_rating_spinner), 71 | withParent(withId(R.id.winetasting_detail_layout)), 72 | isDisplayed())); 73 | appCompatSpinner2.perform(click()); 74 | 75 | ViewInteraction appCompatCheckedTextView = onView( 76 | allOf(withId(android.R.id.text1), withText("Four"), isDisplayed())); 77 | appCompatCheckedTextView.perform(click()); 78 | 79 | ViewInteraction appCompatButton = onView( 80 | allOf(withId(R.id.tasting_detail_save_button), withText("Save Tasting"), 81 | withParent(withId(R.id.winetasting_detail_layout)), 82 | isDisplayed())); 83 | appCompatButton.perform(click()); 84 | 85 | pressBack(); 86 | 87 | ViewInteraction textView = onView( 88 | allOf(withId(R.id.row_tasting_wine_name_textview), withText("it is s (merlot, wine)"), isDisplayed())); 89 | textView.check(matches(withText("it is s (merlot, wine)"))); 90 | 91 | ViewInteraction textView2 = onView( 92 | allOf(withId(R.id.row_tasting_date_textview), withText("6/15/16"), 93 | withParent(allOf(withId(R.id.row_tasting), 94 | withParent(withId(R.id.wine_tasting_recyclerview)))), 95 | isDisplayed())); 96 | textView2.check(matches(withText("6/15/16"))); 97 | 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/designatednerd/wino/model/ParameterizedWineNameTests.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino.model; 2 | 3 | import org.junit.Test; 4 | import org.junit.runner.RunWith; 5 | import org.junit.runners.Parameterized; 6 | 7 | import java.util.Arrays; 8 | 9 | import static org.junit.Assert.assertEquals; 10 | 11 | /** 12 | * An example of a parameterized test which runs the same test function over and over with 13 | * different data. 14 | */ 15 | 16 | @RunWith(Parameterized.class) 17 | public class ParameterizedWineNameTests { 18 | 19 | /******************* 20 | * PARAMETER SETUP * 21 | *******************/ 22 | 23 | /** 24 | * Create an iterable of object arrays that can be handed over to the test 25 | * as parameters to the constructor. 26 | * 27 | * For example, here we have a constructor that takes 4 items: The wine name, 28 | * the vineyard name, the varietal name, and the expected output string. Each 29 | * of these arrays of strings will be passed in to the constructor in that order. 30 | */ 31 | @Parameterized.Parameters 32 | public static Iterable data() { 33 | return Arrays.asList(new Object[][]{ 34 | {"Wine", "Vineyard", "Varietal", "Wine (Varietal, Vineyard)"}, //All info 35 | {"Wine", "Vineyard", null, "Wine (Vineyard)"}, //Missing varietal 36 | {"Wine", null, "Varietal", "Wine (Varietal)"}, //Missing vineyard 37 | {"Wine", null, null, "Wine"}, //Wine Only 38 | {null, null, null, ""} //No info at all 39 | }); 40 | } 41 | 42 | /************* 43 | * VARIABLES * 44 | *************/ 45 | 46 | private String mWineName; 47 | private String mVineyardName; 48 | private String mVarietalName; 49 | private String mExpectedFormattedOutput; 50 | 51 | /*************** 52 | * CONSTRUCTOR * 53 | ***************/ 54 | 55 | public ParameterizedWineNameTests(String aWineName, 56 | String aVineyardName, 57 | String aVarietalName, 58 | String aExpectedFormattedOutput) { 59 | 60 | mWineName = aWineName; 61 | mVineyardName = aVineyardName; 62 | mVarietalName = aVarietalName; 63 | mExpectedFormattedOutput = aExpectedFormattedOutput; 64 | } 65 | 66 | 67 | /*********************** 68 | * PARAMETERIZED TESTS * 69 | ***********************/ 70 | 71 | @Test 72 | public void testFormattingWineName() { 73 | String formatted = WineTasting.wineNameFromInfo(mWineName, mVineyardName, mVarietalName); 74 | assertEquals(mExpectedFormattedOutput, formatted); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/designatednerd/wino/model/WineTastingTests.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino.model; 2 | 3 | import org.junit.Test; 4 | 5 | import static org.junit.Assert.*; 6 | 7 | public class WineTastingTests { 8 | 9 | @Test 10 | public void checkingEqualityNotAffectedByRating() { 11 | String vineyardName = "Test Vineyard"; 12 | String wineName = "Test Wine"; 13 | String varietal = "Shiraz"; 14 | WineTasting.WineType type = WineTasting.WineType.RED; 15 | 16 | WineTasting zeroStarWine = new WineTasting(); 17 | zeroStarWine.vineyardName = vineyardName; 18 | zeroStarWine.wineName = wineName; 19 | zeroStarWine.wineVarietal = varietal; 20 | zeroStarWine.wineType = type; 21 | zeroStarWine.rating = 0; 22 | 23 | WineTasting fiveStarWine = new WineTasting(); 24 | fiveStarWine.vineyardName = vineyardName; 25 | fiveStarWine.wineName = wineName; 26 | fiveStarWine.wineVarietal = varietal; 27 | fiveStarWine.wineType = type; 28 | fiveStarWine.rating = 5; 29 | 30 | assertEquals(zeroStarWine, fiveStarWine); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/designatednerd/wino/ui/EspressoHelpers.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino.ui; 2 | 3 | import android.support.annotation.IdRes; 4 | import android.support.annotation.StringRes; 5 | import android.support.test.espresso.ViewInteraction; 6 | import android.support.v7.widget.RecyclerView; 7 | import android.view.View; 8 | import android.view.ViewGroup; 9 | 10 | import org.hamcrest.Description; 11 | import org.hamcrest.Matcher; 12 | import org.hamcrest.TypeSafeMatcher; 13 | 14 | import static android.support.test.espresso.Espresso.onView; 15 | import static android.support.test.espresso.action.ViewActions.*; 16 | import static android.support.test.espresso.matcher.ViewMatchers.*; 17 | 18 | import static org.hamcrest.CoreMatchers.allOf; 19 | 20 | public class EspressoHelpers { 21 | 22 | /************** 23 | * TEXT ENTRY * 24 | **************/ 25 | 26 | public static void enterTextIntoViewWithHint(String aTextToEnter, @StringRes int aHintResID) { 27 | onView(withHint(aHintResID)).perform(typeText(aTextToEnter)); 28 | } 29 | 30 | public static void enterTextIntoViewWithID(String aTextToEnter, @IdRes int aViewID) { 31 | onView(withId(aViewID)).perform(typeText(aTextToEnter)); 32 | } 33 | 34 | /************* 35 | * SCROLLING * 36 | *************/ 37 | 38 | public static void scrollToViewWithID(@IdRes int aViewIDRes) { 39 | onView(withId(aViewIDRes)).perform(scrollTo()); 40 | } 41 | 42 | /*********** 43 | * TAPPING * 44 | ***********/ 45 | 46 | public static void tapViewWithText(String aText) { 47 | onView(withText(aText)).perform(click()); 48 | } 49 | 50 | public static void tapViewWithText(@StringRes int aTextResID) { 51 | onView(withText(aTextResID)).perform(click()); 52 | } 53 | 54 | public static void tapViewWithID(@IdRes int aViewResID) { 55 | onView(withId(aViewResID)).perform(click()); 56 | } 57 | 58 | /********************** 59 | * RECYCLERVIEW STUFF * 60 | **********************/ 61 | 62 | public static Matcher withRecyclerView(@IdRes int viewId) { 63 | return allOf(isAssignableFrom(RecyclerView.class), withId(viewId)); 64 | } 65 | 66 | 67 | //Modified from https://gist.github.com/tommyd3mdi/2622caecc1b2d498cd1a 68 | 69 | /** 70 | * Allows performing actions on a RecyclerView item with a given title. Mostly useful for 71 | * situations where all titles are guaranteed to be unique 72 | * @param aParentRecyclerViewID The resource ID of the RecyclerView 73 | * @param aRecyclerViewTextViewID The resource ID of the text view where the title should be displayed 74 | * @param title The title which should be displayed, as a string 75 | * @return A ViewInteraction where actions will be performed on a row matching the given parameters. 76 | */ 77 | public static ViewInteraction onRecyclerItemViewWithTitle(@IdRes int aParentRecyclerViewID, 78 | @IdRes int aRecyclerViewTextViewID, 79 | String title) { 80 | 81 | Matcher hasRecyclerViewAsParent = withParent(withRecyclerView(aParentRecyclerViewID)); 82 | Matcher hasChildWithTitleInTextView = withChild(allOf(withId(aRecyclerViewTextViewID), withText(title))); 83 | Matcher hasChildWithChildWithTitleInTextView = withChild(hasChildWithTitleInTextView); 84 | 85 | return onView(allOf(hasRecyclerViewAsParent, 86 | hasChildWithChildWithTitleInTextView)); 87 | 88 | } 89 | 90 | //Yoink: http://stackoverflow.com/a/30073528/681493 91 | private static Matcher nthChildOf(final Matcher parentMatcher, final int childPosition) { 92 | return new TypeSafeMatcher() { 93 | @Override 94 | public void describeTo(Description description) { 95 | description.appendText("with " + childPosition + " child view of type parentMatcher"); 96 | } 97 | 98 | @Override 99 | public boolean matchesSafely(View view) { 100 | if (!(view.getParent() instanceof ViewGroup)) { 101 | return parentMatcher.matches(view.getParent()); 102 | } 103 | 104 | ViewGroup group = (ViewGroup) view.getParent(); 105 | return parentMatcher.matches(view.getParent()) && group.getChildAt(childPosition).equals(view); 106 | } 107 | }; 108 | } 109 | 110 | /** 111 | * Creates a view interaction on a RecylerView's child at a given position. 112 | * @param aParentRecyclerViewID The resource ID of the parent recycler view 113 | * @param aPosition The index of the subview you wish to examine (ie, the row) 114 | * @return A ViewInteraction where actions will be performed on the row's parent view at the 115 | * given index. 116 | */ 117 | public static ViewInteraction onRecyclerItemViewAtPosition(@IdRes int aParentRecyclerViewID, 118 | int aPosition) { 119 | 120 | 121 | return onView(nthChildOf(withRecyclerView(aParentRecyclerViewID), aPosition)); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /app/src/androidTest/java/com/designatednerd/wino/ui/WineTastingDetailUITests.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino.ui; 2 | 3 | import android.content.Context; 4 | import android.support.test.espresso.ViewInteraction; 5 | import android.support.test.espresso.intent.rule.IntentsTestRule; 6 | import android.support.test.rule.ActivityTestRule; 7 | import android.view.View; 8 | 9 | import com.designatednerd.wino.R; 10 | import com.designatednerd.wino.SharedPreferencesHelper; 11 | import com.designatednerd.wino.activity.WineTastingActivity; 12 | import com.designatednerd.wino.model.WineTasting; 13 | 14 | import org.hamcrest.Matcher; 15 | import org.junit.After; 16 | import org.junit.Before; 17 | import org.junit.Rule; 18 | import org.junit.Test; 19 | 20 | import java.util.List; 21 | 22 | import static android.support.test.espresso.Espresso.onView; 23 | import static android.support.test.espresso.Espresso.pressBack; 24 | import static android.support.test.espresso.action.ViewActions.click; 25 | import static android.support.test.espresso.assertion.ViewAssertions.matches; 26 | import static android.support.test.espresso.matcher.ViewMatchers.isCompletelyDisplayed; 27 | import static android.support.test.espresso.matcher.ViewMatchers.isDisplayed; 28 | import static android.support.test.espresso.matcher.ViewMatchers.withChild; 29 | import static android.support.test.espresso.matcher.ViewMatchers.withId; 30 | import static android.support.test.espresso.matcher.ViewMatchers.withText; 31 | import static com.designatednerd.wino.ui.EspressoHelpers.*; 32 | import static org.hamcrest.CoreMatchers.allOf; 33 | import static org.junit.Assert.assertEquals; 34 | import static org.junit.Assert.assertNotNull; 35 | 36 | public class WineTastingDetailUITests { 37 | 38 | @Rule 39 | public final ActivityTestRule wineTastingActivity = 40 | new IntentsTestRule<>(WineTastingActivity.class); 41 | 42 | /****************** 43 | * TEST LIFECYCLE * 44 | ******************/ 45 | 46 | @Before 47 | public void beforeEachTest() { 48 | //Nuke any old state 49 | SharedPreferencesHelper 50 | .getInstance(getContext()) 51 | .nukeAllSharedPreferences(); 52 | 53 | //Go from the initial launch page to the details page. 54 | goToDetailsPage(); 55 | } 56 | 57 | @After 58 | public void afterEachTest() { 59 | //Nuke any state which was just added. 60 | SharedPreferencesHelper 61 | .getInstance(getContext()) 62 | .nukeAllSharedPreferences(); 63 | } 64 | 65 | /****************** 66 | * HELPER METHODS * 67 | ******************/ 68 | 69 | private Context getContext() { 70 | return wineTastingActivity.getActivity(); 71 | } 72 | 73 | private void goToDetailsPage() { 74 | //Tap the add button on the main page. 75 | onView(withId(R.id.add_tasting_button)) 76 | .perform(click()); 77 | } 78 | 79 | private void enterAndSaveWineInfo(String aWineName, 80 | String aVineyardName, 81 | String aVarietal, 82 | int aRating) { 83 | //Enter the information into text views 84 | enterTextIntoViewWithID(aVineyardName, R.id.tasting_detail_vineyard_name_edittext); 85 | enterTextIntoViewWithID(aVarietal, R.id.tasting_detail_varietal_edittext); 86 | enterTextIntoViewWithID(aWineName, R.id.tasting_detail_wine_name_edittext); 87 | 88 | //NOTE: This would do the same thing with stock edit texts, but doesn't work with 89 | //TextInputLayout due to this bug: https://code.google.com/p/android/issues/detail?id=178182 90 | // enterTextIntoViewWithHint(aVineyardName, R.string.vineyard_name); 91 | // enterTextIntoViewWithHint(aVarietal, R.string.wine_varietal); 92 | // enterTextIntoViewWithHint(aWineName, R.string.wine_name); 93 | 94 | //Select a rating using the spinner 95 | tapViewWithID(R.id.tasting_detail_rating_spinner); 96 | 97 | //Pull a string representing a rating from the array rather than hard-coding it 98 | //so you don't have to change the test when the text in the array changes. 99 | String[] ratings = getContext().getResources().getStringArray(R.array.ratings_array); 100 | String desiredRating = ratings[aRating]; 101 | tapViewWithText(desiredRating); 102 | 103 | //Save the tasting 104 | tapViewWithText(R.string.save_tasting_title); 105 | } 106 | 107 | /***************** 108 | * ACTUAL TESTS! * 109 | *****************/ 110 | 111 | @Test 112 | public void savingDataInTheDetailIsPersistedToUserDefaults() { 113 | //Use variables to make these easier to test against. 114 | String testWineName = "Test wine name"; 115 | String testVarietal = "Cabernet Sauvignon"; 116 | String testVineyard = "Rodney Strong"; 117 | int testRating = 3; 118 | 119 | enterAndSaveWineInfo(testWineName, 120 | testVineyard, 121 | testVarietal, 122 | testRating); 123 | 124 | String expectedWineName = WineTasting.wineNameFromInfo(testWineName, testVineyard, testVarietal); 125 | 126 | //Check that input is saving 127 | List savedTastings = SharedPreferencesHelper.getInstance(getContext()) 128 | .getCurrentTastings(); 129 | 130 | //Make sure the saved tastings only has the one which was just added. 131 | assertNotNull(savedTastings); 132 | assertEquals(savedTastings.size(), 1); 133 | 134 | //Grab the tasting and then verify the data matches what was entered. 135 | WineTasting savedTasting = savedTastings.get(0); 136 | 137 | assertEquals(savedTasting.vineyardName, testVineyard); 138 | assertEquals(savedTasting.wineName, testWineName); 139 | assertEquals(savedTasting.wineVarietal, testVarietal); 140 | assertEquals(savedTasting.rating, testRating); 141 | assertEquals(savedTasting.getFullWineName(), expectedWineName); 142 | 143 | //back out to the main view for the next test. 144 | pressBack(); 145 | } 146 | 147 | @Test 148 | public void savingDataInTheDetailShowsUpInTheRecyclerView() { 149 | //Setup test variables 150 | String wineName = "RecyclerView"; 151 | String vineyardName = "Google Vineyards"; 152 | String varietal = "Syrah"; 153 | int rating = 5; 154 | 155 | enterAndSaveWineInfo(wineName, 156 | vineyardName, 157 | varietal, 158 | rating); 159 | 160 | //back out to the main view 161 | pressBack(); 162 | 163 | String expectedWineName = WineTasting.wineNameFromInfo(wineName, vineyardName, varietal); 164 | 165 | //The child of the view this Matcher is passed into has the appropriate title in the title view. 166 | Matcher hasTitleChildWithAppropriateContent = withChild(allOf(withId(R.id.row_tasting_wine_name_textview), 167 | withText(expectedWineName))); 168 | 169 | String stars = ""; 170 | for (int i = 0; i < rating; i++) { 171 | stars = stars + new String(Character.toChars(0x1F31F)); 172 | } 173 | 174 | //The child of the view this Matcher is passed into has the appropriate content in the rating view. 175 | Matcher hasRatingChildWithAppropriateContent = withChild(allOf(withId(R.id.row_tasting_wine_rating_textview), 176 | withText(stars))); 177 | 178 | //The child of the view this is passed into has two children that fulfill the above Matchers - 179 | //This is necessary because these two TextViews are wrapped in a LinearLayout. 180 | Matcher hasChildWithBothChildren = withChild(allOf(hasTitleChildWithAppropriateContent, 181 | hasRatingChildWithAppropriateContent)); 182 | 183 | 184 | //Grab the recycler view at the given index, and make sure all children match. 185 | onRecyclerItemViewAtPosition(R.id.wine_tasting_recyclerview, 0) 186 | .check(matches(hasChildWithBothChildren)); 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 13 | 14 | 15 | 16 | 17 | 18 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /app/src/main/java/com/designatednerd/wino/SharedPreferencesHelper.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino; 2 | 3 | import android.content.Context; 4 | import android.content.SharedPreferences; 5 | 6 | import com.designatednerd.wino.model.WineTasting; 7 | import com.google.gson.Gson; 8 | import com.google.gson.GsonBuilder; 9 | 10 | import java.util.Arrays; 11 | import java.util.List; 12 | 13 | /** 14 | * Helper class to deal with accessing data in shared preferences. 15 | */ 16 | public class SharedPreferencesHelper { 17 | 18 | //Individual preference keys. 19 | private static final String STORED_TASTINGS = "tastings"; 20 | 21 | //The full application preference name. 22 | private static final String APP_PREFERENCE_NAME = "com.designatednerd.wino.sharedprefs"; 23 | 24 | private static SharedPreferencesHelper sSharedInstance; 25 | private SharedPreferences mSharedPreferences; 26 | 27 | //A GSON adapter to help serialize/deserialize JSON 28 | private static final Gson PREFS_GSON = new GsonBuilder().create(); 29 | 30 | /** 31 | * Designated initializer. 32 | * 33 | * @param aContext The current context 34 | */ 35 | public static SharedPreferencesHelper getInstance(Context aContext) { 36 | //Create new if needed. 37 | if (sSharedInstance == null) { 38 | sSharedInstance = new SharedPreferencesHelper(); 39 | 40 | //Grab the shared preferences directly so we don't need the context anymore. 41 | sSharedInstance.mSharedPreferences = aContext.getSharedPreferences(APP_PREFERENCE_NAME, Context.MODE_PRIVATE); 42 | } 43 | 44 | return sSharedInstance; 45 | } 46 | 47 | /** 48 | * @return The current SharedPreferences.Editor object 49 | */ 50 | private SharedPreferences.Editor getSharedPreferencesEditor() { 51 | return mSharedPreferences.edit(); 52 | } 53 | 54 | /** 55 | * Chokepoint method to reset all shared preferences to default values. 56 | */ 57 | public void nukeAllSharedPreferences() { 58 | setCurrentTastings(null); 59 | writeAllToDisk(); 60 | } 61 | 62 | public void writeAllToDisk() { 63 | getSharedPreferencesEditor().commit(); 64 | } 65 | 66 | public void setCurrentTastings(List aTastings) { 67 | SharedPreferences.Editor editor = getSharedPreferencesEditor(); 68 | if (aTastings != null) { 69 | 70 | WineTasting[] tastingsArray = aTastings.toArray(new WineTasting[aTastings.size()]); 71 | String json = PREFS_GSON.toJson(tastingsArray); 72 | 73 | editor.putString(STORED_TASTINGS, json); 74 | } else { 75 | editor.remove(STORED_TASTINGS); 76 | } 77 | 78 | editor.apply(); 79 | } 80 | 81 | public List getCurrentTastings() { 82 | String json = mSharedPreferences.getString(STORED_TASTINGS, null); 83 | if (json != null ) { 84 | WineTasting[] tastings = PREFS_GSON.fromJson(json, WineTasting[].class); 85 | return Arrays.asList(tastings); 86 | } else { 87 | return null; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /app/src/main/java/com/designatednerd/wino/activity/WineTastingActivity.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino.activity; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import com.designatednerd.wino.R; 7 | import com.designatednerd.wino.fragment.WineTastingListFragment; 8 | 9 | /** 10 | * The main activity 11 | */ 12 | public class WineTastingActivity extends AppCompatActivity { 13 | 14 | @Override 15 | public void onCreate(Bundle savedInstanceState) { 16 | super.onCreate(savedInstanceState); 17 | setContentView(R.layout.activity_wine_tasting_frame); 18 | 19 | //Get in to the first fragment 20 | WineTastingListFragment listFragment = new WineTastingListFragment(); 21 | getSupportFragmentManager() 22 | .beginTransaction() 23 | .add(R.id.wine_tasting_frame, listFragment, "LIST") 24 | .commit(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /app/src/main/java/com/designatednerd/wino/activity/WineTastingDetailActivity.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino.activity; 2 | 3 | import android.os.Bundle; 4 | import android.support.v7.app.AppCompatActivity; 5 | 6 | import com.designatednerd.wino.R; 7 | import com.designatednerd.wino.fragment.WineTastingDetailFragment; 8 | import com.designatednerd.wino.model.WineTasting; 9 | 10 | public class WineTastingDetailActivity extends AppCompatActivity { 11 | 12 | public static final String TASTING_TO_DISPLAY_EXTRA = "com.designatednerd.wino.tastingtodisplay"; 13 | public static final String IS_EDITING_EXTRA = "com.designatednerd.wino.isediting"; 14 | 15 | @Override 16 | public void onCreate(Bundle savedInstanceState) { 17 | super.onCreate(savedInstanceState); 18 | setContentView(R.layout.activity_wine_tasting_frame); 19 | 20 | boolean isEditing = getIntent().getBooleanExtra(IS_EDITING_EXTRA, false); 21 | WineTasting tasting = getIntent().getParcelableExtra(TASTING_TO_DISPLAY_EXTRA); 22 | 23 | //Show the detail 24 | WineTastingDetailFragment detailFragment = new WineTastingDetailFragment(); 25 | detailFragment.setTasting(tasting); 26 | detailFragment.setIsEditing(isEditing); 27 | getSupportFragmentManager() 28 | .beginTransaction() 29 | .add(R.id.wine_tasting_frame, detailFragment, "DETAIL") 30 | .commit(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/src/main/java/com/designatednerd/wino/adapter/WineTastingAdapter.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino.adapter; 2 | 3 | import android.support.v7.widget.RecyclerView; 4 | import android.view.LayoutInflater; 5 | import android.view.View; 6 | import android.view.ViewGroup; 7 | import android.widget.TextView; 8 | 9 | import com.designatednerd.wino.R; 10 | import com.designatednerd.wino.model.TastingDateFormatter; 11 | import com.designatednerd.wino.model.WineTasting; 12 | 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | import butterknife.Bind; 17 | import butterknife.ButterKnife; 18 | import butterknife.OnClick; 19 | 20 | public class WineTastingAdapter extends RecyclerView.Adapter { 21 | 22 | /************** 23 | * INTERFACES * 24 | **************/ 25 | 26 | public interface TastingSelectedListener { 27 | /** 28 | * Called whenever a tasting is selected. 29 | * @param aTasting The tasting which was selected. 30 | */ 31 | void selectedTasting(WineTasting aTasting); 32 | } 33 | 34 | /************* 35 | * VARIABLES * 36 | *************/ 37 | 38 | private List mTastings; 39 | private TastingSelectedListener mListener; 40 | 41 | /** 42 | * Designated constructor. 43 | * @param aTastings An array of existing tastings, or null 44 | * @param aListener A TastingSelectedListener object. 45 | */ 46 | public WineTastingAdapter(List aTastings, 47 | TastingSelectedListener aListener) { 48 | mListener = aListener; 49 | updateTastings(aTastings); 50 | } 51 | 52 | /** 53 | * Adds a tasting object to the list. 54 | * @param aTasting The tasting object to add. 55 | */ 56 | public void addTasting(WineTasting aTasting) { 57 | mTastings.add(aTasting); 58 | notifyDataSetChanged(); 59 | } 60 | 61 | public void updateTastings(List aTastings) { 62 | if (aTastings != null) { 63 | mTastings = aTastings; 64 | } else { 65 | mTastings = new ArrayList<>(); 66 | } 67 | notifyDataSetChanged(); 68 | } 69 | 70 | /************************ 71 | * SUPERCLASS OVERRIDES * 72 | ************************/ 73 | 74 | @Override 75 | public WineTastingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { 76 | LayoutInflater inflater = LayoutInflater.from(parent.getContext()); 77 | View inflated = inflater.inflate(R.layout.row_wine_tasting, parent, false); 78 | return new WineTastingViewHolder(inflated); 79 | } 80 | 81 | @Override 82 | public void onBindViewHolder(WineTastingViewHolder holder, int position) { 83 | WineTasting tasting = mTastings.get(position); 84 | holder.configureForTasting(tasting); 85 | } 86 | 87 | @Override 88 | public int getItemCount() { 89 | if (mTastings != null) { 90 | return mTastings.size(); 91 | } else { 92 | return 0; 93 | } 94 | } 95 | 96 | /*************** 97 | * VIEW HOLDER * 98 | ***************/ 99 | 100 | class WineTastingViewHolder extends RecyclerView.ViewHolder { 101 | 102 | /************* 103 | * VARIABLES * 104 | *************/ 105 | 106 | private WineTasting mTasting; 107 | 108 | @Bind(R.id.row_tasting_wine_name_textview) TextView mWineNameTextView; 109 | @Bind(R.id.row_tasting_wine_rating_textview) TextView mWineRatingTextView; 110 | @Bind(R.id.row_tasting_date_textview) TextView mTastingDateTextView; 111 | 112 | /*************** 113 | * CONSTRUCTOR * 114 | ***************/ 115 | 116 | public WineTastingViewHolder(View itemView) { 117 | super(itemView); 118 | ButterKnife.bind(this, itemView); 119 | } 120 | 121 | /***************** 122 | * CONFIGURATION * 123 | *****************/ 124 | 125 | public void configureForTasting(WineTasting aTasting) { 126 | mTasting = aTasting; 127 | mWineNameTextView.setText(mTasting.getFullWineName()); 128 | mWineRatingTextView.setText(mTasting.getRatingString()); 129 | String formattedDate = TastingDateFormatter.shortFormattedDate(mTasting.tastingDate, 130 | itemView.getContext()); 131 | mTastingDateTextView.setText(formattedDate); 132 | } 133 | 134 | /******************** 135 | * ONCLICK LISTENER * 136 | ********************/ 137 | 138 | @OnClick(R.id.row_tasting) 139 | public void rowTapped() { 140 | if (mListener != null) { 141 | mListener.selectedTasting(mTasting); 142 | } else { 143 | throw new RuntimeException("Hey you might want a listener for this!"); 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /app/src/main/java/com/designatednerd/wino/dialog/DatePickerDialogFragment.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino.dialog; 2 | 3 | import android.app.DatePickerDialog; 4 | import android.app.Dialog; 5 | import android.os.Bundle; 6 | import android.support.v4.app.DialogFragment; 7 | import android.widget.DatePicker; 8 | 9 | import java.util.Calendar; 10 | import java.util.Date; 11 | 12 | public class DatePickerDialogFragment extends DialogFragment 13 | implements DatePickerDialog.OnDateSetListener { 14 | 15 | public interface DateSelectedListener { 16 | void selectedDate(Date aDate); 17 | } 18 | 19 | private Date mDate; 20 | private DateSelectedListener mListener; 21 | 22 | public void setDate(Date aDate) { 23 | mDate = aDate; 24 | } 25 | 26 | public void setListener(DateSelectedListener aListener) { 27 | mListener = aListener; 28 | } 29 | 30 | @Override 31 | public Dialog onCreateDialog(Bundle savedInstanceState) { 32 | Calendar calendar = Calendar.getInstance(); 33 | if (mDate == null) { 34 | mDate = new Date(); 35 | } 36 | calendar.setTime(mDate); 37 | 38 | int year = calendar.get(Calendar.YEAR); 39 | int month = calendar.get(Calendar.MONTH); 40 | int day = calendar.get(Calendar.DAY_OF_MONTH); 41 | 42 | // Create a new instance of DatePickerDialog and return it 43 | return new DatePickerDialog(getActivity(), this, year, month, day); 44 | } 45 | 46 | public void onDateSet(DatePicker view, int year, int month, int day) { 47 | if (mListener != null) { 48 | Calendar calendar = Calendar.getInstance(); 49 | calendar.set(year, month, day); 50 | Date dateFromCalendar = calendar.getTime(); 51 | mListener.selectedDate(dateFromCalendar); 52 | } else { 53 | throw new RuntimeException("You're probably gonna want a listener for this."); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /app/src/main/java/com/designatednerd/wino/dialog/RatingPickerDialogFragment.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino.dialog; 2 | 3 | /** 4 | * Created by ellen on 7/14/15. 5 | */ 6 | public class RatingPickerDialogFragment { 7 | } 8 | -------------------------------------------------------------------------------- /app/src/main/java/com/designatednerd/wino/fragment/WineTastingDetailFragment.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino.fragment; 2 | 3 | import android.os.Bundle; 4 | import android.support.v4.app.Fragment; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.ArrayAdapter; 9 | import android.widget.Button; 10 | import android.widget.EditText; 11 | import android.widget.Spinner; 12 | 13 | import com.designatednerd.wino.R; 14 | import com.designatednerd.wino.SharedPreferencesHelper; 15 | import com.designatednerd.wino.dialog.DatePickerDialogFragment; 16 | import com.designatednerd.wino.model.TastingDateFormatter; 17 | import com.designatednerd.wino.model.WineTasting; 18 | 19 | import java.util.ArrayList; 20 | import java.util.Date; 21 | import java.util.List; 22 | 23 | import butterknife.Bind; 24 | import butterknife.ButterKnife; 25 | import butterknife.OnClick; 26 | 27 | /** 28 | * A fragment representing a single Wine Tasting. 29 | */ 30 | public class WineTastingDetailFragment extends Fragment implements DatePickerDialogFragment.DateSelectedListener { 31 | 32 | /************* 33 | * VARIABLES * 34 | *************/ 35 | 36 | private WineTasting mTasting; 37 | private boolean mIsEditing; 38 | 39 | @Bind(R.id.tasting_detail_vineyard_name_edittext) EditText mVineyardNameEditText; 40 | @Bind(R.id.tasting_detail_wine_name_edittext) EditText mWineNameEditText; 41 | @Bind(R.id.tasting_detail_varietal_edittext) EditText mVarietalEditText; 42 | @Bind(R.id.tasting_detail_tasting_date_button) Button mTastingDateButton; 43 | @Bind(R.id.tasting_detail_save_button) Button mSaveButton; 44 | @Bind(R.id.tasting_detail_rating_spinner) Spinner mRatingSpinner; 45 | 46 | /****************** 47 | * VIEW LIFECYCLE * 48 | ******************/ 49 | 50 | @Override 51 | public View onCreateView(LayoutInflater inflater, 52 | ViewGroup container, 53 | Bundle savedInstanceState) { 54 | View rootView = inflater.inflate(R.layout.fragment_wine_tasting_detail, container, false); 55 | ButterKnife.bind(this, rootView); 56 | 57 | //Setup spinner 58 | ArrayAdapter adapter = ArrayAdapter.createFromResource(getActivity(), 59 | R.array.ratings_array, 60 | android.R.layout.simple_spinner_item); 61 | adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); 62 | mRatingSpinner.setAdapter(adapter); 63 | 64 | //Configure for editability 65 | setIsEditing(mIsEditing); 66 | configureForCurrentTasting(); 67 | 68 | return rootView; 69 | } 70 | 71 | /****************** 72 | * PUBLIC SETTERS * 73 | ******************/ 74 | 75 | public void setIsEditing(boolean aIsEditing) { 76 | mIsEditing = aIsEditing; 77 | 78 | if (butterknifeHasFired()) { 79 | mVineyardNameEditText.setEnabled(mIsEditing); 80 | mWineNameEditText.setEnabled(mIsEditing); 81 | mVarietalEditText.setEnabled(mIsEditing); 82 | mTastingDateButton.setEnabled(mIsEditing); 83 | mRatingSpinner.setEnabled(mIsEditing); 84 | 85 | if (mIsEditing) { 86 | mSaveButton.setVisibility(View.VISIBLE); 87 | } else { 88 | mSaveButton.setVisibility(View.GONE); 89 | } 90 | } 91 | } 92 | 93 | public void setTasting(WineTasting aTasting) { 94 | mTasting = aTasting; 95 | configureForCurrentTasting(); 96 | } 97 | 98 | /************************* 99 | * DISPLAY CONFIGURATION * 100 | *************************/ 101 | 102 | private boolean butterknifeHasFired() { 103 | //If butterknife hasn't fired yet, crashes galore since all bound 104 | //variables will be null. 105 | return (mVineyardNameEditText != null); 106 | } 107 | 108 | private void configureForCurrentTasting() { 109 | if (mTasting != null && butterknifeHasFired()) { 110 | mVineyardNameEditText.setText(mTasting.vineyardName); 111 | mWineNameEditText.setText(mTasting.wineName); 112 | mVarietalEditText.setText(mTasting.wineVarietal); 113 | showTastingDisplayDate(); 114 | mRatingSpinner.setSelection(mTasting.rating); 115 | } 116 | } 117 | 118 | private void showTastingDisplayDate() { 119 | String tastingDate = TastingDateFormatter.shortFormattedDate(mTasting.tastingDate, getActivity()); 120 | mTastingDateButton.setText(tastingDate); 121 | } 122 | 123 | /********************** 124 | * ON CLICK LISTENERS * 125 | **********************/ 126 | 127 | @OnClick(R.id.tasting_detail_save_button) 128 | public void saveTasting() { 129 | mTasting.vineyardName = mVineyardNameEditText.getText().toString(); 130 | mTasting.wineName = mWineNameEditText.getText().toString(); 131 | mTasting.wineVarietal = mVarietalEditText.getText().toString(); 132 | mTasting.rating = mRatingSpinner.getSelectedItemPosition(); 133 | 134 | SharedPreferencesHelper preferencesHelper = SharedPreferencesHelper.getInstance(getActivity()); 135 | List currentTastings = preferencesHelper.getCurrentTastings(); 136 | if (currentTastings == null) { 137 | currentTastings = new ArrayList<>(); 138 | } 139 | currentTastings.add(mTasting); 140 | preferencesHelper.setCurrentTastings(currentTastings); 141 | } 142 | 143 | 144 | @OnClick(R.id.tasting_detail_tasting_date_button) 145 | public void showDatePicker() { 146 | DatePickerDialogFragment datePicker = new DatePickerDialogFragment(); 147 | datePicker.setDate(mTasting.tastingDate); 148 | datePicker.setListener(this); 149 | datePicker.show(getFragmentManager(), "DATE"); 150 | } 151 | 152 | /************************** 153 | * DATE SELECTED LISTENER * 154 | **************************/ 155 | 156 | @Override 157 | public void selectedDate(Date aDate) { 158 | mTasting.tastingDate = aDate; 159 | showTastingDisplayDate(); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /app/src/main/java/com/designatednerd/wino/fragment/WineTastingListFragment.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino.fragment; 2 | 3 | import android.content.Intent; 4 | import android.os.Bundle; 5 | import android.support.v4.app.Fragment; 6 | import android.support.v7.widget.LinearLayoutManager; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | 12 | 13 | import com.designatednerd.wino.R; 14 | import com.designatednerd.wino.SharedPreferencesHelper; 15 | import com.designatednerd.wino.activity.WineTastingActivity; 16 | import com.designatednerd.wino.activity.WineTastingDetailActivity; 17 | import com.designatednerd.wino.adapter.WineTastingAdapter; 18 | import com.designatednerd.wino.adapter.WineTastingAdapter.TastingSelectedListener; 19 | import com.designatednerd.wino.model.WineTasting; 20 | 21 | import java.util.List; 22 | 23 | import butterknife.Bind; 24 | import butterknife.ButterKnife; 25 | import butterknife.OnClick; 26 | 27 | /** 28 | * A list fragment representing a list of WineTastings. 29 | */ 30 | public class WineTastingListFragment extends Fragment implements TastingSelectedListener { 31 | 32 | /************* 33 | * VARIABLES * 34 | *************/ 35 | 36 | private WineTastingAdapter mAdapter; 37 | 38 | @Bind(R.id.wine_tasting_recyclerview) RecyclerView mRecyclerView; 39 | 40 | /********************** 41 | * FRAGMENT LIFECYCLE * 42 | **********************/ 43 | 44 | @Override 45 | public View onCreateView(LayoutInflater inflater, 46 | ViewGroup container, 47 | Bundle savedInstanceState) { 48 | View main = inflater.inflate(R.layout.fragment_wine_tasting_list, container, false); 49 | 50 | //Fire off butterknife to populate bound variables 51 | ButterKnife.bind(this, main); 52 | 53 | //Add layout manager to recyclerview 54 | LinearLayoutManager layoutManager = new LinearLayoutManager(getActivity()); 55 | mRecyclerView.setLayoutManager(layoutManager); 56 | 57 | return main; 58 | } 59 | 60 | @Override 61 | public void onResume() { 62 | super.onResume(); 63 | 64 | List tastings = SharedPreferencesHelper.getInstance(getActivity()).getCurrentTastings(); 65 | 66 | if (mAdapter == null) { 67 | //Setup adapter 68 | mAdapter = new WineTastingAdapter(tastings, this); 69 | mRecyclerView.setAdapter(mAdapter); 70 | } else { 71 | mAdapter.updateTastings(tastings); 72 | } 73 | } 74 | 75 | /********************* 76 | * FRAGMENT FUNTIMES * 77 | *********************/ 78 | 79 | private void showTastingDetail(WineTasting aTasting, boolean aIsEditable) { 80 | Intent tastingDetailIntent = new Intent(getActivity(), WineTastingDetailActivity.class); 81 | tastingDetailIntent.putExtra(WineTastingDetailActivity.IS_EDITING_EXTRA, aIsEditable); 82 | tastingDetailIntent.putExtra(WineTastingDetailActivity.TASTING_TO_DISPLAY_EXTRA, aTasting); 83 | startActivity(tastingDetailIntent); 84 | } 85 | 86 | /********************* 87 | * ON CLICK LISTENER * 88 | *********************/ 89 | 90 | @OnClick(R.id.add_tasting_button) 91 | public void addTasting() { 92 | WineTasting tasting = new WineTasting(); 93 | showTastingDetail(tasting, true); 94 | } 95 | 96 | /***************************** 97 | * TASTING SELECTED LISTENER * 98 | *****************************/ 99 | 100 | public void selectedTasting(WineTasting aTasting) { 101 | showTastingDetail(aTasting, false); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/com/designatednerd/wino/model/TastingDateFormatter.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino.model; 2 | 3 | import android.content.Context; 4 | 5 | import java.text.DateFormat; 6 | import java.util.Date; 7 | 8 | public class TastingDateFormatter { 9 | 10 | public static String shortFormattedDate(Date aDate, Context aContext) { 11 | //Get the system short-form formatter. 12 | DateFormat format = android.text.format.DateFormat.getDateFormat(aContext); 13 | return format.format(aDate); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app/src/main/java/com/designatednerd/wino/model/WineTasting.java: -------------------------------------------------------------------------------- 1 | package com.designatednerd.wino.model; 2 | 3 | import android.content.Context; 4 | import android.os.Parcel; 5 | import android.os.Parcelable; 6 | 7 | import com.designatednerd.wino.R; 8 | 9 | import java.util.Date; 10 | 11 | public class WineTasting implements Parcelable { 12 | 13 | public enum WineType { 14 | UNKNOWN, 15 | RED, 16 | WHITE, 17 | ROSE; 18 | 19 | public String getDisplayName(Context aContext) { 20 | switch (this) { 21 | case RED: 22 | return aContext.getString(R.string.red); 23 | case WHITE: 24 | return aContext.getString(R.string.white); 25 | case ROSE: 26 | return aContext.getString(R.string.rose); 27 | default: 28 | return aContext.getString(R.string.not_set); 29 | } 30 | } 31 | 32 | public String parcelableName() { 33 | switch (this) { 34 | case RED: 35 | return "red"; 36 | case WHITE: 37 | return "white"; 38 | case ROSE: 39 | return "rose"; 40 | default: 41 | return "unknown"; 42 | } 43 | } 44 | 45 | public static WineType fromString(String aString) { 46 | if (aString != null) { 47 | if (aString.equalsIgnoreCase(RED.parcelableName())) { 48 | return RED; 49 | } else if (aString.equalsIgnoreCase(WHITE.parcelableName())) { 50 | return WHITE; 51 | } else if (aString.equalsIgnoreCase(ROSE.parcelableName())) { 52 | return ROSE; 53 | } 54 | } 55 | 56 | //Fall-through case 57 | return UNKNOWN; 58 | } 59 | } 60 | 61 | /************* 62 | * VARIABLES * 63 | *************/ 64 | 65 | public String vineyardName; 66 | public String wineName; 67 | public WineType wineType; 68 | public String wineVarietal; 69 | public int rating; 70 | public Date tastingDate; 71 | 72 | /*************** 73 | * CONSTRUCTOR * 74 | ***************/ 75 | 76 | public WineTasting() { 77 | //Default to using the current date. 78 | tastingDate = new Date(); 79 | wineType = WineType.UNKNOWN; 80 | } 81 | 82 | /****************** 83 | * STATIC METHODS * 84 | ******************/ 85 | 86 | public static String wineNameFromInfo(String aWineName, String aVineyardName, String aVarietal) { 87 | if (aWineName != null 88 | && aVineyardName != null 89 | && aVarietal != null) { //got everything 90 | return aWineName + " (" + aVarietal + ", " + aVineyardName + ")"; 91 | } else if (aWineName != null 92 | && aVineyardName !=null) { //No varietal 93 | return aWineName + " (" + aVineyardName + ")"; 94 | } else if (aWineName != null 95 | && aVarietal != null) { //no vineyard 96 | return aWineName + " (" + aVarietal + ")"; 97 | } else if (aWineName != null) { //wine name only 98 | return aWineName; 99 | } else { //Nothing specified 100 | return ""; 101 | } 102 | } 103 | 104 | /************ 105 | * EQUALITY * 106 | ************/ 107 | @Override 108 | public boolean equals(Object aAnother) { 109 | if (aAnother instanceof WineTasting) { 110 | WineTasting other = (WineTasting)aAnother; 111 | if (wineName.equals(other.wineName) 112 | && vineyardName.equals(other.vineyardName) 113 | && wineVarietal.equals(other.wineVarietal) 114 | && tastingDate.equals(other.tastingDate) 115 | && wineType.equals(other.wineType)) { 116 | //If all of these are the same, the wines are equal 117 | return true; 118 | } 119 | } 120 | 121 | //Fall-through 122 | return false; 123 | } 124 | 125 | /******************** 126 | * PARCELABLE STUFF * 127 | ********************/ 128 | 129 | protected WineTasting(Parcel in) { 130 | vineyardName = in.readString(); 131 | wineName = in.readString(); 132 | String wineTypeName = in.readString(); 133 | wineType = WineType.fromString(wineTypeName); 134 | wineVarietal = in.readString(); 135 | rating = in.readInt(); 136 | long tmpTastingDate = in.readLong(); 137 | tastingDate = tmpTastingDate != -1 ? new Date(tmpTastingDate) : null; 138 | } 139 | 140 | @Override 141 | public int describeContents() { 142 | return 0; 143 | } 144 | 145 | @Override 146 | public void writeToParcel(Parcel dest, int flags) { 147 | dest.writeString(vineyardName); 148 | dest.writeString(wineName); 149 | dest.writeString(wineType.parcelableName()); 150 | dest.writeString(wineVarietal); 151 | dest.writeInt(rating); 152 | dest.writeLong(tastingDate != null ? tastingDate.getTime() : -1L); 153 | } 154 | 155 | public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { 156 | @Override 157 | public WineTasting createFromParcel(Parcel in) { 158 | return new WineTasting(in); 159 | } 160 | 161 | @Override 162 | public WineTasting[] newArray(int size) { 163 | return new WineTasting[size]; 164 | } 165 | }; 166 | 167 | 168 | /*************** 169 | * CONVENIENCE * 170 | ***************/ 171 | 172 | public String getFullWineName() { 173 | return wineNameFromInfo(wineName, vineyardName, wineVarietal); 174 | } 175 | 176 | public String getRatingString() { 177 | if (rating == 0) { 178 | int thumbsDownEmoji = 0x1F44E; 179 | return new String(Character.toChars(thumbsDownEmoji)); 180 | } else { 181 | int loops = rating; 182 | String ratingString = ""; 183 | int starEmoji = 0x1F31F; 184 | while (loops > 0) { 185 | ratingString = ratingString + new String(Character.toChars(starEmoji)); 186 | loops--; 187 | } 188 | 189 | return ratingString; 190 | } 191 | } 192 | } -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_add_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/designatednerd/Wino/8ca827435072d2585d929296dcb538aa0bc9af53/app/src/main/res/drawable-hdpi/ic_add_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_create_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/designatednerd/Wino/8ca827435072d2585d929296dcb538aa0bc9af53/app/src/main/res/drawable-hdpi/ic_create_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_add_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/designatednerd/Wino/8ca827435072d2585d929296dcb538aa0bc9af53/app/src/main/res/drawable-mdpi/ic_add_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-mdpi/ic_create_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/designatednerd/Wino/8ca827435072d2585d929296dcb538aa0bc9af53/app/src/main/res/drawable-mdpi/ic_create_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/designatednerd/Wino/8ca827435072d2585d929296dcb538aa0bc9af53/app/src/main/res/drawable-xhdpi/ic_add_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_create_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/designatednerd/Wino/8ca827435072d2585d929296dcb538aa0bc9af53/app/src/main/res/drawable-xhdpi/ic_create_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/designatednerd/Wino/8ca827435072d2585d929296dcb538aa0bc9af53/app/src/main/res/drawable-xxhdpi/ic_add_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_create_white_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/designatednerd/Wino/8ca827435072d2585d929296dcb538aa0bc9af53/app/src/main/res/drawable-xxhdpi/ic_create_white_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_wine_tasting_frame.xml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_wine_tasting_detail.xml: -------------------------------------------------------------------------------- 1 | 8 | 15 | 16 | 20 | 27 | 28 | 29 | 30 | 34 | 41 | 42 | 43 | 44 | 45 | 49 | 56 | 57 | 58 | 59 | 64 |