2015年3月8日日曜日

Offlineでもgeolocationが使えるWebViewを使ったAndroidアプリを作る

前の記事で書いた通り、「AndroidのWebブラウザでは、電波のないところでの位置情報取得は基本的には不可」です。そこで、Webアプリを表示するWebViewだけを持つネイティブアプリを作り、HTML5の機能が動作する設定をすることでWebアプリ側を改修せずにAndroidでも使えるようになります。その作り方を紹介します。


1. Webアプリを読み込むAndroidアプリを作成

  • Android Studioでプロジェクトを作成する。
    • Blank Activityをベースにした場合、APIレベル14(Android4.0)以降で作るとActionBarActivityを継承するが、アクションバーは使用しな場合はActivityの継承に変更するのがオススメです。
  • ネットアクセスのpermissionを設定する。
    • AndroidManifest.xmlにpermissionを追加
  <uses-permission android:name="android.permission.INTERNET" />
  • 回転できないように縦方向で固定する。
    • AndroidManifest.xml内の要素activityの属性に"android:screenOrientation="portrait"を追加
<activitiy ...
    android:screenOrientation="portrait"  >
 ...
  • アプリ起動時にjavascriptを有効にしてWebアプリを読み込む。
    • WebViewを配置 (id = webView)
    • 起動時のActivityクラス(Javaファイル)のonCreateメソッドに以下を追加
    WebView myWebView;
    String urlTop = "http://www.example.com";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        setContentView(R.layout.activity_trek_log_view);
        myWebView = (WebView) findViewById(R.id.webView);
        WebSettings webSettings = myWebView.getSettings();
        //javascriptを有効にする
        webSettings.setJavaScriptEnabled(true);
        myWebView.setWebViewClient(new WebViewClient());
        myWebView.loadUrl(urlTop+"index.html");
    }
※参考:Androidのwebviewとか。

これでWebアプリをAndroidのネイティブアプリとして起動できます。
ただ、このままではHTML5の機能が使用できないため追加の実装を行います。

2.HTML5の機能を有効化する

  • HTML5のlocalStorageとapplication cacheを有効にする。
    • ActivityクラスのonCreateメソッドに以下を追加
        //localStorageが使用できるようにする
        webSettings.setDomStorageEnabled(true); 
        //application cacheを有効にする
        webSettings.setAppCacheEnabled(true); 
        //application cacheのキャッシュ先を指定
        webSettings.setAppCachePath("/appcache/"); 
        //引数は「キャッシュが存在する場合はそれを使用、ない場合はネットワーク経由で取得」
        webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);  
  • 位置情報取得を有効にする。
    • AndroidManifest.xmlにpermissionを追加
    <uses-permission
        android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission
        android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    • ActivityクラスにInterface「LocationListener」を実装する
public class TrekLogView extends Activity
    implements LocationListener{
    • LocationListenerで実装が必要なメソッドをオーバーライドする
    @Override
    public void onLocationChanged(Location location) {}
    @Override
    public void onProviderDisabled(String provider) {}
    @Override
    public void onProviderEnabled(String provider) {}
    @Override
    public void onStatusChanged(
        String provider, int status, Bundle extras) {}
    • HTML5のgeolocation APIを有効化
        webSettings.setGeolocationEnabled(true);
        myWebView.setWebChromeClient(new WebChromeClient(){
          @Override
          public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
                callback.invoke(origin, true, false);
          }
        });
  • アプリ初回起動時(onCreate)に位置情報取得を起動
    LocationManager locationManager;
    String bestProvider;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        initLocationService();
   }
    //初回起動時に実行する
    protected void initLocationService() {
        locationManager = (LocationManager)getSystemService(LOCATION_SERVICE);
        Criteria criteria = new Criteria();
        criteria.setAccuracy(Criteria.ACCURACY_FINE);
        criteria.setPowerRequirement(Criteria.POWER_MEDIUM);
        bestProvider = locationManager.getBestProvider(criteria, true);
    }
  • アプリ復帰時(onResume)に位置情報取得を再開、停止時(onPause)に停止する
    @Override
    protected void onResume() {
        super.onResume();
        locationManager.requestLocationUpdates(bestProvider, 60000,1, this);
    }
    @Override
    protected void onPause() {
        super.onPause();
        locationManager.removeUpdates(this);
    }

これでapplication cache/localStorage/geolocation APIを使ったWebアプリをAndroidのネイティブアプリとして起動できます。

※参考:
  1. WebView で Web Storage / Web SQL Database を利用する設定
  2. AndroidのWebViewをできるだけ速く表示する(キャッシュ・先読み編)
  3. AndroidのWebView内でgeolocationを使う
  4. 条件にあう位置情報プロバイダから、位置情報を利用する簡単な例

3.WebViewを使ったWebアプリの使い勝手を改善

WebアプリをAndroidアプリにしたことで使い勝手が悪いところがあるため改善します。
  • Android端末の「戻るボタン」でWebアプリの「戻る」を割り当てる。
    • ActivityクラスのonKeyDownメソッドにオーバーライドする
        @Override
        public boolean onKeyDown(int keyCode, KeyEvent event) {
                if ((keyCode == KeyEvent.KEYCODE_BACK) &&  myWebView.canGoBack()) {
                myWebView.goBack();
                return true;
            }
            return super.onKeyDown(keyCode, event);
        }
    

  • 特定のURLを持つサイト(map.html)は外部アプリを起動(Intent)する。
    • Activityクラスに以下を追加、変更。
