<file_sep>AgileDay Conference Android Application 
=======================================

This is a conference client created for Italian Agile Day conference.
The aim is to create a conference client application that will help the 
conference visitors by giving uptodate information about sessions, 
and facilitate communication with other conference participants.


### Build process:

* android update project --path AgileDayConferenceApp
* ant -buildfile AgileDayConferenceApp\build.xml release

### Release build instructions to build signed .apk package for Android Market:

* Run tests and check that they are all green ...run the test suite manually into eclipse :(
* Copy italianagileday.keystore in AgileDayConferenceApp/ (you should have it)
* Update build.properties file
  * uncommenting "key.store" and "key.alias"
  * adding target=android-8 (note: tests and build must run successfully also on the minimum 
supported target-4) 
* Update AndroidManifest.xml
  * Remove android:debuggable
  * Add android:installLocation="auto" in "manifest" element (http://developer.android.com/guide/appendix/install-location.html#Compatiblity)
* android update project --path AgileDayConferenceApp
* ant -buildfile AgileDayConferenceApp\build.xml release
  * will ask you for a password (and you should have it)
* smoke test the application on your mobile
* upload the .apk file on the market

### After Release build steps

* Revert build.properties file
* In AndroidManifest.xml, REMOVE android:installLocation="auto" in "manifest" element
* Increase android:versionCode for next release
* Increase android:versionName for next release
* Commit updated AndroidManifest.xml (the only changes should be in android:versionCode and android:versionName)
<file_sep>/*
	Copyright 2010 
	
	Author: <NAME>

	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at

		http://www.apache.org/licenses/LICENSE-2.0

	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
 */
package it.agileday.data;

import it.agileday.utils.Dates;
import it.agileday.data.Session;

import java.util.Date;

import junit.framework.TestCase;

public class SessionTest extends TestCase {
	public void test_validation() {
		Date d1 = Dates.newDate(2010, 1, 1, 10);
		Date d2 = Dates.newDate(2010, 1, 1, 11);

		assertEquals("End date must be greater than start date", new Session().setStart(d1).setEnd(d1).validationMessage());
		assertEquals("End date must be greater than start date", new Session().setStart(d2).setEnd(d1).validationMessage());
		assertTrue(new Session().setStart(d1).setEnd(d2).isValid());
	}

	public void test_toString() {
		assertEquals("session_123", new Session().setId(123).toString());
		assertEquals("session_0", new Session().toString());
	}
}
<file_sep>/*
	Copyright 2010 
	
	Author: <NAME>
	Author: <NAME>
	Author: <NAME>

	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at

		http://www.apache.org/licenses/LICENSE-2.0

	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
 */

package it.agileday.ui.twitter;

import it.agileday2011.R;
import it.agileday.data.Tweet;
import it.agileday.utils.Dates;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import android.content.Context;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.style.TextAppearanceSpan;
import android.text.util.Linkify;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import android.widget.TextView;

class TweetsAdapter extends BaseAdapter {
	private final List<Tweet> tweets;
	private final Context context;

	public TweetsAdapter(Context context) {
		this.context = context;
		this.tweets = new ArrayList<Tweet>();
	}

	public void addTweets(Collection<Tweet> tweets) {
		this.tweets.addAll(tweets);
	}

	public void addLoadingRow() {
		this.tweets.add(null);
	}

	public void removeLoadingRow() {
		tweets.remove(null);
	}

	private LayoutInflater getLayoutInflater() {
		return (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
	}

	@Override
	public int getCount() {
		return tweets.size();
	}

	@Override
	public Tweet getItem(int position) {
		return tweets.get(position);
	}

	@Override
	public long getItemId(int position) {
		return position;
	}

	@Override
	public View getView(int position, View convertView, ViewGroup parent) {
		Tweet tweet = getItem(position);
		if (tweet != null) {
			return getTweetView(tweet, convertView, parent);
		} else {
			return getLoadingView(convertView, parent);
		}
	}

	private View getLoadingView(View convertView, ViewGroup parent) {
		View ret = convertView;
		if (ret == null || ret.getId() != R.id.items_loading_twitter) {
			ret = getLayoutInflater().inflate(R.layout.twitter_item_loading, parent, false);
		}
		return ret;
	}

	private View getTweetView(Tweet tweet, View convertView, ViewGroup parent) {
		View ret = convertView;
		if (ret == null || ret.getId() != R.id.twitter_item) {
			ret = getLayoutInflater().inflate(R.layout.twitter_item, parent, false);
		}
		Date now = Calendar.getInstance().getTime();
		TextView dateText = (TextView) ret.findViewById(R.id.tweet_date);
		dateText.setText(Dates.differenceSmart(now, tweet.date));

		SpannableString ss = new SpannableString(String.format("%s: %s", tweet.fromUser, tweet.text));
		ss.setSpan(new TextAppearanceSpan(context, R.style.TwitterUser), 0, tweet.fromUser.length() + 1, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
		TextView text = (TextView) ret.findViewById(R.id.tweet_text);
		text.setText(ss);

		Linkify.addLinks(text, Linkify.WEB_URLS);
		ImageView image = (ImageView) ret.findViewById(R.id.tweet_image);
		image.setImageBitmap(tweet.profileImage);
		return ret;
	}

}<file_sep>/*
	Copyright 2010 
	
	Author: <NAME>

	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at

		http://www.apache.org/licenses/LICENSE-2.0

	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
 */

package it.agileday.ui;

import android.test.ActivityInstrumentationTestCase2;

public class AboutTest extends ActivityInstrumentationTestCase2<About> {

	private About activity;

	public AboutTest() {
		super("it.agileday.ui", About.class);
	}

	@Override
	protected void setUp() throws Exception {
		super.setUp();
		activity = this.getActivity();
	}

	public void test_a_oncreate_activity() {
		assertNotNull(activity);
	}

}
<file_sep>/*
	Copyright 2010 
	
	Author: <NAME>

	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at

		http://www.apache.org/licenses/LICENSE-2.0

	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
 */

package it.agileday.ui.speakers;

import it.agileday2011.R;
import it.agileday.data.DatabaseHelper;
import it.agileday.data.Speaker;
import it.agileday.data.SpeakerRepository;
import it.agileday.ui.TextViewUtil;

import java.util.List;

import android.app.Activity;
import android.content.Context;
import android.content.res.TypedArray;
import android.database.sqlite.SQLiteDatabase;
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.AdapterView.OnItemSelectedListener;
import android.widget.BaseAdapter;
import android.widget.Gallery;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.ViewAnimator;

public class SpeakersActivity extends Activity {
	static final String TAG = SpeakersActivity.class.getName();
	private ViewAnimator viewAnimator;
	private Gallery gallery;

	@Override
	public void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.speakers);
		viewAnimator = (ViewAnimator) findViewById(R.id.flipper);
		gallery = (Gallery) findViewById(R.id.gallery);

		List<Speaker> speakers = getSpeakers();

		gallery.setAdapter(new ImageAdapter(this, speakers));

		gallery.setOnItemSelectedListener(new OnItemSelectedListener() {
			@Override
			public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
				viewAnimator.setDisplayedChild(position);
			}

			@Override
			public void onNothingSelected(AdapterView<?> parent) {
			}
		});

		fillData(speakers);
	}

	private void fillData(List<Speaker> speakers) {
		for (Speaker speaker : speakers) {
			View view = getLayoutInflater().inflate(R.layout.speakers_item, viewAnimator, false);
			TextView name = (TextView) view.findViewById(R.id.name);
			name.setText(speaker.name);
			TextView bio = (TextView) view.findViewById(R.id.bio);
			TextViewUtil.setHtmlText(speaker.bio, bio);
			viewAnimator.addView(view);
		}
	}

	private List<Speaker> getSpeakers() {
		SQLiteDatabase database = new DatabaseHelper(this).getReadableDatabase();
		try {
			SpeakerRepository repo = new SpeakerRepository(database, this.getResources(), this);
			return repo.getAll();
		} finally {
			database.close();
		}
	}

	public class ImageAdapter extends BaseAdapter {
		int galleryItemBackground;
		private Context context;
		private final List<Speaker> speakers;

		public ImageAdapter(Context c, List<Speaker> speakers) {
			context = c;
			this.speakers = speakers;
			TypedArray a = obtainStyledAttributes(R.styleable.SpeakersGallery);
			galleryItemBackground = a.getResourceId(R.styleable.SpeakersGallery_android_galleryItemBackground, 0);
			a.recycle();
		}

		public int getCount() {
			return speakers.size();
		}

		public Object getItem(int position) {
			return speakers.get(position);
		}

		public long getItemId(int position) {
			return position;
		}

		public View getView(int position, View convertView, ViewGroup parent) {
			float density = getResources().getDisplayMetrics().density;
			
			ImageView i = new ImageView(context);
			i.setImageDrawable(speakers.get(position).image);
			i.setLayoutParams(new Gallery.LayoutParams((int) (130 * density), (int) (150 * density)));
			i.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
			i.setBackgroundResource(galleryItemBackground);
			return i;
		}
	}
}
<file_sep>/*
	Copyright 2010 
	
	Author: <NAME>

	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at

		http://www.apache.org/licenses/LICENSE-2.0

	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
 */

package it.agileday.data;

import it.agileday2011.R;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.Scanner;

import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

public class DatabaseHelper extends SQLiteOpenHelper {
	private static final String TAG = DatabaseHelper.class.getName();
	private static final String DATABASE_NAME = "data.db";
	private static final int DATABASE_VERSION = 81;
	private final Context context;

	public DatabaseHelper(Context context) {
		super(context, DATABASE_NAME, null, DATABASE_VERSION);
		this.context = context;
	}

	@Override
	public void onCreate(SQLiteDatabase db) {
		InputStream stream = context.getResources().openRawResource(R.raw.database);
		execScript(stream, db);
	}

	@Override
	public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
		Log.w(TAG, "Found database version " + oldVersion + ". Rebuilding database version " + newVersion);
		onCreate(db);
	}

	private static void execScript(InputStream stream, SQLiteDatabase db) {
		Scanner scanner;
		try {
			scanner = new Scanner(new InputStreamReader(stream, "UTF-8"));
		} catch (UnsupportedEncodingException e) {
			throw new RuntimeException(e);
		}
		scanner.useDelimiter(";\\s*(\\n|\\r\\n|\\r)");
		while (scanner.hasNext()) {
			String sql = scanner.next();
			db.execSQL(sql);
		}
	}
}<file_sep>/*
	Copyright 2010 
	
	Author: <NAME>
	Author: <NAME>

	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at

		http://www.apache.org/licenses/LICENSE-2.0

	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
 */

