Prohlížeč PDF pro Google Glass

Pro prohlížení PDF dokumentů na platformě Android existuje nepřeberné množství aplikací. Ty jsou ale určeny pro smartphony či tablety, obecně pro masově rozšířená zařízení, která lze snadno ovládat dotykem prstu. Na Google Glass (GG) zatím zůstává oblast uživatelsky přívětivého zobrazování dokumentů značně nepokrytá. Zde jsou možnosti ovládání dotykem omezené, brýle samozřejmě lze ovládat dotykem na bočním panelu, ten ale zvládá rozpoznat pouze několik základních gest. Na následujících řádcích proto popíšu, jak zprovoznit prohlížeč .pdf souborů na GG a jak se vypořádat se specifickými vlastnostmi této platformy.

Začínáme

Samotný vývoj aplikace bude probíhat ve vývojovém prostředí Eclipse, upraveného pro potřeby vývoje pro Android. Předpokládám operační systém Windows, na Unixových systémech je řešení obdobné.
Celý balík lze stáhnout z webu Google Android SDK. Dále budeme potřebovat stáhnout a nainstalovat ještě Java Development Kit, Android NDK a Cygwin (poslední dvě nutné pouze pro kompilaci knihovny). Doporučuji neměnit defaultní složky pro instalaci, předejte se tak zbytečným problémům.


Po zapnutí Eclipse je třeba ještě stáhnout a nainstalovat vývojové balíčky pro systém, se kterým budeme pracovat. V našem případě to bude Glass Development Kit. V aplikaci na horní liště nalezneme ikonku SDK Manager, která spustí správce balíčků, pomocí kterého můžeme přidat Glass Development Kit podle verze Androidu v brýlích, pro které aplikaci píšeme. Viz následující obrázek.

Android SDK Manager v Eclipse

V neposlední řadě je vhodné mít k dispozici Google Glass, nebo alespoň smartphone či tablet s OS Android pro testovaná, protože vestavěný simulátor pracuje svižně pouze na opravdu výkonných strojích.

Výběr knihovny

Nejlepších výsledku při testování knihoven s licencí General Public License (GNU) dosáhla knihovna MuPDF. Testována byla především rychlost, přesnost zobrazení (technické výkresy, obrázky, vektorová grafika), paměťová náročnost a celkový uživatelský dojem, pominu-li samotnou funkčnost, čímž byl výběr pro tuto platformu značně zúžen.
Další testovanou knihovnou byla například APV, která ale měla problémy s rychlostí. Další knihovny poskytovaly buď méně věrné zobrazení, nebo vůbec nešly spustit. Navíc knihovna MuPDF zvládá zobrazit i obrázky či XML soubory, což může být výhodou.

Stažení a kompilace knihovny

Nejprve je potřeba stáhnou zdrojové soubory ze stránek MuPDF Downloads, konkrétně nejnovější verzi označenou *source.tar.gz. Archiv je třeba rozbalit, nejlépe do složky

../User/Android/mupdf

což je defaultní složka pro Android projekty v Eclipse. Výsledek má necelých 50MB, což není málo a po kompilaci ještě velikost naroste na více jak dvojnásobek, ale některé soubory po kompilaci nebudou potřeba. Nainstalovaná aplikace v zařízení má okolo 12MB, což odpovídá kvalitě a rozsahu funkcí.


Nejnáročnějším krokem je samotná kompilace knihovny. Návod lze nalézt také na webu projektu. Nejprve zapneme příkazový řádek a přepneme se do složky se zdrojovými soubory pro Android pomocí příkazu

cd Android/mupdf/platform/android

Pokud ještě složka neobsahuje soubor local.properties, zkopírujeme ho příkazem

cp local.properties.sample local.properties

Následně se přepneme zpět do základní složky a provedeme kompilaci příkazy

cd ../..

make generate

Zde přijde ke slovu správně nainstalovaný Cygwin. Pokud je kompilace úspěšná, je posledním krokem zpracování kompilace nástrojem Android NDK, který zajistí integraci právě zkompilovaných souborů do zdrojových souborů v Javě. Po přepnutí do příslušné složky opět příkazem

cd platform/android

spustíme NDK příkazem

ndk-build

Pokud by příkazový řádek vypsal chybu, že nemůže nalézt příkaz ndk-build, je pravděpodobně špatně nastavená proměnná $PATH. Problém lze obejít prostým přetažením myší souboru ndk-build.cmd ze složky, kde je nainstalováno NDK, do konzole příkazového řádku.
Tím by měla být kompilace úspěšně za námi.

