20190728のAndroidに関する記事は14件です。

UnityのAndroid実機ビルドでつまづいた話その2

前回までのあらすじ

UnityのAndroid実機ビルドでつまづいた話その1で一時的に動いたものの
また動かなくなった
前回うまくいったのはandroidの設定だと思っていたけど
どうやら違ったらしい

結論から言うとPC変えたらいけた

動作環境

Windows 10(前回と違いノートパソコン)
adb ver.1.0.39
Unity 2018.3~2018.4LTS
※2019はGradleが原因でビルドできなかった
Galaxy note 9 Android 9.0

Buildする前に

JDKとSDKのパスを確認
NDKは無くても問題ない

キャプチャ.PNG
Gradleをinternalに変更

Build And Runを実行

キャプチャ.PNG
Use Highest Installをクリック
※何故かUpdate Android SDKをクリックしても意味がない
このことについて詳細をご存知の方はコメントにて知らせていただけると助かります

Buildできなかった原因

十中八九AndroidStudioのバージョン上げたせい
それのせいでplatform-toolsのadbのバージョンが上がって
組み合わせの問題かadbのせいかはわからないけど
エラー吐いたんだと思われる
メインPCではADBを上手く構築できなかった
メインPCでビルドできたらまた記事書きます

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Kotlin】ActivityのLaunchModeについて

Androidアプリ開発でActivityのlaunchModeの設定が結構重要だと感じたので調べたことをまとめます。
特にBack Stackで挙動が大きく変わるので画面遷移の設計とかにもろに影響する内容だと思います。

launchModeの設定には2通りあって

  • Manifestファイルの<activity>タグの属性として設定する方法
  • Intentのfragで設定する方法

です。画面遷移毎に挙動を変える場合は2つ目の方法が必要ですが、常に同じ挙動なのであれば1つ目の方法で良いと思います。

Manifestファイルの<activity>タグの属性として設定する方法

<activity>タグで設定できるlaunchModeは4種類です。
下記のように設定します。
ruby:qiita.rb
puts 'code with syntax'

Manifest.xml
<activity 
          // 省略

          android:launchMode="standard"

          // 省略
</activity>

standard

  • デフォルト値
  • Activityのインスタンスがタスク上に生成される
  • Back Stackの一番上に追加される
  • 複数の異なるタスク上にインスタンスを生成することができるし、同じタスク上に複数のインスタンスを生成することもできる

singleTop

  • Back Stackの一番上にActivityのインスタンスがある場合は新しくインスタンスを生成せずに、既にあるインスタンスをonNewIntent()で呼ぶ。
  • Activityのインスタンスが存在してもBack Stackの一番上でなければ新しいインスタンスを生成する singleTop.png standard.png

singleTask

  • 新しいBack StackのrootとしてActivityのインスタンスを生成する
  • ただし別のBack Stackの上にActivityのインスタンスが存在する場合は、既にあるインスタンスをonNewIntent()で呼び新しいBack Stackのrootにする
  • Activityのインスタンスは常に1つ
    singleTask.png 図1.png

Note: Although the activity starts in a new task, the Back button still returns the user to the previous activity.

singleInstance

  • singleTaskとほぼ同じ

注意点

Note: The behaviors that you specify for your activity with the launchMode attribute can be overridden by flags included with the intent that start your activity,

Intentのfragで上書きされます。

Intentのfragで設定する方法

Intentのfragで設定できる値は3種類です。

FLAG_ACTIVITY_NEW_TASK

  • launchModesingleTaskと同じ

FLAG_ACTIVITY_SINGLE_TOP

  • launchModesingleTopと同じ

FLAG_ACTIVITY_CLEAR_TOP

  • launchModeに該当するものはない
  • 既にActivityのインスタンスが存在する場合、Back Stack上でこのActivityの上にあるインスタンスが全てdestoryされた状態でonNewIntent()メソッドで既存のインスタンスが呼ばれる
  • FLAG_ACTIVITY_CLEAR_TOPFLAG_ACTIVITY_NEW_TASKは一緒に使われることが多い
    • 新しいタスクでインスタンスを生成した時にBack Stackをclearできるから

今回の記事は以上です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidアプリにGoogle Mapを組み込む

最近 Maps SDK for Android を触る機会があったので、アレコレと試しています。
アプリに地図を組み込むのは面倒だと放置していましたが、凝ったことをしなければ組み込みは簡単ですし、マップの上に好きなオブジェクトを置いたり、それらにインタラクションをつけるのも手軽にできます。
そこで、ここでは以下についてまとめました。

  • 地図を組み込んだアクティビティの作成と起動
  • ジェスチャなど、地図についての設定
  • マーカーや図形など、マップオブジェクトの配置
  • マップオブジェクトをタップに反応させるなど、インタラクションの設定

なお、この記事は下記ページにある内容を元に書きました。Maps SDK を使う際には1度確認して頂ければと思います。

Maps SDK for Android Overview

環境

このページに書かれているコード等は以下の環境下で動作・検証しています。

  • macOS Mojave バージョン10.14.5
  • Android Studio 3.4.2
  • Pixel3a + Android 9

自分で実装してみたものは GitHub で公開しています。

MapImpl

APIキーの取得

Maps API を利用するためには、Google Cloud Platform Console にログインして API キーを取得し、それを Android Studio プロジェクトに設定する必要があります。キーを取得するための手順はこちらでご確認ください。


APIキーの取り扱いについて
この記事では取得したキーの用途に制限をかけず、アプリもデバッグモードでしか実行していません。リリース用のキーには、不正使用や割り当ての盗用を防止する措置を実施したうえで使用して下さい。


私が取得したときの手順
参考までに、現時点(2019/07)で私が取得したときの手順を記載しておきます。
ちなみに Google Cloud Platform 上に自分のアカウントがありますので、そのアカウントを使って作ったプロジェクトが存在する状態です。