package it.agileday.data;

import java.util.ArrayList;
import java.util.List;

public class TweetList extends ArrayList<Tweet> implements List<Tweet> {
	private static final long serialVersionUID = 1L;
	private static TweetList empty = new TweetList(0);

	public TweetList(int length) {
		super(length);
	}

	public static TweetList Empty() {
		return empty;
	}

}
<file_sep>/*
	Copyright 2010 
	
	Author: <NAME>
	Author: <NAME>

	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at

		http://www.apache.org/licenses/LICENSE-2.0

	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
 */

package it.agileday.data;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

import android.app.Activity;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;

public class TrackRepository {
	private final SQLiteDatabase db;
	private final Activity activity;

	public TrackRepository(SQLiteDatabase db, Activity activityToManageCursorLifeCicle) {
		this.db = db;
		this.activity = activityToManageCursorLifeCicle;
	}

	public ArrayList<Track> getAll() {
		Map<Integer, Track> ret = new HashMap<Integer, Track>();
		String sql = "SELECT track_id, tracks.title AS track_title, tracks.track_order AS track_order, sessions._id AS session_id, sessions.session_type AS session_type, sessions.title AS session_title, sessions.speakers AS session_speakers, sessions.start AS session_start, sessions.end AS session_end FROM sessions JOIN tracks ON(sessions.track_id = tracks._id) ORDER BY track_id, sessions.start";
		Cursor cursor = db.rawQuery(sql, null);
		if (activity != null)
		{
			activity.startManagingCursor(cursor);
		}

		while (cursor.moveToNext()) {
			getTrack(ret, cursor).addSession(SessionRepository.buildSession(cursor));
		}
		cursor.close();
		
		ArrayList<Track> arrayList = new ArrayList<Track>(ret.size());
		arrayList.addAll(ret.values());
		Collections.sort(arrayList);
				
		return arrayList;
	}

