Android」タグアーカイブ

自作のWebアプリをAndroidアプリ化できなかった

先日、風邪ログというWebアプリをリリースしました。自分の体調を記録して振り返り、自分がどんなときに体調が悪くなるかを理解できるアプリです。Webアプリですが、Google Playストアでもアプリを見つけてもらえるように、Androidアプリ化できるか試してみました。Androidアプリ化の方法はいくつかありますが、今回は一番さくっとできそうなWebViewを使った方法を試しました。

WebViewで動かしてみる

WebViewを使ってWebアプリを動かす方法を、Android公式の記事を参考に試しました。

https://developer.android.com/guide/webapps/webview?hl=ja

まずはAndroid Studioで新規プロジェクトを作成し、Empty ActivityをTemplateとして選びます。この初期状態でアプリを実行すると、空の画面が表示されます。

まずはactivity_main.xmlにWebViewを記載します。

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <!-- 以下のブロックを記載 -->
    <WebView
        android:id="@+id/webview"
        android:layout_width="match_parent"
        android:layout_height="match_parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

次に、インターネット上で動くWebアプリにアクセスできるよう、AndroidManifest.xmlに許可設定を記載します。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.isolity.kazelog">

    <!-- 以下のブロックを記載 -->
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        ...

最後に、MainActivity.ktにWebViewの設定を記載します。

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        // 以降の行を記載
        val myWebView: WebView = findViewById(R.id.webview)
        // Webアプリ内のリンクをWebView内で開くようにする
        myWebView.webViewClient = WebViewClient()
        // JavaScriptの有効化
        myWebView.settings.javaScriptEnabled = true
        // Web Storageの有効化
        myWebView.settings.domStorageEnabled = true
        // Webアプリの読み込みのため、風邪ログのURLを指定
        myWebView.loadUrl("https://kazelog.shoarai.com")
    }
}

上記のコードでアプリを実行すると、風邪ログのトップページが表示されました。

WebView上でGoogleログインできない

ここでやっかいなのが、以下の処理です。

// Webアプリ内のリンクをWebView内で開くようにする
myWebView.webViewClient = WebViewClient()

風邪ログは、EmailによるログインやGoogleアカウントによるログインをサポートしています。ログインをボタンを押したときに、Googleのログイン画面に遷移することになるんですが、上記の設定を行わないとGoogle ChromeなどのAndroid規定のブラウザでログイン画面が開いてしまいます。上記の設定をするとこで、ログイン画面がWebView内で開くようになります。

さてここで本題なのですが、GoogleはWebView内でのログインをサポートしていません。たとえWebView内でGoogleにログインしようとしても、以下のエラー画面が表示されます。

この件に関する公式のドキュメントを探したところ、以下の記事が見つかりました。やはりWebViewからGoogleログインに必要なOAuthリクエストが許可されなくなるとのことです。

「ウェブビュー」と言われる埋め込みブラウザから Google への OAuth リクエストは許可されなくなります。

https://developers-jp.googleblog.com/2016/09/modernizing-oauth-interactions-in-native-apps.html

ということでWebアプリである風邪ログを、Androidアプリとしてリリースすることはできませんでした。Googleログインを行わないWebアプリであれば、リリースできるかもしれませんね。

KotlinでToastをカスタマイズする

先日、ポップアップアラームという設定した時刻にポップアップを表示するアプリを作りました。このページではその中でもポップアップであるToastをカスタマイズして表示する処理について紹介します。

標準のToastを表示する

標準のToastは以下のコードで表示できます。

object ToastView {
    fun showToast(context: Context, message: String) {
        Toast.makeText(context, message, Toast.LENGTH_LONG).show()
    }
}

Toastをカスタマイズする

前述のような標準のToastだと目立たず、通知を見逃しやすいので、上の画像のようにToastのサイズやフォントサイズが大きい独自のレイアウトを作成します。

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:ads="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:weightSum="1">

    <TextView
        android:id="@+id/message"
        android:layout_width="320dp"
        android:layout_height="80dp"
        android:background="@drawable/toast_shape"
        android:gravity="center"
        android:paddingLeft="10dp"
        android:paddingRight="10dp"
        android:textColor="@color/colorBase"
        android:textSize="40sp" />

</LinearLayout>

上記の独自レイアウトを表示します。