Příprava na první spuštění

Po spuštění vývojového prostředí Eclipse nezbývá než načíst zdrojové soubory kliknutím na ikonu "New" vlevo nahoře a vybráním možnosti "Android project from existing code" a připojit brýle či jiné zařízení k počítači přes USB. Možná bude potřeba nainstalovat USB ovladače pro dané zařízení, které lze obvykle stáhnout ze stránek výrobce. Nyní již půjde aplikace zkompilovat a spustit na smartphonu či tabletu (zařízení musí mít v nastavení povolen USB debugging). Pro Google Glass budou třeba ještě další úpravy, viz dále.

Nutné modifikace pro spuštění na Google Glass

Pro běh aplikace na GG je potřeba provést několik modifikací, aby byla aplikace vůbec spustitelná. V první řadě musí být každá aplikace vybavená takzvaným voice triggerem. To je hlasový příkaz, díky kterému lze aplikaci na brýlích spustit hlasovým příkazem. Tento příkaz musí být na oficiálním seznamu, pokud není, lze o něj požádat. V případě, že bude aplikace použita jen pro osobní potřebu, lze toto obejít přidáním

<uses-permission android:name="com.google.android.glass.permission.DEVELOPMENT" />

do AndroidManifest.xml. Samotné definování hlasového příkazu zajistíme pomocí modifikace úseku kódu ve stejném souboru, který souvisí s aktivitou, kterou chceme hlasovým příkazem spouštět, tedy v našem případě

<activity
            android:name="com.artifex.mupdfdemo.ChoosePDFActivity"
            android:label="@string/app_name"
            android:theme="@android:style/Theme.Light" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
<action android:name="com.google.android.glass.action.VOICE_TRIGGER" /> // pro GG voice
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
            <meta-data  
       android:name="com.google.android.glass.VoiceTrigger"
        android:resource="@xml/voice_trigger_start" /> // pro GG voice
        </activity>

Samotný příkaz implementujeme ve dvou krocích. Za prvé vytvoříme soubor voice_trigger_start.xml v res/xml obsahující

<?xml version="1.0" encoding="utf-8"?>
<trigger keyword="@string/glass_voice_trigger">
    <constraints network="false" />
</trigger>

Za druhé přidáme samotný text hlasového příkazu do res/values/strings.xml, tedy například

<string name="glass_voice_trigger">Show library</string>

Na hlasový příkaz OK Glass, show library se nyní spustí naše aplikace. Více informací o hlasových příkazech naleznete zde.

Bezdotykové prohlížení souborů a další úpravy

Protože brýle nenabízejí příliš uživatelsky příjemné ovládání dotykem, můžeme implementovat alespoň částečné ovládání pomocí náklonu hlavy a další užitečné drobnosti.

Zoom a pohyb po stránce - simulátor dotyků

Po mnoha marných pokusech s překreslováním grafiky pomocí volání příslušných funkcí jádra, které obvykle splnily z doposud neznámých důvodů pouze část své předpokládané funkce (jestli lze něco knihovně MuPdf vytknout, pak je to strohá, případně žádná dokumentace), se ukázalo jako nejlepší simulovat klasické doteky displeje a ty poté "podsouvat" již implementovaným metodám v knihovně. K tomu je potřeba vytvořit novou třídu EventSimulator.java s následujícím obsahem.

package com.artifex.mupdfdemo;

import android.annotation.SuppressLint;
import android.os.SystemClock;
import android.view.MotionEvent;

public class EventSimulator {

// Simulace pohybu prstem po displeji
@SuppressLint("Recycle")
public static MotionEvent simulateActionMove(float x, float y) {
long downTime = SystemClock.uptimeMillis();
long eventTime = SystemClock.uptimeMillis() + 100;
int metaState = 0;
MotionEvent motionEvent = MotionEvent.obtain(
    downTime,
    eventTime,
    MotionEvent.ACTION_MOVE,
    x,
    y,
    metaState
);
return motionEvent;
}

// Simulace dotyku prstem na displeji
@SuppressLint("Recycle")
public static MotionEvent simulateActionDown(float x, float y) {
long downTime = SystemClock.uptimeMillis();
long eventTime = SystemClock.uptimeMillis() + 100;
int metaState = 0;
MotionEvent motionEvent = MotionEvent.obtain(
    downTime,
    eventTime,
    MotionEvent.ACTION_DOWN,
    x,
    y,
    metaState
);
return motionEvent;
}

// Simulace ukonceni pohybu po displeji
@SuppressLint("Recycle")
public static MotionEvent simulateActionUp(float x, float y) {
long downTime = SystemClock.uptimeMillis();
long eventTime = SystemClock.uptimeMillis() + 100;
int metaState = 0;
MotionEvent motionEvent = MotionEvent.obtain(
    downTime,
    eventTime,
    MotionEvent.ACTION_UP,
    x,
    y,
    metaState
);
return motionEvent;
}
}