	private Track getTrack(Map<Integer, Track> ret, Cursor cursor) {
		int trackId = cursor.getInt(cursor.getColumnIndexOrThrow("track_id"));
		if (!ret.containsKey(trackId)) {
			ret.put(trackId, buildTrack(cursor));
		}
		return ret.get(trackId);
	}

	private Track buildTrack(Cursor c) {
		Track ret = new Track();
		ret.title = c.getString(c.getColumnIndexOrThrow("track_title"));
		ret.order = c.getInt(c.getColumnIndexOrThrow("track_order"));
		return ret;
	}
}
<file_sep>/*
	Copyright 2010 
	
	Author: <NAME>
	Author: <NAME>
	Author: <NAME>

	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at

		http://www.apache.org/licenses/LICENSE-2.0

	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
 */

package it.agileday.data;

import it.agileday.utils.BitmapCache;
import it.agileday.utils.Dates;
import it.agileday.utils.HttpRestUtil;

import java.net.URLEncoder;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

public class TweetRepository {
	@SuppressWarnings("unused")
	private static final String TAG = TweetRepository.class.getName();
	private static final String URL = "http://search.twitter.com/search.json";

	private String nextPageQueryString;
	private final BitmapCache bitmapCache;

	public TweetRepository(String hashTag, BitmapCache bitmapCache) {
		this.nextPageQueryString = "?result_type=recent&rpp=10&q=" + URLEncoder.encode(hashTag);
		this.bitmapCache = bitmapCache;
	}