@Override
protected void onCreate(Bundle savedInstanceState) {
    ...
    myWebView.setWebViewClient(new MyWebViewClient());
    ...
}
...
private class MyWebViewClient extends WebViewClient {
   @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        boolean isMyLogPublicHost = Uri.parse(url).getHost().equals("www.example.com");
        boolean isMyTestHost = Uri.parse(url).getHost().equals("www.test.com");
        boolean isMyHost = isMyPublicHost || isMyTestHost;

        // 特定ホスト名、かつ、特定文字列を含むURLの場合はWebView内で画面遷移させる
        if (isMyHost && Uri.parse(url).getPath().indexOf("map.html") == -1) {
                return false;
        }
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
        startActivity(intent);
        return true;
    }
}

これで一通り使えるようになりました。
※参考: Building Web Apps in WebView

4.まとめ

最後に、今回作ったソースコードをまとめて載せておきます。
  • AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.appspot.yamanobo.myapplication" >
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission
        android:name="android.permission.ACCESS_FINE_LOCATION"/>
    <uses-permission
        android:name="android.permission.ACCESS_COARSE_LOCATION"/>
    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".MainActivity"
            android:label="@string/app_name"
            android:screenOrientation="portrait" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>
  • MainAcitivity.java
package com.example.myapplication;

import android.content.Intent;
import android.location.Criteria;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.net.Uri;
import android.app.Activity;
import android.os.Bundle;
import android.view.KeyEvent;
import android.webkit.GeolocationPermissions;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;


public class MainActivity extends Activity implements LocationListener {

    WebView myWebView;
    String urlTop = "http://trek-log.appspot.com/";
    LocationManager locationManager;
    String bestProvider;

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        setContentView(R.layout.activity_main);
        myWebView = (WebView) findViewById(R.id.webView);
        WebSettings webSettings = myWebView.getSettings();
        //javascriptを有効にする
        webSettings.setJavaScriptEnabled(true);

        //localStorageが使用できるようにする
        webSettings.setDomStorageEnabled(true);
        //application cacheを有効にする
        webSettings.setAppCacheEnabled(true);
        //application cacheのキャッシュ先を指定
        webSettings.setAppCachePath("/appcache/");
        //引数は「キャッシュが存在する場合はそれを使用、ない場合はネットワーク経由で取得」
        webSettings.setCacheMode(WebSettings.LOAD_CACHE_ELSE_NETWORK);

        webSettings.setGeolocationEnabled(true);
        myWebView.setWebChromeClient(new WebChromeClient(){
            @Override
            public void onGeolocationPermissionsShowPrompt(String origin, GeolocationPermissions.Callback callback) {
                callback.invoke(origin, true, false);
            }
        });

        myWebView.setWebViewClient(new MyWebViewClient());
        myWebView.loadUrl(urlTop+"index.html");

        initLocationService();
    }

    //初回起動時に実行する
    protected void initLocationService() {
        locationManager = (LocationManager)getSystemService(LOCATION_SERVICE);
        Criteria criteria = new Criteria();
        criteria.setAccuracy(Criteria.ACCURACY_FINE);
        criteria.setPowerRequirement(Criteria.POWER_MEDIUM);
        bestProvider = locationManager.getBestProvider(criteria, true);
    }

    @Override
    protected void onResume() {
        super.onResume();
        locationManager.requestLocationUpdates(bestProvider, 60000,1, this);
    }
    @Override
    protected void onPause() {
        super.onPause();
        locationManager.removeUpdates(this);
    }

    @Override
    public void onLocationChanged(Location location) {}
    @Override
    public void onProviderDisabled(String provider) {}
    @Override
    public void onProviderEnabled(String provider) {}
    @Override
    public void onStatusChanged(
            String provider, int status, Bundle extras) {}

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        if ((keyCode == KeyEvent.KEYCODE_BACK) &&  myWebView.canGoBack()) {
            myWebView.goBack();
            return true;
        }
        return super.onKeyDown(keyCode, event);
    }

    private class MyWebViewClient extends WebViewClient {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            boolean isMyPublicHost = Uri.parse(url).getHost().equals("www.trek-log.com");
            boolean isMyTestHost = Uri.parse(url).getHost().equals("www.trek-log-dev.com");
            boolean isMyHost = isMyPublicHost || isMyTestHost;

            // 特定ホスト名、かつ、特定文字列を含むURLの場合はWebView内で画面遷移させる
            if (isMyHost && Uri.parse(url).getPath().indexOf("map.html") == -1) {
                return false;
            }
            Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
            startActivity(intent);
            return true;
        }
    }
}
  • 調査/開発用のAndroid端末としては ASUS ZenFone5 (Android 4.4.2)を使用しています。

5.残りの課題

まだ一部未解決のものがあります。
  • Offline時にはトップページのツイッターボタンは非表示としているが表示されてしまう。
  • 左上のロゴからトップに戻るとレイアウトが不完全になることがある。(初回使用時に発生しやすい。キャッシュが不完全の可能性がある)
また、application cacheの更新の検証はしていないので、引き続き確認したいと思います。

0 件のコメント:

コメントを投稿

深層学習を使ってツイッター画像のラベル付けができるまで(3)

深層学習のラベル精度向上のためにやったこと  前の記事 で、無事マルチラベルのラベル付けができる学習モデルが作成できるようになりました。ただ、その精度は納得できるレベルではなかったので、その改善のために「やったこと」と「起きたこと」を説明します。 やったこと1: 教師...