Zoom a pohyb po stránce - implementace triggerů pro náklon

Pohyb po stránce zajišťují snímače náklonu hlavy (brýli), ve chvíli, kdy dojde k překročení definované spínací úrovně, zavolá se metoda pro pohyb v daném směru (respektive je vygenerován příslušný dotek pomocí simulátoru výše a ten je "podstrčen" příslušné metodě, viz následující kód).


Nejprve je třeba definovat všechny potřebné proměnné proměnné, které budou se senzory pracovat. Děje se tak v souboru MuPDFActivity.java. Deklaraci vložíme na začátek třídy MuPDFActivity.

// deklarace promenych pracujicich se senzory
private static SensorManager sensmanager;
private Sensor accelerometer_ori;
private Sensor magnetic_ori;

Následuje implementace samotného listeneru, tedy úseku programu, který sleduje hodnoty náklonu a v případě překročení stanovené prahové úrovně provede příslušnou akci. To zajišťuje následující blok kódu, který je umístěn hned za výše zmiňovanou deklarací proměnných v MuPDFActivity.java.

// listener pro orientaci , tzn. oboje najednou, ACC i MAG
private final SensorEventListener OriListener = new SensorEventListener() {

float[] arrayoriacc = new float[3];
float[] arrayorimag = new float[3];
private float[] mR = new float[9];
private float[] mOrientation = new float[3];

public void onSensorChanged(SensorEvent event) {
// rozliseni senzoru
switch (event.sensor.getType()) {
case Sensor.TYPE_ACCELEROMETER:
arrayoriacc = event.values.clone();
break;
case Sensor.TYPE_MAGNETIC_FIELD:
arrayorimag = event.values.clone();
break;
}
if (arrayoriacc != null && arrayorimag != null) {
if (SensorManager.getRotationMatrix(mR, null, arrayoriacc,
arrayorimag)) {
SensorManager.getOrientation(mR, mOrientation);

// normalizace
mOrientation[0] = (float) (mOrientation[0] >= 0 ? mOrientation[0]
: mOrientation[0] + 2 * Math.PI);
String oriinfo = String.format(
" ZXY %.2f \n %.2f  %.2f",
Math.toDegrees(mOrientation[0]),
Math.toDegrees(mOrientation[1]),
Math.toDegrees(mOrientation[2]));

String accinfo = String.format(
"Acc %.2f %.2f %.2f", arrayoriacc[0],
arrayoriacc[1], arrayoriacc[2]);

if (arrayoriacc[2] < -2) {
//simulace dotyku, tazeni a pusteni v dannem smeru
mDocView.onTouchEvent(EventSimulator.simulateActionDown(100, 100));
mDocView.onTouchEvent(EventSimulator.simulateActionMove(100, 100));
mDocView.onTouchEvent(EventSimulator.simulateActionMove(100, 110));
mDocView.onTouchEvent(EventSimulator.simulateActionUp(100, 110));

} else if (arrayoriacc[2] > 3) {
mDocView.onTouchEvent(EventSimulator.simulateActionDown(100, 100));
mDocView.onTouchEvent(EventSimulator.simulateActionMove(100, 100));
mDocView.onTouchEvent(EventSimulator.simulateActionMove(100, 90));
mDocView.onTouchEvent(EventSimulator.simulateActionUp(100, 90));
}

if (arrayoriacc[0] < -2) {
mDocView.onTouchEvent(EventSimulator.simulateActionDown(100, 100));
mDocView.onTouchEvent(EventSimulator.simulateActionMove(100, 100));
mDocView.onTouchEvent(EventSimulator.simulateActionMove(90, 100));
mDocView.onTouchEvent(EventSimulator.simulateActionUp(90, 100));

} else if (arrayoriacc[0] > 3) {
mDocView.onTouchEvent(EventSimulator.simulateActionDown(100, 100));
mDocView.onTouchEvent(EventSimulator.simulateActionMove(100, 100));
mDocView.onTouchEvent(EventSimulator.simulateActionMove(110, 100));
mDocView.onTouchEvent(EventSimulator.simulateActionUp(110, 100));
}
}
}
}
public void onAccuracyChanged(Sensor sensor, int accuracy) {
}
};

