Android MapView a onTouch

Nemálo mě, a nejenom mě, štve absence něčeho, jako je onTouch() v mapách na Androidu, protože potřebuju vytvořit dynamické načítání overlayů. MapView sice má onTouchEvent(), ale že by bylo možné jej použít nějakým mnou zamýšleným způsobem, to zrovna ne. Zkoušel jsem pár způsobů, které tu nastíním a taky jsem zkusil jedno ne zrovna dvakrát moc … Pokračovat ve čtení „Android MapView a onTouch“

Nemálo mě, a nejenom mě, štve absence něčeho, jako je onTouch() v mapách na Androidu, protože potřebuju vytvořit dynamické načítání overlayů. MapView sice má onTouchEvent(), ale že by bylo možné jej použít nějakým mnou zamýšleným způsobem, to zrovna ne. Zkoušel jsem pár způsobů, které tu nastíním a taky jsem zkusil jedno ne zrovna dvakrát moc fér řešení. Ale zoufalí lidé se uchylují k zoufalým věcem, však to znáte… Jen to vezmu hodně hopem, protože mám moře jiných věcí na dělání, ale pár lidem jsem to slíbil a sliby se mají plnit a to nejen o Vánocích ;-).

Zatímco natahování je poměrně jasné (AsyncTask, který bude natahovat data ve čtverci, jehož směrem jsem se posunul – na základě kolize souřadnic tohoto čtverce a viewportu – aby to pořád netahalo jak blbé), taková trivialita jako je onTouch už tak jasná není.

Nejprve jsem se snažil přimět k rozumu metodu onTouchEvent() jak na MapView, tak i na Overlayi, bohužel bezúspěšně. Zajímalo by mě, jestli je tento event pozůstatek něčeho zaniklého, nebo se na něj jenom nějak zapomnělo. Tento způsob by měl fungovat nějak takto, což vypadá poměrně prakticky a použitelně – ve switchi checkovat eventy:

[sourcecode language=“java“]public boolean onTouchEvent(MotionEvent ev) {
int action = ev.getAction();
switch (action) {
. . .
}
return super.onTouchEvent(ev);
}[/sourcecode]

Bohužel – z nějakého důvodu tato metoda vůbec nebyla volána. Po nějakém googlení jsem narazil na blog Juriho Strumpflohnera, který řešil stejný problém a vyšpekuloval ho následovně: na mapView navěsit vlastní OnTouchListener(), čili přidat jednoduchou anonymní třídu:

[sourcecode language=“java“]mapView.setOnTouchListener(new OnTouchListener() {
public boolean onTouch(View v, MotionEvent ev) {
return false;
}
});[/sourcecode]

Pokud teda dojde k eventu Touch, jsou procházeny všechny views a případné implementace listenerů v nich. V listeneru implementovaná metoda onTouch je volána nejméně jednou v závislosti na tom, zda vrátí true či false. Pokud vrací false, provede se pouze poprvé a systém je tak chytrý, že už jde příště rovnou na jistotu (aka přímo po metodě, která vrátila true), což je zároveň i kámen úrazu: je nutné zajistit, aby byla volána i metoda „níže“, protože se při true mapa nepohybuje (ale náš onTouch je volaný vždycky). Je teda nutné předat „nižšímu view“ tento event. Nejprve jsem volal dispatchTouchEvent() na view, který přišel do onTouch, ale to je blbost, protože tento view je vlastně onen MapEvent, nad kterým byl navěšen tento listener, takže došlo k zacyklení a StackOverflowError. Takže jsem třídu odanonymizoval a předávám si do ní Context, což je aktivita, uvnitř které je mapView:

[sourcecode language=“java“]public class MyTouchListener implements View.OnTouchListener {
private Activity activity;

public MyTouchListener(Activity activity) {
super();
this.activity = activity;
}

public boolean onTouch(View v, MotionEvent ev) {
activity.dispatchTouchEvent(ev);
return true;
}
}[/sourcecode]

Taky to ovšem nejede, ale oproti dispatchování do view to vydrží o chvíli déle (a pak to stejně přeteče zásobník a zdechne).

Jak už to bývá, funkční řešení bývají ta nejjednodušší, takže dneska tenhle blogpost konečně dorazím!

Ono totiž stačí rozšířit map view a přepsat v něm onTouchEvent, nebo onInterceptTouchEvent (jak je popsáno tady). V čem je zakopán pudl? V ničem, opravdu to funguje. Předchozí Class Cast Exception se dá zbavit velice jednoduše – definovat správný typ v layoutu! Pojďme tedy na to, nejprve vlastní mapView:

[sourcecode language=“java“]package cz.shmoula.pat;

import android.content.Context;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;

import com.google.android.maps.MapView;

