diff --git a/JoggingActivity.scala b/JoggingActivity.scala
new file mode 100644
index 0000000..69ddc1c
--- /dev/null
+++ b/JoggingActivity.scala
@@ -0,0 +1,234 @@
+package de.tomhoefer.jogga
+
+import android.app.Activity
+import android.os.Bundle
+import android.content.Intent
+import android.util.AttributeSet
+import android.view.{View, LayoutInflater, WindowManager}
+import android.widget.{RelativeLayout, LinearLayout, Button, TextView}
+import android.content.Context
+import android.util.Log
+import extensions._
+import extensions.implicits._
+import extensions.ViewGroupExt
+import components._
+import components.linechart._
+import android.view.View.{VISIBLE, INVISIBLE, GONE}
+import concurrent.ops._
+import views.JoggingView
+import models.joggingModel
+import actors.Actor
+import android.graphics.{BitmapFactory, Bitmap, Paint, Canvas}
+
+
+import android.speech.tts.TextToSpeech._
+
+/*
+ * Start-Message fuer den internen Actor - Android scheint self als Actor nicht zu unterstuetzen
+ */
+case class Start
+
+/**
+ * BasisActivity des Screens, der waehrend des Laufens sichtbar ist
+ *
+ * @author tom
+ *
+ */
+class JoggingActivity extends AppDefaultActivity { self =>
+
+ /*
+ * Layout des GPS-Wartescreen
+ */
+ var gps:RelativeLayout = _
+ /*
+ * Buttons die den Status der GPS-Ueberwachung anzeigen
+ */
+ var gpsWaitingView:TextView = _
+ var gpsFoundView:TextView = _
+ var gpsReadyView:TextView = _
+
+ /*
+ * Layout der JoggingScreens
+ */
+ var joggingView:JoggingView = _
+
+ /*
+ * Zeitgeber
+ */
+ var timer:Timer = _
+ /*
+ * Schrittzaehler
+ */
+ var pedometer:Pedometer = _
+ /*
+ * GPS-Tracker
+ */
+ var tracker:Tracker = _
+ /*
+ * Sprach-Synthetisierung
+ */
+ var speecher:Speecher = _
+
+/*
+ override def onActivityResult(requestCode:Int, resultCode:Int, data:Intent) = {
+ if(requestCode == 1) {
+ if(resultCode == 1) {
+ tts = new TextToSpeech(this, new TextToSpeech.OnInitListener {
+ def onInit(status:Int) = {
+ Log.d("test", "speec-init-success: "+status)
+ ttsInit = true
+ if(tts.isLanguageAvailable(Locale.GERMAN) >= 0)
+ tts setLanguage Locale.GERMAN
+ }
+ })
+ tts.setLanguage(Locale.US)
+ } else {
+ startActivity(new Intent("android.speech.tts.engine.INSTALL_TTS_DATA"))
+ }
+ }
+ }
+*/
+
+ /**
+ * Ueberschriebener onCreate-Hook
+ *
+ * @param b Bundle mit Werten der letzten Sitzung
+ */
+ override def onCreate(b:Bundle) = {
+
+ super.onCreate(b)
+ setContentView(R.layout.jogging)
+ getWindow addFlags WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+
+ /*
+ * Views holen
+ */
+ gps = findView(R.id.gps)
+ gpsWaitingView = findView(R.id.gpsWaiting)
+ gpsFoundView = findView(R.id.gpsFound)
+ gpsReadyView = findView(R.id.gpsReady)
+ joggingView = findView(R.id.joggingView)
+ gpsWaitingView startAnimation findAnim(R.anim.gpsbuttons_show)
+
+ /*
+ * Tracker instanziieren und initialen Block uebergeben. Im Block die GUI-Interaktion und Callbacks festlegen.
+ *
+ */
+ tracker = Tracker(this) { (provider, location) =>
+ gpsFoundView setVisibility VISIBLE
+ gpsFoundView startAnimation withAnim(R.anim.gpsbuttons_show) { anim =>
+ anim(end={
+ gpsReadyView setVisibility VISIBLE
+ gpsReadyView startAnimation withAnim(R.anim.gpsbuttons_show) { anim =>
+ anim(end={
+ soundEffect('ding)
+ gpsReadyView touch {
+ gps startAnimation withAnim(R.anim.gpsbuttons_hide) { anim =>
+ anim(end={
+ gps setVisibility GONE
+ startViewUpdates()
+ })
+ }
+ }
+ gpsReadyView startAnimation findAnim(R.anim.gpsbuttonready_repeat)
+ })
+ }
+ })
+ }
+ }
+
+ }
+
+ /*
+ * Wird aufgerufen, sobald der GPS-Wartescreen fertig ist
+ */
+ private def startViewUpdates() = {
+ /*
+ * Anzahl Segmente auf LineChart
+ */
+ val segments = 60
+ /*
+ * Hintergrund-Bitmap
+ */
+ val src = BitmapFactory.decodeResource(getResources(), R.drawable.graph_background)
+ /*
+ * Actor fuer das Zeichnen des Graphen - Bitmap nur kopiert uebergeben um den Forderungen des Actor-Modells gerechet zu werden
+ */
+ val lineChartActor = new LineChartActor(src.copy(src.getConfig, true)) with LineChartStyle
+ lineChartActor.start
+ val paintActor = new Actor {
+ def act() = loop {
+ receive {
+ /*
+ * Sobald die Zeichnung eines neuen LineCharts abgeschlossen ist diesen in die GUI uebernehmen
+ */
+ case LineChartRepaintResponse(lineChart) => runOnGuiThread {
+ joggingView.joggingGraph setImageBitmap lineChart
+ joggingView.joggingGraph.postInvalidate
+ }
+ /*
+ * Schickt eine Repaint-Message an den LineChartActor
+ */
+ case Start => lineChartActor ! LineChartRepaint(
+ LineChartData(
+ segments = segments,
+ max = joggingModel.kmhListMax,
+ maxStr = joggingModel.kmhListMax.toInt+" km/h",
+ min = joggingModel.kmhListMin,
+ minStr = joggingModel.kmhListMin.toInt+" km/h",
+ vals = joggingModel.kmhList.take(segments+1).toList.reverse,
+ color = (255 << 24 | 255 << 16 | 255 << 8 | 255)
+ )
+ )
+ case _ => throw new RuntimeException("unknown message")
+ }
+ }
+ }
+ paintActor start
+
+ speecher = new Speecher(self)
+
+ /*
+ * Timer instanziiern und Observer hinzufuegen - alle 15 sekunden status per sprache ausgeben. Jede Sekunde das joggingModel aktualisieren
+ */
+ timer = new Timer(1000)
+ timer onUpdate { duration =>
+ if(duration % 240000 == 0)
+ speecher speak joggingModel.speechInfo
+ joggingModel.duration = duration
+ runOnGuiThread { joggingView.timeView setText joggingModel.durationStr }
+ }
+
+ /*
+ * Schrittzaehler instanziieren und Observer hinzufuegen. Bei jedem Schritt das joggingModel aktualisieren
+ */
+ pedometer = Pedometer(self)
+ pedometer onUpdate { steps =>
+ joggingModel.steps = steps
+ joggingView.stepsView setText joggingModel.stepsStr
+ }
+
+ /*
+ * Observer fuer das joggingModel hinzufuegen - bei jeder neuen Location, die an das Model uebertragen wurde GUI-Elemente aktualisieren sowie
+ * eine neue Grafik ueber den paintActor asynchron anfordern
+ */
+ joggingModel onLocationRetrieved {
+ joggingView.speedView setText joggingModel.speedStr
+ joggingView.distanceView setText joggingModel.distanceStr
+ joggingView.caloriesView setText joggingModel.caloriesStr
+ if(joggingModel.kmhList.size > 1) {
+ paintActor ! Start
+ }
+ }
+
+ /*
+ * Bei jeder neu empfangenen Location das joggingModel aktualisieren
+ */
+ tracker onUpdate { (provider, location) =>
+ joggingModel + location
+ }
+
+
+ }
+}
+
diff --git a/MainActivity.scala b/MainActivity.scala
new file mode 100644
index 0000000..603e75e
--- /dev/null
+++ b/MainActivity.scala
@@ -0,0 +1,106 @@
+package de.tomhoefer.jogga
+
+import android.app.Activity
+import android.os.Bundle
+import android.view.animation.{Animation, AnimationUtils, LayoutAnimationController}
+import android.view.View
+import android.widget.{ImageView, RelativeLayout, LinearLayout, TextView}
+import android.content.{Intent, Context}
+import android.content.res.Resources
+import extensions._
+import views.SplashScreen
+import extensions.implicits._
+import models.userModel
+
+/**
+ * Eintritts-Activity der Anwendung
+ *
+ */
+class MainActivity extends AppDefaultActivity {
+ /*
+ * Splashscreen-Instanz
+ */
+ private var splash:SplashScreen = _
+ /*
+ * Layout mit Buttons auf den Startscreen
+ */
+ private var mainMenuButtons:LinearLayout = _
+ /*
+ * Username-Control
+ */
+ private var username:TextView = _
+
+
+ /**
+ * Ueberschriebener onCreate-Hook
+ *
+ * @param b Bundle mit Werten der letzten Sitzung
+ */
+ override def onCreate(b:Bundle) = {
+
+ super.onCreate(b)
+ /*
+ * Context an Model uebergeben
+ */
+ userModel.setContext(this)
+ setContentView(R.layout.main)
+
+ /*
+ * Views holen
+ */
+ splash = findView(R.id.splash)
+ username = findView(R.id.username)
+ mainMenuButtons = findView(R.id.mainMenuButtons)
+ splash.status = "Loading sounds..."
+
+ /*
+ * soundPool mit Key-Value-Paaren initialisieren und Block uebergeben der nach dem Laden der Sounds
+ * ausgefuert werden soll. Darin Anfangs-Intro definieren.
+ */
+ soundPool.onFinishedLoading(this, ('ding -> R.raw.ding), ('gps -> R.raw.gps), ('sung -> R.raw.sung)) {
+ splash startAnimation withAnim(R.anim.splash_fadeout) {anim =>
+ anim(end={
+ splash setVisibility View.GONE
+ mainMenuButtons setLayoutAnimation findLayoutAnim(R.anim.layout_mainmenubuttons)
+ mainMenuButtons.startLayoutAnimation
+ withView[ImageView](R.id.mainMenuLogo) { view =>
+ view startAnimation withAnim(R.anim.logo_scaleup) {anim =>
+ anim(end=soundEffect('ding))
+ }
+ }
+ })
+ }
+ splash.status = "Sounds ready!"
+ }
+
+ /*
+ * onTouch-Callback fuer StartButton
+ */
+ withView(R.id.startButton) { view:ImageView =>
+ view touch {
+ val intent = new Intent(this, classOf[JoggingActivity])
+ startActivity(intent)
+ }
+ }
+
+ /*
+ * onTouch-Callback fuer SettingsButton
+ */
+ withView[ImageView](R.id.settingsButton) { view =>
+ view touch {
+ val intent = new Intent(this, classOf[SettingsActivity])
+ startActivity(intent)
+ }
+ }
+ }
+
+ /*
+ * Sobald der Startscreen erscheit ggf den Benutzernamen des Users anzeigen
+ */
+ override def onStart() = {
+ super.onStart()
+ username setText userModel.accountInfo
+ }
+
+}
+
diff --git a/SettingsActivity.scala b/SettingsActivity.scala
new file mode 100644
index 0000000..1b06542
--- /dev/null
+++ b/SettingsActivity.scala
@@ -0,0 +1,156 @@
+package de.tomhoefer.jogga
+
+import java.io.IOException
+import scala.xml.{Elem, XML}
+import android.app.{Activity, Dialog, AlertDialog, ProgressDialog}
+import android.os.Bundle
+import android.view.animation.{Animation, AnimationUtils, LayoutAnimationController}
+import android.view.{View, WindowManager}
+import android.util.Log
+import android.widget.{Spinner, LinearLayout, ArrayAdapter, DatePicker, EditText, Button, TextView}
+import android.webkit.WebView
+import android.content.{Intent, Context, DialogInterface}
+import android.content.res.Resources
+import extensions._
+import extensions.implicits._
+import models.userModel
+import concurrent.ops._
+
+
+/**
+ * Basisklasse des SettingsScreen
+ *
+ * @author tom
+ *
+ */
+class SettingsActivity extends AppDefaultActivity {
+
+ /*
+ * Laender-Auswahl
+ */
+ private var countrySpinner:Spinner = _
+ /*
+ * Geburtstags-Auswahl
+ */
+ private var birthday:DatePicker = _
+ /*
+ * Email-Input
+ */
+ private var email:EditText = _
+ /*
+ * Username-Input
+ */
+ private var username:EditText = _
+ /*
+ * Passwort-Input
+ */
+ private var password:EditText = _
+ /*
+ * Registrierungs-Button
+ */
+ private var registerNow:Button = _
+
+
+ /**
+ * Ueberschriebener onCreate-Hook
+ *
+ * @param b Bundle der letzten Sitzung
+ */
+ override def onCreate(b:Bundle) = {
+ super.onCreate(b)
+ setContentView(R.layout.settings)
+
+ /*
+ * Views holen
+ */
+ countrySpinner = findView(R.id.country)
+ val adapter = ArrayAdapter.createFromResource(this, R.array.planets_array, android.R.layout.simple_spinner_item)
+ adapter setDropDownViewResource android.R.layout.simple_spinner_dropdown_item
+ countrySpinner setAdapter adapter
+
+ birthday = findView(R.id.birthday)
+ birthday init(1980, 7, 22, null)
+ username = findView(R.id.username)
+ registerNow = findView(R.id.registerNow)
+ password = findView(R.id.password)
+ email = findView(R.id.email)
+
+
+ /*
+ * Sobald sich jemand registieren moechte...
+ */
+ registerNow touch {
+
+ /*
+ * Userdaten holen...
+ */
+ val birthdate = this.birthday.getYear+"-"+this.birthday.getMonth+"-"+this.birthday.getDayOfMonth
+ val username = this.username.getText.toString
+ val country = countrySpinner.getSelectedItem.toString
+ val password = this.password.getText.toString
+ val email = this.email.getText.toString
+
+ /*
+ * ... und Fortschrittsanzeige anzeigen
+ */
+ val progess = simpleProgress("Connecting to the web...")
+ /*
+ * Neuen Thread starten, damit GUI-Thread nicht blockiert
+ */
+ spawn {
+ try {
+ /*
+ * Registierung durchfuehren und Daten bzw Callback uebergeben
+ */
+ userModel.register(
+ /*
+ * Userdaten in Map sammeln
+ */
+ options = Map('birthdate -> birthdate, 'country -> country, 'email -> email, 'password -> password, 'username -> username),
+ /*
+ * Block fuer den Erfolgsfall uebergeben
+ */
+ success = { userId =>
+ /*
+ * Block in EventQueue ablegen
+ */
+ runOnGuiThread {
+ progess.dismiss
+ simpleAlert("Registration successful!", "Welcome to Jogga-City "+username+"!")
+ }
+ },
+ /*
+ * Block fuer den Fehlschlag uebergeben
+ */
+ error = { errors =>
+ /*
+ * Block in EventQueue ablegen
+ */
+ runOnGuiThread {
+ progess.dismiss
+ simpleAlert("Please correct your input", errors)
+ }
+ }
+ )
+ } catch {
+ /*
+ * Falls keine Netzwerkverbindung existiert
+ */
+ case ex:IOException => runOnGuiThread {
+ progess.dismiss
+ simpleAlert("IO-Error", "Please make sure that you have a network connection available! Error-Details: "+ex.getMessage)
+ }
+ /*
+ * Falls ein allgemeinerer Fehler aufgetreten ist
+ */
+ case ex:Exception => runOnGuiThread {
+ progess.dismiss
+ simpleAlert("Error", "Sorry, an unexpected error occured, please try again later!")
+ }
+ }
+ }
+ ()
+ }
+ }
+}
+
\ No newline at end of file
diff --git a/components/linechart.scala b/components/linechart.scala
new file mode 100644
index 0000000..f5b405f
--- /dev/null
+++ b/components/linechart.scala
@@ -0,0 +1,151 @@
+package de.tomhoefer.jogga.components.linechart
+
+
+import android.graphics.{Bitmap, Paint, Canvas, Path}
+import actors.Actor
+
+/**
+ * Message-Klasse zum neu Zeichnen
+ *
+ * @param data Liste mit LineChartData-Instanzen
+ */
+class LineChartRepaint(val data:List[LineChartData])
+/**
+ * Companion der variable Parameteranzahl akzeptiert
+ */
+object LineChartRepaint {
+ def apply(data:LineChartData*) = new LineChartRepaint(data.toList)
+ def unapply(repaint:LineChartRepaint) = Some(repaint.data)
+}
+
+/**
+ * Response-Message fuer Repaint-Requests
+ * @param bitmap Erzeugte Bitmap
+ *
+ */
+case class LineChartRepaintResponse(bitmap:Bitmap)
+/**
+ * Typ eines Messageobjektes der Message Repaint. Alle Attribute sind Immutables.
+ */
+case class LineChartData(min:Float, max:Float, vals:List[Float], segments:Int, maxStr:String, minStr:String, color:Int)
+
+
+trait OnlyBitmap {
+ protected[this] val originalBitmap:Bitmap
+}
+
+/**
+ * Actor ueber ein beliebiger LineChart gezeichnet werden kann
+ *
+ * @author tom
+ *
+ */
+class LineChartActor(protected val originalBitmap:Bitmap) extends Actor with OnlyBitmap { this: LineChartStyle =>
+
+ /*
+ * Invarianten
+ */
+ require(originalBitmap != null, "bitmap must not be null")
+
+ def act() = loop {
+ receive {
+ case LineChartRepaint(data:List[LineChartData]) => lineChartRepaint(data)
+ }
+ }
+
+
+ private[this] def lineChartRepaint(data:List[LineChartData]) = {
+ prepare
+ data foreach { lcd =>
+
+ require(lcd.segments > 0, "segments must be larger than 0")
+
+ val width = chartWidth
+ val height = chartHeight
+ val segmentWidth = width / lcd.segments
+ var index = 0
+ setMax(lcd.maxStr)
+ setMin(lcd.minStr)
+ setColor(lcd.color)
+ lcd.vals.sliding(2).foreach { mpmList =>
+ drawSegment(segmentWidth * index, height / lcd.max * mpmList(0), segmentWidth * (index + 1), height / lcd.max * mpmList(1) )
+ index += 1
+ }
+ }
+ reply(LineChartRepaintResponse(renderedBitmap))
+ }
+}
+
+
+
+/**
+ * Klasse, die eine Canvas kapselt und Zeichenroutinen fuer einen LineChart implementiert
+ *
+ */
+trait LineChartStyle extends OnlyBitmap {
+
+ /*
+ * Kopie der Bitmap
+ */
+ protected var bitmapCopy = originalBitmap.copy(originalBitmap.getConfig, true)
+
+ // invariante
+ assert(bitmapCopy != null, "copying bitmap failed")
+
+ protected val linePaint = new Paint(Paint.ANTI_ALIAS_FLAG)
+ linePaint setStrokeCap Paint.Cap.ROUND
+ linePaint setStrokeWidth 5.0f
+ linePaint setColor (255 << 24 | 255 << 16 | 255 << 8 | 255)
+
+ protected val rectPaint = new Paint(Paint.ANTI_ALIAS_FLAG)
+ rectPaint setColor (30 << 24 | 0 << 16 | 0 << 8 | 0)
+ rectPaint setStyle Paint.Style.FILL
+
+ protected val textPaint = new Paint(Paint.ANTI_ALIAS_FLAG)
+ textPaint setTextSize 18.0f
+ textPaint setColor (255 << 24 | 80 << 16 | 80 << 8 | 80)
+ textPaint
+
+ protected val canvas = new Canvas()
+ canvas setBitmap bitmapCopy
+ protected val paddingLeft = 25
+ protected val paddingBottom = 25
+
+ protected var max:String = _
+ protected var min:String = _
+
+ /**
+ * Muss vor einem Aufruf von drawSegment aufgerufen werden
+ */
+ protected def prepare() = {
+ bitmapCopy = originalBitmap.copy(originalBitmap.getConfig, true)
+ assert(bitmapCopy != null, "copying bitmap failed")
+ }
+
+ /**
+ * Zeichnet ein Segment im Chart
+ */
+ protected def drawSegment(startX:Float, startY:Float, stopX:Float, stopY:Float):Unit = {
+ canvas setBitmap bitmapCopy
+ val p = new Path
+ p moveTo(startX + paddingLeft, canvas.getHeight - paddingBottom)
+ p lineTo(startX + paddingLeft, canvas.getHeight - paddingBottom - startY)
+ p lineTo(stopX + paddingLeft, canvas.getHeight - paddingBottom - stopY)
+ p lineTo(stopX + paddingLeft, canvas.getHeight - paddingBottom)
+ p.close
+ canvas.drawPath(p, rectPaint)
+ canvas.drawLine(startX + paddingLeft, canvas.getHeight - startY - paddingBottom, stopX + paddingLeft, canvas.getHeight - stopY - paddingBottom, linePaint)
+ canvas.drawText(min, 10, canvas.getHeight - paddingBottom + 7, textPaint)
+ canvas.drawText(max, 10, paddingBottom + 7, textPaint)
+ }
+ protected def renderedBitmap = bitmapCopy
+ protected def chartWidth = originalBitmap.getWidth - 2 * paddingLeft
+ protected def chartHeight = originalBitmap.getHeight - 2 * paddingBottom
+ protected def setMax(max:String) = this.max = max
+ protected def setMin(min:String) = this.min = min
+ protected def setColor(color:Int) = linePaint setColor color
+}
+
+
+
+
diff --git a/components/musicplayer.scala b/components/musicplayer.scala
new file mode 100644
index 0000000..8be018e
--- /dev/null
+++ b/components/musicplayer.scala
@@ -0,0 +1,164 @@
+package de.tomhoefer.jogga.components.musicplayer
+
+import java.io.{File, FilenameFilter}
+import android.media.MediaPlayer
+import android.util.Log
+
+
+/*
+ * Spezifikation einer Playlist
+ */
+trait Playlist {
+ /*
+ * Gibt den Namen des aktuellen Songs zurueck
+ */
+ def songName():String
+ /*
+ * Gibt den Pfad zum aktuellen Song zurueck
+ */
+ def songPath():String
+ /*
+ * Spielt den naechsten Song ab
+ */
+ def nextSong():Unit
+ /*
+ * Spring zum vorherigen Song
+ */
+ def prevSong():Unit
+}
+
+
+/**
+ * Darueber ist eine gewoehnliche Playliste realisiert die mp3-Dateien aus einem Verzeichnis liest und
+ * in einer internen Liste verwaltet
+ *
+ * @author tom
+ *
+ */
+trait FolderBasedPlaylist extends Playlist {
+ /*
+ * Verzeichnis durchsuchen und nach mp3-Dateien filtern
+ */
+ private val fileList = new File("/mnt/sdcard/MP3/jogga/").listFiles.filter {_.getName.endsWith(".mp3")}
+
+ /*
+ * Invariante - es muss mind eine mp3-Datei verfuegbar sein
+ */
+ assert(fileList.size > 0, "music-folder must contain mp3-files")
+
+ /*
+ * Aktueller Index
+ */
+ private var index = 0
+
+ def songName() = fileList(index).getName
+ def songPath() = fileList(index).getAbsolutePath
+ def nextSong() = {
+ if(index == fileList.length - 1)
+ index = 0
+ else
+ index += 1
+ }
+ def prevSong() = {
+ if(index == 0)
+ index = fileList.length - 1
+ else
+ index -= 1
+ index
+ }
+}
+
+
+/*
+ * Abstrakter Zustand
+ */
+protected trait PlayerState {
+ def next():Unit
+ def prev():Unit
+}
+
+/**
+ * Diese Klasse arbeitet nach dem State-Pattern. Beim Instanziieren muss eine Instanz einer Playlist
+ * uebergeben werden um die selftype-annotation zu erfuellen.
+ *
+ * @author tom
+ *
+ */
+class MusicPlayer { self:Playlist =>
+
+ /*
+ * Konkreter Zustand - Musicplayer gestoppt
+ */
+ private val playerStateStopped = new PlayerState {
+ def next():Unit = {
+ self.play()
+ state = playerStatePlaying
+ }
+ def prev():Unit = {
+ prevSong()
+ state = playerStatePlaying
+ }
+ }
+
+ /*
+ * Konkreter Zustand - Musicplayer in pausiertem Zustand
+ */
+ private val playerStatePaused = new PlayerState {
+ def next():Unit = {
+ mp.start
+ state = playerStatePlaying
+ }
+ def prev():Unit = {
+ prevSong()
+ play()
+ state = playerStatePlaying
+ }
+ }
+ /*
+ * Konkreter Zustand - Musikplayer spielt einen Song
+ */
+ private val playerStatePlaying = new PlayerState {
+ def next():Unit = {
+ nextSong()
+ play()
+ }
+ def prev():Unit = {
+ prevSong()
+ play()
+ }
+ }
+
+ private val mp = new MediaPlayer
+ private var state:PlayerState = playerStateStopped
+
+ mp setOnPreparedListener new MediaPlayer.OnPreparedListener {
+ def onPrepared(mp:MediaPlayer) = mp start
+ }
+
+ mp setOnCompletionListener new MediaPlayer.OnCompletionListener() {
+ def onCompletion(mp:MediaPlayer) = next
+ }
+
+
+ def play() = {
+ mp.reset
+ mp.setDataSource(songPath)
+ mp.prepareAsync
+ }
+
+ def next() = state next
+ def pause() = {
+ mp.pause
+ state = playerStatePaused
+ }
+ def stop() = {
+ mp.stop
+ state = playerStateStopped
+ }
+ def prev() = state prev
+ def release() = {
+ mp.stop
+ mp.release
+ }
+ def songinfo() = songName.replace(".mp3", "")
+}
diff --git a/components/observable.scala b/components/observable.scala
new file mode 100644
index 0000000..91f44db
--- /dev/null
+++ b/components/observable.scala
@@ -0,0 +1,24 @@
+package de.tomhoefer.jogga.components
+
+
+/*
+ * Implementierung des Observer-Pattern
+ */
+trait Observable {
+ /*
+ * Queue mit Observer-Objekten
+ */
+ private val observers = new collection.mutable.Queue[EventHandler]
+ /*
+ * Struktur eines Event-Handler-Callbacks
+ */
+ protected type EventHandler
+ /*
+ * Registriert einen Observer in der internen Queue
+ */
+ def onUpdate(observer:EventHandler):Unit = observers += observer
+ /*
+ * Benachrichtigt alle Observer
+ */
+ def notifyListeners(block:EventHandler => Unit) = observers foreach {block(_)}
+}
\ No newline at end of file
diff --git a/components/pedometer.scala b/components/pedometer.scala
new file mode 100644
index 0000000..4293f4e
--- /dev/null
+++ b/components/pedometer.scala
@@ -0,0 +1,71 @@
+package de.tomhoefer.jogga.components
+
+import android.content.Context
+import android.hardware.{SensorManager, Sensor, SensorEventListener, SensorEvent}
+
+/**
+ * Schrittzaehler, dem ein Observer uebergeben werden kann um bei Schritten
+ * benachrichtigt zu werden
+ *
+ * @param context AnwendungsContext
+ *
+ */
+case class Pedometer(context:Context) extends Observable {
+
+ /*
+ * Invariante - ein Context muss verfuegbar sein
+ */
+ require(context != null, "context must not be null")
+
+ /**
+ * Enumeration fuer Zustaende in einer Bewegung
+ * @author tom
+ *
+ */
+ protected object Dir extends Enumeration {
+ val Top = Value
+ val Bottom = Value
+ }
+
+
+ /*
+ * EventHandler-Struktur festlegen
+ */
+ type EventHandler = Int => Unit
+ /*
+ * Anzahl getaetiger Schritte
+ */
+ private var steps = 0
+ /*
+ * Zustand der Bewegungsrichtung (Oszilliert um Null)
+ */
+ private var state:Dir.Value = _
+ private val sensorManager:SensorManager = context.getSystemService(Context.SENSOR_SERVICE).asInstanceOf[SensorManager]
+ private val accelerometer:Sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
+ /*
+ * Listener fuer neue Sensor-Werte hinzufuegen
+ */
+ private var sensorListener:SensorEventListener = new SensorEventListener {
+ def onSensorChanged(e:SensorEvent) = {
+ val (x,y,z) = (e.values(0), e.values(1), e.values(2))
+ /*
+ * Laenge Bewegungsvektor berechnen
+ */
+ val length = Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2) + Math.pow(z, 2)) - SensorManager.STANDARD_GRAVITY
+ val result = (length * 100).asInstanceOf[Int]
+ if(result >= 100) {
+ state = Dir.Top
+ } else {
+ if(state == Dir.Top) {
+ steps += 1
+ notifyListeners {_(steps)}
+ }
+ state = Dir.Bottom
+ }
+ }
+
+ def onAccuracyChanged(sensor:Sensor, acc:Int) = {}
+ }
+ sensorManager.registerListener(sensorListener, accelerometer, SensorManager.SENSOR_DELAY_GAME)
+}
+
diff --git a/components/speecher.scala b/components/speecher.scala
new file mode 100644
index 0000000..a28ee70
--- /dev/null
+++ b/components/speecher.scala
@@ -0,0 +1,49 @@
+package de.tomhoefer.jogga.components
+
+import java.util.Locale
+import android.speech.tts.TextToSpeech
+import android.content.Context
+
+
+/**
+ * Klasse fuer die Ausfuerhung von Sprachsynthese
+ * @author tom
+ *
+ */
+class Speecher(context:Context) {
+
+ require(context != null, "context must not be null")
+
+ private var prepared = false
+ private var ttsInit = false
+
+ private val tts = new TextToSpeech(context, new TextToSpeech.OnInitListener {
+ def onInit(status:Int) = {
+ if(status == 0) {
+ ttsInit = true
+ }
+ }
+ })
+
+ protected def prepare() = {
+ tts.setSpeechRate(1.25f)
+ if(tts.isLanguageAvailable(Locale.GERMAN) >= 0)
+ tts setLanguage Locale.GERMAN
+ else
+ tts.setLanguage(Locale.US)
+ }
+
+ /**
+ * Spricht den Satz `text`
+ * @param text String der gesprochen werden soll
+ * @return Unit
+ */
+ def speak(text:String) = if(ttsInit) {
+ if(!prepared) {
+ prepare()
+ prepared = true
+ }
+ tts.speak(text, TextToSpeech.QUEUE_ADD, null)
+ }
+
+}
\ No newline at end of file
diff --git a/components/timer.scala b/components/timer.scala
new file mode 100644
index 0000000..6612dd8
--- /dev/null
+++ b/components/timer.scala
@@ -0,0 +1,44 @@
+package de.tomhoefer.jogga.components
+
+import java.util.{Timer => JTimer}
+import java.util.{TimerTask, Date}
+
+
+/**
+ * Zeitgeber - benachrichtigt Observer im Interval von `rate`
+ * @author tom
+ *
+ */
+class Timer(rate:Int) extends Observable {
+
+ /*
+ * Invariante
+ */
+ require(rate > 0, "rate must be > 0")
+
+ type EventHandler = Long => Unit
+ private val timer = new JTimer
+ private var time:Long = _
+
+ override def onUpdate(block:EventHandler) = {
+ /*
+ * Vorbedingung
+ */
+ assert(block != null, "block must not be null")
+
+ super.onUpdate(block)
+ /*
+ * Timer-Thread verwenden
+ */
+ timer.scheduleAtFixedRate(new TimerTask() {
+ def run() = {
+ time += rate
+ /*
+ * Observer benachrichten und vergangene Zeit uebergeben
+ */
+ notifyListeners {_(time - 1000 * 60 * 60)}
+ }
+ }, 0, rate)
+ }
+}
+
diff --git a/components/tracking.scala b/components/tracking.scala
new file mode 100644
index 0000000..4c77372
--- /dev/null
+++ b/components/tracking.scala
@@ -0,0 +1,106 @@
+package de.tomhoefer.jogga.components
+
+import de.tomhoefer.jogga.R
+import concurrent.ops._
+import android.content.Context
+import android.app.Dialog
+import collection.mutable.ListBuffer
+import android.widget.{Button, TextView}
+import android.view.{View, MotionEvent, WindowManager, Window, ViewGroup}
+import android.location.{LocationManager => LM, LocationListener, Location}
+import android.os.Bundle
+import android.util.Log
+import de.tomhoefer.jogga.extensions.implicits._
+import de.tomhoefer.jogga.models.joggingModel
+
+
+/**
+ * TrackingModul - Observer werden benachrichtigt sofern eine neue gueltige Location verfuegbar ist.
+ * Hier koennen Locations ggf gefilert und geglaettet werden.
+ *
+ * Initialer Callback wird ausgefuert sobald erstmalig Locations verfuegbar sind. Observer, die ueber onUpdate registriert wurden,
+ * werden bei jeder neuen Location ausgefuert
+ *
+ * @author tom
+ *
+ */
+
+class Tracker(context:Context, initialCallback:(String, Location) => Unit) extends Observable {
+
+ require(context != null, "context must not be null")
+
+ type EventHandler = (String, Location) => Unit
+ private var isFirstLocation = true
+ private var provider:String = _
+ private val lm:LM = context.getSystemService(Context.LOCATION_SERVICE).asInstanceOf[LM]
+ private val initialObservable = new Observable {
+ type EventHandler = (String, Location) => Unit
+ }
+ initialObservable onUpdate initialCallback
+ /**
+ * GPS basierter LocationListener. Sobald GPS nicht verfuegbar ist, Fallsback auf NetworkListener
+ */
+ protected val gpsLocationListener = new LocationListener{
+ def onProviderEnabled(p:String) = {}
+ def onStatusChanged(p:String, status:Int, b:Bundle) = {}
+ /*
+ * Wenn GPS-Provider am Phone vom User deaktiviert wurde Fallback auf Network - hauptsaechlich fuer debugging interessant
+ */
+ def onProviderDisabled(p:String) = {
+ lm.removeUpdates(this)
+ lm.requestLocationUpdates(LM.NETWORK_PROVIDER, 500, 0, networkLocationListener)
+ }
+ def onLocationChanged(location:Location) = location.getAccuracy match {
+ case acc => {
+ provider = "gps"
+ onLocationReceived(location)
+ }
+ }
+ }
+
+ /**
+ * Network basierter LocationListener
+ */
+ protected val networkLocationListener = new LocationListener {
+ def onProviderEnabled(p:String) = {}
+ def onStatusChanged(p:String, status:Int, b:Bundle) = {}
+ def onProviderDisabled(p:String) = {}
+ def onLocationChanged(location:Location) = location.getAccuracy match {
+ case acc => {
+ provider = "network"
+ onLocationReceived(location)
+ }
+ }
+ }
+ /*
+ * gps Listener als erstes versuchen
+ */
+ lm.requestLocationUpdates("gps", 500, 0, gpsLocationListener)
+
+ /*
+ *
+ */
+ protected def onLocationReceived(location:Location) = {
+ /*
+ * Wenn es die erste Location ist, den initialen Callback erst ausfuehren
+ */
+ if(isFirstLocation) {
+ isFirstLocation = false
+ initialObservable.notifyListeners(_(provider, location))
+ }
+ /*
+ * Observer, die ueber onUpdate registriert wurden benachrichtigen sowie Provider und aktuelle Location an Block uebergeben
+ */
+ notifyListeners {_(provider, location)}
+ }
+}
+
+
+object Tracker {
+ def apply(context:Context)(block:(String, Location) => Unit) = new Tracker(context, block)
+}
+
+
+
+
+
diff --git a/extensions.scala b/extensions.scala
new file mode 100644
index 0000000..9d03da7
--- /dev/null
+++ b/extensions.scala
@@ -0,0 +1,292 @@
+package de.tomhoefer.jogga.extensions
+
+import android.app.{Activity, AlertDialog, ProgressDialog}
+import android.util.Log
+import android.content.DialogInterface
+import android.view.{View, MotionEvent, ViewGroup}
+import android.view.animation.{Animation, AnimationUtils, LayoutAnimationController}
+import android.view.{Window, WindowManager}
+import android.os.Bundle
+import android.media.{SoundPool, AudioManager}
+import android.content.{Context}
+import android.util.AttributeSet
+import android.widget.{LinearLayout, TextView}
+import android.view.{View, LayoutInflater}
+import de.tomhoefer.jogga.R
+import android.util.Log
+import android.media.MediaPlayer
+
+
+/**
+ * Trait der Extensions fuer ViewGroup-Komponenten bereitstellt.
+ *
+ * @author tom
+ *
+ */
+trait ViewGroupExt {
+ this: ViewGroup =>
+ /**
+ * Methode die die View mit der id `id` laedt, an den Block `block` uebergibt und diese ebenfalls zurueckgibt
+ *
+ * @param id Id der zu ladenden View
+ * @param block Callback an den die View mit der id `id` uebergeben wird
+ * @return View mit der id `id`
+ */
+ def withView[A <: View](id:Int)(block:A => Unit) = {
+ val view = findViewById(id).asInstanceOf[A]
+ block(view)
+ view
+ }
+ /**
+ * Methode die die View mit der id `id` laedt, intern castet und zurueck gibt
+ *
+ * @param id Id der zu ladenden View
+ * @return Gefundene View
+ */
+ def findView[A <: View](id:Int) = findViewById(id).asInstanceOf[A]
+ /**
+ * Laedt eine Animatons-Ressource
+ *
+ * @param id Id der zu ladenden Animation
+ * @return Gefundene Animation
+ */
+ def findAnim(id:Int) = AnimationUtils.loadAnimation(getContext, id)
+ /**
+ * Laedt die Animation mit der id `id`, gibt diesen an den block `block` weiter.
+ *
+ * @param id Zu ladende Animation
+ * @param block Auszufuehrender Block
+ * @return Gefundene Animation
+ */
+ def withAnim(id:Int)(block: Animation => Unit) = {
+ val anim = findAnim(id)
+ block(anim)
+ anim
+ }
+ /**
+ * Laedt Layout-Animation mit der id `id`
+ *
+ * @param id Zu ladende Layoutanimations-Ressource
+ * @return Gefundene LayoutAnimation
+ */
+ def findLayoutAnim(id:Int) = AnimationUtils.loadLayoutAnimation(getContext, id)
+}
+
+
+trait ActivityExt {
+ this: Activity =>
+ def withView[A <: View](id:Int)(block:A => Unit) = {
+ val view = findViewById(id).asInstanceOf[A]
+ block(view)
+ view
+ }
+ def findView[A <: View](id:Int) = findViewById(id).asInstanceOf[A]
+ def findAnim(id:Int) = AnimationUtils.loadAnimation(this, id)
+ def withAnim(id:Int)(block: Animation => Unit) = {
+ val anim = findAnim(id)
+ block(anim)
+ anim
+ }
+ def findLayoutAnim(id:Int) = AnimationUtils.loadLayoutAnimation(this, id)
+ /**
+ * Methode fuer Alert-Anzeige
+ *
+ * @param title Titel des Alerts
+ * @param message Nachricht im Alert-Fenster
+ * @return AlertDialog.Builder-Instanz
+ */
+ def simpleAlert(title:String, message:CharSequence) = {
+ val alert = new AlertDialog.Builder(this)
+ alert setTitle title
+ alert setMessage message
+ alert.setPositiveButton("OK", new DialogInterface.OnClickListener() {
+ def onClick(dialog:DialogInterface, arg:Int) = ""
+ })
+ alert.show
+ alert
+ }
+ /**
+ * Methode fuer die Anzeige einer Fortschrittsanzeige
+ *
+ * @param message Nachricht zur Fortschrittsanzeige
+ * @return ProgressDialog-Instanz
+ */
+ def simpleProgress(message:CharSequence) = {
+ val progress = new ProgressDialog(this)
+ progress setMessage message
+ progress setIndeterminate true
+ progress setProgressStyle ProgressDialog.STYLE_SPINNER
+ progress.show
+ progress
+ }
+ /**
+ * Helfer-Methode um die EventQueue zu befuellen
+ * @param Block der im Kontext des EventQueue-Thread ausgefuert werden soll
+ */
+ def runOnGuiThread(block: => Unit) = this runOnUiThread new Runnable {
+ override def run() = block
+ }
+}
+
+/**
+ * Extension die die View-Klasse um Methoden erweitert.
+ *
+ * @author tom
+ *
+ */
+trait ViewExt {
+ val that:View
+ private def touchResponse(value:AnyVal) = value match {
+ case false => false
+ case _ => true
+ }
+ /**
+ * Methoden die den uebergebenen Block bei einem TouchEvent ausfuehrt
+ *
+ * @param callback Auszufuerender Block
+ */
+ def touch(callback: => AnyVal) = that setOnTouchListener new View.OnTouchListener {
+ def onTouch(v:View, e:MotionEvent) = {
+ var result = false
+ if(e.getAction == MotionEvent.ACTION_DOWN) {
+ result = touchResponse(callback)
+ }
+ result
+ }
+ }
+ /**
+ * Ueberladene Methode, die die Original-Parameter View und MotionEvent zusaetzlich an den Block
+ * uebergeben
+ * @param callback Auszufuehrender Block
+ */
+ def touch(callback:(View, MotionEvent) => AnyVal) = that setOnTouchListener new View.OnTouchListener {
+ def onTouch(v:View, e:MotionEvent) = touchResponse(callback(v,e))
+ }
+}
+
+
+/**
+ * Definiert Implizite Konvertierungen.
+ *
+ * @author tom
+ *
+ */
+object implicits {
+
+ /**
+ * Wrapper-Klasse fuer Animations-Objekte
+ *
+ * @param animation Original-Animation
+ */
+ protected class RichAnimation(val animation:Animation) {
+ /**
+ * Intern wird ein gewoehnlicher AnimationListener verwendet, um die uebergebenen Bloecke
+ * bei deren zugehoerigen Ereignissen auszufuehren.
+ *
+ * @param start Block, der beim Start der Animation ausgefuehrt werden soll
+ * @param end Block, der beim Beenden der Animation ausgefuert werden soll
+ * @param repeat Block, der beim Wiederholen einer Animation ausgefuehrt werden soll
+ */
+ def apply(start: => Unit = null, end: => Unit = null, repeat: => Unit = null) = {
+ animation.setAnimationListener(new Animation.AnimationListener {
+ def onAnimationStart(a:Animation) = start
+ def onAnimationEnd(a:Animation) = end
+ def onAnimationRepeat(a:Animation) = repeat
+ })
+ }
+ }
+
+ /**
+ * Wrapper-Klasse fuer View-Instanzen - Methoden aus `ViewExt` sind damit verfuegbar
+ */
+ protected class RichView(val v:View) extends ViewExt {
+ val that:View = v
+ }
+
+ /*
+ * Konvertierung fuer Animationen
+ */
+ implicit def animation2RichAnimation(a:Animation):RichAnimation = new RichAnimation(a)
+ /*
+ * Konvertierung fuer Views
+ */
+ implicit def view2RichView(v:View):RichView = new RichView(v)
+}
+
+
+
+/**
+ * Basisklasse aller Jogga-Activities, die um ActivityExtensions erweitert wurde und per default Fullscreen-Format besitzt.
+ *
+ * @author tom
+ *
+ */
+class AppDefaultActivity extends Activity with ActivityExt {
+
+ /**
+ * Ueberschriebener onCreate-Hook
+ *
+ * @param b Bundle mit Werten der letzten Sitzung
+ */
+ override def onCreate(b:Bundle) = {
+ super.onCreate(b)
+ requestWindowFeature(Window.FEATURE_NO_TITLE);
+ getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
+ }
+
+ def soundEffect(name:Symbol):Unit = soundPool(name)
+}
+
+
+/**
+ * Ueber den Soundpool werden Sounds geladen und mit einem Symbol assoziiert
+ *
+ * @author tom
+ *
+ */
+object soundPool {
+
+ type PairType = Pair[Symbol, Int]
+ protected var _context:Context = _
+ protected val _pool = new SoundPool(10, AudioManager.STREAM_MUSIC, 100)
+ protected var _sounds:Map[Symbol, Int] = _
+ protected var _count = 0
+ protected var _audioManager:AudioManager = _
+
+ /**
+ * Initialisierungsmethode, ueber welche die Sounds geladen und der Audio-Service gestartet wird
+ *
+ * @param context ActivityContext
+ * @param pairs Argument variabler Laenge mit Symbol/Werte-Paaren
+ * @param block Callback, der nach der Initialisierung des soundPool aufgerufen wird
+ */
+ def onFinishedLoading(context:Context, pairs:PairType*)(block: => Unit):Unit = {
+ if(_sounds != null)
+ return block
+ _context = context
+ _audioManager = _context.getSystemService(Context.AUDIO_SERVICE).asInstanceOf[AudioManager]
+ val pairList = pairs.toList
+ _pool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener {
+ def onLoadComplete(p:SoundPool, sampled:Int, status:Int) = {
+ _count += 1
+ if(_count == pairList.size)
+ block
+ }
+ })
+ _sounds = pairList.foldLeft(Map.empty[Symbol, Int]) { (acc, it) => acc + (it._1 -> _pool.load(_context, it._2, 1)) }
+ }
+
+ /**
+ * Spiel den Sound, welcher mit *name* assoziiert ist
+ *
+ * @param name Key des abzuspielenden Sounds
+ * @param loop Zeigt an wie oft der Sound abgespielt werden soll. Optionaler Parameter.
+ */
+ def apply(name:Symbol, loop:Int = 0) = {
+ var streamVolume = _audioManager.getStreamVolume(AudioManager.STREAM_MUSIC)
+ streamVolume = streamVolume / _audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC)
+ _pool.play(_sounds(name), 1, 1, 1, loop, 1f)
+ }
+}
+
+
diff --git a/models/joggingmodel.scala b/models/joggingmodel.scala
new file mode 100644
index 0000000..90aeca0
--- /dev/null
+++ b/models/joggingmodel.scala
@@ -0,0 +1,224 @@
+package de.tomhoefer.jogga.models
+
+import java.text.SimpleDateFormat
+import java.util.Date
+import collection.mutable.{ListBuffer, ArrayBuffer}
+import android.location.Location
+import android.util.Log
+import de.tomhoefer.jogga.components.Observable
+
+/**
+ * Model fuer die Daten, die waehrend des Joggens relevant sind und ggf im UI dargestellt werden sollen.
+ *
+ * @author tom
+ *
+ */
+object joggingModel {
+ /*
+ * Observer-Struktur fuer Callback definieren
+ */
+ private type EventHandler = () => Unit
+ /*
+ * Observable verwenden
+ */
+ private val locationRetrievedObservable = new Observable {
+ type EventHandler = joggingModel.EventHandler
+ }
+ /*
+ * Formatierer fuer Stunden, Minuten und Sekunden
+ */
+ private val sdf = new SimpleDateFormat("HH:mm:ss")
+ /*
+ * Alle Locations - das Model ist ein Listener des Tracking-Moduls
+ * ListBuffer konstante Laufzeit bei prepend und head
+ */
+ private var locations = ListBuffer.empty[Location]
+ /*
+ * Ist true wenn mind 2 Locations in locations enthalten sind.
+ * Hilfsflag um staendige size-Abfrage von locations zu vermeiden da diese bei Listen nur lineare Laufzeit hat
+ */
+ private var enoughLocations = false
+ /*
+ * Alle gemessenen khm-Werte werden hier gespeichert. Lediglich prepend wird verwendet, welche konstante Laufzeit hat
+ */
+ var kmhList = ListBuffer.empty[Float]
+ var kmhListMax:Float = _
+ var kmhListMin:Float = _
+ /*
+ * Die zurueckgelegte Distanz - Cachevariable: Das Delta wird immer hinzu addiert
+ */
+ private var _distanceCache:Float = _
+
+ /*
+ * Aktuelle Geschwindigkeit
+ */
+ private var _speed:Float = _
+ /*
+ * Distanz
+ */
+ private var _distance:Float = _
+ /*
+ * Verbrannte Kalorien
+ */
+ private var _calories:Int = _
+ /*
+ * Schritte
+ */
+ private var _steps:Int = _
+ /*
+ * Dauer des Laufs
+ */
+ private var _duration:Long = _
+ /*
+ * Hoehe
+ */
+ private var _altitude:Option[Float] = _
+ /**
+ * Gibt die aktuelle Location zurueck
+ */
+ def locationCurrent = locations head
+ /**
+ * Gibt die vorhergehende Location zurueck - also die vor locationCurrent
+ */
+ def locationLast = if(enoughLocations) Some(locations take 2 last) else None
+
+ /**
+ * Formatierer fuer Geschwindigkeit
+ */
+ def speedStr = _speed.toInt+" km/h"
+ def speed = _speed
+ def speed_=(_speed:Float) = this._speed = _speed
+
+ /**
+ * Formatierer fuer Distanz
+ */
+ def distanceStr = _distance.toInt+" m"
+ def distance = _distance
+ def distance_=(_distance:Float) = this._distance = _distance
+
+ /**
+ * Formatierer Kalorien
+ */
+ def caloriesStr = _calories+" kcal"
+ def calories = _calories
+ def calories_=(_calories:Int) = this._calories = _calories
+
+ /**
+ * Formatierer Schritte
+ */
+ def stepsStr = _steps+" steps"
+ def steps = _steps
+ def steps_=(_steps:Int) = this._steps = _steps
+
+ /**
+ * Formatierer Laufdauer
+ */
+ def durationStr = sdf.format(new Date(_duration))
+ def duration = _duration
+ def duration_=(_duration:Long) = this._duration = _duration
+
+ /**
+ * Das Distanz in Metern zwischen den letzten beiden Locations
+ *
+ * @return Distanz in Metern
+ */
+ protected def locationDelta = locationLast match {
+ case Some(last) => locationCurrent distanceTo last
+ case None => 0
+ }
+
+ /**
+ * Berechnet und gibt die momentane Geschwindigkeit zurueck
+ *
+ * @return Geschwindigkeit
+ */
+ protected def calcSpeed = {
+ val cur = locationCurrent
+ locationLast match {
+ case Some(locationLast) => {
+ val deltaDist = locationDelta
+ val deltaTime = cur.getTime - locationLast.getTime
+ deltaDist / deltaTime * 1000 * 3.6f
+ }
+ case None => 0
+ }
+ }
+
+ /**
+ * Gibt eine Option zur aktuellen Hoehe zurueck
+ */
+ protected def calcAltitude = locationCurrent.hasAltitude match {
+ case true => Some(locationCurrent.getAltitude.asInstanceOf[Float])
+ case false => None
+ }
+
+ /**
+ * Berechnet und gibt die bisher gelaufene Distanz zurueck
+ */
+ protected def calcDistance = {
+ _distanceCache += locationDelta
+ _distanceCache
+ }
+
+ /**
+ * Berechnet und gibt die Kalorien zurueck (http://jogmap.de/civic4/?q=node/10036 )
+ */
+ protected def calcCalories = (85 * _distanceCache / 1000).toInt
+
+ /**
+ * BerechnetWerte neue bei einer neuen Location
+ */
+ protected def update() = {
+ _altitude = calcAltitude
+ _speed = calcSpeed
+ _distance = calcDistance
+ _calories = calcCalories
+ }
+
+ /**
+ * Fuegt eine neue Location hinzu, fuert darauf aufbauende Berechnungen aus und
+ * benachrichtigt Observer die an den resultaten Interessiert sind
+ */
+ def +(location:Location) = {
+
+ assert(location != null, "location must not be null")
+
+ if(! locations.isEmpty) {
+ // es werden zumindest 2 locations benoetigt
+ locationLast match {
+ case Some(locationLast) => {
+ var diffTime = location.getTime - locationLast.getTime
+ var diffDist = location distanceTo locationLast
+ // in meter umrechnen
+ var mpm = (diffDist / diffTime * 1000 * 3.6f).toInt
+ kmhList prepend mpm
+ if(mpm > kmhListMax)
+ kmhListMax = mpm
+ else if(mpm < kmhListMin)
+ kmhListMin = mpm
+ }
+ case _ =>
+ }
+ }
+ // in gesamtliste einfuegen
+ locations prepend location
+ // short-circuit-evaluation aus java ueber 2x if nachbauen
+ if(!enoughLocations) if(locations.size > 1) enoughLocations = true
+ update()
+ locationRetrievedObservable notifyListeners {_()}
+ }
+
+ /**
+ * Fuehrt `block` aus, sobald Berechnungen im Zuge einer neuen Location abgeschlossen wurden
+ */
+ def onLocationRetrieved(block: => Unit) = locationRetrievedObservable.onUpdate(block _)
+
+ /**
+ * Text fuer Sprachsynthetisierung
+ */
+ def speechInfo = {
+"""
+Deine Geschwindigkeit ist momentan """+speed.toInt+""" kmh. Du bist bisher """+distance.toInt+""" Meter gelaufen und hast dabei """+calories.toInt+""" Kalorien verbrannt. Das ist super!
+"""
+ }
+}
\ No newline at end of file
diff --git a/models/usermodel.scala b/models/usermodel.scala
new file mode 100644
index 0000000..31e1e7f
--- /dev/null
+++ b/models/usermodel.scala
@@ -0,0 +1,60 @@
+package de.tomhoefer.jogga.models
+
+
+import android.content.Context
+import android.util.Log
+import de.tomhoefer.jogga.service.backend._
+import android.preference.PreferenceManager
+
+object userModel {
+
+ private var backend = Backend getDefault
+ private var _context:Context = _
+ // kapselung ist dennoch erhalten durch uniform access principle
+ var userId:Int = _
+ var username:String = _
+
+ //private def getPrefs = _context.getSharedPreferences("user", Context.MODE_PRIVATE)
+ private def getPrefs = PreferenceManager.getDefaultSharedPreferences(_context)
+
+ def setContext(context:Context) = {
+ _context = context
+ reload
+ }
+
+ def reload() = {
+ val prefs = getPrefs
+ userId = prefs.getInt("userId", -1)
+ prefs.getString("username", "")
+ }
+
+ def persist = {
+ val editor = getPrefs.edit
+ editor.putString("username", username)
+ editor.putInt("userId", userId)
+ editor.commit()
+ }
+
+ def accountInfo = username match {
+ case "" | null => "You are not using an account"
+ case x => "Your account: "+x
+ }
+
+
+ def register(options:Map[Symbol, String], success:Int => Unit, error: String => Unit):Unit = backend.register(
+ options = options,
+ error = error,
+ success = { userId:Int =>
+ this.userId = userId
+ username = options('username)
+ /*
+ * Im Erolg userModel speichern
+ */
+ persist
+ /*
+ * original-success callback aufrufen
+ */
+ success(userId)
+ }
+ )
+}
\ No newline at end of file
diff --git a/service/backend.scala b/service/backend.scala
new file mode 100644
index 0000000..60d0678
--- /dev/null
+++ b/service/backend.scala
@@ -0,0 +1,78 @@
+package de.tomhoefer.jogga.service.backend
+
+import org.apache.http.impl.client.DefaultHttpClient
+import org.apache.http.client.methods.HttpPost
+import org.apache.http.entity.StringEntity
+import xml.XML
+import android.text.Html
+
+
+private class RestfulBackend extends Backend {
+
+ /**
+ * Gibt eine HttpPost-Instanz zur URL `url` zurueck
+ *
+ * @param url Die Url
+ */
+ protected def getDefaultPost(url:String) = {
+ val post = new HttpPost(url)
+ post.setHeader("Content-Type", "text/xml")
+ post.setHeader("Accept", "text/xml")
+ post
+ }
+
+ /**
+ * Die Registrierungsmethode fuer neue Jogga-User
+ *
+ * @param options Map mit Benutzerwerten
+ * @param success Block, der im Erfolgsfall aufgerufen wird und Username und UserId uebergeben bekommt
+ * @param error Block, der bei einem Fehlversuch aufgerufen wird und Fehlerdetails uebergeben bekommt
+ */
+ override def register(options:Map[Symbol, String], success:(Int) => Unit, error: String => Unit):Unit = {
+
+ /*
+ * xml-struktur aufbauen
+ */
+ var xml =
"+error.text+"
"} + val errors = Html.fromHtml(errorStr.toString).toString + error(errors) + } + } +} + + +trait Backend { + def register(options:Map[Symbol, String], success:(Int) => Unit, error: String => Unit):Unit +} + + +object Backend { + def getDefault:Backend = new RestfulBackend +} \ No newline at end of file diff --git a/views/joggingview.scala b/views/joggingview.scala new file mode 100644 index 0000000..148c8db --- /dev/null +++ b/views/joggingview.scala @@ -0,0 +1,54 @@ +package de.tomhoefer.jogga.views + +import de.tomhoefer.jogga.R +import android.app.Activity +import android.os.Bundle +import android.util.AttributeSet +import android.view.{View, LayoutInflater} +import android.widget.{RelativeLayout, LinearLayout, Button, TextView, ImageView} +import android.content.Context +import android.util.Log +import android.view.View.{VISIBLE, INVISIBLE, GONE} +import concurrent.ops._ +import de.tomhoefer.jogga.extensions._ +import de.tomhoefer.jogga.extensions.implicits._ +import de.tomhoefer.jogga.components.musicplayer._ +import de.tomhoefer.jogga.models.joggingModel + + +class JoggingView(context:Context, as:AttributeSet) extends RelativeLayout(context, as) with ViewGroupExt { + + def this(context:Context) = this(context, null) + + private val li = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE).asInstanceOf[LayoutInflater] + li.inflate(R.layout.jogging_view, this, true) + val timeView:TextView = findView(R.id.joggingTime) + val stepsView:TextView = findView(R.id.joggingSteps) + val joggingGraph:ImageView = findView(R.id.joggingGraph) + val speedView:TextView = findView(R.id.joggingSpeed) + val distanceView:TextView = findView(R.id.joggingDistance) + val caloriesView:TextView = findView(R.id.joggingCalories) + var options:ImageView = findView(R.id.joggingOptions) + var playerNext:ImageView = findView(R.id.playerNext) + var playerPause:ImageView = findView(R.id.playerPause) + var playerPrev:ImageView = findView(R.id.playerPrev) + var songinfo:TextView = findView(R.id.songinfo) + val player = new MusicPlayer with FolderBasedPlaylist + + + playerNext touch { + player.next + songinfo setText player.songinfo + } + playerPause touch { + player pause + } + playerPrev touch { + player.prev + songinfo setText player.songinfo + } + options touch { + + } + +} diff --git a/views/splashscreen.scala b/views/splashscreen.scala new file mode 100644 index 0000000..948b0cb --- /dev/null +++ b/views/splashscreen.scala @@ -0,0 +1,25 @@ +package de.tomhoefer.jogga.views + +import android.app.Activity +import android.view.{Window, WindowManager} +import android.os.Bundle +import android.media.{SoundPool, AudioManager} +import android.content.{Context} +import android.util.AttributeSet +import android.widget.{LinearLayout, TextView} +import android.view.{View, LayoutInflater} +import de.tomhoefer.jogga.R +import android.util.Log + +import de.tomhoefer.jogga.extensions.ActivityExt + + +class SplashScreen(c:Context, as:AttributeSet) extends LinearLayout(c,as) { + private val that:View = this + def this(c:Context) = this(c, null) + private val inf = getContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE).asInstanceOf[LayoutInflater] + inf.inflate(R.layout.splash, this, true) + private val splashText = findViewById(R.id.splashText).asInstanceOf[TextView] + def status = splashText.getText + def status_= (_status:String) = splashText.setText(_status) +} \ No newline at end of file