Dále je potřeba inicializovat proměnné při startu aplikace, to zajistíme přidáním následujícího úseku kódu do metody onCreate.

sensmanager = (SensorManager) getSystemService(Context.SENSOR_SERVICE);
accelerometer_ori = sensmanager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
magnetic_ori = sensmanager.getDefaultSensor(Sensor.TYPE_MAGNETIC_FIELD);

Posledním krokem je zajištění zapínání a vypínání senzorů při uspávání a probouzení zařízení. Do metody onPause přidáme

sensmanager.unregisterListener(OriListener);

a do metody onResume

if (!sensmanager.registerListener(OriListener, magnetic_ori,
SensorManager.SENSOR_DELAY_UI)) {
}
if (!sensmanager.registerListener(OriListener, accelerometer_ori,
SensorManager.SENSOR_DELAY_UI)) {
}

Po přidání těchto úseků kódu by již mělo fungovat snímání náklonu a posun po stránce s příslušným překreslováním.

Zoom a pohyb po stránce - implementace listenerů pro zoom

Další nezbytnou funkcionalitou je možnost přiblížení či oddálení pohledu. Jsme opět v MuPDFActivity.java a opět na začátku třídy MuPDFActivity. Následující kód zajistí přiblížení či oddálení pohledu při přejetí prstem dopředu nebo dozadu po dotykové ploše brýlí.

    @Override
    public boolean onGenericMotionEvent(MotionEvent event)
    {
        if (mGestureDetector != null) {
            return mGestureDetector.onMotionEvent(event);
        }
        return false;
    }

    private GestureDetector createGestureDetector(Context context)
    {
        GestureDetector gestureDetector = new GestureDetector(context);

        // Vytvori gesture detector pro sledovani zakladnich dotykovych udalosti
        gestureDetector.setBaseListener( new GestureDetector.BaseListener() {
            @Override
            public boolean onGesture(Gesture gesture) {
                if (gesture == Gesture.TAP) {

                    return true;
                } else if (gesture == Gesture.TWO_TAP) {

                    return true;
                } else if (gesture == Gesture.SWIPE_RIGHT) {
                // oddaleni
                mDocView.setScaleX(mDocView.getScaleX()*(float)0.5);
                mDocView.setScaleY(mDocView.getScaleY()*(float)0.5);
                    return true;
                } else if (gesture == Gesture.SWIPE_LEFT) {
                // priblizeni     
                mDocView.setScaleX(mDocView.getScaleX()*2);
                mDocView.setScaleY(mDocView.getScaleY()*2);                
                    return true;
                }
                return false;
            }
        });
        return gestureDetector;
    }

A stejně jako v předchozím případě následuje inicializace proměnných v metodě onCreate.

try {
mGestureDetector = createGestureDetector(this);
} catch (NoClassDefFoundError e) {
}

Tím je implementace zoomu dokončena.

Další nezbytné úpravy

Aby aplikace při prohlížení neusínala, přidáme do metody onCreate ještě

getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

Vzhledem k tomu, že je knihovna určena pro prohlížení na větších displejích, obsahuje i lišty, které sice nabízejí užitečné nástroje, ale na brýlích je nevyužijeme. Proto je vhodné zakomentovat řádek

//layout.addView(mButtonsView);

aby při čtení lišty už na tak dost malém zobrazení nerušily. Aby uživatel nemusel po načtení souboru vždy zobrazení přibližovat, protože při zvětšení na úrovni jedna je soubor nepoužitelný, je třeba změnit defaultní úroveň přiblížení v souboru ReaderView.java například takto

private float             mScale     = 4.0f; // puvodne 1.0f

Závěr

Uvedený postup by měl zajistit funkčnost aplikace, která samozřejmě bude potřebovat další kosmetické úpravy. Aplikace pracuje s širokou škálou dokumentů počínaje novinami a časopisy v elektronické formě a konče technickými výkresy a manuály. Testy zobrazení různé grafiky i textů jsou níže. Co se týče výkonu, aplikace pracuje rychle a plynule i s většími dokumenty, jako jsou knihy v PDF nebo vědecké práce.

Test vykreslování technických výkresů

Test vykreslování běžných textů a grafiky

Tags: 

O autorovi

Autorovi je 29 let, a pracuje ve jako systémový inženýr ve společnosti Valeo. Studoval Kybernetiku a robotiku na Fakultě elektrotechnické ČVUT v Praze. Ve svém volném čase se zabývá hudbou a vším okolo ní, jezdí na kole nebo chodí plavat. Podívejte se na více informací nebo na Curriculum Vitae.