object ToastView {
    fun showToast(context: Context, message: String) {
        val inflate = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        val view = inflate.inflate(R.layout.toast, null)
        val textView = view.findViewById(R.id.message) as TextView
        textView.text = text

        Toast(context).run {
            this.view = view
            duration = Toast.LENGTH_LONG
            setGravity(Gravity.BOTTOM, 0, 250)
            show()
        }
    }
}

KotlinでAndroidの時刻を検知する

先日、ポップアップアラームという設定した時刻にポップアップを表示するアプリを作りました。このページではその中でも時刻の検知の処理について紹介します。

アラームを開始、停止する

Androidアプリで、ある時刻になったら処理を実行したい場合、AlarmManagerを使用します。厳密には、ある時刻になったことを通知するのではなく、ある時間を経過したことを通知できます。
今回はアラームアプリなので、ある程度正確な時間を通知できるsetExact()を呼びます。通知はBroadcastReceiverで受け取ります。
以下のクラスでは、アラームを開始して1度だけ通知します。別のアラームがすでに実行中なら、実行中のアラームは停止し、新しいアラームが開始されます。

object OnceAlarmManager {
    private const val REQUEST_CODE = 0
    private var alarmManager: AlarmManager? = null
    private var pendingIntent: PendingIntent? = null

    fun startAlarm(context: Context, calendar: Calendar) {
        alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        val intent = Intent(context, AlarmBroadcastReceiver::class.java)
        pendingIntent = PendingIntent.getBroadcast(
                context, REQUEST_CODE, intent, PendingIntent.FLAG_UPDATE_CURRENT)
        alarmManager?.setExact(AlarmManager.RTC, calendar.timeInMillis, pendingIntent)
    }

    fun stopAlarm() {
        alarmManager?.cancel(pendingIntent)
    }
}

アラームアプリでは、1つのアラームで時刻通知したら次のアラームを開始する必要があるので、以下の通り、設定がONになっているアラームを開始します。

object WeeklyAlarmManager {
    fun startNextAlarm(context: Context) {
        var weeklyAlarms = WeeklyAlarmDataManager.weeklyAlarms
        if (WeeklyAlarmUtil.hasPowerOn(weeklyAlarms)) {
            val calendar = WeeklyAlarmUtil.getNextAlarmAsCalendar(weeklyAlarms)
            OnceAlarmManager.startAlarm(context, calendar)
        } else {
            OnceAlarmManager.stopAlarm()
        }
    }
}

アラーム通知を受け取る

前述の通り、BroadcastReceiverでアラーム通知を受け取ります。通知を受け取ったら、ポップアップを表示し、次のアラームを開始します。

class AlarmBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // Show popup.
        WeeklyAlarmManager.startNextAlarm(context)
    }
}

BroadcastReceiverをアプリに登録します。

<application
    ...
    <receiver android:name=".AlarmBroadcastReceiver">
    </receiver>
</application>

端末の再起動時やタイムゾーンの変更時に、アラームを再設定する

端末が再起動されたことやタイムゾーンが変更されたことを、BroadcastReceiverで受け取れるよう、actionを設定します。

<application
    ...
    <receiver android:name=".AlarmBroadcastReceiver">
        <intent-filter>
            <action android:name="android.intent.action.BOOT_COMPLETED" />
        </intent-filter>
        <intent-filter>
            <action android:name="android.intent.action.TIMEZONE_CHANGED" />
            <action android:name="android.intent.action.TIME_SET" />
            <action android:name="android.intent.action.DATE_CHANGED" />
        </intent-filter>
        <intent-filter>
            <action android:name="android.intent.action.PACKAGE_REPLACED" />
            <data
                android:path="com.isolity.toastalarm"
                android:scheme="package" />
        </intent-filter>
    </receiver>
</application>

上記で設定したactionの通知時にも、BroadcastReceiverのonReceive()が実行されます。アラーム通知の場合のみ(ここではactionがnullのとき)、ポップアップメッセージを表示します。

class AlarmBroadcastReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.action == null) {
            // Show popup.
        }
        WeeklyAlarmManager.startNextAlarm(context)
    }
}

改善点

上記コードの改善点として、例えば、アラーム通知の判定がaction==nullだとわかりにくいので、アクション名つけた方がいいですね。あとはDIとかテストの方法はこれから調べます。

※Huaweiのスマホでの注意事項