1. Google Cloud Platform Console にアクセスします。
2. Platform 上の任意のプロジェクトを開きます。
3. コンソールの左ペインに"Googleマップ"がありますので、それをクリック。
4. "Maps SDK for Android" をクリック。
5. 『有効にする』ボタンをクリックすると、『APIを有効にしています』というメッセージが出るのでしばらく待ちます。
6. 有効になったら『認証情報』をクリック。
7. 『認証情報を作成』をクリックし、展開されるサブメニューから『APIキー』をクリック。
8. API キーが発行されるので、これをコピーしておきます。

アクティビティの作成と起動

地図が組み込まれたアクティビティを作成するには、Android Studio のメニューから作成するのが簡単です。
※直接 MapView をレイアウトに埋め込むこともできるはずですが、調べきれてません…。

1. アクティビティの作成

プロジェクトと一緒に作成する方法と、既存のプロジェクトに追加する方法があります。
ちなみに必要なライブラリは Android Studio が自動的に追加してくれます。作成後の build.gradle を確認してみてください。

1-1. プロジェクトと一緒に作成する

Android Studio の起動画面で "Start a new Android Studio Project" を選択し、『Create New Project』の画面で "Google Maps Activity" を選択します。
そのままプロジェクト名や Package Name などを入力してプロジェクトを作成すると、地図が組み込まれたアクティビティも一緒に作成されます。

1-2.既存のプロジェクトに追加する

Android Studio のメニューから "File" を選択するか、プロジェクトビューで右クリックすると表示されるコンテキストメニューで "New" > "Google" > "Google Maps Activity" をクリックします。
そのあとアクティビティ名などを任意に設定して『Finish』をクリックすれば地図が組み込まれたアクティビティが追加されます。

2. APIキーの設定

res/values に『google_maps_api.xml』というファイルができています。
『YOUR_KEY_HERE』の部分を Console で取得したAPIキーに変更して下さい。

<string name="google_maps_key" translatable="false" templateMergeStrategy="preserve">YOUR_KEY_HERE</string>

3. アクティビティの起動

作成したアクティビティを起動すると地図が表示されます。ちなみにシドニーを中心に表示されているのは、自動生成されたコードがそのようになっているためです。
map_01.jpg

地図自体の設定や操作

作成されたアクティビティには OnMapReadyCallback インターフェイスが実装されており、 onMapReady というメソッドが追加されています。このメソッドは、地図の準備ができたときに呼び出されるコールバックです。
地図関連の操作をするときは、基本的に onMapReady で取得できる GoogleMap オブジェクトの各メソッドを呼び出します。

表示位置の変更

作成されたアクティビティを実行すると、カメラ位置(=地図の中心)がシドニーになっています。これを別の場所に変更します。
GoogleMap#moveCamera メソッドで、カメラ位置や表示倍率を変更できます。
moveCamera の引数は CameraUpdate クラスです。これは CameraUpdateFactory クラスで生成できます。

CameraUpdateFactory のメソッド(一部)

メソッド 機能
newLatLng(LatLng latLng) 任意の緯度経度を中心に表示
newLatLngZoom(LatLng latLng, float zoom) 任意の緯度経度を中心に、任意の倍率で表示
zoomTo(float zoom) 現在の表示位置を任意の倍率で表示
newCameraPosition(CameraPosition cameraPosition) CameraPositionオブジェクトに設定した内容で表示

例えば、大阪駅を中心に16倍で表示したい場合は以下のようになります。

val osakaStation = LatLng(34.702423,135.495972)
moveCamera(CameraUpdateFactory.newLatLngZoom(osakaStation, 16.0f))

位置や倍率だけでなく仰角や回転角など、より詳細な表示位置を設定したい場合は newCameraPosition メソッドを使ってください。引数の CameraPosition は CameraPosition.Builderを使って生成できます。

地図の種類を変更

GoogleMap#setMapType(int type) で表示する地図の種類を変更することができます。type にセットする値は、GoogleMap クラスの定数5つから選べます。

setMapTypeの引数と表示結果

MAP_TYPE_NORMAL MAP_TYPE_SATELLITE MAP_TYPE_HYBRID MAP_TYPE_TERRAIN MAP_TYPE_NONE
通常 衛星写真 衛星+地図マーカー 地形図 なし
normal1.jpg satellite1.jpg hybrid1.jpg terrain1.jpg none1.jpg

地図表示に制限を設ける

GoogleMap クラスのメソッドで、表示可能な領域や倍率を制限することができます。

表示制限に関するメソッド(一部)

メソッド 機能
setLatLngBoundsForCameraTarget(LatLngBounds bounds) 指定した領域からカメラ位置が外に移動しないよう制限する
setMaxZoomPreference(float maxZoomPreference) 指定した倍率以上にズームインしないよう制限する
setMinZoomPreference(float minZoomPreference) 指定した倍率以下にズームアウトしないよう制限する

以下はズームを14倍〜16倍に制限し、特定の領域外にカメラが移動しないようにするコードです。

googleMap.apply {
    setMaxZoomPreference(16.0f)
    setMinZoomPreference(14.0f)
    setLatLngBoundsForCameraTarget(
        LatLngBounds(LatLng(34.695332 ,135.486831), LatLng(34.707469, 135.501508)))
}

余談ですが LatLngBounds をコンストラクタから生成する場合は、地図上の2点を指定して領域を定義します。このとき緯度経度の指定は「southwest, northeast」の順ですので注意してください(私はしばらく「northwest, southeast」と勝手に間違って悩みました)。

地図の操作に制限を設ける

GoogleMap#getUiSettings() メソッドで得られる UiSettings オブジェクトを操作することで、ジェスチャの有効/無効などを切り替えることができます。
UiSettings クラスには操作だけでなく、UI部品関連のメソッドも用意されていますので、APIを確認してみてください。

