Hintergründiges Teil 1

Windows Phone Apps im Hintergrund mit Background Agents
Kommentare

Auf der Windows-Phone-Plattform kann immer nur eine App aktiv im Vordergrund ausgeführt werden. Alle im Hintergrund befindlichen Apps werden gestoppt. Diese aus zwei Teilen bestehende Artikelserie soll einen Überblick über die Ausnahmen und Möglichkeiten geben, um Aufgaben im Hintergrund auszuführen.

Unter Windows Phone ist es nicht möglich, mehrere Apps gleichzeitig laufen zu lassen. Es kann immer jeweils nur eine App im Vordergrund ausgeführt werden. Eine der wenigen Ausnahmen bilden Apps, die im Hintergrund Positionsdaten verarbeiten. Der Grund für diese Einschränkung liegt in den knappen Ressourcen auf einem mobilen Gerät. Jede Anwendung, die im Hintergrund weiterläuft, verbraucht Prozessor-, Speicher- und Akkuressourcen. Um eine möglichst lange Laufzeit zu ermöglichen, wurde die Option zum Ausführen von Aufgaben im Hintergrund auf so genannte Hintergrundagenten (engl. Background Agents) beschränkt. Auf der Windows-Phone-8.1-Plattform gibt es zwei Möglichkeiten zur Entwicklung von Hintergrundaufgaben. Wie unter Windows Phone 8.0 stehen weiterhin die Background Agents in einem Silverlight-für-Windows-Phone-Projekt zur Verfügung. Dazu gekommen sind die Background Tasks in einem Windows-Runtime-(WinRT)- und/oder Universal-App-Projekt.

Dieser Artikel gliedert sich in zwei Teile. In diesem ersten Teil wird es um Alarme, Erinnerungen, benutzerdefinierte Agenten sowie Hintergrundaudio gehen, im zweiten Teil um Dateiübertragungen und Location Tracking im Hintergrund sowie die neuen BackGroundTasks der Windows-Phone-8.1-Plattform. Die Quellcodes zu diesem Artikel finden Sie hier.

Background Agents (Windows Phone 8 und 8.1)

Auf der Silverlight-für-Windows-Phone-Plattform kann eine Aktion zu einem bestimmten Zeitpunkt ausgeführt werden. Alle diese Aktionen leiten sich von der Basisklasse ScheduledAction ab, die sich in die zwei Gruppen ScheduledNotification (Benachrichtigungen) und ScheduledTask (Aufgaben) unterteilt (Abb. 1). Die ScheduledNotifications unterteilen sich wiederum in Reminder (Erinnerung) und Alarm (Wecker). Ein ScheduledTask ist entweder ein PeriodicTask oder ein RessourceIntensiveTask.

Abb. 1: Übersicht über die Basisklasse „ScheduledAction“

Alarme und Erinnerungen

Die einfachste Möglichkeit zur Erstellung einer Hintergrundaufgabe ist die Verwendung einer Erinnerung (Reminder) oder eines Weckers (Alarm) – siehe Beispielprojekt „01 Notifications“. Um eine Erinnerung zu erstellen, legen Sie zunächst eine Klasse Reminder mit einem eindeutigen Namen an. Anschließend können Sie den Zeitpunkt festlegen, an dem Sie die Erinnerung in Form einer Toast Notification erhalten möchten, Sie können bestimmen, wie lange die Erinnerung gültig sein soll, wie oft die Erinnerung wiederholt werden soll sowie ggf. einen DeepLink in Ihre App integrieren, der geöffnet wird, wenn der Benutzer auf die Toast Notification tippt. Nachdem Sie alles konfiguriert haben, fügen Sie die ScheduledNotification mithilfe des ScheduledActionService hinzu:

Reminder myReminder = new Reminder("myFirstReminder") {
  BeginTime = DateTime.Now + TimeSpan.FromMinutes(5),
  ExpirationTime = DateTime.Now + TimeSpan.FromDays(2),
  RecurrenceType = RecurrenceInterval.Daily,
  NavigationUri = new Uri("/ReminderPage.xaml?from=myReminder")
};
ScheduledActionService.Add(myReminder);

Auf die gleiche Weise lässt sich alternativ auch ein Alarm erstellen. Der Unterscheid zwischen einer Erinnerung und einem Alarm liegt im Wesentlichen darin, dass Sie bei einem Alarm keinen DeepLink in Ihre App, dafür aber einen eigenen Sound angeben können. Die Sound-Datei muss sich dabei als Ressource oder Content innerhalb ihrer App befinden:

Alarm myAlarm = new Alarm("myAlarm") {
  Content = "Mein Alarm",
    BeginTime = DateTime.Now + TimeSpan.FromMinutes(5),
    ExpirationTime = DateTime.Now + TimeSpan.FromDays(2),
    RecurrenceType = RecurrenceInterval.Daily,
    Sound = new Uri("/Assembly;component/dir/alarm.mp3", UriKind.Relative)
};
ScheduledActionService.Add(myAlarm);

Eine App kann die von Ihnen angelegten Alarme und Erinnerungen mithilfe der Klasse ScheduledActionService verwalten. Die generische Methode GetAction kann alle bereits angelegten Alarme oder Erinnerungen abrufen. Alternativ können einzelne Notifications mithilfe ihres Namens gefunden werden. Dabei ist allerdings zu beachten, dass diese Funktion die Basisklasse ScheduledNotification zurückgibt:

// Abrufen aller Alarme
var alarms = ScheduledActionService.GetActions();
// Finden eines einzelnen Alarms
var alarm = ScheduledActionService.Find("myAlarm");
// Ersetzen eines einzelnen Alarms 
ScheduledActionService.Replace(alarm);
// Entfernen eines einzelnen Alarms
ScheduledActionService.Remove("myAlarm");
</alarm>

Die Genauigkeit eines Alarms oder einer Erinnerung beträgt auf dem Windows Phone plus/minus eine Minute. Wenn die ExpirationTime vor der BeginTime oder die BeginTime in der Vergangenheit liegt, wird eine entsprechende Exception beim Anlegen der Notification geworfen.

Hintergrundaufgaben

Eine Hintergrundaufgabe besteht aus zwei Teilen: einem Codeteil, der in der Vordergrund-App ausgeführt wird, und einem zweiten Teil, der im Hintergrundagent ausgeführt wird. Hierfür stehen verschiedene Agents bereit, die sich wiederum in zwei Gruppen unterteilen: die System-Agents sind bereits vorprogrammierte Agents mit einer vordefinierten Aufgabe, die Generic-Agents beinhalten benutzerdefinierte Hintergrundaufgaben (Abb. 2).

Abb. 2: Übersicht über Background-Agents

Eine benutzerdefinierte Hintergrundaufgabe wird in einem separaten „Scheduled Task Agent“-Projekt erstellt. Der Agent leitet sich von der Basisklasse ScheduledTaskAgent ab. Der Code, der später im Hintergrund laufen soll, wird in der überschiebenen Funktion OnInvoke abgelegt. Um einen Agent und eine App miteinander zu verbinden, ist es erforderlich, eine Referenz auf das Agentprojekt vom Vordergrund-App-Projekt aus anzulegen (Abb. 3). Es kann maximal ein Agent pro App verwendet werden. Anschließend kann der Agent mithilfe eines ScheduledTask für die Ausführung geplant (scheduled) werden. Der genaue Zeitpunkt, wann und mit welchen Rahmenbedingungen der Agent letztlich zur Ausführung kommt, hängt davon ab, welche der zwei ScheduledTasks (PeriodicTask oder RessourceIntensiveTask) zur Ausführung des Agenten verwendet wird.

Abb. 3: Ausführung eines Agents

Dabei gelten folgende Bedingungen: Ein PeriodicTask läuft ca. alle 30 Minuten plus/minus zehn Minuten für ca. 15 bis 25 Sekunden (oder kürzer). Eine exakte Zeit wird nicht garantiert. Das Betriebssystem entscheidet, wie lange ein benutzerdefinierter Agent laufen darf. Der Stromsparmodus kann das Laufen ggf. unterbinden und pro Gerät dürfen max. sechs bzw. neun Agenten gleichzeitig aktiv sein. Eine RessourceIntensiveTask läuft nur, wenn eine externe Energiequelle verbunden ist, eine WiFi-Verbindung besteht, der Akku mindestens zu 90 Prozent vollgeladen ist, der Lockscreen aktiv ist und kein Gespräch geführt wird. Es ist möglich, dass eine RessourceIntensiveTask aufgrund dieser Bedingungen nie ausgeführt wird. Das Beispielprojekt „20 PeriodicUpdateAgent“ zeigt die Implementierung eines ScheduledTaskAgent im Zusammenspiel mit der App „02 GenericAgent“ (Listing 1).

