Android-skolen del 3: Snak satellitter og Google-kort med den grønne robot

Så er det atter tid til at knække leddene i programmørfingrene og skrive en portion Android-kode. Tredje lektion splejser Google Maps og GPS i en kort-applikation.

Velkommen til tredje undervisningstime i Version2's Android-skole, hvor vi får Google Maps og telefonens indbyggede GPS til at spille sammen i en simpel kort-applikation.

I denne tredje lektion er det slut med at arbejde indenfor de trygge rammer af Android-emulatoren ? nu er det den virkelige verdens Android-telefon, der skal frem på bordet og sluttes til udvikler-pc'en.

Denne uges gæsteunderviser er softwareudvikler Mikkel Bruun, der blandt andet står bag applikationen iTog til både iPhone og Android.

Mikkel Bruun bruger GPS'en til at bestemme togpassagerens aktuelle position for at kunne levere data om næste station på vejen og afgangene derfra.

Hvis ikke du kan komme i nærheden af en Android-telefon med version 1.6 af styresystemet, kan du dog sagtens læse videre alligevel.

Det kan nemlig lade sig gøre at skrive en applikation til Android-emulatoren, der simulerer satellit-kommunikation med 'falske' GPS-data. Du vil dog være lidt mere på egen hånd i den del af lektionen, der handler om at hive data ind fra GPS'en, hvis ikke du ejer en telefon.

Vi lægger ud med at skrive en simpel applikation, der viser Eiffeltårnet i Paris på et Google-kort ud fra et sæt statiske koordinater.

1) De indledende knæbøjninger ? Google API'er og hul igennem til telefonen.

Start Eclipse.

Allerførst skal du sikre dig, at du har installeret de såkaldte Google API'er. Åbn AVD-manageren i Eclipse - det lille, sorte mobiltelefon-ikon i menu-linjen ? og tjek punktet Available Packages.

Fold træet ud i feltet til højre. Hvis du har mulighed for at installere Google API'erne, skal du gøre det nu.

Når installationen er færdig, skal vi sikre os, at der er hul igennem til vores telefon, så vi kan bruge den i stedet for Android-emulatoren.

På din Android-telefon vælges Indstillinger -> Programmer -> Udvikling , hvor USB-fejlretning slås til med et flueben. Når du er i gang, kan du lige så godt sikre, at GPS er slået til under Indstillinger -> Lokalitet -> Aktiver GPS-satellitter.

På udviklingsmaskinen skal vi sørge for, at telefonen genkendes som et 'running Android device', når vi skal lægge applikationen over på telefonen.

På Windows-platformen gøres det ved at installere en USB-driver fra http://developer.android.com/sdk/win-usb.html.

Er man Mac-ejer, skulle man kunne læne sig tilbage uden at pille ved indstillingerne.

Linux-folket skal sætte en lille USB-regel op og ændre lidt rettigheder, hvilket også står beskrevet på http://developer.android.com/sdk/win-usb.html.

I Eclipse angiver vi nu under Run -> Run configurations..., at vi gerne vil kunne vælge vores target manuelt, når vi kører Android-applikationen. Det giver os mulighed for at vælge vores Android-telefon i stedet for emulatoren.

Vælg Manual under fanebladet Target og gem ændringen.

2) Bliv dus med Google Maps

Nu er vi klar til at skrive første udgave af vores kortapplikation, der viser Eiffeltårnet i Paris ud fra statiske koordinater. Det udvider vi senere med rigtige koordinater fra telefonens GPS.

Opret et Android-projekt i Eclipse ganske som du plejer. Vælg Google API's version 1.6 og navngiv som vist på billedet, eller efter eget valg.

Vi begynder med at sætte klassen AndroidSkolenDel3 til at arve fra MapActivity i stedet for Activity. Det gør samtidig, at vi skal implementere metoden isRouteDisplayed(), hvilket Eclipse kan gøre for os. Lad metoden stå tom.

Koden ser nu ud som følger:

01 /** Called when the activity is first created. */ 02     @Override 03     **public ****void **onCreate(Bundle savedInstanceState) { 04         **super**.onCreate(savedInstanceState); 05         setContentView(R.layout.main); 06     } 07  08   @Override 09   **protected ****boolean **isRouteDisplayed() { 10     // TODO Auto-generated method stub 11     **return false**; 12   }

Vi har nu selve strukturen for vores første, simple applikation. Vi lader filen AndroidSkolenDel3.java stå et øjeblik og retter opmærksomheden mod main.xml, der ligger i projektundermappen res/layout/.

Ryd det autogenererede LinearLayout i main.xml og erstat det med:

01 "http://schemas.android.com/apk/res/android" 02     android:orientation="vertical" 03     android:layout_width="fill_parent" 04     android:layout_height="fill_parent" 05     > 06      07  08     android:id="@+id/mapview1" 09     android:layout_width="fill_parent"  10     android:layout_height="fill_parent"  11     android:enabled="true"  12     android:clickable="true"  13     android:apiKey="api_key_here" />  14 

Koden fortæller Android, at vi gerne vil oprette et MapView og lægge et Google Maps-kort ind i det.

Læg mærke til linje 13

android:apiKey="api_key_here"

, hvor vi skal erstatte værdien api_key_here med en rigtig Google Maps-nøgle, der angiver, at du er godkendt til at bruge Google Maps-tjenesten.

Detaljerne findes her http://code.google.com/android/add-ons/google-apis/mapkey.html, men pointen er, at du skal have genereret en nøgle ud fra et MD5 fingerprint.

MD5 fingerprint'et hentes fra en kommandoprompt under Windows eller terminal under Mac OS X eller Linux ved at udføre kommandoen:

keytool -list -alias androiddebugkey -storepass android -keypass android -key store debug.keystore

fra biblioteket .android (bemærk punktummet), der for alle platformes vedkommende ligger i roden af bruger-biblioteket (f.eks. /home/'brugernavn'/.android under Linux).

Herefter kommer vores MD5 fingerprint til syne, som du skal kopiere og indsætte i feltet på siden http://code.google.com/android/add-ons/google-apis/maps-api-signup.html

Nu har du din Google Maps-nøgle, som skal indsættes i main.xml som vist tidligere.

Gå tilbage til AndroidSkolenDel3.java. Her skal vi nu hente et kort frem og bede om at få det vist i vores MapView.

Det hele sker i metoden onCreate:

01     **private **MapView mapView; 02     **private **MapController mc; 03    04     **public ****void **onCreate(Bundle savedInstanceState) { 05         **super**.onCreate(savedInstanceState); 06         setContentView(R.layout.main); 07          08         mapView = (MapView) findViewById(R.id.mapview1); 09         mc = mapView.getController(); 10               11         GeoPoint p = **new **GeoPoint( 12             (**int**) (48.8583 * 1E6),  13             (**int**) (2.2945 * 1E6)); 14  15         mc.animateTo(p); 16         mc.setZoom(17);  17         mapView.setSatellite(**true**); 18         mapView.invalidate(); 19     }

Først sætter vi et MapView, mapView, op, som bruges til visning af selve kortet. Herefter henter vi en map controller, mc, som bruges til at håndtere panorering og zoom på selve kortet. (linje 8-9)

Vi opretter efterfølgende et såkaldt GeoPoint ud fra de decimale koordinater på Eiffeltårnet i Paris og beder derefter map controller'en om at vise lokationen og sætte et fornuftigt zoom-niveau som standard. (linje 11-16)

Til sidst beder vi vores MapView om at sætte satellitvisning til, så vi får lagt satellit-billeder på kortet, og sætter kortet til at tegne med invalidate()-metoden. (linje 17-18)

Inden vi kan lægge programmet op på telefonen, skal vi som et sidste punkt bede vores AndroidManifest.xml om at bruge Google Maps og samtidig give os lov til at benytte dataforbindelsen på telefonen til at hente kort ned fra nettet.

Det gøres ved at tilføje:

"com.google.android.maps" />

som et tag under application-tag'et. Derefter tilføjer vi

  "android.permission.INTERNET">

som et tag under manifest-tag'et. Dermed kommer manifest-filen til at tage sig ud som følger:

01 <?xml version="1.0" encoding="utf-8"?> 02 "http://schemas.android.com/apk/res/android" 03       **package**="dk.version2.as27" 04       android:versionCode="1" 05       android:versionName="1.0"> 06        07     "android.permission.INTERNET"> 08        09     "@drawable/icon" android:label="@string/app_name"> 10       "com.google.android.maps" /> 11         ".MapsTest" 12                   android:label="@string/app_name"> 13              14                 "android.intent.action.MAIN" /> 15                 "android.intent.category.LAUNCHER" /> 16              17          18  19      20     "4" /> 21  22 

Oversæt og kør projektet med ctrl-b og ctrl-F11. Husk at vælge telefonen som target.

Nu skulle den franske hovedstads vartegn gerne vises i applikationen på telefonen.

Illustration: Picasa 3.0

3) Få Google Maps og GPS til at gå op i en højere enhed