操作制限に関するメソッド(一部)

メソッド 機能
setScrollGesturesEnabled(boolean enabled) スクロール操作(1本指でドラッグ)の有効/無効を設定
setZoomGesturesEnabled(boolean enabled) ズーム操作(ピンチイン・アウト)の有効/無効を設定
setRotateGesturesEnabled(boolean enabled) 回転操作(2本指で回す)の有効/無効を設定
setTiltGesturesEnabled(boolean enabled) チルト操作(2本指で引き起こす)の有効/無効を設定
setAllGesturesEnabled(boolean enabled) 全てのジェスチャの有効/無効を設定

以下では、スクロール/ズーム/回転/チルトを全て無効にしています。 setAllGesturesEnabled を使えば一括で無効にできます。

// Kotlinなのでプロパティで操作
googleMap.uiSettings.run {
    isScrollGesturesEnabled = false
    isTiltGesturesEnabled = false
    isRotateGesturesEnabled = false
    isZoomGesturesEnabled = false
}

マップオブジェクト

マップオブジェクトは、地図上に描画できるマーカーや円、多角形などのことです。

種類

マップオブジェクトの種類は以下の通りです。

種類 クラス 描画用のメソッド クリックリスナ1
マーカー Marker addMarker(MarkerOptions options) OnMarkerClickListener
Circle addCircle(CircleOptions options) OnCircleClickListener
Polyline addPolyline(PolylineOptions options) OnPolylineClickListener
多角形 Polygon addPolygon(PolygonOptions options) OnPolygonClickListener
画像 GroundOverlay addGroundOverlay(GroundOverlayOptions options) OnGroundOverlayClickListener
タイル画像 TileOverlay addTileOverlay(TileOverlayOptions options) -

個人的に気になったのは、Circle を描画するときの半径がメートル指定ということです。
例えばランドマークに何らかのマークをつけたいときに Circle にしてしまうと、ズームするとその円が大きくなってしまいます。
何かの目印にしたい場合は、Circle でなく Marker をつける方がよさそうです。

 

マップオブジェクトを描画

これも GoogleMap オブジェクトを介して描画します。どのオブジェクトであれ、基本的に手順は以下の通りです。各オブジェクトを描画するためのオプションクラスについては、前項の表で描画用メソッドの引数を参照して下さい。

  1. 目的のマップオブジェクトに対応したオプションクラスのオブジェクトを生成
  2. 1.のオブジェクトに、描画する色やサイズなどのプロパティを設定
  3. GoogleMap の各セッタに、2.のオブジェクトをセット

例1:枠線が緑色&内部が黄色&半径20メートルの円を、大阪駅を中心として描画

googleMap.addCircle(
    CircleOptions()
        .center(LatLng(34.702423, 135.495972))
        .radius(20.0)
        .fillColor(Color.YELLOW)
        .strokeColor(Color.GREEN)
        .zIndex(1.0f)
)

例2:"ground_overlay" という Drawable 画像を、大阪駅を中心とした1辺300メートルの領域に描画

googleMap.addGroundOverlay(
    GroundOverlayOptions()
        .position(LatLng(34.702423, 135.495972), 300.0f)
        .image(BitmapDescriptorFactory.fromResource(R.drawable.ground_overlay))
        .zIndex(2.0f)
)

なお zIndex プロパティには、図形が重なるときにどちらを前面にするか指定します。
例1と例2を同じアクティビティの地図に描画すると Ground Overlay が前にくるので画像しか見えませんが、zIndex 値を逆にすると、円が画像に乗っかった形で描画されます。

マップオブジェクトにインタラクションを設定する

インタラクションを設定したいマップオブジェクトに対応するリスナを作成し、GoogleMap オブジェクトに設定します。リスナについては前項の表を参考にしてください。
クリックの対象とするオブジェクトには、オプションでclickable(true)をセットしないと反応しないことに注意してください。
また、Marker には Click 以外にもリスナがあります。APIドキュメントで確認してみてください。

以下の例では、描画した Circle をタップしたときに『Circle is Tapped!!』とトーストで表示しています。

googleMap.run {
    addCircle(
        CircleOptions()
            .center(LatLng(34.702423, 135.495972))
            .radius(20.0)
            .fillColor(Color.YELLOW)
            .strokeColor(Color.GREEN)
            .zIndex(2.0f)
            .clickable(true)
    )

    setOnCircleClickListener { circle ->
        Toast.makeText(this@MapsActivity, "Circle is tapped!!", Toast.LENGTH_SHORT).show()
    }
}

タップされたのが何か判別する

複数のマップオブジェクトを描画したとき、何がタップされたか判別する方法についてです。
マップオブジェクトは、生成された際に固有のIDを割り当てられます。GoogleMap#addXXXXX の戻り値は描画されたオブジェクトなので、このIDを保持しておいて、リスナの戻り値と照らし合わせるという手段があります。

以下の例では、2つの円のどちらかをタップしたときに、IDからタップされた方を判別してトーストを出力しています。

googleMap.run {
    val firstCircle = addCircle(
        CircleOptions().center(LatLng(34.702423, 135.495972)).clickable(true)
    )

    val secondCircle = addCircle(
        CircleOptions().center(LatLng(34.695332, 135.501508)).clickable(true)
    )

    setOnCircleClickListener { circle ->
        val toastText = when (circle.id) {
            firstCircle.id -> "First"
            secondCircle.id -> "Second"
            else -> "Unknown"
        }

        Toast.makeText(this@MapsActivity, "$toastText is tapped!!", Toast.LENGTH_SHORT).show()
    }
}