Huaweiのスマホでは、以下の設定をしないと、アプリをKillしたあとにAlarmManagerの処理が止まってしまい、通知を受け取れなくなってしまいます。
設定 > 詳細設定 > バッテリーマネージャー > 保護されたアプリ > 対象のアプリをONにする
アプリごとに設定が必要なので、アプリを初めてインストールしたときに設定しておきます。僕はHuaweiのP8liteというスマホを使っていて、Emulatorでは動くのに実機で動かず非常に苦労しました。。Android開発者は各端末の知識も必要で大変ですね;

KotlinでAndroidアラームアプリを作った

Androidのアラームアプリを作ったときにやったこと、調べたことを紹介します。

作ったアプリ

ポップアップアラームという設定した時刻にポップアップを表示するアプリを作りました。
ストアに公開したので、ぜひ触ってみてください!

やったこと

Cordova/PhoneGapでAndroidアプリを作ったことはありますが、ネイティブで作るのは初めてなので、まずは言語仕様の理解から始めました。

  1. Kotlinの理解
  2. 開発環境の構築
  3. 実装
  4. アイコンの作成
  5. リリース

Kotlinの理解

開発言語として、JavaではなくKotlinを選びました。
型推論が欲しいこと、Null安全に興味があったこと、周囲の評判が良かったことが理由です。
※先日、KotlinがAndroidアプリ公式の開発言語に選ばれましたが、僕が選んだ後の話だったのでニュースを見たときは幸せを感じました笑
言語仕様は、その言語を書き始める前にざっと知っておいた方が効率がいいと思っているので、以下の本を読みました。

Kotlinスタートブック

その他、実装中にリスト操作について知りたいことが多かったので、以下の記事を参考にさせていただきました。

Qiita:Kotlin のコレクション使い方メモ

開発環境の構築

Android Studioのインストールは公式サイトより。
Android StudioへのKotlinの導入は、前述のKotlin本を参考にしました。取り急ぎこれで十分かと。
JavaのコードをコピペするとKotlinコードに自動的に変換されるのは、Kotlinの大きなメリットでもありますね。

実装

実装した主な機能は以下の通り。

Androidネイティブの実装は初めてということもあり、だいぶ詰まりました…
細かい部分も含めて、別ページで紹介しようと思います。

広告の実装

アプリにバナー広告を実装してみました。使ったのはGoogleのAdMobです。手順は公式サイトを参考のこと。Android Studioと連携できるのでほぼ自動で実装できました。

アイコンの作成

アイコンの作成には、Inkscapeという無料の画像編集ソフトを使いました。

リリース

リリースの際、APKに署名する必要がありますが、これもAndroid Studioからできます。
むしろ、アプリの説明文を記載するのがなかなか大変ですね。

かかった時間

ざっくりですが、開発にかかった実時間の内訳は以下のような感じ。
といっても期間としては3ヶ月ぐらいかかってます。途中、実装に詰まって手が止まりました;

  • Kotlinの理解 → 1日
  • 開発環境の構築 → 1日
  • 実装 → 2週間
  • アイコンの作成 → 半日
  • リリース → 1日

振り返り

Contextの扱いであったりResourceファイルの違いであったり、Android開発の知識が乏しく実装中に手が止まることが多かったです。実装する前にAndroidの基礎本を読んでおけばよかったかなと。
試しに広告を入れてみましたが、はじめから広告ありきで画面設計、実装しておけば、広告を表示する隙間をあとで開ける必要はなかったかなと。

IonicアプリのCordovaをアップデートする

久しぶりにGoogle Play Developer Consoleを見たら、アラートが表示されていました。
PhoneGap version alert
アラートが出ているのはIonicで作ったアプリです。このアプリが使用しているCordovaのバージョンに脆弱性があるようで、期限内にアップロードしてね、という通知でした。

Ionicのアップデート

Ionicが依存するCordovaのバージョンは、Ionicのバージョンによって決まります。よってIonicをアップデートする必要があります。

$ npm i -g ionic

アプリのアップデート

Ionicをアップデートしても、アプリはアップデートされないため、一度アプリを消してから作り直す必要があります。まずはアプリを削除します。

$ ionic platform rm android

アプリをビルドし直します。

$ ionic build android

今後も脆弱性が見つかるかもしれないので、都度アップデートする必要がありますね。