Nu skal du udvide applikationen til at vise et Google Maps ud fra din nuværende lokalitet i stedet for et vilkårligt, hardkodet sted i verden.

Kort sagt skal du nu erstatte det statiske Geopoint i koden med et nyt af slagsen, der henter sine koordinater fra telefonens GPS.

Vi begynder med at skrælle et hovedparten af koden ud af vores onCreate()-metode, så den kommer til at tage sig sådan ud:

1   **public ****void **onCreate(Bundle savedInstanceState) { 2     **super**.onCreate(savedInstanceState); 3     setContentView(R.layout.main); 4  5     mapView = (MapView) findViewById(R.id.mapview1); 6     mapView.setBuiltInZoomControls(**true**); 7     mc = mapView.getController(); 8   }

Du har nu angivet, at onCreate() skal oprette et mapview og en map controller ? ganske som i det foregående eksempel. Samtidig har vi med linje 6 gjort det muligt at zoome ind og ud på kortet.

Vi lader herefter onCreate() stå, som den er, og placerer den GPS-relaterede del af koden i en ny metode, onResume(), som vi placerer umiddelbart under onCreate()-metoden.

»Når applikationen starter, kaldes onCreate, og bagefter onResume. Hvis app'en sendes i baggrunden ved for eksempel skift til homescreen eller en anden app, lægges den oprindelige app i baggrunden og "sover". Hvis vi så starter den igen, kaldes onResume, og ikke onCreate, så vi bruger onResume for at undgå redundans i koden,« forklarer Mikkel Bruun.

Vores onResume()-metode ser ud som følger:

01   **protected ****void **onResume() { 02     List providers; 03     **super**.onResume(); 04  05     locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); 06     providers = locationManager.getProviders(**true**); 07  08     **if **(!providers.isEmpty()) { 09       location = locationManager.getLastKnownLocation((String) providers 10           .get(0)); 11       locationManager.requestLocationUpdates((String) providers.get(0), 12           15000, 1, **this**); 13     } 14     GeoPoint p = **new **GeoPoint((**int**) (location.getLatitude() * 1E6), 15         (**int**) (location.getLongitude() * 1E6)); 16     mc.animateTo(p); 17     mc.setZoom(17); 18     mapView.setSatellite(**true**); 19     mapView.setStreetView(**true**); 20     mapView.invalidate(); 21   }

Inden vi går videre, sætter vi vores klasse AndroidSkolenDel3 til at implementere interfacet LocationListener, hvilket betyder, at vi nu både extend'er MapActivity og implementerer LocationListener.

Det betyder samtidig, at vi nu skal tilføje en række metoder, som LocationListener kræver implementeret. Få Eclipse til selv at indsætte metoderne onProviderDisabled, onProviderEnabled, onStatusChanged og onLocationChanged.

Vi tager nu onResume()-koden et par skridt af gangen.

I linje 5 i onResume() oprettes et LocationManager-objekt. Klassen LocationManager bruges generelt til at tilgå telefonens lokationstjenester, GPS og mobilnet.