これ以外にも、マップオブジェクトは View と同じく setTag/getTag メソッドが用意されています。
GoogleMap#addXXXXX メソッドでマップオブジェクトを描画したあとに setTag で必要な情報を格納しておいて、クリックリスナで getTag を使うという手もよいかも知れません。 

まとめ

Maps SDK for Android を使ってアプリに地図を組み込む方法について書いてみました。
Maps SDK はここに書いた以外にも、様々な機能を備えています。以下の2点については特に便利だと感じました。

  • マップオブジェクトの Marker をカスタマイズする方法
  • Google Maps Android API Utility Library を使って、地図を更に拡張する方法

ただこれも書くと長くなりすぎるので、そのうち別の記事で書きたいと思います。


  1. 全て GoogleMap クラスのインターフェイスです。 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Start an Activity from a Notification (日本語訳)

※以下の文章は、Start an Activity from a Notificationの内容を理解するために翻訳したものです。


通知からアクティビティを開始するときは、ユーザーが期待するナビゲーション機能を維持する必要があります。戻るをタップすると、ユーザーはアプリの通常のワークフローからホーム画面に戻ります。最近使った画面を開くと、アクティビティが別のタスクとして表示されます。このナビゲーション体験を維持するためには、新しいタスクでアクティビティを開始する必要があります。

通知のタップ動作を設定する基本的な方法は通知の作成で説明されていますが、このページでは通知のアクションに対してPendingIntentを設定して新しいタスクとバックスタックを作成する方法について説明します。しかし、これをどのように行うかは、開始しているアクティビティの種類によって異なります。

通常のアクティビティ
これは、アプリの通常のUXフローの一部として存在するアクティビティです。そのため、ユーザーが通知からアクティビティに到着したときに、新しいタスクに完全なバックスタックを含める必要があり、戻るを押してアプリ階層を上に移動できるようにします。

特別なアクティビティ
ユーザーがこのアクティビティを通知から開始した場合にのみ表示されます。ある意味では、このアクティビティは通知自体に表示するのが難しい情報を提供することによって通知UIを拡張します。したがって、このアクティビティはバックスタックを必要としません。

通常のアクティビティのPendingIntentを設定する

通知から「通常のアクティビティ」を開始するには、TaskStackBuilderを使用してPendingIntentを設定し、次のように新しいバックスタックを作成します。

アプリのアクティビティ階層を定義する

アプリのマニフェストファイルの各<activity>要素にandroid:parentActivityName属性を追加して、アクティビティの自然な階層を定義します。

<activity
    android:name=".MainActivity"
    android:label="@string/app_name" >
    <intent-filter>
        <action android:name="android.intent.action.MAIN" />
        <category android:name="android.intent.category.LAUNCHER" />
    </intent-filter>
</activity>
<!-- MainActivityはResultActivityの親です -->
<activity
    android:name=".ResultActivity"
    android:parentActivityName=".MainActivity" />
    ...
</activity>

バックスタックでPendingIntentを構築する

バックスタックのアクティビティを含むアクティビティを開始するには、TaskStackBuilderのインスタンスを作成し、addNextIntentWithParentStack()を呼び出して、開始するアクティビティのIntentを渡す必要があります。

上記のように各アクティビティの親アクティビティを定義していれば、getPendingIntent()を呼び出してバックスタック全体を含むPendingIntentを受け取ることができます。

// 開始したいアクティビティのインテントを作成します。
Intent resultIntent = new Intent(this, ResultActivity.class);
// TaskStackBuilderを作成し、バックスタックを膨張させるIntentを追加します。
TaskStackBuilder stackBuilder = TaskStackBuilder.create(this);
stackBuilder.addNextIntentWithParentStack(resultIntent);
// バックスタック全体を含むPendingIntentを取得します。
PendingIntent resultPendingIntent =
        stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

必要に応じて、TaskStackBuilder.editIntentAt()を呼び出して、スタック内のIntentオブジェクトに引数を追加できます。これは、ユーザーがそれまでナビゲートしたときに、バックスタック内のアクティビティが意味のあるデータを表示するようにするために必要な場合があります。

それから、いつものようにPendingIntentを通知に渡すことができます。

NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID);
builder.setContentIntent(resultPendingIntent);
...
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(NOTIFICATION_ID, builder.build());

特別なアクティビティのPendingIntentを設定する

通知から開始される「特別なアクティビティ」はバックスタックを必要としないため、getActivity()を呼び出してPendingIntentを作成できますが、マニフェストで適切なタスクオプションを定義したことも確認する必要があります。

1.マニフェストで、<activity>要素に次の属性を追加します。

android:taskAffinity=""
コードで使用するFLAG_ACTIVITY_NEW_TASKフラグと組み合わせると、この属性を空白に設定することで、このアクティビティがアプリのデフォルトタスクに入らないようにすることができます。アプリのデフォルトのアフィニティを持つ既存のタスクは影響を受けません。

android:excludeFromRecents="true"
ユーザーが誤ってそのタスクに戻ることができないように、最近使ったアプリから新しいタスクを除外します。

例えば:

<activity
    android:name=".ResultActivity"
    android:launchMode="singleTask"
    android:taskAffinity=""
    android:excludeFromRecents="true">
</activity>

2.通知を作成して発行します。

a. Activityを開始するIntentを作成します。
b. フラグFLAG_ACTIVITY_NEW_TASKおよびFLAG_ACTIVITY_CLEAR_TASKを指定してsetFlags()を呼び出して、新しい空のタスクで開始するようにActivityを設定します。
c. getActivity()を呼び出してPendingIntentを作成します。

例えば:

Intent notifyIntent = new Intent(this, ResultActivity.class);
// 新しい空のタスクで開始するようにアクティビティを設定します。
notifyIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK
                    | Intent.FLAG_ACTIVITY_CLEAR_TASK);
