Android, Eieren, Services en Actions?

Door Wiebbe op maandag 22 juni 2009 14:59 - Reacties (1)
CategorieŽn: android, java, Views: 3.420

Eier wekkers

Zoals lezers van mijn vorige 2 blogs misschien hebben gelezen ben ik nogal bezig met het schrijven van AppWidgets voor Android Cupcake. Waar het oorspronkelijk mee begon was eigenlijk een stopwatch/eier wekker timer voor het homescreen. Een klokje leek mij de meest logische stap!

Nou heb ik nu al 2 manieren besproken om een klokje weer te geven, de eerste is door elke seconde een update te verzenden, de tweede doormiddel van een Widget chronometer die zelf telt en zelfs ook nog secondes laat zien.

Maar dan kom ik nog steeds niet op een manier die handig is voor een eierwekker/countdown timer.

Als ik de eerste manier gebruik, dan zal ik het zeer makkelijk kunnen maken. Het grote nadeel is dat ongeacht of je bezig bent met een timer je ALTIJD elke seconde een update uitvoert. Is ontzettend zonde van de performance en je batterij!

De tweede manier is leuk, maar kan alleen naar boven tellen. Alhoewel ik het misschien negatief kan gebruiken (dus -01:00 opgeven) gaat me dat waarschijnlijk alleen maar nog meer problemen opleveren.

Dus wat nu?

Ik kon het me niet anders voorstellen dat de grote bij google hier niet aan gedacht zouden hebben. Als je kijkt naar bijvoorbeeld de AnalogClock AppWidget van het homescreen dan zie je dat hij precies gelijk loopt op de minuut, standaard is ingesteld op elke Android phone en ook niet je batterij leeg slurpt. Dus hoe zullen ze dat dan doen?

Android Intents/Actions

De Android ontwikkelaar hebben zogenaamde Intents ingebouwd. Intents kunnen op meerdere manieren gebruikt worden, voornamelijk worden ze gebruikt in een applicatie om een Action uit te voeren.

Je kan bijvoorbeeld de ACTION_VIEW intent gebruikt om een call naar buiten uit te voeren. Op het moment dat een application op de Android phone een Intent uitvoert, zal deze vaak ook een broadcastIntent doen en een systemwijd event starten waar je voor kan registreren deze te ontvangen.

Dit ontvangen gebeurd via een zogenaamde BroadcastReceiver. Deze registreer je, geeft aan met een filter op welke intents je wilt reageren en deze kan je hierna dan ontvangen.

Zoals mede-tweaker Snake al poste in een eerdere post, kan je een service registreren in Android. Op zich is dit niet nodig, alles wat we doen zou je gewoon in de application zelf kunnen doen.

Het registreren van de BroadCastReceiver en de callback die hij dan krijgt doormiddel van de Intents gaat als volgt:

Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private BroadcastReceiver mClockInfoReceiver = new BroadcastReceiver() { 
            @Override 
            public void onReceive(Context context, Intent intent) {
                
                RemoteViews updateViews = null;
                
                 String action = intent.getAction(); 
                 if (Intent.ACTION_TIME_TICK.equals(action)) { 

                     Log.d(TAG, "ACTION_TIME_TICK Has been hit!");
                     
                     updateViews = new RemoteViews(context.getPackageName(), R.layout.widget);

                     final Calendar c = Calendar.getInstance();
                     
                     updateViews.setTextViewText(R.id.Klok, new StringBuilder().append(DateFormat.format("kk:mm", c)));
                     updateViews.setTextViewText(R.id.Datum, new StringBuilder().append(DateFormat.format("EE, dd MMM yy", c)));
                     
                     ComponentName thisWidget = new ComponentName(context, DigitaleKlokService.class);
                     AppWidgetManager manager = AppWidgetManager.getInstance(context);
                     manager.updateAppWidget(thisWidget, updateViews);
                 } 
             } 
       };



Zoals je hier ziet declareer ik hier een nieuwe variabele van het type BroadcastReceiver genaamd mClockInfoReceiver. In deze declaratie MOET je van eclipse de OnReceive method overriden. Deze method ontvangt de callback/broadcast die gedaan wordt door alle Intents in de gehele system.

Aangezien we deze maar 1x hoeven te registreren zou je in de OnEnabled in plaats van de OnUpdate van je AppWidgetProvider de receiver kunnen registreren:


Java:
1
2
3
4
5
6
7
8
9
public class DigitaleKlokService extends AppWidgetProvider {

    final static String TAG = "DigitaleKlokService";
    
