diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 1806e7d..0000000 --- a/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -.svn -src -.class \ No newline at end of file diff --git a/.svn/all-wcprops b/.svn/all-wcprops new file mode 100644 index 0000000..f110c6c --- /dev/null +++ b/.svn/all-wcprops @@ -0,0 +1,29 @@ +K 25 +svn:wc:ra_dav:version-url +V 68 +/svn/!svn/ver/2086/trunk/assignments/logan_linn/final/PivotalTrackie +END +default.properties +K 25 +svn:wc:ra_dav:version-url +V 87 +/svn/!svn/ver/1887/trunk/assignments/logan_linn/final/PivotalTrackie/default.properties +END +.classpath +K 25 +svn:wc:ra_dav:version-url +V 79 +/svn/!svn/ver/1887/trunk/assignments/logan_linn/final/PivotalTrackie/.classpath +END +.project +K 25 +svn:wc:ra_dav:version-url +V 77 +/svn/!svn/ver/1887/trunk/assignments/logan_linn/final/PivotalTrackie/.project +END +AndroidManifest.xml +K 25 +svn:wc:ra_dav:version-url +V 88 +/svn/!svn/ver/2183/trunk/assignments/logan_linn/final/PivotalTrackie/AndroidManifest.xml +END diff --git a/.svn/dir-props b/.svn/dir-props new file mode 100644 index 0000000..041ae65 --- /dev/null +++ b/.svn/dir-props @@ -0,0 +1,16 @@ +K 10 +svn:ignore +V 113 +# Eclipse meta-information +/.project +/.classpath +/.settings +bin + +# Local Properties +local.properties + +# SVN +.svn + +END diff --git a/.svn/entries b/.svn/entries new file mode 100644 index 0000000..e12767b --- /dev/null +++ b/.svn/entries @@ -0,0 +1,211 @@ +10 + +dir +2086 +https://vtnetapps.googlecode.com/svn/trunk/assignments/logan_linn/final/PivotalTrackie +https://vtnetapps.googlecode.com/svn + + + +2010-11-30T01:50:07.295384Z +2086 +logan.linn@gmail.com +has-props +has-prop-mods + + + + + + + + + + + + +13dd798f-c9a9-0d3a-1410-a4e436c6fec4 + + + + + + + +immediates +((conflict 4 .git dir update deleted edited (version https://vtnetapps.googlecode.com/svn 4 1887 trunk/assignments/logan_linn/final/PivotalTrackie/.git dir) (version https://vtnetapps.googlecode.com/svn 4 1970 trunk/assignments/logan_linn/final/PivotalTrackie/.git none))) + +default.properties +file + + + + +2010-11-01T20:04:10.000000Z +8d79ea50b3b4eb3970a455abbc97b61a +2010-11-08T03:02:57.877705Z +1887 +loganlinn + + + + + + + + + + + + + + + + + + + + + +364 + +.classpath +file + + + + +2010-11-01T20:04:10.000000Z +20cf7e6f1cd84dd2fc63bde4738a2a5e +2010-11-08T03:02:57.877705Z +1887 +loganlinn + + + + + + + + + + + + + + + + + + + + + +273 + +.git +dir + + + +add + + + + + + + + + + + + + +copied +https://vtnetapps.googlecode.com/svn/trunk/assignments/logan_linn/final/PivotalTrackie/.git +1887 + +assets +dir + +.project +file + + + + +2010-11-01T20:04:10.000000Z +6e7fc9fafe2df3163535ddc5419849d0 +2010-11-08T03:02:57.877705Z +1887 +loganlinn + + + + + + + + + + + + + + + + + + + + + +817 + +AndroidManifest.xml +file +2183 + + + +2010-12-01T23:40:12.000000Z +0ccad628faa88afe504efb7320e74b31 +2010-12-02T04:08:08.804433Z +2183 +loganlinn + + + + + + + + + + + + + + + + + + + + + +1076 + +src +dir + +bin +dir + +design +dir + +res +dir + diff --git a/.svn/text-base/.classpath.svn-base b/.svn/text-base/.classpath.svn-base new file mode 100644 index 0000000..609aa00 --- /dev/null +++ b/.svn/text-base/.classpath.svn-base @@ -0,0 +1,7 @@ + + + + + + + diff --git a/.svn/text-base/.project.svn-base b/.svn/text-base/.project.svn-base new file mode 100644 index 0000000..b1e1a2c --- /dev/null +++ b/.svn/text-base/.project.svn-base @@ -0,0 +1,33 @@ + + + PivotalTrackie + + + + + + com.android.ide.eclipse.adt.ResourceManagerBuilder + + + + + com.android.ide.eclipse.adt.PreCompilerBuilder + + + + + org.eclipse.jdt.core.javabuilder + + + + + com.android.ide.eclipse.adt.ApkBuilder + + + + + + com.android.ide.eclipse.adt.AndroidNature + org.eclipse.jdt.core.javanature + + diff --git a/.svn/text-base/AndroidManifest.xml.svn-base b/.svn/text-base/AndroidManifest.xml.svn-base new file mode 100644 index 0000000..b300123 --- /dev/null +++ b/.svn/text-base/AndroidManifest.xml.svn-base @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.svn/text-base/default.properties.svn-base b/.svn/text-base/default.properties.svn-base new file mode 100644 index 0000000..0b9250e --- /dev/null +++ b/.svn/text-base/default.properties.svn-base @@ -0,0 +1,11 @@ +# This file is automatically generated by Android Tools. +# Do not modify this file -- YOUR CHANGES WILL BE ERASED! +# +# This file must be checked in Version Control Systems. +# +# To customize properties used by the Ant build system use, +# "build.properties", and override values to adapt the script to your +# project structure. + +# Project target. +target=android-8 diff --git a/README b/README deleted file mode 100644 index e69de29..0000000 diff --git a/src/com/.svn/all-wcprops b/src/com/.svn/all-wcprops new file mode 100644 index 0000000..78adb46 --- /dev/null +++ b/src/com/.svn/all-wcprops @@ -0,0 +1,5 @@ +K 25 +svn:wc:ra_dav:version-url +V 76 +/svn/!svn/ver/2086/trunk/assignments/logan_linn/final/PivotalTrackie/src/com +END diff --git a/src/com/.svn/dir-prop-base b/src/com/.svn/dir-prop-base new file mode 100644 index 0000000..95b2379 --- /dev/null +++ b/src/com/.svn/dir-prop-base @@ -0,0 +1,6 @@ +K 10 +svn:ignore +V 6 +.git* + +END diff --git a/src/com/.svn/entries b/src/com/.svn/entries new file mode 100644 index 0000000..52537ca --- /dev/null +++ b/src/com/.svn/entries @@ -0,0 +1,31 @@ +10 + +dir +2086 +https://vtnetapps.googlecode.com/svn/trunk/assignments/logan_linn/final/PivotalTrackie/src/com +https://vtnetapps.googlecode.com/svn + + + +2010-11-30T01:50:07.295384Z +2086 +logan.linn@gmail.com +has-props + + + + + + + + + + + + + +13dd798f-c9a9-0d3a-1410-a4e436c6fec4 + +loganlinn +dir + diff --git a/src/com/loganlinn/.svn/all-wcprops b/src/com/loganlinn/.svn/all-wcprops new file mode 100644 index 0000000..39caa90 --- /dev/null +++ b/src/com/loganlinn/.svn/all-wcprops @@ -0,0 +1,5 @@ +K 25 +svn:wc:ra_dav:version-url +V 86 +/svn/!svn/ver/2086/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn +END diff --git a/src/com/loganlinn/.svn/dir-prop-base b/src/com/loganlinn/.svn/dir-prop-base new file mode 100644 index 0000000..95b2379 --- /dev/null +++ b/src/com/loganlinn/.svn/dir-prop-base @@ -0,0 +1,6 @@ +K 10 +svn:ignore +V 6 +.git* + +END diff --git a/src/com/loganlinn/.svn/entries b/src/com/loganlinn/.svn/entries new file mode 100644 index 0000000..b50f391 --- /dev/null +++ b/src/com/loganlinn/.svn/entries @@ -0,0 +1,54 @@ +10 + +dir +2086 +https://vtnetapps.googlecode.com/svn/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn +https://vtnetapps.googlecode.com/svn + + + +2010-11-30T01:50:07.295384Z +2086 +logan.linn@gmail.com +has-props + + + + + + + + + + + + + +13dd798f-c9a9-0d3a-1410-a4e436c6fec4 + +pivotaltrackie +dir + +pivotaltracker +dir + + + + + + + + + + + + + + + + + + +https://vtnetapps.googlecode.com/svn/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/pivotaltracker +1887 + diff --git a/src/com/loganlinn/pivotaltracker/.svn/all-wcprops b/src/com/loganlinn/pivotaltracker/.svn/all-wcprops new file mode 100644 index 0000000..7e22f7f --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/all-wcprops @@ -0,0 +1,71 @@ +K 25 +svn:wc:ra_dav:version-url +V 101 +/svn/!svn/ver/2086/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker +END +PivotalTrackerListener.java +K 25 +svn:wc:ra_dav:version-url +V 129 +/svn/!svn/ver/2028/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker/PivotalTrackerListener.java +END +Story.java +K 25 +svn:wc:ra_dav:version-url +V 112 +/svn/!svn/ver/2389/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker/Story.java +END +IterationEntry.java +K 25 +svn:wc:ra_dav:version-url +V 121 +/svn/!svn/ver/2626/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker/IterationEntry.java +END +StoryEntry.java +K 25 +svn:wc:ra_dav:version-url +V 117 +/svn/!svn/ver/2587/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker/StoryEntry.java +END +CursorQuery.java +K 25 +svn:wc:ra_dav:version-url +V 118 +/svn/!svn/ver/2183/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker/CursorQuery.java +END +PivotalTrackerError.java +K 25 +svn:wc:ra_dav:version-url +V 126 +/svn/!svn/ver/2028/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker/PivotalTrackerError.java +END +Project.java +K 25 +svn:wc:ra_dav:version-url +V 114 +/svn/!svn/ver/2170/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker/Project.java +END +Member.java +K 25 +svn:wc:ra_dav:version-url +V 113 +/svn/!svn/ver/2389/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker/Member.java +END +ProjectEntry.java +K 25 +svn:wc:ra_dav:version-url +V 119 +/svn/!svn/ver/2588/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker/ProjectEntry.java +END +MemberEntry.java +K 25 +svn:wc:ra_dav:version-url +V 118 +/svn/!svn/ver/2389/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker/MemberEntry.java +END +PivotalTracker.java +K 25 +svn:wc:ra_dav:version-url +V 121 +/svn/!svn/ver/2626/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker/PivotalTracker.java +END diff --git a/src/com/loganlinn/pivotaltracker/.svn/dir-prop-base b/src/com/loganlinn/pivotaltracker/.svn/dir-prop-base new file mode 100644 index 0000000..95b2379 --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/dir-prop-base @@ -0,0 +1,6 @@ +K 10 +svn:ignore +V 6 +.git* + +END diff --git a/src/com/loganlinn/pivotaltracker/.svn/entries b/src/com/loganlinn/pivotaltracker/.svn/entries new file mode 100644 index 0000000..5b84dda --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/entries @@ -0,0 +1,660 @@ +10 + +dir +2086 +https://vtnetapps.googlecode.com/svn/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker +https://vtnetapps.googlecode.com/svn + + + +2010-11-30T01:50:07.295384Z +2086 +logan.linn@gmail.com +has-props + + + + + + + + + + + + + +13dd798f-c9a9-0d3a-1410-a4e436c6fec4 + + + + + + + + +() + +PivotalTrackerListener.java +file + + + + +2010-11-22T14:43:30.000000Z +27a36b8e0312f607f4c8b49aa5b3ff4b +2010-11-26T19:31:41.563373Z +2028 +loganlinn + + + + + + + + + + + + + + + + + + + + + +170 + +StoryEntry.java +file +2587 + + + +2010-12-06T06:52:26.000000Z +6436ae026aca946880b6c27666a39dd2 +2010-12-07T22:40:29.935924Z +2587 +loganlinn + + + + + + + + + + + + + + + + + + + + + +4839 + +PivotalTrackerResource.java +file +2183 + + + + + + + + + + + + + + + + + + + +deleted + +StoryTask.java +file +2389 + + + + + + + + + + + + + + + + + + + +deleted + +CursorQuery.java +file +2183 + + + +2010-12-01T22:09:33.000000Z +ee21bc582e42882b3073198ab778f492 +2010-12-02T04:08:08.804433Z +2183 +loganlinn + + + + + + + + + + + + + + + + + + + + + +99 + +PivotalTrackerError.java +file + + + + +2010-11-22T14:45:57.000000Z +52387d4eadbd82d1df700f731f3d51b9 +2010-11-26T19:31:41.563373Z +2028 +loganlinn + + + + + + + + + + + + + + + + + + + + + +511 + +Project.java +file +2170 + + + +2010-12-01T15:16:22.000000Z +4e372ffba8937a64c715cf5346c7a454 +2010-12-01T18:03:53.612881Z +2170 +loganlinn + + + + + + + + + + + + + + + + + + + + + +415 + +Member.java +file +2389 + + + +2010-12-02T23:03:04.000000Z +6e54ab1bf00ce16d474b33ee3c67825c +2010-12-03T04:10:27.338840Z +2389 +loganlinn + + + + + + + + + + + + + + + + + + + + + +259 + +ProjectIntegration.java +file + + + + +2010-11-15T16:29:33.000000Z +2b7fdf54e07910e51508bd22cbfcf2a9 +2010-11-18T06:04:04.323673Z +1972 +loganlinn + + + + + + + + + + + + + + + + + + + + + +76 + +IterationEntry.java +file +2626 + + + +2010-12-08T14:42:33.000000Z +df1ae25c2601b97a2355bce45c63d93c +2010-12-10T17:14:25.002194Z +2626 +loganlinn + + + + + + + + + + + + + + + + + + + + + +73 + +xml +dir + +Story.java +file +2389 + + + +2010-12-03T01:57:00.000000Z +2722d5bd8c89989c2c8f849f1910c310 +2010-12-03T04:10:27.338840Z +2389 +loganlinn + + + + + + + + + + + + + + + + + + + + + +1236 + +Token.java +file +2183 + + + + + + + + + + + + + + + + + + + +deleted + +AsyncPivotalTrackerRunner.java +file +2183 + + + + + + + + + + + + + + + + + + + +deleted + +PivotalTrackerAPI.java +file +2389 + + + + + + + + + + + + + + + + + + + +deleted + +ProjectEntry.java +file +2588 + + + +2010-12-07T22:41:23.000000Z +d4211bd14d5c0cb74f93a9730c87baa8 +2010-12-08T00:10:34.757519Z +2588 +loganlinn + + + + + + + + + + + + + + + + + + + + + +4988 + +Attachment.java +file + + + + +2010-11-15T16:29:33.000000Z +67e6181d4b0ad6fdb6b403dc94998cb9 +2010-11-18T06:04:04.323673Z +1972 +loganlinn + + + + + + + + + + + + + + + + + + + + + +68 + +MemberEntry.java +file +2389 + + + +2010-12-02T23:52:59.000000Z +ab7263eeaacab054ad3951c6bdfe1b53 +2010-12-03T04:10:27.338840Z +2389 +loganlinn + + + + + + + + + + + + + + + + + + + + + +1775 + +PivotalTracker.java +file +2626 + + + +2010-12-08T14:40:02.000000Z +cf22c9390153e55e70ff513df4388917 +2010-12-10T17:14:25.002194Z +2626 +loganlinn + + + + + + + + + + + + + + + + + + + + + +10464 + +ProjectMember.java +file + + + + +2010-11-15T16:29:33.000000Z +4fcceb7714719c2b7434248820195f97 +2010-11-18T06:04:04.323673Z +1972 +loganlinn + + + + + + + + + + + + + + + + + + + + + +71 + +IternationEntry.java +file +2626 + + + + + + + + + + + + + + + + + + + +deleted + diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/Attachment.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/Attachment.java.svn-base new file mode 100644 index 0000000..fbd610f --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/Attachment.java.svn-base @@ -0,0 +1,5 @@ +package com.loganlinn.pivotaltracker; + +public class Attachment { + +} diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/CursorQuery.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/CursorQuery.java.svn-base new file mode 100644 index 0000000..ac05bff --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/CursorQuery.java.svn-base @@ -0,0 +1,5 @@ +package com.loganlinn.pivotaltracker; + +public interface CursorQuery { + String[] PROJECTION = {}; +} diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/IterationEntry.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/IterationEntry.java.svn-base new file mode 100644 index 0000000..2057b6e --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/IterationEntry.java.svn-base @@ -0,0 +1,5 @@ +package com.loganlinn.pivotaltracker; + +public class IterationEntry { + +} diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/Member.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/Member.java.svn-base new file mode 100644 index 0000000..56434ce --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/Member.java.svn-base @@ -0,0 +1,12 @@ +package com.loganlinn.pivotaltracker; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Members; + +public class Member { + public interface DefaultMemberQuery extends CursorQuery{ + String[] PROJECTION = { + Members.NAME + }; + int NAME = 0; + } +} diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/MemberEntry.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/MemberEntry.java.svn-base new file mode 100644 index 0000000..fd4d878 --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/MemberEntry.java.svn-base @@ -0,0 +1,71 @@ +package com.loganlinn.pivotaltracker; + +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.END_TAG; +import static org.xmlpull.v1.XmlPullParser.START_TAG; +import static org.xmlpull.v1.XmlPullParser.TEXT; + +import java.io.IOException; +import java.util.HashMap; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Members; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.SyncColumns; + +public class MemberEntry extends HashMap{ + public static final String[] FIELDS = { + Members.NAME, + Members.EMAIL, + Members.INITIALS, + }; + + public interface MemberTags{ + String MEMBER_ID = "id"; + } + + public MemberEntry(){ + for(String f : FIELDS){ + put(f,null); + } + } + + public void sanitizeNullValues(){ + for(String k: keySet()){ + if(get(k) == null){ + put(k,""); + } + } + } + + public static MemberEntry fromParser(XmlPullParser parser) + throws XmlPullParserException, IOException { + + final int depth = parser.getDepth(); + final MemberEntry entry = new MemberEntry(); + String tag = null; + int type; + while (((type = parser.next()) != END_TAG || parser.getDepth() > depth) + && type != END_DOCUMENT) { + if (type == START_TAG) { + tag = parser.getName(); + } else if (type == END_TAG) { + tag = null; + } else if (type == TEXT) { + if(tag != null){ + String text = parser.getText(); + if(MemberTags.MEMBER_ID.equals(tag)){ + entry.put(Members.MEMBER_ID, text);//store as story_id not id + }else if(entry.containsKey(tag)){ + entry.put(tag, text); + } + } + } + } + + entry.put(SyncColumns.UPDATED, String.valueOf(System.currentTimeMillis())); + + return entry; + } +} diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/PivotalTracker.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/PivotalTracker.java.svn-base new file mode 100644 index 0000000..783f98b --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/PivotalTracker.java.svn-base @@ -0,0 +1,341 @@ +package com.loganlinn.pivotaltracker; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpParams; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Base64; +import android.util.Log; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.ui.XMLTokenHandler; +import com.loganlinn.pivotaltrackie.util.HttpUtils; + +public class PivotalTracker extends HttpUtils { + private static final String TAG = "PivotalTrackerAPI"; + public static final String BASE_ADDRESS = "http://www.pivotaltracker.com"; + public static final String BASE_HTTPS_ADDRESS = "https://www.pivotaltracker.com"; + public static final String TOKEN_PATH = ""; + private static Context sContext; + private static PivotalTracker sInstance = null; + // + private static String sToken = "408384a051c11410ce069a01c1cc665f";// REPLACE + // WITH + // YOUR + // TOKEN + // FOR + // TESTING + private static SimpleDateFormat sDateFormat = new SimpleDateFormat( + "yyyy/MM/dd kk:mm:ss zzz"); // ex 2010/12/03 14:41:52 UTC + private ContentResolver mContentResolver; + + public static String getStoriesUrl(int projectId) { + return BASE_ADDRESS + + String.format("/services/v3/projects/%d/stories", projectId); + } + + public static String getStoriesUrl(String projectId) { + return BASE_ADDRESS + + String.format("/services/v3/projects/%s/stories", projectId); + } + + public static String getStoryUrl(int projectId, int storyId) { + return BASE_ADDRESS + + String.format("/services/v3/projects/%d/stories/%d", + projectId, storyId); + } + + public static String getStoryUrl(String projectId, String storyId) { + return BASE_ADDRESS + + String.format("/services/v3/projects/%s/stories/%s", + projectId, storyId); + } + + public static String getIterationUrl(String projectId) { + return BASE_ADDRESS + + String.format("/services/v3/projects/%s/iterations", + projectId); + } + + /** + * Forces the singleton instance to be recreated (if it exists) with the + * given context + * + * @param ctx + * @return + */ + public static synchronized PivotalTracker newInstance(Context ctx) { + sInstance = new PivotalTracker(ctx); + return sInstance; + } + + /** + * Get the singleton PivotalTracker instance If an instance already exists, + * create one with the passed context, otherwise the context is not used + * + * @param ctx + * @return + */ + public static synchronized PivotalTracker getInstance(Context ctx) { + Log.i(TAG, "Fetching PivotalTracker API"); + if (sInstance == null) { + sInstance = new PivotalTracker(ctx); + } + + return sInstance; + } + + /** + * Private constructor for singleton pattern + * + * @param context + */ + private PivotalTracker(Context context) { + sContext = context; + sInstance = this; + mContentResolver = context.getContentResolver(); + } + + public static String getTokenUrl() { + return BASE_HTTPS_ADDRESS + TOKEN_PATH; + } + + public void fetchToken(final String username, final String password, + PivotalTrackerListener authenticateListener) { + // + // HttpClient client = getHttpClient(sContext); + // URLConnection con = null; + // try { + // con = new URL(getTokenUrl()).openConnection(); + // + // if (con instanceof HttpsURLConnection) { + // Log.w(TAG, "fetchToken connection is Https"); + // // ((HttpsURLConnection) con) + // // .setHostnameVerifier(new AllowAllHostnameVerifier()); + // } + // + // HttpsURLConnection httpsCon = (HttpsURLConnection) con; + // httpsCon.setHostnameVerifier(new AllowAllHostnameVerifier()); + // + // final byte[] credentials = ((String) username + ":" + password) + // .getBytes(); + // + // httpsCon.setRequestProperty("Authorization", "Basic " + // + Base64.encodeToString(credentials, Base64.DEFAULT)); + // httpsCon.setAllowUserInteraction(false); + // httpsCon.setInstanceFollowRedirects(true); + // httpsCon.setRequestMethod("GET"); + // + // Log.i(TAG, "Requesting URL: " + httpsCon.getURL()); + // httpsCon.connect(); + // + // if (httpsCon.getResponseCode() == HttpsURLConnection.HTTP_OK) { + // final String token = new XMLTokenHandler() + // .getTokenAndClose(httpsCon.getInputStream()); + // Log.i(TAG, "Token = " + token); + // + // saveToken(token); + // + // authenticateListener.onSuccess(token); + // } else { + // authenticateListener.onError(new PivotalTrackerError( + // "HttpsConnection responseCode=" + // + httpsCon.getResponseCode())); + // } + // } catch (MalformedURLException e) { + // authenticateListener + // .onError(new PivotalTrackerError(e.getMessage())); + // e.printStackTrace(); + // return; + // } catch (IOException e) { + // authenticateListener + // .onError(new PivotalTrackerError(e.getMessage())); + // e.printStackTrace(); + // return; + // } + + /** + * TODO: FIX HTTPS SUPPORT! This throws javax.net.ssl.SSLException: Not + * trusted server certificate! + */ + HttpParams parameters = new BasicHttpParams(); + SchemeRegistry schemeRegistry = new SchemeRegistry(); + SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory(); + sslSocketFactory + .setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + schemeRegistry.register(new Scheme("https", sslSocketFactory, 443)); + ClientConnectionManager manager = new ThreadSafeClientConnManager( + parameters, schemeRegistry); + HttpClient httpClient = new DefaultHttpClient(manager, parameters); + + // DefaultHttpClient client = HttpUtils.getHttpClient(sContext); + + HttpGet request = new HttpGet(this.getTokenUrl()); + final byte[] credentials = ((String) username + ":" + password) + .getBytes(); + request.setHeader("Authorization", "Basic " + + Base64.encodeToString(credentials, Base64.DEFAULT)); + try { + + HttpResponse response = httpClient.execute(request); + + final String token = new XMLTokenHandler() + .getTokenAndClose(response.getEntity().getContent()); + Log.i(TAG, "Token: " + token); + + // saveToken(token); + + authenticateListener.onSuccess(token); + } catch (ClientProtocolException e) { + authenticateListener + .onError(new PivotalTrackerError(e.getMessage())); + e.printStackTrace(); + } catch (IOException e) { + authenticateListener + .onError(new PivotalTrackerError(e.getMessage())); + e.printStackTrace(); + } + + } + + public static String getToken(Context context) { + sContext = context; + return getToken(); + } + + public static String getToken() { + if (sToken == null) { + Log.i(TAG, "Getting token from SavedPreferences"); + + final SharedPreferences prefs = getPreferences(); + sToken = prefs.getString(Prefs.TOKEN, null); + } + + return sToken; + } + + public static HttpUriRequest getGetRequest(String url) { + final HttpUriRequest request = new HttpGet(url); + request.addHeader("X-TrackerToken", getToken()); + return request; + } + + public static HttpUriRequest getPutRequest(String url) { + final HttpUriRequest request = new HttpPut(url); + request.addHeader("X-TrackerToken", getToken()); + return request; + } + + public static HttpUriRequest getPostRequest(String url) { + final HttpUriRequest request = new HttpPost(url); + request.addHeader("X-TrackerToken", getToken()); + return request; + } + + public static HttpUriRequest getDeleteRequest(String url) { + final HttpUriRequest request = new HttpDelete(url); + request.addHeader("X-TrackerToken", getToken()); + return request; + } + + public static HttpUriRequest getPostRequest(StoryEntry story) { + final String projectId = story.get(Stories.PROJECT_ID); + Log.i(TAG, "Creating new story for project " + projectId); + + final HttpPost request = (HttpPost) getPostRequest(getStoriesUrl(projectId)); + try { + final StringEntity entity = new StringEntity(story.toXml()); + + entity.setContentType("text/xml"); + + request.setHeader("Content-Type", "application/xml"); + request.setEntity(entity); + + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return request; + } + + public static HttpUriRequest getPutRequest(StoryEntry story) { + final String projectId = story.get(Stories.PROJECT_ID); + final String storyId = story.get(Stories.STORY_ID); + + final HttpPut request = (HttpPut) getPutRequest(getStoryUrl(projectId, + storyId)); + try { + final StringEntity entity = new StringEntity(story.toXml()); + + entity.setContentType("text/xml"); + + request.setHeader("Content-Type", "application/xml"); + request.setEntity(entity); + + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return request; + } + + public static HttpUriRequest getDeleteStoryRequest(int storyId, + int projectId) { + + return (HttpDelete) getDeleteRequest(getStoryUrl(storyId, projectId)); + } + + private void saveToken(String token) { + final SharedPreferences prefs = getPreferences(); + prefs.edit().putString(Prefs.TOKEN, token); + } + + public static SharedPreferences getPreferences() { + return sContext.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE); + } + + public interface Prefs { + String NAME = "pivotaltracker"; + String TOKEN = "token"; + String ID = "id"; + } + + public static SimpleDateFormat getUTCDateFormat() { + SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss 'UTC'");// simpledateformat + // is + // not + // thread + // safe + // -- + // return + // a + // new + // instance + df.setTimeZone(TimeZone.getTimeZone("UTC")); + return df; + } + + public static SimpleDateFormat getDateFormat() { + return getUTCDateFormat(); // use the UTC format for now + } +} diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/PivotalTrackerError.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/PivotalTrackerError.java.svn-base new file mode 100644 index 0000000..0bbbb9d --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/PivotalTrackerError.java.svn-base @@ -0,0 +1,30 @@ +package com.loganlinn.pivotaltracker; + +public class PivotalTrackerError extends Throwable { + + private int mStatusCode; + + public PivotalTrackerError(String message) { + super(message); + } + + public PivotalTrackerError(int status) { + mStatusCode = status; + } + + /** + * @return the mStatusCode + */ + public int getmStatusCode() { + return mStatusCode; + } + + /** + * @param mStatusCode + * the mStatusCode to set + */ + public void setmStatusCode(int mStatusCode) { + this.mStatusCode = mStatusCode; + } + +} diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/PivotalTrackerListener.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/PivotalTrackerListener.java.svn-base new file mode 100644 index 0000000..5237f27 --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/PivotalTrackerListener.java.svn-base @@ -0,0 +1,6 @@ +package com.loganlinn.pivotaltracker; + +public interface PivotalTrackerListener { + public void onSuccess(String token); + public void onError(PivotalTrackerError error); +} diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/PivotalTrackerResponseListener.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/PivotalTrackerResponseListener.java.svn-base new file mode 100644 index 0000000..036c2b7 --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/PivotalTrackerResponseListener.java.svn-base @@ -0,0 +1,5 @@ +package com.loganlinn.pivotaltracker; + +public interface AuthenticateListener { + +} diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/Project.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/Project.java.svn-base new file mode 100644 index 0000000..7d8014c --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/Project.java.svn-base @@ -0,0 +1,15 @@ +package com.loganlinn.pivotaltracker; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Projects; + +public class Project { + public interface DefaultProjectsQuery extends CursorQuery { + String[] PROJECTION = {Projects.PROJECT_ID, Projects.NAME, + Projects.CURRENT_VELOCITY, Projects.LAST_ACTIVITY_AT}; + + int PROJECT_ID = 0; + int NAME = 1; + int CURRENT_VELOCITY = 2; + int LAST_ACTIVITY_AT = 3; + } +} diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/ProjectEntry.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/ProjectEntry.java.svn-base new file mode 100644 index 0000000..035d0e1 --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/ProjectEntry.java.svn-base @@ -0,0 +1,160 @@ +package com.loganlinn.pivotaltracker; + +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.END_TAG; +import static org.xmlpull.v1.XmlPullParser.START_TAG; +import static org.xmlpull.v1.XmlPullParser.TEXT; + +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.HashMap; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.util.Log; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract; + +public class ProjectEntry extends HashMap { + private static final String TAG = "ProjectEntry"; + public static final String[] FIELDS = { + ProjectTags.ID, + ProjectTags.NAME, + ProjectTags.ITERATION_LENGTH, + ProjectTags.WEEK_START_DAY, + ProjectTags.POINT_SCALE, + ProjectTags.ACCOUNT, + ProjectTags.VELOCITY_SCHEME, + ProjectTags.CURRENT_VELOCITY, + ProjectTags.INITIAL_VELOCITY, + ProjectTags.NUMBER_OF_DONE_ITERATIONS_TO_SHOW, + ProjectTags.LABELS, + ProjectTags.ALLOW_ATTACHMENTS, + ProjectTags.PUBLIC, + ProjectTags.USE_HTTPS, + ProjectTags.BUGS_AND_CHORES_ARE_ESTIMATABLE, + ProjectTags.COMMIT_MODE, + ProjectTags.LAST_ACTIVITY_AT, + /*ProjectTags.MEMBERSHIPS,*/ + ProjectTags.INTEGRATIONS + }; + + /* Expected XML Tags for Project */ + public interface ProjectTags { + String PROJECTS = "projects"; + String PROJECT = "project"; + + String ID = "id"; + String NAME = "name"; + String ITERATION_LENGTH = "iteration_length"; + String WEEK_START_DAY = "week_start_day"; + String POINT_SCALE = "point_scale"; + String ACCOUNT = "account"; + String VELOCITY_SCHEME = "velocity_scheme"; + String CURRENT_VELOCITY = "current_velocity"; + String INITIAL_VELOCITY = "initial_velocity"; + String NUMBER_OF_DONE_ITERATIONS_TO_SHOW = "number_of_done_iterations_to_show"; + String LABELS = "labels"; + String ALLOW_ATTACHMENTS = "allow_attachments"; + String PUBLIC = "public"; + String USE_HTTPS = "use_https"; + String BUGS_AND_CHORES_ARE_ESTIMATABLE = "bugs_and_chores_are_estimatable"; + String COMMIT_MODE = "commit_mode"; + String LAST_ACTIVITY_AT = "last_activity_at"; + String MEMBERSHIPS = "memberships"; + String INTEGRATIONS = "integrations"; + + } + private long mUpdated = ProjectContract.UPDATED_UNKNOWN; + private static final SimpleDateFormat sDatetimeFormat = PivotalTracker.getDateFormat(); + + public ProjectEntry(){ + for(String f : FIELDS){ + put(f,null); + } + } + public void setUpdated(long u){ + mUpdated = u; + } + + public long getUpdated() { + return mUpdated; + } + + public static ProjectEntry fromParser(XmlPullParser parser) + throws XmlPullParserException, IOException { + final int depth = parser.getDepth(); + final ProjectEntry entry = new ProjectEntry(); + + String tag = null; + int type; + boolean inMemberships = false; + + while (((type = parser.next()) != END_TAG || parser.getDepth() > depth) + && type != END_DOCUMENT) { + if(inMemberships) continue; + if (type == START_TAG) { + tag = parser.getName(); + if(ProjectTags.MEMBERSHIPS.equals(tag)){ + inMemberships = true; + } + } else if (type == END_TAG) { + if(ProjectTags.MEMBERSHIPS.equals(parser.getName())){ + inMemberships = false; + } + tag = null; + } else if (type == TEXT) { + if(tag != null){ + String text = parser.getText(); + //Log.i(TAG, "tag="+tag+"\ttext="+text); + + if(ProjectTags.LAST_ACTIVITY_AT.equals(tag)){ + try { + entry.setUpdated(sDatetimeFormat.parse(text).getTime()); + } catch (ParseException e) { + Log.w(TAG, "Failed to parse updated date"); + e.printStackTrace(); + } + } + + if(entry.containsKey(tag)){ + entry.put(tag, text); + Log.i(TAG, tag+"="+text); + + } + } + +// if (ProjectTags.ID.equals(tag)) { +// } else if (ProjectTags.NAME.equals(tag)){ +// } else if (ProjectTags.ITERATION_LENGTH.equals(tag)){ +// } else if (ProjectTags.WEEK_START_DAY.equals(tag)){ +// } else if (ProjectTags.POINT_SCALE.equals(tag)){ +// } else if (ProjectTags.VELOCITY_SCHEME.equals(tag)){ +// } else if (ProjectTags.CURRENT_VELOCITY.equals(tag)){ +// } else if (ProjectTags.INITIAL_VELOCITY.equals(tag)){ +// } else if (ProjectTags.NUMBER_OF_DONE_ITERATIONS_TO_SHOW.equals(tag)){ +// } else if (ProjectTags.LABELS.equals(tag)){ +// } else if (ProjectTags.ALLOW_ATTACHMENTS.equals(tag)){ +// } else if (ProjectTags.USE_HTTPS.equals(tag)){ +// } else if (ProjectTags.PUBLIC.equals(tag)){ +// } else if (ProjectTags.USE_HTTPS.equals(tag)){ +// } else if (ProjectTags.BUGS_AND_CHORES_ARE_ESTIMATABLE.equals(tag)){ +// } else if (ProjectTags.COMMIT_MODE.equals(tag)){ +// } else if (ProjectTags.LAST_ACTIVITY_AT.equals(tag)){ +// } else if (ProjectTags.MEMBERSHIPS.equals(tag)){ +// } else if (ProjectTags.INTEGRATIONS.equals(tag)){ +// } else if (ProjectTags.MEMBERSHIPS.equals(tag)){ +// //TODO parseMembers(parser, batch, resolver); +// } + } + } + + return entry; + } + + +} + + diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/ProjectIntegration.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/ProjectIntegration.java.svn-base new file mode 100644 index 0000000..1919245 --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/ProjectIntegration.java.svn-base @@ -0,0 +1,5 @@ +package com.loganlinn.pivotaltracker; + +public class ProjectIntegration { + +} diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/ProjectMember.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/ProjectMember.java.svn-base new file mode 100644 index 0000000..84e43d5 --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/ProjectMember.java.svn-base @@ -0,0 +1,5 @@ +package com.loganlinn.pivotaltracker; + +public class ProjectMember { + +} diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/Story.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/Story.java.svn-base new file mode 100644 index 0000000..94bb56d --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/Story.java.svn-base @@ -0,0 +1,55 @@ +package com.loganlinn.pivotaltracker; + +import com.loganlinn.pivotaltracker.StoryEntry.StoryTags; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; + +public class Story { + public static interface DefaultStoriesQuery extends CursorQuery { + String[] PROJECTION = { + Stories.STORY_ID, + Stories.PROJECT_ID, + Stories.NAME, + Stories.CURRENT_STATE, + Stories.ESTIMATE, + Stories.STORY_TYPE + }; + int STORY_ID = 0; + int PROJECT_ID = 1; + int NAME = 2; + int CURRENT_STATE = 3; + int ESTIMATE = 4; + int STORY_TYPE = 5; + } + public static interface DetailStoriesQuery{ + String[] PROJECTION = { + Stories.STORY_ID, + Stories.PROJECT_ID, + Stories.STORY_TYPE, + Stories.URL, + Stories.ESTIMATE, + Stories.CURRENT_STATE, + Stories.DESCRIPTION, + Stories.NAME, + Stories.REQUESTED_BY, + Stories.OWNED_BY, + Stories.CREATED_AT, + Stories.ACCEPTED_AT, + Stories.LABELS, + }; + int STORY_ID = 0; + int PROJECT_ID = 1; + int STORY_TYPE = 2; + int URL = 3; + int ESTIMATE = 4; + int CURRENT_STATE = 5; + int DESCRIPTION = 6; + int NAME = 7; + int REQUESTED_BY = 8; + int OWNED_BY = 9; + int CREATED_AT = 10; + int ACCEPTED_AT = 11; + int LABELS = 12; + //TODO: add full story query + } + +} diff --git a/src/com/loganlinn/pivotaltracker/.svn/text-base/StoryEntry.java.svn-base b/src/com/loganlinn/pivotaltracker/.svn/text-base/StoryEntry.java.svn-base new file mode 100644 index 0000000..ca74eac --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/.svn/text-base/StoryEntry.java.svn-base @@ -0,0 +1,196 @@ +package com.loganlinn.pivotaltracker; + +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.END_TAG; +import static org.xmlpull.v1.XmlPullParser.START_TAG; +import static org.xmlpull.v1.XmlPullParser.TEXT; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.database.Cursor; +import android.os.Bundle; +import android.util.Log; + +import com.loganlinn.pivotaltracker.Story.DefaultStoriesQuery; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.SyncColumns; + +public class StoryEntry extends HashMap { + private static final String TAG = "StoryEntry"; + + public static final String[] FIELDS = { + Stories.STORY_ID, + StoryTags.PROJECT_ID, + StoryTags.STORY_TYPE, + StoryTags.URL, + StoryTags.ESTIMATE, + StoryTags.CURRENT_STATE, + StoryTags.DESCRIPTION, + StoryTags.NAME, + StoryTags.REQUESTED_BY, + StoryTags.OWNED_BY, + StoryTags.CREATED_AT, + StoryTags.ACCEPTED_AT, + StoryTags.LABELS + //TODO: Allow add-in fields for integrations + }; + + public static final String[] SAVEABLE_FIELDS = { + StoryTags.ESTIMATE, + StoryTags.DESCRIPTION, + StoryTags.NAME, + StoryTags.REQUESTED_BY, + StoryTags.OWNED_BY, + StoryTags.LABELS + }; + + public interface StoryTags { + String STORY = "story"; + + String STORY_ID = "id"; + String PROJECT_ID = "project_id"; + String STORY_TYPE = "story_type"; + String URL = "url"; + String ESTIMATE = "estimate"; + String CURRENT_STATE = "current_state"; + String DESCRIPTION = "description"; + String NAME = "name"; + String REQUESTED_BY = "requested_by"; + String OWNED_BY = "owned_by"; + String CREATED_AT = "created_at"; + String ACCEPTED_AT = "accepted_at"; + String LABELS = "labels"; + String ATTACHMENTS = "attachments"; + } + + public StoryEntry(){ + for(String f : FIELDS){ + put(f,null); + } + } + + public void sanitizeNullValues(){ + for(String k: keySet()){ + if(get(k) == null){ + put(k,""); + } + } + } + public static StoryEntry fromCursor(Cursor cursor){ + return fromCursor(cursor, DefaultStoriesQuery.PROJECTION); + } + + + public static StoryEntry fromCursor(Cursor cursor, String[] projection){ + final StoryEntry entry = new StoryEntry(); + final int len = projection.length; + + for(int i = 0; i < len; i++){ + entry.put(projection[i], cursor.getString(i)); + } + + return entry; + } + + public static StoryEntry fromParser(XmlPullParser parser) + throws XmlPullParserException, IOException { + final int depth = parser.getDepth(); + final StoryEntry entry = new StoryEntry(); + + String tag = null; + int type; + + while (((type = parser.next()) != END_TAG || parser.getDepth() > depth) + && type != END_DOCUMENT) { + if (type == START_TAG) { + tag = parser.getName(); + } else if (type == END_TAG) { + tag = null; + } else if (type == TEXT) { + if(tag != null){ + String text = parser.getText(); + if(StoryTags.STORY_ID.equals(tag)){ + entry.put(Stories.STORY_ID, text);//store as story_id not id + }else if(entry.containsKey(tag)){ + entry.put(tag, text); + } + } + } + } + + entry.put(SyncColumns.UPDATED, String.valueOf(System.currentTimeMillis())); + + return entry; + } + + public static StoryEntry fromBundle(Bundle data, String[] projection) { + final StoryEntry entry = new StoryEntry(); + for(String f: projection){ + if(Stories.STORY_ID.equals(f) || Stories.PROJECT_ID.equals(f)){ + final Object storyId = data.get(f); + + if(storyId instanceof String){ + entry.put(f, (String) storyId); + }else{ + entry.put(f, String.valueOf(storyId)); + } + }else{ + entry.put(f, data.getString(f)); + } + } + return entry; + } + + public static StoryEntry fromBundle(Bundle data){ + return fromBundle(data, Story.DefaultStoriesQuery.PROJECTION); + } + + public Bundle toBundle() { + final Bundle bundle = new Bundle(); + for(String f: FIELDS){ + bundle.putString(f, get(f)); + } + return bundle; + } + + public String toXml(){ + final StringBuilder builder = new StringBuilder(); + Log.i(TAG, this.toString()); + + builder.append(""); + for(String k: SAVEABLE_FIELDS){ + final String v = get(k); + if(v != null && !v.equals("")){ + builder.append("<"+k+">"); + builder.append(v); + builder.append(""); + } + } + builder.append(""); + Log.i(TAG, builder.toString()); + + return builder.toString(); + } + + public String toString(){ + final StringBuilder builder = new StringBuilder(); + builder.append("{"); + + for(String k : keySet()){ + builder.append(k+":"+get(k)); + builder.append(", "); + } + + builder.append("}"); + + return builder.toString(); + } +} + diff --git a/src/com/loganlinn/pivotaltracker/Attachment.java b/src/com/loganlinn/pivotaltracker/Attachment.java new file mode 100644 index 0000000..fbd610f --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/Attachment.java @@ -0,0 +1,5 @@ +package com.loganlinn.pivotaltracker; + +public class Attachment { + +} diff --git a/src/com/loganlinn/pivotaltracker/CursorQuery.java b/src/com/loganlinn/pivotaltracker/CursorQuery.java new file mode 100644 index 0000000..ac05bff --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/CursorQuery.java @@ -0,0 +1,5 @@ +package com.loganlinn.pivotaltracker; + +public interface CursorQuery { + String[] PROJECTION = {}; +} diff --git a/src/com/loganlinn/pivotaltracker/IterationEntry.java b/src/com/loganlinn/pivotaltracker/IterationEntry.java new file mode 100644 index 0000000..2057b6e --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/IterationEntry.java @@ -0,0 +1,5 @@ +package com.loganlinn.pivotaltracker; + +public class IterationEntry { + +} diff --git a/src/com/loganlinn/pivotaltracker/Member.java b/src/com/loganlinn/pivotaltracker/Member.java new file mode 100644 index 0000000..56434ce --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/Member.java @@ -0,0 +1,12 @@ +package com.loganlinn.pivotaltracker; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Members; + +public class Member { + public interface DefaultMemberQuery extends CursorQuery{ + String[] PROJECTION = { + Members.NAME + }; + int NAME = 0; + } +} diff --git a/src/com/loganlinn/pivotaltracker/MemberEntry.java b/src/com/loganlinn/pivotaltracker/MemberEntry.java new file mode 100644 index 0000000..fd4d878 --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/MemberEntry.java @@ -0,0 +1,71 @@ +package com.loganlinn.pivotaltracker; + +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.END_TAG; +import static org.xmlpull.v1.XmlPullParser.START_TAG; +import static org.xmlpull.v1.XmlPullParser.TEXT; + +import java.io.IOException; +import java.util.HashMap; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Members; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.SyncColumns; + +public class MemberEntry extends HashMap{ + public static final String[] FIELDS = { + Members.NAME, + Members.EMAIL, + Members.INITIALS, + }; + + public interface MemberTags{ + String MEMBER_ID = "id"; + } + + public MemberEntry(){ + for(String f : FIELDS){ + put(f,null); + } + } + + public void sanitizeNullValues(){ + for(String k: keySet()){ + if(get(k) == null){ + put(k,""); + } + } + } + + public static MemberEntry fromParser(XmlPullParser parser) + throws XmlPullParserException, IOException { + + final int depth = parser.getDepth(); + final MemberEntry entry = new MemberEntry(); + String tag = null; + int type; + while (((type = parser.next()) != END_TAG || parser.getDepth() > depth) + && type != END_DOCUMENT) { + if (type == START_TAG) { + tag = parser.getName(); + } else if (type == END_TAG) { + tag = null; + } else if (type == TEXT) { + if(tag != null){ + String text = parser.getText(); + if(MemberTags.MEMBER_ID.equals(tag)){ + entry.put(Members.MEMBER_ID, text);//store as story_id not id + }else if(entry.containsKey(tag)){ + entry.put(tag, text); + } + } + } + } + + entry.put(SyncColumns.UPDATED, String.valueOf(System.currentTimeMillis())); + + return entry; + } +} diff --git a/src/com/loganlinn/pivotaltracker/PivotalTracker.java b/src/com/loganlinn/pivotaltracker/PivotalTracker.java new file mode 100644 index 0000000..783f98b --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/PivotalTracker.java @@ -0,0 +1,341 @@ +package com.loganlinn.pivotaltracker; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.text.SimpleDateFormat; +import java.util.TimeZone; + +import org.apache.http.HttpResponse; +import org.apache.http.client.ClientProtocolException; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpDelete; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.methods.HttpPut; +import org.apache.http.client.methods.HttpUriRequest; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpParams; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Base64; +import android.util.Log; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.ui.XMLTokenHandler; +import com.loganlinn.pivotaltrackie.util.HttpUtils; + +public class PivotalTracker extends HttpUtils { + private static final String TAG = "PivotalTrackerAPI"; + public static final String BASE_ADDRESS = "http://www.pivotaltracker.com"; + public static final String BASE_HTTPS_ADDRESS = "https://www.pivotaltracker.com"; + public static final String TOKEN_PATH = ""; + private static Context sContext; + private static PivotalTracker sInstance = null; + // + private static String sToken = "408384a051c11410ce069a01c1cc665f";// REPLACE + // WITH + // YOUR + // TOKEN + // FOR + // TESTING + private static SimpleDateFormat sDateFormat = new SimpleDateFormat( + "yyyy/MM/dd kk:mm:ss zzz"); // ex 2010/12/03 14:41:52 UTC + private ContentResolver mContentResolver; + + public static String getStoriesUrl(int projectId) { + return BASE_ADDRESS + + String.format("/services/v3/projects/%d/stories", projectId); + } + + public static String getStoriesUrl(String projectId) { + return BASE_ADDRESS + + String.format("/services/v3/projects/%s/stories", projectId); + } + + public static String getStoryUrl(int projectId, int storyId) { + return BASE_ADDRESS + + String.format("/services/v3/projects/%d/stories/%d", + projectId, storyId); + } + + public static String getStoryUrl(String projectId, String storyId) { + return BASE_ADDRESS + + String.format("/services/v3/projects/%s/stories/%s", + projectId, storyId); + } + + public static String getIterationUrl(String projectId) { + return BASE_ADDRESS + + String.format("/services/v3/projects/%s/iterations", + projectId); + } + + /** + * Forces the singleton instance to be recreated (if it exists) with the + * given context + * + * @param ctx + * @return + */ + public static synchronized PivotalTracker newInstance(Context ctx) { + sInstance = new PivotalTracker(ctx); + return sInstance; + } + + /** + * Get the singleton PivotalTracker instance If an instance already exists, + * create one with the passed context, otherwise the context is not used + * + * @param ctx + * @return + */ + public static synchronized PivotalTracker getInstance(Context ctx) { + Log.i(TAG, "Fetching PivotalTracker API"); + if (sInstance == null) { + sInstance = new PivotalTracker(ctx); + } + + return sInstance; + } + + /** + * Private constructor for singleton pattern + * + * @param context + */ + private PivotalTracker(Context context) { + sContext = context; + sInstance = this; + mContentResolver = context.getContentResolver(); + } + + public static String getTokenUrl() { + return BASE_HTTPS_ADDRESS + TOKEN_PATH; + } + + public void fetchToken(final String username, final String password, + PivotalTrackerListener authenticateListener) { + // + // HttpClient client = getHttpClient(sContext); + // URLConnection con = null; + // try { + // con = new URL(getTokenUrl()).openConnection(); + // + // if (con instanceof HttpsURLConnection) { + // Log.w(TAG, "fetchToken connection is Https"); + // // ((HttpsURLConnection) con) + // // .setHostnameVerifier(new AllowAllHostnameVerifier()); + // } + // + // HttpsURLConnection httpsCon = (HttpsURLConnection) con; + // httpsCon.setHostnameVerifier(new AllowAllHostnameVerifier()); + // + // final byte[] credentials = ((String) username + ":" + password) + // .getBytes(); + // + // httpsCon.setRequestProperty("Authorization", "Basic " + // + Base64.encodeToString(credentials, Base64.DEFAULT)); + // httpsCon.setAllowUserInteraction(false); + // httpsCon.setInstanceFollowRedirects(true); + // httpsCon.setRequestMethod("GET"); + // + // Log.i(TAG, "Requesting URL: " + httpsCon.getURL()); + // httpsCon.connect(); + // + // if (httpsCon.getResponseCode() == HttpsURLConnection.HTTP_OK) { + // final String token = new XMLTokenHandler() + // .getTokenAndClose(httpsCon.getInputStream()); + // Log.i(TAG, "Token = " + token); + // + // saveToken(token); + // + // authenticateListener.onSuccess(token); + // } else { + // authenticateListener.onError(new PivotalTrackerError( + // "HttpsConnection responseCode=" + // + httpsCon.getResponseCode())); + // } + // } catch (MalformedURLException e) { + // authenticateListener + // .onError(new PivotalTrackerError(e.getMessage())); + // e.printStackTrace(); + // return; + // } catch (IOException e) { + // authenticateListener + // .onError(new PivotalTrackerError(e.getMessage())); + // e.printStackTrace(); + // return; + // } + + /** + * TODO: FIX HTTPS SUPPORT! This throws javax.net.ssl.SSLException: Not + * trusted server certificate! + */ + HttpParams parameters = new BasicHttpParams(); + SchemeRegistry schemeRegistry = new SchemeRegistry(); + SSLSocketFactory sslSocketFactory = SSLSocketFactory.getSocketFactory(); + sslSocketFactory + .setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER); + schemeRegistry.register(new Scheme("https", sslSocketFactory, 443)); + ClientConnectionManager manager = new ThreadSafeClientConnManager( + parameters, schemeRegistry); + HttpClient httpClient = new DefaultHttpClient(manager, parameters); + + // DefaultHttpClient client = HttpUtils.getHttpClient(sContext); + + HttpGet request = new HttpGet(this.getTokenUrl()); + final byte[] credentials = ((String) username + ":" + password) + .getBytes(); + request.setHeader("Authorization", "Basic " + + Base64.encodeToString(credentials, Base64.DEFAULT)); + try { + + HttpResponse response = httpClient.execute(request); + + final String token = new XMLTokenHandler() + .getTokenAndClose(response.getEntity().getContent()); + Log.i(TAG, "Token: " + token); + + // saveToken(token); + + authenticateListener.onSuccess(token); + } catch (ClientProtocolException e) { + authenticateListener + .onError(new PivotalTrackerError(e.getMessage())); + e.printStackTrace(); + } catch (IOException e) { + authenticateListener + .onError(new PivotalTrackerError(e.getMessage())); + e.printStackTrace(); + } + + } + + public static String getToken(Context context) { + sContext = context; + return getToken(); + } + + public static String getToken() { + if (sToken == null) { + Log.i(TAG, "Getting token from SavedPreferences"); + + final SharedPreferences prefs = getPreferences(); + sToken = prefs.getString(Prefs.TOKEN, null); + } + + return sToken; + } + + public static HttpUriRequest getGetRequest(String url) { + final HttpUriRequest request = new HttpGet(url); + request.addHeader("X-TrackerToken", getToken()); + return request; + } + + public static HttpUriRequest getPutRequest(String url) { + final HttpUriRequest request = new HttpPut(url); + request.addHeader("X-TrackerToken", getToken()); + return request; + } + + public static HttpUriRequest getPostRequest(String url) { + final HttpUriRequest request = new HttpPost(url); + request.addHeader("X-TrackerToken", getToken()); + return request; + } + + public static HttpUriRequest getDeleteRequest(String url) { + final HttpUriRequest request = new HttpDelete(url); + request.addHeader("X-TrackerToken", getToken()); + return request; + } + + public static HttpUriRequest getPostRequest(StoryEntry story) { + final String projectId = story.get(Stories.PROJECT_ID); + Log.i(TAG, "Creating new story for project " + projectId); + + final HttpPost request = (HttpPost) getPostRequest(getStoriesUrl(projectId)); + try { + final StringEntity entity = new StringEntity(story.toXml()); + + entity.setContentType("text/xml"); + + request.setHeader("Content-Type", "application/xml"); + request.setEntity(entity); + + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return request; + } + + public static HttpUriRequest getPutRequest(StoryEntry story) { + final String projectId = story.get(Stories.PROJECT_ID); + final String storyId = story.get(Stories.STORY_ID); + + final HttpPut request = (HttpPut) getPutRequest(getStoryUrl(projectId, + storyId)); + try { + final StringEntity entity = new StringEntity(story.toXml()); + + entity.setContentType("text/xml"); + + request.setHeader("Content-Type", "application/xml"); + request.setEntity(entity); + + } catch (UnsupportedEncodingException e) { + e.printStackTrace(); + } + return request; + } + + public static HttpUriRequest getDeleteStoryRequest(int storyId, + int projectId) { + + return (HttpDelete) getDeleteRequest(getStoryUrl(storyId, projectId)); + } + + private void saveToken(String token) { + final SharedPreferences prefs = getPreferences(); + prefs.edit().putString(Prefs.TOKEN, token); + } + + public static SharedPreferences getPreferences() { + return sContext.getSharedPreferences(Prefs.NAME, Context.MODE_PRIVATE); + } + + public interface Prefs { + String NAME = "pivotaltracker"; + String TOKEN = "token"; + String ID = "id"; + } + + public static SimpleDateFormat getUTCDateFormat() { + SimpleDateFormat df = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss 'UTC'");// simpledateformat + // is + // not + // thread + // safe + // -- + // return + // a + // new + // instance + df.setTimeZone(TimeZone.getTimeZone("UTC")); + return df; + } + + public static SimpleDateFormat getDateFormat() { + return getUTCDateFormat(); // use the UTC format for now + } +} diff --git a/src/com/loganlinn/pivotaltracker/PivotalTrackerError.java b/src/com/loganlinn/pivotaltracker/PivotalTrackerError.java new file mode 100644 index 0000000..0bbbb9d --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/PivotalTrackerError.java @@ -0,0 +1,30 @@ +package com.loganlinn.pivotaltracker; + +public class PivotalTrackerError extends Throwable { + + private int mStatusCode; + + public PivotalTrackerError(String message) { + super(message); + } + + public PivotalTrackerError(int status) { + mStatusCode = status; + } + + /** + * @return the mStatusCode + */ + public int getmStatusCode() { + return mStatusCode; + } + + /** + * @param mStatusCode + * the mStatusCode to set + */ + public void setmStatusCode(int mStatusCode) { + this.mStatusCode = mStatusCode; + } + +} diff --git a/src/com/loganlinn/pivotaltracker/PivotalTrackerListener.java b/src/com/loganlinn/pivotaltracker/PivotalTrackerListener.java new file mode 100644 index 0000000..5237f27 --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/PivotalTrackerListener.java @@ -0,0 +1,6 @@ +package com.loganlinn.pivotaltracker; + +public interface PivotalTrackerListener { + public void onSuccess(String token); + public void onError(PivotalTrackerError error); +} diff --git a/src/com/loganlinn/pivotaltracker/Project.java b/src/com/loganlinn/pivotaltracker/Project.java new file mode 100644 index 0000000..7d8014c --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/Project.java @@ -0,0 +1,15 @@ +package com.loganlinn.pivotaltracker; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Projects; + +public class Project { + public interface DefaultProjectsQuery extends CursorQuery { + String[] PROJECTION = {Projects.PROJECT_ID, Projects.NAME, + Projects.CURRENT_VELOCITY, Projects.LAST_ACTIVITY_AT}; + + int PROJECT_ID = 0; + int NAME = 1; + int CURRENT_VELOCITY = 2; + int LAST_ACTIVITY_AT = 3; + } +} diff --git a/src/com/loganlinn/pivotaltracker/ProjectEntry.java b/src/com/loganlinn/pivotaltracker/ProjectEntry.java new file mode 100644 index 0000000..035d0e1 --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/ProjectEntry.java @@ -0,0 +1,160 @@ +package com.loganlinn.pivotaltracker; + +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.END_TAG; +import static org.xmlpull.v1.XmlPullParser.START_TAG; +import static org.xmlpull.v1.XmlPullParser.TEXT; + +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.HashMap; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.util.Log; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract; + +public class ProjectEntry extends HashMap { + private static final String TAG = "ProjectEntry"; + public static final String[] FIELDS = { + ProjectTags.ID, + ProjectTags.NAME, + ProjectTags.ITERATION_LENGTH, + ProjectTags.WEEK_START_DAY, + ProjectTags.POINT_SCALE, + ProjectTags.ACCOUNT, + ProjectTags.VELOCITY_SCHEME, + ProjectTags.CURRENT_VELOCITY, + ProjectTags.INITIAL_VELOCITY, + ProjectTags.NUMBER_OF_DONE_ITERATIONS_TO_SHOW, + ProjectTags.LABELS, + ProjectTags.ALLOW_ATTACHMENTS, + ProjectTags.PUBLIC, + ProjectTags.USE_HTTPS, + ProjectTags.BUGS_AND_CHORES_ARE_ESTIMATABLE, + ProjectTags.COMMIT_MODE, + ProjectTags.LAST_ACTIVITY_AT, + /*ProjectTags.MEMBERSHIPS,*/ + ProjectTags.INTEGRATIONS + }; + + /* Expected XML Tags for Project */ + public interface ProjectTags { + String PROJECTS = "projects"; + String PROJECT = "project"; + + String ID = "id"; + String NAME = "name"; + String ITERATION_LENGTH = "iteration_length"; + String WEEK_START_DAY = "week_start_day"; + String POINT_SCALE = "point_scale"; + String ACCOUNT = "account"; + String VELOCITY_SCHEME = "velocity_scheme"; + String CURRENT_VELOCITY = "current_velocity"; + String INITIAL_VELOCITY = "initial_velocity"; + String NUMBER_OF_DONE_ITERATIONS_TO_SHOW = "number_of_done_iterations_to_show"; + String LABELS = "labels"; + String ALLOW_ATTACHMENTS = "allow_attachments"; + String PUBLIC = "public"; + String USE_HTTPS = "use_https"; + String BUGS_AND_CHORES_ARE_ESTIMATABLE = "bugs_and_chores_are_estimatable"; + String COMMIT_MODE = "commit_mode"; + String LAST_ACTIVITY_AT = "last_activity_at"; + String MEMBERSHIPS = "memberships"; + String INTEGRATIONS = "integrations"; + + } + private long mUpdated = ProjectContract.UPDATED_UNKNOWN; + private static final SimpleDateFormat sDatetimeFormat = PivotalTracker.getDateFormat(); + + public ProjectEntry(){ + for(String f : FIELDS){ + put(f,null); + } + } + public void setUpdated(long u){ + mUpdated = u; + } + + public long getUpdated() { + return mUpdated; + } + + public static ProjectEntry fromParser(XmlPullParser parser) + throws XmlPullParserException, IOException { + final int depth = parser.getDepth(); + final ProjectEntry entry = new ProjectEntry(); + + String tag = null; + int type; + boolean inMemberships = false; + + while (((type = parser.next()) != END_TAG || parser.getDepth() > depth) + && type != END_DOCUMENT) { + if(inMemberships) continue; + if (type == START_TAG) { + tag = parser.getName(); + if(ProjectTags.MEMBERSHIPS.equals(tag)){ + inMemberships = true; + } + } else if (type == END_TAG) { + if(ProjectTags.MEMBERSHIPS.equals(parser.getName())){ + inMemberships = false; + } + tag = null; + } else if (type == TEXT) { + if(tag != null){ + String text = parser.getText(); + //Log.i(TAG, "tag="+tag+"\ttext="+text); + + if(ProjectTags.LAST_ACTIVITY_AT.equals(tag)){ + try { + entry.setUpdated(sDatetimeFormat.parse(text).getTime()); + } catch (ParseException e) { + Log.w(TAG, "Failed to parse updated date"); + e.printStackTrace(); + } + } + + if(entry.containsKey(tag)){ + entry.put(tag, text); + Log.i(TAG, tag+"="+text); + + } + } + +// if (ProjectTags.ID.equals(tag)) { +// } else if (ProjectTags.NAME.equals(tag)){ +// } else if (ProjectTags.ITERATION_LENGTH.equals(tag)){ +// } else if (ProjectTags.WEEK_START_DAY.equals(tag)){ +// } else if (ProjectTags.POINT_SCALE.equals(tag)){ +// } else if (ProjectTags.VELOCITY_SCHEME.equals(tag)){ +// } else if (ProjectTags.CURRENT_VELOCITY.equals(tag)){ +// } else if (ProjectTags.INITIAL_VELOCITY.equals(tag)){ +// } else if (ProjectTags.NUMBER_OF_DONE_ITERATIONS_TO_SHOW.equals(tag)){ +// } else if (ProjectTags.LABELS.equals(tag)){ +// } else if (ProjectTags.ALLOW_ATTACHMENTS.equals(tag)){ +// } else if (ProjectTags.USE_HTTPS.equals(tag)){ +// } else if (ProjectTags.PUBLIC.equals(tag)){ +// } else if (ProjectTags.USE_HTTPS.equals(tag)){ +// } else if (ProjectTags.BUGS_AND_CHORES_ARE_ESTIMATABLE.equals(tag)){ +// } else if (ProjectTags.COMMIT_MODE.equals(tag)){ +// } else if (ProjectTags.LAST_ACTIVITY_AT.equals(tag)){ +// } else if (ProjectTags.MEMBERSHIPS.equals(tag)){ +// } else if (ProjectTags.INTEGRATIONS.equals(tag)){ +// } else if (ProjectTags.MEMBERSHIPS.equals(tag)){ +// //TODO parseMembers(parser, batch, resolver); +// } + } + } + + return entry; + } + + +} + + diff --git a/src/com/loganlinn/pivotaltracker/ProjectIntegration.java b/src/com/loganlinn/pivotaltracker/ProjectIntegration.java new file mode 100644 index 0000000..1919245 --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/ProjectIntegration.java @@ -0,0 +1,5 @@ +package com.loganlinn.pivotaltracker; + +public class ProjectIntegration { + +} diff --git a/src/com/loganlinn/pivotaltracker/ProjectMember.java b/src/com/loganlinn/pivotaltracker/ProjectMember.java new file mode 100644 index 0000000..84e43d5 --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/ProjectMember.java @@ -0,0 +1,5 @@ +package com.loganlinn.pivotaltracker; + +public class ProjectMember { + +} diff --git a/src/com/loganlinn/pivotaltracker/Story.java b/src/com/loganlinn/pivotaltracker/Story.java new file mode 100644 index 0000000..94bb56d --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/Story.java @@ -0,0 +1,55 @@ +package com.loganlinn.pivotaltracker; + +import com.loganlinn.pivotaltracker.StoryEntry.StoryTags; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; + +public class Story { + public static interface DefaultStoriesQuery extends CursorQuery { + String[] PROJECTION = { + Stories.STORY_ID, + Stories.PROJECT_ID, + Stories.NAME, + Stories.CURRENT_STATE, + Stories.ESTIMATE, + Stories.STORY_TYPE + }; + int STORY_ID = 0; + int PROJECT_ID = 1; + int NAME = 2; + int CURRENT_STATE = 3; + int ESTIMATE = 4; + int STORY_TYPE = 5; + } + public static interface DetailStoriesQuery{ + String[] PROJECTION = { + Stories.STORY_ID, + Stories.PROJECT_ID, + Stories.STORY_TYPE, + Stories.URL, + Stories.ESTIMATE, + Stories.CURRENT_STATE, + Stories.DESCRIPTION, + Stories.NAME, + Stories.REQUESTED_BY, + Stories.OWNED_BY, + Stories.CREATED_AT, + Stories.ACCEPTED_AT, + Stories.LABELS, + }; + int STORY_ID = 0; + int PROJECT_ID = 1; + int STORY_TYPE = 2; + int URL = 3; + int ESTIMATE = 4; + int CURRENT_STATE = 5; + int DESCRIPTION = 6; + int NAME = 7; + int REQUESTED_BY = 8; + int OWNED_BY = 9; + int CREATED_AT = 10; + int ACCEPTED_AT = 11; + int LABELS = 12; + //TODO: add full story query + } + +} diff --git a/src/com/loganlinn/pivotaltracker/StoryEntry.java b/src/com/loganlinn/pivotaltracker/StoryEntry.java new file mode 100644 index 0000000..ca74eac --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/StoryEntry.java @@ -0,0 +1,196 @@ +package com.loganlinn.pivotaltracker; + +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.END_TAG; +import static org.xmlpull.v1.XmlPullParser.START_TAG; +import static org.xmlpull.v1.XmlPullParser.TEXT; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.database.Cursor; +import android.os.Bundle; +import android.util.Log; + +import com.loganlinn.pivotaltracker.Story.DefaultStoriesQuery; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.SyncColumns; + +public class StoryEntry extends HashMap { + private static final String TAG = "StoryEntry"; + + public static final String[] FIELDS = { + Stories.STORY_ID, + StoryTags.PROJECT_ID, + StoryTags.STORY_TYPE, + StoryTags.URL, + StoryTags.ESTIMATE, + StoryTags.CURRENT_STATE, + StoryTags.DESCRIPTION, + StoryTags.NAME, + StoryTags.REQUESTED_BY, + StoryTags.OWNED_BY, + StoryTags.CREATED_AT, + StoryTags.ACCEPTED_AT, + StoryTags.LABELS + //TODO: Allow add-in fields for integrations + }; + + public static final String[] SAVEABLE_FIELDS = { + StoryTags.ESTIMATE, + StoryTags.DESCRIPTION, + StoryTags.NAME, + StoryTags.REQUESTED_BY, + StoryTags.OWNED_BY, + StoryTags.LABELS + }; + + public interface StoryTags { + String STORY = "story"; + + String STORY_ID = "id"; + String PROJECT_ID = "project_id"; + String STORY_TYPE = "story_type"; + String URL = "url"; + String ESTIMATE = "estimate"; + String CURRENT_STATE = "current_state"; + String DESCRIPTION = "description"; + String NAME = "name"; + String REQUESTED_BY = "requested_by"; + String OWNED_BY = "owned_by"; + String CREATED_AT = "created_at"; + String ACCEPTED_AT = "accepted_at"; + String LABELS = "labels"; + String ATTACHMENTS = "attachments"; + } + + public StoryEntry(){ + for(String f : FIELDS){ + put(f,null); + } + } + + public void sanitizeNullValues(){ + for(String k: keySet()){ + if(get(k) == null){ + put(k,""); + } + } + } + public static StoryEntry fromCursor(Cursor cursor){ + return fromCursor(cursor, DefaultStoriesQuery.PROJECTION); + } + + + public static StoryEntry fromCursor(Cursor cursor, String[] projection){ + final StoryEntry entry = new StoryEntry(); + final int len = projection.length; + + for(int i = 0; i < len; i++){ + entry.put(projection[i], cursor.getString(i)); + } + + return entry; + } + + public static StoryEntry fromParser(XmlPullParser parser) + throws XmlPullParserException, IOException { + final int depth = parser.getDepth(); + final StoryEntry entry = new StoryEntry(); + + String tag = null; + int type; + + while (((type = parser.next()) != END_TAG || parser.getDepth() > depth) + && type != END_DOCUMENT) { + if (type == START_TAG) { + tag = parser.getName(); + } else if (type == END_TAG) { + tag = null; + } else if (type == TEXT) { + if(tag != null){ + String text = parser.getText(); + if(StoryTags.STORY_ID.equals(tag)){ + entry.put(Stories.STORY_ID, text);//store as story_id not id + }else if(entry.containsKey(tag)){ + entry.put(tag, text); + } + } + } + } + + entry.put(SyncColumns.UPDATED, String.valueOf(System.currentTimeMillis())); + + return entry; + } + + public static StoryEntry fromBundle(Bundle data, String[] projection) { + final StoryEntry entry = new StoryEntry(); + for(String f: projection){ + if(Stories.STORY_ID.equals(f) || Stories.PROJECT_ID.equals(f)){ + final Object storyId = data.get(f); + + if(storyId instanceof String){ + entry.put(f, (String) storyId); + }else{ + entry.put(f, String.valueOf(storyId)); + } + }else{ + entry.put(f, data.getString(f)); + } + } + return entry; + } + + public static StoryEntry fromBundle(Bundle data){ + return fromBundle(data, Story.DefaultStoriesQuery.PROJECTION); + } + + public Bundle toBundle() { + final Bundle bundle = new Bundle(); + for(String f: FIELDS){ + bundle.putString(f, get(f)); + } + return bundle; + } + + public String toXml(){ + final StringBuilder builder = new StringBuilder(); + Log.i(TAG, this.toString()); + + builder.append(""); + for(String k: SAVEABLE_FIELDS){ + final String v = get(k); + if(v != null && !v.equals("")){ + builder.append("<"+k+">"); + builder.append(v); + builder.append(""); + } + } + builder.append(""); + Log.i(TAG, builder.toString()); + + return builder.toString(); + } + + public String toString(){ + final StringBuilder builder = new StringBuilder(); + builder.append("{"); + + for(String k : keySet()){ + builder.append(k+":"+get(k)); + builder.append(", "); + } + + builder.append("}"); + + return builder.toString(); + } +} + diff --git a/src/com/loganlinn/pivotaltracker/xml/.svn/all-wcprops b/src/com/loganlinn/pivotaltracker/xml/.svn/all-wcprops new file mode 100644 index 0000000..921168f --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/xml/.svn/all-wcprops @@ -0,0 +1,5 @@ +K 25 +svn:wc:ra_dav:version-url +V 105 +/svn/!svn/ver/2028/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker/xml +END diff --git a/src/com/loganlinn/pivotaltracker/xml/.svn/entries b/src/com/loganlinn/pivotaltracker/xml/.svn/entries new file mode 100644 index 0000000..98d3ec3 --- /dev/null +++ b/src/com/loganlinn/pivotaltracker/xml/.svn/entries @@ -0,0 +1,52 @@ +10 + +dir +2086 +https://vtnetapps.googlecode.com/svn/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker/xml +https://vtnetapps.googlecode.com/svn + + + +2010-11-26T19:31:41.563373Z +2028 +loganlinn + + + + + + + + + + + + + + +13dd798f-c9a9-0d3a-1410-a4e436c6fec4 + +XMLTokenHandler.java +file +2587 + + + + + + + + + + + + + + + + + + + +deleted + diff --git a/src/com/loganlinn/pivotaltrackie/.svn/all-wcprops b/src/com/loganlinn/pivotaltrackie/.svn/all-wcprops new file mode 100644 index 0000000..7f874f6 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/.svn/all-wcprops @@ -0,0 +1,5 @@ +K 25 +svn:wc:ra_dav:version-url +V 101 +/svn/!svn/ver/2029/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie +END diff --git a/src/com/loganlinn/pivotaltrackie/.svn/dir-prop-base b/src/com/loganlinn/pivotaltrackie/.svn/dir-prop-base new file mode 100644 index 0000000..95b2379 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/.svn/dir-prop-base @@ -0,0 +1,6 @@ +K 10 +svn:ignore +V 6 +.git* + +END diff --git a/src/com/loganlinn/pivotaltrackie/.svn/entries b/src/com/loganlinn/pivotaltrackie/.svn/entries new file mode 100644 index 0000000..16c32f3 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/.svn/entries @@ -0,0 +1,63 @@ +10 + +dir +2086 +https://vtnetapps.googlecode.com/svn/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie +https://vtnetapps.googlecode.com/svn + + + +2010-11-26T19:33:38.466730Z +2029 +loganlinn +has-props + + + + + + + + + + + + + +13dd798f-c9a9-0d3a-1410-a4e436c6fec4 + +provider +dir + +service +dir + +io +dir + +ui +dir + +util +dir + + + + + + + + + + + + + + + + + + +https://vtnetapps.googlecode.com/svn/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltracker/util +2028 + diff --git a/src/com/loganlinn/pivotaltrackie/io/.svn/all-wcprops b/src/com/loganlinn/pivotaltrackie/io/.svn/all-wcprops new file mode 100644 index 0000000..f5cf49f --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/.svn/all-wcprops @@ -0,0 +1,47 @@ +K 25 +svn:wc:ra_dav:version-url +V 104 +/svn/!svn/ver/2029/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/io +END +RemoteStoriesHandler.java +K 25 +svn:wc:ra_dav:version-url +V 130 +/svn/!svn/ver/2626/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/io/RemoteStoriesHandler.java +END +RemoteProjectsHandler.java +K 25 +svn:wc:ra_dav:version-url +V 131 +/svn/!svn/ver/2389/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/io/RemoteProjectsHandler.java +END +LocalExecutor.java +K 25 +svn:wc:ra_dav:version-url +V 123 +/svn/!svn/ver/2029/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/io/LocalExecutor.java +END +RemoteExecutor.java +K 25 +svn:wc:ra_dav:version-url +V 124 +/svn/!svn/ver/2626/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/io/RemoteExecutor.java +END +RemoteIterationsHandler.java +K 25 +svn:wc:ra_dav:version-url +V 133 +/svn/!svn/ver/2626/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/io/RemoteIterationsHandler.java +END +SaveStoryHandler.java +K 25 +svn:wc:ra_dav:version-url +V 126 +/svn/!svn/ver/2389/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/io/SaveStoryHandler.java +END +XmlHandler.java +K 25 +svn:wc:ra_dav:version-url +V 120 +/svn/!svn/ver/2587/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/io/XmlHandler.java +END diff --git a/src/com/loganlinn/pivotaltrackie/io/.svn/entries b/src/com/loganlinn/pivotaltrackie/io/.svn/entries new file mode 100644 index 0000000..4cced77 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/.svn/entries @@ -0,0 +1,266 @@ +10 + +dir +2086 +https://vtnetapps.googlecode.com/svn/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/io +https://vtnetapps.googlecode.com/svn + + + +2010-11-26T19:33:38.466730Z +2029 +loganlinn + + + + + + + + + + + + + + +13dd798f-c9a9-0d3a-1410-a4e436c6fec4 + +RemoteStoriesHandler.java +file +2626 + + + +2010-12-08T14:16:36.000000Z +2f47555cd407aa23c25e7340d56d7a9c +2010-12-10T17:14:25.002194Z +2626 +loganlinn + + + + + + + + + + + + + + + + + + + + + +2567 + +RemoteProjectsHandler.java +file +2389 + + + +2010-12-03T03:47:41.000000Z +a23082e9e13f83c582f4de2e7cc15d0b +2010-12-03T04:10:27.338840Z +2389 +loganlinn + + + + + + + + + + + + + + + + + + + + + +4579 + +LocalExecutor.java +file + + + + +2010-11-26T19:33:03.000000Z +decdd11deaa4d4735fee993853188125 +2010-11-26T19:33:38.466730Z +2029 +loganlinn + + + + + + + + + + + + + + + + + + + + + +2363 + +RemoteExecutor.java +file +2626 + + + +2010-12-08T14:36:22.000000Z +2c45cab577e506d21f0e41ff359925fb +2010-12-10T17:14:25.002194Z +2626 +loganlinn + + + + + + + + + + + + + + + + + + + + + +4093 + +RemoteIterationsHandler.java +file +2626 + + + +2010-12-08T14:44:42.000000Z +5127fda977d2e8e969e13b09d0b6fe38 +2010-12-10T17:14:25.002194Z +2626 +loganlinn + + + + + + + + + + + + + + + + + + + + + +841 + +SaveStoryHandler.java +file +2389 + + + +2010-12-02T23:13:43.000000Z +66419a69050d0b0e087665084e11759d +2010-12-03T04:10:27.338840Z +2389 +loganlinn + + + + + + + + + + + + + + + + + + + + + +78 + +XmlHandler.java +file +2587 + + + +2010-12-06T06:32:05.000000Z +333264e549afa9c09ddafc5bc5e23834 +2010-12-07T22:40:29.935924Z +2587 +loganlinn + + + + + + + + + + + + + + + + + + + + + +4179 + diff --git a/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/LocalExecutor.java.svn-base b/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/LocalExecutor.java.svn-base new file mode 100644 index 0000000..b00edd6 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/LocalExecutor.java.svn-base @@ -0,0 +1,69 @@ +/* + * Copyright 2010 Google Inc. + * + * 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 com.loganlinn.pivotaltrackie.io; + +import java.io.IOException; +import java.io.InputStream; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; + +import com.loganlinn.pivotaltrackie.io.XmlHandler.HandlerException; +import com.loganlinn.pivotaltrackie.util.ParserUtils; + +/** + * Opens a local {@link Resources#getXml(int)} and passes the resulting + * {@link XmlPullParser} to the given {@link XmlHandler}. + */ +public class LocalExecutor { + private Resources mRes; + private ContentResolver mResolver; + + public LocalExecutor(Resources res, ContentResolver resolver) { + mRes = res; + mResolver = resolver; + } + + public void execute(Context context, String assetName, XmlHandler handler) + throws HandlerException { + try { + final InputStream input = context.getAssets().open(assetName); + final XmlPullParser parser = ParserUtils.newPullParser(input); + handler.parseAndApply(parser, mResolver); + } catch (HandlerException e) { + throw e; + } catch (XmlPullParserException e) { + throw new HandlerException("Problem parsing local asset: " + assetName, e); + } catch (IOException e) { + throw new HandlerException("Problem parsing local asset: " + assetName, e); + } + } + + public void execute(int resId, XmlHandler handler) throws HandlerException { + final XmlResourceParser parser = mRes.getXml(resId); + try { + handler.parseAndApply(parser, mResolver); + } finally { + parser.close(); + } + } +} + diff --git a/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/RemoteExecutor.java.svn-base b/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/RemoteExecutor.java.svn-base new file mode 100644 index 0000000..03ee325 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/RemoteExecutor.java.svn-base @@ -0,0 +1,132 @@ +/* + * Copyright 2010 Google Inc. + * + * 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 com.loganlinn.pivotaltrackie.io; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.ConnectivityManager; +import android.util.Log; + +import com.loganlinn.pivotaltracker.PivotalTracker; +import com.loganlinn.pivotaltrackie.io.XmlHandler.HandlerException; +import com.loganlinn.pivotaltrackie.util.ParserUtils; + +/** + * Executes an {@link HttpUriRequest} and passes the result as an + * {@link XmlPullParser} to the given {@link XmlHandler}. + */ +public class RemoteExecutor { + private static final String TAG = "RemoteExecutor"; + + private final HttpClient mHttpClient; + private final ContentResolver mResolver; + + public RemoteExecutor(HttpClient httpClient, ContentResolver resolver) { + mHttpClient = httpClient; + mResolver = resolver; + } + + /** + * Execute a {@link HttpGet} request, passing a valid response through + * {@link XmlHandler#parseAndApply(XmlPullParser, ContentResolver)}. + * @throws ResponseException + */ + public void executeGet(String url, XmlHandler handler) + throws HandlerException { + + execute(PivotalTracker.getGetRequest(url), handler); // get a + // HttpRequest + // object with + // the token in + // header + } + + public void executePut(String url, XmlHandler handler) + throws HandlerException { + + execute(PivotalTracker.getPutRequest(url), handler); + } + + public void executePost(String url, XmlHandler handler) + throws HandlerException { + + execute(PivotalTracker.getPostRequest(url), handler); + } + + /** + * Execute this {@link HttpUriRequest}, passing a valid response through + * {@link XmlHandler#parseAndApply(XmlPullParser, ContentResolver)}. + * @throws ResponseException + */ + public void execute(HttpUriRequest request, XmlHandler handler) + throws HandlerException { + Log.i(TAG, "Executing Remote "+request.getMethod()+" Request: " + request.getURI()); + + try { + // Execute the HttpRequest + final HttpResponse resp = mHttpClient.execute(request); + final int status = resp.getStatusLine().getStatusCode(); + Log.i(TAG, "Request Response: " + status); + + final boolean notFound = (status != HttpStatus.SC_NOT_FOUND); + + if (status != HttpStatus.SC_OK && !notFound) { + throw new HandlerException("Unexpected server response " + + resp.getStatusLine() + " for " + + request.getRequestLine()); + } + + + + if(notFound){ + final InputStream input = resp.getEntity().getContent(); + // Parse the input stream using the handler + try { + + final XmlPullParser parser = ParserUtils.newPullParser(input); + handler.parseAndApply(parser, mResolver); + + } catch (XmlPullParserException e) { + throw new HandlerException("Malformed response for " + + request.getRequestLine(), e); + } finally { + if (input != null) + input.close(); + } + }else if(handler.getContentUri() != null){ + Log.i(TAG, "Deleting "+handler.getContentUri().toString()); + mResolver.delete(handler.getContentUri(), null, null); + } + } catch (HandlerException e) { + throw e; + } catch (IOException e) { + throw new HandlerException("Problem reading remote response for " + + request.getRequestLine(), e); + } + } + +} \ No newline at end of file diff --git a/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/RemoteIterationsHandler.java.svn-base b/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/RemoteIterationsHandler.java.svn-base new file mode 100644 index 0000000..c56ed47 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/RemoteIterationsHandler.java.svn-base @@ -0,0 +1,31 @@ +package com.loganlinn.pivotaltrackie.io; + +import java.io.IOException; +import java.util.ArrayList; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract; + +import android.content.ContentProviderOperation; +import android.content.ContentResolver; + +public class RemoteIterationsHandler extends XmlHandler { + private static final String TAG = "RemoteIterationsHandler"; + + private RemoteExecutor mExecutor; + + public RemoteIterationsHandler(RemoteExecutor executor) { + super(ProjectContract.CONTENT_AUTHORITY); + mExecutor = executor; + } + @Override + public ArrayList parse(XmlPullParser parser, + ContentResolver resolver) throws XmlPullParserException, + IOException { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/RemoteProjectsHandler.java.svn-base b/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/RemoteProjectsHandler.java.svn-base new file mode 100644 index 0000000..672f367 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/RemoteProjectsHandler.java.svn-base @@ -0,0 +1,102 @@ +package com.loganlinn.pivotaltrackie.io; + +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.START_TAG; + +import java.io.IOException; +import java.util.ArrayList; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.net.Uri; +import android.util.Log; + +import com.loganlinn.pivotaltracker.ProjectEntry; +import com.loganlinn.pivotaltracker.ProjectEntry.ProjectTags; +import com.loganlinn.pivotaltrackie.provider.ProjectContract; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Projects; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.SyncColumns; +import com.loganlinn.pivotaltrackie.util.Lists; +import com.loganlinn.pivotaltrackie.util.ParserUtils; + +public class RemoteProjectsHandler extends XmlHandler { + private static final String TAG = "RemoteProjectsHandler"; + + private RemoteExecutor mExecutor; + + public RemoteProjectsHandler(RemoteExecutor executor) { + super(ProjectContract.CONTENT_AUTHORITY); + mExecutor = executor; + } + + @Override + public ArrayList parse(XmlPullParser parser, + ContentResolver resolver) throws XmlPullParserException, + IOException { + final ArrayList batch = Lists.newArrayList(); + + int type; + + while ((type = parser.next()) != END_DOCUMENT) { + if (type == START_TAG + && ProjectEntry.ProjectTags.PROJECT + .equals(parser.getName())) { + final ProjectEntry entry = ProjectEntry.fromParser(parser); + Log.d(TAG, "found project " + entry.toString()); + final String projectId = entry.get(ProjectTags.ID); + final Uri projectUri = Projects.buildProjectUri(projectId); + + final long localUpdated = ParserUtils.queryItemUpdated(projectUri, resolver); + final long serverUpdated = entry.getUpdated(); + Log.i(TAG, "found localUpdated=" + localUpdated + ", server=" + serverUpdated); + //if (localUpdated >= serverUpdated) continue; + Log.i(TAG, "Updating project with id= "+projectId); + batch.add(ContentProviderOperation.newDelete(projectUri).build()); + + final ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(Projects.CONTENT_URI); + builder.withValue(SyncColumns.UPDATED, serverUpdated); + builder.withValue(Projects.PROJECT_ID, entry.get(ProjectTags.ID)); + builder.withValue(Projects.NAME, entry.get(ProjectTags.NAME)); + builder.withValue(Projects.ITERATION_LENGTH, entry.get(ProjectTags.ITERATION_LENGTH)); + builder.withValue(Projects.WEEK_START_DAY, entry.get(ProjectTags.WEEK_START_DAY)); + builder.withValue(Projects.POINT_SCALE, entry.get(ProjectTags.POINT_SCALE)); + builder.withValue(Projects.ACCOUNT, entry.get(ProjectTags.ACCOUNT)); + builder.withValue(Projects.VELOCITY_SCHEME, entry.get(ProjectTags.VELOCITY_SCHEME)); + builder.withValue(Projects.CURRENT_VELOCITY, entry.get(ProjectTags.CURRENT_VELOCITY)); + builder.withValue(Projects.INITIAL_VELOCITY, entry.get(ProjectTags.INITIAL_VELOCITY)); + builder.withValue(Projects.NUMBER_OF_DONE_ITERATIONS_TO_SHOW, entry.get(ProjectTags.NUMBER_OF_DONE_ITERATIONS_TO_SHOW)); + builder.withValue(Projects.ALLOW_ATTACHMENTS, entry.get(ProjectTags.ALLOW_ATTACHMENTS)); + builder.withValue(Projects.PUBLIC, entry.get(ProjectTags.USE_HTTPS)); + builder.withValue(Projects.USE_HTTPS, entry.get(ProjectTags.USE_HTTPS)); + builder.withValue(Projects.BUGS_AND_CHORES_ARE_ESTIMATABLE, entry.get(ProjectTags.BUGS_AND_CHORES_ARE_ESTIMATABLE)); + builder.withValue(Projects.COMMIT_MODE, entry.get(ProjectTags.COMMIT_MODE)); + builder.withValue(Projects.LAST_ACTIVITY_AT, entry.get(ProjectTags.LAST_ACTIVITY_AT)); + builder.withValue(Projects.LABELS, entry.get(ProjectTags.LABELS)); + batch.add(builder.build()); + } + } + + return batch; + } + +// private ContentValues queryProjectDetails(Uri uri, ContentResolver resolver){ +// final ContentValues values = new ContentValues(); +// final Cursor cursor = resolver.query(uri, ProjectsQuery.PROJECTION, null, null, null); +// try { +// if (cursor.moveToFirst()) { +// values.put(SyncColumns.UPDATED, cursor.getLong(Projects.UPDATED)); +// values.put(Vendors.STARRED, cursor.getInt(VendorsQuery.STARRED)); +// } else { +// values.put(SyncColumns.UPDATED, ScheduleContract.UPDATED_NEVER); +// } +// } finally { +// cursor.close(); +// } +// return values; +// } + + +} diff --git a/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/RemoteStoriesHandler.java.svn-base b/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/RemoteStoriesHandler.java.svn-base new file mode 100644 index 0000000..1fe073f --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/RemoteStoriesHandler.java.svn-base @@ -0,0 +1,81 @@ +package com.loganlinn.pivotaltrackie.io; + +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.START_TAG; + +import java.io.IOException; +import java.util.ArrayList; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.net.Uri; +import android.util.Log; + +import com.loganlinn.pivotaltracker.StoryEntry; +import com.loganlinn.pivotaltracker.StoryEntry.StoryTags; +import com.loganlinn.pivotaltrackie.provider.ProjectContract; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.SyncColumns; +import com.loganlinn.pivotaltrackie.util.Lists; +import com.loganlinn.pivotaltrackie.util.ParserUtils; + +public class RemoteStoriesHandler extends XmlHandler { + private static final String TAG = "RemoteStoriesHandler"; + + private RemoteExecutor mExecutor; + public Uri mContentUri = null; + + public RemoteStoriesHandler(RemoteExecutor executor) { + super(ProjectContract.CONTENT_AUTHORITY); + mExecutor = executor; + } + + @Override + public ArrayList parse(XmlPullParser parser, + ContentResolver resolver) throws XmlPullParserException, + IOException { + final ArrayList batch = Lists.newArrayList(); + + int type; + while ((type = parser.next()) != END_DOCUMENT) { + if (type == START_TAG + && StoryEntry.StoryTags.STORY.equals(parser.getName())) { + final StoryEntry entry = StoryEntry.fromParser(parser); + + final String storyId = entry.get(Stories.STORY_ID); + // final String projectId = entry.get(StoryTags.PROJECT_ID); + + final Uri storyUri = Stories.buildStoryUri(storyId); + final long localUpdated = ParserUtils.queryItemUpdated( + storyUri, resolver); + + // TODO: Determine if story needs to be updated + + if (localUpdated > 0) { + Log.i(TAG, "Updating story with id= " + storyId); + batch.add(ContentProviderOperation.newDelete(storyUri) + .build()); + } else { + Log.i(TAG, "Inserting story with id= " + storyId); + } + + final ContentProviderOperation.Builder builder = ContentProviderOperation + .newInsert(Stories.CONTENT_URI); + + builder.withValue(SyncColumns.UPDATED, entry + .get(SyncColumns.UPDATED)); + + for (String f : entry.keySet()) { + builder.withValue(f, entry.get(f)); + } + + batch.add(builder.build()); + } + } + //TODO: delete removed stories from local store + return batch; + } +} diff --git a/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/SaveStoryHandler.java.svn-base b/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/SaveStoryHandler.java.svn-base new file mode 100644 index 0000000..ba20ac3 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/SaveStoryHandler.java.svn-base @@ -0,0 +1,5 @@ +package com.loganlinn.pivotaltrackie.io; + +public class SaveStoryHandler { + +} diff --git a/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/XmlHandler.java.svn-base b/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/XmlHandler.java.svn-base new file mode 100644 index 0000000..3a6aae5 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/.svn/text-base/XmlHandler.java.svn-base @@ -0,0 +1,115 @@ +/* + * Copyright 2010 Google Inc. + * + * 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 com.loganlinn.pivotaltrackie.io; + +import java.io.IOException; +import java.util.ArrayList; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.OperationApplicationException; +import android.net.Uri; +import android.os.RemoteException; + +/** + * Abstract class that handles reading and parsing an {@link XmlPullParser} into + * a set of {@link ContentProviderOperation}. It catches recoverable network + * exceptions and rethrows them as {@link HandlerException}. Any local + * {@link ContentProvider} exceptions are considered unrecoverable. + *

+ * This class is only designed to handle simple one-way synchronization. + */ +public abstract class XmlHandler { + private final String mAuthority; + private Uri mContentUri; //story or project uri + public XmlHandler(String authority) { + mAuthority = authority; + } + + /** + * Parse the given {@link XmlPullParser}, turning into a series of + * {@link ContentProviderOperation} that are immediately applied using the + * given {@link ContentResolver}. + */ + public void parseAndApply(XmlPullParser parser, ContentResolver resolver) + throws HandlerException { + try { + final ArrayList batch = parse(parser, resolver); + resolver.applyBatch(mAuthority, batch); + + } catch (HandlerException e) { + throw e; + } catch (XmlPullParserException e) { + throw new HandlerException("Problem parsing XML response", e); + } catch (IOException e) { + throw new HandlerException("Problem reading response", e); + } catch (RemoteException e) { + // Failed binder transactions aren't recoverable + throw new RuntimeException("Problem applying batch operation", e); + } catch (OperationApplicationException e) { + // Failures like constraint violation aren't recoverable + // TODO: write unit tests to exercise full provider + // TODO: consider catching version checking asserts here, and then + // wrapping around to retry parsing again. + throw new RuntimeException("Problem applying batch operation", e); + } + } + + /** + * Parse the given {@link XmlPullParser}, returning a set of + * {@link ContentProviderOperation} that will bring the + * {@link ContentProvider} into sync with the parsed data. + */ + public abstract ArrayList parse(XmlPullParser parser, + ContentResolver resolver) throws XmlPullParserException, IOException; + + /** + * General {@link IOException} that indicates a problem occured while + * parsing or applying an {@link XmlPullParser}. + */ + public static class HandlerException extends IOException { + public HandlerException(String message) { + super(message); + } + + public HandlerException(String message, Throwable cause) { + super(message); + initCause(cause); + } + + @Override + public String toString() { + if (getCause() != null) { + return getLocalizedMessage() + ": " + getCause(); + } else { + return getLocalizedMessage(); + } + } + } + + public void setContentUri(Uri contentUri) { + mContentUri = contentUri; + + } + + protected Uri getContentUri(){ + return mContentUri; + } +} \ No newline at end of file diff --git a/src/com/loganlinn/pivotaltrackie/io/LocalExecutor.java b/src/com/loganlinn/pivotaltrackie/io/LocalExecutor.java new file mode 100644 index 0000000..b00edd6 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/LocalExecutor.java @@ -0,0 +1,69 @@ +/* + * Copyright 2010 Google Inc. + * + * 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 com.loganlinn.pivotaltrackie.io; + +import java.io.IOException; +import java.io.InputStream; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.Resources; +import android.content.res.XmlResourceParser; + +import com.loganlinn.pivotaltrackie.io.XmlHandler.HandlerException; +import com.loganlinn.pivotaltrackie.util.ParserUtils; + +/** + * Opens a local {@link Resources#getXml(int)} and passes the resulting + * {@link XmlPullParser} to the given {@link XmlHandler}. + */ +public class LocalExecutor { + private Resources mRes; + private ContentResolver mResolver; + + public LocalExecutor(Resources res, ContentResolver resolver) { + mRes = res; + mResolver = resolver; + } + + public void execute(Context context, String assetName, XmlHandler handler) + throws HandlerException { + try { + final InputStream input = context.getAssets().open(assetName); + final XmlPullParser parser = ParserUtils.newPullParser(input); + handler.parseAndApply(parser, mResolver); + } catch (HandlerException e) { + throw e; + } catch (XmlPullParserException e) { + throw new HandlerException("Problem parsing local asset: " + assetName, e); + } catch (IOException e) { + throw new HandlerException("Problem parsing local asset: " + assetName, e); + } + } + + public void execute(int resId, XmlHandler handler) throws HandlerException { + final XmlResourceParser parser = mRes.getXml(resId); + try { + handler.parseAndApply(parser, mResolver); + } finally { + parser.close(); + } + } +} + diff --git a/src/com/loganlinn/pivotaltrackie/io/RemoteExecutor.java b/src/com/loganlinn/pivotaltrackie/io/RemoteExecutor.java new file mode 100644 index 0000000..03ee325 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/RemoteExecutor.java @@ -0,0 +1,132 @@ +/* + * Copyright 2010 Google Inc. + * + * 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 com.loganlinn.pivotaltrackie.io; + +import java.io.IOException; +import java.io.InputStream; + +import org.apache.http.HttpResponse; +import org.apache.http.HttpStatus; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpUriRequest; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.ContentResolver; +import android.content.Context; +import android.net.ConnectivityManager; +import android.util.Log; + +import com.loganlinn.pivotaltracker.PivotalTracker; +import com.loganlinn.pivotaltrackie.io.XmlHandler.HandlerException; +import com.loganlinn.pivotaltrackie.util.ParserUtils; + +/** + * Executes an {@link HttpUriRequest} and passes the result as an + * {@link XmlPullParser} to the given {@link XmlHandler}. + */ +public class RemoteExecutor { + private static final String TAG = "RemoteExecutor"; + + private final HttpClient mHttpClient; + private final ContentResolver mResolver; + + public RemoteExecutor(HttpClient httpClient, ContentResolver resolver) { + mHttpClient = httpClient; + mResolver = resolver; + } + + /** + * Execute a {@link HttpGet} request, passing a valid response through + * {@link XmlHandler#parseAndApply(XmlPullParser, ContentResolver)}. + * @throws ResponseException + */ + public void executeGet(String url, XmlHandler handler) + throws HandlerException { + + execute(PivotalTracker.getGetRequest(url), handler); // get a + // HttpRequest + // object with + // the token in + // header + } + + public void executePut(String url, XmlHandler handler) + throws HandlerException { + + execute(PivotalTracker.getPutRequest(url), handler); + } + + public void executePost(String url, XmlHandler handler) + throws HandlerException { + + execute(PivotalTracker.getPostRequest(url), handler); + } + + /** + * Execute this {@link HttpUriRequest}, passing a valid response through + * {@link XmlHandler#parseAndApply(XmlPullParser, ContentResolver)}. + * @throws ResponseException + */ + public void execute(HttpUriRequest request, XmlHandler handler) + throws HandlerException { + Log.i(TAG, "Executing Remote "+request.getMethod()+" Request: " + request.getURI()); + + try { + // Execute the HttpRequest + final HttpResponse resp = mHttpClient.execute(request); + final int status = resp.getStatusLine().getStatusCode(); + Log.i(TAG, "Request Response: " + status); + + final boolean notFound = (status != HttpStatus.SC_NOT_FOUND); + + if (status != HttpStatus.SC_OK && !notFound) { + throw new HandlerException("Unexpected server response " + + resp.getStatusLine() + " for " + + request.getRequestLine()); + } + + + + if(notFound){ + final InputStream input = resp.getEntity().getContent(); + // Parse the input stream using the handler + try { + + final XmlPullParser parser = ParserUtils.newPullParser(input); + handler.parseAndApply(parser, mResolver); + + } catch (XmlPullParserException e) { + throw new HandlerException("Malformed response for " + + request.getRequestLine(), e); + } finally { + if (input != null) + input.close(); + } + }else if(handler.getContentUri() != null){ + Log.i(TAG, "Deleting "+handler.getContentUri().toString()); + mResolver.delete(handler.getContentUri(), null, null); + } + } catch (HandlerException e) { + throw e; + } catch (IOException e) { + throw new HandlerException("Problem reading remote response for " + + request.getRequestLine(), e); + } + } + +} \ No newline at end of file diff --git a/src/com/loganlinn/pivotaltrackie/io/RemoteIterationsHandler.java b/src/com/loganlinn/pivotaltrackie/io/RemoteIterationsHandler.java new file mode 100644 index 0000000..c56ed47 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/RemoteIterationsHandler.java @@ -0,0 +1,31 @@ +package com.loganlinn.pivotaltrackie.io; + +import java.io.IOException; +import java.util.ArrayList; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract; + +import android.content.ContentProviderOperation; +import android.content.ContentResolver; + +public class RemoteIterationsHandler extends XmlHandler { + private static final String TAG = "RemoteIterationsHandler"; + + private RemoteExecutor mExecutor; + + public RemoteIterationsHandler(RemoteExecutor executor) { + super(ProjectContract.CONTENT_AUTHORITY); + mExecutor = executor; + } + @Override + public ArrayList parse(XmlPullParser parser, + ContentResolver resolver) throws XmlPullParserException, + IOException { + // TODO Auto-generated method stub + return null; + } + +} diff --git a/src/com/loganlinn/pivotaltrackie/io/RemoteProjectsHandler.java b/src/com/loganlinn/pivotaltrackie/io/RemoteProjectsHandler.java new file mode 100644 index 0000000..672f367 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/RemoteProjectsHandler.java @@ -0,0 +1,102 @@ +package com.loganlinn.pivotaltrackie.io; + +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.START_TAG; + +import java.io.IOException; +import java.util.ArrayList; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.net.Uri; +import android.util.Log; + +import com.loganlinn.pivotaltracker.ProjectEntry; +import com.loganlinn.pivotaltracker.ProjectEntry.ProjectTags; +import com.loganlinn.pivotaltrackie.provider.ProjectContract; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Projects; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.SyncColumns; +import com.loganlinn.pivotaltrackie.util.Lists; +import com.loganlinn.pivotaltrackie.util.ParserUtils; + +public class RemoteProjectsHandler extends XmlHandler { + private static final String TAG = "RemoteProjectsHandler"; + + private RemoteExecutor mExecutor; + + public RemoteProjectsHandler(RemoteExecutor executor) { + super(ProjectContract.CONTENT_AUTHORITY); + mExecutor = executor; + } + + @Override + public ArrayList parse(XmlPullParser parser, + ContentResolver resolver) throws XmlPullParserException, + IOException { + final ArrayList batch = Lists.newArrayList(); + + int type; + + while ((type = parser.next()) != END_DOCUMENT) { + if (type == START_TAG + && ProjectEntry.ProjectTags.PROJECT + .equals(parser.getName())) { + final ProjectEntry entry = ProjectEntry.fromParser(parser); + Log.d(TAG, "found project " + entry.toString()); + final String projectId = entry.get(ProjectTags.ID); + final Uri projectUri = Projects.buildProjectUri(projectId); + + final long localUpdated = ParserUtils.queryItemUpdated(projectUri, resolver); + final long serverUpdated = entry.getUpdated(); + Log.i(TAG, "found localUpdated=" + localUpdated + ", server=" + serverUpdated); + //if (localUpdated >= serverUpdated) continue; + Log.i(TAG, "Updating project with id= "+projectId); + batch.add(ContentProviderOperation.newDelete(projectUri).build()); + + final ContentProviderOperation.Builder builder = ContentProviderOperation.newInsert(Projects.CONTENT_URI); + builder.withValue(SyncColumns.UPDATED, serverUpdated); + builder.withValue(Projects.PROJECT_ID, entry.get(ProjectTags.ID)); + builder.withValue(Projects.NAME, entry.get(ProjectTags.NAME)); + builder.withValue(Projects.ITERATION_LENGTH, entry.get(ProjectTags.ITERATION_LENGTH)); + builder.withValue(Projects.WEEK_START_DAY, entry.get(ProjectTags.WEEK_START_DAY)); + builder.withValue(Projects.POINT_SCALE, entry.get(ProjectTags.POINT_SCALE)); + builder.withValue(Projects.ACCOUNT, entry.get(ProjectTags.ACCOUNT)); + builder.withValue(Projects.VELOCITY_SCHEME, entry.get(ProjectTags.VELOCITY_SCHEME)); + builder.withValue(Projects.CURRENT_VELOCITY, entry.get(ProjectTags.CURRENT_VELOCITY)); + builder.withValue(Projects.INITIAL_VELOCITY, entry.get(ProjectTags.INITIAL_VELOCITY)); + builder.withValue(Projects.NUMBER_OF_DONE_ITERATIONS_TO_SHOW, entry.get(ProjectTags.NUMBER_OF_DONE_ITERATIONS_TO_SHOW)); + builder.withValue(Projects.ALLOW_ATTACHMENTS, entry.get(ProjectTags.ALLOW_ATTACHMENTS)); + builder.withValue(Projects.PUBLIC, entry.get(ProjectTags.USE_HTTPS)); + builder.withValue(Projects.USE_HTTPS, entry.get(ProjectTags.USE_HTTPS)); + builder.withValue(Projects.BUGS_AND_CHORES_ARE_ESTIMATABLE, entry.get(ProjectTags.BUGS_AND_CHORES_ARE_ESTIMATABLE)); + builder.withValue(Projects.COMMIT_MODE, entry.get(ProjectTags.COMMIT_MODE)); + builder.withValue(Projects.LAST_ACTIVITY_AT, entry.get(ProjectTags.LAST_ACTIVITY_AT)); + builder.withValue(Projects.LABELS, entry.get(ProjectTags.LABELS)); + batch.add(builder.build()); + } + } + + return batch; + } + +// private ContentValues queryProjectDetails(Uri uri, ContentResolver resolver){ +// final ContentValues values = new ContentValues(); +// final Cursor cursor = resolver.query(uri, ProjectsQuery.PROJECTION, null, null, null); +// try { +// if (cursor.moveToFirst()) { +// values.put(SyncColumns.UPDATED, cursor.getLong(Projects.UPDATED)); +// values.put(Vendors.STARRED, cursor.getInt(VendorsQuery.STARRED)); +// } else { +// values.put(SyncColumns.UPDATED, ScheduleContract.UPDATED_NEVER); +// } +// } finally { +// cursor.close(); +// } +// return values; +// } + + +} diff --git a/src/com/loganlinn/pivotaltrackie/io/RemoteStoriesHandler.java b/src/com/loganlinn/pivotaltrackie/io/RemoteStoriesHandler.java new file mode 100644 index 0000000..1fe073f --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/RemoteStoriesHandler.java @@ -0,0 +1,81 @@ +package com.loganlinn.pivotaltrackie.io; + +import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; +import static org.xmlpull.v1.XmlPullParser.START_TAG; + +import java.io.IOException; +import java.util.ArrayList; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.net.Uri; +import android.util.Log; + +import com.loganlinn.pivotaltracker.StoryEntry; +import com.loganlinn.pivotaltracker.StoryEntry.StoryTags; +import com.loganlinn.pivotaltrackie.provider.ProjectContract; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.SyncColumns; +import com.loganlinn.pivotaltrackie.util.Lists; +import com.loganlinn.pivotaltrackie.util.ParserUtils; + +public class RemoteStoriesHandler extends XmlHandler { + private static final String TAG = "RemoteStoriesHandler"; + + private RemoteExecutor mExecutor; + public Uri mContentUri = null; + + public RemoteStoriesHandler(RemoteExecutor executor) { + super(ProjectContract.CONTENT_AUTHORITY); + mExecutor = executor; + } + + @Override + public ArrayList parse(XmlPullParser parser, + ContentResolver resolver) throws XmlPullParserException, + IOException { + final ArrayList batch = Lists.newArrayList(); + + int type; + while ((type = parser.next()) != END_DOCUMENT) { + if (type == START_TAG + && StoryEntry.StoryTags.STORY.equals(parser.getName())) { + final StoryEntry entry = StoryEntry.fromParser(parser); + + final String storyId = entry.get(Stories.STORY_ID); + // final String projectId = entry.get(StoryTags.PROJECT_ID); + + final Uri storyUri = Stories.buildStoryUri(storyId); + final long localUpdated = ParserUtils.queryItemUpdated( + storyUri, resolver); + + // TODO: Determine if story needs to be updated + + if (localUpdated > 0) { + Log.i(TAG, "Updating story with id= " + storyId); + batch.add(ContentProviderOperation.newDelete(storyUri) + .build()); + } else { + Log.i(TAG, "Inserting story with id= " + storyId); + } + + final ContentProviderOperation.Builder builder = ContentProviderOperation + .newInsert(Stories.CONTENT_URI); + + builder.withValue(SyncColumns.UPDATED, entry + .get(SyncColumns.UPDATED)); + + for (String f : entry.keySet()) { + builder.withValue(f, entry.get(f)); + } + + batch.add(builder.build()); + } + } + //TODO: delete removed stories from local store + return batch; + } +} diff --git a/src/com/loganlinn/pivotaltrackie/io/SaveStoryHandler.java b/src/com/loganlinn/pivotaltrackie/io/SaveStoryHandler.java new file mode 100644 index 0000000..ba20ac3 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/SaveStoryHandler.java @@ -0,0 +1,5 @@ +package com.loganlinn.pivotaltrackie.io; + +public class SaveStoryHandler { + +} diff --git a/src/com/loganlinn/pivotaltrackie/io/XmlHandler.java b/src/com/loganlinn/pivotaltrackie/io/XmlHandler.java new file mode 100644 index 0000000..3a6aae5 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/io/XmlHandler.java @@ -0,0 +1,115 @@ +/* + * Copyright 2010 Google Inc. + * + * 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 com.loganlinn.pivotaltrackie.io; + +import java.io.IOException; +import java.util.ArrayList; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.content.ContentProvider; +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.OperationApplicationException; +import android.net.Uri; +import android.os.RemoteException; + +/** + * Abstract class that handles reading and parsing an {@link XmlPullParser} into + * a set of {@link ContentProviderOperation}. It catches recoverable network + * exceptions and rethrows them as {@link HandlerException}. Any local + * {@link ContentProvider} exceptions are considered unrecoverable. + *

+ * This class is only designed to handle simple one-way synchronization. + */ +public abstract class XmlHandler { + private final String mAuthority; + private Uri mContentUri; //story or project uri + public XmlHandler(String authority) { + mAuthority = authority; + } + + /** + * Parse the given {@link XmlPullParser}, turning into a series of + * {@link ContentProviderOperation} that are immediately applied using the + * given {@link ContentResolver}. + */ + public void parseAndApply(XmlPullParser parser, ContentResolver resolver) + throws HandlerException { + try { + final ArrayList batch = parse(parser, resolver); + resolver.applyBatch(mAuthority, batch); + + } catch (HandlerException e) { + throw e; + } catch (XmlPullParserException e) { + throw new HandlerException("Problem parsing XML response", e); + } catch (IOException e) { + throw new HandlerException("Problem reading response", e); + } catch (RemoteException e) { + // Failed binder transactions aren't recoverable + throw new RuntimeException("Problem applying batch operation", e); + } catch (OperationApplicationException e) { + // Failures like constraint violation aren't recoverable + // TODO: write unit tests to exercise full provider + // TODO: consider catching version checking asserts here, and then + // wrapping around to retry parsing again. + throw new RuntimeException("Problem applying batch operation", e); + } + } + + /** + * Parse the given {@link XmlPullParser}, returning a set of + * {@link ContentProviderOperation} that will bring the + * {@link ContentProvider} into sync with the parsed data. + */ + public abstract ArrayList parse(XmlPullParser parser, + ContentResolver resolver) throws XmlPullParserException, IOException; + + /** + * General {@link IOException} that indicates a problem occured while + * parsing or applying an {@link XmlPullParser}. + */ + public static class HandlerException extends IOException { + public HandlerException(String message) { + super(message); + } + + public HandlerException(String message, Throwable cause) { + super(message); + initCause(cause); + } + + @Override + public String toString() { + if (getCause() != null) { + return getLocalizedMessage() + ": " + getCause(); + } else { + return getLocalizedMessage(); + } + } + } + + public void setContentUri(Uri contentUri) { + mContentUri = contentUri; + + } + + protected Uri getContentUri(){ + return mContentUri; + } +} \ No newline at end of file diff --git a/src/com/loganlinn/pivotaltrackie/provider/.svn/all-wcprops b/src/com/loganlinn/pivotaltrackie/provider/.svn/all-wcprops new file mode 100644 index 0000000..b49841e --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/provider/.svn/all-wcprops @@ -0,0 +1,23 @@ +K 25 +svn:wc:ra_dav:version-url +V 110 +/svn/!svn/ver/2029/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/provider +END +ProjectProvider.java +K 25 +svn:wc:ra_dav:version-url +V 131 +/svn/!svn/ver/2587/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/provider/ProjectProvider.java +END +ProjectContract.java +K 25 +svn:wc:ra_dav:version-url +V 131 +/svn/!svn/ver/2587/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/provider/ProjectContract.java +END +ProjectDatabase.java +K 25 +svn:wc:ra_dav:version-url +V 131 +/svn/!svn/ver/2389/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/provider/ProjectDatabase.java +END diff --git a/src/com/loganlinn/pivotaltrackie/provider/.svn/dir-prop-base b/src/com/loganlinn/pivotaltrackie/provider/.svn/dir-prop-base new file mode 100644 index 0000000..95b2379 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/provider/.svn/dir-prop-base @@ -0,0 +1,6 @@ +K 10 +svn:ignore +V 6 +.git* + +END diff --git a/src/com/loganlinn/pivotaltrackie/provider/.svn/entries b/src/com/loganlinn/pivotaltrackie/provider/.svn/entries new file mode 100644 index 0000000..bdae669 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/provider/.svn/entries @@ -0,0 +1,130 @@ +10 + +dir +2086 +https://vtnetapps.googlecode.com/svn/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/provider +https://vtnetapps.googlecode.com/svn + + + +2010-11-26T19:33:38.466730Z +2029 +loganlinn +has-props + + + + + + + + + + + + + +13dd798f-c9a9-0d3a-1410-a4e436c6fec4 + +ProjectProvider.java +file +2587 + + + +2010-12-03T05:04:36.000000Z +3a1856bbdb272abbea29a04b5278d9f7 +2010-12-07T22:40:29.935924Z +2587 +loganlinn + + + + + + + + + + + + + + + + + + + + + +5469 + +ProjectContract.java +file +2587 + + + +2010-12-06T06:09:58.000000Z +af46039d0283eb614e4d7266b214d249 +2010-12-07T22:40:29.935924Z +2587 +loganlinn + + + + + + + + + + + + + + + + + + + + + +7141 + +ProjectDatabase.java +file +2389 + + + +2010-12-03T03:41:27.000000Z +d987323aa0ead9515780e358a603afed +2010-12-03T04:10:27.338840Z +2389 +loganlinn + + + + + + + + + + + + + + + + + + + + + +6101 + diff --git a/src/com/loganlinn/pivotaltrackie/provider/.svn/text-base/ProjectContract.java.svn-base b/src/com/loganlinn/pivotaltrackie/provider/.svn/text-base/ProjectContract.java.svn-base new file mode 100644 index 0000000..d22ef0b --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/provider/.svn/text-base/ProjectContract.java.svn-base @@ -0,0 +1,214 @@ +package com.loganlinn.pivotaltrackie.provider; + +import java.util.HashMap; + +import android.net.Uri; +import android.provider.BaseColumns; +import android.util.Log; + +public class ProjectContract { + /** + * Special value for {@link SyncColumns#UPDATED} indicating that an entry + * has never been updated, or doesn't exist yet. + */ + public static final long UPDATED_NEVER = -2; + + /** + * Special value for {@link SyncColumns#UPDATED} indicating that the last + * update time is unknown, usually when inserted from a local file source. + */ + public static final long UPDATED_UNKNOWN = -1; + + interface ProjectsColumns { + String PROJECT_ID = "project_id"; + String NAME = "name"; + String ITERATION_LENGTH = "iteration_length"; + String WEEK_START_DAY = "week_start_day"; + String POINT_SCALE = "point_scale"; + String ACCOUNT = "account"; + String VELOCITY_SCHEME = "velocity_sceme"; + String CURRENT_VELOCITY = "current_velocity"; + String INITIAL_VELOCITY = "initial_velocity"; + String NUMBER_OF_DONE_ITERATIONS_TO_SHOW = "iterations_shown"; + String ALLOW_ATTACHMENTS = "allow_attachments"; + String PUBLIC = "public"; + String USE_HTTPS = "use_https"; + String BUGS_AND_CHORES_ARE_ESTIMATABLE = "estimate_bugs_chores"; + String COMMIT_MODE = "commit_mode"; + String LAST_ACTIVITY_AT = "last_activity"; + String LABELS = "labels"; + } + + interface IterationColumns { + String ITERATION_ID = "iteration_id"; + String PROJECT_ID = Projects.PROJECT_ID; + String NUMBER = "number"; + String START = "start"; + String END = "end"; + + } + + interface StoriesColumns { + String STORY_ID = "story_id"; + String PROJECT_ID = Projects.PROJECT_ID; + String STORY_TYPE = "story_type"; + String URL = "url"; + String ESTIMATE = "estimate"; + String CURRENT_STATE = "current_state"; + String DESCRIPTION = "description"; + String NAME = "name"; + String REQUESTED_BY = "requested_by"; + String OWNED_BY = "owned_by"; + String CREATED_AT = "created_at"; + String ACCEPTED_AT = "accepted_at"; + String LABELS = "labels"; + String ITERATION_ID = IterationColumns.ITERATION_ID; + } + + interface MembersColumns { + String MEMBER_ID = "member_id"; + String PROJECT_ID = "project_id"; + String EMAIL = "email"; + String NAME = "name"; + String INITIALS = "initials"; + String ROLE = "role"; + } + + public interface SyncColumns { + /** Last time this entry was updated or synchronized. */ + String UPDATED = "updated"; + } + + public static final String CONTENT_AUTHORITY = "com.loganlinn.pivotaltrackie"; + + private static final Uri BASE_CONTENT_URI = Uri.parse("content://" + + CONTENT_AUTHORITY); + + private static final String PATH_PROJECTS = "projects"; + private static final String PATH_STORIES = "stories"; + private static final String PATH_MEMBERS = "members"; + private static final String PATH_ITERATIONS = "iterations"; + + public static class Projects implements ProjectsColumns, BaseColumns, + SyncColumns { + private static final String TAG = "Projects"; + + public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon() + .appendPath(PATH_PROJECTS).build(); + public static final String CONTENT_TYPE = "vnd.loganlinn.cursor.dir/vnd.pivotaltrackie.project"; + public static final String CONTENT_ITEM_TYPE = "vnd.loganlinn.cursor.item/vnd.pivotaltrackie.project"; + + public static Uri buildProjectUri(String projectId) { + return CONTENT_URI.buildUpon().appendPath(projectId).build(); + } + + public static Uri buildProjectUri(Integer projectId) { + return buildProjectUri(projectId.toString()); + } + + public static String getProjectId(Uri uri) { + Log.i(TAG, "ProjectId in [" + uri + "] is " + + uri.getPathSegments().get(1)); + return uri.getPathSegments().get(1); + } + } + + public static class Members implements MembersColumns, BaseColumns, + SyncColumns { + private static final String TAG = "Members"; + public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon() + .appendPath(PATH_MEMBERS).build(); + public static final String CONTENT_TYPE = "vnd.loganlinn.cursor.dir/vnd.pivotaltrackie.member"; + public static final String CONTENT_ITEM_TYPE = "vnd.loganlinn.cursor.item/vnd.pivotaltrackie.member"; + + public static Uri buildMemberUri(String memberId) { + return CONTENT_URI.buildUpon().appendPath(memberId).build(); + } + + public static String getMemberId(Uri uri) { + Log.i(TAG, "MemberId in [" + uri + "] is " + + uri.getPathSegments().get(1)); + return uri.getPathSegments().get(1); + } + } + + public static class Iterations implements IterationColumns, BaseColumns, + SyncColumns { + public static final String TAG = "Iterations"; + public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon() + .appendPath(PATH_ITERATIONS).build(); + public static final String CONTENT_TYPE = "vnd.loganlinn.cursor.dir/vnd.pivotaltrackie.iteration"; + public static final String CONTENT_ITEM_TYPE = "vnd.loganlinn.cursor.item/vnd.pivotaltrackie.iteration"; + + public static Uri buildIterationUri(String iterationId) { + return CONTENT_URI.buildUpon().appendPath(iterationId).build(); + } + + public static String getIterationId(Uri uri) { + return uri.getPathSegments().get(1); + } + } + + public static class Stories implements StoriesColumns, BaseColumns, + SyncColumns { + private static final String TAG = "Stories"; + public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon() + .appendPath(PATH_STORIES).build(); + public static final String CONTENT_TYPE = "vnd.loganlinn.cursor.dir/vnd.pivotaltrackie.story"; + public static final String CONTENT_ITEM_TYPE = "vnd.loganlinn.cursor.item/vnd.pivotaltrackie.story"; + + public static final String STORY_TYPE_FEATURE = "feature"; + public static final String STORY_TYPE_CHORE = "chore"; + public static final String STORY_TYPE_BUG = "bug"; + public static final String CURRENT_STATUS_ACCEPTED = "accepted"; + public static final String CURRENT_STATUS_UNSCHEDULED = "unscheduled"; + public static final String CURRENT_STATUS_FINISHED = "finished"; + public static final String CURRENT_STATUS_DELIVERED = "delivered"; + public static final String CURRENT_STATUS_STARTED = "started"; + public static final int NEW_STORY = -2; + + /** Default "ORDER BY" clause. */ + // public static final String DEFAULT_SORT = BlocksColumns.BLOCK_START + + // " ASC, " + // + BlocksColumns.BLOCK_END + " ASC"; + + public static Uri buildStoryUri(String storyId) { + return CONTENT_URI.buildUpon().appendPath(storyId).build(); + } + + public static Uri buildStoryUri(Integer storyId) { + return buildStoryUri(storyId.toString()); + } + + public static Uri buildStoryFilterUri(StoryFilter filter) { + return CONTENT_URI.buildUpon() + .appendPath(filter.getEncodedResult()).build(); + } + + public static String getStoryId(Uri uri) { + return uri.getPathSegments().get(1); + } + + } + + + interface StoryFilter { + HashMap filterValues_ = new HashMap(); + + public void addLabelFilter(String label); + + public void addIdFilter(String id); + + public void addStoryTypeFilter(String type); + + public void addCurrentStateFilter(String state); + + public void addNameFilter(String name); + + public String getEncodedResult(); + } + + private ProjectContract() { + + } +} diff --git a/src/com/loganlinn/pivotaltrackie/provider/.svn/text-base/ProjectDatabase.java.svn-base b/src/com/loganlinn/pivotaltrackie/provider/.svn/text-base/ProjectDatabase.java.svn-base new file mode 100644 index 0000000..d7495bd --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/provider/.svn/text-base/ProjectDatabase.java.svn-base @@ -0,0 +1,147 @@ +package com.loganlinn.pivotaltrackie.provider; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.provider.BaseColumns; +import android.util.Log; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract.IterationColumns; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.MembersColumns; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.ProjectsColumns; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.StoriesColumns; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.SyncColumns; + +public class ProjectDatabase extends SQLiteOpenHelper { + private static final String TAG = "ProjectDatabase"; + + private static final String DATABASE_NAME = "project.db"; + private static final int DATABASE_VERSION = 14; + + interface Tables { + String PROJECTS = "projects"; + String STORIES = "stories"; + String MEMBERS = "members"; + String STORY_TASK = "story_task"; + String STORY_ATTACHMENT = "story_attachment"; + String PROJECT_INTEGRATIONS = "project_integrations"; + String PROJECT_LABELS = "project_labels"; + String STORY_FILTERS = "story_filters"; + String ITERATIONS = "iterations"; + + // Joins + /* + * Example: String SESSIONS_JOIN_BLOCKS_ROOMS = "sessions " + + * "LEFT OUTER JOIN blocks ON sessions.block_id=blocks.block_id " + + * "LEFT OUTER JOIN rooms ON sessions.room_id=rooms.room_id"; + */ + } + + private interface References{ + String PROJECT_ID = "REFERENCES " + Tables.PROJECTS + "(" + ProjectsColumns.PROJECT_ID + ")"; + String ITERATION_ID = "REFERENCES " + Tables.ITERATIONS + "("+ IterationColumns.ITERATION_ID + ")"; + } + + public ProjectDatabase(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + Log.i(TAG, "Creating database tables"); + /* + * Projects Table + */ + db.execSQL("CREATE TABLE " + Tables.PROJECTS + " (" + + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + ProjectsColumns.PROJECT_ID + " INTEGER NOT NULL," + + ProjectsColumns.NAME + " TEXT NOT NULL," + + ProjectsColumns.ITERATION_LENGTH + " INTEGER NOT NULL," + + ProjectsColumns.WEEK_START_DAY + " TEXT NOT NULL," + + ProjectsColumns.POINT_SCALE + " TEXT NOT NULL," + + ProjectsColumns.ACCOUNT + " TEXT NOT NULL," + + ProjectsColumns.VELOCITY_SCHEME + " TEXT NOT NULL," + + ProjectsColumns.CURRENT_VELOCITY + " INTEGER NOT NULL," + + ProjectsColumns.INITIAL_VELOCITY + " INTEGER NOT NULL," + + ProjectsColumns.NUMBER_OF_DONE_ITERATIONS_TO_SHOW + " INTEGER NOT NULL," + + ProjectsColumns.ALLOW_ATTACHMENTS + " INTEGER NOT NULL," + + ProjectsColumns.PUBLIC + " INTEGER NOT NULL," + + ProjectsColumns.USE_HTTPS + " INTEGER NOT NULL," + + ProjectsColumns.BUGS_AND_CHORES_ARE_ESTIMATABLE + " INTEGER," + + ProjectsColumns.COMMIT_MODE + " INTEGER," + + ProjectsColumns.LAST_ACTIVITY_AT + " TEXT," + + ProjectsColumns.LABELS + " TEXT," + + SyncColumns.UPDATED + " INTEGER NOT NULL," + + "UNIQUE (" + BaseColumns._ID + ") ON CONFLICT REPLACE)"); + + /* + * Stories Table + */ + db.execSQL("CREATE TABLE " + Tables.STORIES + " (" + + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + StoriesColumns.STORY_ID + " INTEGER NOT NULL," + + StoriesColumns.PROJECT_ID + " INTEGER NOT NULL "+ References.PROJECT_ID +"," + + StoriesColumns.ITERATION_ID + " INTEGER "+References.ITERATION_ID+"," + + StoriesColumns.STORY_TYPE + " TEXT NOT NULL," + + StoriesColumns.URL + " TEXT NOT NULL," + + StoriesColumns.ESTIMATE + " INTEGER," + + StoriesColumns.CURRENT_STATE + " TEXT NOT NULL," + + StoriesColumns.DESCRIPTION + " TEXT," + + StoriesColumns.NAME + " TEXT NOT NULL," + + StoriesColumns.REQUESTED_BY + " TEXT," + + StoriesColumns.OWNED_BY + " TEXT," + + StoriesColumns.CREATED_AT + " TEXT," + + StoriesColumns.ACCEPTED_AT + " TEXT," + + StoriesColumns.LABELS + " TEXT," + + SyncColumns.UPDATED + " INTEGER NOT NULL," + + "UNIQUE (" + BaseColumns._ID + ") ON CONFLICT REPLACE)"); + + /* + * Members Table + */ + db.execSQL("CREATE TABLE " + Tables.MEMBERS + " (" + + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + MembersColumns.MEMBER_ID + " INTEGER NOT NULL," + + MembersColumns.PROJECT_ID + " INTEGER NOT NULL "+ References.PROJECT_ID +"," + + MembersColumns.EMAIL + " TEXT NOT NULL," + + MembersColumns.NAME + " TEXT NOT NULL," + + MembersColumns.INITIALS + " TEXT NOT NULL," + + MembersColumns.ROLE + " TEXT NOT NULL," + + "UNIQUE (" + BaseColumns._ID + ") ON CONFLICT REPLACE)"); + + /* + * Iterations Table + */ + db.execSQL("CREATE TABLE " + Tables.ITERATIONS + " (" + + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + IterationColumns.ITERATION_ID + " INTEGER NOT NULL," + + IterationColumns.PROJECT_ID + " INTEGER NOT NULL "+References.PROJECT_ID+"," + + IterationColumns.NUMBER + " INTEGER NOT NULL," + + IterationColumns.START + " TEXT NOT NULL," + + IterationColumns.END + " TEXT NOT NULL," + + SyncColumns.UPDATED + " INTEGER NOT NULL," + + "UNIQUE (" + BaseColumns._ID + ") ON CONFLICT REPLACE)"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.d(TAG, "onUpgrade() from " + oldVersion + " to " + newVersion); + + if(oldVersion != DATABASE_VERSION){ + Log.w(TAG, "Destroying old data during upgrade"); + db.execSQL("DROP TABLE IF EXISTS " + Tables.PROJECTS); + db.execSQL("DROP TABLE IF EXISTS " + Tables.MEMBERS); + db.execSQL("DROP TABLE IF EXISTS " + Tables.STORIES); + db.execSQL("DROP TABLE IF EXISTS " + Tables.PROJECT_INTEGRATIONS); + db.execSQL("DROP TABLE IF EXISTS " + Tables.PROJECT_LABELS); + db.execSQL("DROP TABLE IF EXISTS " + Tables.STORY_ATTACHMENT); + db.execSQL("DROP TABLE IF EXISTS " + Tables.STORY_TASK); + db.execSQL("DROP TABLE IF EXISTS " + Tables.STORY_FILTERS); + db.execSQL("DROP TABLE IF EXISTS " + Tables.ITERATIONS); + onCreate(db); + } + + + } + +} diff --git a/src/com/loganlinn/pivotaltrackie/provider/.svn/text-base/ProjectProvider.java.svn-base b/src/com/loganlinn/pivotaltrackie/provider/.svn/text-base/ProjectProvider.java.svn-base new file mode 100644 index 0000000..d8368c7 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/provider/.svn/text-base/ProjectProvider.java.svn-base @@ -0,0 +1,174 @@ + package com.loganlinn.pivotaltrackie.provider; + +import java.util.Arrays; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.util.Log; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Members; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Projects; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.provider.ProjectDatabase.Tables; +import com.loganlinn.pivotaltrackie.util.SelectionBuilder; + +public class ProjectProvider extends ContentProvider { + private static final String TAG = "ProjectProvider"; + private static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE); + private static final String MIME_XML = "text/xml"; + private static final UriMatcher sUriMatcher = buildUriMatcher(); + + // Uri values + private static final int PROJECTS = 100; + private static final int PROJECTS_ID = 101; + private static final int STORIES = 200; + private static final int STORIES_ID = 201; + private static final int MEMBERS = 300; + private static final int MEMBERS_ID = 301; + + private ProjectDatabase mOpenHelper; + + private static UriMatcher buildUriMatcher() { + final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); + final String authority = ProjectContract.CONTENT_AUTHORITY; + + matcher.addURI(authority, "projects", PROJECTS); + matcher.addURI(authority, "projects/*", PROJECTS_ID); + + matcher.addURI(authority, "stories", STORIES); + matcher.addURI(authority, "stories/*", STORIES_ID); + + matcher.addURI(authority, "members", MEMBERS); + matcher.addURI(authority, "members/*", MEMBERS_ID); + + return matcher; + } + + @Override + public String getType(Uri uri) { + final int match = sUriMatcher.match(uri); + switch (match) { + case PROJECTS: + return Projects.CONTENT_TYPE; + case PROJECTS_ID: + return Projects.CONTENT_ITEM_TYPE; + case STORIES: + return Stories.CONTENT_TYPE; + case STORIES_ID: + return Stories.CONTENT_ITEM_TYPE; + case MEMBERS: + return Members.CONTENT_TYPE; + case MEMBERS_ID: + return Members.CONTENT_ITEM_TYPE; + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + } + + @Override + public boolean onCreate() { + final Context context = getContext(); + mOpenHelper = new ProjectDatabase(context); + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + if (LOGV) + Log.v(TAG, "query(uri=" + uri + ", proj=" + + Arrays.toString(projection) + ")"); + final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + + // final int match = uriMatcher_.match(uri); + + // switch(match){ + // default: { + final SelectionBuilder builder = buildSelection(uri); + return builder.where(selection, selectionArgs).query(db, projection, + sortOrder); + // } + // } + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + if (LOGV) + Log.v(TAG, "insert(uri=" + uri + ", values=" + values.toString() + + ")"); + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final int match = sUriMatcher.match(uri); + + switch (match) { + case PROJECTS: + db.insertOrThrow(Tables.PROJECTS, null, values); + return Projects.buildProjectUri(values + .getAsString(Projects.PROJECT_ID)); + case STORIES: + db.insertOrThrow(Tables.STORIES, null, values); + return Stories.buildStoryUri(values.getAsString(Stories.STORY_ID)); + case MEMBERS: + db.insertOrThrow(Tables.MEMBERS, null, values); + return Members + .buildMemberUri(values.getAsString(Members.MEMBER_ID)); + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + } + + @Override + public int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + if (LOGV) + Log.v(TAG, "update(uri=" + uri + ", values=" + values.toString() + + ")"); + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final SelectionBuilder builder = buildSelection(uri); + return builder.where(selection, selectionArgs).update(db, values); + + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + if (LOGV) + Log.v(TAG, "delete(uri=" + uri + ")"); + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final SelectionBuilder builder = buildSelection(uri); + return builder.where(selection, selectionArgs).delete(db); + } + + private SelectionBuilder buildSelection(Uri uri) { + final SelectionBuilder builder = new SelectionBuilder(); + final int match = sUriMatcher.match(uri); + String projectId; + String storyId; + switch (match) { + case PROJECTS: + return builder.table(Tables.PROJECTS); + case PROJECTS_ID: + projectId = Projects.getProjectId(uri); + return builder.table(Tables.PROJECTS).where( + Projects.PROJECT_ID + "=?", projectId); + case STORIES: + return builder.table(Tables.STORIES); + case STORIES_ID: + storyId = Stories.getStoryId(uri); + return builder.table(Tables.STORIES).where( + Stories.STORY_ID + "=?", storyId); + case MEMBERS: + return builder.table(Tables.MEMBERS); + case MEMBERS_ID: + final String memberId = Members.getMemberId(uri); + return builder.table(Tables.MEMBERS).where( + Members.MEMBER_ID + "=?", memberId); + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + } + +} diff --git a/src/com/loganlinn/pivotaltrackie/provider/ProjectContract.java b/src/com/loganlinn/pivotaltrackie/provider/ProjectContract.java new file mode 100644 index 0000000..d22ef0b --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/provider/ProjectContract.java @@ -0,0 +1,214 @@ +package com.loganlinn.pivotaltrackie.provider; + +import java.util.HashMap; + +import android.net.Uri; +import android.provider.BaseColumns; +import android.util.Log; + +public class ProjectContract { + /** + * Special value for {@link SyncColumns#UPDATED} indicating that an entry + * has never been updated, or doesn't exist yet. + */ + public static final long UPDATED_NEVER = -2; + + /** + * Special value for {@link SyncColumns#UPDATED} indicating that the last + * update time is unknown, usually when inserted from a local file source. + */ + public static final long UPDATED_UNKNOWN = -1; + + interface ProjectsColumns { + String PROJECT_ID = "project_id"; + String NAME = "name"; + String ITERATION_LENGTH = "iteration_length"; + String WEEK_START_DAY = "week_start_day"; + String POINT_SCALE = "point_scale"; + String ACCOUNT = "account"; + String VELOCITY_SCHEME = "velocity_sceme"; + String CURRENT_VELOCITY = "current_velocity"; + String INITIAL_VELOCITY = "initial_velocity"; + String NUMBER_OF_DONE_ITERATIONS_TO_SHOW = "iterations_shown"; + String ALLOW_ATTACHMENTS = "allow_attachments"; + String PUBLIC = "public"; + String USE_HTTPS = "use_https"; + String BUGS_AND_CHORES_ARE_ESTIMATABLE = "estimate_bugs_chores"; + String COMMIT_MODE = "commit_mode"; + String LAST_ACTIVITY_AT = "last_activity"; + String LABELS = "labels"; + } + + interface IterationColumns { + String ITERATION_ID = "iteration_id"; + String PROJECT_ID = Projects.PROJECT_ID; + String NUMBER = "number"; + String START = "start"; + String END = "end"; + + } + + interface StoriesColumns { + String STORY_ID = "story_id"; + String PROJECT_ID = Projects.PROJECT_ID; + String STORY_TYPE = "story_type"; + String URL = "url"; + String ESTIMATE = "estimate"; + String CURRENT_STATE = "current_state"; + String DESCRIPTION = "description"; + String NAME = "name"; + String REQUESTED_BY = "requested_by"; + String OWNED_BY = "owned_by"; + String CREATED_AT = "created_at"; + String ACCEPTED_AT = "accepted_at"; + String LABELS = "labels"; + String ITERATION_ID = IterationColumns.ITERATION_ID; + } + + interface MembersColumns { + String MEMBER_ID = "member_id"; + String PROJECT_ID = "project_id"; + String EMAIL = "email"; + String NAME = "name"; + String INITIALS = "initials"; + String ROLE = "role"; + } + + public interface SyncColumns { + /** Last time this entry was updated or synchronized. */ + String UPDATED = "updated"; + } + + public static final String CONTENT_AUTHORITY = "com.loganlinn.pivotaltrackie"; + + private static final Uri BASE_CONTENT_URI = Uri.parse("content://" + + CONTENT_AUTHORITY); + + private static final String PATH_PROJECTS = "projects"; + private static final String PATH_STORIES = "stories"; + private static final String PATH_MEMBERS = "members"; + private static final String PATH_ITERATIONS = "iterations"; + + public static class Projects implements ProjectsColumns, BaseColumns, + SyncColumns { + private static final String TAG = "Projects"; + + public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon() + .appendPath(PATH_PROJECTS).build(); + public static final String CONTENT_TYPE = "vnd.loganlinn.cursor.dir/vnd.pivotaltrackie.project"; + public static final String CONTENT_ITEM_TYPE = "vnd.loganlinn.cursor.item/vnd.pivotaltrackie.project"; + + public static Uri buildProjectUri(String projectId) { + return CONTENT_URI.buildUpon().appendPath(projectId).build(); + } + + public static Uri buildProjectUri(Integer projectId) { + return buildProjectUri(projectId.toString()); + } + + public static String getProjectId(Uri uri) { + Log.i(TAG, "ProjectId in [" + uri + "] is " + + uri.getPathSegments().get(1)); + return uri.getPathSegments().get(1); + } + } + + public static class Members implements MembersColumns, BaseColumns, + SyncColumns { + private static final String TAG = "Members"; + public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon() + .appendPath(PATH_MEMBERS).build(); + public static final String CONTENT_TYPE = "vnd.loganlinn.cursor.dir/vnd.pivotaltrackie.member"; + public static final String CONTENT_ITEM_TYPE = "vnd.loganlinn.cursor.item/vnd.pivotaltrackie.member"; + + public static Uri buildMemberUri(String memberId) { + return CONTENT_URI.buildUpon().appendPath(memberId).build(); + } + + public static String getMemberId(Uri uri) { + Log.i(TAG, "MemberId in [" + uri + "] is " + + uri.getPathSegments().get(1)); + return uri.getPathSegments().get(1); + } + } + + public static class Iterations implements IterationColumns, BaseColumns, + SyncColumns { + public static final String TAG = "Iterations"; + public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon() + .appendPath(PATH_ITERATIONS).build(); + public static final String CONTENT_TYPE = "vnd.loganlinn.cursor.dir/vnd.pivotaltrackie.iteration"; + public static final String CONTENT_ITEM_TYPE = "vnd.loganlinn.cursor.item/vnd.pivotaltrackie.iteration"; + + public static Uri buildIterationUri(String iterationId) { + return CONTENT_URI.buildUpon().appendPath(iterationId).build(); + } + + public static String getIterationId(Uri uri) { + return uri.getPathSegments().get(1); + } + } + + public static class Stories implements StoriesColumns, BaseColumns, + SyncColumns { + private static final String TAG = "Stories"; + public static final Uri CONTENT_URI = BASE_CONTENT_URI.buildUpon() + .appendPath(PATH_STORIES).build(); + public static final String CONTENT_TYPE = "vnd.loganlinn.cursor.dir/vnd.pivotaltrackie.story"; + public static final String CONTENT_ITEM_TYPE = "vnd.loganlinn.cursor.item/vnd.pivotaltrackie.story"; + + public static final String STORY_TYPE_FEATURE = "feature"; + public static final String STORY_TYPE_CHORE = "chore"; + public static final String STORY_TYPE_BUG = "bug"; + public static final String CURRENT_STATUS_ACCEPTED = "accepted"; + public static final String CURRENT_STATUS_UNSCHEDULED = "unscheduled"; + public static final String CURRENT_STATUS_FINISHED = "finished"; + public static final String CURRENT_STATUS_DELIVERED = "delivered"; + public static final String CURRENT_STATUS_STARTED = "started"; + public static final int NEW_STORY = -2; + + /** Default "ORDER BY" clause. */ + // public static final String DEFAULT_SORT = BlocksColumns.BLOCK_START + + // " ASC, " + // + BlocksColumns.BLOCK_END + " ASC"; + + public static Uri buildStoryUri(String storyId) { + return CONTENT_URI.buildUpon().appendPath(storyId).build(); + } + + public static Uri buildStoryUri(Integer storyId) { + return buildStoryUri(storyId.toString()); + } + + public static Uri buildStoryFilterUri(StoryFilter filter) { + return CONTENT_URI.buildUpon() + .appendPath(filter.getEncodedResult()).build(); + } + + public static String getStoryId(Uri uri) { + return uri.getPathSegments().get(1); + } + + } + + + interface StoryFilter { + HashMap filterValues_ = new HashMap(); + + public void addLabelFilter(String label); + + public void addIdFilter(String id); + + public void addStoryTypeFilter(String type); + + public void addCurrentStateFilter(String state); + + public void addNameFilter(String name); + + public String getEncodedResult(); + } + + private ProjectContract() { + + } +} diff --git a/src/com/loganlinn/pivotaltrackie/provider/ProjectDatabase.java b/src/com/loganlinn/pivotaltrackie/provider/ProjectDatabase.java new file mode 100644 index 0000000..d7495bd --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/provider/ProjectDatabase.java @@ -0,0 +1,147 @@ +package com.loganlinn.pivotaltrackie.provider; + +import android.content.Context; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; +import android.provider.BaseColumns; +import android.util.Log; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract.IterationColumns; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.MembersColumns; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.ProjectsColumns; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.StoriesColumns; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.SyncColumns; + +public class ProjectDatabase extends SQLiteOpenHelper { + private static final String TAG = "ProjectDatabase"; + + private static final String DATABASE_NAME = "project.db"; + private static final int DATABASE_VERSION = 14; + + interface Tables { + String PROJECTS = "projects"; + String STORIES = "stories"; + String MEMBERS = "members"; + String STORY_TASK = "story_task"; + String STORY_ATTACHMENT = "story_attachment"; + String PROJECT_INTEGRATIONS = "project_integrations"; + String PROJECT_LABELS = "project_labels"; + String STORY_FILTERS = "story_filters"; + String ITERATIONS = "iterations"; + + // Joins + /* + * Example: String SESSIONS_JOIN_BLOCKS_ROOMS = "sessions " + + * "LEFT OUTER JOIN blocks ON sessions.block_id=blocks.block_id " + + * "LEFT OUTER JOIN rooms ON sessions.room_id=rooms.room_id"; + */ + } + + private interface References{ + String PROJECT_ID = "REFERENCES " + Tables.PROJECTS + "(" + ProjectsColumns.PROJECT_ID + ")"; + String ITERATION_ID = "REFERENCES " + Tables.ITERATIONS + "("+ IterationColumns.ITERATION_ID + ")"; + } + + public ProjectDatabase(Context context) { + super(context, DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + Log.i(TAG, "Creating database tables"); + /* + * Projects Table + */ + db.execSQL("CREATE TABLE " + Tables.PROJECTS + " (" + + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + ProjectsColumns.PROJECT_ID + " INTEGER NOT NULL," + + ProjectsColumns.NAME + " TEXT NOT NULL," + + ProjectsColumns.ITERATION_LENGTH + " INTEGER NOT NULL," + + ProjectsColumns.WEEK_START_DAY + " TEXT NOT NULL," + + ProjectsColumns.POINT_SCALE + " TEXT NOT NULL," + + ProjectsColumns.ACCOUNT + " TEXT NOT NULL," + + ProjectsColumns.VELOCITY_SCHEME + " TEXT NOT NULL," + + ProjectsColumns.CURRENT_VELOCITY + " INTEGER NOT NULL," + + ProjectsColumns.INITIAL_VELOCITY + " INTEGER NOT NULL," + + ProjectsColumns.NUMBER_OF_DONE_ITERATIONS_TO_SHOW + " INTEGER NOT NULL," + + ProjectsColumns.ALLOW_ATTACHMENTS + " INTEGER NOT NULL," + + ProjectsColumns.PUBLIC + " INTEGER NOT NULL," + + ProjectsColumns.USE_HTTPS + " INTEGER NOT NULL," + + ProjectsColumns.BUGS_AND_CHORES_ARE_ESTIMATABLE + " INTEGER," + + ProjectsColumns.COMMIT_MODE + " INTEGER," + + ProjectsColumns.LAST_ACTIVITY_AT + " TEXT," + + ProjectsColumns.LABELS + " TEXT," + + SyncColumns.UPDATED + " INTEGER NOT NULL," + + "UNIQUE (" + BaseColumns._ID + ") ON CONFLICT REPLACE)"); + + /* + * Stories Table + */ + db.execSQL("CREATE TABLE " + Tables.STORIES + " (" + + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + StoriesColumns.STORY_ID + " INTEGER NOT NULL," + + StoriesColumns.PROJECT_ID + " INTEGER NOT NULL "+ References.PROJECT_ID +"," + + StoriesColumns.ITERATION_ID + " INTEGER "+References.ITERATION_ID+"," + + StoriesColumns.STORY_TYPE + " TEXT NOT NULL," + + StoriesColumns.URL + " TEXT NOT NULL," + + StoriesColumns.ESTIMATE + " INTEGER," + + StoriesColumns.CURRENT_STATE + " TEXT NOT NULL," + + StoriesColumns.DESCRIPTION + " TEXT," + + StoriesColumns.NAME + " TEXT NOT NULL," + + StoriesColumns.REQUESTED_BY + " TEXT," + + StoriesColumns.OWNED_BY + " TEXT," + + StoriesColumns.CREATED_AT + " TEXT," + + StoriesColumns.ACCEPTED_AT + " TEXT," + + StoriesColumns.LABELS + " TEXT," + + SyncColumns.UPDATED + " INTEGER NOT NULL," + + "UNIQUE (" + BaseColumns._ID + ") ON CONFLICT REPLACE)"); + + /* + * Members Table + */ + db.execSQL("CREATE TABLE " + Tables.MEMBERS + " (" + + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + MembersColumns.MEMBER_ID + " INTEGER NOT NULL," + + MembersColumns.PROJECT_ID + " INTEGER NOT NULL "+ References.PROJECT_ID +"," + + MembersColumns.EMAIL + " TEXT NOT NULL," + + MembersColumns.NAME + " TEXT NOT NULL," + + MembersColumns.INITIALS + " TEXT NOT NULL," + + MembersColumns.ROLE + " TEXT NOT NULL," + + "UNIQUE (" + BaseColumns._ID + ") ON CONFLICT REPLACE)"); + + /* + * Iterations Table + */ + db.execSQL("CREATE TABLE " + Tables.ITERATIONS + " (" + + BaseColumns._ID + " INTEGER PRIMARY KEY AUTOINCREMENT," + + IterationColumns.ITERATION_ID + " INTEGER NOT NULL," + + IterationColumns.PROJECT_ID + " INTEGER NOT NULL "+References.PROJECT_ID+"," + + IterationColumns.NUMBER + " INTEGER NOT NULL," + + IterationColumns.START + " TEXT NOT NULL," + + IterationColumns.END + " TEXT NOT NULL," + + SyncColumns.UPDATED + " INTEGER NOT NULL," + + "UNIQUE (" + BaseColumns._ID + ") ON CONFLICT REPLACE)"); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + Log.d(TAG, "onUpgrade() from " + oldVersion + " to " + newVersion); + + if(oldVersion != DATABASE_VERSION){ + Log.w(TAG, "Destroying old data during upgrade"); + db.execSQL("DROP TABLE IF EXISTS " + Tables.PROJECTS); + db.execSQL("DROP TABLE IF EXISTS " + Tables.MEMBERS); + db.execSQL("DROP TABLE IF EXISTS " + Tables.STORIES); + db.execSQL("DROP TABLE IF EXISTS " + Tables.PROJECT_INTEGRATIONS); + db.execSQL("DROP TABLE IF EXISTS " + Tables.PROJECT_LABELS); + db.execSQL("DROP TABLE IF EXISTS " + Tables.STORY_ATTACHMENT); + db.execSQL("DROP TABLE IF EXISTS " + Tables.STORY_TASK); + db.execSQL("DROP TABLE IF EXISTS " + Tables.STORY_FILTERS); + db.execSQL("DROP TABLE IF EXISTS " + Tables.ITERATIONS); + onCreate(db); + } + + + } + +} diff --git a/src/com/loganlinn/pivotaltrackie/provider/ProjectProvider.java b/src/com/loganlinn/pivotaltrackie/provider/ProjectProvider.java new file mode 100644 index 0000000..d8368c7 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/provider/ProjectProvider.java @@ -0,0 +1,174 @@ + package com.loganlinn.pivotaltrackie.provider; + +import java.util.Arrays; + +import android.content.ContentProvider; +import android.content.ContentValues; +import android.content.Context; +import android.content.UriMatcher; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; +import android.util.Log; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Members; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Projects; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.provider.ProjectDatabase.Tables; +import com.loganlinn.pivotaltrackie.util.SelectionBuilder; + +public class ProjectProvider extends ContentProvider { + private static final String TAG = "ProjectProvider"; + private static final boolean LOGV = Log.isLoggable(TAG, Log.VERBOSE); + private static final String MIME_XML = "text/xml"; + private static final UriMatcher sUriMatcher = buildUriMatcher(); + + // Uri values + private static final int PROJECTS = 100; + private static final int PROJECTS_ID = 101; + private static final int STORIES = 200; + private static final int STORIES_ID = 201; + private static final int MEMBERS = 300; + private static final int MEMBERS_ID = 301; + + private ProjectDatabase mOpenHelper; + + private static UriMatcher buildUriMatcher() { + final UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); + final String authority = ProjectContract.CONTENT_AUTHORITY; + + matcher.addURI(authority, "projects", PROJECTS); + matcher.addURI(authority, "projects/*", PROJECTS_ID); + + matcher.addURI(authority, "stories", STORIES); + matcher.addURI(authority, "stories/*", STORIES_ID); + + matcher.addURI(authority, "members", MEMBERS); + matcher.addURI(authority, "members/*", MEMBERS_ID); + + return matcher; + } + + @Override + public String getType(Uri uri) { + final int match = sUriMatcher.match(uri); + switch (match) { + case PROJECTS: + return Projects.CONTENT_TYPE; + case PROJECTS_ID: + return Projects.CONTENT_ITEM_TYPE; + case STORIES: + return Stories.CONTENT_TYPE; + case STORIES_ID: + return Stories.CONTENT_ITEM_TYPE; + case MEMBERS: + return Members.CONTENT_TYPE; + case MEMBERS_ID: + return Members.CONTENT_ITEM_TYPE; + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + } + + @Override + public boolean onCreate() { + final Context context = getContext(); + mOpenHelper = new ProjectDatabase(context); + return true; + } + + @Override + public Cursor query(Uri uri, String[] projection, String selection, + String[] selectionArgs, String sortOrder) { + if (LOGV) + Log.v(TAG, "query(uri=" + uri + ", proj=" + + Arrays.toString(projection) + ")"); + final SQLiteDatabase db = mOpenHelper.getReadableDatabase(); + + // final int match = uriMatcher_.match(uri); + + // switch(match){ + // default: { + final SelectionBuilder builder = buildSelection(uri); + return builder.where(selection, selectionArgs).query(db, projection, + sortOrder); + // } + // } + } + + @Override + public Uri insert(Uri uri, ContentValues values) { + if (LOGV) + Log.v(TAG, "insert(uri=" + uri + ", values=" + values.toString() + + ")"); + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final int match = sUriMatcher.match(uri); + + switch (match) { + case PROJECTS: + db.insertOrThrow(Tables.PROJECTS, null, values); + return Projects.buildProjectUri(values + .getAsString(Projects.PROJECT_ID)); + case STORIES: + db.insertOrThrow(Tables.STORIES, null, values); + return Stories.buildStoryUri(values.getAsString(Stories.STORY_ID)); + case MEMBERS: + db.insertOrThrow(Tables.MEMBERS, null, values); + return Members + .buildMemberUri(values.getAsString(Members.MEMBER_ID)); + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + } + + @Override + public int update(Uri uri, ContentValues values, String selection, + String[] selectionArgs) { + if (LOGV) + Log.v(TAG, "update(uri=" + uri + ", values=" + values.toString() + + ")"); + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final SelectionBuilder builder = buildSelection(uri); + return builder.where(selection, selectionArgs).update(db, values); + + } + + @Override + public int delete(Uri uri, String selection, String[] selectionArgs) { + if (LOGV) + Log.v(TAG, "delete(uri=" + uri + ")"); + final SQLiteDatabase db = mOpenHelper.getWritableDatabase(); + final SelectionBuilder builder = buildSelection(uri); + return builder.where(selection, selectionArgs).delete(db); + } + + private SelectionBuilder buildSelection(Uri uri) { + final SelectionBuilder builder = new SelectionBuilder(); + final int match = sUriMatcher.match(uri); + String projectId; + String storyId; + switch (match) { + case PROJECTS: + return builder.table(Tables.PROJECTS); + case PROJECTS_ID: + projectId = Projects.getProjectId(uri); + return builder.table(Tables.PROJECTS).where( + Projects.PROJECT_ID + "=?", projectId); + case STORIES: + return builder.table(Tables.STORIES); + case STORIES_ID: + storyId = Stories.getStoryId(uri); + return builder.table(Tables.STORIES).where( + Stories.STORY_ID + "=?", storyId); + case MEMBERS: + return builder.table(Tables.MEMBERS); + case MEMBERS_ID: + final String memberId = Members.getMemberId(uri); + return builder.table(Tables.MEMBERS).where( + Members.MEMBER_ID + "=?", memberId); + default: + throw new UnsupportedOperationException("Unknown uri: " + uri); + } + } + +} diff --git a/src/com/loganlinn/pivotaltrackie/service/.svn/all-wcprops b/src/com/loganlinn/pivotaltrackie/service/.svn/all-wcprops new file mode 100644 index 0000000..0a7bcf4 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/service/.svn/all-wcprops @@ -0,0 +1,11 @@ +K 25 +svn:wc:ra_dav:version-url +V 109 +/svn/!svn/ver/2029/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/service +END +SyncService.java +K 25 +svn:wc:ra_dav:version-url +V 126 +/svn/!svn/ver/2626/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/service/SyncService.java +END diff --git a/src/com/loganlinn/pivotaltrackie/service/.svn/dir-prop-base b/src/com/loganlinn/pivotaltrackie/service/.svn/dir-prop-base new file mode 100644 index 0000000..95b2379 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/service/.svn/dir-prop-base @@ -0,0 +1,6 @@ +K 10 +svn:ignore +V 6 +.git* + +END diff --git a/src/com/loganlinn/pivotaltrackie/service/.svn/entries b/src/com/loganlinn/pivotaltrackie/service/.svn/entries new file mode 100644 index 0000000..3ffedad --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/service/.svn/entries @@ -0,0 +1,62 @@ +10 + +dir +2086 +https://vtnetapps.googlecode.com/svn/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/service +https://vtnetapps.googlecode.com/svn + + + +2010-11-26T19:33:38.466730Z +2029 +loganlinn +has-props + + + + + + + + + + + + + +13dd798f-c9a9-0d3a-1410-a4e436c6fec4 + +SyncService.java +file +2626 + + + +2010-12-08T15:14:18.000000Z +1f6b210c59bd48181b9252b04c1c5eb0 +2010-12-10T17:14:25.002194Z +2626 +loganlinn + + + + + + + + + + + + + + + + + + + + + +7514 + diff --git a/src/com/loganlinn/pivotaltrackie/service/.svn/text-base/SyncService.java.svn-base b/src/com/loganlinn/pivotaltrackie/service/.svn/text-base/SyncService.java.svn-base new file mode 100644 index 0000000..9d14bf8 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/service/.svn/text-base/SyncService.java.svn-base @@ -0,0 +1,221 @@ +package com.loganlinn.pivotaltrackie.service; + +import java.io.IOException; +import java.util.ArrayList; + +import org.apache.http.client.HttpClient; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.app.IntentService; +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.os.Bundle; +import android.os.ResultReceiver; +import android.util.Log; + +import com.loganlinn.pivotaltracker.PivotalTracker; +import com.loganlinn.pivotaltracker.StoryEntry; +import com.loganlinn.pivotaltracker.PivotalTracker.Prefs; +import com.loganlinn.pivotaltrackie.io.RemoteExecutor; +import com.loganlinn.pivotaltrackie.io.RemoteProjectsHandler; +import com.loganlinn.pivotaltrackie.io.RemoteStoriesHandler; +import com.loganlinn.pivotaltrackie.io.XmlHandler; +import com.loganlinn.pivotaltrackie.io.XmlHandler.HandlerException; +import com.loganlinn.pivotaltrackie.provider.ProjectContract; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Projects; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.util.HttpUtils; + +/** + * IntentService is a base class for Services that handle asynchronous requests + * (expressed as Intents) on demand. Clients send requests through + * startService(Intent) calls; the service is started as needed, handles each + * Intent in turn using a worker thread, and stops itself when it runs out of + * work. + * + * @author loganlinn + * + */ +public class SyncService extends IntentService { + private static final String TAG = "SyncService"; + public static final String EXTRA_STATUS_RECEIVER = "com.loganlinn.pivotaltrackie.extra.STATUS_RECEIVER"; + // public static final String ACTION_SYNC_PROJECTS = + // "com.loganlinn.pivotaltrackie.action.SYNC_PROJECTS"; + public static final int STATUS_RUNNING = 0x1; + public static final int STATUS_ERROR = 0x2; + public static final int STATUS_FINISHED = 0x3; + + public static final String ACTION_SYNC_PROJECTS = "com.loganlinn.pivotaltrackie.action.SYNC_PROJECTS"; + public static final String ACTION_SYNC_STORIES = "com.loganlinn.pivotaltrackie.action.SYNC_STORIES"; + public static final String ACTION_SYNC_STORY = "com.loganlinn.pivotaltrackie.action.SYNC_STORY"; + public static final String ACTION_DELETE_STORY = "com.loganlinn.pivotaltrackie.action.DELETE_STORY"; + + public static final String SEND_TO_API = "SendToAPI"; + private static final String PROJECTS_URL = "http://www.pivotaltracker.com/services/v3/projects"; + + private RemoteExecutor mRemoteExecutor; + + public SyncService() { + super(TAG); + } + + @Override + public void onCreate() { + super.onCreate(); + + final HttpClient httpClient = HttpUtils.getHttpClient(this); + final ContentResolver resolver = getContentResolver(); + + // localExecutor_ = new LocalExecutor(getResources(), resolver); + mRemoteExecutor = new RemoteExecutor(httpClient, resolver); + } + + private boolean connectionAvailable() { + + ConnectivityManager cm = ((ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE)); + final boolean network = cm.getNetworkInfo( + ConnectivityManager.TYPE_MOBILE).isAvailable(); + final boolean wifi = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI) + .isAvailable(); + + return network | wifi | true; + } + + @Override + protected void onHandleIntent(Intent intent) { + Log.d(TAG, "onHandleIntent(intent=" + intent.toString() + ")"); + final ResultReceiver receiver = intent + .getParcelableExtra(EXTRA_STATUS_RECEIVER); + if (receiver != null) + receiver.send(STATUS_RUNNING, Bundle.EMPTY); + final SharedPreferences prefs = getSharedPreferences(Prefs.NAME, + Context.MODE_PRIVATE); + final Context context = this; + Bundle returnData = Bundle.EMPTY; + + if (connectionAvailable()) { + + try { + final long startRemote = System.currentTimeMillis(); + final String action = intent.getAction(); + + if (ACTION_SYNC_PROJECTS.equals(action)) { + mRemoteExecutor.executeGet(PROJECTS_URL, + new RemoteProjectsHandler(mRemoteExecutor)); + } else if (ACTION_SYNC_STORIES.equals(action)) { + + final String projectId = Projects.getProjectId(intent + .getData()); + Log.i(TAG, "Fetching stories for project_id=" + projectId); + mRemoteExecutor.executeGet(PivotalTracker + .getStoriesUrl(projectId), + new RemoteStoriesHandler(mRemoteExecutor)); + + } else if (ACTION_SYNC_STORY.equals(action)) { + final Bundle intentData = intent.getExtras(); + if (intentData != null) { + + getContentResolver().delete(Stories.CONTENT_URI, null, + null); + final RemoteStoriesHandler handler = new RemoteStoriesHandler( + mRemoteExecutor); + + final int projectId = intent.getIntExtra( + Stories.PROJECT_ID, -1); + final int storyId = intent.getIntExtra( + Stories.STORY_ID, -1); + Log.i(TAG, "Syncing story with id = " + + String.valueOf(storyId)); + if (intentData.getBoolean(SEND_TO_API, false)) { + final StoryEntry story = StoryEntry + .fromBundle(intentData); + + if (story.get(Stories.STORY_ID) == String + .valueOf(Stories.NEW_STORY)) { // add the + // story + Log.i(TAG, "Adding the story"); + mRemoteExecutor.execute(PivotalTracker + .getPostRequest(story), handler); + } else { // update story + Log.i(TAG, "Updating the story"); + mRemoteExecutor.execute(PivotalTracker + .getPutRequest(story), handler); + } + } else if (storyId != -1 && projectId != -1) { + handler.setContentUri(Stories + .buildStoryUri(storyId)); + + mRemoteExecutor.executeGet(PivotalTracker + .getStoryUrl(projectId, storyId), handler); + } + } + + } else if (ACTION_DELETE_STORY.equals(action)) { + + final int projectId = intent.getIntExtra( + Stories.PROJECT_ID, -5); + final int storyId = intent.getIntExtra(Stories.STORY_ID, + Stories.NEW_STORY); + + if (projectId > 0 && storyId > 0) { + Log.i(TAG, "Deleting story " + storyId); + mRemoteExecutor.execute(PivotalTracker + .getDeleteStoryRequest(projectId, storyId), + new XmlHandler( + ProjectContract.CONTENT_AUTHORITY) { + + @Override + public ArrayList parse( + XmlPullParser parser, + ContentResolver resolver) + throws XmlPullParserException, + IOException { + final ArrayList batch = new ArrayList(); + return batch; + } + + }); + } + + } + + Log.d(TAG, "Remote sync took " + + (System.currentTimeMillis() - startRemote) + + "ms for " + action); + + } catch (HandlerException e) { + + } catch (Exception e) { + + Log.e(TAG, "Problem while syncing", e); + + if (receiver != null) { + // Pass back error to surface listener + final Bundle bundle = new Bundle(); + bundle.putString(Intent.EXTRA_TEXT, e.toString()); + receiver.send(STATUS_ERROR, bundle); + } else { + Log.d(TAG, "No receiver found"); + } + } + }else{ + //we do not have connection + if(receiver != null){ + receiver.send(STATUS_ERROR, returnData); + } + } + // Announce success to any surface listener + Log.d(TAG, "sync finished"); + if (receiver != null) + receiver.send(STATUS_FINISHED, returnData); + } + + private void syncProjects() { + + } +} diff --git a/src/com/loganlinn/pivotaltrackie/service/SyncService.java b/src/com/loganlinn/pivotaltrackie/service/SyncService.java new file mode 100644 index 0000000..9d14bf8 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/service/SyncService.java @@ -0,0 +1,221 @@ +package com.loganlinn.pivotaltrackie.service; + +import java.io.IOException; +import java.util.ArrayList; + +import org.apache.http.client.HttpClient; +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; + +import android.app.IntentService; +import android.content.ContentProviderOperation; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.net.ConnectivityManager; +import android.os.Bundle; +import android.os.ResultReceiver; +import android.util.Log; + +import com.loganlinn.pivotaltracker.PivotalTracker; +import com.loganlinn.pivotaltracker.StoryEntry; +import com.loganlinn.pivotaltracker.PivotalTracker.Prefs; +import com.loganlinn.pivotaltrackie.io.RemoteExecutor; +import com.loganlinn.pivotaltrackie.io.RemoteProjectsHandler; +import com.loganlinn.pivotaltrackie.io.RemoteStoriesHandler; +import com.loganlinn.pivotaltrackie.io.XmlHandler; +import com.loganlinn.pivotaltrackie.io.XmlHandler.HandlerException; +import com.loganlinn.pivotaltrackie.provider.ProjectContract; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Projects; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.util.HttpUtils; + +/** + * IntentService is a base class for Services that handle asynchronous requests + * (expressed as Intents) on demand. Clients send requests through + * startService(Intent) calls; the service is started as needed, handles each + * Intent in turn using a worker thread, and stops itself when it runs out of + * work. + * + * @author loganlinn + * + */ +public class SyncService extends IntentService { + private static final String TAG = "SyncService"; + public static final String EXTRA_STATUS_RECEIVER = "com.loganlinn.pivotaltrackie.extra.STATUS_RECEIVER"; + // public static final String ACTION_SYNC_PROJECTS = + // "com.loganlinn.pivotaltrackie.action.SYNC_PROJECTS"; + public static final int STATUS_RUNNING = 0x1; + public static final int STATUS_ERROR = 0x2; + public static final int STATUS_FINISHED = 0x3; + + public static final String ACTION_SYNC_PROJECTS = "com.loganlinn.pivotaltrackie.action.SYNC_PROJECTS"; + public static final String ACTION_SYNC_STORIES = "com.loganlinn.pivotaltrackie.action.SYNC_STORIES"; + public static final String ACTION_SYNC_STORY = "com.loganlinn.pivotaltrackie.action.SYNC_STORY"; + public static final String ACTION_DELETE_STORY = "com.loganlinn.pivotaltrackie.action.DELETE_STORY"; + + public static final String SEND_TO_API = "SendToAPI"; + private static final String PROJECTS_URL = "http://www.pivotaltracker.com/services/v3/projects"; + + private RemoteExecutor mRemoteExecutor; + + public SyncService() { + super(TAG); + } + + @Override + public void onCreate() { + super.onCreate(); + + final HttpClient httpClient = HttpUtils.getHttpClient(this); + final ContentResolver resolver = getContentResolver(); + + // localExecutor_ = new LocalExecutor(getResources(), resolver); + mRemoteExecutor = new RemoteExecutor(httpClient, resolver); + } + + private boolean connectionAvailable() { + + ConnectivityManager cm = ((ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE)); + final boolean network = cm.getNetworkInfo( + ConnectivityManager.TYPE_MOBILE).isAvailable(); + final boolean wifi = cm.getNetworkInfo(ConnectivityManager.TYPE_WIFI) + .isAvailable(); + + return network | wifi | true; + } + + @Override + protected void onHandleIntent(Intent intent) { + Log.d(TAG, "onHandleIntent(intent=" + intent.toString() + ")"); + final ResultReceiver receiver = intent + .getParcelableExtra(EXTRA_STATUS_RECEIVER); + if (receiver != null) + receiver.send(STATUS_RUNNING, Bundle.EMPTY); + final SharedPreferences prefs = getSharedPreferences(Prefs.NAME, + Context.MODE_PRIVATE); + final Context context = this; + Bundle returnData = Bundle.EMPTY; + + if (connectionAvailable()) { + + try { + final long startRemote = System.currentTimeMillis(); + final String action = intent.getAction(); + + if (ACTION_SYNC_PROJECTS.equals(action)) { + mRemoteExecutor.executeGet(PROJECTS_URL, + new RemoteProjectsHandler(mRemoteExecutor)); + } else if (ACTION_SYNC_STORIES.equals(action)) { + + final String projectId = Projects.getProjectId(intent + .getData()); + Log.i(TAG, "Fetching stories for project_id=" + projectId); + mRemoteExecutor.executeGet(PivotalTracker + .getStoriesUrl(projectId), + new RemoteStoriesHandler(mRemoteExecutor)); + + } else if (ACTION_SYNC_STORY.equals(action)) { + final Bundle intentData = intent.getExtras(); + if (intentData != null) { + + getContentResolver().delete(Stories.CONTENT_URI, null, + null); + final RemoteStoriesHandler handler = new RemoteStoriesHandler( + mRemoteExecutor); + + final int projectId = intent.getIntExtra( + Stories.PROJECT_ID, -1); + final int storyId = intent.getIntExtra( + Stories.STORY_ID, -1); + Log.i(TAG, "Syncing story with id = " + + String.valueOf(storyId)); + if (intentData.getBoolean(SEND_TO_API, false)) { + final StoryEntry story = StoryEntry + .fromBundle(intentData); + + if (story.get(Stories.STORY_ID) == String + .valueOf(Stories.NEW_STORY)) { // add the + // story + Log.i(TAG, "Adding the story"); + mRemoteExecutor.execute(PivotalTracker + .getPostRequest(story), handler); + } else { // update story + Log.i(TAG, "Updating the story"); + mRemoteExecutor.execute(PivotalTracker + .getPutRequest(story), handler); + } + } else if (storyId != -1 && projectId != -1) { + handler.setContentUri(Stories + .buildStoryUri(storyId)); + + mRemoteExecutor.executeGet(PivotalTracker + .getStoryUrl(projectId, storyId), handler); + } + } + + } else if (ACTION_DELETE_STORY.equals(action)) { + + final int projectId = intent.getIntExtra( + Stories.PROJECT_ID, -5); + final int storyId = intent.getIntExtra(Stories.STORY_ID, + Stories.NEW_STORY); + + if (projectId > 0 && storyId > 0) { + Log.i(TAG, "Deleting story " + storyId); + mRemoteExecutor.execute(PivotalTracker + .getDeleteStoryRequest(projectId, storyId), + new XmlHandler( + ProjectContract.CONTENT_AUTHORITY) { + + @Override + public ArrayList parse( + XmlPullParser parser, + ContentResolver resolver) + throws XmlPullParserException, + IOException { + final ArrayList batch = new ArrayList(); + return batch; + } + + }); + } + + } + + Log.d(TAG, "Remote sync took " + + (System.currentTimeMillis() - startRemote) + + "ms for " + action); + + } catch (HandlerException e) { + + } catch (Exception e) { + + Log.e(TAG, "Problem while syncing", e); + + if (receiver != null) { + // Pass back error to surface listener + final Bundle bundle = new Bundle(); + bundle.putString(Intent.EXTRA_TEXT, e.toString()); + receiver.send(STATUS_ERROR, bundle); + } else { + Log.d(TAG, "No receiver found"); + } + } + }else{ + //we do not have connection + if(receiver != null){ + receiver.send(STATUS_ERROR, returnData); + } + } + // Announce success to any surface listener + Log.d(TAG, "sync finished"); + if (receiver != null) + receiver.send(STATUS_FINISHED, returnData); + } + + private void syncProjects() { + + } +} diff --git a/src/com/loganlinn/pivotaltrackie/ui/.svn/all-wcprops b/src/com/loganlinn/pivotaltrackie/ui/.svn/all-wcprops new file mode 100644 index 0000000..32127bc --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/.svn/all-wcprops @@ -0,0 +1,47 @@ +K 25 +svn:wc:ra_dav:version-url +V 104 +/svn/!svn/ver/2029/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/ui +END +XMLTokenHandler.java +K 25 +svn:wc:ra_dav:version-url +V 125 +/svn/!svn/ver/2587/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/ui/XMLTokenHandler.java +END +ProjectActivity.java +K 25 +svn:wc:ra_dav:version-url +V 125 +/svn/!svn/ver/2626/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/ui/ProjectActivity.java +END +EditStoryActivity.java +K 25 +svn:wc:ra_dav:version-url +V 127 +/svn/!svn/ver/2626/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/ui/EditStoryActivity.java +END +HomeActivity.java +K 25 +svn:wc:ra_dav:version-url +V 122 +/svn/!svn/ver/2588/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/ui/HomeActivity.java +END +LoginActivity.java +K 25 +svn:wc:ra_dav:version-url +V 123 +/svn/!svn/ver/2183/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/ui/LoginActivity.java +END +ViewStoryActivity.java +K 25 +svn:wc:ra_dav:version-url +V 127 +/svn/!svn/ver/2588/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/ui/ViewStoryActivity.java +END +ActivityState.java +K 25 +svn:wc:ra_dav:version-url +V 123 +/svn/!svn/ver/2183/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/ui/ActivityState.java +END diff --git a/src/com/loganlinn/pivotaltrackie/ui/.svn/entries b/src/com/loganlinn/pivotaltrackie/ui/.svn/entries new file mode 100644 index 0000000..79cacf4 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/.svn/entries @@ -0,0 +1,314 @@ +10 + +dir +2086 +https://vtnetapps.googlecode.com/svn/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/ui +https://vtnetapps.googlecode.com/svn + + + +2010-11-26T19:33:38.466730Z +2029 +loganlinn + + + + + + + + + + + + + + +13dd798f-c9a9-0d3a-1410-a4e436c6fec4 + +XMLTokenHandler.java +file +2587 + + + +2010-12-06T05:12:11.000000Z +06df2ee80c8675c8524b455a3538c2b0 +2010-12-07T22:40:29.935924Z +2587 +loganlinn + + + + + + + + + + + + + + + + + + + + + +2028 + +ProjectActivity.java +file +2626 + + + +2010-12-08T14:25:35.000000Z +8a79c577e8e9835e61a56fc9b536abf4 +2010-12-10T17:14:25.002194Z +2626 +loganlinn + + + + + + + + + + + + + + + + + + + + + +12169 + +EditStoryActivity.java +file +2626 + + + +2010-12-08T14:49:26.000000Z +b03154886250de7d0044e8560c5c124b +2010-12-10T17:14:25.002194Z +2626 +loganlinn + + + + + + + + + + + + + + + + + + + + + +10785 + +HomeActivity.java +file +2588 + + + +2010-12-07T22:44:02.000000Z +79e0f5e563b083c45316dbf0442abd3f +2010-12-08T00:10:34.757519Z +2588 +loganlinn + + + + + + + + + + + + + + + + + + + + + +8929 + +LoginActivity.java +file +2183 + + + +2010-12-02T03:23:53.000000Z +23d3ba5e7e173afa6413ac9d188c5ca5 +2010-12-02T04:08:08.804433Z +2183 +loganlinn + + + + + + + + + + + + + + + + + + + + + +1741 + +ViewStoryActivity.java +file +2588 + + + +2010-12-07T23:54:09.000000Z +2a94362d8e31f9d7b5930a10bcfb45de +2010-12-08T00:10:34.757519Z +2588 +loganlinn + + + + + + + + + + + + + + + + + + + + + +7710 + +ActivityState.java +file +2183 + + + +2010-12-02T01:01:23.000000Z +68003816ccbb4a3dea04fe44efaa34de +2010-12-02T04:08:08.804433Z +2183 +loganlinn + + + + + + + + + + + + + + + + + + + + + +322 + +StoryActivity.java +file +2183 + + + + + + + + + + + + + + + + + + + +deleted + +PreferencesActivity.java +file +2183 + + + + + + + + + + + + + + + + + + + +deleted + diff --git a/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/ActivityState.java.svn-base b/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/ActivityState.java.svn-base new file mode 100644 index 0000000..8fe607e --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/ActivityState.java.svn-base @@ -0,0 +1,14 @@ +package com.loganlinn.pivotaltrackie.ui; + +import android.os.Handler; + +import com.loganlinn.pivotaltrackie.util.DetachableResultReceiver; + +class ActivityState { + public DetachableResultReceiver mReceiver; + public boolean mSyncing = false; + + ActivityState() { + mReceiver = new DetachableResultReceiver(new Handler()); + } +} diff --git a/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/EditStoryActivity.java.svn-base b/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/EditStoryActivity.java.svn-base new file mode 100644 index 0000000..0d5bad4 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/EditStoryActivity.java.svn-base @@ -0,0 +1,316 @@ +package com.loganlinn.pivotaltrackie.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ContentResolver; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import com.loganlinn.pivotaltracker.StoryEntry; +import com.loganlinn.pivotaltracker.Member.DefaultMemberQuery; +import com.loganlinn.pivotaltrackie.R; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Members; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.service.SyncService; +import com.loganlinn.pivotaltrackie.util.DetachableResultReceiver; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler; +import com.loganlinn.pivotaltrackie.util.SelectionBuilder; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler.AsyncQueryListener; + +//import com.loganlinn.pivotaltrackie.provider.ProjectContract; +public class EditStoryActivity extends Activity implements AsyncQueryListener, + DetachableResultReceiver.Receiver { + private static final String TAG = "EditStoryActivity"; + + private Spinner mEstimateSpinner; + private Spinner mStoryTypeSpinner; + private Spinner mRequestedBySpinner; + private Spinner mOwnedBySpinner; + private EditText mLabelsText; + private EditText mDescriptionText; + private EditText mNameText; + private TextView mTitle; + private int mProjectId; + private int mStoryId; + private StoryEntry mStoryEntry = null; + + private ArrayList mMembers; + private ActivityState mState; + private ContentResolver mContentResolver; + private NotifyingAsyncQueryHandler mQueryHandler; + private ArrayAdapter mMemberListAdapter; + private ArrayAdapter mEstimateAdapter; + private ArrayAdapter mStoryTypeAdapter; + private ArrayList mMemberList; + private ArrayList mEstimateList; + private ArrayList mStoryTypeList; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_edit_story); + + // Check for previous state + mState = (ActivityState) getLastNonConfigurationInstance(); + + final boolean previousState = (mState != null); + + if (previousState) { + mState.mReceiver.setReceiver(this); + } else { + mState = new ActivityState(); + mState.mReceiver.setReceiver(this); + } + + mContentResolver = getContentResolver(); + mQueryHandler = new NotifyingAsyncQueryHandler(mContentResolver, this); + + mStoryId = getIntent().getIntExtra(Stories.STORY_ID, Stories.NEW_STORY); + mProjectId = getIntent().getIntExtra(Stories.PROJECT_ID, -5); + Log.i(TAG, "Editing story "+mStoryId+" in project "+mProjectId); + final Bundle bundleData = getIntent().getExtras(); + if (bundleData != null) { + mStoryEntry = StoryEntry.fromBundle(bundleData); + mStoryEntry.sanitizeNullValues(); + Log.i(TAG, mStoryEntry.toString()); + + } + + // TODO: get point scheme from project + final int estimateValues = R.array.estimate123; + final int spinnerItem = android.R.layout.simple_spinner_item; + final Resources resources = this.getResources(); + + mEstimateList = new ArrayList(Arrays.asList(resources + .getStringArray(estimateValues))); + mStoryTypeList = new ArrayList(Arrays.asList(resources + .getStringArray(R.array.story_types))); + mMemberList = new ArrayList(); + mMemberList.add(""); // add empty value + + mEstimateAdapter = ArrayAdapter.createFromResource(this, + estimateValues, spinnerItem); + mStoryTypeAdapter = ArrayAdapter.createFromResource(this, + R.array.story_types, spinnerItem); + + mMemberListAdapter = new ArrayAdapter(this, spinnerItem, mMemberList); + + mStoryTypeAdapter + .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mEstimateAdapter + .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mMemberListAdapter + .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + mEstimateSpinner = (Spinner) findViewById(R.id.sp_estimate); + mEstimateSpinner.setAdapter(mEstimateAdapter); + mStoryTypeSpinner = (Spinner) findViewById(R.id.sp_story_type); + mStoryTypeSpinner.setAdapter(mStoryTypeAdapter); + + mRequestedBySpinner = (Spinner) findViewById(R.id.sp_requested_by); + mRequestedBySpinner.setAdapter(mMemberListAdapter); + mOwnedBySpinner = (Spinner) findViewById(R.id.sp_owned_by); + mOwnedBySpinner.setAdapter(mMemberListAdapter); + + mLabelsText = (EditText) findViewById(R.id.et_labels); + mDescriptionText = (EditText) findViewById(R.id.et_description); + mNameText = (EditText) findViewById(R.id.et_story_name); + + // When creating a new story, show a clear button instead of delete + // button + mTitle = (TextView) findViewById(R.id.tv_title_bar_title); + if (mStoryId == Stories.NEW_STORY) { + mTitle.setText(R.string.add_story); + ImageButton clearButton = (ImageButton) findViewById(R.id.ic_title_cancel); + ImageButton deleteButton = (ImageButton) findViewById(R.id.ic_title_delete); + deleteButton.setVisibility(ImageButton.INVISIBLE); + clearButton.setVisibility(ImageButton.VISIBLE); + ImageView delSeparator = (ImageView) findViewById(R.id.iv_delete_sep); + ImageView clrSeparator = (ImageView) findViewById(R.id.iv_clear_sep); + delSeparator.setVisibility(ImageView.INVISIBLE); + clrSeparator.setVisibility(ImageView.INVISIBLE); + + + + } else { + mTitle.setText(R.string.edit_story); + displayData(); + } + + } + + public void setSpinnerByValue(String value, List list, Spinner spinner) { + int position = list.indexOf(value); + if (position == -1 && "".equals(value) == false) { + Log.i(TAG, value + " not in list, adding & moving spinner to it"); + Log.i(TAG, list.toString()); + Log.i(TAG, spinner.toString()); + + position = list.size(); + list.add(value); + } + try{ + spinner.setSelection(position); + }catch(java.lang.IndexOutOfBoundsException e){ + e.printStackTrace(); + } + } + + public void onCancelClick(View v) { + finish(); + } + + public void onSaveClick(View v) { + Log.i(TAG, "Saving story for project " + mProjectId); + + stageData(); + Intent intent = new Intent(SyncService.ACTION_SYNC_STORY, null, this, + SyncService.class); + + Bundle storyData = mStoryEntry.toBundle(); + storyData.putBoolean(SyncService.SEND_TO_API, true); + intent.putExtras(storyData); + intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, mState.mReceiver); + intent.putExtra(Stories.PROJECT_ID, mProjectId); + intent.putExtra(Stories.STORY_ID, mStoryId); + startService(intent); + finish(); + } + + public void onDeleteClick(View v) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage("Are you sure you want to delete?").setCancelable( + false).setPositiveButton("Yes", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Intent intent = new Intent( + SyncService.ACTION_DELETE_STORY, null, EditStoryActivity.this, + SyncService.class); + + intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, + mState.mReceiver); + intent.putExtra(Stories.PROJECT_ID, mProjectId); + intent.putExtra(Stories.STORY_ID, mStoryId); + + startService(intent); + + EditStoryActivity.this.finish(); + } + }).setNegativeButton("No", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + AlertDialog alert = builder.create(); + alert.show(); + } + + public void stageData() { + mStoryEntry.put(Stories.STORY_ID, String.valueOf(mStoryId)); + mStoryEntry.put(Stories.NAME, mNameText.getText().toString()); + mStoryEntry.put(Stories.DESCRIPTION, mDescriptionText.getText() + .toString()); + mStoryEntry.put(Stories.LABELS, mLabelsText.getText().toString()); + String estimate = ((CharSequence) mEstimateSpinner + .getSelectedItem()).toString(); + estimate = estimate.replace(" points", ""); + mStoryEntry.put(Stories.ESTIMATE, estimate); + mStoryEntry.put(Stories.STORY_TYPE, ((CharSequence) mStoryTypeSpinner + .getSelectedItem()).toString()); + mStoryEntry.put(Stories.OWNED_BY, ((CharSequence) mOwnedBySpinner + .getSelectedItem()).toString()); + mStoryEntry.put(Stories.REQUESTED_BY, + ((CharSequence) mRequestedBySpinner.getSelectedItem()) + .toString()); + } + + public void displayData() { + + mNameText.setText(mStoryEntry.get(Stories.NAME)); + mDescriptionText.setText(mStoryEntry.get(Stories.DESCRIPTION)); + mLabelsText.setText(mStoryEntry.get(Stories.LABELS)); + setSpinnerByValue(mStoryEntry.get(Stories.REQUESTED_BY), mMemberList, + mRequestedBySpinner); + setSpinnerByValue(mStoryEntry.get(Stories.OWNED_BY), mMemberList, + mOwnedBySpinner); + + String estimate = mStoryEntry.get(Stories.ESTIMATE); + if(estimate.length() == 0){ + estimate = "0 points"; + }else{ + estimate += " points"; + } + Log.i(TAG, "ESTIMATE: "+estimate); + + setSpinnerByValue(estimate, mEstimateList, + mEstimateSpinner); + setSpinnerByValue(mStoryEntry.get(Stories.STORY_TYPE), mStoryTypeList, + mStoryTypeSpinner); + + } + + public void queryMembers() { + final Uri membersUri = Members.CONTENT_URI; + final SelectionBuilder builder = new SelectionBuilder(); + builder.where(Members.PROJECT_ID + "=?", String.valueOf(mProjectId)); + mQueryHandler.startQuery(membersUri, DefaultMemberQuery.PROJECTION); + } + + @Override + public void onQueryComplete(int token, Object cookie, Cursor cursor) { + if (cursor != null && cursor.moveToFirst()) { + do { + + mMembers.add(cursor.getString(DefaultMemberQuery.NAME)); + + } while (cursor.moveToNext()); + } + displayData(); + } + + @Override + public void onReceiveResult(int resultCode, Bundle resultData) { + Log.v(TAG, "Received Result code=" + resultCode); + Log.v(TAG, resultData.keySet().toString()); + + switch (resultCode) { + case SyncService.STATUS_RUNNING: { + mState.mSyncing = true; + break; + } + case SyncService.STATUS_FINISHED: { + mState.mSyncing = false; + Toast.makeText(EditStoryActivity.this, "Save sucessful", + Toast.LENGTH_LONG).show(); + break; + } + case SyncService.STATUS_ERROR: { + mState.mSyncing = false; + final String errorText = getString(R.string.toast_sync_error, + resultData.getStringArray(Intent.EXTRA_TEXT)); + Toast + .makeText(EditStoryActivity.this, errorText, + Toast.LENGTH_LONG).show(); + } + } + } + +} diff --git a/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/HomeActivity.java.svn-base b/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/HomeActivity.java.svn-base new file mode 100644 index 0000000..864b537 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/HomeActivity.java.svn-base @@ -0,0 +1,308 @@ +package com.loganlinn.pivotaltrackie.ui; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.AdapterView.OnItemClickListener; + +import com.loganlinn.pivotaltracker.PivotalTracker; +import com.loganlinn.pivotaltracker.Project.DefaultProjectsQuery; +import com.loganlinn.pivotaltrackie.R; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Projects; +import com.loganlinn.pivotaltrackie.service.SyncService; +import com.loganlinn.pivotaltrackie.util.DateTimeUtils; +import com.loganlinn.pivotaltrackie.util.DetachableResultReceiver; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler.AsyncQueryListener; + +public class HomeActivity extends Activity implements AsyncQueryListener, + DetachableResultReceiver.Receiver, OnItemClickListener { + private static final String TAG = "HomeActivity"; + + private ActivityState mState; + private Handler mMessageHandler = new Handler(); + private NotifyingAsyncQueryHandler mQueryHandler; + + public ProjectListAdapter mListAdapter; + protected ListView mListView; + protected List mProjectList; + + private ContentResolver mContentResolver; + private final SimpleDateFormat mDatetimeFormat = PivotalTracker + .getDateFormat(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (showLoginIfNoTokenExists()) { + return; + } + + setContentView(R.layout.activity_home); + + mState = (ActivityState) getLastNonConfigurationInstance(); + final boolean previousState = (mState != null); + + if (previousState) { + mState.mReceiver.setReceiver(this); + updateRefreshStatus(); + // reloadProjects(true); + } else { + mState = new ActivityState(); + mState.mReceiver.setReceiver(this); + onRefreshClick(null); + } + mContentResolver = getContentResolver(); + // Set up handler for now project list query + mQueryHandler = new NotifyingAsyncQueryHandler(mContentResolver, this); + + mListView = (ListView) findViewById(R.id.lv_home); + mListView.setOnItemClickListener(this); + mListAdapter = new ProjectListAdapter(this); + mProjectList = new ArrayList(); + + mListView.setAdapter(mListAdapter); + + } + + private boolean showLoginIfNoTokenExists() { + + String token = PivotalTracker.getToken(getBaseContext()); + + if (token == null) { + Log.i(TAG, "No token found, showing login activity"); + Intent intent = new Intent(this, LoginActivity.class); + startActivity(intent); + return true; + } + return false; + } + + /* (non-Javadoc) + * @see android.app.Activity#onResume() + */ + @Override + protected void onResume() { + updateRefreshStatus(); + super.onResume(); + } + + @Override + public void onQueryComplete(int token, Object cookie, Cursor cursor) { + if (cursor == null) + return; + try { + if (cursor.moveToFirst()) { + mProjectList.clear(); + do { + + final Object[] projectData = { + cursor.getInt(DefaultProjectsQuery.PROJECT_ID), + cursor.getString(DefaultProjectsQuery.NAME), + cursor + .getInt(DefaultProjectsQuery.CURRENT_VELOCITY), + cursor + .getString(DefaultProjectsQuery.LAST_ACTIVITY_AT) }; + Log.v(TAG, "Adding Project, " + projectData[1] + + ", to list"); + mProjectList.add(projectData); + } while (cursor.moveToNext()); + + mListAdapter.notifyDataSetChanged(); + } else { + onNoProjectsToDisplay(); + } + } finally { + cursor.close(); + } + } + + private void updateRefreshStatus() { + // TODO: if syncing, show animation, otherwise show refresh button + findViewById(R.id.btn_title_refresh).setVisibility( + mState.mSyncing ? View.GONE : View.VISIBLE); + findViewById(R.id.title_refresh_progress).setVisibility( + mState.mSyncing ? View.VISIBLE : View.GONE); + } + + @Override + public Object onRetainNonConfigurationInstance() { + // Clear any strong references to this Activity, we'll reattach to + // handle events on the other side. + mState.mReceiver.clearReceiver(); + return mState; + } + + @Override + public void onReceiveResult(int resultCode, Bundle resultData) { + Log.v(TAG, "Received Result code=" + resultCode); + Log.v(TAG, resultData.keySet().toString()); + + switch (resultCode) { + case SyncService.STATUS_RUNNING: { + mState.mSyncing = true; + updateRefreshStatus(); + break; + } + case SyncService.STATUS_FINISHED: { + mState.mSyncing = false; + updateRefreshStatus(); + queryProjects(); + // TODO: reload with results + break; + } + case SyncService.STATUS_ERROR: { + mState.mSyncing = false; + updateRefreshStatus(); + final String errorText = getString(R.string.toast_sync_error, + resultData.getStringArray(Intent.EXTRA_TEXT)); + Toast.makeText(HomeActivity.this, errorText, Toast.LENGTH_LONG) + .show(); + } + } + } + + private void onNoProjectsToDisplay() { + Toast.makeText(HomeActivity.this, "No projects found!", + Toast.LENGTH_SHORT).show(); + } + + private void queryProjects() { + final Uri uri = Projects.CONTENT_URI; + mQueryHandler.startQuery(uri, DefaultProjectsQuery.PROJECTION, null, + null, null); + + } + + /** Handle "refresh" title-bar action. */ + public void onRefreshClick(View v) { + // trigger off background sync + final Intent intent = new Intent(SyncService.ACTION_SYNC_PROJECTS, + null, this, SyncService.class); + intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, mState.mReceiver); + startService(intent); + + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, + long id) { + final Object[] projectData = mProjectList.get(position); + final Integer projectId = (Integer) projectData[DefaultProjectsQuery.PROJECT_ID]; + final String projectName = (String) projectData[DefaultProjectsQuery.NAME]; + + final Intent intent = new Intent(getBaseContext(), + ProjectActivity.class); + intent.putExtra(Projects.PROJECT_ID, projectId); + intent.putExtra(Projects.NAME, projectName); + startActivity(intent); + } + + private class ProjectListAdapter extends BaseAdapter { + private LayoutInflater mInflater; + + public ProjectListAdapter(Context context) { + mInflater = LayoutInflater.from(context); + + } + + @Override + public int getCount() { + return mProjectList.size(); + } + + @Override + public Object getItem(int position) { + return mProjectList.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int pos, View convertView, ViewGroup parent) { + // A ViewHolder keeps references to children views to avoid + // unneccessary calls + // to findViewById() on each row. + ViewHolder holder; + // When convertView is not null, we can reuse it directly, there is + // no need + // to reinflate it. We only inflate a new View when the convertView + // supplied + // by ListView is null. + + if (convertView == null) { + convertView = mInflater.inflate(R.layout.list_item_project, + null); + holder = new ViewHolder(); + holder.mProjectName = (TextView) convertView + .findViewById(R.id.tv_project_name); + holder.mLastActivity = (TextView) convertView + .findViewById(R.id.tv_project_last_activity); + holder.mCurrentVelocity = (TextView) convertView + .findViewById(R.id.tv_project_velocity); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + final Object[] projectData = mProjectList.get(pos); + String lastActivity = (String) projectData[DefaultProjectsQuery.LAST_ACTIVITY_AT]; + if (lastActivity != null) { + try { + Log.i(TAG, lastActivity); + lastActivity = "2010/12/06 14:34:29 UTC"; + + final Date d = mDatetimeFormat.parse(lastActivity); + long updated = d.getTime(); + lastActivity = "Last activity " + + DateTimeUtils.getInstance( + convertView.getContext()) + .getTimeDiffString(updated).toLowerCase(); + } catch (ParseException e) { + e.printStackTrace(); + } + }else{ + lastActivity = ""; + } + holder.mProjectName + .setText((CharSequence) projectData[DefaultProjectsQuery.NAME]); + holder.mLastActivity.setText(lastActivity); + holder.mCurrentVelocity + .setText(String + .valueOf(projectData[DefaultProjectsQuery.CURRENT_VELOCITY])); + + return convertView; + } + + private class ViewHolder { + TextView mProjectName; + TextView mLastActivity; + TextView mCurrentVelocity; + } + + } +} diff --git a/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/LoginActivity.java.svn-base b/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/LoginActivity.java.svn-base new file mode 100644 index 0000000..5852fd1 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/LoginActivity.java.svn-base @@ -0,0 +1,53 @@ +package com.loganlinn.pivotaltrackie.ui; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import com.loganlinn.pivotaltracker.PivotalTrackerError; +import com.loganlinn.pivotaltracker.PivotalTrackerListener; +import com.loganlinn.pivotaltracker.PivotalTracker; +import com.loganlinn.pivotaltrackie.R; +import com.loganlinn.pivotaltrackie.util.DetachableResultReceiver; + +public class LoginActivity extends Activity implements PivotalTrackerListener { + private static final String TAG = "LoginActivity"; + private Button submitButton_; + private EditText usernameText_; + private EditText passwordText_; + private PivotalTracker mPivotalTracker; + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + + mPivotalTracker = PivotalTracker.getInstance(getBaseContext()); + submitButton_ = (Button) findViewById(R.id.btn_login); + usernameText_ = (EditText) findViewById(R.id.et_username); + passwordText_ = (EditText) findViewById(R.id.et_password); + + } + + public void onSubmitClick(View v){ + + final String username = usernameText_.getText().toString(); + final String password = passwordText_.getText().toString(); + + mPivotalTracker.fetchToken(username, password, this); + + } + + @Override + public void onError(PivotalTrackerError error) { + Toast.makeText(getBaseContext(), "Could not authenticate", Toast.LENGTH_LONG); + } + + @Override + public void onSuccess(String token) { + finish(); + } +} \ No newline at end of file diff --git a/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/ProjectActivity.java.svn-base b/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/ProjectActivity.java.svn-base new file mode 100644 index 0000000..0133731 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/ProjectActivity.java.svn-base @@ -0,0 +1,380 @@ +package com.loganlinn.pivotaltrackie.ui; + +import java.util.List; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.GestureDetector; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnTouchListener; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.ViewFlipper; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; + +import com.loganlinn.pivotaltracker.StoryEntry; +import com.loganlinn.pivotaltracker.Story.DefaultStoriesQuery; +import com.loganlinn.pivotaltrackie.R; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Projects; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.service.SyncService; +import com.loganlinn.pivotaltrackie.util.DetachableResultReceiver; +import com.loganlinn.pivotaltrackie.util.FlingGestureListener; +import com.loganlinn.pivotaltrackie.util.Lists; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler; +import com.loganlinn.pivotaltrackie.util.SelectionBuilder; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler.AsyncQueryListener; + +public class ProjectActivity extends Activity implements AsyncQueryListener, + DetachableResultReceiver.Receiver, OnItemClickListener, + OnItemLongClickListener { + private static final String TAG = "ProjectActivity"; + private static final Integer EMPTY_PROJECT_ID = -1; + + private List[] mStoryList; + private ListView[] mListView; + private StoryListAdapter[] mListAdapter; + private ActivityState mState; + private ContentResolver mContentResolver; + private NotifyingAsyncQueryHandler mQueryHandler; + private Handler mMessageHandler = new Handler(); + private Integer mProjectId; + + private ViewFlipper mListViewFlipper; + private GestureDetector mGestureDetector; + private FlingGestureListener mGestureListener; + + private TextView mTitle; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_project); + + mState = (ActivityState) getLastNonConfigurationInstance(); + final boolean previousState = (mState != null); + + if (previousState) { + mState.mReceiver.setReceiver(this); + updateRefreshStatus(); + reloadStories(true); + } else { + mProjectId = getIntent().getIntExtra(Projects.PROJECT_ID, + EMPTY_PROJECT_ID); + final String projectName = getIntent().getStringExtra(Projects.NAME); + if(projectName != null){ + mTitle = (TextView) findViewById(R.id.tv_title_bar_title); + mTitle.setText(projectName); + + } + Log.i(TAG, "Initializing stories for project id=" + mProjectId); + mState = new ActivityState(); + mState.mReceiver.setReceiver(this); + onRefreshClick(null); + } + mContentResolver = getContentResolver(); + + mQueryHandler = new NotifyingAsyncQueryHandler(mContentResolver, this); + + mTitle = (TextView) findViewById(R.id.tv_title_bar_title); + mStoryList = new List[2]; + mStoryList[0] = Lists.newArrayList(); + //mStoryList[1] = Lists.newArrayList(); + + mListView = new ListView[2]; + mListView[0] = (ListView) findViewById(R.id.lv_stories0); + //mListView[1] = (ListView) findViewById(R.id.lv_stories1); + mListView[0].setOnItemClickListener(this); + mListView[0].setOnItemLongClickListener(this); + //mListView[1].setOnItemClickListener(this); + //mListView[1].setOnItemLongClickListener(this); + mListAdapter = new StoryListAdapter[2]; + + mListAdapter[0] = new StoryListAdapter(this, mStoryList[0]); + mListView[0].setAdapter(mListAdapter[0]); + + //mListAdapter[1] = new StoryListAdapter(this, mStoryList[1]); + //mListView[1].setAdapter(mListAdapter[1]); + + mListViewFlipper = (ViewFlipper) findViewById(R.id.vf_stories_flipper); + + mGestureListener = new FlingGestureListener(mListViewFlipper); + mGestureDetector = new GestureDetector(mGestureListener); + + OnTouchListener emptyTouchListener = new OnTouchListener(){ + //Used to padd touch event to parent, where touch event is handled + @Override + public boolean onTouch(View v, MotionEvent event) { + return mGestureDetector.onTouchEvent(event); + } + }; + mListView[0].setOnTouchListener(emptyTouchListener); + //mListView[1].setOnTouchListener(emptyTouchListener); + queryStories(); + } + private void updateTitle(){ + switch(mGestureListener.mCurrentFlipperIndex){ + + } + } + private void reloadStories(boolean forceRefresh) { + queryStories(); + } + + private void queryStories() { + final Uri uri = Stories.CONTENT_URI; + final SelectionBuilder builder = new SelectionBuilder(); + + builder.where(Stories.PROJECT_ID + "=?", mProjectId.toString()); + + + //Start the query, receive response in onQueryComplete method + mQueryHandler.startQuery(uri, + DefaultStoriesQuery.PROJECTION, builder.getSelection(), builder + .getSelectionArgs(), null); + + } + + @Override + public void onQueryComplete(int token, Object cookie, Cursor cursor) { + //TODO: Split up stories based on iteration + //TODO: Display in UI that there are multiple lists + if(cursor == null) return; + try { + if (cursor.moveToFirst()) { + mStoryList[0].clear(); + //mStoryList[1].clear(); + int i = 0; + do { + final StoryEntry story = StoryEntry.fromCursor(cursor); + //SPLIT the lists + mStoryList[0].add(story); + + } while (cursor.moveToNext()); + mListAdapter[0].notifyDataSetChanged(); + //mListAdapter[1].notifyDataSetChanged(); + } else { + Log.i(TAG, "No stories found in database for project"); + } + + } finally { + cursor.close(); + } + + } + /* (non-Javadoc) + * @see android.app.Activity#onTouchEvent(android.view.MotionEvent) + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + return mGestureDetector.onTouchEvent(event); + } + + /** Handle "refresh" title-bar action. */ + public void onRefreshClick(View v) { + // trigger off background sync + if (mProjectId == EMPTY_PROJECT_ID) { + Log.e(TAG, "Project ID unspecified. Cannot refresh"); + finish(); + } + final Intent intent = new Intent(SyncService.ACTION_SYNC_STORIES, + Projects.buildProjectUri(mProjectId), this, SyncService.class); + intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, mState.mReceiver); + startService(intent); + } + + private void updateRefreshStatus() { + // TODO: if syncing, show animation, otherwise show refresh button + findViewById(R.id.btn_title_refresh).setVisibility( + mState.mSyncing ? View.GONE : View.VISIBLE); + findViewById(R.id.title_refresh_progress).setVisibility( + mState.mSyncing ? View.VISIBLE : View.GONE); + } + + public void onNewStoryClick(View v){ + final Intent intent = new Intent(this, EditStoryActivity.class); + intent.putExtra(Stories.PROJECT_ID, mProjectId); + intent.putExtra(Stories.STORY_ID, Stories.NEW_STORY); + startActivity(intent); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, + long id) { + Log.i(TAG,"Clicked story at position "+position); + final ListView listView = (ListView) view.getParent(); + final ListAdapter listAdapter = listView.getAdapter(); + final StoryEntry entry = (StoryEntry) listAdapter.getItem(position); //mStoryList[mGestureListener.mCurrentFlipperIndex].get(position); + + Intent intent = new Intent(this, ViewStoryActivity.class); + + final int storyId = Integer.valueOf(entry.get(Stories.STORY_ID)); + intent.putExtra(Stories.STORY_ID, storyId); + intent.putExtra(Stories.PROJECT_ID, mProjectId); + startActivity(intent); + } + + /* (non-Javadoc) + * @see android.app.Activity#onResume() + */ + @Override + protected void onResume() { + // TODO Auto-generated method stub + onRefreshClick(null); + super.onResume(); + } + /** + * TODO: Show story menu + */ + @Override + public boolean onItemLongClick(AdapterView parent, View view, + int position, long id) { + + return true; + } + + private class StoryListAdapter extends BaseAdapter { + private LayoutInflater mInflater; + private List mList; + public StoryListAdapter(Context context, List list) { + mInflater = LayoutInflater.from(context); + mList = list; + } + + @Override + public int getCount() { + return mList.size(); + } + + @Override + public Object getItem(int position) { + return mList.get(position); + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + + if (convertView == null) { + convertView = mInflater.inflate(R.layout.list_item_story, null); + holder = new ViewHolder(); + holder.mName = (TextView) convertView + .findViewById(R.id.tv_story_name); + holder.mEstimate = (TextView) convertView + .findViewById(R.id.tv_story_estimate); + holder.mTypeIcon = (ImageView) convertView.findViewById(R.id.iv_story_type); + convertView.setTag(holder); + + } else { + holder = (ViewHolder) convertView.getTag(); + } + + final StoryEntry story = (StoryEntry) getItem(position); + final String estimate = story.get(Stories.ESTIMATE); + holder.mName.setText(story.get(Stories.NAME)); + + if ("-1".equals(estimate)) { + holder.mEstimate.setText(""); + } else { + holder.mEstimate.setText(estimate); + } + + final String currentStatus = story.get(Stories.CURRENT_STATE); + + //Set the background color of the story depending on its status + if(Stories.CURRENT_STATUS_ACCEPTED.equals(currentStatus)){ + convertView.setBackgroundResource(R.color.story_accepted); + }else if(Stories.CURRENT_STATUS_DELIVERED.equals(currentStatus)){ + convertView.setBackgroundResource(R.color.story_delivered); + }else if(Stories.CURRENT_STATUS_STARTED.equals(currentStatus)){ + convertView.setBackgroundResource(R.color.story_started); + }else if(Stories.CURRENT_STATUS_FINISHED.equals(currentStatus)){ + convertView.setBackgroundResource(R.color.story_finished); + }else{ // CURRENT_STATUS_UNSCHEDULED + convertView.setBackgroundResource(R.color.story_unscheduled); + } + + final String storyType = story.get(Stories.STORY_TYPE); + + if(Stories.STORY_TYPE_BUG.equals(storyType)){ + holder.mTypeIcon.setImageResource(R.drawable.bug_icon); + holder.mTypeIcon.setVisibility(ImageView.VISIBLE); + }else if(Stories.STORY_TYPE_FEATURE.equals(storyType)){ + holder.mTypeIcon.setImageResource(R.drawable.feature); + holder.mTypeIcon.setVisibility(ImageView.VISIBLE); + }else if(Stories.STORY_TYPE_CHORE.equals(storyType)){ + holder.mTypeIcon.setImageResource(R.drawable.chore_icon); + holder.mTypeIcon.setVisibility(ImageView.VISIBLE); + }else{ + holder.mTypeIcon.setVisibility(ImageView.INVISIBLE); + } + + return convertView; + } + + private class ViewHolder { + TextView mName; + TextView mEstimate; + ImageView mTypeIcon; + } + } + + @Override + public Object onRetainNonConfigurationInstance() { + // Clear any strong references to this Activity, we'll reattach to + // handle events on the other side. + mState.mReceiver.clearReceiver(); + return mState; + } + + @Override + public void onReceiveResult(int resultCode, Bundle resultData) { + Log.v(TAG, "Received Result code=" + resultCode); + Log.v(TAG, resultData.keySet().toString()); + + switch (resultCode) { + case SyncService.STATUS_RUNNING: { + mState.mSyncing = true; + updateRefreshStatus(); + break; + } + case SyncService.STATUS_FINISHED: { + mState.mSyncing = false; + updateRefreshStatus(); + reloadStories(false); + // TODO: reload with results + break; + } + case SyncService.STATUS_ERROR: { + mState.mSyncing = false; + updateRefreshStatus(); + final String errorText = getString(R.string.toast_sync_error, + resultData.getStringArray(Intent.EXTRA_TEXT)); + Toast.makeText(ProjectActivity.this, errorText, Toast.LENGTH_LONG) + .show(); + } + } + + } + +} diff --git a/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/ViewStoryActivity.java.svn-base b/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/ViewStoryActivity.java.svn-base new file mode 100644 index 0000000..67bf381 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/ViewStoryActivity.java.svn-base @@ -0,0 +1,245 @@ +package com.loganlinn.pivotaltrackie.ui; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import com.loganlinn.pivotaltracker.StoryEntry; +import com.loganlinn.pivotaltracker.Story.DetailStoriesQuery; +import com.loganlinn.pivotaltrackie.R; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Projects; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.service.SyncService; +import com.loganlinn.pivotaltrackie.util.DetachableResultReceiver; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler; +import com.loganlinn.pivotaltrackie.util.SelectionBuilder; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler.AsyncQueryListener; + +public class ViewStoryActivity extends Activity implements AsyncQueryListener, + DetachableResultReceiver.Receiver { + private static final String TAG = "ViewStoryActivity"; + private TextView mStoryName; + private TextView mRequestedBy; + private TextView mOwnedBy; + private TextView mAcceptedAt; + private TextView mCreatedAt; + private TextView mCurrentState; + private TextView mDescription; + private TextView mLabels; + private TextView mStoryType; + private TextView mEstimate; + + private Integer mStoryId; + private Integer mProjectId; + + private StoryEntry mStoryEntry; + + private ActivityState mState; + private ContentResolver mContentResolver; + private NotifyingAsyncQueryHandler mQueryHandler; + private Handler mMessageHandler = new Handler(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_story); + // Get intent data + mStoryId = getIntent().getIntExtra(Stories.STORY_ID, -1); + mProjectId = getIntent().getIntExtra(Stories.PROJECT_ID, -1); + + if (mStoryId == -1 || mProjectId == -1) { + Log.e(TAG, "Need story_id and project id,leaving activity"); + + finish(); + } + // Check for previous state + mState = (ActivityState) getLastNonConfigurationInstance(); + + final boolean previousState = (mState != null); + + if (previousState) { + mState.mReceiver.setReceiver(this); + updateRefreshStatus(); + } else { + mState = new ActivityState(); + mState.mReceiver.setReceiver(this); + onRefreshClick(null); + } + mContentResolver = getContentResolver(); + mQueryHandler = new NotifyingAsyncQueryHandler(mContentResolver, this); + + mStoryName = (TextView) findViewById(R.id.tv_story_name); + mRequestedBy = (TextView) findViewById(R.id.tv_story_requested_by); + mOwnedBy = (TextView) findViewById(R.id.tv_story_owned_by); + mAcceptedAt = (TextView) findViewById(R.id.tv_story_accepted_at); + mCreatedAt = (TextView) findViewById(R.id.tv_story_created_at); + mCurrentState = (TextView) findViewById(R.id.tv_story_current_state); + mDescription = (TextView) findViewById(R.id.tv_story_description); + mLabels = (TextView) findViewById(R.id.tv_story_labels); + mStoryType = (TextView) findViewById(R.id.tv_story_type); + mEstimate = (TextView) findViewById(R.id.tv_story_estimate); + + queryStory(); + } + + private void reloadStory(boolean force) { + if (force) { // if forced, do the query. the async listener (this) will + // return back here + queryStory(); + return; + } + mStoryEntry.sanitizeNullValues(); + final String storyName = mStoryEntry.get(Stories.NAME); + final String storyType = mStoryEntry.get(Stories.STORY_TYPE); + final String requestedBy = mStoryEntry.get(Stories.REQUESTED_BY); + final String ownedBy = mStoryEntry.get(Stories.OWNED_BY); + final String acceptedAt = mStoryEntry.get(Stories.ACCEPTED_AT); + final String createdAt = mStoryEntry.get(Stories.CREATED_AT); + final String currentState = mStoryEntry.get(Stories.CURRENT_STATE); + String description = mStoryEntry.get(Stories.DESCRIPTION); + final String labels = mStoryEntry.get(Stories.LABELS); + String estimate = mStoryEntry.get(Stories.ESTIMATE); + + if(estimate.length() == 0){ + estimate = "0 points"; + }else{ + estimate += " points"; + } + + + Log.i(TAG, "Displaying story: " + storyName); + + mEstimate.setText(estimate); + mStoryType.setText(storyType); + mStoryName.setText(storyName); + mRequestedBy.setText(requestedBy); + mOwnedBy.setText(ownedBy); + mAcceptedAt.setText(acceptedAt); + mCreatedAt.setText(createdAt); + mCurrentState.setText(currentState); + mDescription.setText(description); + mLabels.setText(labels); + } + + + /* + * (non-Javadoc) + * + * @see android.app.Activity#onResume() + */ + @Override + protected void onResume() { + updateRefreshStatus(); + queryStory(); + super.onResume(); + } + + private void updateRefreshStatus() { + // TODO: if syncing, show animation, otherwise show refresh button + findViewById(R.id.btn_title_refresh).setVisibility( + mState.mSyncing ? View.GONE : View.VISIBLE); + findViewById(R.id.title_refresh_progress).setVisibility( + mState.mSyncing ? View.VISIBLE : View.GONE); + } + + private void queryStory() { + final String storyId = mStoryId.toString(); + final Uri uri = Stories.buildStoryUri(storyId); + + final SelectionBuilder builder = new SelectionBuilder(); + builder.where(Stories.STORY_ID + "=?", storyId); + + mQueryHandler.startQuery(uri, DetailStoriesQuery.PROJECTION, builder + .getSelection(), builder.getSelectionArgs(), null); + + } + + // Called when the edit button on the toolbar is clicked + public void onEditClick(View v) { + final Intent intent = new Intent(this, EditStoryActivity.class); + + intent.putExtras(mStoryEntry.toBundle()); + intent.putExtra(Stories.STORY_ID, mStoryId); // ensure the intent is + // passed an integer + intent.putExtra(Stories.PROJECT_ID, mProjectId); + startActivity(intent); + + } + + // Called when the refresh button on toolbar is clicked + public void onRefreshClick(View v) { + final Intent intent = new Intent(SyncService.ACTION_SYNC_STORY, Stories + .buildStoryUri(mStoryId), this, SyncService.class); + + intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, mState.mReceiver); + intent.putExtra(Stories.PROJECT_ID, mProjectId); + intent.putExtra(Stories.STORY_ID, mStoryId); + + startService(intent); + } + + @Override + public void onQueryComplete(int token, Object cookie, Cursor cursor) { + if (cursor == null) + return; + try { + if (cursor.moveToFirst()) { + mStoryEntry = StoryEntry.fromCursor(cursor, + DetailStoriesQuery.PROJECTION); + } + } finally { + cursor.close(); + } + reloadStory(false); + Log.i(TAG, mStoryEntry.toString()); + } + + @Override + public void onReceiveResult(int resultCode, Bundle resultData) { + Log.v(TAG, "Received Result code=" + resultCode); + Log.v(TAG, resultData.keySet().toString()); + + switch (resultCode) { + case SyncService.STATUS_RUNNING: { + mState.mSyncing = true; + updateRefreshStatus(); + break; + } + case SyncService.STATUS_FINISHED: { + mState.mSyncing = false; + updateRefreshStatus(); + reloadStory(true); + // TODO: reload with results + break; + } + case SyncService.STATUS_ERROR: { + mState.mSyncing = false; + updateRefreshStatus(); + reloadStory(true); + final String errorText = getString(R.string.toast_sync_error, + resultData.getStringArray(Intent.EXTRA_TEXT)); + Toast + .makeText(ViewStoryActivity.this, errorText, + Toast.LENGTH_LONG).show(); + } + } + + } + + @Override + public Object onRetainNonConfigurationInstance() { + // Clear any strong references to this Activity, we'll reattach to + // handle events on the other side. + mState.mReceiver.clearReceiver(); + return mState; + } +} diff --git a/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/XMLTokenHandler.java.svn-base b/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/XMLTokenHandler.java.svn-base new file mode 100644 index 0000000..d9f51b0 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/.svn/text-base/XMLTokenHandler.java.svn-base @@ -0,0 +1,70 @@ +package com.loganlinn.pivotaltrackie.ui; + +import java.io.IOException; +import java.io.InputStream; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +import android.util.Log; + +public class XMLTokenHandler extends DefaultHandler { + + private static final String TAG = "XMLTokenHandler"; + + private StringBuilder token = new StringBuilder(); + private String currentElementName=""; + + public void startElement(String uri, String name, String qName, Attributes attr) { + currentElementName = name.trim(); + } + + public void endElement(String uri, String name, String qName) throws SAXException { + currentElementName = ""; + } + + public void characters(char ch[], int start, int length) { + + String chars = (new String(ch).substring(start, start + length)); + + if (currentElementName.equalsIgnoreCase("guid")) { + token.append(chars); + } + } + + public String getTokenAndClose(InputStream in) { + try { + parse(in); + in.close(); + return token.toString(); + } catch (ParserConfigurationException e) { + Log.e(TAG,"ParserConfigurationException "+e.getMessage()); + throw new RuntimeException("ParserConfigurationException "+e.getMessage(),e); + } catch (SAXException e) { + Log.e(TAG,"SAXException "+e.getMessage()); + throw new RuntimeException("SAXException "+e.getMessage(),e); + } catch (IOException e) { + Log.e(TAG,"IOException "+e.getMessage()); + throw new RuntimeException("IOException "+e.getMessage(),e); + } + } + + private void parse(InputStream in) throws ParserConfigurationException, SAXException, IOException { + SAXParserFactory spf = SAXParserFactory.newInstance(); + SAXParser sp; + + sp = spf.newSAXParser(); + XMLReader xr = sp.getXMLReader(); + xr.setContentHandler(this); + InputSource is = new InputSource(in); + xr.parse(is); + + } +} diff --git a/src/com/loganlinn/pivotaltrackie/ui/ActivityState.java b/src/com/loganlinn/pivotaltrackie/ui/ActivityState.java new file mode 100644 index 0000000..8fe607e --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/ActivityState.java @@ -0,0 +1,14 @@ +package com.loganlinn.pivotaltrackie.ui; + +import android.os.Handler; + +import com.loganlinn.pivotaltrackie.util.DetachableResultReceiver; + +class ActivityState { + public DetachableResultReceiver mReceiver; + public boolean mSyncing = false; + + ActivityState() { + mReceiver = new DetachableResultReceiver(new Handler()); + } +} diff --git a/src/com/loganlinn/pivotaltrackie/ui/EditStoryActivity.java b/src/com/loganlinn/pivotaltrackie/ui/EditStoryActivity.java new file mode 100644 index 0000000..0d5bad4 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/EditStoryActivity.java @@ -0,0 +1,316 @@ +package com.loganlinn.pivotaltrackie.ui; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ContentResolver; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.res.Resources; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.ArrayAdapter; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import com.loganlinn.pivotaltracker.StoryEntry; +import com.loganlinn.pivotaltracker.Member.DefaultMemberQuery; +import com.loganlinn.pivotaltrackie.R; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Members; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.service.SyncService; +import com.loganlinn.pivotaltrackie.util.DetachableResultReceiver; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler; +import com.loganlinn.pivotaltrackie.util.SelectionBuilder; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler.AsyncQueryListener; + +//import com.loganlinn.pivotaltrackie.provider.ProjectContract; +public class EditStoryActivity extends Activity implements AsyncQueryListener, + DetachableResultReceiver.Receiver { + private static final String TAG = "EditStoryActivity"; + + private Spinner mEstimateSpinner; + private Spinner mStoryTypeSpinner; + private Spinner mRequestedBySpinner; + private Spinner mOwnedBySpinner; + private EditText mLabelsText; + private EditText mDescriptionText; + private EditText mNameText; + private TextView mTitle; + private int mProjectId; + private int mStoryId; + private StoryEntry mStoryEntry = null; + + private ArrayList mMembers; + private ActivityState mState; + private ContentResolver mContentResolver; + private NotifyingAsyncQueryHandler mQueryHandler; + private ArrayAdapter mMemberListAdapter; + private ArrayAdapter mEstimateAdapter; + private ArrayAdapter mStoryTypeAdapter; + private ArrayList mMemberList; + private ArrayList mEstimateList; + private ArrayList mStoryTypeList; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_edit_story); + + // Check for previous state + mState = (ActivityState) getLastNonConfigurationInstance(); + + final boolean previousState = (mState != null); + + if (previousState) { + mState.mReceiver.setReceiver(this); + } else { + mState = new ActivityState(); + mState.mReceiver.setReceiver(this); + } + + mContentResolver = getContentResolver(); + mQueryHandler = new NotifyingAsyncQueryHandler(mContentResolver, this); + + mStoryId = getIntent().getIntExtra(Stories.STORY_ID, Stories.NEW_STORY); + mProjectId = getIntent().getIntExtra(Stories.PROJECT_ID, -5); + Log.i(TAG, "Editing story "+mStoryId+" in project "+mProjectId); + final Bundle bundleData = getIntent().getExtras(); + if (bundleData != null) { + mStoryEntry = StoryEntry.fromBundle(bundleData); + mStoryEntry.sanitizeNullValues(); + Log.i(TAG, mStoryEntry.toString()); + + } + + // TODO: get point scheme from project + final int estimateValues = R.array.estimate123; + final int spinnerItem = android.R.layout.simple_spinner_item; + final Resources resources = this.getResources(); + + mEstimateList = new ArrayList(Arrays.asList(resources + .getStringArray(estimateValues))); + mStoryTypeList = new ArrayList(Arrays.asList(resources + .getStringArray(R.array.story_types))); + mMemberList = new ArrayList(); + mMemberList.add(""); // add empty value + + mEstimateAdapter = ArrayAdapter.createFromResource(this, + estimateValues, spinnerItem); + mStoryTypeAdapter = ArrayAdapter.createFromResource(this, + R.array.story_types, spinnerItem); + + mMemberListAdapter = new ArrayAdapter(this, spinnerItem, mMemberList); + + mStoryTypeAdapter + .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mEstimateAdapter + .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + mMemberListAdapter + .setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + + mEstimateSpinner = (Spinner) findViewById(R.id.sp_estimate); + mEstimateSpinner.setAdapter(mEstimateAdapter); + mStoryTypeSpinner = (Spinner) findViewById(R.id.sp_story_type); + mStoryTypeSpinner.setAdapter(mStoryTypeAdapter); + + mRequestedBySpinner = (Spinner) findViewById(R.id.sp_requested_by); + mRequestedBySpinner.setAdapter(mMemberListAdapter); + mOwnedBySpinner = (Spinner) findViewById(R.id.sp_owned_by); + mOwnedBySpinner.setAdapter(mMemberListAdapter); + + mLabelsText = (EditText) findViewById(R.id.et_labels); + mDescriptionText = (EditText) findViewById(R.id.et_description); + mNameText = (EditText) findViewById(R.id.et_story_name); + + // When creating a new story, show a clear button instead of delete + // button + mTitle = (TextView) findViewById(R.id.tv_title_bar_title); + if (mStoryId == Stories.NEW_STORY) { + mTitle.setText(R.string.add_story); + ImageButton clearButton = (ImageButton) findViewById(R.id.ic_title_cancel); + ImageButton deleteButton = (ImageButton) findViewById(R.id.ic_title_delete); + deleteButton.setVisibility(ImageButton.INVISIBLE); + clearButton.setVisibility(ImageButton.VISIBLE); + ImageView delSeparator = (ImageView) findViewById(R.id.iv_delete_sep); + ImageView clrSeparator = (ImageView) findViewById(R.id.iv_clear_sep); + delSeparator.setVisibility(ImageView.INVISIBLE); + clrSeparator.setVisibility(ImageView.INVISIBLE); + + + + } else { + mTitle.setText(R.string.edit_story); + displayData(); + } + + } + + public void setSpinnerByValue(String value, List list, Spinner spinner) { + int position = list.indexOf(value); + if (position == -1 && "".equals(value) == false) { + Log.i(TAG, value + " not in list, adding & moving spinner to it"); + Log.i(TAG, list.toString()); + Log.i(TAG, spinner.toString()); + + position = list.size(); + list.add(value); + } + try{ + spinner.setSelection(position); + }catch(java.lang.IndexOutOfBoundsException e){ + e.printStackTrace(); + } + } + + public void onCancelClick(View v) { + finish(); + } + + public void onSaveClick(View v) { + Log.i(TAG, "Saving story for project " + mProjectId); + + stageData(); + Intent intent = new Intent(SyncService.ACTION_SYNC_STORY, null, this, + SyncService.class); + + Bundle storyData = mStoryEntry.toBundle(); + storyData.putBoolean(SyncService.SEND_TO_API, true); + intent.putExtras(storyData); + intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, mState.mReceiver); + intent.putExtra(Stories.PROJECT_ID, mProjectId); + intent.putExtra(Stories.STORY_ID, mStoryId); + startService(intent); + finish(); + } + + public void onDeleteClick(View v) { + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage("Are you sure you want to delete?").setCancelable( + false).setPositiveButton("Yes", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + Intent intent = new Intent( + SyncService.ACTION_DELETE_STORY, null, EditStoryActivity.this, + SyncService.class); + + intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, + mState.mReceiver); + intent.putExtra(Stories.PROJECT_ID, mProjectId); + intent.putExtra(Stories.STORY_ID, mStoryId); + + startService(intent); + + EditStoryActivity.this.finish(); + } + }).setNegativeButton("No", + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); + AlertDialog alert = builder.create(); + alert.show(); + } + + public void stageData() { + mStoryEntry.put(Stories.STORY_ID, String.valueOf(mStoryId)); + mStoryEntry.put(Stories.NAME, mNameText.getText().toString()); + mStoryEntry.put(Stories.DESCRIPTION, mDescriptionText.getText() + .toString()); + mStoryEntry.put(Stories.LABELS, mLabelsText.getText().toString()); + String estimate = ((CharSequence) mEstimateSpinner + .getSelectedItem()).toString(); + estimate = estimate.replace(" points", ""); + mStoryEntry.put(Stories.ESTIMATE, estimate); + mStoryEntry.put(Stories.STORY_TYPE, ((CharSequence) mStoryTypeSpinner + .getSelectedItem()).toString()); + mStoryEntry.put(Stories.OWNED_BY, ((CharSequence) mOwnedBySpinner + .getSelectedItem()).toString()); + mStoryEntry.put(Stories.REQUESTED_BY, + ((CharSequence) mRequestedBySpinner.getSelectedItem()) + .toString()); + } + + public void displayData() { + + mNameText.setText(mStoryEntry.get(Stories.NAME)); + mDescriptionText.setText(mStoryEntry.get(Stories.DESCRIPTION)); + mLabelsText.setText(mStoryEntry.get(Stories.LABELS)); + setSpinnerByValue(mStoryEntry.get(Stories.REQUESTED_BY), mMemberList, + mRequestedBySpinner); + setSpinnerByValue(mStoryEntry.get(Stories.OWNED_BY), mMemberList, + mOwnedBySpinner); + + String estimate = mStoryEntry.get(Stories.ESTIMATE); + if(estimate.length() == 0){ + estimate = "0 points"; + }else{ + estimate += " points"; + } + Log.i(TAG, "ESTIMATE: "+estimate); + + setSpinnerByValue(estimate, mEstimateList, + mEstimateSpinner); + setSpinnerByValue(mStoryEntry.get(Stories.STORY_TYPE), mStoryTypeList, + mStoryTypeSpinner); + + } + + public void queryMembers() { + final Uri membersUri = Members.CONTENT_URI; + final SelectionBuilder builder = new SelectionBuilder(); + builder.where(Members.PROJECT_ID + "=?", String.valueOf(mProjectId)); + mQueryHandler.startQuery(membersUri, DefaultMemberQuery.PROJECTION); + } + + @Override + public void onQueryComplete(int token, Object cookie, Cursor cursor) { + if (cursor != null && cursor.moveToFirst()) { + do { + + mMembers.add(cursor.getString(DefaultMemberQuery.NAME)); + + } while (cursor.moveToNext()); + } + displayData(); + } + + @Override + public void onReceiveResult(int resultCode, Bundle resultData) { + Log.v(TAG, "Received Result code=" + resultCode); + Log.v(TAG, resultData.keySet().toString()); + + switch (resultCode) { + case SyncService.STATUS_RUNNING: { + mState.mSyncing = true; + break; + } + case SyncService.STATUS_FINISHED: { + mState.mSyncing = false; + Toast.makeText(EditStoryActivity.this, "Save sucessful", + Toast.LENGTH_LONG).show(); + break; + } + case SyncService.STATUS_ERROR: { + mState.mSyncing = false; + final String errorText = getString(R.string.toast_sync_error, + resultData.getStringArray(Intent.EXTRA_TEXT)); + Toast + .makeText(EditStoryActivity.this, errorText, + Toast.LENGTH_LONG).show(); + } + } + } + +} diff --git a/src/com/loganlinn/pivotaltrackie/ui/HomeActivity.java b/src/com/loganlinn/pivotaltrackie/ui/HomeActivity.java new file mode 100644 index 0000000..864b537 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/HomeActivity.java @@ -0,0 +1,308 @@ +package com.loganlinn.pivotaltrackie.ui; + +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.AdapterView.OnItemClickListener; + +import com.loganlinn.pivotaltracker.PivotalTracker; +import com.loganlinn.pivotaltracker.Project.DefaultProjectsQuery; +import com.loganlinn.pivotaltrackie.R; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Projects; +import com.loganlinn.pivotaltrackie.service.SyncService; +import com.loganlinn.pivotaltrackie.util.DateTimeUtils; +import com.loganlinn.pivotaltrackie.util.DetachableResultReceiver; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler.AsyncQueryListener; + +public class HomeActivity extends Activity implements AsyncQueryListener, + DetachableResultReceiver.Receiver, OnItemClickListener { + private static final String TAG = "HomeActivity"; + + private ActivityState mState; + private Handler mMessageHandler = new Handler(); + private NotifyingAsyncQueryHandler mQueryHandler; + + public ProjectListAdapter mListAdapter; + protected ListView mListView; + protected List mProjectList; + + private ContentResolver mContentResolver; + private final SimpleDateFormat mDatetimeFormat = PivotalTracker + .getDateFormat(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + if (showLoginIfNoTokenExists()) { + return; + } + + setContentView(R.layout.activity_home); + + mState = (ActivityState) getLastNonConfigurationInstance(); + final boolean previousState = (mState != null); + + if (previousState) { + mState.mReceiver.setReceiver(this); + updateRefreshStatus(); + // reloadProjects(true); + } else { + mState = new ActivityState(); + mState.mReceiver.setReceiver(this); + onRefreshClick(null); + } + mContentResolver = getContentResolver(); + // Set up handler for now project list query + mQueryHandler = new NotifyingAsyncQueryHandler(mContentResolver, this); + + mListView = (ListView) findViewById(R.id.lv_home); + mListView.setOnItemClickListener(this); + mListAdapter = new ProjectListAdapter(this); + mProjectList = new ArrayList(); + + mListView.setAdapter(mListAdapter); + + } + + private boolean showLoginIfNoTokenExists() { + + String token = PivotalTracker.getToken(getBaseContext()); + + if (token == null) { + Log.i(TAG, "No token found, showing login activity"); + Intent intent = new Intent(this, LoginActivity.class); + startActivity(intent); + return true; + } + return false; + } + + /* (non-Javadoc) + * @see android.app.Activity#onResume() + */ + @Override + protected void onResume() { + updateRefreshStatus(); + super.onResume(); + } + + @Override + public void onQueryComplete(int token, Object cookie, Cursor cursor) { + if (cursor == null) + return; + try { + if (cursor.moveToFirst()) { + mProjectList.clear(); + do { + + final Object[] projectData = { + cursor.getInt(DefaultProjectsQuery.PROJECT_ID), + cursor.getString(DefaultProjectsQuery.NAME), + cursor + .getInt(DefaultProjectsQuery.CURRENT_VELOCITY), + cursor + .getString(DefaultProjectsQuery.LAST_ACTIVITY_AT) }; + Log.v(TAG, "Adding Project, " + projectData[1] + + ", to list"); + mProjectList.add(projectData); + } while (cursor.moveToNext()); + + mListAdapter.notifyDataSetChanged(); + } else { + onNoProjectsToDisplay(); + } + } finally { + cursor.close(); + } + } + + private void updateRefreshStatus() { + // TODO: if syncing, show animation, otherwise show refresh button + findViewById(R.id.btn_title_refresh).setVisibility( + mState.mSyncing ? View.GONE : View.VISIBLE); + findViewById(R.id.title_refresh_progress).setVisibility( + mState.mSyncing ? View.VISIBLE : View.GONE); + } + + @Override + public Object onRetainNonConfigurationInstance() { + // Clear any strong references to this Activity, we'll reattach to + // handle events on the other side. + mState.mReceiver.clearReceiver(); + return mState; + } + + @Override + public void onReceiveResult(int resultCode, Bundle resultData) { + Log.v(TAG, "Received Result code=" + resultCode); + Log.v(TAG, resultData.keySet().toString()); + + switch (resultCode) { + case SyncService.STATUS_RUNNING: { + mState.mSyncing = true; + updateRefreshStatus(); + break; + } + case SyncService.STATUS_FINISHED: { + mState.mSyncing = false; + updateRefreshStatus(); + queryProjects(); + // TODO: reload with results + break; + } + case SyncService.STATUS_ERROR: { + mState.mSyncing = false; + updateRefreshStatus(); + final String errorText = getString(R.string.toast_sync_error, + resultData.getStringArray(Intent.EXTRA_TEXT)); + Toast.makeText(HomeActivity.this, errorText, Toast.LENGTH_LONG) + .show(); + } + } + } + + private void onNoProjectsToDisplay() { + Toast.makeText(HomeActivity.this, "No projects found!", + Toast.LENGTH_SHORT).show(); + } + + private void queryProjects() { + final Uri uri = Projects.CONTENT_URI; + mQueryHandler.startQuery(uri, DefaultProjectsQuery.PROJECTION, null, + null, null); + + } + + /** Handle "refresh" title-bar action. */ + public void onRefreshClick(View v) { + // trigger off background sync + final Intent intent = new Intent(SyncService.ACTION_SYNC_PROJECTS, + null, this, SyncService.class); + intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, mState.mReceiver); + startService(intent); + + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, + long id) { + final Object[] projectData = mProjectList.get(position); + final Integer projectId = (Integer) projectData[DefaultProjectsQuery.PROJECT_ID]; + final String projectName = (String) projectData[DefaultProjectsQuery.NAME]; + + final Intent intent = new Intent(getBaseContext(), + ProjectActivity.class); + intent.putExtra(Projects.PROJECT_ID, projectId); + intent.putExtra(Projects.NAME, projectName); + startActivity(intent); + } + + private class ProjectListAdapter extends BaseAdapter { + private LayoutInflater mInflater; + + public ProjectListAdapter(Context context) { + mInflater = LayoutInflater.from(context); + + } + + @Override + public int getCount() { + return mProjectList.size(); + } + + @Override + public Object getItem(int position) { + return mProjectList.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int pos, View convertView, ViewGroup parent) { + // A ViewHolder keeps references to children views to avoid + // unneccessary calls + // to findViewById() on each row. + ViewHolder holder; + // When convertView is not null, we can reuse it directly, there is + // no need + // to reinflate it. We only inflate a new View when the convertView + // supplied + // by ListView is null. + + if (convertView == null) { + convertView = mInflater.inflate(R.layout.list_item_project, + null); + holder = new ViewHolder(); + holder.mProjectName = (TextView) convertView + .findViewById(R.id.tv_project_name); + holder.mLastActivity = (TextView) convertView + .findViewById(R.id.tv_project_last_activity); + holder.mCurrentVelocity = (TextView) convertView + .findViewById(R.id.tv_project_velocity); + convertView.setTag(holder); + } else { + holder = (ViewHolder) convertView.getTag(); + } + + final Object[] projectData = mProjectList.get(pos); + String lastActivity = (String) projectData[DefaultProjectsQuery.LAST_ACTIVITY_AT]; + if (lastActivity != null) { + try { + Log.i(TAG, lastActivity); + lastActivity = "2010/12/06 14:34:29 UTC"; + + final Date d = mDatetimeFormat.parse(lastActivity); + long updated = d.getTime(); + lastActivity = "Last activity " + + DateTimeUtils.getInstance( + convertView.getContext()) + .getTimeDiffString(updated).toLowerCase(); + } catch (ParseException e) { + e.printStackTrace(); + } + }else{ + lastActivity = ""; + } + holder.mProjectName + .setText((CharSequence) projectData[DefaultProjectsQuery.NAME]); + holder.mLastActivity.setText(lastActivity); + holder.mCurrentVelocity + .setText(String + .valueOf(projectData[DefaultProjectsQuery.CURRENT_VELOCITY])); + + return convertView; + } + + private class ViewHolder { + TextView mProjectName; + TextView mLastActivity; + TextView mCurrentVelocity; + } + + } +} diff --git a/src/com/loganlinn/pivotaltrackie/ui/LoginActivity.java b/src/com/loganlinn/pivotaltrackie/ui/LoginActivity.java new file mode 100644 index 0000000..5852fd1 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/LoginActivity.java @@ -0,0 +1,53 @@ +package com.loganlinn.pivotaltrackie.ui; + +import android.app.Activity; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Toast; + +import com.loganlinn.pivotaltracker.PivotalTrackerError; +import com.loganlinn.pivotaltracker.PivotalTrackerListener; +import com.loganlinn.pivotaltracker.PivotalTracker; +import com.loganlinn.pivotaltrackie.R; +import com.loganlinn.pivotaltrackie.util.DetachableResultReceiver; + +public class LoginActivity extends Activity implements PivotalTrackerListener { + private static final String TAG = "LoginActivity"; + private Button submitButton_; + private EditText usernameText_; + private EditText passwordText_; + private PivotalTracker mPivotalTracker; + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + + mPivotalTracker = PivotalTracker.getInstance(getBaseContext()); + submitButton_ = (Button) findViewById(R.id.btn_login); + usernameText_ = (EditText) findViewById(R.id.et_username); + passwordText_ = (EditText) findViewById(R.id.et_password); + + } + + public void onSubmitClick(View v){ + + final String username = usernameText_.getText().toString(); + final String password = passwordText_.getText().toString(); + + mPivotalTracker.fetchToken(username, password, this); + + } + + @Override + public void onError(PivotalTrackerError error) { + Toast.makeText(getBaseContext(), "Could not authenticate", Toast.LENGTH_LONG); + } + + @Override + public void onSuccess(String token) { + finish(); + } +} \ No newline at end of file diff --git a/src/com/loganlinn/pivotaltrackie/ui/ProjectActivity.java b/src/com/loganlinn/pivotaltrackie/ui/ProjectActivity.java new file mode 100644 index 0000000..0133731 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/ProjectActivity.java @@ -0,0 +1,380 @@ +package com.loganlinn.pivotaltrackie.ui; + +import java.util.List; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Context; +import android.content.Intent; +import android.database.Cursor; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.GestureDetector; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnTouchListener; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import android.widget.ListAdapter; +import android.widget.ListView; +import android.widget.TextView; +import android.widget.Toast; +import android.widget.ViewFlipper; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.AdapterView.OnItemLongClickListener; + +import com.loganlinn.pivotaltracker.StoryEntry; +import com.loganlinn.pivotaltracker.Story.DefaultStoriesQuery; +import com.loganlinn.pivotaltrackie.R; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Projects; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.service.SyncService; +import com.loganlinn.pivotaltrackie.util.DetachableResultReceiver; +import com.loganlinn.pivotaltrackie.util.FlingGestureListener; +import com.loganlinn.pivotaltrackie.util.Lists; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler; +import com.loganlinn.pivotaltrackie.util.SelectionBuilder; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler.AsyncQueryListener; + +public class ProjectActivity extends Activity implements AsyncQueryListener, + DetachableResultReceiver.Receiver, OnItemClickListener, + OnItemLongClickListener { + private static final String TAG = "ProjectActivity"; + private static final Integer EMPTY_PROJECT_ID = -1; + + private List[] mStoryList; + private ListView[] mListView; + private StoryListAdapter[] mListAdapter; + private ActivityState mState; + private ContentResolver mContentResolver; + private NotifyingAsyncQueryHandler mQueryHandler; + private Handler mMessageHandler = new Handler(); + private Integer mProjectId; + + private ViewFlipper mListViewFlipper; + private GestureDetector mGestureDetector; + private FlingGestureListener mGestureListener; + + private TextView mTitle; + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_project); + + mState = (ActivityState) getLastNonConfigurationInstance(); + final boolean previousState = (mState != null); + + if (previousState) { + mState.mReceiver.setReceiver(this); + updateRefreshStatus(); + reloadStories(true); + } else { + mProjectId = getIntent().getIntExtra(Projects.PROJECT_ID, + EMPTY_PROJECT_ID); + final String projectName = getIntent().getStringExtra(Projects.NAME); + if(projectName != null){ + mTitle = (TextView) findViewById(R.id.tv_title_bar_title); + mTitle.setText(projectName); + + } + Log.i(TAG, "Initializing stories for project id=" + mProjectId); + mState = new ActivityState(); + mState.mReceiver.setReceiver(this); + onRefreshClick(null); + } + mContentResolver = getContentResolver(); + + mQueryHandler = new NotifyingAsyncQueryHandler(mContentResolver, this); + + mTitle = (TextView) findViewById(R.id.tv_title_bar_title); + mStoryList = new List[2]; + mStoryList[0] = Lists.newArrayList(); + //mStoryList[1] = Lists.newArrayList(); + + mListView = new ListView[2]; + mListView[0] = (ListView) findViewById(R.id.lv_stories0); + //mListView[1] = (ListView) findViewById(R.id.lv_stories1); + mListView[0].setOnItemClickListener(this); + mListView[0].setOnItemLongClickListener(this); + //mListView[1].setOnItemClickListener(this); + //mListView[1].setOnItemLongClickListener(this); + mListAdapter = new StoryListAdapter[2]; + + mListAdapter[0] = new StoryListAdapter(this, mStoryList[0]); + mListView[0].setAdapter(mListAdapter[0]); + + //mListAdapter[1] = new StoryListAdapter(this, mStoryList[1]); + //mListView[1].setAdapter(mListAdapter[1]); + + mListViewFlipper = (ViewFlipper) findViewById(R.id.vf_stories_flipper); + + mGestureListener = new FlingGestureListener(mListViewFlipper); + mGestureDetector = new GestureDetector(mGestureListener); + + OnTouchListener emptyTouchListener = new OnTouchListener(){ + //Used to padd touch event to parent, where touch event is handled + @Override + public boolean onTouch(View v, MotionEvent event) { + return mGestureDetector.onTouchEvent(event); + } + }; + mListView[0].setOnTouchListener(emptyTouchListener); + //mListView[1].setOnTouchListener(emptyTouchListener); + queryStories(); + } + private void updateTitle(){ + switch(mGestureListener.mCurrentFlipperIndex){ + + } + } + private void reloadStories(boolean forceRefresh) { + queryStories(); + } + + private void queryStories() { + final Uri uri = Stories.CONTENT_URI; + final SelectionBuilder builder = new SelectionBuilder(); + + builder.where(Stories.PROJECT_ID + "=?", mProjectId.toString()); + + + //Start the query, receive response in onQueryComplete method + mQueryHandler.startQuery(uri, + DefaultStoriesQuery.PROJECTION, builder.getSelection(), builder + .getSelectionArgs(), null); + + } + + @Override + public void onQueryComplete(int token, Object cookie, Cursor cursor) { + //TODO: Split up stories based on iteration + //TODO: Display in UI that there are multiple lists + if(cursor == null) return; + try { + if (cursor.moveToFirst()) { + mStoryList[0].clear(); + //mStoryList[1].clear(); + int i = 0; + do { + final StoryEntry story = StoryEntry.fromCursor(cursor); + //SPLIT the lists + mStoryList[0].add(story); + + } while (cursor.moveToNext()); + mListAdapter[0].notifyDataSetChanged(); + //mListAdapter[1].notifyDataSetChanged(); + } else { + Log.i(TAG, "No stories found in database for project"); + } + + } finally { + cursor.close(); + } + + } + /* (non-Javadoc) + * @see android.app.Activity#onTouchEvent(android.view.MotionEvent) + */ + @Override + public boolean onTouchEvent(MotionEvent event) { + return mGestureDetector.onTouchEvent(event); + } + + /** Handle "refresh" title-bar action. */ + public void onRefreshClick(View v) { + // trigger off background sync + if (mProjectId == EMPTY_PROJECT_ID) { + Log.e(TAG, "Project ID unspecified. Cannot refresh"); + finish(); + } + final Intent intent = new Intent(SyncService.ACTION_SYNC_STORIES, + Projects.buildProjectUri(mProjectId), this, SyncService.class); + intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, mState.mReceiver); + startService(intent); + } + + private void updateRefreshStatus() { + // TODO: if syncing, show animation, otherwise show refresh button + findViewById(R.id.btn_title_refresh).setVisibility( + mState.mSyncing ? View.GONE : View.VISIBLE); + findViewById(R.id.title_refresh_progress).setVisibility( + mState.mSyncing ? View.VISIBLE : View.GONE); + } + + public void onNewStoryClick(View v){ + final Intent intent = new Intent(this, EditStoryActivity.class); + intent.putExtra(Stories.PROJECT_ID, mProjectId); + intent.putExtra(Stories.STORY_ID, Stories.NEW_STORY); + startActivity(intent); + } + + @Override + public void onItemClick(AdapterView parent, View view, int position, + long id) { + Log.i(TAG,"Clicked story at position "+position); + final ListView listView = (ListView) view.getParent(); + final ListAdapter listAdapter = listView.getAdapter(); + final StoryEntry entry = (StoryEntry) listAdapter.getItem(position); //mStoryList[mGestureListener.mCurrentFlipperIndex].get(position); + + Intent intent = new Intent(this, ViewStoryActivity.class); + + final int storyId = Integer.valueOf(entry.get(Stories.STORY_ID)); + intent.putExtra(Stories.STORY_ID, storyId); + intent.putExtra(Stories.PROJECT_ID, mProjectId); + startActivity(intent); + } + + /* (non-Javadoc) + * @see android.app.Activity#onResume() + */ + @Override + protected void onResume() { + // TODO Auto-generated method stub + onRefreshClick(null); + super.onResume(); + } + /** + * TODO: Show story menu + */ + @Override + public boolean onItemLongClick(AdapterView parent, View view, + int position, long id) { + + return true; + } + + private class StoryListAdapter extends BaseAdapter { + private LayoutInflater mInflater; + private List mList; + public StoryListAdapter(Context context, List list) { + mInflater = LayoutInflater.from(context); + mList = list; + } + + @Override + public int getCount() { + return mList.size(); + } + + @Override + public Object getItem(int position) { + return mList.get(position); + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + ViewHolder holder; + + if (convertView == null) { + convertView = mInflater.inflate(R.layout.list_item_story, null); + holder = new ViewHolder(); + holder.mName = (TextView) convertView + .findViewById(R.id.tv_story_name); + holder.mEstimate = (TextView) convertView + .findViewById(R.id.tv_story_estimate); + holder.mTypeIcon = (ImageView) convertView.findViewById(R.id.iv_story_type); + convertView.setTag(holder); + + } else { + holder = (ViewHolder) convertView.getTag(); + } + + final StoryEntry story = (StoryEntry) getItem(position); + final String estimate = story.get(Stories.ESTIMATE); + holder.mName.setText(story.get(Stories.NAME)); + + if ("-1".equals(estimate)) { + holder.mEstimate.setText(""); + } else { + holder.mEstimate.setText(estimate); + } + + final String currentStatus = story.get(Stories.CURRENT_STATE); + + //Set the background color of the story depending on its status + if(Stories.CURRENT_STATUS_ACCEPTED.equals(currentStatus)){ + convertView.setBackgroundResource(R.color.story_accepted); + }else if(Stories.CURRENT_STATUS_DELIVERED.equals(currentStatus)){ + convertView.setBackgroundResource(R.color.story_delivered); + }else if(Stories.CURRENT_STATUS_STARTED.equals(currentStatus)){ + convertView.setBackgroundResource(R.color.story_started); + }else if(Stories.CURRENT_STATUS_FINISHED.equals(currentStatus)){ + convertView.setBackgroundResource(R.color.story_finished); + }else{ // CURRENT_STATUS_UNSCHEDULED + convertView.setBackgroundResource(R.color.story_unscheduled); + } + + final String storyType = story.get(Stories.STORY_TYPE); + + if(Stories.STORY_TYPE_BUG.equals(storyType)){ + holder.mTypeIcon.setImageResource(R.drawable.bug_icon); + holder.mTypeIcon.setVisibility(ImageView.VISIBLE); + }else if(Stories.STORY_TYPE_FEATURE.equals(storyType)){ + holder.mTypeIcon.setImageResource(R.drawable.feature); + holder.mTypeIcon.setVisibility(ImageView.VISIBLE); + }else if(Stories.STORY_TYPE_CHORE.equals(storyType)){ + holder.mTypeIcon.setImageResource(R.drawable.chore_icon); + holder.mTypeIcon.setVisibility(ImageView.VISIBLE); + }else{ + holder.mTypeIcon.setVisibility(ImageView.INVISIBLE); + } + + return convertView; + } + + private class ViewHolder { + TextView mName; + TextView mEstimate; + ImageView mTypeIcon; + } + } + + @Override + public Object onRetainNonConfigurationInstance() { + // Clear any strong references to this Activity, we'll reattach to + // handle events on the other side. + mState.mReceiver.clearReceiver(); + return mState; + } + + @Override + public void onReceiveResult(int resultCode, Bundle resultData) { + Log.v(TAG, "Received Result code=" + resultCode); + Log.v(TAG, resultData.keySet().toString()); + + switch (resultCode) { + case SyncService.STATUS_RUNNING: { + mState.mSyncing = true; + updateRefreshStatus(); + break; + } + case SyncService.STATUS_FINISHED: { + mState.mSyncing = false; + updateRefreshStatus(); + reloadStories(false); + // TODO: reload with results + break; + } + case SyncService.STATUS_ERROR: { + mState.mSyncing = false; + updateRefreshStatus(); + final String errorText = getString(R.string.toast_sync_error, + resultData.getStringArray(Intent.EXTRA_TEXT)); + Toast.makeText(ProjectActivity.this, errorText, Toast.LENGTH_LONG) + .show(); + } + } + + } + +} diff --git a/src/com/loganlinn/pivotaltrackie/ui/ViewStoryActivity.java b/src/com/loganlinn/pivotaltrackie/ui/ViewStoryActivity.java new file mode 100644 index 0000000..67bf381 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/ViewStoryActivity.java @@ -0,0 +1,245 @@ +package com.loganlinn.pivotaltrackie.ui; + +import android.app.Activity; +import android.content.ContentResolver; +import android.content.Intent; +import android.database.Cursor; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import com.loganlinn.pivotaltracker.StoryEntry; +import com.loganlinn.pivotaltracker.Story.DetailStoriesQuery; +import com.loganlinn.pivotaltrackie.R; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Projects; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.Stories; +import com.loganlinn.pivotaltrackie.service.SyncService; +import com.loganlinn.pivotaltrackie.util.DetachableResultReceiver; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler; +import com.loganlinn.pivotaltrackie.util.SelectionBuilder; +import com.loganlinn.pivotaltrackie.util.NotifyingAsyncQueryHandler.AsyncQueryListener; + +public class ViewStoryActivity extends Activity implements AsyncQueryListener, + DetachableResultReceiver.Receiver { + private static final String TAG = "ViewStoryActivity"; + private TextView mStoryName; + private TextView mRequestedBy; + private TextView mOwnedBy; + private TextView mAcceptedAt; + private TextView mCreatedAt; + private TextView mCurrentState; + private TextView mDescription; + private TextView mLabels; + private TextView mStoryType; + private TextView mEstimate; + + private Integer mStoryId; + private Integer mProjectId; + + private StoryEntry mStoryEntry; + + private ActivityState mState; + private ContentResolver mContentResolver; + private NotifyingAsyncQueryHandler mQueryHandler; + private Handler mMessageHandler = new Handler(); + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_view_story); + // Get intent data + mStoryId = getIntent().getIntExtra(Stories.STORY_ID, -1); + mProjectId = getIntent().getIntExtra(Stories.PROJECT_ID, -1); + + if (mStoryId == -1 || mProjectId == -1) { + Log.e(TAG, "Need story_id and project id,leaving activity"); + + finish(); + } + // Check for previous state + mState = (ActivityState) getLastNonConfigurationInstance(); + + final boolean previousState = (mState != null); + + if (previousState) { + mState.mReceiver.setReceiver(this); + updateRefreshStatus(); + } else { + mState = new ActivityState(); + mState.mReceiver.setReceiver(this); + onRefreshClick(null); + } + mContentResolver = getContentResolver(); + mQueryHandler = new NotifyingAsyncQueryHandler(mContentResolver, this); + + mStoryName = (TextView) findViewById(R.id.tv_story_name); + mRequestedBy = (TextView) findViewById(R.id.tv_story_requested_by); + mOwnedBy = (TextView) findViewById(R.id.tv_story_owned_by); + mAcceptedAt = (TextView) findViewById(R.id.tv_story_accepted_at); + mCreatedAt = (TextView) findViewById(R.id.tv_story_created_at); + mCurrentState = (TextView) findViewById(R.id.tv_story_current_state); + mDescription = (TextView) findViewById(R.id.tv_story_description); + mLabels = (TextView) findViewById(R.id.tv_story_labels); + mStoryType = (TextView) findViewById(R.id.tv_story_type); + mEstimate = (TextView) findViewById(R.id.tv_story_estimate); + + queryStory(); + } + + private void reloadStory(boolean force) { + if (force) { // if forced, do the query. the async listener (this) will + // return back here + queryStory(); + return; + } + mStoryEntry.sanitizeNullValues(); + final String storyName = mStoryEntry.get(Stories.NAME); + final String storyType = mStoryEntry.get(Stories.STORY_TYPE); + final String requestedBy = mStoryEntry.get(Stories.REQUESTED_BY); + final String ownedBy = mStoryEntry.get(Stories.OWNED_BY); + final String acceptedAt = mStoryEntry.get(Stories.ACCEPTED_AT); + final String createdAt = mStoryEntry.get(Stories.CREATED_AT); + final String currentState = mStoryEntry.get(Stories.CURRENT_STATE); + String description = mStoryEntry.get(Stories.DESCRIPTION); + final String labels = mStoryEntry.get(Stories.LABELS); + String estimate = mStoryEntry.get(Stories.ESTIMATE); + + if(estimate.length() == 0){ + estimate = "0 points"; + }else{ + estimate += " points"; + } + + + Log.i(TAG, "Displaying story: " + storyName); + + mEstimate.setText(estimate); + mStoryType.setText(storyType); + mStoryName.setText(storyName); + mRequestedBy.setText(requestedBy); + mOwnedBy.setText(ownedBy); + mAcceptedAt.setText(acceptedAt); + mCreatedAt.setText(createdAt); + mCurrentState.setText(currentState); + mDescription.setText(description); + mLabels.setText(labels); + } + + + /* + * (non-Javadoc) + * + * @see android.app.Activity#onResume() + */ + @Override + protected void onResume() { + updateRefreshStatus(); + queryStory(); + super.onResume(); + } + + private void updateRefreshStatus() { + // TODO: if syncing, show animation, otherwise show refresh button + findViewById(R.id.btn_title_refresh).setVisibility( + mState.mSyncing ? View.GONE : View.VISIBLE); + findViewById(R.id.title_refresh_progress).setVisibility( + mState.mSyncing ? View.VISIBLE : View.GONE); + } + + private void queryStory() { + final String storyId = mStoryId.toString(); + final Uri uri = Stories.buildStoryUri(storyId); + + final SelectionBuilder builder = new SelectionBuilder(); + builder.where(Stories.STORY_ID + "=?", storyId); + + mQueryHandler.startQuery(uri, DetailStoriesQuery.PROJECTION, builder + .getSelection(), builder.getSelectionArgs(), null); + + } + + // Called when the edit button on the toolbar is clicked + public void onEditClick(View v) { + final Intent intent = new Intent(this, EditStoryActivity.class); + + intent.putExtras(mStoryEntry.toBundle()); + intent.putExtra(Stories.STORY_ID, mStoryId); // ensure the intent is + // passed an integer + intent.putExtra(Stories.PROJECT_ID, mProjectId); + startActivity(intent); + + } + + // Called when the refresh button on toolbar is clicked + public void onRefreshClick(View v) { + final Intent intent = new Intent(SyncService.ACTION_SYNC_STORY, Stories + .buildStoryUri(mStoryId), this, SyncService.class); + + intent.putExtra(SyncService.EXTRA_STATUS_RECEIVER, mState.mReceiver); + intent.putExtra(Stories.PROJECT_ID, mProjectId); + intent.putExtra(Stories.STORY_ID, mStoryId); + + startService(intent); + } + + @Override + public void onQueryComplete(int token, Object cookie, Cursor cursor) { + if (cursor == null) + return; + try { + if (cursor.moveToFirst()) { + mStoryEntry = StoryEntry.fromCursor(cursor, + DetailStoriesQuery.PROJECTION); + } + } finally { + cursor.close(); + } + reloadStory(false); + Log.i(TAG, mStoryEntry.toString()); + } + + @Override + public void onReceiveResult(int resultCode, Bundle resultData) { + Log.v(TAG, "Received Result code=" + resultCode); + Log.v(TAG, resultData.keySet().toString()); + + switch (resultCode) { + case SyncService.STATUS_RUNNING: { + mState.mSyncing = true; + updateRefreshStatus(); + break; + } + case SyncService.STATUS_FINISHED: { + mState.mSyncing = false; + updateRefreshStatus(); + reloadStory(true); + // TODO: reload with results + break; + } + case SyncService.STATUS_ERROR: { + mState.mSyncing = false; + updateRefreshStatus(); + reloadStory(true); + final String errorText = getString(R.string.toast_sync_error, + resultData.getStringArray(Intent.EXTRA_TEXT)); + Toast + .makeText(ViewStoryActivity.this, errorText, + Toast.LENGTH_LONG).show(); + } + } + + } + + @Override + public Object onRetainNonConfigurationInstance() { + // Clear any strong references to this Activity, we'll reattach to + // handle events on the other side. + mState.mReceiver.clearReceiver(); + return mState; + } +} diff --git a/src/com/loganlinn/pivotaltrackie/ui/XMLTokenHandler.java b/src/com/loganlinn/pivotaltrackie/ui/XMLTokenHandler.java new file mode 100644 index 0000000..d9f51b0 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/ui/XMLTokenHandler.java @@ -0,0 +1,70 @@ +package com.loganlinn.pivotaltrackie.ui; + +import java.io.IOException; +import java.io.InputStream; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +import android.util.Log; + +public class XMLTokenHandler extends DefaultHandler { + + private static final String TAG = "XMLTokenHandler"; + + private StringBuilder token = new StringBuilder(); + private String currentElementName=""; + + public void startElement(String uri, String name, String qName, Attributes attr) { + currentElementName = name.trim(); + } + + public void endElement(String uri, String name, String qName) throws SAXException { + currentElementName = ""; + } + + public void characters(char ch[], int start, int length) { + + String chars = (new String(ch).substring(start, start + length)); + + if (currentElementName.equalsIgnoreCase("guid")) { + token.append(chars); + } + } + + public String getTokenAndClose(InputStream in) { + try { + parse(in); + in.close(); + return token.toString(); + } catch (ParserConfigurationException e) { + Log.e(TAG,"ParserConfigurationException "+e.getMessage()); + throw new RuntimeException("ParserConfigurationException "+e.getMessage(),e); + } catch (SAXException e) { + Log.e(TAG,"SAXException "+e.getMessage()); + throw new RuntimeException("SAXException "+e.getMessage(),e); + } catch (IOException e) { + Log.e(TAG,"IOException "+e.getMessage()); + throw new RuntimeException("IOException "+e.getMessage(),e); + } + } + + private void parse(InputStream in) throws ParserConfigurationException, SAXException, IOException { + SAXParserFactory spf = SAXParserFactory.newInstance(); + SAXParser sp; + + sp = spf.newSAXParser(); + XMLReader xr = sp.getXMLReader(); + xr.setContentHandler(this); + InputSource is = new InputSource(in); + xr.parse(is); + + } +} diff --git a/src/com/loganlinn/pivotaltrackie/util/.svn/all-wcprops b/src/com/loganlinn/pivotaltrackie/util/.svn/all-wcprops new file mode 100644 index 0000000..0d180e2 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/.svn/all-wcprops @@ -0,0 +1,29 @@ +K 25 +svn:wc:ra_dav:version-url +V 106 +/svn/!svn/ver/2029/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/util +END +HttpUtils.java +K 25 +svn:wc:ra_dav:version-url +V 121 +/svn/!svn/ver/2170/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/util/HttpUtils.java +END +FlingGestureListener.java +K 25 +svn:wc:ra_dav:version-url +V 132 +/svn/!svn/ver/2183/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/util/FlingGestureListener.java +END +ParserUtils.java +K 25 +svn:wc:ra_dav:version-url +V 123 +/svn/!svn/ver/2170/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/util/ParserUtils.java +END +DateTimeUtils.java +K 25 +svn:wc:ra_dav:version-url +V 125 +/svn/!svn/ver/2183/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/util/DateTimeUtils.java +END diff --git a/src/com/loganlinn/pivotaltrackie/util/.svn/dir-prop-base b/src/com/loganlinn/pivotaltrackie/util/.svn/dir-prop-base new file mode 100644 index 0000000..95b2379 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/.svn/dir-prop-base @@ -0,0 +1,6 @@ +K 10 +svn:ignore +V 6 +.git* + +END diff --git a/src/com/loganlinn/pivotaltrackie/util/.svn/entries b/src/com/loganlinn/pivotaltrackie/util/.svn/entries new file mode 100644 index 0000000..b4ae432 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/.svn/entries @@ -0,0 +1,368 @@ +10 + +dir +2086 +https://vtnetapps.googlecode.com/svn/trunk/assignments/logan_linn/final/PivotalTrackie/src/com/loganlinn/pivotaltrackie/util +https://vtnetapps.googlecode.com/svn + + + +2010-11-26T19:33:38.466730Z +2029 +loganlinn +has-props + + + + + + + + + + + + + +13dd798f-c9a9-0d3a-1410-a4e436c6fec4 + +Maps.java +file + + + + +2010-11-26T19:33:05.000000Z +083652568510e1f5780323329208118f +2010-11-26T19:33:38.466730Z +2029 +loganlinn + + + + + + + + + + + + + + + + + + + + + +689 + +DetachableResultReceiver.java +file + + + + +2010-11-26T19:33:05.000000Z +5e623536726af3ff2863b7448fd7ddcc +2010-11-26T19:33:38.466730Z +2029 +loganlinn + + + + + + + + + + + + + + + + + + + + + +1906 + +HttpUtils.java +file +2170 + + + +2010-11-26T22:25:27.000000Z +7a5bdff18e82a6194f9582015b85a655 +2010-12-01T18:03:53.612881Z +2170 +loganlinn + + + + + + + + + + + + + + + + + + + + + +7122 + +FlingGestureListener.java +file +2183 + + + +2010-12-01T23:04:43.000000Z +b0e44a4112c4858a1ea1db38544f75e9 +2010-12-02T04:08:08.804433Z +2183 +loganlinn + + + + + + + + + + + + + + + + + + + + + +2737 + +NotifyingAsyncQueryHandler.java +file + + + + +2010-11-26T19:33:05.000000Z +debedfc2b1ac2a445bfe500011f8c9fc +2010-11-26T19:33:38.466730Z +2029 +loganlinn + + + + + + + + + + + + + + + + + + + + + +4739 + +PivotalTrackerToken.java +file + + + + +2010-11-26T19:33:05.000000Z +96ca263045436cbfed993190b9282f7d +2010-11-26T19:33:38.466730Z +2029 +loganlinn + + + + + + + + + + + + + + + + + + + + + +1731 + +SelectionBuilder.java +file + + + + +2010-11-26T19:33:05.000000Z +4bb9992b10e337429d3f7552fa10bf38 +2010-11-26T19:33:38.466730Z +2029 +loganlinn + + + + + + + + + + + + + + + + + + + + + +5612 + +ParserUtils.java +file +2170 + + + +2010-11-26T23:55:18.000000Z +3ba2bae9567deb2c6ed29874520a6204 +2010-12-01T18:03:53.612881Z +2170 +loganlinn + + + + + + + + + + + + + + + + + + + + + +1435 + +DateTimeUtils.java +file +2183 + + + +2010-12-02T02:14:52.000000Z +8ceb65d8407d7fcbee490ce813ab634a +2010-12-02T04:08:08.804433Z +2183 +loganlinn + + + + + + + + + + + + + + + + + + + + + +4164 + +Lists.java +file + + + + +2010-11-26T19:33:05.000000Z +8c2599088b65b1fe9a286d5608ca496f +2010-11-26T19:33:38.466730Z +2029 +loganlinn + + + + + + + + + + + + + + + + + + + + + +1571 + diff --git a/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/DateTimeUtils.java.svn-base b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/DateTimeUtils.java.svn-base new file mode 100644 index 0000000..a4d94ce --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/DateTimeUtils.java.svn-base @@ -0,0 +1,103 @@ +package com.loganlinn.pivotaltrackie.util; + +import java.text.DateFormatSymbols; +import java.util.Calendar; + +import android.content.Context; +import android.text.format.DateUtils; + +import com.loganlinn.pivotaltrackie.R; + +public class DateTimeUtils extends DateUtils { + + private static String mTimestampLabelYesterday; + private static String mTimestampLabelToday; + private static String mTimestampLabelJustNow; + private static String mTimestampLabelMinutesAgo; + private static String mTimestampLabelHoursAgo; + private static String mTimestampLabelHourAgo; + private static Context sContext; + private static DateTimeUtils sInstance; + /** + * Singleton constructor, needed to get access to the application context & strings for i18n + * @param context Context + * @return DateTimeUtils singleton instance + * @throws Exception + */ + public static DateTimeUtils getInstance(Context context) { + sContext = context; + if (sInstance == null) { + sInstance = new DateTimeUtils(); + mTimestampLabelYesterday = context.getResources().getString(R.string.WidgetProvider_timestamp_yesterday); + mTimestampLabelToday = context.getResources().getString(R.string.WidgetProvider_timestamp_today); + mTimestampLabelJustNow = context.getResources().getString(R.string.WidgetProvider_timestamp_just_now); + mTimestampLabelMinutesAgo = context.getResources().getString(R.string.WidgetProvider_timestamp_minutes_ago); + mTimestampLabelHoursAgo = context.getResources().getString(R.string.WidgetProvider_timestamp_hours_ago); + mTimestampLabelHourAgo = context.getResources().getString(R.string.WidgetProvider_timestamp_hour_ago); + } + return sInstance; + } + + /** + * Checks if the given date is yesterday. + * + * @param date - Date to check. + * @return TRUE if the date is yesterday, FALSE otherwise. + */ + public static boolean isYesterday(long date) { + + final Calendar currentDate = Calendar.getInstance(); + currentDate.setTimeInMillis(date); + + final Calendar yesterdayDate = Calendar.getInstance(); + yesterdayDate.add(Calendar.DATE, -1); + + return yesterdayDate.get(Calendar.YEAR) == currentDate.get(Calendar.YEAR) && yesterdayDate.get(Calendar.DAY_OF_YEAR) == currentDate.get(Calendar.DAY_OF_YEAR); + } + + public static String[] weekdays = new DateFormatSymbols().getWeekdays(); // get day names + public static final long millisInADay = 1000 * 60 * 60 * 24; + + + + /** + * Displays a user-friendly date difference string + * @param timedate Timestamp to format as date difference from now + * @return Friendly-formatted date diff string + */ + public String getTimeDiffString(long timedate) { + Calendar startDateTime = Calendar.getInstance(); + Calendar endDateTime = Calendar.getInstance(); + endDateTime.setTimeInMillis(timedate); + long milliseconds1 = startDateTime.getTimeInMillis(); + long milliseconds2 = endDateTime.getTimeInMillis(); + long diff = milliseconds1 - milliseconds2; + + long hours = diff / (60 * 60 * 1000); + long minutes = diff / (60 * 1000); + minutes = minutes - 60 * hours; + long seconds = diff / (1000); + + boolean isToday = DateTimeUtils.isToday(timedate); + boolean isYesterday = DateTimeUtils.isYesterday(timedate); + + if (hours > 0 && hours < 12) { + return hours==1? String.format(mTimestampLabelHourAgo,hours) : String.format(mTimestampLabelHoursAgo,hours); + } else if (hours <= 0) { + if (minutes > 0) + return String.format(mTimestampLabelMinutesAgo,minutes); + else { + return mTimestampLabelJustNow; + } + } else if (isToday) { + return mTimestampLabelToday; + } else if (isYesterday) { + return mTimestampLabelYesterday; + } else if (startDateTime.getTimeInMillis() - timedate < millisInADay * 6) { + return weekdays[endDateTime.get(Calendar.DAY_OF_WEEK)]; + } else { + return formatDateTime(sContext, timedate, DateUtils.FORMAT_NUMERIC_DATE); + } + } + +} diff --git a/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/DetachableResultReceiver.java.svn-base b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/DetachableResultReceiver.java.svn-base new file mode 100644 index 0000000..22aec47 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/DetachableResultReceiver.java.svn-base @@ -0,0 +1,60 @@ +/* + * Copyright 2010 Google Inc. + * + * 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 com.loganlinn.pivotaltrackie.util; + +import android.app.Activity; +import android.app.Service; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.util.Log; + +/** + * Proxy {@link ResultReceiver} that offers a listener interface that can be + * detached. Useful for when sending callbacks to a {@link Service} where a + * listening {@link Activity} can be swapped out during configuration changes. + */ +public class DetachableResultReceiver extends ResultReceiver { + private static final String TAG = "DetachableResultReceiver"; + + private Receiver mReceiver; + + public DetachableResultReceiver(Handler handler) { + super(handler); + } + + public void clearReceiver() { + mReceiver = null; + } + + public void setReceiver(Receiver receiver) { + mReceiver = receiver; + } + + public interface Receiver { + public void onReceiveResult(int resultCode, Bundle resultData); + } + + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (mReceiver != null) { + mReceiver.onReceiveResult(resultCode, resultData); + } else { + Log.w(TAG, "Dropping result on floor for code " + resultCode + ": " + + resultData.toString()); + } + } +} diff --git a/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/FlingGestureListener.java.svn-base b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/FlingGestureListener.java.svn-base new file mode 100644 index 0000000..beef88c --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/FlingGestureListener.java.svn-base @@ -0,0 +1,87 @@ +package com.loganlinn.pivotaltrackie.util; + +import android.content.Context; +import android.util.Log; +import android.view.MotionEvent; +import android.view.GestureDetector.SimpleOnGestureListener; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.ViewFlipper; + +import com.loganlinn.pivotaltrackie.R; + +public class FlingGestureListener extends SimpleOnGestureListener { + private static final String TAG = "FlingGestureListener"; + + private static final int SWIPE_MIN_DISTANCE = 120; + private static final int SWIPE_MAX_OFF_PATH = 250; + private static final int SWIPE_THRESHOLD_VELOCITY = 200; + + private ViewFlipper mViewFlipper; + private Animation mSlideRightIn; + private Animation mSlideRightOut; + private Animation mSlideLeftIn; + private Animation mSlideLeftOut; + + public int mFlipperLength = 2; // Number of views in the flipper + public int mCurrentFlipperIndex = 0; // Current position of the flipper - + + // used to avoid circular rotation + + public FlingGestureListener(ViewFlipper flipper) { + mViewFlipper = flipper; + final Context context = flipper.getContext(); + mSlideRightIn = AnimationUtils.loadAnimation(context, + R.anim.slide_right_in); + mSlideRightOut = AnimationUtils.loadAnimation(context, + R.anim.slide_right_out); + mSlideLeftIn = AnimationUtils.loadAnimation(context, + R.anim.slide_left_in); + mSlideLeftOut = AnimationUtils.loadAnimation(context, + R.anim.slide_left_out); + } + + /* + * (non-Javadoc) + * + * @see + * android.view.GestureDetector.SimpleOnGestureListener#onFling(android. + * view.MotionEvent, android.view.MotionEvent, float, float) + */ + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + // Flip between views, but do not rotate circularly + if (e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE + && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) { + + if (mCurrentFlipperIndex < mFlipperLength-1) { + mViewFlipper.setInAnimation(mSlideRightIn); + mViewFlipper.setOutAnimation(mSlideRightOut); + mViewFlipper.showNext(); + + mCurrentFlipperIndex++; + Log.i(TAG, "Flipped to next, ind=" + mCurrentFlipperIndex); + } else { + Log.i(TAG, "Didn't flip to next"); + + } + } else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE + && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) { + + if (mCurrentFlipperIndex > 0) { + mViewFlipper.setInAnimation(mSlideLeftIn); + mViewFlipper.setOutAnimation(mSlideLeftOut); + mViewFlipper.showPrevious(); + + mCurrentFlipperIndex--; + Log.i(TAG, "Flipped to previous, ind=" + mCurrentFlipperIndex); + } else { + Log.i(TAG, "Didn't flip to previous"); + } + } + + return super.onFling(e1, e2, velocityX, velocityY); + } + +} diff --git a/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/HttpUtils.java.svn-base b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/HttpUtils.java.svn-base new file mode 100644 index 0000000..48d54ab --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/HttpUtils.java.svn-base @@ -0,0 +1,195 @@ +package com.loganlinn.pivotaltrackie.util; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.util.zip.GZIPInputStream; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; + +import org.apache.http.Header; +import org.apache.http.HeaderElement; +import org.apache.http.HttpEntity; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponse; +import org.apache.http.HttpResponseInterceptor; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.conn.ssl.X509HostnameVerifier; +import org.apache.http.entity.HttpEntityWrapper; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.SingleClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; +import org.apache.http.protocol.HttpContext; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.text.format.DateUtils; +import android.util.Log; + +import com.loganlinn.pivotaltrackie.R; + +public class HttpUtils { + private static final int SECOND_IN_MILLIS = (int) DateUtils.SECOND_IN_MILLIS; + private static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding"; + private static final String ENCODING_GZIP = "gzip"; + + public static class PTHttpClient extends DefaultHttpClient { + private static final String TAG = "PTHttpClient"; + + final Context context; + + public PTHttpClient(HttpParams params, Context context) { + super(params); + this.context = context; + } + + @Override + protected ClientConnectionManager createClientConnectionManager() { + Log.i(TAG, "createClientConnectionManager"); + + SchemeRegistry registry = new SchemeRegistry(); + registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); + // Register for port 443 our SSLSocketFactory with our keystore + // to the ConnectionManager + registry.register(new Scheme("https", newSSLSocketFactory(), 443)); + return new SingleClientConnManager(getParams(), registry); + } + + private SSLSocketFactory newSSLSocketFactory() { + try { + // Get an instance of the Bouncy Castle KeyStore format + KeyStore trusted = KeyStore.getInstance("BKS"); + // Get the raw resource, which contains the keystore with + // your trusted certificates (root and any intermediate certs) + InputStream in = context.getResources().openRawResource(R.raw.pivotaltracker); + try { + // Initialize the keystore with the provided trusted certificates + // Also provide the password of the keystore + trusted.load(in, "mysecret".toCharArray()); + } finally { + in.close(); + } + // Pass the keystore to the SSLSocketFactory. The factory is responsible + // for the verification of the server certificate. + SSLSocketFactory sf = new SSLSocketFactory(trusted); + // Hostname verification from certificate + // http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d4e506 + sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER); + return sf; + } catch (Exception e) { + throw new AssertionError(e); + } + } + } + + /** + * Generate and return a {@link HttpClient} configured for general use, + * including setting an application-specific user-agent string. + */ + public static DefaultHttpClient getHttpClient(Context context) { + final HttpParams params = new BasicHttpParams(); + + // Use generous timeouts for slow mobile networks + HttpConnectionParams + .setConnectionTimeout(params, 20 * SECOND_IN_MILLIS); + HttpConnectionParams.setSoTimeout(params, 20 * SECOND_IN_MILLIS); + + HttpConnectionParams.setSocketBufferSize(params, 8192); + HttpProtocolParams.setUserAgent(params, buildUserAgent(context)); + + final DefaultHttpClient client = new DefaultHttpClient(params); + +// final PTHttpClient client = new PTHttpClient(params, context); +// HostnameVerifier hostnameVerifier = org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER; +// +// DefaultHttpClient _client = new DefaultHttpClient(); +// +// SchemeRegistry registry = new SchemeRegistry(); +// SSLSocketFactory socketFactory = SSLSocketFactory.getSocketFactory(); +// socketFactory.setHostnameVerifier((X509HostnameVerifier) hostnameVerifier); +// registry.register(new Scheme("https", socketFactory, 443)); +// SingleClientConnManager mgr = new SingleClientConnManager(_client.getParams(), registry); +// DefaultHttpClient client = new DefaultHttpClient(mgr, _client.getParams()); +// +// HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier); +// +// client.addRequestInterceptor(new HttpRequestInterceptor() { +// public void process(HttpRequest request, HttpContext context) { +// // Add header to accept gzip content +// if (!request.containsHeader(HEADER_ACCEPT_ENCODING)) { +// request.addHeader(HEADER_ACCEPT_ENCODING, ENCODING_GZIP); +// } +// +// } +// }); +// +// client.addResponseInterceptor(new HttpResponseInterceptor() { +// public void process(HttpResponse response, HttpContext context) { +// // Inflate any responses compressed with gzip +// final HttpEntity entity = response.getEntity(); +// final Header encoding = entity.getContentEncoding(); +// if (encoding != null) { +// for (HeaderElement element : encoding.getElements()) { +// if (element.getName().equalsIgnoreCase(ENCODING_GZIP)) { +// response.setEntity(new InflatingEntity(response +// .getEntity())); +// break; +// } +// } +// } +// } +// }); + + return client; + } + + /** + * Build and return a user-agent string that can identify this application + * to remote servers. Contains the package name and version code. + */ + private static String buildUserAgent(Context context) { + try { + final PackageManager manager = context.getPackageManager(); + final PackageInfo info = manager.getPackageInfo(context + .getPackageName(), 0); + + // Some APIs require "(gzip)" in the user-agent string. + return info.packageName + "/" + info.versionName + " (" + + info.versionCode + ") (gzip)"; + } catch (NameNotFoundException e) { + return null; + } + } + + /** + * Simple {@link HttpEntityWrapper} that inflates the wrapped + * {@link HttpEntity} by passing it through {@link GZIPInputStream}. + */ + private static class InflatingEntity extends HttpEntityWrapper { + public InflatingEntity(HttpEntity wrapped) { + super(wrapped); + } + + @Override + public InputStream getContent() throws IOException { + return new GZIPInputStream(wrappedEntity.getContent()); + } + + @Override + public long getContentLength() { + return -1; + } + } +} diff --git a/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/Lists.java.svn-base b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/Lists.java.svn-base new file mode 100644 index 0000000..c512569 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/Lists.java.svn-base @@ -0,0 +1,49 @@ +package com.loganlinn.pivotaltrackie.util; + +import java.util.ArrayList; +import java.util.Collections; + +/** + * Provides static methods for creating {@code List} instances easily, and other + * utility methods for working with lists. + */ +public class Lists { + + /** + * Creates an empty {@code ArrayList} instance. + * + *

Note: if you only need an immutable empty List, use + * {@link Collections#emptyList} instead. + * + * @return a newly-created, initially-empty {@code ArrayList} + */ + public static ArrayList newArrayList() { + return new ArrayList(); + } + + /** + * Creates a resizable {@code ArrayList} instance containing the given + * elements. + * + *

Note: due to a bug in javac 1.5.0_06, we cannot support the + * following: + * + *

{@code List list = Lists.newArrayList(sub1, sub2);} + * + *

where {@code sub1} and {@code sub2} are references to subtypes of + * {@code Base}, not of {@code Base} itself. To get around this, you must + * use: + * + *

{@code List list = Lists.newArrayList(sub1, sub2);} + * + * @param elements the elements that the list should contain, in order + * @return a newly-created {@code ArrayList} containing those elements + */ + public static ArrayList newArrayList(E... elements) { + int capacity = (elements.length * 110) / 100 + 5; + ArrayList list = new ArrayList(capacity); + Collections.addAll(list, elements); + return list; + } +} + diff --git a/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/Maps.java.svn-base b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/Maps.java.svn-base new file mode 100644 index 0000000..c4d4021 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/Maps.java.svn-base @@ -0,0 +1,27 @@ +package com.loganlinn.pivotaltrackie.util; + +import java.util.HashMap; +import java.util.LinkedHashMap; + +/** + * Provides static methods for creating mutable {@code Maps} instances easily. + */ +public class Maps { + /** + * Creates a {@code HashMap} instance. + * + * @return a newly-created, initially-empty {@code HashMap} + */ + public static HashMap newHashMap() { + return new HashMap(); + } + + /** + * Creates a {@code LinkedHashMap} instance. + * + * @return a newly-created, initially-empty {@code HashMap} + */ + public static LinkedHashMap newLinkedHashMap() { + return new LinkedHashMap(); + } +} diff --git a/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/NotifyingAsyncQueryHandler.java.svn-base b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/NotifyingAsyncQueryHandler.java.svn-base new file mode 100644 index 0000000..51cdfd4 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/NotifyingAsyncQueryHandler.java.svn-base @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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 com.loganlinn.pivotaltrackie.util; + +import java.lang.ref.WeakReference; + +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +/** + * Slightly more abstract {@link AsyncQueryHandler} that helps keep a + * {@link WeakReference} back to a listener. Will properly close any + * {@link Cursor} if the listener ceases to exist. + *

+ * This pattern can be used to perform background queries without leaking + * {@link Context} objects. + * + * @hide pending API council review + */ +public class NotifyingAsyncQueryHandler extends AsyncQueryHandler { + private WeakReference mListener; + + /** + * Interface to listen for completed query operations. + */ + public interface AsyncQueryListener { + void onQueryComplete(int token, Object cookie, Cursor cursor); + } + + public NotifyingAsyncQueryHandler(ContentResolver resolver, AsyncQueryListener listener) { + super(resolver); + setQueryListener(listener); + } + + /** + * Assign the given {@link AsyncQueryListener} to receive query events from + * asynchronous calls. Will replace any existing listener. + */ + public void setQueryListener(AsyncQueryListener listener) { + mListener = new WeakReference(listener); + } + + /** + * Clear any {@link AsyncQueryListener} set through + * {@link #setQueryListener(AsyncQueryListener)} + */ + public void clearQueryListener() { + mListener = null; + } + + /** + * Begin an asynchronous query with the given arguments. When finished, + * {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)} is + * called if a valid {@link AsyncQueryListener} is present. + */ + public void startQuery(Uri uri, String[] projection) { + startQuery(-1, null, uri, projection, null, null, null); + } + + /** + * Begin an asynchronous query with the given arguments. When finished, + * {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)} is called + * if a valid {@link AsyncQueryListener} is present. + * + * @param token Unique identifier passed through to + * {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)} + */ + public void startQuery(int token, Uri uri, String[] projection) { + startQuery(token, null, uri, projection, null, null, null); + } + + /** + * Begin an asynchronous query with the given arguments. When finished, + * {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)} is called + * if a valid {@link AsyncQueryListener} is present. + */ + public void startQuery(Uri uri, String[] projection, String sortOrder) { + startQuery(-1, null, uri, projection, null, null, sortOrder); + } + + /** + * Begin an asynchronous query with the given arguments. When finished, + * {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)} is called + * if a valid {@link AsyncQueryListener} is present. + */ + public void startQuery(Uri uri, String[] projection, String selection, + String[] selectionArgs, String orderBy) { + startQuery(-1, null, uri, projection, selection, selectionArgs, orderBy); + } + + /** + * Begin an asynchronous update with the given arguments. + */ + public void startUpdate(Uri uri, ContentValues values) { + startUpdate(-1, null, uri, values, null, null); + } + + public void startInsert(Uri uri, ContentValues values) { + startInsert(-1, null, uri, values); + } + + public void startDelete(Uri uri) { + startDelete(-1, null, uri, null, null); + } + + /** {@inheritDoc} */ + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + final AsyncQueryListener listener = mListener == null ? null : mListener.get(); + if (listener != null) { + listener.onQueryComplete(token, cookie, cursor); + } else if (cursor != null) { + cursor.close(); + } + } +} diff --git a/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/ParserUtils.java.svn-base b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/ParserUtils.java.svn-base new file mode 100644 index 0000000..200f91c --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/ParserUtils.java.svn-base @@ -0,0 +1,47 @@ +package com.loganlinn.pivotaltrackie.util; + +import java.io.InputStream; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.SyncColumns; + +public class ParserUtils { + + + private static XmlPullParserFactory factory_; + + public static XmlPullParser newPullParser(InputStream input) throws XmlPullParserException { + if(factory_ == null){ + factory_ = XmlPullParserFactory.newInstance(); + } + final XmlPullParser parser = factory_.newPullParser(); + parser.setInput(input, null); + return parser; + } + + /** + * Query and return the {@link SyncColumns#UPDATED} time for the requested + * {@link Uri}. Expects the {@link Uri} to reference a single item. + */ + public static long queryItemUpdated(Uri uri, ContentResolver resolver) { + final String[] projection = { SyncColumns.UPDATED }; + final Cursor cursor = resolver.query(uri, projection, null, null, null); + try { + if (cursor.moveToFirst()) { + return cursor.getLong(0); + } else { + return ProjectContract.UPDATED_NEVER; + } + } finally { + cursor.close(); + } + } +} diff --git a/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/PivotalTrackerToken.java.svn-base b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/PivotalTrackerToken.java.svn-base new file mode 100644 index 0000000..c0e2033 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/PivotalTrackerToken.java.svn-base @@ -0,0 +1,69 @@ +package com.loganlinn.pivotaltrackie.util; + +import java.io.IOException; +import java.io.InputStream; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.apache.http.HttpRequest; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +public class PivotalTrackerToken { + private static final String TAG = "PivotalTrackerToken"; + public static final String PREFS = "pivotaltracker_auth"; + public static final String TOKEN = "token"; + private static String sToken = null; + + private static final SharedPreferences getPrefs(Context ctx){ + return ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE); + } + + public static String getToken(Context ctx){ + if(sToken == null){ + + final SharedPreferences prefs = getPrefs(ctx); + sToken = prefs.getString(TOKEN, null); + + } + + return sToken; + } + + public static void clearToken(Context ctx){ + final SharedPreferences prefs = getPrefs(ctx); + prefs.edit().remove(TOKEN); + } + + private static String fetchToken(Context ctx, String username, String password){ + Log.i(TAG, "Fetching PT token for username="+username); + String token = null; + + DefaultHttpClient client = new DefaultHttpClient(); + HttpRequest req = new HttpGet(); + + + + getPrefs(ctx).edit().putString(TOKEN, token); + sToken = token; + return sToken; + } + + + private PivotalTrackerToken(){ + + } + + +} diff --git a/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/SelectionBuilder.java.svn-base b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/SelectionBuilder.java.svn-base new file mode 100644 index 0000000..2116ed3 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/.svn/text-base/SelectionBuilder.java.svn-base @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +/* + * Modifications: + * -Imported from AOSP frameworks/base/core/java/com/android/internal/content + * -Changed package name + */ + +package com.loganlinn.pivotaltrackie.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.text.TextUtils; +import android.util.Log; + + + +/** + * Helper for building selection clauses for {@link SQLiteDatabase}. Each + * appended clause is combined using {@code AND}. This class is not + * thread safe. + */ +public class SelectionBuilder { + private static final String TAG = "SelectionBuilder"; + private static final boolean LOGV = false; + + private String mTable = null; + private Map mProjectionMap = Maps.newHashMap(); + private StringBuilder mSelection = new StringBuilder(); + private ArrayList mSelectionArgs = Lists.newArrayList(); + + /** + * Reset any internal state, allowing this builder to be recycled. + */ + public SelectionBuilder reset() { + mTable = null; + mSelection.setLength(0); + mSelectionArgs.clear(); + return this; + } + + /** + * Append the given selection clause to the internal state. Each clause is + * surrounded with parenthesis and combined using {@code AND}. + */ + public SelectionBuilder where(String selection, String... selectionArgs) { + if (TextUtils.isEmpty(selection)) { + if (selectionArgs != null && selectionArgs.length > 0) { + throw new IllegalArgumentException( + "Valid selection required when including arguments="); + } + + // Shortcut when clause is empty + return this; + } + + if (mSelection.length() > 0) { + mSelection.append(" AND "); + } + + mSelection.append("(").append(selection).append(")"); + if (selectionArgs != null) { + for (String arg : selectionArgs) { + mSelectionArgs.add(arg); + } + } + + return this; + } + + public SelectionBuilder table(String table) { + mTable = table; + return this; + } + + private void assertTable() { + if (mTable == null) { + throw new IllegalStateException("Table not specified"); + } + } + + public SelectionBuilder mapToTable(String column, String table) { + mProjectionMap.put(column, table + "." + column); + return this; + } + + public SelectionBuilder map(String fromColumn, String toClause) { + mProjectionMap.put(fromColumn, toClause + " AS " + fromColumn); + return this; + } + + /** + * Return selection string for current internal state. + * + * @see #getSelectionArgs() + */ + public String getSelection() { + return mSelection.toString(); + } + + /** + * Return selection arguments for current internal state. + * + * @see #getSelection() + */ + public String[] getSelectionArgs() { + return mSelectionArgs.toArray(new String[mSelectionArgs.size()]); + } + + private void mapColumns(String[] columns) { + for (int i = 0; i < columns.length; i++) { + final String target = mProjectionMap.get(columns[i]); + if (target != null) { + columns[i] = target; + } + } + } + + @Override + public String toString() { + return "SelectionBuilder[table=" + mTable + ", selection=" + getSelection() + + ", selectionArgs=" + Arrays.toString(getSelectionArgs()) + "]"; + } + + /** + * Execute query using the current internal state as {@code WHERE} clause. + */ + public Cursor query(SQLiteDatabase db, String[] columns, String orderBy) { + return query(db, columns, null, null, orderBy, null); + } + + /** + * Execute query using the current internal state as {@code WHERE} clause. + */ + public Cursor query(SQLiteDatabase db, String[] columns, String groupBy, + String having, String orderBy, String limit) { + assertTable(); + if (columns != null) mapColumns(columns); + if (LOGV) Log.v(TAG, "query(columns=" + Arrays.toString(columns) + ") " + this); + return db.query(mTable, columns, getSelection(), getSelectionArgs(), groupBy, having, + orderBy, limit); + } + + /** + * Execute update using the current internal state as {@code WHERE} clause. + */ + public int update(SQLiteDatabase db, ContentValues values) { + assertTable(); + if (LOGV) Log.v(TAG, "update() " + this); + return db.update(mTable, values, getSelection(), getSelectionArgs()); + } + + /** + * Execute delete using the current internal state as {@code WHERE} clause. + */ + public int delete(SQLiteDatabase db) { + assertTable(); + if (LOGV) Log.v(TAG, "delete() " + this); + return db.delete(mTable, getSelection(), getSelectionArgs()); + } +} diff --git a/src/com/loganlinn/pivotaltrackie/util/DateTimeUtils.java b/src/com/loganlinn/pivotaltrackie/util/DateTimeUtils.java new file mode 100644 index 0000000..a4d94ce --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/DateTimeUtils.java @@ -0,0 +1,103 @@ +package com.loganlinn.pivotaltrackie.util; + +import java.text.DateFormatSymbols; +import java.util.Calendar; + +import android.content.Context; +import android.text.format.DateUtils; + +import com.loganlinn.pivotaltrackie.R; + +public class DateTimeUtils extends DateUtils { + + private static String mTimestampLabelYesterday; + private static String mTimestampLabelToday; + private static String mTimestampLabelJustNow; + private static String mTimestampLabelMinutesAgo; + private static String mTimestampLabelHoursAgo; + private static String mTimestampLabelHourAgo; + private static Context sContext; + private static DateTimeUtils sInstance; + /** + * Singleton constructor, needed to get access to the application context & strings for i18n + * @param context Context + * @return DateTimeUtils singleton instance + * @throws Exception + */ + public static DateTimeUtils getInstance(Context context) { + sContext = context; + if (sInstance == null) { + sInstance = new DateTimeUtils(); + mTimestampLabelYesterday = context.getResources().getString(R.string.WidgetProvider_timestamp_yesterday); + mTimestampLabelToday = context.getResources().getString(R.string.WidgetProvider_timestamp_today); + mTimestampLabelJustNow = context.getResources().getString(R.string.WidgetProvider_timestamp_just_now); + mTimestampLabelMinutesAgo = context.getResources().getString(R.string.WidgetProvider_timestamp_minutes_ago); + mTimestampLabelHoursAgo = context.getResources().getString(R.string.WidgetProvider_timestamp_hours_ago); + mTimestampLabelHourAgo = context.getResources().getString(R.string.WidgetProvider_timestamp_hour_ago); + } + return sInstance; + } + + /** + * Checks if the given date is yesterday. + * + * @param date - Date to check. + * @return TRUE if the date is yesterday, FALSE otherwise. + */ + public static boolean isYesterday(long date) { + + final Calendar currentDate = Calendar.getInstance(); + currentDate.setTimeInMillis(date); + + final Calendar yesterdayDate = Calendar.getInstance(); + yesterdayDate.add(Calendar.DATE, -1); + + return yesterdayDate.get(Calendar.YEAR) == currentDate.get(Calendar.YEAR) && yesterdayDate.get(Calendar.DAY_OF_YEAR) == currentDate.get(Calendar.DAY_OF_YEAR); + } + + public static String[] weekdays = new DateFormatSymbols().getWeekdays(); // get day names + public static final long millisInADay = 1000 * 60 * 60 * 24; + + + + /** + * Displays a user-friendly date difference string + * @param timedate Timestamp to format as date difference from now + * @return Friendly-formatted date diff string + */ + public String getTimeDiffString(long timedate) { + Calendar startDateTime = Calendar.getInstance(); + Calendar endDateTime = Calendar.getInstance(); + endDateTime.setTimeInMillis(timedate); + long milliseconds1 = startDateTime.getTimeInMillis(); + long milliseconds2 = endDateTime.getTimeInMillis(); + long diff = milliseconds1 - milliseconds2; + + long hours = diff / (60 * 60 * 1000); + long minutes = diff / (60 * 1000); + minutes = minutes - 60 * hours; + long seconds = diff / (1000); + + boolean isToday = DateTimeUtils.isToday(timedate); + boolean isYesterday = DateTimeUtils.isYesterday(timedate); + + if (hours > 0 && hours < 12) { + return hours==1? String.format(mTimestampLabelHourAgo,hours) : String.format(mTimestampLabelHoursAgo,hours); + } else if (hours <= 0) { + if (minutes > 0) + return String.format(mTimestampLabelMinutesAgo,minutes); + else { + return mTimestampLabelJustNow; + } + } else if (isToday) { + return mTimestampLabelToday; + } else if (isYesterday) { + return mTimestampLabelYesterday; + } else if (startDateTime.getTimeInMillis() - timedate < millisInADay * 6) { + return weekdays[endDateTime.get(Calendar.DAY_OF_WEEK)]; + } else { + return formatDateTime(sContext, timedate, DateUtils.FORMAT_NUMERIC_DATE); + } + } + +} diff --git a/src/com/loganlinn/pivotaltrackie/util/DetachableResultReceiver.java b/src/com/loganlinn/pivotaltrackie/util/DetachableResultReceiver.java new file mode 100644 index 0000000..22aec47 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/DetachableResultReceiver.java @@ -0,0 +1,60 @@ +/* + * Copyright 2010 Google Inc. + * + * 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 com.loganlinn.pivotaltrackie.util; + +import android.app.Activity; +import android.app.Service; +import android.os.Bundle; +import android.os.Handler; +import android.os.ResultReceiver; +import android.util.Log; + +/** + * Proxy {@link ResultReceiver} that offers a listener interface that can be + * detached. Useful for when sending callbacks to a {@link Service} where a + * listening {@link Activity} can be swapped out during configuration changes. + */ +public class DetachableResultReceiver extends ResultReceiver { + private static final String TAG = "DetachableResultReceiver"; + + private Receiver mReceiver; + + public DetachableResultReceiver(Handler handler) { + super(handler); + } + + public void clearReceiver() { + mReceiver = null; + } + + public void setReceiver(Receiver receiver) { + mReceiver = receiver; + } + + public interface Receiver { + public void onReceiveResult(int resultCode, Bundle resultData); + } + + @Override + protected void onReceiveResult(int resultCode, Bundle resultData) { + if (mReceiver != null) { + mReceiver.onReceiveResult(resultCode, resultData); + } else { + Log.w(TAG, "Dropping result on floor for code " + resultCode + ": " + + resultData.toString()); + } + } +} diff --git a/src/com/loganlinn/pivotaltrackie/util/FlingGestureListener.java b/src/com/loganlinn/pivotaltrackie/util/FlingGestureListener.java new file mode 100644 index 0000000..beef88c --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/FlingGestureListener.java @@ -0,0 +1,87 @@ +package com.loganlinn.pivotaltrackie.util; + +import android.content.Context; +import android.util.Log; +import android.view.MotionEvent; +import android.view.GestureDetector.SimpleOnGestureListener; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.ViewFlipper; + +import com.loganlinn.pivotaltrackie.R; + +public class FlingGestureListener extends SimpleOnGestureListener { + private static final String TAG = "FlingGestureListener"; + + private static final int SWIPE_MIN_DISTANCE = 120; + private static final int SWIPE_MAX_OFF_PATH = 250; + private static final int SWIPE_THRESHOLD_VELOCITY = 200; + + private ViewFlipper mViewFlipper; + private Animation mSlideRightIn; + private Animation mSlideRightOut; + private Animation mSlideLeftIn; + private Animation mSlideLeftOut; + + public int mFlipperLength = 2; // Number of views in the flipper + public int mCurrentFlipperIndex = 0; // Current position of the flipper - + + // used to avoid circular rotation + + public FlingGestureListener(ViewFlipper flipper) { + mViewFlipper = flipper; + final Context context = flipper.getContext(); + mSlideRightIn = AnimationUtils.loadAnimation(context, + R.anim.slide_right_in); + mSlideRightOut = AnimationUtils.loadAnimation(context, + R.anim.slide_right_out); + mSlideLeftIn = AnimationUtils.loadAnimation(context, + R.anim.slide_left_in); + mSlideLeftOut = AnimationUtils.loadAnimation(context, + R.anim.slide_left_out); + } + + /* + * (non-Javadoc) + * + * @see + * android.view.GestureDetector.SimpleOnGestureListener#onFling(android. + * view.MotionEvent, android.view.MotionEvent, float, float) + */ + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, + float velocityY) { + // Flip between views, but do not rotate circularly + if (e1.getX() - e2.getX() > SWIPE_MIN_DISTANCE + && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) { + + if (mCurrentFlipperIndex < mFlipperLength-1) { + mViewFlipper.setInAnimation(mSlideRightIn); + mViewFlipper.setOutAnimation(mSlideRightOut); + mViewFlipper.showNext(); + + mCurrentFlipperIndex++; + Log.i(TAG, "Flipped to next, ind=" + mCurrentFlipperIndex); + } else { + Log.i(TAG, "Didn't flip to next"); + + } + } else if (e2.getX() - e1.getX() > SWIPE_MIN_DISTANCE + && Math.abs(velocityX) > SWIPE_THRESHOLD_VELOCITY) { + + if (mCurrentFlipperIndex > 0) { + mViewFlipper.setInAnimation(mSlideLeftIn); + mViewFlipper.setOutAnimation(mSlideLeftOut); + mViewFlipper.showPrevious(); + + mCurrentFlipperIndex--; + Log.i(TAG, "Flipped to previous, ind=" + mCurrentFlipperIndex); + } else { + Log.i(TAG, "Didn't flip to previous"); + } + } + + return super.onFling(e1, e2, velocityX, velocityY); + } + +} diff --git a/src/com/loganlinn/pivotaltrackie/util/HttpUtils.java b/src/com/loganlinn/pivotaltrackie/util/HttpUtils.java new file mode 100644 index 0000000..48d54ab --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/HttpUtils.java @@ -0,0 +1,195 @@ +package com.loganlinn.pivotaltrackie.util; + +import java.io.IOException; +import java.io.InputStream; +import java.security.KeyStore; +import java.util.zip.GZIPInputStream; + +import javax.net.ssl.HostnameVerifier; +import javax.net.ssl.HttpsURLConnection; + +import org.apache.http.Header; +import org.apache.http.HeaderElement; +import org.apache.http.HttpEntity; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.HttpResponse; +import org.apache.http.HttpResponseInterceptor; +import org.apache.http.client.HttpClient; +import org.apache.http.conn.ClientConnectionManager; +import org.apache.http.conn.scheme.PlainSocketFactory; +import org.apache.http.conn.scheme.Scheme; +import org.apache.http.conn.scheme.SchemeRegistry; +import org.apache.http.conn.ssl.SSLSocketFactory; +import org.apache.http.conn.ssl.X509HostnameVerifier; +import org.apache.http.entity.HttpEntityWrapper; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.impl.conn.SingleClientConnManager; +import org.apache.http.params.BasicHttpParams; +import org.apache.http.params.HttpConnectionParams; +import org.apache.http.params.HttpParams; +import org.apache.http.params.HttpProtocolParams; +import org.apache.http.protocol.HttpContext; + +import android.content.Context; +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.content.pm.PackageManager.NameNotFoundException; +import android.text.format.DateUtils; +import android.util.Log; + +import com.loganlinn.pivotaltrackie.R; + +public class HttpUtils { + private static final int SECOND_IN_MILLIS = (int) DateUtils.SECOND_IN_MILLIS; + private static final String HEADER_ACCEPT_ENCODING = "Accept-Encoding"; + private static final String ENCODING_GZIP = "gzip"; + + public static class PTHttpClient extends DefaultHttpClient { + private static final String TAG = "PTHttpClient"; + + final Context context; + + public PTHttpClient(HttpParams params, Context context) { + super(params); + this.context = context; + } + + @Override + protected ClientConnectionManager createClientConnectionManager() { + Log.i(TAG, "createClientConnectionManager"); + + SchemeRegistry registry = new SchemeRegistry(); + registry.register(new Scheme("http", PlainSocketFactory.getSocketFactory(), 80)); + // Register for port 443 our SSLSocketFactory with our keystore + // to the ConnectionManager + registry.register(new Scheme("https", newSSLSocketFactory(), 443)); + return new SingleClientConnManager(getParams(), registry); + } + + private SSLSocketFactory newSSLSocketFactory() { + try { + // Get an instance of the Bouncy Castle KeyStore format + KeyStore trusted = KeyStore.getInstance("BKS"); + // Get the raw resource, which contains the keystore with + // your trusted certificates (root and any intermediate certs) + InputStream in = context.getResources().openRawResource(R.raw.pivotaltracker); + try { + // Initialize the keystore with the provided trusted certificates + // Also provide the password of the keystore + trusted.load(in, "mysecret".toCharArray()); + } finally { + in.close(); + } + // Pass the keystore to the SSLSocketFactory. The factory is responsible + // for the verification of the server certificate. + SSLSocketFactory sf = new SSLSocketFactory(trusted); + // Hostname verification from certificate + // http://hc.apache.org/httpcomponents-client-ga/tutorial/html/connmgmt.html#d4e506 + sf.setHostnameVerifier(SSLSocketFactory.STRICT_HOSTNAME_VERIFIER); + return sf; + } catch (Exception e) { + throw new AssertionError(e); + } + } + } + + /** + * Generate and return a {@link HttpClient} configured for general use, + * including setting an application-specific user-agent string. + */ + public static DefaultHttpClient getHttpClient(Context context) { + final HttpParams params = new BasicHttpParams(); + + // Use generous timeouts for slow mobile networks + HttpConnectionParams + .setConnectionTimeout(params, 20 * SECOND_IN_MILLIS); + HttpConnectionParams.setSoTimeout(params, 20 * SECOND_IN_MILLIS); + + HttpConnectionParams.setSocketBufferSize(params, 8192); + HttpProtocolParams.setUserAgent(params, buildUserAgent(context)); + + final DefaultHttpClient client = new DefaultHttpClient(params); + +// final PTHttpClient client = new PTHttpClient(params, context); +// HostnameVerifier hostnameVerifier = org.apache.http.conn.ssl.SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER; +// +// DefaultHttpClient _client = new DefaultHttpClient(); +// +// SchemeRegistry registry = new SchemeRegistry(); +// SSLSocketFactory socketFactory = SSLSocketFactory.getSocketFactory(); +// socketFactory.setHostnameVerifier((X509HostnameVerifier) hostnameVerifier); +// registry.register(new Scheme("https", socketFactory, 443)); +// SingleClientConnManager mgr = new SingleClientConnManager(_client.getParams(), registry); +// DefaultHttpClient client = new DefaultHttpClient(mgr, _client.getParams()); +// +// HttpsURLConnection.setDefaultHostnameVerifier(hostnameVerifier); +// +// client.addRequestInterceptor(new HttpRequestInterceptor() { +// public void process(HttpRequest request, HttpContext context) { +// // Add header to accept gzip content +// if (!request.containsHeader(HEADER_ACCEPT_ENCODING)) { +// request.addHeader(HEADER_ACCEPT_ENCODING, ENCODING_GZIP); +// } +// +// } +// }); +// +// client.addResponseInterceptor(new HttpResponseInterceptor() { +// public void process(HttpResponse response, HttpContext context) { +// // Inflate any responses compressed with gzip +// final HttpEntity entity = response.getEntity(); +// final Header encoding = entity.getContentEncoding(); +// if (encoding != null) { +// for (HeaderElement element : encoding.getElements()) { +// if (element.getName().equalsIgnoreCase(ENCODING_GZIP)) { +// response.setEntity(new InflatingEntity(response +// .getEntity())); +// break; +// } +// } +// } +// } +// }); + + return client; + } + + /** + * Build and return a user-agent string that can identify this application + * to remote servers. Contains the package name and version code. + */ + private static String buildUserAgent(Context context) { + try { + final PackageManager manager = context.getPackageManager(); + final PackageInfo info = manager.getPackageInfo(context + .getPackageName(), 0); + + // Some APIs require "(gzip)" in the user-agent string. + return info.packageName + "/" + info.versionName + " (" + + info.versionCode + ") (gzip)"; + } catch (NameNotFoundException e) { + return null; + } + } + + /** + * Simple {@link HttpEntityWrapper} that inflates the wrapped + * {@link HttpEntity} by passing it through {@link GZIPInputStream}. + */ + private static class InflatingEntity extends HttpEntityWrapper { + public InflatingEntity(HttpEntity wrapped) { + super(wrapped); + } + + @Override + public InputStream getContent() throws IOException { + return new GZIPInputStream(wrappedEntity.getContent()); + } + + @Override + public long getContentLength() { + return -1; + } + } +} diff --git a/src/com/loganlinn/pivotaltrackie/util/Lists.java b/src/com/loganlinn/pivotaltrackie/util/Lists.java new file mode 100644 index 0000000..c512569 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/Lists.java @@ -0,0 +1,49 @@ +package com.loganlinn.pivotaltrackie.util; + +import java.util.ArrayList; +import java.util.Collections; + +/** + * Provides static methods for creating {@code List} instances easily, and other + * utility methods for working with lists. + */ +public class Lists { + + /** + * Creates an empty {@code ArrayList} instance. + * + *

Note: if you only need an immutable empty List, use + * {@link Collections#emptyList} instead. + * + * @return a newly-created, initially-empty {@code ArrayList} + */ + public static ArrayList newArrayList() { + return new ArrayList(); + } + + /** + * Creates a resizable {@code ArrayList} instance containing the given + * elements. + * + *

Note: due to a bug in javac 1.5.0_06, we cannot support the + * following: + * + *

{@code List list = Lists.newArrayList(sub1, sub2);} + * + *

where {@code sub1} and {@code sub2} are references to subtypes of + * {@code Base}, not of {@code Base} itself. To get around this, you must + * use: + * + *

{@code List list = Lists.newArrayList(sub1, sub2);} + * + * @param elements the elements that the list should contain, in order + * @return a newly-created {@code ArrayList} containing those elements + */ + public static ArrayList newArrayList(E... elements) { + int capacity = (elements.length * 110) / 100 + 5; + ArrayList list = new ArrayList(capacity); + Collections.addAll(list, elements); + return list; + } +} + diff --git a/src/com/loganlinn/pivotaltrackie/util/Maps.java b/src/com/loganlinn/pivotaltrackie/util/Maps.java new file mode 100644 index 0000000..c4d4021 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/Maps.java @@ -0,0 +1,27 @@ +package com.loganlinn.pivotaltrackie.util; + +import java.util.HashMap; +import java.util.LinkedHashMap; + +/** + * Provides static methods for creating mutable {@code Maps} instances easily. + */ +public class Maps { + /** + * Creates a {@code HashMap} instance. + * + * @return a newly-created, initially-empty {@code HashMap} + */ + public static HashMap newHashMap() { + return new HashMap(); + } + + /** + * Creates a {@code LinkedHashMap} instance. + * + * @return a newly-created, initially-empty {@code HashMap} + */ + public static LinkedHashMap newLinkedHashMap() { + return new LinkedHashMap(); + } +} diff --git a/src/com/loganlinn/pivotaltrackie/util/NotifyingAsyncQueryHandler.java b/src/com/loganlinn/pivotaltrackie/util/NotifyingAsyncQueryHandler.java new file mode 100644 index 0000000..51cdfd4 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/NotifyingAsyncQueryHandler.java @@ -0,0 +1,134 @@ +/* + * Copyright (C) 2009 The Android Open Source Project + * + * 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 com.loganlinn.pivotaltrackie.util; + +import java.lang.ref.WeakReference; + +import android.content.AsyncQueryHandler; +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; + +/** + * Slightly more abstract {@link AsyncQueryHandler} that helps keep a + * {@link WeakReference} back to a listener. Will properly close any + * {@link Cursor} if the listener ceases to exist. + *

+ * This pattern can be used to perform background queries without leaking + * {@link Context} objects. + * + * @hide pending API council review + */ +public class NotifyingAsyncQueryHandler extends AsyncQueryHandler { + private WeakReference mListener; + + /** + * Interface to listen for completed query operations. + */ + public interface AsyncQueryListener { + void onQueryComplete(int token, Object cookie, Cursor cursor); + } + + public NotifyingAsyncQueryHandler(ContentResolver resolver, AsyncQueryListener listener) { + super(resolver); + setQueryListener(listener); + } + + /** + * Assign the given {@link AsyncQueryListener} to receive query events from + * asynchronous calls. Will replace any existing listener. + */ + public void setQueryListener(AsyncQueryListener listener) { + mListener = new WeakReference(listener); + } + + /** + * Clear any {@link AsyncQueryListener} set through + * {@link #setQueryListener(AsyncQueryListener)} + */ + public void clearQueryListener() { + mListener = null; + } + + /** + * Begin an asynchronous query with the given arguments. When finished, + * {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)} is + * called if a valid {@link AsyncQueryListener} is present. + */ + public void startQuery(Uri uri, String[] projection) { + startQuery(-1, null, uri, projection, null, null, null); + } + + /** + * Begin an asynchronous query with the given arguments. When finished, + * {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)} is called + * if a valid {@link AsyncQueryListener} is present. + * + * @param token Unique identifier passed through to + * {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)} + */ + public void startQuery(int token, Uri uri, String[] projection) { + startQuery(token, null, uri, projection, null, null, null); + } + + /** + * Begin an asynchronous query with the given arguments. When finished, + * {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)} is called + * if a valid {@link AsyncQueryListener} is present. + */ + public void startQuery(Uri uri, String[] projection, String sortOrder) { + startQuery(-1, null, uri, projection, null, null, sortOrder); + } + + /** + * Begin an asynchronous query with the given arguments. When finished, + * {@link AsyncQueryListener#onQueryComplete(int, Object, Cursor)} is called + * if a valid {@link AsyncQueryListener} is present. + */ + public void startQuery(Uri uri, String[] projection, String selection, + String[] selectionArgs, String orderBy) { + startQuery(-1, null, uri, projection, selection, selectionArgs, orderBy); + } + + /** + * Begin an asynchronous update with the given arguments. + */ + public void startUpdate(Uri uri, ContentValues values) { + startUpdate(-1, null, uri, values, null, null); + } + + public void startInsert(Uri uri, ContentValues values) { + startInsert(-1, null, uri, values); + } + + public void startDelete(Uri uri) { + startDelete(-1, null, uri, null, null); + } + + /** {@inheritDoc} */ + @Override + protected void onQueryComplete(int token, Object cookie, Cursor cursor) { + final AsyncQueryListener listener = mListener == null ? null : mListener.get(); + if (listener != null) { + listener.onQueryComplete(token, cookie, cursor); + } else if (cursor != null) { + cursor.close(); + } + } +} diff --git a/src/com/loganlinn/pivotaltrackie/util/ParserUtils.java b/src/com/loganlinn/pivotaltrackie/util/ParserUtils.java new file mode 100644 index 0000000..200f91c --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/ParserUtils.java @@ -0,0 +1,47 @@ +package com.loganlinn.pivotaltrackie.util; + +import java.io.InputStream; + +import org.xmlpull.v1.XmlPullParser; +import org.xmlpull.v1.XmlPullParserException; +import org.xmlpull.v1.XmlPullParserFactory; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; + +import com.loganlinn.pivotaltrackie.provider.ProjectContract; +import com.loganlinn.pivotaltrackie.provider.ProjectContract.SyncColumns; + +public class ParserUtils { + + + private static XmlPullParserFactory factory_; + + public static XmlPullParser newPullParser(InputStream input) throws XmlPullParserException { + if(factory_ == null){ + factory_ = XmlPullParserFactory.newInstance(); + } + final XmlPullParser parser = factory_.newPullParser(); + parser.setInput(input, null); + return parser; + } + + /** + * Query and return the {@link SyncColumns#UPDATED} time for the requested + * {@link Uri}. Expects the {@link Uri} to reference a single item. + */ + public static long queryItemUpdated(Uri uri, ContentResolver resolver) { + final String[] projection = { SyncColumns.UPDATED }; + final Cursor cursor = resolver.query(uri, projection, null, null, null); + try { + if (cursor.moveToFirst()) { + return cursor.getLong(0); + } else { + return ProjectContract.UPDATED_NEVER; + } + } finally { + cursor.close(); + } + } +} diff --git a/src/com/loganlinn/pivotaltrackie/util/PivotalTrackerToken.java b/src/com/loganlinn/pivotaltrackie/util/PivotalTrackerToken.java new file mode 100644 index 0000000..c0e2033 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/PivotalTrackerToken.java @@ -0,0 +1,69 @@ +package com.loganlinn.pivotaltrackie.util; + +import java.io.IOException; +import java.io.InputStream; + +import javax.xml.parsers.ParserConfigurationException; +import javax.xml.parsers.SAXParser; +import javax.xml.parsers.SAXParserFactory; + +import org.apache.http.HttpRequest; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.impl.client.DefaultHttpClient; +import org.xml.sax.Attributes; +import org.xml.sax.InputSource; +import org.xml.sax.SAXException; +import org.xml.sax.XMLReader; +import org.xml.sax.helpers.DefaultHandler; + +import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; + +public class PivotalTrackerToken { + private static final String TAG = "PivotalTrackerToken"; + public static final String PREFS = "pivotaltracker_auth"; + public static final String TOKEN = "token"; + private static String sToken = null; + + private static final SharedPreferences getPrefs(Context ctx){ + return ctx.getSharedPreferences(PREFS, Context.MODE_PRIVATE); + } + + public static String getToken(Context ctx){ + if(sToken == null){ + + final SharedPreferences prefs = getPrefs(ctx); + sToken = prefs.getString(TOKEN, null); + + } + + return sToken; + } + + public static void clearToken(Context ctx){ + final SharedPreferences prefs = getPrefs(ctx); + prefs.edit().remove(TOKEN); + } + + private static String fetchToken(Context ctx, String username, String password){ + Log.i(TAG, "Fetching PT token for username="+username); + String token = null; + + DefaultHttpClient client = new DefaultHttpClient(); + HttpRequest req = new HttpGet(); + + + + getPrefs(ctx).edit().putString(TOKEN, token); + sToken = token; + return sToken; + } + + + private PivotalTrackerToken(){ + + } + + +} diff --git a/src/com/loganlinn/pivotaltrackie/util/SelectionBuilder.java b/src/com/loganlinn/pivotaltrackie/util/SelectionBuilder.java new file mode 100644 index 0000000..2116ed3 --- /dev/null +++ b/src/com/loganlinn/pivotaltrackie/util/SelectionBuilder.java @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2010 The Android Open Source Project + * + * 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. + */ + +/* + * Modifications: + * -Imported from AOSP frameworks/base/core/java/com/android/internal/content + * -Changed package name + */ + +package com.loganlinn.pivotaltrackie.util; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Map; + +import android.content.ContentValues; +import android.database.Cursor; +import android.database.sqlite.SQLiteDatabase; +import android.text.TextUtils; +import android.util.Log; + + + +/** + * Helper for building selection clauses for {@link SQLiteDatabase}. Each + * appended clause is combined using {@code AND}. This class is not + * thread safe. + */ +public class SelectionBuilder { + private static final String TAG = "SelectionBuilder"; + private static final boolean LOGV = false; + + private String mTable = null; + private Map mProjectionMap = Maps.newHashMap(); + private StringBuilder mSelection = new StringBuilder(); + private ArrayList mSelectionArgs = Lists.newArrayList(); + + /** + * Reset any internal state, allowing this builder to be recycled. + */ + public SelectionBuilder reset() { + mTable = null; + mSelection.setLength(0); + mSelectionArgs.clear(); + return this; + } + + /** + * Append the given selection clause to the internal state. Each clause is + * surrounded with parenthesis and combined using {@code AND}. + */ + public SelectionBuilder where(String selection, String... selectionArgs) { + if (TextUtils.isEmpty(selection)) { + if (selectionArgs != null && selectionArgs.length > 0) { + throw new IllegalArgumentException( + "Valid selection required when including arguments="); + } + + // Shortcut when clause is empty + return this; + } + + if (mSelection.length() > 0) { + mSelection.append(" AND "); + } + + mSelection.append("(").append(selection).append(")"); + if (selectionArgs != null) { + for (String arg : selectionArgs) { + mSelectionArgs.add(arg); + } + } + + return this; + } + + public SelectionBuilder table(String table) { + mTable = table; + return this; + } + + private void assertTable() { + if (mTable == null) { + throw new IllegalStateException("Table not specified"); + } + } + + public SelectionBuilder mapToTable(String column, String table) { + mProjectionMap.put(column, table + "." + column); + return this; + } + + public SelectionBuilder map(String fromColumn, String toClause) { + mProjectionMap.put(fromColumn, toClause + " AS " + fromColumn); + return this; + } + + /** + * Return selection string for current internal state. + * + * @see #getSelectionArgs() + */ + public String getSelection() { + return mSelection.toString(); + } + + /** + * Return selection arguments for current internal state. + * + * @see #getSelection() + */ + public String[] getSelectionArgs() { + return mSelectionArgs.toArray(new String[mSelectionArgs.size()]); + } + + private void mapColumns(String[] columns) { + for (int i = 0; i < columns.length; i++) { + final String target = mProjectionMap.get(columns[i]); + if (target != null) { + columns[i] = target; + } + } + } + + @Override + public String toString() { + return "SelectionBuilder[table=" + mTable + ", selection=" + getSelection() + + ", selectionArgs=" + Arrays.toString(getSelectionArgs()) + "]"; + } + + /** + * Execute query using the current internal state as {@code WHERE} clause. + */ + public Cursor query(SQLiteDatabase db, String[] columns, String orderBy) { + return query(db, columns, null, null, orderBy, null); + } + + /** + * Execute query using the current internal state as {@code WHERE} clause. + */ + public Cursor query(SQLiteDatabase db, String[] columns, String groupBy, + String having, String orderBy, String limit) { + assertTable(); + if (columns != null) mapColumns(columns); + if (LOGV) Log.v(TAG, "query(columns=" + Arrays.toString(columns) + ") " + this); + return db.query(mTable, columns, getSelection(), getSelectionArgs(), groupBy, having, + orderBy, limit); + } + + /** + * Execute update using the current internal state as {@code WHERE} clause. + */ + public int update(SQLiteDatabase db, ContentValues values) { + assertTable(); + if (LOGV) Log.v(TAG, "update() " + this); + return db.update(mTable, values, getSelection(), getSelectionArgs()); + } + + /** + * Execute delete using the current internal state as {@code WHERE} clause. + */ + public int delete(SQLiteDatabase db) { + assertTable(); + if (LOGV) Log.v(TAG, "delete() " + this); + return db.delete(mTable, getSelection(), getSelectionArgs()); + } +}