// PendingIntentを作成します。
PendingIntent notifyPendingIntent = PendingIntent.getActivity(
        this, 0, notifyIntent, PendingIntent.FLAG_UPDATE_CURRENT
);

3.それから、いつものようにPendingIntentを通知に渡すことができます。

NotificationCompat.Builder builder = new NotificationCompat.Builder(this, CHANNEL_ID);
builder.setContentIntent(notifyPendingIntent);
...
NotificationManagerCompat notificationManager = NotificationManagerCompat.from(this);
notificationManager.notify(NOTIFICATION_ID, builder.build());

さまざまなタスクオプションとバックスタックの仕組みについての詳細は、タスクとバックスタックを読んでください。 通知を使用するサンプルコードについては、Android Notifications Sampleを参照してください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

今更だけど Android Instant Apps を試してみた (unity)

前提

Android Instant Appsってなんだろう?

  • アプリのサブセットをインストールせずに「今すぐ試す」ものです。
    • ストアに「今すぐ試す」ボタンが付きます。
    • 有料アプリの無料体験版にできます。
    • 「Install版がストアにない状態」=「Instant版だけ」では使えません。
  • アプリ中にインストールボタンを付けられるので、インストールの導線になります。
  • 「アプリを起動するリンク」を作ることもできます。
    • Webアプリのような感じで使えます。
    • ただし、端末側で許可設定が必要です。

※あくまでも私の理解です。

準備

  • 基本的には、上記のGoogleの記事に従います。
  • play-instant-unity-plugin をプロジェクトに導入します。
  • android studio に Google Play Instant Development SDK を導入します。

できること

  • ビルド自体は、公式記事通りに進めれば、容易にできます。
    • サイズ制限がある以外に違いはありませんし、テストする分にはサイズ制限もありません。
    • PlayerSettings以外は、何も変えずともビルドできます。
  • InstantとInstallを切り替えてビルドできます。
    • PLAY_INSTANTというシンボルを使って、条件コンパイルできます。
  • InstantAppを(実/仮想)デバイスに送って起動できます。
  • コンソールへアップロード可能なInstant版APKが作れます。
  • 容量制限がありますが、InstantからInstallへデータを引き継げます。(CookieApi)
  • アプリサイズ制限をオーバーしていても、テストトラックで試すことはできます。

できないこと

  • InstantとInstallでPlayerSettingsを切り替えてくれません。
    • Instantでは、いくつかのPlayerSettingsが強制されます。
    • プロジェクトやブランチを別にするなどの工夫が必要です。
  • エディタ環境では、InstantからInstallへのデータ引き継ぎができません。
    • try {CookieApiで保存/取得} catch {PlayerPrefsで保存/取得}として、エディタ上でシミュレートしました。
  • Android App Bundleをサポートしていません。
    • もちろん、InstantとInstallの混成aabを作ることもできません。
    • これのせいで、サイズ制限を超えられませんでした。

留意点

  • サイズを減らすために、複数のPlayerSettingsに対して更新が提示されます。
    • いくつかは強制のようで、更新しないとビルドできません。
      • 更新しなくてもビルドできるものもありました。
    • 推奨通りに更新されたものを元に戻したい場合は、PlayerSettingsで個別に直すことになります。
    • ビルドタイプをInstallに切り替えても、元に戻りません。
      • InstallとInstantで設定を別にしたい場合は、プロジェクトやブランチを別にすることになるでしょう。
  • Install版は、Instant版より大きなビルド番号でなければなりません。
    • ツール側で混成aabがサポートされれば、同じビルド番号でも良いものと思われます。
  • InstantからInstallへのデータ引き継ぎ(CookieApi)には、容量制限があります。
    • 保存可能なサイズはCookieApi.GetInstantAppCookieMaxSizeBytes ()で取得可能です。
  • テスト機のGoogle設定で、「バックアップ」⇒「アプリのデータ」の「自動復元」を切っておきましょう。
    • インストール直後にデータが復活するので、データの引き継ぎが阻害されます。

結論

  • aabの生成がサポートされるまでは厳しいです。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Kotlin】LocalDateTimeで2つの日付を比較し、日数の差分を取得する

はじめに

日付/時間を扱うAPIには

  • 日付のみを保持するLocalDate
  • 時刻のみを保持するLocalTime
  • 日付と時刻の両方を保持するLocalDateTime

があり、やりたいことに合わせて適切なAPIを使うわけですが、
日数の差分を取得するためには、LocalDateTimeで日付を比較する方法がスマートだと感じたので、ここにメモしておきます。

※なお、「日付」の比較ということで当初はLocalDatecompareToメソッドを使っていたのですが、下記記事に書いた通り、意図しない結果になることがありました。

【Kotlin】LocalDateで2つの日付を比較し、日数の差分を取得する - Qiita
【Android】LocalDate#compareToで日付を比較する際の注意点 - Qiita

コード

非常にシンプルで、ChronoUnit.DAYS.betweenを使うことで日数の差分が取得できます。
LocalDateTimeChronoUnitはThreeTenABPライブラリのものを使っています。)

val now = LocalDateTime.now() //2019-07-28T15:31:59.754
val day1 = LocalDateTime.parse("2019-08-28T10:15:30.123")
val diff = ChronoUnit.DAYS.between(now, day1) // diff: 30

ChronoUnitとは

ChronoUnitは日付、時間などを操作するための「単位」を集めた列挙型です。
1日の概念を表すDAYSの他にも、1時間の概念を表すHOURS、1マイクロ秒の概念を表すMICROS、1世紀の概念を表すCENTURIESなどなど、多くの単位が定義されています。

詳細:ChronoUnit (Java Platform SE 8)

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android端末にインストールされた認証局でサーバ証明書を検証する

目的

