From f5cc71c0b77f11b2138a0b84176b9649eba6920a Mon Sep 17 00:00:00 2001 From: thoefer Date: Mon, 7 May 2012 08:40:48 +0200 Subject: [PATCH] initial --- JoggingActivity.scala | 234 ++++++++++++++++++++++++++++ MainActivity.scala | 106 +++++++++++++ SettingsActivity.scala | 156 +++++++++++++++++++ components/linechart.scala | 151 ++++++++++++++++++ components/musicplayer.scala | 164 ++++++++++++++++++++ components/observable.scala | 24 +++ components/pedometer.scala | 71 +++++++++ components/speecher.scala | 49 ++++++ components/timer.scala | 44 ++++++ components/tracking.scala | 106 +++++++++++++ extensions.scala | 292 +++++++++++++++++++++++++++++++++++ models/joggingmodel.scala | 224 +++++++++++++++++++++++++++ models/usermodel.scala | 60 +++++++ service/backend.scala | 78 ++++++++++ views/joggingview.scala | 54 +++++++ views/splashscreen.scala | 25 +++ 16 files changed, 1838 insertions(+) create mode 100644 JoggingActivity.scala create mode 100644 MainActivity.scala create mode 100644 SettingsActivity.scala create mode 100644 components/linechart.scala create mode 100644 components/musicplayer.scala create mode 100644 components/observable.scala create mode 100644 components/pedometer.scala create mode 100644 components/speecher.scala create mode 100644 components/timer.scala create mode 100644 components/tracking.scala create mode 100644 extensions.scala create mode 100644 models/joggingmodel.scala create mode 100644 models/usermodel.scala create mode 100644 service/backend.scala create mode 100644 views/joggingview.scala create mode 100644 views/splashscreen.scala 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 = + {options('birthdate)} + {options('country)} + {options('email)} + {options('password)} + {options('username)} + + + /* + * Http-Funktionalitaet instanziieren + */ + val client = new DefaultHttpClient + // 192.168.178.32 + val post = getDefaultPost("http://192.168.1.34/users") + val entity = new StringEntity(xml.toString) + post setEntity entity + /* + * execute blockiert + */ + val response = client execute post + val responseXml = XML load(response.getEntity.getContent) + + if(response.getStatusLine.getStatusCode == 201) { + val username = (responseXml \ "username").text + val userId = (responseXml \ "id").text.toInt + success(userId) + } else { + val errorStr = (responseXml \\ "error").foldLeft(new StringBuilder("")) {(acc, error) => acc append "

"+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