namespace PeriodicUpdateAgant {
  public class ScheduledAgent : ScheduledTaskAgent {
    /// <remarks>
    /// ScheduledAgent constructor, initializes the UnhandledException handler
    /// </remarks>
    static ScheduledAgent() {
      // Subscribe to the managed exception handler
      Deployment.Current.Dispatcher.BeginInvoke(delegate {
        Application.Current.UnhandledException += UnhandledException;
      });
    }

    /// Code to execute on Unhandled Exceptions
    private static void UnhandledException(object sender, ApplicationUnhandledExceptionEventArgs e) {
      if (Debugger.IsAttached) {
        // An unhandled exception has occurred; break into the debugger
        Debugger.Break();
      }
    }


    protected override void OnInvoke(ScheduledTask task) {
      string toastMessage = DateTime.Now.ToString("");

      var toast = new ShellToast {
        Title = "Background Agent Sample", 
        Content = toastMessage
      };
      toast.Show();

      #if DEBUG
      ScheduledActionService.LaunchForTest(task.Name,TimeSpan.FromSeconds(60));
      #endif

      NotifyComplete();
    }
  }
}

Innerhalb der Methode OnInvoke wird eine neue Toast-Nachricht erstellt. Diese Nachricht wird jeweils angezeigt, wenn der Agent zur Ausführung kommt. Da Agenten aufgrund der zuvor beschriebenen Ausführungsregeln nur ca. alle 30 Minuten ausgeführt werden, gibt es die Möglichkeit, einen Agenten während des Debuggens mit der Funktion LaunchForTest der Klasse ScheduledActionService früher auszuführen. Soll der Agent mehrfach aufgerufen werden, können Sie den Aufruf für Test- und Analysezwecke auch im Agenten wiederholen:

ScheduledActionService.LaunchForTest(task.Name,TimeSpan.FromSeconds(60));

Dabei ist aber unbedingt zu beachten, dass der Aufruf der Funktion nicht im ausgelieferten Code vorhanden ist, da das Projekt sonst bei der Zertifizierung im Store abgelehnt wird. Nachdem eine Referenz vom Agentprojekt auf das Vordergrundprojekt eingefügt wurde, kann der Agent z. B. in der OnNavigateTo-Methode für die Ausführung geplant werden (Listing 2). Falls der Agent bereits für die Ausführung geplant ist, sollte dieser vor der Neuplanung zunächst entfernt werden.

protected override void OnNavigatedTo(NavigationEventArgs e) {
  // Referenz auf die PeriodicTask ermitteln
  var periodicTask = ScheduledActionService.Find("PeriodicAgent") as PeriodicTask;

  if (periodicTask != null) {
    RemoveAgent("PeriodicAgent");
  }

  // Eine periodicTask muss eine Beschreibung enthalten, die auf der Settings-Seite vom OS angezeigt werden kann
  periodicTask = new PeriodicTask("PeriodicAgent") {
    Description = "This demos a periodic task."
  };

  // Alternativ als ResourceIntensiveTask möglich
  //// Eine ResourceIntensiveTask muss eine Beschreibung enthalten, die auf der Settings-Seite vom OS angezeigt werden kann
  //var intensivTask = new ResourceIntensiveTask("PeriodicAgent") {
    //  Description = "This demos a periodic task."
  //};

  // Das Hinzufügen eines Agents sollte immer in einem Try-Block erfolgen, der Benutzer könnte Agents für diese App oder alle Agents deaktiviert haben, bzw. es könnten keine Ressourcen mehr übrig sein.
  try {
    ScheduledActionService.Add(periodicTask);

    // Wenn Debug aktiv ist, die Funktion LaunchForTest verwenden
    #if(DEBUG)
    ScheduledActionService.LaunchForTest("PeriodicAgent", TimeSpan.FromSeconds(30));
    #endif
  } catch (InvalidOperationException exception) {
    if (exception.Message.Contains("BNS Error: The action is disabled")) {
      MessageBox.Show("Hintergrundaufgaben sind für diese App deaktiviert.");
    }

    if (
      exception.Message.Contains(
        "BNS Error: The maximum number of ScheduledActions of this type have already been added.")) {
      // Keine Aktion erforderlich, das System gibt selbstständig keine Meldung mit dem aktuellen Limit aus.

    }
  } catch (SchedulerServiceException) {
    // Keine weitere Aktion erforderlich
  }
}