PhoneGapアプリの公開用APKを作る

このページでは、PhoneGapで作ったAndroidアプリの公開用APKを作るまでを紹介します。APKは「android application package file」の略です。

事前準備

証明書を作る

Google playにアプリを公開するには、アプリに署名する必要があります。これは、アプリの正当性を保証するためです。
まず、署名するための証明書を作ります。keytoolはJDKに入っているので、パスが通っていればどこでも実行できます。パスワードなど色々聞かれるので、好きに答えてください。「よろしいですか?」と聞かれたら、「y」と答えれば次に進みます。

  今回の設定値
エイリアス名 shoarai
アプリ名 app
有効日数  10000
$ keytool -genkey -v -keystore .keystore -alias shoarai -keyalg RSA -validity 10000
> キーストアのパスワードを入力してください:
> 新規パスワードを再入力してください:
> 姓名を入力してください。
> 組織単位名を入力してください。
> 組織名を入力してください。
> 都市名または地域名を入力してください。
> 都道府県名を入力してください。
> この単位に該当する2文字の国コードを入力してください。
> ...でよろしいですか。
10,000日間有効な2,048ビットのRSAの鍵ペアと自己署名型証明書(SHA256withRSA)を生成しています
	ディレクトリ名: ... の鍵パスワードを入力してください
	(キーストアのパスワードと同じ場合はRETURNを押してください):

公開用APKを作る

APKを生成する

アプリの実装が完了したらリリースビルドし、APKを生成します。platforms/android/ant-buildにapp-release-unsigned.apkというファイルが生成されます。appの部分にはアプリ名が入ります。

$ phonegap build --release

署名する

事前に作った証明書でapkに署名します。

$ jarsigner -verbose -keystore .keystore app-release-unsigned.apk shoarai

最適化する

Google playにアプリを公開するには、最適化もする必要があります。最適化後のapkのファイル名をapp.apkとします。これで公開用APIの完成です。

$ zipalign -v 4 app-release-unsigned.apk app.apk

エラー

前述の最適化にて、zipalignコマンドが見つからないことがあります。

-bash: zipalign: command not found

zipalignはANDROID SDKのBuild-toolsに入っています。もしまだBuild-toolsが入ってないなら、Android SDK Managerからインストールしましょう。また、パスを通さないと実行できません。パスを通すのが面倒であれば、下記のように直接アクセスして実行してもいいです。

$ /Applications/android-sdk-macosx/build-tools/21.1.1/zipalign ...

ちなみに

APK生成、署名、最適化をまとめて行えるように下記のようなバッチファイルを作り、証明書と一緒にプロジェクトフォルダに置いておくと楽ですね。

MY_DIRNAME=$(dirname $0)
cd $MY_DIRNAME
# 設定値
UNSIGNED_FILE=platforms/android/ant-build/app-release-unsigned.apk
SIGNED_FILE=app.apk
ALIAS=shoarai
phonegap build --release
jarsigner -verbose -keystore .keystore $UNSIGNED_FILE $ALIAS
zipalign -v 4 $UNSIGNED_FILE $SIGNED_FILE

警告が表示される(2015-12-24追記)

いつの間にか、署名時に下記の警告が表示されるようになっていました。

警告:
-tsaまたは-tsacertが指定されていないため、このjarにはタイムスタンプが付加されていません。
タイムスタンプがないと、署名者証明書の有効期限(2043-05-10)後または将来の失効日後に
、ユーザーはこのjarを検証できない可能性があります。

JDK1.7からタイムスタンプが必要になったそうです。署名を下記のように修正すると警告が表示されなくなります。

$ jarsigner -verbose -tsa https://timestamp.digicert.com -keystore .keystore app-release-unsigned.apk shoarai

PhoneGapでアプリを作る

このページでは、PhoneGapをインストール後にサンプルプロジェクトを作成し、エミュレータで起動するまでを紹介します。

PhoneGapの導入

PhoneGapをインストールし、phonegapコマンドを使えるようにします。

事前準備

  • Node.jsがインストール済み
  • Antがインストール済み
  • Android SDKがインストール済み

PhoneGapのインストール

npmからPhoneGapをインストールします。

$ npm i -g phonegap

インストールが終わったら、PhoneGapのインストール状況を確認します。PhoneGapのコマンド一覧などが参照できます。