タイトル通りだが、方法を見つけるのに苦労したので記載しておく。
これを実装すると、自前のRoot認証局が署名したサーバ証明書でも検証してくれる。
自前のRoot認証局のインストール方法は、「設定」>「セキュリティ」>「SDカードからインストール」をタップし、証明書(PEM形式でよい)を選択する。インストールに成功すると、「設定」>「セキュリティ」>「信頼できる認証情報」の「ユーザー」タブに選択した証明書が表示される。

実装

fun connect() {
    // 端末にインストールされている「信頼できる認証情報」ストレージを取得する
    val caStore = KeyStore.getInstance("AndroidCAStore").apply {
        load(null)
    }
    // デバッグ用: 「信頼できる認証情報」をログ出力する
    caStore.aliases().toList().forEach { alias ->
        Log.d("----", "alias=${alias}, certificate=${caStore.getCertificate(alias)}")
    }
    // SSLハンドシェイク時に受信したサーバ証明書の署名者を、検証してくれるTrustManagerを生成するオブジェクトを作成する
    val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()).apply {
        init(caStore)
    }
    // 上で作成したTrustManagerを使う SSLContext を作成する
    val context: SSLContext = SSLContext.getInstance("TLS").apply {
        init(null, trustManagerFactory.trustManagers, null)
    }
    // HTTPS 通信を行う
    val url = URL("https://example.com/index.html")
    val urlConnection = (url.openConnection() as HttpsURLConnection).apply {
        sslSocketFactory = context.socketFactory
    }
    // デバッグ用: 通信結果をログ出力する
    val inputStream = urlConnection.inputStream
    Log.d("----", InputStreamReader(inputStream).readText())
}

注意点

端末にRoot認証局をインストールし、サーバ証明書を中間認証局(Root認証局で署名)で署名しているにもかかわらず、検証に失敗する場合がある。その時は、下記のように自前のX509TrustManagerを実装し、サーバから受信する証明書チェーンを確認する。

fun connect() {
    val context: SSLContext = SSLContext.getInstance("TLS").apply {
        //        init(null, trustManagerFactory.trustManagers, null)
        init(
            null,
            arrayOf(object : X509TrustManager {
                override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) {
                }

                override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) {
                    // デバッグ用: サーバから受信した証明書チェーンをログ出力する
                    Log.d("----", "chain=${Arrays.toString(chain)}")
                }

                override fun getAcceptedIssuers(): Array<X509Certificate> {
                    return emptyArray()
                }
            }),
            null
        )
    }
    // HTTPS 通信を行う
    val url = URL("https://example.com/index.html")
    val urlConnection = (url.openConnection() as HttpsURLConnection).apply {
        sslSocketFactory = context.socketFactory
    }
    // デバッグ用: 通信結果をログ出力する
    val inputStream = urlConnection.inputStream
    Log.d("----", InputStreamReader(inputStream).readText())
}
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidのWebViewでAutofillを無効化する

はじめに

自前のWebViewを実装するときにHTMLのフォーム入力でAutofill表示しないようにしたかったときのメモ。
image.png

検証環境
* Pixel3
* Android 9

解決策

WebViewの importantForAutofillIMPORTANT_FOR_AUTOFILL_NOを指定する(Android O / API 26以上の場合のみ)。

        webView.apply {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                importantForAutofill = View.IMPORTANT_FOR_AUTOFILL_NO
            }
        }

image.png

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】LocalDate#compareToで日付を比較する際の注意点

以前、下の記事で、日付を比較する方法としてLocalDate#compareToを挙げました。
【Kotlin】日付を比較し、その差分を取得する - Qiita
しかし、日数の差分を取得する目的で利用しようとすると、場合によっては期待結果と異なることわかったので備忘録として書いておきます。

LocalDate#compareToの中身を確認する

実際に比較が走っている箇所はこちらです↓

LocalDate
int compareTo0(LocalDate otherDate) {
    int cmp = this.year - otherDate.year;
    if (cmp == 0) {
        cmp = this.month - otherDate.month;
        if (cmp == 0) {
            cmp = this.day - otherDate.day;
        }
    }

    return cmp;
}
  • まずyearを比較する
  • その差がなければmonthを比較する
  • さらに差がなければdayを比較する

という流れになっています。
つまり、日数の差分を比較・取得できるのは年数・月数が同じ場合に限定され、それ以外の場合は意図した結果とならないことがわかります。

実際に比較してみる

同じ年、同じ月、違う日の場合

val day1 = LocalDate.parse("2019-07-01")
val day2 = LocalDate.parse("2019-07-05")
val diffDay = day2.compareTo(day1)  // diffDay: 4

「日数の差分」が取得できます。

同じ年、違う月、違う日の場合

val day1 = LocalDate.parse("2019-07-01")
val day2 = LocalDate.parse("2019-08-05")
val diffMonth = day2.compareTo(day1)  // diffMonth: 1

→月の比較が行われるため、「日数の差分」は取得できません。

違う年、違う月、違う日の場合

val day1 = LocalDate.parse("2017-07-01")
val day2 = LocalDate.parse("2019-08-05")
val diffYear = day2.compareTo(day1)  // diffYear: 2

→年の比較が行われるため、「日数の差分」は取得できません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Programming News】Qiitaまとめ記事 Weekly July 4rd week, 2019 Vol.2

筆者が昨日2019/7/21(日)~7/27(土)に気になったQiitaの記事のまとめのまとめを作成しました。日々のまとめ記事のWeekly版です。

皆様が興味のある言語や思いもよらぬハック方法をこの中から見つけられたら幸いです。

Java

Python

Ruby

Rails

C

Android

Swift

Kotlin

Flutter

Fulx

JavaScript

React

Node.js

Vue.js

Vuex

Nuxt.js

Nest.js

Angular

jQuery

TypeScript

ReactNative

Laravel

C

PHP

CakePHP

Rust

Go言語

R言語

Scala

Unity

A-Frame