Hintergrundaudio

Für Windows Phone gibt es zwei Möglichkeiten zur Wiedergabe von Audiodaten: entweder mithilfe des Mediaelements innerhalb der Vordergrund-App oder mithilfe des Mediaelements via BackGroundAudioPlayer. Die Verwendung des Mediaelements in der Vordergrund-App hat den Vorteil, dass alle Funktionen direkt aufgerufen werden können, aber auch den Nachteil, dass die Wiedergabe stoppt, sobald die App in den Hintergrund verschoben wird. Aus diesem Grund soll die Wiedergabe mithilfe eines entsprechenden Agents im Hintergrund im Folgenden etwas genauer betrachtet werden.

Für das Abspielen von Audiodateien oder -streams im Hintergrund ist der BackgroundAudioPlayer zuständig. Dieser stellt eine Schnittstelle zwischen der Vordergrund-App und dem Agenten bereit. Die Befehle der Vordergrund-App werden mithilfe des BackgroundAudioPlayer an den Agenten weitergegeben, Ereignisse im Gegenzug vom Agenten an die App übermittelt. Es gibt nur einen BackgroundAudioPlayer, der nicht nur mit den Vordergrund-Apps, sondern auch mit den Steuerelementen der Universal Volume Control interagiert. Der AudioPlayerAgent stellt dem Hintergrund-Media-Element (Media Queue) die Metainformationen des jeweils zu spielenden Titels bereit (Abb. 4). Da sich der AudioPlayerAgent und die Vordergrund-App nicht im gleichen Task befinden, ist es erforderlich, dass sich die abzuspielenden Daten entweder im gemeinsamen Isolated Storage oder auf einem Streaming-Server außerhalb befinden. Es ist nicht möglich, eine Ressource oder ein Content-Element mit dem BackgroundAudioPlayer abzuspielen.

Abb. 4: Ausführung eines Agents

Nach dem Beenden der Vordergrund-App bleibt jeweils der AudioPlayerAgent der zuletzt verwendeten App mit einem entsprechenden Agent aktiv. Auf diese Weise ist es möglich, die Wiedergabe über die Universal Volume Control zu steuern, ohne die App zu starten.

Der Audio-Player-Agent im Beispiel „30 BackgroundAudioAgent“ hat eine statische Liste von Elementen (Listing 3).

