前の記事で書いた通り、
「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のネイティブアプリとして起動できます。
※参考:
- WebView で Web Storage / Web SQL Database を利用する設定
- AndroidのWebViewをできるだけ速く表示する(キャッシュ・先読み編)
- AndroidのWebView内でgeolocationを使う
- 条件にあう位置情報プロバイダから、位置情報を利用する簡単な例
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)する。
@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.まとめ
最後に、今回作ったソースコードをまとめて載せておきます。
<?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>
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の更新の検証はしていないので、引き続き確認したいと思います。