I linje 6 henter vi en liste af satellitter, som vi tjekker for indhold i linje 8. Hvis ikke listen er tom, henter vi den senest kendte lokation over i et Location-objekt, linje 9. (Tip til emulator-folket: Android indeholder en 'dummy' GPS-provider. Du kan hente et sæt 'mock' GPS-koordinater med Location location = locationManager.getCurrentLocation("gps");)

Samtidig beder vi vores LocationManager om at hente fremtidige opdateringer fra GPS'en, linje 10.

(NB: Ordentlig kodestil ville være at tilføje en 'else' til vores 'if', som viser en fejlmeddelelse på skærmen, hvis listen er tom, og der dermed ikke er nogle tilgængelige satellitter. Det er udeladt for at holde koden simpel.)

I linje 14-15 begynder tingene at minde om noget, vi kender fra tidligere. Her oprettes et GeoPoint, men denne gang ud fra den aktuelle GPS-position, frem for statiske værdier.

I linje 16-20 er fremgangsmåden ganske som før, hvor vi får tegnet kortet ud fra lokationen. Her vælger vi dog også at slå Streetview til på kortet.

Nu vender vi blikket mod den autogenererede skabelon for metoden onLocationChanged, som LocationListener-interfacet krævede implementeret.

Det er den eneste af de fire metode-skabeloner, som vi rent faktisk gør noget ved.

onLocationChanged sikrer, at vi får opdateret og tegnet kortet ud fra den nye GPS-position, hvis vi flytter os med telefonen.

Indholdet i onLocationChanged er dybest set det samme som linje 14-20 i onResume()-metoden.

1 **public ****void **onLocationChanged(Location loc) { 2     GeoPoint p = **new **GeoPoint((**int**) (loc.getLatitude() * 1E6), (**int**) (loc 3         .getLongitude() * 1E6)); 4     mc.animateTo(p); 5     mc.setZoom(17); 6     mapView.setSatellite(**true**); 7     mapView.setStreetView(**true**); 8     mapView.invalidate(); 9   }

Nu mangler vi blot at give vores applikation lov til at bruge GPS'en. Det gør vi ved at tilføje følgende tags i AndroidManifest.xml.

  "android.permission.ACCESS_FINE_LOCATION">                                  "android.permission.ACCESS_COARSE_LOCATION">                               

De to tags sikrer, at vi kan falde tilbage på det grovkornede niveau (COARSE) med triangulering, hvis ikke der er GPS-signal til telefonen (FINE).

Det adskiller sig fra iPhone-platformen, forklarer Mikkel Bruun.

»På iPhone beder du bare om en location, og så kan du spørge på, hvor præcis den er. På Android er det lidt mere kompliceret, da man ofte bør tage stilling til, hvor præcist en lokation skal bestemmes. Om det er via reel GPS eller via Wifi eller sendemaster,« siger Mikkel Bruun.

Nu er vi klar til at afprøve vores færdige applikation. Lad os tage et kig på hele kildekoden:

**public class **AndroidSkolenDel3 **extends **MapActivity                   **implements **LocationListener {   **private **Location location;   **private **MapView mapView;   **private **MapController mc;   **private **LocationManager locationManager;   **public ****void **onCreate(Bundle savedInstanceState) {     **super**.onCreate(savedInstanceState);     setContentView(R.layout.main);     mapView = (MapView) findViewById(R.id.mapview1);     mapView.setBuiltInZoomControls(**true**);     mc = mapView.getController();   }   **protected ****void **onResume() {     List providers;     **super**.onResume();     locationManager = (LocationManager) getSystemService(Context.LOCATION_SERVICE);     providers = locationManager.getProviders(**true**);     **if **(!providers.isEmpty()) {       location = locationManager.getLastKnownLocation((String) providers           .get(0));       locationManager.requestLocationUpdates((String) providers.get(0),           15000, 1, **this**);     }     GeoPoint p = **new **GeoPoint((**int**) (location.getLatitude() * 1E6),         (**int**) (location.getLongitude() * 1E6));     mc.animateTo(p);     mc.setZoom(17);     mapView.setSatellite(**true**);     mapView.setStreetView(**true**);     mapView.invalidate();   }   **public ****void **onLocationChanged(Location loc) {     GeoPoint p = **new **GeoPoint((**int**) (loc.getLatitude() * 1E6), (**int**) (loc         .getLongitude() * 1E6));     mc.animateTo(p);     mc.setZoom(17);     mapView.setSatellite(**true**);     mapView.setStreetView(**true**);     mapView.invalidate();   }   @Override   **public ****void **onProviderDisabled(String provider) {     // TODO   }   @Override   **public ****void **onProviderEnabled(String provider) {     // TODO   }   @Override   **public ****void **onStatusChanged(String provider, **int **status, Bundle extras) {     // TODO   }   @Override   **protected ****boolean **isRouteDisplayed() {     // TODO     **return false**;   } }

Der er altså tre primære metoder i selve hovedklassen:

  • onCreate(): står for at oprette vores mapview, når applikationen startes.

  • onResume(): kaldes, når brugeren interagerer med applikationen på skærmen, eller når applikationen kaldes frem efter at have været lagt midlertidigt i baggrunden. Står for at hente GPS-koordinater og tegne kort.

  • onLocationChanged(): kaldes, når GPS'en har registreret en ændring i vores position.

Oversæt og kør applikationen med ctrl+b og ctrl+F11.

Herefter skulle du gerne se et kort over det sted, hvor du pt. befinder dig. Du kan kalde zoom-knapperne frem med et tryk på skærmen.

Illustration: Picasa 3.0

Det var alt i tredje undervisningtime i Version2's Android-skole.

»Det er altid interessant at se sin egen position i forhold til andre lokationer. Så hvis man vil eksperimentere lidt med ruter og afstande, giver location.distanceTo(otherLocation) afstanden mellem to lokationer i meter,« siger Mikkel Bruun om en af mulighederne for at bygge videre på GPS/Google Maps-applikationen.

Næste gang ser vi nærmere på trådning af den grafiske brugergrænseflade i en Android-applikation.

Litteratur:
[1] http://developer.android.com

Skab Danmarks bedste Android-applikation og vind en eksklusiv tur med alt betalt til verdens største mobilmesse i Barcelona. Deadline er 11. januar 2010. Læs mere på http://www.version2.dk/android

Tips og korrekturforslag til denne historie sendes til tip@version2.dk
Kommentarer (22)
sortSortér kommentarer
  • Ældste først
  • Nyeste først
  • Bedste først
Thorvald Aagaard

I den første onCreate bruger du mv og mc, men disse er ikke erklæret. De skal være

private MapView mv;
private MapController mc;

senere i forløbet begynder du dog at bruge mapView i stedet for mv, så jeg vil foreslå at du bruger variabelnavnet mapView allerede i første eksempel

Thorvald

  • 0
  • 0
Brian Christensen

Fin artikkel, glæder mig til at komme igang... Ved du noget om hvorvidt det er muligt at sende værdierne ud af telefonen over usb?

Den ville jo være fin at sætte til en microprocesser el. lign, så har man lige pludselig en masse fine sensore :D

  • 0
  • 0
Benni Bennetsen

Halløj igen,

så fandt jeg endelig keytool, håber at kunne hjælpe andre nu med mine spørgsmål :)
Jeg fandt keytool i C:\Program Files (x86)\Java\jre6\bin>

Nu prøver jeg at gøre sådan her:
keytool -list -alias androiddebugkey \
-keystore C:\Users\Administrator.android.keystore \
-storepass android -keypass android

men får fejlen:
C:\Program Files (x86)\Java\jre6\bin>-storepass android -keypass android
'-storepass' blev ikke genkendt som en intern eller ekstern kommando,
et program eller en batchfil.

  • 0
  • 0
Benni Bennetsen

Halløj igen,

så fandt jeg endelig keytool, håber at kunne hjælpe andre nu med mine spørgsmål :)
Jeg fandt keytool i C:\Program Files (x86)\Java\jre6\bin>

Nu prøver jeg at gøre sådan her:
keytool -list -alias androiddebugkey \
-keystore C:\Users\Administrator.android.keystore \
-storepass android -keypass android