private static List _playList = new List {
  new AudioTrack(new Uri("Demo01.mp3", UriKind.Relative), "Title 01", "Artist 01", "Album 01", null),
  new AudioTrack(new Uri("Demo02.mp3", UriKind.Relative), "Title 02", "Artist 02", "Album 02", null),
  new AudioTrack(new Uri("Demo03.mp3", UriKind.Relative), "Title 03", "Artist 03", "Album 03", null),

  // Eine Streaming-Adresse 
  new AudioTrack(new Uri("<a class="elf-external elf-icon" href="http://traffic.libsyn.com/wpradio/WPRadio_29.mp3" rel="nofollow">http://traffic.libsyn.com/wpradio/WPRadio_29.mp3</a>", UriKind.Absolute),
    "Episode 29",
    "Windows Phone Radio",
    "Windows Phone Radio Podcast",
  null)
};
</audiotrack> </audiotrack>

Der Agent verfügt über zwei überschriebene Methoden, die jeweils auf Ereignisse des Players oder auf Benutzerbefehle reagieren: OnPlayStateChanged und OnUserAction (Listing 4).

protected override void OnUserAction(BackgroundAudioPlayer player, AudioTrack track, UserAction action, object param) {
  switch (action) {
    case UserAction.Play:
    if (player.PlayerState != PlayState.Playing) {
      player.Play();
    }
    break;
    case UserAction.Stop:
      player.Stop();
    break;
    case UserAction.Pause:
      player.Pause();
    break;
    case UserAction.FastForward:
      player.FastForward();
    break;
    case UserAction.Rewind:
      player.Rewind();
    break;
    case UserAction.Seek:
      player.Position = (TimeSpan)param;
    break;
    case UserAction.SkipNext:
      player.Track = GetNextTrack();
    break;
    case UserAction.SkipPrevious:
      AudioTrack previousTrack = GetPreviousTrack();
    if (previousTrack != null) {
      player.Track = previousTrack;
    }
    break;
  }

  NotifyComplete();
}

Die Methoden GetNextTrack( ) und GetPreviousTrack( ) wählen jeweils den nächsten oder vorherigen Audiotrack aus (Listing 5).

private AudioTrack GetNextTrack() {
  if (++_currentTrackNumber >= _playList.Count) {
    _currentTrackNumber = 0;
  }

  AudioTrack track = _playList[_currentTrackNumber];
  return track;
}
private AudioTrack GetPreviousTrack() {
  if (--_currentTrackNumber < 0) {
    _currentTrackNumber = _playList.Count -1;
  }

  AudioTrack track = _playList[_currentTrackNumber];
  return track;
}

Die Vordergrund-App „04 BackgroundAudio“ benötigt die Hilfsfunktion CopyToIsolatedStorage, um die eingebetteten Sounds aus der App in den IsolatedStorage zu kopieren (Listing 6).

private void CopyToIsolatedStorage() {
  using (IsolatedStorageFile storage = IsolatedStorageFile.GetUserStoreForApplication()) {
    var files = new string[] { "Demo01.mp3", "Demo02.mp3", "Demo03.mp3" };

    foreach (var fileName in files) {
      if (!storage.FileExists(fileName)) {
        string filePath = "Audio/" + fileName;
        StreamResourceInfo resource = Application.GetResourceStream(new Uri(filePath, UriKind.Relative));

        using (IsolatedStorageFileStream file = storage.CreateFile(fileName)) {
          const int chunkSize = 4096;
          var bytes = new byte[chunkSize];
          int byteCount;

          while ((byteCount = resource.Stream.Read(bytes, 0, chunkSize)) > 0) {
            file.Write(bytes, 0, byteCount);
          }
        }
      }
    }
  }
}

Beim Navigieren auf die MainPage wird der Ereignis-Handler des BackgroundAudioPlayers verbunden. Dieser liefert Ereignisse des Agenten und des MediaElements (Listing 7).

protected override void OnNavigatedTo(NavigationEventArgs e) {
  BackgroundAudioPlayer.Instance.PlayStateChanged += new EventHandler(Instance_PlayStateChanged);

  if (PlayState.Playing == BackgroundAudioPlayer.Instance.PlayerState) {
    playButton.Content = "pause";
    txtCurrentTrack.Text = BackgroundAudioPlayer.Instance.Track.Title +
      " by " +
    BackgroundAudioPlayer.Instance.Track.Artist;

  } else {
    playButton.Content = "play";
    txtCurrentTrack.Text = "";
  }
}

Wenn sich der Status des Players ändert, soll sich auch die Beschriftung der Buttons und des Informationstexts ändern (Listing 8).

void Instance_PlayStateChanged(object sender, EventArgs e) {
  switch (BackgroundAudioPlayer.Instance.PlayerState) {
    case PlayState.Playing:
    playButton.Content = "pause";
    break;

    case PlayState.Paused:
    case PlayState.Stopped:
      playButton.Content = "play";
    break;
  }

  if (null != BackgroundAudioPlayer.Instance.Track) {
    txtCurrentTrack.Text = BackgroundAudioPlayer.Instance.Track.Title +
      " by " +
    BackgroundAudioPlayer.Instance.Track.Artist;
  }
}

Anschließend werden die Buttons jeweils mithilfe des BackGroundAudioPlayers mit dem Agent verbunden (Listing 9).

private void prevButton_Click(object sender, RoutedEventArgs e) {
  BackgroundAudioPlayer.Instance.SkipPrevious();
}
private void playButton_Click(object sender, RoutedEventArgs e) {
  if (PlayState.Playing == BackgroundAudioPlayer.Instance.PlayerState) {
    BackgroundAudioPlayer.Instance.Pause();
  } else {
    BackgroundAudioPlayer.Instance.Play();
  }
}
private void nextButton_Click(object sender, RoutedEventArgs e) {
  BackgroundAudioPlayer.Instance.SkipNext();
}

Ausblick

Im zweiten Teil dieser Artikelserie wird es um Hintergrunddateiübertragungen, Location Tracking im Hintergrund sowie die neuen BackGroundTasks der Windows-Phone-8.1-Plattform gehen.

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -