Mobiltelefonapplikationer med J2ME: (Preliminära) lösningar till tentamen 2006-06-03

Observera att detta är förslag på lösningar. Det kan finnas andra lösningar som också är korrekta, och det kan hända att en del av lösningarna är mer omfattande än vad som krävs för full poäng på uppgiften.

Uppgift 1 (10 p)

Den här uppgiften ska man bara göra om man läst kursen enligt den gamla kursplanen (höstterminen 2005), när det inte ingick några obligatoriska inlämningsuppgifter i kursen.
import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

public class Logger extends MIDlet implements CommandListener {
    private Display displayen;
    private TextBox loggboxen;
    private List logglistan;
    private Command avslutakommandot, bytkommandot, hejkommandot, hoppkommandot, rensakommandot;

    public Logger() {
        loggboxen = new TextBox("Givna kommandon", "", 1000, TextField.UNEDITABLE);
        logglistan = new List("Givna kommandon", List.IMPLICIT);

        avslutakommandot = new Command("Avsluta", "Avsluta komihåg-listan", Command.EXIT, 0);
        bytkommandot = new Command("Byt", "Byt", Command.SCREEN, 0);
        rensakommandot = new Command("Rensa", "Rensa", Command.SCREEN, 1);
        hejkommandot = new Command("Hej", "Hej", Command.SCREEN, 1);
        hoppkommandot = new Command("Hopp", "Hopp", Command.SCREEN, 1);

        loggboxen.addCommand(avslutakommandot);
        loggboxen.addCommand(bytkommandot);
        loggboxen.addCommand(rensakommandot);
        loggboxen.addCommand(hejkommandot);
        loggboxen.addCommand(hoppkommandot);

        loggboxen.setCommandListener(this);

        logglistan.addCommand(avslutakommandot);
        logglistan.addCommand(bytkommandot);
        logglistan.addCommand(rensakommandot);
        logglistan.addCommand(hejkommandot);
        logglistan.addCommand(hoppkommandot);

        logglistan.setCommandListener(this);
    } // Logger

    public void startApp() {
        displayen = Display.getDisplay(this);
        displayen.setCurrent(loggboxen);
    }

    public void commandAction(Command kommandot, Displayable s) {
        if (kommandot == avslutakommandot) {
            destroyApp(false);
            notifyDestroyed();
        }
        else if (kommandot == bytkommandot) {
            if (displayen.getCurrent() == loggboxen)
                displayen.setCurrent(logglistan);
            else if (displayen.getCurrent() == logglistan)
                displayen.setCurrent(loggboxen);
        }
        else if (kommandot == rensakommandot) {
            loggboxen.setString("");
            logglistan.deleteAll();
        }
        else if (kommandot == hejkommandot) {
            loggboxen.insert("Hej!\n", loggboxen.size());
            logglistan.append("Hej!", null);
        }
        else if (kommandot == hoppkommandot) {
            loggboxen.insert("Hopp!\n", loggboxen.size());
            logglistan.append("Hopp!", null);
        }
        else {
            // Förhoppningsvis omöjligt
        }
    } // commandAction

    public void destroyApp(boolean unconditional) { }

    public void pauseApp() { }
} // class Logger

Uppgift 2 (10 p)

a) (8p)

import javax.microedition.midlet.*;
import javax.microedition.lcdui.*;

public class BadCalculator extends MIDlet implements CommandListener {
    private Display displayen;
    private Form formuläret;
    StringItem etiketten;
    Gauge mätare1, mätare2;
    private Command exitkommandot, plus, minus, gånger, delat;

    public BadCalculator() {
        formuläret = new Form("En riktigt dålig miniräknare");

        mätare1 = new Gauge("Ställ in ena talet:", true, 10, 0);
        formuläret.append(mätare1);
        mätare2 = new Gauge("Ställ in andra talet", true, 10, 0);
        formuläret.append(mätare2);
        etiketten = new StringItem("Resultat:", "");
        formuläret.append(etiketten);

        exitkommandot = new Command("Avsluta", Command.EXIT, 0);
        plus = new Command("+", Command.SCREEN, 1);
        minus = new Command("-", Command.SCREEN, 1);
        gånger = new Command("*", Command.SCREEN, 1);
        delat = new Command("/", Command.SCREEN, 1);

        formuläret.addCommand(plus);
        formuläret.addCommand(minus);
        formuläret.addCommand(gånger);
        formuläret.addCommand(delat);
        formuläret.addCommand(exitkommandot);

        formuläret.setCommandListener(this);
    } // BadCalculator

    public void startApp() {
        displayen = Display.getDisplay(this);
        displayen.setCurrent(formuläret);
    }

    public void commandAction(Command kommandot, Displayable s) {
        if (kommandot == exitkommandot) {
            destroyApp(false);
            notifyDestroyed();
        }
        else if (kommandot == plus) {
            etiketten.setText("" + (mätare1.getValue() + mätare2.getValue()));
        }
        else if (kommandot == minus) {
            etiketten.setText("" + (mätare1.getValue() - mätare2.getValue()));
        }
        else if (kommandot == gånger) {
            etiketten.setText("" + (mätare1.getValue() * mätare2.getValue()));
        }
        else if (kommandot == delat) {
            etiketten.setText("" + (mätare1.getValue() / mätare2.getValue()));
        }
        else {
            // Förhoppningsvis omöjligt
        }
    } // commandAction

    public void destroyApp(boolean unconditional) { }

    public void pauseApp() { }
} // class BadCalculator

Kommentar: På bilderna i tentan råkade "+" förekomma två gånger. Det ena av dem ska vara "*".

b) (1p)

Användargränssnittet kan skilja ganska mycket mellan olika J2ME-implementationer, till exempel mellan en viss riktig mobiltelefon och den emulator som följer med WTK. Utseendet på gränssnittskomponenteran och sättet att ge kommandon kan vara helt olika. I det här fallet var det antagligen så att på kompisens mobiltelefon såg Gauge-mätarna annorlunda ut, kanske som på den här bilden från en W800i:

FormTest på en riktig Sony-Ericsson w800i

Uppgift 3 (9 p)

a) (4p)

Enkel lösning:

private void skapa() throws RecordStoreException {
    RecordStore postlagret =
        RecordStore.openRecordStore("heltalslagret", true);
    for (int i = 0; i < 100; ++i) {
        String strängen = "" + i;
        byte[] data = strängen.getBytes();
        postlagret.addRecord(data, 0, data.length);
    }
    postlagret.closeRecordStore();
}

Den som läst kapitel 17 i boken vet att man alltid ska städa upp efter sig. Det kan ju uppstå ett fel i metoden, som då kastar en RecordStoreException och inte stänger postlagret ordentligt. Så här borde metoden egentligen se ut:

private void skapa() throws RecordStoreException {
    RecordStore postlagret = null;
    try {
        postlagret = RecordStore.openRecordStore("heltalslagret", true);
        for (int i = 0; i < 100; ++i) {
            String strängen = "" + i;
            byte[] data = strängen.getBytes();
            postlagret.addRecord(data, 0, data.length);
        }
    }
    finally {
        if (postlagret !=null)
            postlagret.closeRecordStore();
    }
}

b) (4p)

Enkel lösning:

private int summera() throws RecordStoreException {
    int summan = 0;
    RecordStore postlagret =
        RecordStore.openRecordStore("heltalslagret", false);
    RecordEnumeration re =
        postlagret.enumerateRecords(null, null, false);

    while (re.hasNextElement()) {
        byte[] data = re.nextRecord();
        String strängen = new String(data);
        int talet = Integer.parseInt(strängen);
        summan += talet;
    }

    re.destroy();
    postlagret.closeRecordStore();

    return summan;
}

Precis som i a-uppgiften ska det egentligen finnas en finally-gren:

private int summera() throws RecordStoreException {
    int summan;
    RecordStore postlagret = null;
    RecordEnumeration re = null;
    try {
        summan = 0;
        postlagret = RecordStore.openRecordStore("heltalslagret", false);
        re = postlagret.enumerateRecords(null, null, false);

        while (re.hasNextElement()) {
            byte[] data = re.nextRecord();
            String strängen = new String(data);
            int talet = Integer.parseInt(strängen);
            summan += talet;
        }

        return summan;
    }
    finally {
        if (re != null)
            re.destroy();
        if (postlagret != null)
            postlagret.closeRecordStore();
    }
}

c) (1p)

private int säkersumma() {
    try {
        return summera();
    }
    catch (Exception e) {
        return 0;
    }
}

Uppgift 4 (4 p)

a) (2p)

Fördelar:

Nackdelar:

b) (2p)

Fördelar:

Nackdelar:

Kommentar: Deluppgift b kunde tydligen tolkas på olika sätt. Rimliga svar, med minst en fördel och en nackdel, ger poäng.

Uppgift 5 (6 p)

a) (1p)

En konfiguartion "specificerar en JVM och en uppsättning grundläggande API:er för en viss familj av enheter", som kursboken uttrycker det. En konfiguartion är alltså en grundläggande standard för vad en viss klass av små Java-kapabla datorer ska klara av.

b) (1p)

Grundläggande egenskaper, som hur mycket minne som datorn minst ska innehålla. I och med att JVM:en ingår i konfigurationen, ingår det också i konfigurationen vilka grundläggande Java-klasser (String och Thread, till exempel) som ska finnas tillgängliga.

c) (1p)

Boken säger att "en profil är ett lager ovanpå en konfiguration, vilken adderar de API:er och specifikationer som behövs för att utveckla tillämpningsprogram för en viss familj av enheter". Det kan man undra vad det egentligen betyder, men det handlar om att man har fler krav, och fler Java-klasser, förutom de som ingår i konfigurationen. Om en konfiguration (i det här fallet konfigurationen CLDC) kräver att datorn ska ha minst 32 kilobyte minne (som kan användas av Java-maskinen), och inte säger något om någon bildskärm, så kan en profil (i det här fallet MIDP) kräva 128 kilobyte minne för objektlagring, plus en bildskärm på minst 96x54 bildpunkter.

d) (1p)

Ytterligare Java-API:er och Java-klasser, samt andra krav, till exempel på minnesmängd och skärmstorlek.

e) (1p)

f) (1p)

Alla mobila enheter har inte alla finesser, till exempel Blåtand, och då ska man inte heller ha med dem i deras Java-miljöer. När det behövs nya funktioner går det också snabbare att utveckla, specificera och godkänna ett litet "paket" än att ändra en hel konfiguration eller profil.

Uppgift 6 (3 p)

a) (1p)

Innan ett Java-program körs, kontrolleras den kompilerade byte-koden så att den uppfyller reglerne för hur byte-kod ska se ut, och inte gör något skadligt. I J2ME är den kontrollen uppdelad i två delar: en för-kontroll (pre-verifiering) som görs på en vanlig dator i samband med att MIDleten paketeras, och en efter-kontroll som görs på mobiltelefonen, precis innan MIDleten ska köras.

b) (1p)

Jämfört med en vanlig dator har de flesta mobiltelefoner långsam processor och litet minne, så det skulle gå långsamt (eller kanske inte alls) att göra hela verifieringen på telefonen.

c) (1p)

När man laddar ner en MIDlet till sin telefon, måste man lita på att för-verifieringen verkligen gjorts, och gjorts rätt. På telefonen sker ju bara en mindre kontroll. Om man får en MIDlet från okänd avsändare, skulle den alltså kunna ställa till skada av olika slag genom att förbigå Javas vanliga säkerhetskontroller.

Uppgift 7 (3 p)

a) (1p)

Det finns ingen begränsning, förutom vad som får plats i minnet på telefonen. (Tror jag. Jag har inte hittat något om någon gräns, och jag har provkört med över tusen i ett formulär.)

b) (2p)

Mobiltelefoner har normalt en mycket liten bildskärm, och begränsade möjligheter att navigera på den (kanske bara med pilknappar, eller en liten och obekväm styrspak). Därför kan man förstås inte alls bygga samma typ av stora användargränssnitt som på vanliga skrivbordsdatorer, utan de måste vara mycket mindre och enklare.

Helst bör man undvika att ha fler komponenter på en skärmbild än vad som får plats utan att behöva scrolla. En lista med många val kanske behöver vara scrollbar (haha!), men hela objekt, till exempel en Gauge-mätare, bör inte hamna helt utanför skärmen.


Thomas Padron-McCarthy (Thomas.Padron-McCarthy@tech.oru.se) 16 juni 2006