Is it just み?

[Android] Zugriff auf UI durch einen Service

by on Sep.29, 2011, under Programmierung

Da ich mich gerade wieder mit Android beschäftige, möchte ich hier einmal einen einfachen Weg darstellen, wie man die UI (Activity) von einem Hintergrund-Dienst (Service) aus verändern kann.

Wenn man im Netz nach einer Lösung für dieses Problem sucht, wird man schnell in Richtung Handler geschickt – diese Lösung finde ich für Minimalaufgaben zu groß und zu aufwändig. Ich wollte einfach nur Nachrichten an die UI schicken, sofern diese überhaupt da ist – dafür benötigt man keinen extra Handler.

Das Problem ist folgendes: Der Service läuft im Hintergrund und losgelöst von der UI – er arbeitet also in einem eigenen Thread. Die UI kann nur aus ihrem eigenen Thread verändert werden. Sobald ich also versuche aus dem Service direkt eine Methode in der UI-Activity aufzurufen um eben diese zu verändern, hagelt es Exceptions. Um das zu umgehen muss ich also irgendwie dem UI-Thread sagen “mach das für mich” – hier ist die einfachste Methode, die ich gefunden habe:

Man gibt dem UI-Thread Runnable-Objekte in seine Queue von abzuarbeitenden Aufgaben mit Hilfe der post()-Methode der View.

Und nun die Details:

Erst einmal muss ich den Service starten, so dass er unabhängig von der UI im Hintergrund läuft – das ist ein anderes Thema, dazu gibt es aber genügend Tutorials im Netz.

Wenn der Service einmal läuft muss er natürlich wissen ob eine Activity vorhanden ist und falls ja, wie er sie erreichen kann. Und hier kommt die schnelle Lösung ins Spiel, die mir zwar theoretisch ein wenig Kopfschmerzen macht, aber praktisch wunderbar funktioniert (sollte dies ein erfahrener Android-Entwickler lesen: Ich freue mich sehr über Verbesserungsvorschläge): Da der Service nur einmal im Hintergrund läuft, geben wir ihm einfach eine statische Singleton-Methode, die von der Activity abgefragt werden kann.

Jetzt müssen wir dem Service nur noch mitteilen, dass die Activity vorhanden ist (quasi wie einen Listener hinzufügen) und schon kann der Service jederzeit mit der Activity kommunizieren. Dafür erstellen wir eine Activity-Variable im Service und eine setActivity()-Methode. (Und falls wir mehrere Activities benachrichtigen wollen machen wir daraus eben eine List<Activity> und eine addActivity()-Methode.)

Jetzt kann der Service jederzeit Methoden auf der Activity aufrufen (so lange diese vorhanden ist).

Wir gehen also folgendermaßen vor:

  1. Activity wird gestartet.
  2. Activity erstellt einen Intent zum Starten des Service (da der Service nur einmal gestartet wird, wird dieser also ignoriert wenn der Service schon läuft).
  3. Die Activity erstellt einen Thread, der nichts anderes macht als die Singleton-Methode des Services aufzurufen – und zwar so lange bis ein valides Service-objekt zurückgegeben wird (das sollte im Normalfall sehr schnell gehen) und diesem nun die Activity mitteilt.
  4. Der Service überprüft wenn er etwas mitteilen soll ob die Activity vorhanden ist – und falls ja, teilt der Activity mit welche Methode ausgeführt werden soll.

Punkt vier ist der interessant Punkt, denn hier passiert die Übergabe von einem Thread in den anderen. In meinem Beispiel will ich der Activity einfach nur einen Text hinzufügen, dafür benutze ich folgenden Code in dem ich das Layout der Activity direkt manipuliere:

    public void showMessage(String message) {
        TextView tv = new TextView(this);
        tv.setText(message);
        layout.addView(tv);        
    }

Vorher habe ich das Layout (hier ein LinearLayout) und die View (hier Scrollview) in der onCreate()-Methode der Activity mit folgendem Code in den Membervariablen layout und view abgespeichert, so dass ich nun darauf zugreifen kann:

        view = new ScrollView(this);
        layout = new LinearLayout(this);
        layout.setOrientation(LinearLayout.VERTICAL);
        view.addView(layout);        

        setContentView(view);

Die Methode showMessage() funktioniert so aber nur, wenn sie im UI-Thread aufgerufen wird. Um das ganze aus dem Thread des Service ausführen zu können, verändere ich den Code folgendermaßen: (“MainActivity” muss durch den Namen der Activity-Klasse ersetzt werden.)

    public void showMessage(final String message) {
        view.post(new Runnable() {
            @Override
            public void run() {
                TextView tv = new TextView(MainActivity.this);
                tv.setText(message);
                layout.addView(tv);        
            }
        });
    }