	public TweetList getNextPage() {

		if (!hasNextPage())
			return TweetList.Empty();

		JSONObject json = HttpRestUtil.httpGetJsonObject(String.format("%s%s", URL, nextPageQueryString));

		nextPageQueryString = json.optString("next_page", null);

		try {
			return fromJson(json.getJSONArray("results"));
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}

	public boolean hasNextPage() {
		return nextPageQueryString != null;
	}

	private TweetList fromJson(JSONArray ary) throws Exception {
		TweetList tweets = new TweetList(ary.length());
		for (int i = 0; i < ary.length(); i++) {
			tweets.add(buildTweet(ary.getJSONObject(i)));
		}
		return tweets;
	}

	public Tweet buildTweet(JSONObject json) {
		Tweet ret = new Tweet();
		try {
			ret.id = json.getLong("id");
			ret.text = json.getString("text");
			ret.fromUser = json.getString("from_user");
			ret.date = Dates.fromTweeterString(json.getString("created_at"));
			ret.profileImage = bitmapCache.get(json.getString("profile_image_url"));
		} catch (JSONException e) {
			throw new RuntimeException(e);
		}
		return ret;
	}

}
<file_sep>/*
	Copyright 2010 
	
	Author: <NAME>

	Licensed under the Apache License, Version 2.0 (the "License");
	you may not use this file except in compliance with the License.
	You may obtain a copy of the License at

		http://www.apache.org/licenses/LICENSE-2.0

	Unless required by applicable law or agreed to in writing, software
	distributed under the License is distributed on an "AS IS" BASIS,
	WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
	See the License for the specific language governing permissions and
	limitations under the License.
 */

package it.agileday.data;

import android.database.sqlite.SQLiteDatabase;
import android.test.AndroidTestCase;

public class SessionRepositoryTest extends AndroidTestCase {

	private SessionRepository m_repo;

	@Override
	protected void setUp() throws Exception {
		super.setUp();
		SQLiteDatabase db = new DatabaseHelper(getContext()).getReadableDatabase();
		m_repo = new SessionRepository(db, null);
	}

	public void test_loading_a_session() {
		final int sessionId = 1;

		Session session = m_repo.getSessionFromId(sessionId);
		assertNotNull(session);
		assertEquals(sessionId, session.getId());
	}

	public void test_loading_an_unavailble_session() {
		final int sessionId = -2;

		Session session = m_repo.getSessionFromId(sessionId);
		assertNotNull(session);
		assertEquals(0, session.getId());
		assertEquals("", session.type);
	}

}
