├── settings.gradle
├── AndroidBootcamp
├── .gitignore
├── src
│ ├── main
│ │ ├── ic_launcher-web.png
│ │ ├── assets
│ │ │ └── treasures
│ │ │ │ ├── Treasures1.jpg
│ │ │ │ ├── Treasures2.jpg
│ │ │ │ ├── Treasures3.jpg
│ │ │ │ ├── Treasures4.jpg
│ │ │ │ ├── Treasures5.jpg
│ │ │ │ ├── Treasures6.jpg
│ │ │ │ ├── Treasures7.jpg
│ │ │ │ └── Treasures8.jpg
│ │ ├── res
│ │ │ ├── drawable-hdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── drawable-mdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── drawable-xhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── drawable-xxhdpi
│ │ │ │ └── ic_launcher.png
│ │ │ ├── values
│ │ │ │ ├── colors.xml
│ │ │ │ ├── dimens.xml
│ │ │ │ ├── strings.xml
│ │ │ │ └── styles.xml
│ │ │ ├── layout
│ │ │ │ ├── activity_hello.xml
│ │ │ │ ├── fragment_map.xml
│ │ │ │ ├── fragment_treasure_list.xml
│ │ │ │ ├── activity_whoami.xml
│ │ │ │ └── fragment_high_scores.xml
│ │ │ ├── values-w820dp
│ │ │ │ └── dimens.xml
│ │ │ └── menu
│ │ │ │ ├── who_am_i.xml
│ │ │ │ └── hello_android.xml
│ │ ├── java
│ │ │ └── com
│ │ │ │ └── thoughtworks
│ │ │ │ └── androidbootcamp
│ │ │ │ ├── util
│ │ │ │ ├── Properties.java
│ │ │ │ ├── TreasureService.java
│ │ │ │ ├── FileUtils.java
│ │ │ │ └── TreasureLoader.java
│ │ │ │ ├── model
│ │ │ │ ├── Locatable.java
│ │ │ │ ├── Score.java
│ │ │ │ ├── Attempt.java
│ │ │ │ ├── Treasure.java
│ │ │ │ └── Game.java
│ │ │ │ └── controller
│ │ │ │ ├── WhoAmIActivity.java
│ │ │ │ ├── adapter
│ │ │ │ ├── HighScoreAdapter.java
│ │ │ │ └── TreasureListAdapter.java
│ │ │ │ ├── fragment
│ │ │ │ ├── HighScoresFragment.java
│ │ │ │ ├── TreasureMapFragment.java
│ │ │ │ └── TreasureListFragment.java
│ │ │ │ └── HelloAndroid.java
│ │ └── AndroidManifest.xml
│ └── androidTest
│ │ ├── libs
│ │ └── espresso-1.0-SNAPSHOT-bundled.jar
│ │ └── java
│ │ └── com
│ │ └── thoughtworks
│ │ └── androidbootcamp
│ │ └── test
│ │ ├── helpers
│ │ ├── Given.java
│ │ └── When.java
│ │ └── HelloAndroidTest.java
└── build.gradle
├── .gitignore
├── AndroidBootcampTest
├── .gitignore
├── src
│ └── test
│ │ ├── resources
│ │ └── org.robolectric.Config.properties
│ │ └── java
│ │ └── com
│ │ └── thoughtworks
│ │ └── androidbootcamp
│ │ ├── model
│ │ ├── AttemptTest.java
│ │ └── GameTest.java
│ │ └── controller
│ │ ├── adapter
│ │ └── TreasureListAdapterTest.java
│ │ ├── fragment
│ │ └── TreasureListFragmentTest.java
│ │ └── HelloAndroidUnitTest.java
└── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew.bat
├── BDDinAS.md
├── gradlew
└── README.md
/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':AndroidBootcamp', ':AndroidBootcampTest'
--------------------------------------------------------------------------------
/AndroidBootcamp/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | /.idea/
3 | .DS_Store
4 | *.iml
5 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | /local.properties
3 | /.idea/
4 | .DS_Store
5 | *.iml
6 |
--------------------------------------------------------------------------------
/AndroidBootcampTest/.gitignore:
--------------------------------------------------------------------------------
1 | /build
2 | .gradle
3 | /local.properties
4 | /.idea/
5 | .DS_Store
6 | *.iml
--------------------------------------------------------------------------------
/AndroidBootcampTest/src/test/resources/org.robolectric.Config.properties:
--------------------------------------------------------------------------------
1 | manifest=../AndroidBootcamp/src/main/AndroidManifest.xml
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/ic_launcher-web.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/AndroidBootcamp/src/main/ic_launcher-web.png
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/assets/treasures/Treasures1.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/AndroidBootcamp/src/main/assets/treasures/Treasures1.jpg
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/assets/treasures/Treasures2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/AndroidBootcamp/src/main/assets/treasures/Treasures2.jpg
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/assets/treasures/Treasures3.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/AndroidBootcamp/src/main/assets/treasures/Treasures3.jpg
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/assets/treasures/Treasures4.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/AndroidBootcamp/src/main/assets/treasures/Treasures4.jpg
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/assets/treasures/Treasures5.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/AndroidBootcamp/src/main/assets/treasures/Treasures5.jpg
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/assets/treasures/Treasures6.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/AndroidBootcamp/src/main/assets/treasures/Treasures6.jpg
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/assets/treasures/Treasures7.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/AndroidBootcamp/src/main/assets/treasures/Treasures7.jpg
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/assets/treasures/Treasures8.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/AndroidBootcamp/src/main/assets/treasures/Treasures8.jpg
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/drawable-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/AndroidBootcamp/src/main/res/drawable-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/drawable-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/AndroidBootcamp/src/main/res/drawable-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/drawable-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/AndroidBootcamp/src/main/res/drawable-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/drawable-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/AndroidBootcamp/src/main/res/drawable-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/AndroidBootcamp/src/androidTest/libs/espresso-1.0-SNAPSHOT-bundled.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ThoughtWorksAustralia/AndroidBootcampProject/HEAD/AndroidBootcamp/src/androidTest/libs/espresso-1.0-SNAPSHOT-bundled.jar
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #F68E2D
4 | #FFFFFF
5 | #F6E9DD
6 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Wed Feb 19 23:03:56 EST 2014
2 | distributionBase=GRADLE_USER_HOME
3 | distributionPath=wrapper/dists
4 | zipStoreBase=GRADLE_USER_HOME
5 | zipStorePath=wrapper/dists
6 | distributionUrl=http\://services.gradle.org/distributions/gradle-1.11-all.zip
7 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/util/Properties.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.util;
2 |
3 | /**
4 | * Created by trogdor on 2/04/14.
5 | */
6 | public class Properties {
7 | public static String SERVICE_URL = "http://android-bootcamp-rest-server.herokuapp.com";
8 | }
9 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/layout/activity_hello.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/model/Locatable.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.model;
2 |
3 | /**
4 | * Created by macosgrove on 27/04/2014.
5 | */
6 | public interface Locatable {
7 | public double getLatitude();
8 | public double getLongitude();
9 | public void setCoordinates(double latitude, double longitude);
10 | public String getName();
11 | }
12 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/values-w820dp/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 64dp
6 |
7 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/layout/fragment_map.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/menu/who_am_i.xml:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/menu/hello_android.xml:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/AndroidBootcampTest/src/test/java/com/thoughtworks/androidbootcamp/model/AttemptTest.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.model;
2 |
3 | import org.junit.Test;
4 |
5 | import static org.hamcrest.MatcherAssert.assertThat;
6 | import static org.hamcrest.core.IsEqual.equalTo;
7 |
8 | /**
9 | * Created by macosgrove on 4/05/2014.
10 | */
11 | public class AttemptTest {
12 |
13 | @Test
14 | public void nameShouldIncludeCountAndDistance() {
15 | Attempt attempt = new Attempt(1, 2, "", 7);
16 | attempt.setDistance(53);
17 | assertThat(attempt.getName(), equalTo("Attempt 7: 53m"));
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/values/dimens.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | 16dp
4 | 16dp
5 | 16dp
6 | 24dp
7 |
8 | 36sp
9 | 24sp
10 | 20sp
11 | 160dp
12 | 10dp
13 |
14 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/util/TreasureService.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.util;
2 |
3 | import com.thoughtworks.androidbootcamp.model.Score;
4 | import com.thoughtworks.androidbootcamp.model.Treasure;
5 |
6 | import java.util.List;
7 |
8 | import retrofit.Callback;
9 | import retrofit.http.Body;
10 | import retrofit.http.GET;
11 | import retrofit.http.POST;
12 |
13 | /**
14 | * Created by trogdor on 2/04/14.
15 | */
16 | public interface TreasureService {
17 | @GET("/treasures")
18 | List listTreasures();
19 |
20 | @GET("/players/top/10")
21 | List listHighScores();
22 |
23 | @POST("/players")
24 | void postScore(@Body Score score, Callback scoreCallback);
25 | }
26 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/androidTest/java/com/thoughtworks/androidbootcamp/test/helpers/Given.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.test.helpers;
2 |
3 | import com.thoughtworks.androidbootcamp.R;
4 |
5 | import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
6 | import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
7 | import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.typeText;
8 | import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
9 |
10 | /**
11 | * Created by macosgrove on 4/01/2014.
12 | * Setup methods to make the functional tests more readable, in BDD style
13 | */
14 | public class Given {
15 | public static void thePlayerHasEnteredTheirName() {
16 | onView(withId(R.id.player_field)).perform(typeText("Joe"));
17 | onView(withId(R.id.player_ok_button)).perform(click());
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | AndroidBootcamp
5 | Treasure List
6 | High Scores
7 | Map
8 | Settings
9 | Who Am I
10 | What is your name?
11 | OK
12 | Find these treasures:
13 | High Scores:
14 | Map:
15 | Hello blank fragment
16 | Finish the Game!
17 |
18 | Good game! Your final score was %d. \nCheck out the Treasure Map to see where all the treasures are.
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/androidTest/java/com/thoughtworks/androidbootcamp/test/helpers/When.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.test.helpers;
2 |
3 | import static com.google.android.apps.common.testing.ui.espresso.Espresso.onData;
4 | import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
5 | import static com.google.android.apps.common.testing.ui.espresso.action.ViewActions.click;
6 | import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
7 | import static org.hamcrest.core.AllOf.allOf;
8 | import static org.hamcrest.core.Is.is;
9 | import static org.hamcrest.core.IsInstanceOf.instanceOf;
10 |
11 | /**
12 | * Created by macosgrove on 4/01/2014.
13 | * Action methods to make the functional tests more readable, in BDD style
14 | */
15 | public class When {
16 | public static void iOpenTheMenuAndSelectItem(String menuItem) {
17 | onView(withText("Treasure List")).perform(click());
18 | onData(allOf(is(instanceOf(String.class)), is(menuItem)))
19 | .perform(click());
20 |
21 |
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/AndroidBootcampTest/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | mavenLocal()
4 | mavenCentral()
5 | maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
6 | }
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:0.7.+'
9 | classpath 'com.novoda:gradle-android-test-plugin:0.9.8-SNAPSHOT'
10 | }
11 | }
12 |
13 | repositories {
14 | mavenLocal()
15 | mavenCentral()
16 | maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
17 | }
18 |
19 | apply plugin: 'java'
20 | apply plugin: 'android-test'
21 |
22 | android {
23 | projectUnderTest ':AndroidBootcamp'
24 | }
25 |
26 | dependencies {
27 | testCompile 'org.hamcrest:hamcrest-all:1.3'
28 | testCompile 'com.android.support:support-v4:19.0.1'
29 | testCompile 'com.google.collections:google-collections:1.0'
30 | testCompile 'junit:junit:4.11'
31 | testCompile 'org.mockito:mockito-core:1.9.5'
32 | testCompile 'com.squareup:fest-android:1.0.7'
33 | testCompile 'org.robolectric:robolectric:2.3-SNAPSHOT'
34 | }
35 |
--------------------------------------------------------------------------------
/AndroidBootcamp/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | mavenCentral()
4 | }
5 | dependencies {
6 | classpath 'com.android.tools.build:gradle:0.9.+'
7 | classpath 'com.jakewharton.sdkmanager:gradle-plugin:0.9.+'
8 | }
9 | }
10 | apply plugin: 'android-sdk-manager'
11 | apply plugin: 'android'
12 |
13 | repositories {
14 | mavenCentral()
15 | }
16 |
17 | android {
18 | compileSdkVersion 19
19 | buildToolsVersion '19.0.3'
20 |
21 | defaultConfig {
22 | minSdkVersion 17
23 | targetSdkVersion 19
24 | testInstrumentationRunner "com.google.android.apps.common.testing.testrunner.GoogleInstrumentationTestRunner"
25 | }
26 | }
27 |
28 | dependencies {
29 | compile 'com.android.support:appcompat-v7:+'
30 | compile 'com.android.support:support-v4:+'
31 | compile 'com.squareup.picasso:picasso:+'
32 | compile 'com.squareup.retrofit:retrofit:1.5.0'
33 | compile 'com.google.collections:google-collections:1.0'
34 | compile 'com.google.android.gms:play-services:4.3.23'
35 | androidTestCompile fileTree(dir: 'src/androidTest/libs', includes: ['*.jar'])
36 | }
37 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/controller/WhoAmIActivity.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.controller;
2 |
3 | import android.app.Activity;
4 | import android.content.Intent;
5 | import android.os.Bundle;
6 | import android.view.View;
7 | import android.widget.EditText;
8 |
9 | import com.thoughtworks.androidbootcamp.R;
10 |
11 | public class WhoAmIActivity extends Activity {
12 |
13 | public static final String PLAYER_DATA = "com.thoughtworks.androidbootcamp.PLAYER_DATA";
14 |
15 | @Override
16 | protected void onCreate(Bundle savedInstanceState) {
17 | super.onCreate(savedInstanceState);
18 | setContentView(R.layout.activity_whoami);
19 | }
20 |
21 | public void returnPlayer(View view) {
22 |
23 | Intent returnIntent = new Intent();
24 | returnIntent.putExtra(WhoAmIActivity.PLAYER_DATA, getPlayerName());
25 |
26 | setResult(RESULT_OK, returnIntent);
27 |
28 | finish();
29 | }
30 |
31 | private String getPlayerName() {
32 | View playerField = findViewById(R.id.player_field);
33 | return ((EditText) playerField).getText().toString();
34 | }
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/layout/fragment_treasure_list.xml:
--------------------------------------------------------------------------------
1 |
8 |
9 |
15 |
25 |
26 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/util/FileUtils.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.util;
2 |
3 | import android.os.Environment;
4 | import android.util.Log;
5 |
6 | import java.io.File;
7 | import java.io.IOException;
8 | import java.text.SimpleDateFormat;
9 | import java.util.Date;
10 |
11 | /**
12 | * Created by alex on 11/03/2014.
13 | */
14 | public class FileUtils {
15 |
16 | private static final String TAG = "FileUtils";
17 |
18 | public static File getExternalPublicFile(String type, String folder) throws IOException {
19 | File mediaStorageDir = new File(Environment.getExternalStoragePublicDirectory(
20 | type), folder);
21 |
22 | if (!mediaStorageDir.exists()) {
23 | if (!mediaStorageDir.mkdirs()) {
24 | Log.d(TAG, "failed to create directory");
25 | return null;
26 | }
27 | }
28 |
29 | // Create a media file name
30 | String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
31 |
32 | return File.createTempFile(
33 | "IMG_" + timeStamp, /* prefix */
34 | ".jpg", /* suffix */
35 | mediaStorageDir /* directory */
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/model/Score.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.model;
2 |
3 | import java.io.Serializable;
4 |
5 | /**
6 | * Created by macosgrove on 21/04/2014.
7 | */
8 | public class Score implements Serializable {
9 | //For best performance, implement Parcelable rather than Serializable
10 | //See http://www.developerphil.com/parcelable-vs-serializable/, for example
11 | private int gameVersion;
12 | private String id;
13 | private String name;
14 | private int score;
15 |
16 | public Score(String name, int score, int gameVersion) {
17 | this.name = name;
18 | this.score = score;
19 | this.gameVersion = gameVersion;
20 | }
21 |
22 | public int getGameVersion() {
23 | return gameVersion;
24 | }
25 |
26 | public void setGameVersion(int gameVersion) {
27 | this.gameVersion = gameVersion;
28 | }
29 |
30 | public String getId() {
31 | return id;
32 | }
33 |
34 | public void setId(String id) {
35 | this.id = id;
36 | }
37 |
38 | public String getName() {
39 | return name;
40 | }
41 |
42 | public void setName(String name) {
43 | this.name = name;
44 | }
45 |
46 | public int getScore() {
47 | return score;
48 | }
49 |
50 | public void setScore(int score) {
51 | this.score = score;
52 | }
53 | }
54 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/controller/adapter/HighScoreAdapter.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.controller.adapter;
2 |
3 | import android.content.Context;
4 | import android.view.View;
5 | import android.view.ViewGroup;
6 | import android.widget.BaseAdapter;
7 | import android.widget.TextView;
8 |
9 | import com.thoughtworks.androidbootcamp.model.Score;
10 |
11 | import java.util.List;
12 |
13 | /**
14 | * Created by macosgrove on 21/04/2014.
15 | */
16 | public class HighScoreAdapter extends BaseAdapter {
17 | Context mContext;
18 | List mHighScores;
19 |
20 | public HighScoreAdapter(Context context, List highScores) {
21 | this.mContext = context;
22 | this.mHighScores = highScores;
23 | }
24 |
25 | @Override
26 | public int getCount() {
27 | return mHighScores.size();
28 | }
29 |
30 | @Override
31 | public Object getItem(int i) {
32 | return mHighScores.get(i);
33 | }
34 |
35 | @Override
36 | public long getItemId(int i) {
37 | return mHighScores.get(i).hashCode();
38 | }
39 |
40 | @Override
41 | public View getView(int i, View view, ViewGroup viewGroup) {
42 | TextView textView = (view == null) ? new TextView(mContext) : (TextView) view;
43 | Score score = mHighScores.get(i);
44 | textView.setText(score.getName() + " : " + score.getScore());
45 | return textView;
46 | }
47 |
48 | }
49 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/layout/activity_whoami.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
15 |
16 |
24 |
32 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
10 |
11 |
18 |
23 |
30 |
31 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/model/Attempt.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.model;
2 |
3 | import java.io.Serializable;
4 | import java.util.List;
5 |
6 | import static com.google.common.collect.Lists.newArrayList;
7 | import static java.lang.String.format;
8 |
9 | /**
10 | * Created by macosgrove on 23/04/2014.
11 | */
12 | public class Attempt implements Serializable, Locatable {
13 | //For best performance, implement Parcelable rather than Serializable
14 | //See http://www.developerphil.com/parcelable-vs-serializable/, for example
15 | private int distance = Integer.MAX_VALUE;
16 | private List coordinates;
17 | private String photoPath;
18 | private int count;
19 |
20 | public Attempt(double latitude, double longitude, String photoPath, int count) {
21 | this.photoPath = photoPath;
22 | this.count = count;
23 | //Longitude is stored first for consistency with Treasure
24 | coordinates = newArrayList(longitude, latitude);
25 | }
26 |
27 | public int getDistance() {
28 | return distance;
29 | }
30 |
31 | public void setDistance(int distance) {
32 | this.distance = distance;
33 | }
34 |
35 | @Override
36 | public double getLatitude() {
37 | return coordinates.get(1);
38 | }
39 |
40 | @Override
41 | public double getLongitude() {
42 | return coordinates.get(0);
43 | }
44 |
45 | @Override
46 | public void setCoordinates(double latitude, double longitude) {
47 | coordinates.clear();
48 | coordinates.add(longitude);
49 | coordinates.add(latitude);
50 | }
51 |
52 | @Override
53 | public String getName() {
54 | return format("Attempt %d: %dm", count, distance);
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/controller/adapter/TreasureListAdapter.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.controller.adapter;
2 |
3 | import android.content.Context;
4 | import android.view.View;
5 | import android.view.ViewGroup;
6 | import android.widget.BaseAdapter;
7 | import android.widget.ImageView;
8 |
9 | import com.squareup.picasso.Picasso;
10 | import com.thoughtworks.androidbootcamp.model.Treasure;
11 | import com.thoughtworks.androidbootcamp.controller.HelloAndroid;
12 | import com.thoughtworks.androidbootcamp.util.Properties;
13 |
14 | import java.util.List;
15 |
16 | /**
17 | * Created by trogdor on 5/03/14.
18 | */
19 | public class TreasureListAdapter extends BaseAdapter{
20 | Context context;
21 | List treasures;
22 |
23 | public TreasureListAdapter(HelloAndroid activity) {
24 | this.context = activity;
25 | this.treasures = activity.getTreasures();
26 | }
27 |
28 | @Override
29 | public int getCount() {
30 | return treasures.size();
31 | }
32 |
33 | @Override
34 | public Object getItem(int i) {
35 | return treasures.get(i);
36 | }
37 |
38 | @Override
39 | public long getItemId(int i) {
40 | return treasures.get(i).hashCode();
41 | }
42 |
43 | @Override
44 | public View getView(int i, View view, ViewGroup viewGroup) {
45 | ImageView imageView = (view == null) ? new ImageView(context) : (ImageView) view;
46 | imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);
47 | String url = getUrlForTreasure(treasures.get(i));
48 | Picasso.with(context).load(url).resize(640, 480).centerCrop().into(imageView);
49 | return imageView;
50 | }
51 |
52 | public String getUrlForTreasure(Treasure treasure) {
53 | return Properties.SERVICE_URL + "/" + treasure.getUrl().replace("public/", "");
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/model/Treasure.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.model;
2 |
3 | import java.io.Serializable;
4 | import java.util.ArrayList;
5 | import java.util.List;
6 |
7 | public class Treasure implements Serializable, Locatable {
8 | //For best performance, implement Parcelable rather than Serializable
9 | //See http://www.developerphil.com/parcelable-vs-serializable/, for example
10 | private String address;
11 | private List coordinates = new ArrayList();
12 | private String id;
13 | private String name;
14 | private String url;
15 |
16 | public String getAddress() {
17 | return address;
18 | }
19 |
20 | public void setAddress(String address) {
21 | this.address = address;
22 | }
23 |
24 | public List getCoordinates() {
25 | return coordinates;
26 | }
27 |
28 | @Override
29 | public double getLatitude() {
30 | return coordinates.get(1);
31 | }
32 |
33 | @Override
34 | public double getLongitude() {
35 | return coordinates.get(0);
36 | }
37 |
38 | @Override
39 | public void setCoordinates(double latitude, double longitude) {
40 | coordinates.clear();
41 | //Longitude is stored first because that's how the server sends it through
42 | coordinates.add(longitude);
43 | coordinates.add(latitude);
44 | }
45 |
46 | public void setCoordinates(List coordinates) {
47 | this.coordinates = coordinates;
48 | }
49 |
50 | public String getId() {
51 | return id;
52 | }
53 |
54 | public void setId(String id) {
55 | this.id = id;
56 | }
57 |
58 | public String getName() {
59 | return name;
60 | }
61 |
62 | public void setName(String name) {
63 | this.name = name;
64 | }
65 |
66 | public String getUrl() {
67 | return url;
68 | }
69 |
70 | public void setUrl(String url) {
71 | this.url = url;
72 | }
73 |
74 | }
--------------------------------------------------------------------------------
/AndroidBootcampTest/src/test/java/com/thoughtworks/androidbootcamp/controller/adapter/TreasureListAdapterTest.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.controller.adapter;
2 |
3 | import android.app.Activity;
4 | import android.content.Context;
5 |
6 | import com.thoughtworks.androidbootcamp.model.Treasure;
7 | import com.thoughtworks.androidbootcamp.controller.HelloAndroid;
8 |
9 | import org.junit.Assert;
10 |
11 | import org.junit.Test;
12 | import org.junit.runner.RunWith;
13 | import org.mockito.Mock;
14 | import org.mockito.Mockito;
15 | import org.robolectric.Robolectric;
16 | import org.robolectric.RobolectricTestRunner;
17 | import org.robolectric.annotation.Config;
18 |
19 | import java.util.ArrayList;
20 | import java.util.List;
21 |
22 | import static org.mockito.Mockito.spy;
23 | import static org.mockito.Mockito.verify;
24 | import static org.mockito.Mockito.when;
25 |
26 | @RunWith(RobolectricTestRunner.class)
27 | @Config(emulateSdk = 18)
28 | public class TreasureListAdapterTest {
29 |
30 | @Test
31 | public void shouldReturnItemCountAsNumberOfImagesProvided() throws Exception {
32 | int numTreasures = 10;
33 | List treasureList = new ArrayList();
34 | for (int i=0; i< numTreasures; i++) {
35 | treasureList.add(new Treasure());
36 | }
37 | HelloAndroid activity = Mockito.mock(HelloAndroid.class);
38 | when(activity.getTreasures()).thenReturn(treasureList);
39 | TreasureListAdapter adapter = new TreasureListAdapter(activity);
40 | Assert.assertEquals(10, adapter.getCount());
41 | }
42 |
43 | @Test
44 | public void shouldReturnAbsoluteURLOfTreasure() throws Exception {
45 | String expectedUrl = "http://android-bootcamp-rest-server.herokuapp.com/images/treasure.jpg";
46 | Treasure treasure = new Treasure();
47 | treasure.setUrl("images/treasure.jpg");
48 | TreasureListAdapter adapter = new TreasureListAdapter(Mockito.mock(HelloAndroid.class));
49 |
50 | String generatedUrl = adapter.getUrlForTreasure(treasure);
51 |
52 | Assert.assertEquals(expectedUrl, generatedUrl);
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/res/layout/fragment_high_scores.xml:
--------------------------------------------------------------------------------
1 |
6 |
15 |
16 |
23 |
24 |
30 |
31 |
37 |
38 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
19 |
20 |
25 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
38 |
41 |
42 |
43 |
46 |
49 |
50 |
51 |
52 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/util/TreasureLoader.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.util;
2 |
3 | import android.content.Context;
4 |
5 | import java.io.File;
6 | import java.io.FileOutputStream;
7 | import java.io.FilenameFilter;
8 | import java.io.IOException;
9 | import java.io.InputStream;
10 | import java.io.OutputStream;
11 |
12 | /**
13 | * Created by trogdor on 4/03/14.
14 | */
15 | public class TreasureLoader {
16 | static String TREASURES_ASSETS_SUBDIR = "treasures";
17 | static String TREASURES_CACHE_SUBDIR = "/treasures/";
18 | Context context;
19 |
20 | public TreasureLoader(Context context) {
21 | this.context = context;
22 | }
23 |
24 | public void copySampleImages() {
25 | try {
26 | String treasuresDir = context.getExternalCacheDir() + TREASURES_CACHE_SUBDIR;
27 | new File(treasuresDir).mkdir();
28 | String[] assets = context.getAssets().list(TREASURES_ASSETS_SUBDIR);
29 | for(int i=0; i 0)
47 | out.write(buffer, 0, len);
48 | out.close();
49 | asset.close();
50 | } catch (IOException e) {
51 |
52 | }
53 | }
54 |
55 | public String[] getSampleImagePaths() {
56 | String cacheDir = context.getExternalCacheDir() + TREASURES_CACHE_SUBDIR;
57 | String[] fileNames = new File(cacheDir).list(new FilenameFilter() {
58 | @Override
59 | public boolean accept(File file, String s) {
60 | return s.contains(".jpg");
61 | }
62 | });
63 |
64 | String[] filesWithPaths = new String[fileNames.length];
65 | for (int i=0; i < fileNames.length; i++)
66 | filesWithPaths[i] = cacheDir + "/" + fileNames[i];
67 |
68 | return filesWithPaths;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/androidTest/java/com/thoughtworks/androidbootcamp/test/HelloAndroidTest.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.test;
2 |
3 | import android.test.ActivityInstrumentationTestCase2;
4 |
5 | import com.thoughtworks.androidbootcamp.R;
6 | import com.thoughtworks.androidbootcamp.controller.HelloAndroid;
7 | import com.thoughtworks.androidbootcamp.test.helpers.Given;
8 | import com.thoughtworks.androidbootcamp.test.helpers.When;
9 |
10 | import static com.google.android.apps.common.testing.ui.espresso.Espresso.onView;
11 | import static com.google.android.apps.common.testing.ui.espresso.assertion.ViewAssertions.matches;
12 | import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.Visibility.VISIBLE;
13 | import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withEffectiveVisibility;
14 | import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withId;
15 | import static com.google.android.apps.common.testing.ui.espresso.matcher.ViewMatchers.withText;
16 | import static org.hamcrest.Matchers.containsString;
17 |
18 | /**
19 | * Functional test for the HelloAndroid activity
20 | */
21 | public class HelloAndroidTest extends ActivityInstrumentationTestCase2 {
22 |
23 | @SuppressWarnings("deprecation")
24 | public HelloAndroidTest() {
25 | // This constructor was deprecated - but we want to support lower API levels.
26 | super("com.thoughtworks.androidbootcamp", HelloAndroid.class);
27 | }
28 |
29 | @Override
30 | protected void setUp() throws Exception {
31 | super.setUp();
32 | // Espresso will not launch our activity for us, we must launch it via getActivity().
33 | getActivity();
34 | }
35 |
36 | public void testTreasureList() throws InterruptedException {
37 | Given.thePlayerHasEnteredTheirName();
38 |
39 | When.iOpenTheMenuAndSelectItem("Treasure List");
40 |
41 | onView(withId(R.id.section_label)).check(matches(withText(containsString("Find these treasures:"))));
42 | }
43 |
44 | public void testHighScores() throws InterruptedException {
45 | Given.thePlayerHasEnteredTheirName();
46 |
47 | When.iOpenTheMenuAndSelectItem("High Scores");
48 |
49 | onView(withId(R.id.section_label)).check(matches(withText(containsString("High Scores:"))));
50 | }
51 |
52 | public void testMap() throws InterruptedException {
53 | Given.thePlayerHasEnteredTheirName();
54 |
55 | When.iOpenTheMenuAndSelectItem("Map");
56 |
57 | onView(withId(R.id.map_container)).check(matches(withEffectiveVisibility(VISIBLE)));
58 | }
59 |
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/controller/fragment/HighScoresFragment.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.controller.fragment;
2 |
3 |
4 |
5 | import android.app.Fragment;
6 | import android.os.AsyncTask;
7 | import android.os.Bundle;
8 | import android.view.LayoutInflater;
9 | import android.view.View;
10 | import android.view.ViewGroup;
11 | import android.widget.ListView;
12 |
13 | import com.thoughtworks.androidbootcamp.R;
14 | import com.thoughtworks.androidbootcamp.controller.adapter.HighScoreAdapter;
15 | import com.thoughtworks.androidbootcamp.model.Score;
16 | import com.thoughtworks.androidbootcamp.util.Properties;
17 | import com.thoughtworks.androidbootcamp.util.TreasureService;
18 |
19 | import java.util.List;
20 |
21 | import retrofit.RestAdapter;
22 |
23 |
24 | /**
25 | * A simple {@link android.support.v4.app.Fragment} subclass.
26 | *
27 | */
28 | public class HighScoresFragment extends Fragment {
29 |
30 |
31 | private TreasureService mTreasureService;
32 | private List mHighScores;
33 | private HighScoreAdapter mHighScoreAdapter;
34 |
35 | public HighScoresFragment() {
36 | // Required empty public constructor
37 | }
38 |
39 |
40 | @Override
41 | public void onCreate(Bundle savedInstanceState) {
42 | super.onCreate(savedInstanceState);
43 | mTreasureService = new RestAdapter.Builder()
44 | .setEndpoint(Properties.SERVICE_URL)
45 | .build()
46 | .create(TreasureService.class);
47 | }
48 |
49 | @Override
50 | public View onCreateView(LayoutInflater inflater, ViewGroup container,
51 | Bundle savedInstanceState) {
52 | // Inflate the layout for this fragment
53 | return inflater.inflate(R.layout.fragment_high_scores, container, false);
54 | }
55 |
56 | @Override
57 | public void onViewCreated(View view, Bundle savedInstanceState) {
58 | super.onViewCreated(view, savedInstanceState);
59 | final ListView listView = (ListView) view.findViewById(R.id.high_score_list);
60 | new AsyncTask>() {
61 | @Override
62 | protected List doInBackground(Void... voids) {
63 | return mTreasureService.listHighScores();
64 | }
65 |
66 | @Override
67 | protected void onPostExecute(List highScores) {
68 | mHighScores = highScores;
69 | mHighScoreAdapter = new HighScoreAdapter(getActivity(), mHighScores);
70 | listView.setAdapter(mHighScoreAdapter);
71 | }
72 | }.execute();
73 | }
74 |
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @if "%DEBUG%" == "" @echo off
2 | @rem ##########################################################################
3 | @rem
4 | @rem Gradle startup script for Windows
5 | @rem
6 | @rem ##########################################################################
7 |
8 | @rem Set local scope for the variables with windows NT shell
9 | if "%OS%"=="Windows_NT" setlocal
10 |
11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
12 | set DEFAULT_JVM_OPTS=
13 |
14 | set DIRNAME=%~dp0
15 | if "%DIRNAME%" == "" set DIRNAME=.
16 | set APP_BASE_NAME=%~n0
17 | set APP_HOME=%DIRNAME%
18 |
19 | @rem Find java.exe
20 | if defined JAVA_HOME goto findJavaFromJavaHome
21 |
22 | set JAVA_EXE=java.exe
23 | %JAVA_EXE% -version >NUL 2>&1
24 | if "%ERRORLEVEL%" == "0" goto init
25 |
26 | echo.
27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
28 | echo.
29 | echo Please set the JAVA_HOME variable in your environment to match the
30 | echo location of your Java installation.
31 |
32 | goto fail
33 |
34 | :findJavaFromJavaHome
35 | set JAVA_HOME=%JAVA_HOME:"=%
36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
37 |
38 | if exist "%JAVA_EXE%" goto init
39 |
40 | echo.
41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
42 | echo.
43 | echo Please set the JAVA_HOME variable in your environment to match the
44 | echo location of your Java installation.
45 |
46 | goto fail
47 |
48 | :init
49 | @rem Get command-line arguments, handling Windowz variants
50 |
51 | if not "%OS%" == "Windows_NT" goto win9xME_args
52 | if "%@eval[2+2]" == "4" goto 4NT_args
53 |
54 | :win9xME_args
55 | @rem Slurp the command line arguments.
56 | set CMD_LINE_ARGS=
57 | set _SKIP=2
58 |
59 | :win9xME_args_slurp
60 | if "x%~1" == "x" goto execute
61 |
62 | set CMD_LINE_ARGS=%*
63 | goto execute
64 |
65 | :4NT_args
66 | @rem Get arguments from the 4NT Shell from JP Software
67 | set CMD_LINE_ARGS=%$
68 |
69 | :execute
70 | @rem Setup the command line
71 |
72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
73 |
74 | @rem Execute Gradle
75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
76 |
77 | :end
78 | @rem End local scope for the variables with windows NT shell
79 | if "%ERRORLEVEL%"=="0" goto mainEnd
80 |
81 | :fail
82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
83 | rem the _cmd.exe /c_ return code!
84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
85 | exit /b 1
86 |
87 | :mainEnd
88 | if "%OS%"=="Windows_NT" endlocal
89 |
90 | :omega
91 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/model/Game.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.model;
2 |
3 | import java.io.Serializable;
4 | import java.util.ArrayList;
5 | import java.util.HashMap;
6 | import java.util.List;
7 | import java.util.Map;
8 |
9 | public class Game implements Serializable {
10 | private static final int GAME_VERSION = 1;
11 | //For best performance, implement Parcelable rather than Serializable
12 | //See http://www.developerphil.com/parcelable-vs-serializable/, for example
13 | private Map attempts;
14 | private Score score;
15 | private boolean ended = false;
16 |
17 | public Game() {
18 | attempts = new HashMap();
19 | score = new Score("", 0, GAME_VERSION);
20 | }
21 |
22 | public boolean hasEnded() {
23 | return ended;
24 | }
25 |
26 | public void setPlayer(String player) {
27 | score.setName(player);
28 | }
29 |
30 | public String getPlayer() {
31 | return score.getName();
32 | }
33 |
34 | public List getTreasures() {
35 | //I'd prefer to use Google Collection's newArrayList() here, but that causes
36 | //java.lang.IllegalAccessError: Class ref in pre-verified class resolved to unexpected implementation
37 | //when running the instrumentation tests
38 | return new ArrayList(attempts.keySet());
39 | }
40 |
41 | public void setTreasures(List treasures) {
42 | attempts.clear();
43 | for (Treasure treasure : treasures) {
44 | attempts.put(treasure, null);
45 | }
46 | }
47 |
48 | public Attempt getAttemptForTreasure(Treasure treasure) {
49 | return attempts.get(treasure);
50 | }
51 |
52 | public int recordAttempt(Treasure treasure, Attempt attempt) {
53 | Attempt currentBestAttempt = getAttemptForTreasure(treasure);
54 | int difference = Integer.MAX_VALUE;
55 | if (currentBestAttempt != null) {
56 | difference = currentBestAttempt.getDistance() - attempt.getDistance();
57 | }
58 | if (difference >= 0) {
59 | attempts.put(treasure, attempt);
60 | }
61 | return difference;
62 | }
63 |
64 | public boolean hasPreviouslyAttemptedTreasure(Treasure treasure) {
65 | return (getAttemptForTreasure(treasure) != null);
66 | }
67 |
68 | public List getAttempts() {
69 | List nonNullAttempts = new ArrayList();
70 | for (Attempt attempt : attempts.values()) {
71 | if (attempt != null) {
72 | nonNullAttempts.add(attempt);
73 | }
74 | }
75 | return nonNullAttempts;
76 | }
77 |
78 | public boolean hasNoTreasures() {
79 | return attempts.isEmpty();
80 | }
81 |
82 | private int calculateScore() {
83 | List attempts = getAttempts();
84 | int totalScore = 0;
85 | for (Attempt attempt : attempts) {
86 | totalScore += Math.max(1000 - attempt.getDistance(), 0);
87 | }
88 | int treasureCount = getTreasures().size();
89 | return treasureCount == 0 ? 0 : totalScore / treasureCount;
90 | }
91 |
92 | public Score getScore() {
93 | score.setScore(calculateScore());
94 | return score;
95 | }
96 |
97 | public void end() {
98 | ended = true;
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/AndroidBootcampTest/src/test/java/com/thoughtworks/androidbootcamp/controller/fragment/TreasureListFragmentTest.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.controller.fragment;
2 |
3 | import com.thoughtworks.androidbootcamp.model.Attempt;
4 | import com.thoughtworks.androidbootcamp.model.Game;
5 | import com.thoughtworks.androidbootcamp.model.Treasure;
6 |
7 | import org.junit.Before;
8 | import org.junit.Test;
9 | import org.junit.runner.RunWith;
10 | import org.robolectric.RobolectricTestRunner;
11 | import org.robolectric.annotation.Config;
12 |
13 | import java.io.IOException;
14 |
15 | import static org.hamcrest.MatcherAssert.assertThat;
16 | import static org.hamcrest.core.Is.is;
17 | import static org.hamcrest.core.IsEqual.equalTo;
18 | import static org.hamcrest.core.StringStartsWith.startsWith;
19 | import static org.mockito.Mockito.mock;
20 | import static org.mockito.Mockito.spy;
21 | import static org.mockito.Mockito.verify;
22 | import static org.mockito.Mockito.when;
23 |
24 |
25 | /**
26 | * Created by macosgrove on 25/04/2014.
27 | */
28 | @RunWith(RobolectricTestRunner.class)
29 | @Config(emulateSdk = 18)
30 |
31 | public class TreasureListFragmentTest {
32 |
33 | private TreasureListFragment fragment;
34 | private Treasure treasure;
35 |
36 | @Before
37 | public void setUp() {
38 | fragment = spy(new TreasureListFragment());
39 | treasure = mock(Treasure.class);
40 | when(fragment.getSelectedTreasure()).thenReturn(treasure);
41 | }
42 |
43 | @Test
44 | public void shouldCalculateDistance() {
45 | Attempt attempt = new Attempt(-33.866365, 151.210816, "", 0);
46 | Treasure treasure = new Treasure();
47 | treasure.setCoordinates(-33.866441, 151.211395);
48 | int distance = fragment.calculateDistance(attempt, treasure);
49 | assertThat(distance, is(54));
50 | int distance2 = fragment.calculateDistance(treasure, attempt);
51 | assertThat(distance2, is(54));
52 | }
53 |
54 | @Test
55 | public void shouldRecordAttempt() throws IOException {
56 | Attempt attempt = new Attempt(-33.866365, 151.210816, "the photo path", 0);
57 | Game game = mock(Game.class);
58 | when(fragment.getCurrentPhotoPath()).thenReturn("the photo path");
59 | when(fragment.createAttemptForPhoto("the photo path")).thenReturn(attempt);
60 | when(fragment.getGame()).thenReturn(game);
61 |
62 | fragment.onTreasureAttempted();
63 |
64 | verify(fragment).calculateDistance(treasure, attempt);
65 | verify(game).recordAttempt(treasure, attempt);
66 | }
67 |
68 | @Test
69 | public void shouldIncrementAttemptCounterWhenCreatingAttempt() {
70 | Attempt attempt1 = fragment.createAttemptForPhoto("one");
71 | Attempt attempt2 = fragment.createAttemptForPhoto("two");
72 |
73 | assertThat(attempt1.getName(), startsWith("Attempt 1:"));
74 | assertThat(attempt2.getName(), startsWith("Attempt 2:"));
75 | }
76 |
77 | @Test
78 | public void shouldConstructMessageForCloserAttempt() {
79 | assertThat(fragment.getMessageForDifference(5), equalTo("\nYay! This attempt is 5 metres closer than your previous best!"));
80 | }
81 |
82 | @Test
83 | public void shouldConstructMessageForFurtherAttempt() {
84 | assertThat(fragment.getMessageForDifference(-3), equalTo("\nSadly, this attempt is 3 metres further than your previous best."));
85 | }
86 |
87 | @Test
88 | public void shouldConstructMessageForEqualAttempt() {
89 | assertThat(fragment.getMessageForDifference(0), equalTo("\nThis equals your previous best attempt."));
90 | }
91 |
92 |
93 | }
94 |
--------------------------------------------------------------------------------
/AndroidBootcampTest/src/test/java/com/thoughtworks/androidbootcamp/controller/HelloAndroidUnitTest.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.controller;
2 |
3 | import com.thoughtworks.androidbootcamp.model.Score;
4 | import com.thoughtworks.androidbootcamp.model.Treasure;
5 |
6 | import org.junit.Before;
7 | import org.junit.Test;
8 | import org.junit.runner.RunWith;
9 | import org.robolectric.Robolectric;
10 | import org.robolectric.RobolectricTestRunner;
11 | import org.robolectric.annotation.Config;
12 |
13 | import java.io.ByteArrayOutputStream;
14 | import java.util.List;
15 |
16 | import retrofit.converter.GsonConverter;
17 | import retrofit.mime.TypedOutput;
18 |
19 | import static com.google.common.collect.Lists.newArrayList;
20 | import static org.hamcrest.CoreMatchers.is;
21 | import static org.hamcrest.MatcherAssert.assertThat;
22 | import static org.hamcrest.Matchers.containsString;
23 | import static org.hamcrest.core.IsEqual.equalTo;
24 | import static org.mockito.Mockito.doReturn;
25 | import static org.mockito.Mockito.mock;
26 | import static org.mockito.Mockito.spy;
27 | import static org.mockito.Mockito.verify;
28 |
29 | @Config(emulateSdk = 18)
30 | @RunWith(RobolectricTestRunner.class)
31 | public class HelloAndroidUnitTest {
32 |
33 | public static final int TREASURE_MENU_ITEM = 0;
34 | public static final int HIGH_SCORES_MENU_ITEM = 1;
35 | public static final int MAP_MENU_ITEM = 2;
36 | HelloAndroid activity;
37 |
38 | @Before
39 | public void setUp() {
40 | activity = spy(Robolectric.buildActivity(HelloAndroid.class).create().get());
41 | }
42 |
43 | @Test
44 | public void shouldShowTreasureListWhenFirstSpinnerItemSelected() throws Exception {
45 | activity.onNavigationItemSelected(TREASURE_MENU_ITEM, 0);
46 | verify(activity).showTreasureList();
47 | }
48 |
49 | @Test
50 | public void shouldShowHighScoresWhenSecondSpinnerItemSelected() throws Exception {
51 | activity.onNavigationItemSelected(HIGH_SCORES_MENU_ITEM, 0);
52 | verify(activity).showHighScores();
53 | }
54 |
55 | @Test
56 | public void shouldShowMapWhenThirdSpinnerItemSelected() throws Exception {
57 | activity.onNavigationItemSelected(MAP_MENU_ITEM, 0);
58 | verify(activity).showMap();
59 | }
60 |
61 | @Test
62 | public void shouldSetTreasuresOnGame() throws Exception {
63 | Treasure treasure = mock(Treasure.class);
64 | List treasures = newArrayList(treasure);
65 | activity.setTreasures(treasures);
66 | List actual = activity.getGame().getTreasures();
67 | assertThat(actual.size(), is(1));
68 | assertThat(actual.get(0), is(treasure));
69 | }
70 |
71 | @Test
72 | public void shouldGetTreasuresFromGame() throws Exception {
73 | Treasure treasure = mock(Treasure.class);
74 | List treasures = newArrayList(treasure);
75 | activity.getGame().setTreasures(treasures);
76 | List actual = activity.getTreasures();
77 | assertThat(actual.size(), is(1));
78 | assertThat(actual.get(0), is(treasure));
79 | }
80 |
81 | @Test
82 | public void shouldCreateEndGameMessage() throws Exception {
83 | doReturn(3363).when(activity).getScore();
84 |
85 | assertThat(activity.getEndGameMessage(), containsString("Good game! Your final score was 3363."));
86 | }
87 |
88 | @Test
89 | public void shouldConstructScoreJsonViaGsonConverter() throws Exception {
90 | Score score = new Score("Hermione", 254, 3);
91 | GsonConverter converter = activity.createGsonConverter();
92 | TypedOutput typedOutput = converter.toBody(score);
93 | ByteArrayOutputStream os = new ByteArrayOutputStream(256);
94 | typedOutput.writeTo(os);
95 | String actualScoreJson = os.toString("UTF-8");
96 | String expectedScoreJson = "{\"game_version\":3,\"name\":\"Hermione\",\"score\":254}";
97 | assertThat(actualScoreJson, equalTo(expectedScoreJson));
98 | }
99 |
100 |
101 |
102 | }
103 |
--------------------------------------------------------------------------------
/BDDinAS.md:
--------------------------------------------------------------------------------
1 | ###How I Got BDD-TDD Working In Android Studio 0.4.3
2 |
3 | My goal was to be able to drive a [BDD-TDD cycle](http://boostagile.com/test-driven-development-and-agile/) using Android instrument tests with Espresso for the outer BDD cycle, and JUnit with Robolectric for the inner TDD cycle.
4 |
5 | This turned out to be a non-trivial exercise, as JUnit is not officially supported as of AS 0.4.3
6 |
7 |
8 | 1. Installed the standalone Espresso library as per [the instructions](https://code.google.com/p/android-test-kit/wiki/Espresso). (The separate library with dependencies did not work.)
9 | 2. Wrote some [behaviour tests](https://github.com/macosgrove/AndroidBootcampProject/commit/ff41e46f18da9ab904607f62a766368459b78db2). These were easy to run within AS and failed in the expected way (as the code is not yet implemented).
10 | 3. Following advice from novoda's [gradle-android-test-plugin](https://github.com/novoda/gradle-android-test-plugin), created a [separate module for unit tests and added the plugin to gradle for that module](https://github.com/macosgrove/AndroidBootcampProject/commit/816cc9f7dc56a3d3d09b040891992d336f6bc277). Now, running the tests from the command line with
11 | ```gradle check --debug```
12 | results in this error:
13 | ```java.lang.NoClassDefFoundError: org/gradle/api/artifacts/result/ResolvedModuleVersionResult```
14 | 4. Downgraded gradle to v1.9 [using homebrew](http://stackoverflow.com/questions/3987683/homebrew-install-specific-version-of-formula). Now running the tests from the command line with
15 | ```gradle check```
16 | sometimes works, and sometimes reports a success without compiling or running the tests.
17 | ```gradle check --info```
18 | or
19 | ```gradle check --debug```
20 | seem to work consistently. Note that the first run takes a very long time as Robolectric downloads.
21 | At this stage, running the tests within AS results in
22 | ```!!! JUnit version 3.8 or later expected```
23 | 5. Following the advice [here](http://kostyay.name/android-studio-robolectric-gradle-getting-work/), obtained the classpath from the console line above the previous error message, modified it to move JUnit 4.11 to the head of the path and add my test-debug output directory, and added it to the VM Options in the run configuration in AS. Hurray, tests now run in AS! HOWEVER, the test source is not compiled within AS. If you clean the project and re-run the test in AS, you get this error:
24 | ```Class not found: "com.thoughtworks.androidbootcamp.controller.HelloAndroidTest"```
25 | 6. Created a new run configuration for gradle. Set task to compileTestDebugJava. Added it to the 'before launch' part of the JUnit test run configuration. At this stage, the test and compile run in parallel and the compile finishes after the test finishes so you still get the class not found exception if the project has been cleaned.
26 | 7. Moved the compile run configuration to before the Make in the 'before launch'. Now the test works!
27 | 8. Made the same changes to the Default JUnit run configuration so future tests would run correctly.
28 | 9. At this stage simple tests work but Robolectric can't create any views as it doesn't know where to find the resources.
29 | Created Robolectric configuration file org.robolectric.Config.properties under src/test/resources and added the folder to the classpath in the run configurations.
30 | 10. Robolectric still can't find the Android resources in ```/Users/macosgrove/AndroidStudioProjects/AndroidBootcampProject/AndroidBootcamp/build/exploded-bundles/ComAndroidSupportAppcompatV71900.aar/res```
31 | Result is this:
32 | ```java.lang.RuntimeException: huh? can't find parent for StyleData{name='AppTheme', parent='Theme_AppCompat_Light_DarkActionBar'}```
33 | 11. Upgraded to Robolectric 2.3-SNAPSHOT. Installed Maven using ```brew install maven```, set ANDROID_HOME to ```/Applications/Android Studio.app/sdk``` and followed instructions at [Robolectric's github](https://github.com/robolectric/robolectric) to install AppCompat V4. Added AppCompat V4 dependency to testCompile in the test build.gradle.
34 | 12. Still giving the ```huh?``` error. Next tried adding test-project.properties and project.properties in same folder as AndroidManifest.xml.
35 | Project.properties:
36 | ```
37 | target=android-18
38 | android.library.reference.1=../AndroidBootcamp/build/exploded-bundles/ComAndroidSupportAppcompatV71901.aar/res
39 | ```
40 | Test.properties:
41 | ```
42 | android.library.reference.1=../AndroidBootcamp/build/exploded-bundles/ComAndroidSupportAppcompatV71901.aar/res
43 | ```
44 | Tried various values for the path. Nothing changed the outcome.
45 | 13. Removed the use of support classes and raised the minimum target sdk version to 14. This now works.
46 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | ##############################################################################
4 | ##
5 | ## Gradle start up script for UN*X
6 | ##
7 | ##############################################################################
8 |
9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
10 | DEFAULT_JVM_OPTS=""
11 |
12 | APP_NAME="Gradle"
13 | APP_BASE_NAME=`basename "$0"`
14 |
15 | # Use the maximum available, or set MAX_FD != -1 to use that value.
16 | MAX_FD="maximum"
17 |
18 | warn ( ) {
19 | echo "$*"
20 | }
21 |
22 | die ( ) {
23 | echo
24 | echo "$*"
25 | echo
26 | exit 1
27 | }
28 |
29 | # OS specific support (must be 'true' or 'false').
30 | cygwin=false
31 | msys=false
32 | darwin=false
33 | case "`uname`" in
34 | CYGWIN* )
35 | cygwin=true
36 | ;;
37 | Darwin* )
38 | darwin=true
39 | ;;
40 | MINGW* )
41 | msys=true
42 | ;;
43 | esac
44 |
45 | # For Cygwin, ensure paths are in UNIX format before anything is touched.
46 | if $cygwin ; then
47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
48 | fi
49 |
50 | # Attempt to set APP_HOME
51 | # Resolve links: $0 may be a link
52 | PRG="$0"
53 | # Need this for relative symlinks.
54 | while [ -h "$PRG" ] ; do
55 | ls=`ls -ld "$PRG"`
56 | link=`expr "$ls" : '.*-> \(.*\)$'`
57 | if expr "$link" : '/.*' > /dev/null; then
58 | PRG="$link"
59 | else
60 | PRG=`dirname "$PRG"`"/$link"
61 | fi
62 | done
63 | SAVED="`pwd`"
64 | cd "`dirname \"$PRG\"`/" >&-
65 | APP_HOME="`pwd -P`"
66 | cd "$SAVED" >&-
67 |
68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
69 |
70 | # Determine the Java command to use to start the JVM.
71 | if [ -n "$JAVA_HOME" ] ; then
72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
73 | # IBM's JDK on AIX uses strange locations for the executables
74 | JAVACMD="$JAVA_HOME/jre/sh/java"
75 | else
76 | JAVACMD="$JAVA_HOME/bin/java"
77 | fi
78 | if [ ! -x "$JAVACMD" ] ; then
79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
80 |
81 | Please set the JAVA_HOME variable in your environment to match the
82 | location of your Java installation."
83 | fi
84 | else
85 | JAVACMD="java"
86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
87 |
88 | Please set the JAVA_HOME variable in your environment to match the
89 | location of your Java installation."
90 | fi
91 |
92 | # Increase the maximum file descriptors if we can.
93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
94 | MAX_FD_LIMIT=`ulimit -H -n`
95 | if [ $? -eq 0 ] ; then
96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
97 | MAX_FD="$MAX_FD_LIMIT"
98 | fi
99 | ulimit -n $MAX_FD
100 | if [ $? -ne 0 ] ; then
101 | warn "Could not set maximum file descriptor limit: $MAX_FD"
102 | fi
103 | else
104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
105 | fi
106 | fi
107 |
108 | # For Darwin, add options to specify how the application appears in the dock
109 | if $darwin; then
110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
111 | fi
112 |
113 | # For Cygwin, switch paths to Windows format before running java
114 | if $cygwin ; then
115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
117 |
118 | # We build the pattern for arguments to be converted via cygpath
119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
120 | SEP=""
121 | for dir in $ROOTDIRSRAW ; do
122 | ROOTDIRS="$ROOTDIRS$SEP$dir"
123 | SEP="|"
124 | done
125 | OURCYGPATTERN="(^($ROOTDIRS))"
126 | # Add a user-defined pattern to the cygpath arguments
127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
129 | fi
130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
131 | i=0
132 | for arg in "$@" ; do
133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
135 |
136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
138 | else
139 | eval `echo args$i`="\"$arg\""
140 | fi
141 | i=$((i+1))
142 | done
143 | case $i in
144 | (0) set -- ;;
145 | (1) set -- "$args0" ;;
146 | (2) set -- "$args0" "$args1" ;;
147 | (3) set -- "$args0" "$args1" "$args2" ;;
148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
154 | esac
155 | fi
156 |
157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
158 | function splitJvmOpts() {
159 | JVM_OPTS=("$@")
160 | }
161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
163 |
164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
165 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/controller/fragment/TreasureMapFragment.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.controller.fragment;
2 |
3 |
4 | import android.app.Fragment;
5 | import android.app.FragmentTransaction;
6 | import android.location.Location;
7 | import android.os.Bundle;
8 | import android.util.Log;
9 | import android.view.LayoutInflater;
10 | import android.view.View;
11 | import android.view.ViewGroup;
12 | import android.widget.Toast;
13 |
14 | import com.google.android.gms.common.ConnectionResult;
15 | import com.google.android.gms.common.GooglePlayServicesClient;
16 | import com.google.android.gms.common.GooglePlayServicesUtil;
17 | import com.google.android.gms.location.LocationClient;
18 | import com.google.android.gms.maps.CameraUpdate;
19 | import com.google.android.gms.maps.CameraUpdateFactory;
20 | import com.google.android.gms.maps.GoogleMap;
21 | import com.google.android.gms.maps.MapFragment;
22 | import com.google.android.gms.maps.model.BitmapDescriptorFactory;
23 | import com.google.android.gms.maps.model.LatLng;
24 | import com.google.android.gms.maps.model.MarkerOptions;
25 | import com.thoughtworks.androidbootcamp.R;
26 | import com.thoughtworks.androidbootcamp.controller.HelloAndroid;
27 | import com.thoughtworks.androidbootcamp.model.Locatable;
28 |
29 | import java.util.List;
30 |
31 | import static com.google.android.gms.maps.model.BitmapDescriptorFactory.HUE_GREEN;
32 | import static com.google.android.gms.maps.model.BitmapDescriptorFactory.HUE_YELLOW;
33 |
34 |
35 | public class TreasureMapFragment extends Fragment implements GooglePlayServicesClient.ConnectionCallbacks,
36 | GooglePlayServicesClient.OnConnectionFailedListener {
37 |
38 | private final static String TAG = "TreasureMapFragment";
39 |
40 | private GoogleMap mMap;
41 | private LocationClient mLocationClient;
42 | private MapFragment mMapFragment;
43 | private HelloAndroid mActivity;
44 |
45 | @Override
46 | public void onCreate(Bundle savedInstanceState) {
47 | super.onCreate(savedInstanceState);
48 | mActivity = (HelloAndroid) this.getActivity();
49 | mLocationClient = new LocationClient(mActivity, this, this);
50 | }
51 |
52 | @Override
53 | public View onCreateView(LayoutInflater inflater, ViewGroup container,
54 | Bundle savedInstanceState) {
55 | View mView = inflater.inflate(R.layout.fragment_map, container, false);
56 | addMapFragment();
57 |
58 | return mView;
59 | }
60 |
61 | private void addMapFragment() {
62 | mMapFragment = MapFragment.newInstance();
63 | FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
64 | transaction.add(R.id.map_container, mMapFragment)
65 | .commit();
66 | }
67 |
68 | @Override
69 | public void onStart() {
70 | super.onStart();
71 | mLocationClient.connect();
72 | }
73 |
74 | @Override
75 | public void onConnected(Bundle bundle) {
76 | Toast.makeText(mActivity, "Connected to Play Services", Toast.LENGTH_SHORT).show();
77 | mMap = mMapFragment.getMap();
78 | moveToCurrentLocation();
79 | setMarkers(mActivity.getAttempts(), HUE_GREEN);
80 | if (mActivity.hasEnded()) {
81 | setMarkers(mActivity.getTreasures(), HUE_YELLOW);
82 | }
83 | }
84 |
85 | @Override
86 | public void onStop() {
87 | mLocationClient.disconnect();
88 | super.onStop();
89 | }
90 |
91 | @Override
92 | public void onDisconnected() {
93 | Toast.makeText(mActivity, "Disconnected from Play Services", Toast.LENGTH_SHORT).show();
94 | }
95 |
96 | @Override
97 | public void onConnectionFailed(ConnectionResult connectionResult) {
98 | if (connectionResult.hasResolution()) {
99 | Toast.makeText(mActivity, "Connected to Play Services failed", Toast.LENGTH_SHORT).show();
100 | //TODO: Display an error message and retry
101 | //See https://developer.android.com/training/location/retrieve-current.html
102 | }
103 | }
104 |
105 | public void setMarkers(List extends Locatable> locatables, float hue) {
106 | if (canUseMap()) {
107 | for (Locatable locatable : locatables) {
108 | mMap.addMarker(createMarkerForLocatable(locatable, hue));
109 | }
110 | }
111 | }
112 |
113 | private boolean canUseMap() {
114 | return mMap != null & servicesConnected();
115 | }
116 |
117 | private void moveToCurrentLocation() {
118 | if (canUseMap()) {
119 | mMap.setMyLocationEnabled(true);
120 | Location myLocation = mLocationClient.getLastLocation();
121 | if (myLocation != null) {
122 | LatLng latLng = new LatLng(myLocation.getLatitude(), myLocation.getLongitude());
123 | CameraUpdate cameraUpdate = CameraUpdateFactory.newLatLngZoom(latLng, 16.0f);
124 | mMap.animateCamera(cameraUpdate);
125 | }
126 | }
127 | }
128 |
129 | private MarkerOptions createMarkerForLocatable(Locatable locatable, float hue) {
130 | LatLng position = new LatLng(
131 | locatable.getLatitude(),
132 | locatable.getLongitude());
133 | MarkerOptions markerOption = new MarkerOptions();
134 | markerOption
135 | .position(position)
136 | .title(locatable.getName())
137 | .icon(BitmapDescriptorFactory.defaultMarker(hue));
138 | return markerOption;
139 | }
140 |
141 | private boolean servicesConnected() {
142 | int resultCode = GooglePlayServicesUtil.isGooglePlayServicesAvailable(mActivity);
143 | if (ConnectionResult.SUCCESS == resultCode) {
144 | Log.d("Location Updates", "Google Play services is available.");
145 | return true;
146 | } else {
147 | //TODO: Handle the failure
148 | //See https://developer.android.com/training/location/retrieve-current.html
149 | Log.e(TAG, "Could not connect to play services.");
150 | return false;
151 | }
152 | }
153 |
154 | }
155 |
--------------------------------------------------------------------------------
/AndroidBootcampTest/src/test/java/com/thoughtworks/androidbootcamp/model/GameTest.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.model;
2 |
3 | import org.junit.Before;
4 | import org.junit.Test;
5 |
6 | import java.util.ArrayList;
7 | import java.util.List;
8 |
9 | import static com.google.common.collect.Lists.newArrayList;
10 | import static org.hamcrest.CoreMatchers.equalTo;
11 | import static org.hamcrest.CoreMatchers.nullValue;
12 | import static org.hamcrest.MatcherAssert.assertThat;
13 | import static org.hamcrest.Matchers.hasItems;
14 | import static org.hamcrest.core.Is.is;
15 | import static org.junit.Assert.assertFalse;
16 | import static org.junit.Assert.assertTrue;
17 | import static org.mockito.Mockito.mock;
18 |
19 | public class GameTest {
20 |
21 | private Game game;
22 | private Treasure treasure;
23 |
24 | @Before
25 | public void setUp() throws Exception {
26 | treasure = mock(Treasure.class);
27 | game = gameWithTreasures(newArrayList(treasure));
28 | }
29 |
30 | @Test
31 | public void shouldHaveAListOfTreasures() {
32 | assertThat(game.getTreasures().size(), is(1));
33 | }
34 |
35 | @Test
36 | public void shouldBeAbleToRecordAttempts() {
37 | assertThat(game.getAttemptForTreasure(treasure), is(nullValue()));
38 | Attempt attempt = new Attempt(1, 2, "", 0);
39 | game.recordAttempt(treasure, attempt);
40 | assertThat(game.getAttemptForTreasure(treasure), is(attempt));
41 | }
42 |
43 | @Test
44 | public void shouldRetainTheBestAttempt() {
45 | Attempt attempt = new Attempt(1, 2, "", 0);
46 | attempt.setDistance(5);
47 | Attempt betterAttempt = new Attempt(4, 5, "", 0);
48 | betterAttempt.setDistance(3);
49 | game.recordAttempt(treasure, betterAttempt);
50 | game.recordAttempt(treasure, attempt);
51 | assertThat(game.getAttemptForTreasure(treasure), is(betterAttempt));
52 |
53 | }
54 |
55 | @Test
56 | public void shouldRetainTheNewerOfTwoEqualAttempts() {
57 | Attempt firstAttempt = new Attempt(1, 2, "", 0);
58 | firstAttempt.setDistance(5);
59 | Attempt secondAttempt = new Attempt(4, 5, "", 0);
60 | secondAttempt.setDistance(5);
61 | game.recordAttempt(treasure, firstAttempt);
62 | game.recordAttempt(treasure, secondAttempt);
63 | assertThat(game.getAttemptForTreasure(treasure), is(secondAttempt));
64 |
65 | }
66 |
67 | @Test
68 | public void shouldUnderstandHasPreviouslyAttemptedTreasure() {
69 | assertFalse(game.hasPreviouslyAttemptedTreasure(treasure));
70 | Attempt attempt = new Attempt(1, 2, "", 0);
71 | game.recordAttempt(treasure, attempt);
72 | assertTrue(game.hasPreviouslyAttemptedTreasure(treasure));
73 | }
74 |
75 | @Test
76 | public void shouldReturnDistanceDifferenceBetweenBestAndCurrentAttempt() {
77 | Attempt attempt = new Attempt(1, 2, "", 0);
78 | attempt.setDistance(5);
79 | Attempt betterAttempt = new Attempt(4, 5, "", 0);
80 | betterAttempt.setDistance(3);
81 | game.recordAttempt(treasure, betterAttempt);
82 | assertThat(game.recordAttempt(treasure, attempt), is(-2));
83 | }
84 |
85 | @Test
86 | public void shouldReturnAttemptsList() {
87 | Treasure treasure1 = mock(Treasure.class);
88 | Treasure treasure2 = mock(Treasure.class);
89 | Attempt attempt1 = mock(Attempt.class);
90 | Attempt attempt2 = mock(Attempt.class);
91 | Game game = gameWithTreasures(newArrayList(treasure1, treasure2));
92 | game.recordAttempt(treasure1, attempt1);
93 | game.recordAttempt(treasure2, attempt2);
94 |
95 | assertThat(game.getAttempts(), hasItems(attempt1, attempt2));
96 | }
97 |
98 | @Test
99 | public void shouldNotIncludeNullsInAttemptsList() {
100 | Treasure treasure1 = mock(Treasure.class);
101 | Treasure treasure2 = mock(Treasure.class);
102 | Attempt attempt1 = mock(Attempt.class);
103 | Game game = gameWithTreasures(newArrayList(treasure1, treasure2));
104 | game.recordAttempt(treasure1, attempt1);
105 |
106 | List expectedAttempts = newArrayList(attempt1);
107 | assertThat(game.getAttempts(), equalTo(expectedAttempts));
108 | }
109 |
110 | @Test
111 | public void shouldHaveNoTreasuresWhenCreated() {
112 | Game game = new Game();
113 | assertTrue(game.hasNoTreasures());
114 | }
115 |
116 | @Test
117 | public void shouldHaveTreasuresAfterSettingTreasures() {
118 | Game game = new Game();
119 | game.setTreasures(newArrayList(treasure));
120 | assertFalse(game.hasNoTreasures());
121 | }
122 |
123 | @Test
124 | public void shouldCalculateScoreBasedOnDistances() {
125 | Game game = gameWithAttemptsAtDistances(newArrayList(150));
126 | assertThat(game.getScore().getScore(), is(1000 - 150));
127 | }
128 |
129 | @Test
130 | public void shouldScoreZeroWhenDistanceIsGreaterThan1000() {
131 | Game game = gameWithAttemptsAtDistances(newArrayList(1500));
132 | assertThat(game.getScore().getScore(), is(0));
133 | }
134 |
135 | @Test
136 | public void shouldScoreZeroWhenTreasureIsNotAttempted() {
137 | ArrayList distances = newArrayList();
138 | distances.add(null);
139 | Game game = gameWithAttemptsAtDistances(distances);
140 | assertThat(game.getScore().getScore(), is(0));
141 | }
142 |
143 | @Test
144 | public void shouldScoreRoundedAverageOverAllTreasures() {
145 | Game game = gameWithAttemptsAtDistances(newArrayList(500, null, 1000));
146 | assertThat(game.getScore().getScore(), is(166));
147 | }
148 |
149 | @Test
150 | public void shouldScoreZeroWhenNoTreasures() {
151 | ArrayList emptyList = newArrayList();
152 | Game game = gameWithAttemptsAtDistances(emptyList);
153 | assertThat(game.getScore().getScore(), is(0));
154 | }
155 |
156 | private Game gameWithAttemptsAtDistances(ArrayList distances) {
157 | Game game = new Game();
158 | int count = 0;
159 | for (Integer distance: distances) {
160 | Treasure treasure = mock(Treasure.class);
161 | Attempt attempt = null;
162 | if (distance != null) {
163 | attempt = new Attempt(1, 1, "", ++count);
164 | attempt.setDistance(distance);
165 | }
166 | game.recordAttempt(treasure, attempt);
167 | }
168 | return game;
169 | }
170 |
171 | private Game gameWithTreasures(List treasures) {
172 | Game game = new Game();
173 | game.setTreasures(newArrayList(treasures));
174 | return game;
175 | }
176 |
177 | }
178 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/controller/fragment/TreasureListFragment.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.controller.fragment;
2 |
3 |
4 | import android.app.Activity;
5 | import android.app.Fragment;
6 | import android.content.Intent;
7 | import android.location.Location;
8 | import android.media.ExifInterface;
9 | import android.net.Uri;
10 | import android.os.AsyncTask;
11 | import android.os.Bundle;
12 | import android.os.Environment;
13 | import android.provider.MediaStore;
14 | import android.util.Log;
15 | import android.view.LayoutInflater;
16 | import android.view.View;
17 | import android.view.ViewGroup;
18 | import android.widget.AdapterView;
19 | import android.widget.GridView;
20 | import android.widget.Toast;
21 |
22 | import com.thoughtworks.androidbootcamp.R;
23 | import com.thoughtworks.androidbootcamp.controller.HelloAndroid;
24 | import com.thoughtworks.androidbootcamp.controller.adapter.TreasureListAdapter;
25 | import com.thoughtworks.androidbootcamp.model.Attempt;
26 | import com.thoughtworks.androidbootcamp.model.Game;
27 | import com.thoughtworks.androidbootcamp.model.Locatable;
28 | import com.thoughtworks.androidbootcamp.model.Treasure;
29 | import com.thoughtworks.androidbootcamp.util.FileUtils;
30 | import com.thoughtworks.androidbootcamp.util.TreasureService;
31 |
32 | import java.io.File;
33 | import java.io.IOException;
34 | import java.util.List;
35 |
36 | import static java.lang.Math.round;
37 | import static java.lang.String.format;
38 |
39 | /**
40 | * A simple {@link android.support.v4.app.Fragment} subclass.
41 | *
42 | */
43 | public class TreasureListFragment extends Fragment {
44 |
45 | private static final String TAG = "TreasureListFragment";
46 | private static final int CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE = 100;
47 |
48 | private TreasureService mTreasureService;
49 |
50 | private String mCurrentPhotoPath;
51 | private Treasure mSelectedTreasure;
52 |
53 | private TreasureListAdapter mTreasureListAdapter;
54 | private Game mGame;
55 | private int mAttemptCount = 0;
56 | private HelloAndroid mActivity;
57 |
58 | public TreasureListFragment() {
59 | // Required empty public constructor
60 | }
61 |
62 | public Game getGame() {
63 | return mGame;
64 | }
65 |
66 | public Treasure getSelectedTreasure() {
67 | return mSelectedTreasure;
68 | }
69 |
70 | public String getCurrentPhotoPath() {
71 | return mCurrentPhotoPath;
72 | }
73 |
74 | @Override
75 | public void onCreate(Bundle savedInstanceState) {
76 | super.onCreate(savedInstanceState);
77 | mActivity = (HelloAndroid) getActivity();
78 | mTreasureService = mActivity.getTreasureService();
79 | mGame = mActivity.getGame();
80 | }
81 |
82 | @Override
83 | public View onCreateView(LayoutInflater inflater, ViewGroup container,
84 | Bundle savedInstanceState) {
85 | // Inflate the layout for this fragment
86 | return inflater.inflate(R.layout.fragment_treasure_list, container, false);
87 | }
88 |
89 | @Override
90 | public void onViewCreated(View view, Bundle savedInstanceState) {
91 | super.onViewCreated(view, savedInstanceState);
92 | final GridView gridView = (GridView) view.findViewById(R.id.treasure_list);
93 | gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
94 | @Override
95 | public void onItemClick(AdapterView> parent, View view, int position, long id) {
96 | mSelectedTreasure = (Treasure) mTreasureListAdapter.getItem(position);
97 | takePhoto();
98 | }
99 | });
100 | if (mGame.hasNoTreasures()) {
101 | retrieveTreasuresFromServer(gridView);
102 | } else {
103 | setListAdapter(gridView);
104 | }
105 | }
106 |
107 | private void retrieveTreasuresFromServer(final GridView gridView) {
108 | new AsyncTask>() {
109 | @Override
110 | protected List doInBackground(Void... voids) {
111 | return mTreasureService.listTreasures();
112 | }
113 |
114 | @Override
115 | protected void onPostExecute(List treasures) {
116 | mGame.setTreasures(treasures);
117 | setListAdapter(gridView);
118 | }
119 | }.execute();
120 | }
121 |
122 | private void setListAdapter(GridView gridView) {
123 | mTreasureListAdapter = new TreasureListAdapter(mActivity);
124 | gridView.setAdapter(mTreasureListAdapter);
125 | }
126 |
127 | public void takePhoto() {
128 | // Create a new intent for taking a photo
129 | Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
130 |
131 | // Create the File where the photo should go
132 | try {
133 | File photoFile = FileUtils.getExternalPublicFile(Environment.DIRECTORY_PICTURES,
134 | getString(R.string.app_name));
135 |
136 | // store the path so we know where it is later
137 | mCurrentPhotoPath = photoFile.getAbsolutePath();
138 |
139 | // tell camera app when to put the photo
140 | intent.putExtra(MediaStore.EXTRA_OUTPUT, Uri.fromFile(photoFile));
141 |
142 | if (intent.resolveActivity(mActivity.getPackageManager()) != null) {
143 | // ask to use the camera
144 | startActivityForResult(intent, CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE);
145 | }
146 |
147 | } catch (IOException ex) {
148 | Log.e(TAG, "Error opening file", ex);
149 | }
150 | }
151 |
152 | @Override
153 | public void onActivityResult(int requestCode, int resultCode, Intent data) {
154 | super.onActivityResult(requestCode, resultCode, data);
155 |
156 | // check that its the right result and that it was successful
157 | if (requestCode == CAPTURE_IMAGE_ACTIVITY_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
158 | onTreasureAttempted();
159 | }
160 | }
161 |
162 | protected void onTreasureAttempted() {
163 | // Retrieve geocode info from the taken photo and treasure
164 | Game game = getGame();
165 | if (game.hasEnded()) {
166 | Toast.makeText(mActivity, "Too late for that now!", Toast.LENGTH_LONG).show();
167 | return;
168 | }
169 | Attempt attempt = createAttemptForPhoto(getCurrentPhotoPath());
170 | Treasure thisTreasure = getSelectedTreasure();
171 | attempt.setDistance(calculateDistance(thisTreasure, attempt));
172 | boolean previouslyAttemptedTreasure = game.hasPreviouslyAttemptedTreasure(thisTreasure);
173 |
174 | int distanceDifference = game.recordAttempt(thisTreasure, attempt);
175 |
176 | String message = format("Your photo is %s metres from treasure", attempt.getDistance());
177 | if (previouslyAttemptedTreasure) {
178 | message += getMessageForDifference(distanceDifference);
179 | }
180 | Toast.makeText(getActivity(), message,
181 | Toast.LENGTH_LONG).show();
182 | }
183 |
184 | protected String getMessageForDifference(int distanceDifference) {
185 | if (distanceDifference > 0) {
186 | return format("\nYay! This attempt is %s metres closer than your previous best!", distanceDifference);
187 | } else if (distanceDifference < 0) {
188 | return format("\nSadly, this attempt is %s metres further than your previous best.", -distanceDifference);
189 | } else {
190 | return "\nThis equals your previous best attempt.";
191 | }
192 | }
193 |
194 | protected Attempt createAttemptForPhoto(String photoPath) {
195 | try {
196 | // Note: The built in camera in Genymotion devices does not record geolocation
197 | // To test this on a Genymotion emulation, you need to install google play store,
198 | // Download the CameraMX app, turn on location services, set the current location,
199 | // And select the CameraMX app when you take the photo
200 | ExifInterface exifInterface = new ExifInterface(new File(photoPath).getCanonicalPath());
201 | float latlng[] = new float[2];
202 | exifInterface.getLatLong(latlng);
203 |
204 | return new Attempt(latlng[0], latlng[1], photoPath, ++mAttemptCount);
205 |
206 | } catch (IOException e) {
207 | Log.e(TAG, "Unable to retrieve exif tags from image", e);
208 | return null;
209 | }
210 | }
211 |
212 | protected int calculateDistance(Locatable place1, Locatable place2) {
213 | Location place1Loc = new Location("place1");
214 | place1Loc.setLatitude(place1.getLatitude());
215 | place1Loc.setLongitude(place1.getLongitude());
216 | Location place2Loc = new Location("place2");
217 | place2Loc.setLatitude(place2.getLatitude());
218 | place2Loc.setLongitude(place2.getLongitude());
219 | return round(place1Loc.distanceTo(place2Loc));
220 | }
221 | }
222 |
--------------------------------------------------------------------------------
/AndroidBootcamp/src/main/java/com/thoughtworks/androidbootcamp/controller/HelloAndroid.java:
--------------------------------------------------------------------------------
1 | package com.thoughtworks.androidbootcamp.controller;
2 |
3 | import android.app.ActionBar;
4 | import android.app.Activity;
5 | import android.content.Intent;
6 | import android.os.Bundle;
7 | import android.util.Log;
8 | import android.view.Menu;
9 | import android.view.MenuItem;
10 | import android.view.View;
11 | import android.widget.ArrayAdapter;
12 | import android.widget.Toast;
13 |
14 | import com.google.gson.FieldNamingPolicy;
15 | import com.google.gson.GsonBuilder;
16 | import com.thoughtworks.androidbootcamp.R;
17 | import com.thoughtworks.androidbootcamp.controller.fragment.HighScoresFragment;
18 | import com.thoughtworks.androidbootcamp.controller.fragment.TreasureListFragment;
19 | import com.thoughtworks.androidbootcamp.controller.fragment.TreasureMapFragment;
20 | import com.thoughtworks.androidbootcamp.model.Attempt;
21 | import com.thoughtworks.androidbootcamp.model.Game;
22 | import com.thoughtworks.androidbootcamp.model.Score;
23 | import com.thoughtworks.androidbootcamp.model.Treasure;
24 | import com.thoughtworks.androidbootcamp.util.Properties;
25 | import com.thoughtworks.androidbootcamp.util.TreasureLoader;
26 | import com.thoughtworks.androidbootcamp.util.TreasureService;
27 |
28 | import java.util.List;
29 |
30 | import retrofit.Callback;
31 | import retrofit.RestAdapter;
32 | import retrofit.RetrofitError;
33 | import retrofit.client.Response;
34 | import retrofit.converter.GsonConverter;
35 |
36 | import static java.lang.String.format;
37 |
38 | public class HelloAndroid extends Activity implements ActionBar.OnNavigationListener {
39 |
40 | /**
41 | * The serialization (saved instance state) Bundle key representing the
42 | * current dropdown position.
43 | */
44 | private static final String STATE_SELECTED_NAVIGATION_ITEM = "selected_navigation_item";
45 | private static final String STATE_GAME = "game_state";
46 | private static final int PROMPT_FOR_PLAYER = 1000;
47 | private static final String TAG = "HelloAndroid activity";
48 | private Game mGame;
49 | private TreasureService mTreasureService;
50 |
51 | public TreasureService getTreasureService() {
52 | return mTreasureService;
53 | }
54 |
55 | @Override
56 | protected void onCreate(Bundle savedInstanceState) {
57 | super.onCreate(savedInstanceState);
58 | setContentView(R.layout.activity_hello);
59 |
60 | TreasureLoader treasureLoader = new TreasureLoader(this);
61 | treasureLoader.copySampleImages();
62 | mTreasureService = createTreasureService();
63 |
64 | // Set up the action bar to show a dropdown list.
65 | final android.app.ActionBar actionBar = getActionBar();
66 | actionBar.setDisplayShowTitleEnabled(false);
67 | actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_LIST);
68 |
69 | // Set up the dropdown list navigation in the action bar.
70 | actionBar.setListNavigationCallbacks(
71 | // Specify a SpinnerAdapter to populate the dropdown list.
72 | new ArrayAdapter(
73 | actionBar.getThemedContext(),
74 | android.R.layout.simple_list_item_1,
75 | android.R.id.text1,
76 | new String[]{
77 | getString(R.string.title_section1),
78 | getString(R.string.title_section2),
79 | getString(R.string.title_section3),
80 | }),
81 | this);
82 | if (savedInstanceState == null) {
83 | promptForPlayer();
84 | }
85 | }
86 |
87 | protected TreasureService createTreasureService() {
88 | return new RestAdapter.Builder()
89 | .setEndpoint(Properties.SERVICE_URL)
90 | .setConverter(createGsonConverter())
91 | .build()
92 | .create(TreasureService.class);
93 | }
94 |
95 | protected GsonConverter createGsonConverter() {
96 | return new GsonConverter(
97 | new GsonBuilder()
98 | .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
99 | .create()
100 | );
101 | }
102 |
103 | private void promptForPlayer() {
104 | Intent i = new Intent(this, WhoAmIActivity.class);
105 | startActivityForResult(i, PROMPT_FOR_PLAYER);
106 | }
107 |
108 | @Override
109 | protected void onActivityResult(int requestCode, int resultCode, Intent data) {
110 | super.onActivityResult(requestCode, resultCode, data);
111 | if (requestCode == PROMPT_FOR_PLAYER) {
112 | if (resultCode == RESULT_OK) {
113 | getGame().setPlayer(data.getStringExtra(WhoAmIActivity.PLAYER_DATA));
114 | welcomePlayer();
115 | }
116 | }
117 | }
118 |
119 | private void welcomePlayer() {
120 | Toast.makeText(this, "Welcome " + getGame().getPlayer(), Toast.LENGTH_LONG);
121 | }
122 |
123 | @Override
124 | public void onRestoreInstanceState(Bundle savedInstanceState) {
125 | // Restore the previously serialized current dropdown position.
126 | if (savedInstanceState.containsKey(STATE_SELECTED_NAVIGATION_ITEM)) {
127 | getActionBar().setSelectedNavigationItem(
128 | savedInstanceState.getInt(STATE_SELECTED_NAVIGATION_ITEM));
129 | }
130 | if (savedInstanceState.containsKey(STATE_GAME)) {
131 | mGame = (Game) savedInstanceState.getSerializable(STATE_GAME);
132 | welcomePlayer();
133 | }
134 | }
135 |
136 | @Override
137 | public void onSaveInstanceState(Bundle outState) {
138 | // Serialize the current dropdown position.
139 | outState.putInt(STATE_SELECTED_NAVIGATION_ITEM,
140 | getActionBar().getSelectedNavigationIndex());
141 | outState.putSerializable(STATE_GAME, getGame());
142 | }
143 |
144 |
145 | @Override
146 | public boolean onCreateOptionsMenu(Menu menu) {
147 |
148 | // Inflate the menu; this adds items to the action bar if it is present.
149 | getMenuInflater().inflate(R.menu.hello_android, menu);
150 | return true;
151 | }
152 |
153 | @Override
154 | public boolean onOptionsItemSelected(MenuItem item) {
155 | // Handle action bar item clicks here. The action bar will
156 | // automatically handle clicks on the Home/Up button, so long
157 | // as you specify a parent activity in AndroidManifest.xml.
158 | switch (item.getItemId()) {
159 | case R.id.action_settings:
160 | return true;
161 | }
162 | return super.onOptionsItemSelected(item);
163 | }
164 |
165 | @Override
166 | public boolean onNavigationItemSelected(int position, long id) {
167 | switch (position) {
168 | case 0:
169 | showTreasureList();
170 | break;
171 | case 1:
172 | showHighScores();
173 | break;
174 | case 2:
175 | showMap();
176 | break;
177 | }
178 | return true;
179 | }
180 |
181 | protected void showMap() {
182 | getFragmentManager().beginTransaction()
183 | .replace(R.id.container, new TreasureMapFragment())
184 | .commit();
185 | }
186 |
187 | protected void showTreasureList() {
188 | getFragmentManager().beginTransaction()
189 | .replace(R.id.container, new TreasureListFragment())
190 | .commit();
191 | }
192 |
193 | protected void showHighScores() {
194 | getFragmentManager().beginTransaction()
195 | .replace(R.id.container, new HighScoresFragment())
196 | .commit();
197 | }
198 |
199 | public void setTreasures(List treasures) {
200 | getGame().setTreasures(treasures);
201 | }
202 |
203 | public Game getGame() {
204 | if (mGame == null) {
205 | mGame = new Game();
206 | }
207 | return mGame;
208 | }
209 |
210 | public List getTreasures() {
211 | return getGame().getTreasures();
212 | }
213 |
214 | public List getAttempts() {
215 | return getGame().getAttempts();
216 | }
217 |
218 | public boolean hasEnded() {
219 | return getGame().hasEnded();
220 | }
221 |
222 | public void endGame(View view) {
223 | getGame().end();
224 | Toast.makeText(this, getEndGameMessage(), Toast.LENGTH_LONG).show();
225 | sendScoreToServer();
226 | }
227 |
228 | protected String getEndGameMessage() {
229 | // Should use string resources not hardcoded strings for any string that may
230 | // one day need translation - ie anything appearing in the UI
231 | return format(getResources().getString(R.string.end_game_message), getScore());
232 | }
233 |
234 | private void sendScoreToServer() {
235 | // This is an example of how to use Retrofit's callback for asynchronous server calls,
236 | // instead of using an Async task
237 | Callback callback = new Callback() {
238 | @Override
239 | public void success(Score score, Response response) {
240 | showHighScores();
241 | }
242 |
243 | @Override
244 | public void failure(RetrofitError error) {
245 | Log.e(TAG, "Failure posting score");
246 | error.printStackTrace();
247 | }
248 | };
249 | mTreasureService.postScore(getGame().getScore(), callback);
250 | }
251 |
252 | public int getScore() {
253 | return getGame().getScore().getScore();
254 | }
255 |
256 | }
257 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | AndroidBootcampProject
2 | ======================
3 |
4 | ## Objectives
5 | To teach Java programmers the basics of Android development using Agile best practices, by incrementally developing a small game over a period of six weeks.
6 |
7 | ## The App: Treasure Hunt
8 | A single player game. The app supplies a list of photos of 'treasures' in your local area, with hints on how to find them. Your job is to run (or stroll) around the area to find each treasure and take a photo of it. Points are awarded according to how many treasures you can locate and how close you were to the original photo location.
9 |
10 | ## Pre-Bootcamp Preparation
11 |
12 | Follow these setup steps before your first session:
13 |
14 | 1. Install [Java SE SDK 7](http://www.oracle.com/technetwork/java/javase/downloads/index.html) if you don't have it
15 | 1. Install [Git](http://git-scm.com/book/en/Getting-Started-Installing-Git) if you don't have it
16 | 1. Download and install [Android Studio](http://tools.android.com/download/studio/canary)
17 | 1. Create a directory called AndroidStudioProjects. Change to that directory. At the command line, type
18 | ```git clone https://github.com/ThoughtWorksAustralia/AndroidBootcampProject.git```
19 | 1. If you previously had a version of Android installed on your machine, you may need to use Android's [SDK manager](http://developer.android.com/tools/help/sdk-manager.html) to install Build Tools v19.
20 | 1. (Optional) Download and install [Genymotion](https://cloud.genymotion.com/page/launchpad/download/)
21 | 1. Create an account on the [Genymotion cloud](https://cloud.genymotion.com/page/customer/login/)
22 | 1. (Mac only) Download and install [VirtualBox](https://www.virtualbox.org/wiki/Downloads)
23 | 1. Download and install Genymotion and Genymotion Shell
24 | 1. Install the Genymotion plugin for Android Studio:
25 | 1. Android Studio > Preferences > Plugins
26 | 1. Browse Repositories
27 | 1. Search for Genymotion
28 | 1. Choose Download and Install from the context menu
29 | 1. Open the Genymotion app and create a new virtual device
30 |
31 |
32 |
33 | ## Week 1: Hello Android
34 | ###Prerequisites:
35 | In the Android Bootcamp directory you set up earlier, do:
36 | ```
37 | git stash
38 | git checkout start-week-1
39 | ```
40 |
41 |
42 | ###Goals:
43 | * Develop a working "hello world" app and explore its anatomy.
44 | * Allow the user to enter their name.
45 |
46 | ###Material:
47 | [View the Presentation](http://prezi.com/jibn_vzm9rml/?utm_campaign=share&utm_medium=copy)
48 | [Exercise 1: Explore the Lifecycle](https://github.com/macosgrove/AndroidBootcampProject/commit/bd381649f0981bc9d74b90af2389acc364f16914)
49 | [Exercise 2: Create a Second Activity Part 1](https://github.com/macosgrove/AndroidBootcampProject/commit/f794de4638037308e2100b5bf73043df89540231),
50 | [Part 2](https://github.com/macosgrove/AndroidBootcampProject/commit/173f178c6a933500e047120ce2f94b8046d927a7)
51 | [Exercise 3: Capture the Player's Name](https://github.com/macosgrove/AndroidBootcampProject/commit/1716a164608a5710869d36562379d0203dfc2b64)
52 | [Exercise 4: Keep the game state safe](https://github.com/macosgrove/AndroidBootcampProject/commit/50e7ab9107659897a733298e2504c14578ba85c9),
53 | [Optional extra](https://github.com/macosgrove/AndroidBootcampProject/commit/2059476941cedb19db271ee68140b844a7404998)
54 |
55 | ### Resources
56 | [Android Developers site](http://developer.android.com/develop/index.html)
57 | [The Busy Coder's Guide to Android Development](http://commonsware.com/Android/)
58 | [Saving and retrieving instance state](http://www.intertech.com/Blog/saving-and-retrieving-android-instance-state-part-1/)
59 | [Parcelable vs Serializable](http://www.developerphil.com/parcelable-vs-serializable/)
60 | [Genymotion Users Guide](https://cloud.genymotion.com/page/doc/)
61 | [Better performance with the Android emulator](http://stackoverflow.com/questions/2662650/making-the-android-emulator-run-faster)
62 | [Gradle](http://www.gradle.org/)
63 |
64 | ## Week 2: BDD with Android
65 | ###Prerequisites:
66 | In the Android Bootcamp directory you set up earlier, do:
67 | ```
68 | git stash
69 | git checkout start-week-2
70 | ```
71 |
72 | ###Goals:
73 | * Add a framework for behaviour driven development including a unit test and a functional test
74 | * Allow the user to view treasures, high scores, and a map page.
75 |
76 | ###Material:
77 | [View the Presentation](http://prezi.com/78y82u9ld2yy/?utm_campaign=share&utm_medium=copy)
78 | [Exercise 1: Create a failing instrument test](https://github.com/macosgrove/AndroidBootcampProject/commit/9f5d25952ac48d4e6c9ea5a0345c1ece2c43ddae)
79 | [Exercise 2: Add Espresso to the test Part 1](https://github.com/macosgrove/AndroidBootcampProject/commit/5d137c44445bdcc97bb02a53246a3fe6f44a1915),
80 | [Part 2](https://github.com/macosgrove/AndroidBootcampProject/commit/6a10373f167e709c907797aa00953ee899bd8ed2)
81 | [Exercise 3: Some new behaviour](https://github.com/macosgrove/AndroidBootcampProject/commit/ff41e46f18da9ab904607f62a766368459b78db2)
82 | [Exercise 4: Add Unit Tests Part 1,](https://github.com/macosgrove/AndroidBootcampProject/commit/816cc9f7dc56a3d3d09b040891992d336f6bc277)
83 | [Part 2,](https://github.com/macosgrove/AndroidBootcampProject/commit/527d923d5f9d0fe8072422b72c56ba01ee9e5d1c)
84 | [Part 3](https://github.com/macosgrove/AndroidBootcampProject/commit/0549ce579badab5c61b150f71b7bfd0faaf36243)
85 | [Step by step instructions](https://github.com/macosgrove/AndroidBootcampProject/blob/master/BDDinAS.md)
86 | [Exercise 5: Make the tests pass](https://github.com/macosgrove/AndroidBootcampProject/commit/9ef2627ff2e6e40b4d526ba604aadba9bbf128e9)
87 |
88 | ### Resources
89 | [Behaviour Driven Development](http://dannorth.net/introducing-bdd/)
90 | [Testing the Android Way](http://blog.bignerdranch.com/2583-testing-the-android-way/)
91 | [Serious Unit Testing on Android](http://eclipsesource.com/blogs/2012/06/15/serious-unit-testing-on-android/)
92 | [Espresso](https://code.google.com/p/android-test-kit/wiki/Espresso)
93 | [Robolectric](http://robolectric.org/)
94 | [FEST](http://square.github.io/fest-android/)
95 | [Mockito](https://code.google.com/p/mockito/)
96 | [JUnit](https://github.com/junit-team/junit/wiki)
97 |
98 | ## Week 3: Layout and Design
99 | ###Prerequisites:
100 | In the Android Bootcamp directory you set up earlier, do:
101 | ```
102 | git stash
103 | git checkout start-week-3
104 | ```
105 |
106 | ###Goals:
107 | * Layout the welcome screen
108 | * Display a list of treasures
109 |
110 | ###Material:
111 | [View the Presentation](http://prezi.com/v9yrnlv2yerk/?utm_campaign=share&utm_medium=copy&rc=ex0share)
112 | [Exercise 1 Part 1,2,3: Prettify the welcome screen](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/ff34f3207670a7713621aad9814eb367c72cb9e5)
113 | [Exercise 1 Part 4,5,6: Further enhancement](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/db5dbfcbf49251a0b4b8debc622b91c9e18bbb99)
114 | [Exercise 2 Part 1: Write a test for the Treasure List](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/4fa78d569cda9687481846eea6d5662de50d440c)
115 | [Exercise 2 Part 2,3: Create the gridview and adapter](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/4fa78d569cda9687481846eea6d5662de50d440c)
116 |
117 | ### Resources
118 | [Designing for Multiple Screens](http://developer.android.com/training/multiscreen/index.html)
119 | [Styles and Themes](http://developer.android.com/guide/topics/ui/themes.html)
120 | [Layouts](http://developer.android.com/guide/topics/ui/declaring-layout.html)
121 | [Grid View](http://developer.android.com/guide/topics/ui/layout/gridview.html)
122 |
123 | ## Week 4: Using the Camera
124 | ###Prerequisites:
125 | In the Android Bootcamp directory you set up earlier, do:
126 | ```
127 | git stash
128 | git checkout start-week-4
129 | ```
130 |
131 | The default camera app in Genymotion doesn't capture the GPS details for taken photos so we need to install a different camera (otherwise you can use a real device)
132 |
133 | 1. Install google play services, Download the appropriate Google Apps for Android zip file for your device from here: http://stackoverflow.com/a/20013322. Make sure the version of Google Apps you use matches the Android version on your emulated device. For example [gapps-kk-20140105-signed.zip](http://www.androidfilehost.com/?fid=23311191640114013) works with the Google Nexus 5 4.4.2 Genymotion device.
134 | 2. Drag the downloaded zip onto your running Genymotion emulator
135 | 3. Ignore any errors and restart the emulator
136 | 4. Now using the playstore in your emulator find the App called Camera MX and install it
137 | 5. If you cant find it here is the link https://play.google.com/store/apps/details?id=com.magix.camera_mx
138 |
139 | ###Goals:
140 | * Find out how to access the Camera app
141 | * Take a photo of a found Treasure using the phones built in camera app
142 | * Evaluate its proximity to the listed Treasure
143 |
144 | ###Material:
145 | [View the Presentation](http://prezi.com/cvbktfttlnj4/?utm_campaign=share&utm_medium=copy&rc=ex0share)
146 | [Exercise 1: Register a on item click listener for the treasure list](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/2499eac0bdb27ae576bbec69a480ac5080e99e65)
147 | [Exercise 2: Use an Intent to create a photo](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/205897d08c958d37e42f908bc1634dcc319a0c08)
148 | [Exercise 3: Get the captured photo](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/d53461cd1d33c4b87c634ffdb36e1abcb589d2f4)
149 | [Exercise 4: Calculate the distance between the two photos location and show in Toast message](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/8f12a82aa5541fdf568b5f64127b5ccd558a6e62)
150 |
151 | ### Resources
152 | [EXIF](http://en.wikipedia.org/wiki/Exchangeable_image_file_format)
153 | [Intents](http://developer.android.com/guide/components/intents-filters.html)
154 | [Using the Camera in Android](http://developer.android.com/guide/topics/media/camera.html)
155 | [Android Location API (can use to calc distances between two points)](http://developer.android.com/reference/android/location/Location.html)
156 | [Toasts in Android](http://developer.android.com/guide/topics/ui/notifiers/toasts.html)
157 |
158 | ## Week 5: Data and Communication
159 | ###Prerequisites:
160 | In the Android Bootcamp directory you set up earlier, do:
161 | ```
162 | git stash
163 | git checkout start-week-5
164 | ```
165 |
166 | ###Goals:
167 | * Learn about handling data and communicating with a server
168 | * Retrieve a real list of Treasures from a server
169 |
170 | ###Material:
171 | [View the Presentation](http://prezi.com/-svdzfuq7wbi/?utm_campaign=share&utm_medium=copy)
172 | [Exercise 1, Exercise 2 pt1: Write a model for the treasures](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/71612668c44226bdff331438274fd99c7041a442)
173 | [Exercise 2 pt2: Writing the client pt 1](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/1f7d13322bd41c009e79f6351875d7bd0b4ddb34)
174 | [Exercise 2 pt3: Writing the client pt 2](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/e095dfbbe51cbd8eb353099b650f28559825c394)
175 | [Exercise 3 pt1: Using the client](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/1f7d13322bd41c009e79f6351875d7bd0b4ddb34)
176 | [Exercise 3 pt2: Fixing the client](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/992ec60bb00ed3469300cb3b295af7df3405e243)
177 |
178 | ### Resources
179 | [Processes and Threads](http://developer.android.com/guide/components/processes-and-threads.html)
180 | [Retrofit](http://square.github.io/retrofit/)
181 | [JSON](http://www.json.org/)
182 | [http://www.jsonschema2pojo.org](http://www.jsonschema2pojo.org)
183 | [Source code for the Android Bootcamp server](https://github.com/ThoughtWorksAustralia/android-bootcamp-rest-server)
184 |
185 | ## Week 6: Playing with Maps
186 | ###Prerequisites:
187 | In the Android Bootcamp directory you set up earlier, do:
188 | ```
189 | git stash
190 | git checkout start-week-6
191 | ```
192 | Ensure you've installed Google Apps on your emulated device as per the instructions for [week 4](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject#week-4-using-the-camera).
193 |
194 | Open the Android SDK manager (Tools > Android > SDK Manager from Android Studio).
195 | Check Android Support Repository, Google Play Services and Google Repository from the Extras section at the bottom of the window, and click Install Packages.
196 |
197 | ###Goals:
198 | * Explore the Google Maps API
199 | * Complete the game with a map of actual and found Treasure locations. Generate a score.
200 |
201 | ###Material:
202 | [View the Presentation](http://prezi.com/podoewjswxxz/android-bootcamp-week-6/)
203 | [Excercise 1: Set up for using Play Services](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/ac292303cecba45fd3cb45317abd1a73e68fe48f)
204 | NOTE: This exercise causes all Robolectric tests to fail when run on the command line, due to [Robolectric issue 1025!](https://github.com/robolectric/robolectric/issues/1025)
205 | [Exercise 2a: Display the map](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/d5976f1ade07ad7afd34c6fd6bd91828667eb46a)
206 | [Exercise 2b: Nested fragments must be added programmatically!](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/597f5f74e9f5f289faf74fa5cecbc91e9aaac6a4)
207 | [Exercise 2c: Connect to Google Play Services and zoom to our current location](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/ccb596ecf39c0c415007b856a3436e78a558d0e0)
208 | [Exercise 3: Map attempts](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/ab2200d177f207d78815908511bfde8dfb4de50f)
209 | [Exercise 4a: Add Finish Game button to High Scores fragment, and improve styling.](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/50bcb05014be2f4cbf9688796d1c5a2f373d95a8)
210 | [Exercise 4b: Finish the game, and show Treasures on the map once it has ended.](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/a9cffdbe57fd4740be6db7e82a520be90f3dc21d)
211 | [Exercise 4c: Send player's score to the server, and refresh the high scores](https://github.com/ThoughtWorksAustralia/AndroidBootcampProject/commit/56714ec52b4f7d6e2f91a86e7940aea281f8721b)
212 |
213 | ##### Let's explore
214 | [Using image thumbnails as map markers](https://developers.google.com/maps/documentation/android/marker#customize_the_marker_image)
215 | [Clustering markers](https://developers.google.com/maps/documentation/android/utility/marker-clustering)
216 | [Zooming to a collection of markers](http://stackoverflow.com/questions/14636118/android-set-goolgemap-bounds-from-from-database-of-points)
217 | [Drawing a walking route](http://stackoverflow.com/questions/14444228/android-how-to-draw-route-directions-google-maps-api-v2-from-current-location-t)
218 |
219 | ### Resources
220 | [Google Play Services](http://developer.android.com/google/index.html)
221 | [Installing and Configuring the Google Maps V2 API](https://developers.google.com/maps/documentation/android/start#installing_the_google_maps_android_v2_api)
222 | [Google Maps API](http://developer.android.com/google/play-services/maps.html)
223 | [Nested fragments must be added programmatically](http://developer.android.com/about/versions/android-4.2.html#NestedFragments)
224 | [Maps in the Genymotion emulator](http://www.webupd8.org/2013/11/android-x86-emulator-genymotion-20.html)
225 | [Sending high score to server - Retrofit with Custom GSON Converter and Callback](http://square.github.io/retrofit/)
226 |
227 |
--------------------------------------------------------------------------------