Man beachte das “final” vor dem Parameter – dies ist notwendig um den Wert der Variablen im anderen Thread nutzen zu können. Nun muss ich showMessage() nur noch mit meiner Nachricht als Parameter aufrufen und es wird automatisch im UI-Thread ausgeführt.

 

Damit alles läuft und der Service auch startet: Nicht vergessen den Service in die AndroidManifest.xml einzutragen.


In meinem Fall handelt es sich um einen Hintergrunddienst, der passiv auf Location-Updates horcht und diese für spätere Verwendung speichert, hier der Beispiel-Quelltext (den Location-Kram habe ich entfernt und durch eine UI-Spam-Methode ersetzt):

Activity:

package mi.android.locationTracker;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.LinearLayout;
import android.widget.ScrollView;
import android.widget.TextView;

public class MainActivity extends Activity {
    // Classes should not contain public Properties. See Coding Guidelines.
    ////////////////////////////////// Constants ///////////////////////////////////
    /////////////////////////////// Static Properties //////////////////////////////
    ///////////////////////////// Public Static Methods ////////////////////////////
    ////////////////////// Protected & Private Static Methods //////////////////////
    ////////////////////////////////// Properties //////////////////////////////////

    private LinearLayout layout;
    private ScrollView view;

    private LocationTrackerService service;

    /////////////////////////// Constructor & Destructor ///////////////////////////
    ////////////////////////////// Implements Activity /////////////////////////////

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Create UI
        view = new ScrollView(this);
        layout = new LinearLayout(this);
        layout.setOrientation(LinearLayout.VERTICAL);
        view.addView(layout);        

        setContentView(view);

        showMessage("UI created...");

        showMessage("Starting service...");

        // Start Service if not already started
        startService(new Intent(this, LocationTrackerService.class));

        showMessage("Waiting for service...");

        // Now tell the service we are here
        new ServiceFetcher(this).start();
    }

    //////////////////////////////// Public Methods ////////////////////////////////

    public void showMessage(final String message) {
        view.post(
            new Runnable() {
                @Override
                public void run() {
                    TextView tv = new TextView(MainActivity.this);
                    tv.setText(message);
                    layout.addView(tv);
                }
            }
        );
    }

    ////////////////////////// Protected & Private Methods /////////////////////////
    //////////////////////////////// Nested Classes ////////////////////////////////

    public class ServiceFetcher extends Thread {
        MainActivity activity = null;

        public ServiceFetcher(MainActivity activity) {
            super();
            this.activity = activity;
        }

        @Override
        public void run() {
            while ((service = LocationTrackerService.getInstance()) == null) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {}
            }

            if (service instanceof LocationTrackerService) {
                service.setActivity(activity);
                showMessage("Service connected...");
            } else {
                showMessage("Service error:" + String.valueOf(service));
            }
        }
    }
}

Service:

package mi.android.locationTracker;

import java.util.Date;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;

public class LocationTrackerService extends Service {
    // Classes should not contain public Properties. See Coding Guidelines.
    ////////////////////////////////// Constants ///////////////////////////////////
    /////////////////////////////// Static Properties //////////////////////////////

    private static LocationTrackerService instance = null;

    ///////////////////////////// Public Static Methods ////////////////////////////

    public static LocationTrackerService getInstance() {
        return instance;
    }

    ////////////////////// Protected & Private Static Methods //////////////////////
    ////////////////////////////////// Properties //////////////////////////////////

    private MainActivity activity = null;

    /////////////////////////// Constructor & Destructor ///////////////////////////
    ////////////////////////////// Implements Service //////////////////////////////

    @Override
    public IBinder onBind(Intent arg0) {
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();

        // Horribly simple singleton. Should suffice for our purposes
        instance = this;

        // [snipped] Request passive Location updates etc. Not part of this tutorial...

        // Just for kicks, spam the UI:
        new Thread() {
            @Override
            public void run() {
                while (true) {
                    try {
                        onSomethingHappened();                        
                        Thread.sleep(5000);
                    } catch (Exception e) {
                        // TODO: handle exception
                    }
                }
            }
        }.start();

    }

    public void onSomethingHappened() {
        // Pseudo-method that is called when the activity should be notified

        if (activity instanceof MainActivity) {
            try {
                StringBuffer message = new StringBuffer();
                message.append(new Date().toLocaleString());
                message.append("Calling UI...");

                activity.showMessage(message.toString());
            } catch (Exception e) {
                // TODO: The activity does not seem to be there.
            }
        }

    }

    //////////////////////////////// Public Methods ////////////////////////////////

    public void setActivity(MainActivity activity) {
        this.activity = activity;
    }

    ////////////////////////// Protected & Private Methods /////////////////////////
    //////////////////////////////// Nested Classes ////////////////////////////////
}

HTH und ich freue mich über Verbesserungsvorschläge.

Ähnliche Beiträge

:,

Comments are closed.