public class AreaMapView extends MapView {
private Context mContext;

public AreaMapView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
mContext = context;
}

public boolean onTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_MOVE) {
Log.i("!!!!!", "Pohyb!");
}
return super.onTouchEvent(ev);
}
}[/sourcecode]

Teď následuje onen výše zmíněný kámen úrazu, který dříve hulákal Class Cast výjimkou a je to docela logické, ale nevěděl jsem, že je možné vkládat do layoutů vlastní komponenty. Teď už to vím, tak je možné psát něco zhruba takového:

[sourcecode language=“xml“]<?xml version="1.0" encoding="utf-8"?>

<cz.shmoula.pat.AreaMapView
xmlns:android="http://schemas.android.com/apk/res/android"

android:id="@+id/areaMapView"

android:layout_width="fill_parent"
android:layout_height="fill_parent"

android:clickable="true"
android:apiKey="api_klíč"
/>
[/sourcecode]

No a v aktivitě se tato komponenta dohledá každému známým způsobem:

[sourcecode language=“java“]AreaMapView mapView = (AreaMapView) findViewById(R.id.areaMapView);[/sourcecode]

Toť vše! Opravdu, v jednoduchosti je síla. Takže teď přidat nějaké spočítání delta a pokud bylo pohnuto viewportem více, než je výše uvedený čtverec v onu stranu, tak načíst další. Na závěr se ještě musím přiznat, že toto řešení jsem nevyplodil sám, ale sprostě se nechal inspirovat OpenCachingem od Garmina, který mě nakopl na správnou cestu směrem k výsledku. Při pídění se po řešení jsem taky narazil na projekt mapview-overlay-manager, ve kterém se nachází Lazy loading a který si nechávám k dalšímu prostudování a hádám, že nebudu sám.

Dekompilace androidích aplikací

Někdo se rád pídí po tom, jak věci fungují, někdo se rád občas nechává inspirovat cizími kusy kódu a někdo prachsprostě krade (možná by se to dalo nazvat nějakou IT kleptomanií). Všem těmto skupinám by ovšem mohl přijít vhod tento blogpost, protože v několika krocích popisuje operaci jednoduchou (tu jednoduchost myslím vážně, zabývat se dedexerem, … Pokračovat ve čtení „Dekompilace androidích aplikací“

Někdo se rád pídí po tom, jak věci fungují, někdo se rád občas nechává inspirovat cizími kusy kódu a někdo prachsprostě krade (možná by se to dalo nazvat nějakou IT kleptomanií). Všem těmto skupinám by ovšem mohl přijít vhod tento blogpost, protože v několika krocích popisuje operaci jednoduchou (tu jednoduchost myslím vážně, zabývat se dedexerem, aka android disassemblerem opravdu nehodlám, z toho už jsem vyrostl a zpohodlněl) – a sice rozbalování a dekompilaci androidí aplikace.

Jak je tvořena androidí aplikace asi každý vývojář ví, takže tuto část vypustíme a přejděme blíže k věci, což by mohl být aapt, neboli Android Asset Packing Tool, který je součástí nástrojů v SDK. Tento nástroj je jednak zodpovědný za parsování všemožných souborů (včetně manifestu) uvnitř projektu a následné přetváření takových properties do R.java souboru, ale především i za proces opačný, čili dolování informací z již hotových .apk souborů a to i podepsaných a zarovnaných. Nebudeme chodit kolem horké kaše a skočme rovnou do nějakého příkladu. Půjčil jsem si APKčko z mého rozdělaného projektu a provedl nad ním následující kouzlo:

[sourcecode type=“bash“]vbalak@vbalak-desktop:~/develop/workspace.sts/PlantATree/client/target$ aapt l -a pat-client.apk
res/layout/area_map.xml
res/layout/main.xml
res/layout/tree_details.xml
res/layout/tree_list.xml
res/layout/tree_list_item.xml
res/menu/map_menu.xml
AndroidManifest.xml
resources.arsc
res/drawable-hdpi/icon.png
res/drawable-ldpi/icon.png
res/drawable-mdpi/icon.png
classes.dex
org/codehaus/jackson/map/VERSION.txt
org/codehaus/jackson/impl/VERSION.txt
META-INF/MANIFEST.MF
META-INF/CERT.SF
META-INF/CERT.RSA
[/sourcecode]

Nejprve je vidět seznam souborů nacházejících se v archivu (.apk je obyčejný zip soubor, takže je možné jej jednoduše rozbalit, v tom není žádná věda). Další část (resource table) je zajímavější – jedná se o identifikátory zdrojů, neboli to, co je v souboru R.java – to jsou ty hexa čísla uvedená bezprostředně za spec resource a dále typ tohoto zdroje – layout, string, drawable, id…

[sourcecode type=“plain“]Resource table:
Package Groups (1)
Package Group 0 id=127 packageCount=1 name=cz.shmoula.pat
Package 0 id=127 name=cz.shmoula.pat typeCount=6
type 0 configCount=0 entryCount=0
type 1 configCount=3 entryCount=1
spec resource 0x7f020000 cz.shmoula.pat:drawable/icon: flags=0x00000100
spec resource 0x7f04000a cz.shmoula.pat:string/menu_toggle_view: flags=0x00000000
. . .[/sourcecode]

Nejzajímavější je ovšem část poslední, což je sám velký manifest. Kdo měl možnost nahlédnout do AndroidManifest.xml a teď kouká na tento výpis, všímá si velké podobnosti (pokud potlačí rozdílný způsob zobrazení – tohle není xml ;-)). Jsou zde krásně vidět využívaná oprávnění, je zde vidět, jakou program používá ikonu (to je ten hexa kód, který se dá dohledat v části se zdroji – Resources Table). Jsou zde vidět jednotlivé aktivity, parametry aktivit a definované intent-filtry a také využívané knihovny – prostě kompletní manifest, akorát jinak formátovaný. Pro naše další potřeby potřebujeme vstupní bod do aplikace, což bude LAUNCHER a ten je nastaven pro třídu MainActivity, která je v balíčku cz.shmoula.pat, bude se nám hodit za chvíli. Za zmínku ještě stojí anotherMapProcess v poslední aktivitě – dvě mapy se v jedné aplikaci nesnesou, tudíž je nutné je spouštět v rámci jiného procesu, tento parametr tohle zajišťuje (taky jsem nad tím tenkrát dlouho koumal).

[sourcecode type=“plain“]Android manifest:
N: android=http://schemas.android.com/apk/res/android
E: manifest (line=2)
A: android:versionCode(0x0101021b)=(type 0x10)0x1
A: android:versionName(0x0101021c)="0.1" (Raw: "0.1")
A: package="cz.shmoula.pat" (Raw: "cz.shmoula.pat")
E: uses-permission (line=5)
A: android:name(0x01010003)="android.permission.INTERNET" (Raw: "android.permission.INTERNET")
E: application (line=7)
A: android:label(0x01010001)=@0x7f040001
A: android:icon(0x01010002)=@0x7f020000
E: activity (line=8)
A: android:name(0x01010003)=".MainActivity" (Raw: ".MainActivity")
E: intent-filter (line=9)
E: action (line=10)
A: android:name(0x01010003)="android.intent.action.MAIN" (Raw: "android.intent.action.MAIN")
E: category (line=11)
A: android:name(0x01010003)="android.intent.category.LAUNCHER" (Raw: "android.intent.category.LAUNCHER")
E: activity (line=15)
A: android:name(0x01010003)=".LookAroundActivity" (Raw: ".LookAroundActivity")
. . .
E: activity (line=16)
A: android:theme(0x01010000)=@0x1030006
A: android:name(0x01010003)=".ShowAreaMapActivity" (Raw: ".ShowAreaMapActivity")
A: android:process(0x01010011)=":anotherMapProcess" (Raw: ":anotherMapProcess")
E: uses-library (line=22)
A: android:name(0x01010003)="com.google.android.maps" (Raw: "com.google.android.maps")
[/sourcecode]

V této fázi tedy máme hrubý přehled o struktuře aplikace, bylo by vhodné se dostat ke třídám. Ty jsou zabalené uvnitř souboru classes.dex, který se nacházi uvnitř APKčka, takže bych prosil rozzipovat tento archiv. Tento soubor je víceméně nějakým způsobem rozsypaný Java ARchive a pro jeho sesypání do použitelného formátu slouží utilitka dex2jar, která se spouští jednoduchým způsobem:

[sourcecode type=“bash“]vbalak@vbalak-desktop:~/develop/stuff/dex2jar-0.0.7.8-SNAPSHOT$ ./dex2jar.sh classes.dex
version:0.0.7.8-SNAPSHOT
3 [main] INFO pxb.android.dex2jar.v3.Main – dex2jar classes.dex -&gt; classes.dex.dex2jar.jar
Done.[/sourcecode]

Získali jsme tedy opravdový archiv .jar, který je opět možné rozzipovat a koukat na jednotlivé zkompilované třídy. Také je možné udělat víc – pomocí některého java dekompileru je možné se podívat dovnitř, já volil Java Decompiler. Pomocí něj je možné otevřít získaný .jar soubor a v levé části otevřít požadovanou třídu (výše jsme zjistili, že vstupním bodem je MainActivity) a užívat pocitu vítězství, nebo začít vykrádat se začít inspirovat cizím kódem. Toď vše, pro ilustraci ještě přikládám shot JavaDecompileru.

JavaDecompiler - screenshot
JavaDecompiler - screenshot