PowerApp

Line

HTML

CSS

Sass

SQL

MySQL

PostgreSQL

Oracle

MongoDB

SQL Server

ビッグデータ

Visual Studio Code

IntelliJ IDEA

AI

IoC

Git

AWS

Azure

Oracle Cloud

IBM Clod

Active Directory

インフラ

ブロックチェーン

Ethereum

セキュリティ

機械学習

Network

RPA

CI

Docker

Heroku

VirtualBox

kubernetes

OpenID

OAuth2.0

Elasticsearch

Linux

Cent OS

Windows

Google API

Google Apps Script

Google Cloud Platform

Google Colaboratory

Google Drive

Firebase

Server Side

CSS

BootStrap

WordPress

Develop

PowerShell

Vim

awk

LaTex

Redmine

UML

Raspberry

RPA

IoT

Alexa

Line

SharePoint

VBA

ShellScript

Nim

Emacs

WPF

UI

Ansible

Arduino

Julia

Coral

ionic

QRCode

OCR

資格

転職

更新情報

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

AndroidのマルチモジュールでLint結果をDangerでコメントする

Androidアプリ開発をマルチモジュールですすめていた場合、Lintを実行すると各モジュールのbuildフォルダ配下にlintチェック結果が生成されてしまうため、Danger等でプルリクにコメントしていた場合にファイルを1つしか選択できず、特定のモジュールのみのコメントになってしまいます。そのため、各モジュールの結果を1つのファイルにマージしてあげてからDangerにそのファイルを指定してあげる ことで解決させます。

詳しい説明やBitriseでの設定などを ブログ のほうに記載したので、こちらでは方法のみを記載します。

lintの設定

各モジュールで以下のlint設定を行い、lint結果のファイルがプロジェクトのルートの build-reports というフォルダに集約されるようにします。

build.gradle
android {
    lintOptions {
        xmlReport true
        xmlOutput rootProject.file("./build-reports/lint-results-${project.getDisplayName()}.xml")
    }
}

lintチェック結果のXMLファイルをマージする

Lint実行後に、生成されたXMLファイルを全てマージした新しいXMLファイルを生成します。そのために、以下のRubyスクリプトをプロジェクトのルートで実行します。実行にはnokogiriが必要なのでインストールしておきます。

gem install nokogiri
merge_lint_results.rb
require 'nokogiri'

Dir.chdir('build-reports')

new_doc = nil

Dir::glob('lint-results-*.xml') do |item|
  file = File.new(item)

  if new_doc.nil?
    new_doc = Nokogiri.XML(file)
  else
    doc = Nokogiri.XML(file)
    issues = doc.search('issue')
    new_doc.at('issues').add_child(issues)
  end
end

File.open('lint-results.xml', 'w') do |file|
  unless new_doc.nil?
    file.puts(new_doc.to_xml(indent: 4))
  end
end
ruby ./merge_lint_results.rb

すると build-reportslint-results.xml というファイルが生成されます。

Dangerでファイルを指定する

あとはDangerfileで生成された lint-results.xml を指定してあげます。

android_lint.skip_gradle_task = true
android_lint.filtering = false
android_lint.report_file = "build-reports/lint-results.xml"
android_lint.lint(inline_mode: true)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Programming News】Qiitaまとめ記事 July 27, 2019 Vol.13

筆者が2019/7/27(土)に気になったQiitaの記事をまとめました。昨日のまとめ記事はこちら

2019/7/15(月)~2019/7/20(土)のWeeklyのまとめのまとめ記事もこちらで公開しております。

Java

Python

Ruby

Rails

JavaScript

Node.js

Vue.js

Android

Swift

PHP

A-Frame

Line

MySQL

Azure

AWS

Firebase

TypeScript

Google Apps Script

Go言語

Rust

Julia

ShellScript

Unity

Docker

Develop

Raspberry

Heroku

OAuth2.0

Visual Studio Code

IntelliJ IDEA

更新情報

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Navigationを実際に使ってみた時のまとめ

Navigationとは?

公式ページ

画面遷移に関する事をいい感じに実装できる優れもの
以下の3つのコンポーネントを理解する事が重要 :sparkles:

  • Navigation graph
    • 画面遷移を1つのXMLファイルで集中的に管理
  • NavHost
    • ナビゲーショングラフから目的地を表示する空のコンテナ
  • NavController
    • ナビゲーション管理

Navigationを使う事の利点

  • Fragmentトランザクションの管理
  • Up, Backイベント処理
  • アニメーションとトランジションのための標準化されたリソースを提供
  • DeepLinkの処理
  • 最小限の追加作業で、ナビゲーションパネルや下部ナビゲーションなどのナビゲーションUIパターンを含めることができる
  • Safe Args
  • ViewModelのサポート

Navigation Editor

AndroidStudio 3.3以上の環境でナビゲーションをGUIで操作できる

:computer:環境構築


新しく NavigationSample としてプロジェクトを作成し
dependenciesはNavigationとSafeArgsをインストールします。

rootのbuild.gradle

    repositories {
        google()
    }
    dependencies {
        // SafeArgs
        def nav_version = "2.1.0-alpha06"
        classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version"
    }

moduleの build.gradle

apply plugin: "androidx.navigation.safeargs.kotlin"

dependencies {
    // Navigation
    def navi_version = "2.1.0-alpha06"
    implementation "androidx.navigation:navigation-fragment-ktx:$navi_version"
    implementation "androidx.navigation:navigation-ui-ktx:$navi_version"
}

公式ドキュメント

:pencil: 実装


Navigation Graph の作成

AndroidStudioでリソース作成時にリソースタイプを Navigation に設定し、
ファイル名を入力して作成
ファイル名はGet started通りに nav_graph.xml で作成

nav1.png

2つのフラグメントの作成

  • MainFragment
class MainFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_main, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        buttonToSecond.setOnClickListener {
            Navigation.findNavController(it).navigate(R.id.action_mainFragment_to_secondFragment)
        }
    }
}
  • SecondFragment
class SecondFragment : Fragment() {

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_second, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        buttonToMain.setOnClickListener {
            Navigation.findNavController(it).navigate(R.id.action_secondFragment_to_mainFragment)
        }
    }
}

※ Navigation.findNavController... は以下の様にも書ける

buttonToSub.setOnClickListener(Navigation.createNavigateOnClickListener(R.id.action_xxx, null))

Navigation Editor での設定

「New Destination」をクリックすると既に存在しているFragmentやActivityが
追加できる。またこの画面から新規にFragment等を作成もできる。

nav2.png

そして上記の2つのフラグメントを繋ぐように設定した navigation graphがこちら

<?xml version="1.0" encoding="utf-8"?>
<navigation xmlns:android="http://schemas.android.com/apk/res/android"
            xmlns:app="http://schemas.android.com/apk/res-auto"
            xmlns:tools="http://schemas.android.com/tools" android:id="@+id/nav_graph"
            app:startDestination="@id/mainFragment">

    <fragment android:id="@+id/mainFragment" android:name="slowhand.com.navigationsample.MainFragment"
              android:label="fragment_main" tools:layout="@layout/fragment_main">
        <action android:id="@+id/action_mainFragment_to_secondFragment" app:destination="@id/secondFragment"/>
    </fragment>
    <fragment android:id="@+id/secondFragment" android:name="slowhand.com.navigationsample.SecondFragment"
              android:label="fragment_second" tools:layout="@layout/fragment_second">
        <action android:id="@+id/action_secondFragment_to_mainFragment" app:destination="@id/mainFragment"/>
    </fragment>
</navigation>

このようになりました。

NavHostFragmentの指定

MainActivityのレイアウトファイルで、NavHostFragmentを追加しています。

<?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"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">

    <fragment
            android:id="@+id/nav_host_fragment"
            android:name="androidx.navigation.fragment.NavHostFragment"
            android:layout_width="0dp"
            android:layout_height="0dp"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:defaultNavHost="true"
            app:navGraph="@navigation/nav_graph"/>

</androidx.constraintlayout.widget.ConstraintLayout>

app:defaultNavHost="true" 端末の戻るボタンを制御する。
またデフォルトにできるのは通常1つのNavHostのみになります。

別Activityへの遷移

NavigationEditorのNew DestinationでActivityも追加できるので、
Activityを追加し、Fragmentからactionを追加してやる :sparkles:


Safe args

ドキュメント

Navigation Graphにargumentを追加

    <fragment android:id="@+id/secondFragment" android:name="slowhand.com.navigationsample.SecondFragment"
              android:label="fragment_second" tools:layout="@layout/fragment_second">
        <action android:id="@+id/action_secondFragment_to_mainFragment" app:destination="@id/mainFragment"/>
        <!-- こちら -->
        <argument android:name="text"
                  android:defaultValue="default"
                  app:argType="string" />
    </fragment>

argumentを渡す側

        buttonToSecond.setOnClickListener {
//            ↓の様にも書ける
//            val bundle = bundleOf("text" to "hello")
//            Navigation.findNavController(it).navigate(R.id.action_mainFragment_to_secondFragment, bundle)

            Navigation.findNavController(it).navigate(
                MainFragmentDirections.actionMainFragmentToSecondFragment(text = "hello"))
        }

argumentを受け取る側

textView.text = SecondFragmentArgs.fromBundle(arguments ?: return).text

XXXXFragmentDirectionsXXXXXFragmentArgs が自動生成される :sparkles:

DialogFragment

ダイアログのNavigationを行う方法です。
先に表示先のダイアログを作成します。

class SimpleDialogFragment : DialogFragment() {

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {

        return AlertDialog.Builder(requireActivity())
            .setTitle("")
            .setMessage("")
            .setPositiveButton("OK") { _, which ->
                ...
            }
            .create()
    }
}

Navigation Editorの「New Destination」から作成したDialogFragmentを追加します。
次にDialogに渡すパラメータを追加して行きます。

nav3.png

一回ビルドしなおすとパラメータをSafeArgsで受け取れるようになります。

val args = arguments?.let { SimpleDialogFragmentArgs.fromBundle(it) }

実際に使う場合は遷移元のFragmentをMainFragmentとすると

            Navigation.findNavController(it).navigate(
                MainFragmentDirections.actionMainFragmentToSimpleDialogFragment(arg = 0))

こんな感じで書けます :sparkles:

:bomb: バッドノウハウ


java.lang.IllegalArgumentException: navigation destination xxxxx.dev:id/action_xxxFragment_to_xxxDialogFragment is unknown to this NavController

こんなエラーが出た場合はDestinationの繋ぎ方を見直して間違ってないか要確認。


java.lang.IllegalStateException: View 
xxxx does not have a NavController set

DialogからDialog、またはFragmenntへ遷移しようとしていた為。
どうもDialogから他のFragmentへは遷移できなさそうでした。
stackoverflow
googleissue

:link: 参考URL


  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Programming News】Qiitaまとめ記事 July 26, 2019 Vol.12

遅くなりましたが筆者が2019/7/26(金)に気になったQiitaの記事をまとめました。昨日のまとめ記事はこちら

2019/7/15(月)~2019/7/20(土)のWeeklyのまとめのまとめ記事もこちらで公開しております。

Python

JavaScript

Ruby

Android

Swift

Kotlin

Rails

React

Nuxt.js

Laravel

Sass

PHP

MySQL

Oracle

AWS

Docker

Visual Studio

IBM Cloud

Unity

TypeScript

Raspberry

Julia

Git

Vim

VirtualBox

更新情報

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む