        public void onEnabled(Context context) 
        {
            registerReceiver(mClockInfoReceiver, new IntentFilter(Intent.ACTION_TIME_TICK));
            
        }



Hiermee registreer je hem, hij start de receiver en poef, we wachten op alle results. Zoals je ook ziet heb ik de Intent ACTION_TIME_TICK geregistreerd, of op gefilterd. Als je kijkt op de Intent pagina zie je dat deze:
Broadcast Action: The current time has changed. Sent every minute.
Dat is inderdaad precies waar ik naar zocht! Er staat er nog veeeeeel meer, maar voor nu hebben we alleen deze nodig.

De oplossing die we nu hebben werkt, maar helaas niet goed. Op het moment dat we de receiver starten gaat jou application zitten wachten op de Intent ACTION_TIME_CHANGED. Terwijl hij dit doet vind de Android software dat hij niet respond en wil hem killen. Erg onhandig dus.

De oplossing hiervoor is om gebruik te maken van een Service.

Android Services

Services in Android doen veel hetzelfde als in Windows. Ze draaien op de achtergrond en doen daar updates of reageren op callbacks. In Android kan je ze dus gebruiken om updates uit te voeren zonder dat jou AppWidget een ANR krijgt.

Het gebruik hiervan is net zoals de rest van het hele Android programmeren, simpel!

Eerst moet je nog in je AndroidManifest.xml aangeven dat je gebruik maakt van een service, en als volgt:


XML:
1
<service android:name=".DigitaleKlokService$UpdateService" />



Hiermee geef je aan dat de service UpdateService heet en onderdeel is van de DigitaleKlokService class!

Om hem te kunnen gebruiken moeten we een nieuwe class aanmaken die we laten overerven van de Service class. Wij noemen hem UpdateService


Java:
1
public static class UpdateService extends Service {



Deze class staat overigens in de DigitaleKlokService klasse en we willen hem maar 1x gedeclareerd hebben. Vandaar de static. Je zou hem ook gewoon buiten je class kunnen laten, maar dit is zoals de Android chaps het hebben gedaan in hun eigen AppWidgets!

De service heeft eigenlijk maar 1 belangrijke method namelijk de OnStart() method. Deze method wordt aangeroepen op het moment dat we de service gaan starten. Het enige wat wij willen doen is dus een Broadcast receiver starten!


Java:
1
2
3
4
5
@Override
        public void onStart(Intent intent, int startId) {
        
            registerReceiver(mClockInfoReceiver, new IntentFilter(Intent.ACTION_TIME_TICK));
        }



hier maken we net als hiervoor de BroadCastReceiver aan zodat hij de tijd kan ontvangen.

Op het moment dat er nu een ACTION_TIME_TICK event binnen komt op de service, krijgt hij een schop en zal hij de widget gaan updaten. Een belangrijk punt is dat hij de update nu alleen maar uit voert voor de eerste widget die je plaatst.


De gehele class van DigitaleKlokService is nu als volgt:


Java:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
public class DigitaleKlokService extends AppWidgetProvider {

    final static String TAG = "DigitaleKlokService";
    
    private Intent runningService;
    private boolean initStarted;
    
    public void onEnabled(Context context) 
    {
        if(!initStarted)
        {
            runningService = new Intent(context, UpdateService.class);
            // To prevent any ANR timeouts, we perform the update in a service
            context.startService(runningService);
            
            Log.d(TAG, "Service Started");
            
            RemoteViews updateViews = null;
            
            updateViews = new RemoteViews(context.getPackageName(), R.layout.widget);
            
            final Calendar c = Calendar.getInstance();
             
            
            //updateViews.setTextViewText(R.id.level, Integer.toString(level));
            updateViews.setTextViewText(R.id.Klok,     new StringBuilder().append(DateFormat.format("kk:mm", c)));
            updateViews.setTextViewText(R.id.Datum,    new StringBuilder().append(DateFormat.format("EE, dd MMM yy", c)));
         
            
            // Push update for this widget to the home screen
            ComponentName thisWidget = new ComponentName(context , DigitaleKlokService.class);
            AppWidgetManager manager = AppWidgetManager.getInstance(context);
            manager.updateAppWidget(thisWidget, updateViews);
            
            initStarted = true;
        }
    }
    
    public void onDisabled(Context context) 
    {
        if(initStarted)
        {
            context.stopService(runningService);
            
            Log.d(TAG, "Service Stopped");
        }
    }
    
    public static class UpdateService extends Service {
        @Override
        public void onStart(Intent intent, int startId) {
        
            registerReceiver(mClockInfoReceiver, new IntentFilter(Intent.ACTION_TIME_TICK));
        }

        private BroadcastReceiver mClockInfoReceiver = new BroadcastReceiver() { 
            @Override 
            public void onReceive(Context context, Intent intent) {
                
                RemoteViews updateViews = null;
                
                 String action = intent.getAction(); 
                 if (Intent.ACTION_TIME_TICK.equals(action)) { 

                     Log.d(TAG, "ACTION_TIME_TICK Has been hit!");
                     
                     updateViews = new RemoteViews(context.getPackageName(), R.layout.widget);

                     final Calendar c = Calendar.getInstance();
                     
                     updateViews.setTextViewText(R.id.Klok, new StringBuilder().append(DateFormat.format("kk:mm", c)));
                     updateViews.setTextViewText(R.id.Datum, new StringBuilder().append(DateFormat.format("EE, dd MMM yy", c)));
                     
                     
                      // Push update for this widget to the home screen
                     ComponentName thisWidget = new ComponentName(context, DigitaleKlokService.class);
                     AppWidgetManager manager = AppWidgetManager.getInstance(context);
                     manager.updateAppWidget(thisWidget, updateViews);
                 } 
            } 
       }; 
        
        @Override
        public IBinder onBind(Intent intent) {
            // We don't need to bind to this service
            return null;
        }
    }
}



Wat hier nog wel belangrijk is, is dat je ziet dat ik hem bij de onstart nog eenmaal even update qua tijd. Dit doe ik omdat anders pas na de 1e minuut de tijd up to date zou zijn. Dit is hem dus, de tijd updaten doormiddel van ACTION_TIME_TICK.

Als je dit na bouwt zou ik nog kijken of je ACTION_TIME_CHANGED en ACTION_DATE_CHANGED kan toevoegen. Want als je nu handmatig te tijd update, blijft deze staan op de oude tot de eerste TICK tegenkomt ;)

Volgende post, de daadwerkelijke eijer wekken!

Volgende: Als er een macbook over de dam is... 04-'10 Als er een macbook over de dam is...
Volgende: Android DigitaleKlok widget Redux 06-'09 Android DigitaleKlok widget Redux

Reacties


Door Tweakers user Snake, maandag 22 juni 2009 17:39

Nice post. Even onderlijnen dat je die service MOET specifieren in je AndroidManifest.xml, anders runt hij gewoonweg niet.

Reageren is niet meer mogelijk