$ phonegap
...
Commands:
  create <path>        create a phonegap project
  build <platform>     build a specific platform
  install <platform>   install a specific platform
  run <platform>       build and install a specific platform
  local [command]      development on local system
  remote [command]     development in cloud with phonegap/build
  platform [command]   update a platform version
  plugin [command]     add, remove, and list plugins
  help [command]       output usage information
  version              output version number
...

プロジェクトの作成

PhoneGapコマンドから以下の内容を持つプロジェクトを作ります。このプロジェクトはAndroidやiOSなどのプラットフォームに共通のプロジェクトです。

プロジェクト名

まず、フォルダ名、パッケージ名、プロジェクト名は以下の通りとします。 ちなみにフォルダ名以外は未入力でも、デフォルトで設定されるので特に決めなくても大丈夫です。

  今回の入力値 デフォルト値
フォルダ名 Hello -(未入力不可)
パッケージ名 com.shoarai.hello com.phonegap.helloworld
プロジェクト名 HelloWorld Hello World

プロジェクトの生成

phonegapコマンドで上記の内容を持ったプロジェクトを生成します。

$ phonegap create Hello com.shoarai.hello HelloWorld

作成されるHelloフォルダの構成は以下の通りです。wwwフォルダ内にプラットフォームに共通するHTML、JavaScriptファイルがあります。

Hello
  ├─ .cordova
  ├─ merges
  ├─ platforms
  ├─ plugins
  └─ www

Helloフォルダに移動します。

$ cd Hello
.../Hello>

アプリケーションの作成

作成したプロジェクトからアプリケーションの実行ファイルを作成します。今回はAndroidアプリを作成しますが、PhoneGapはiosやWindowsPhoneのアプリも作成できます。

プロジェクトのビルド

phonegapコマンドでプロジェクトをビルドします。AndroidアプリケーションとしてビルドするためにAndroid SDKを利用しています。

$ phonegap build android
...
[phonegap] successfully compiled Android app

Androidアプリとしてビルドすると、platformsフォルダ内にandroidフォルダが作成されます。また、iOSとしてビルドするとiosフォルダが追加されることになります。 それぞれのフォルダ内に生成されるplartforms/…/wwwフォルダは、Hello/wwwフォルダがコピーされたものです。つまり、Hello/www内を編集して、 ビルドすると指定したplartforms/…/wwwが更新されることになります。

Hello
  ├─ .cordova
  ├─ merges
  ├─ platforms
  │      └─ android
  │             ├─ assets
  │             │     └─ www
  │              ...
  ├─ plugins
  └─ www

エラー

ビルド時に起こったエラーを記述しておきます。

Please install Android target 19 (the Android newest SDK)

これはPhoneGapが対応するAndroid SDKが入ってないよ!ということです。解決策は2つ。「①対応するSDK Platformをインストールする」または「②PhoneGap側のSDKの対応バージョンを変更する」です。 SDK側を合わせるか、PhoneGap側を合わせるかということです。
①は、エラー文にあるSDKバージョン(ここでは19)をインストールします。
②は、対応するバージョンを.cordova/lib/android/cordova/3.1.0(PhoneGapのバージョン)/frameworkフォルダ内のproject.propertiesファイルのtargetの値で確認できます。 ここではtarget=android-19をインストール済みのSDKのバージョン(android-18など)に変更します。

アプリケーションの実行

作成したAndroidアプリを実行します。

エミュレータの起動

まず、アプリを実行するエミュレータを起動します。AVD(Android Virtual Device Manager)を実行し、起動するエミュレータのAVD名を確認します。もしくは起動するAVDを新規作成します。

$ android avd

ここでは、AVD名がNexus5のエミュレータを起動します。

$ emulator -avd Nexus5

アプリケーションの実行

作成したAndroidアプリケーションをエミュレータで実行します。

$ phonegap run android
...
[phonegap] successfully installed onto emulator

phonegap
Androidアプリケーションをエミュレータで実行できました!実行前に実機を接続しておくと、優先して実機で実行されます。 このときは[phonegap] successfully installed onto deviceが表示されます。

ちなみに

今回は説明のためにアプリケーションのビルドと実行を分けましたが、phonegap run androidを実行すれば、phonegap build androidを実行しなくてもアプリケーションの実行ファイルが生成されてます。