men får fejlen:
C:\Program Files (x86)\Java\jre6\bin>-storepass android -keypass android
'-storepass' blev ikke genkendt som en intern eller ekstern kommando,
et program eller en batchfil.

  • 0
  • 0
Mikkel Meister

Hej Benni -

Tak for dit indlæg. Jeg kan sagtens se, at der er mange ligheder mellem koden i min artikel og den, du henviser til.

Det skyldes nok, at der kun er så mange måder at løse opgaven på. I denne artikel har jeg modtaget stor hjælp til koden fra Mikkel Bruun ('gæsteunderviseren' i artiklen), der har haft afgørende indflydelse på, hvordan koden ser ud i ovenstående artikel.

Der er også - så vidt jeg lige kan se - en afgørende forskel på struktureringen af koden i de to artikler. I den, du henviser til, placeres selve location-koden i onCreate(), mens den i ovenstående fordeles på onResume() og onLocationChanged() (hvorfor det gøres, redegør Mikkel Bruun for i artiklen).

Sagt med andre ord er der altså ikke tale om, at den artikel, du henviser til, er den 'oprindelige artikel'. I så fald havde jeg citeret og linket tydeligt til den.

Men ja, du vil nok kunne finde flere eksempler på nettet, hvor der er et vist kodeoverlap til andre eksempler. Ét af målene med Android-platformen har netop været, at de lidt mere ikke-trivielle opgaver (som GPS-håndtering fx) skal kunne løses med relativt få linjer kode.

Til sidst: glædeligt, at der her næsten et år efter offentliggørelse stadig er læsere, der går eksemplet igennem og kommenterer artiklerne i Android-skolen.

Bliv endelig ved :-).

Mvh Mikkel, v2.dk

  • 0
  • 0
Benni Bennetsen

Det er i hvert fald skægt at lege med, og er rigtig fiks med jeres lille skole - super godt :=)

Måske I engang var friske på at udvide med lidt up2date info med 2.2, der er jo nok kommet nye muligheder :)

Når jeg sætter telefon til, så skal jeg vælge, hvad jeg ønsker at den skal, lade, være som drev osv.. går ud fra man her skal vælge som drev ? men kan ikke lige se hvordan jeg så kan køre den på telefon, dukker ikke op som devise når jeg kører programmet ?

  • 0
  • 0
Benni Bennetsen

Tja nu har jeg indtil videre så eksporteret den og så downloaded den for at teste.. Kan fint starte op, men den viser kun blankt kort ikke hvor jeg er - og den blinker heller ikke med ikon i øverste linie, der normal indikerer at den prøver at søge efter gps.. hvad kan jeg mon have glemt ?

  • 0
  • 0
Jacob Hassing

Hmmm strengt taget mangler det også i eksemplet her på siden. Det burde have været:
14 <action android:name="android.intent.action.MAIN" />
15 <category android:name="android.intent.category.LAUNCHER" />
16 </intent-filter>
17 </activity>
18 <uses-library android:name="com.google.android.maps" />
19 </application>

og fejlmeddelelsen i loggen var rettelig:
java.lang.ClassNotFoundException: net.learn2develop.GoogleMaps.MapsActivity in loader dalvik.system.PathClassLoader

  • 0
  • 0
Mikael D_

Er klar over, at denne artikel er gammel, men skriver i håb om, at andre stadig kigger med.
Første del af koden giver bare et tomt map. Efter hele øvelsen giver programmet Force Close. Har nu copy/pasted hele koden fra Version2 og indsat api key, men det ændrer ikke på noget. Andre Apps lavet i Eclipse fungerer udemærket.
Nogen idéer?

Mvh
Mikael

  • 0
  • 0
Leif Frederiksen

Jeg har implementeret den del der snakker med GPS'en men ikke maps delen. Jeg får Forced closed når jeg afvikler programmet. Jeg har sat de to permissions i manifestet: ACCESS_FINE_LOCATION og ACCESS_COARSE_LOCATION.

Nogle ideer til hvad jeg mangler?

Venligst
Leif

  • 0
  • 0
Log ind eller Opret konto for at kommentere