20210906のAndroidに関する記事は3件です。

LocationManagerでAbstractMethodErrorが発生する件

現在、Androidで位置情報を取得する方法としては、FusedLocationProviderClientを使用するのが一般的かと思いますが、一部のGMSの使えない環境も考慮してLocationManagerを使用する場合もあるかと思います。このLocationManagerでAbstractMethodErrorが発生するという現象に遭遇したので書いておきます。 何が起こるのか パーミッションの取得とか諸々置いておいて、位置情報を取得しようとするとLocationManagerのインスタンスを取得して、requestLocationUpdatesにパラメータ付きでLocationListenerを登録します。 位置情報が取得できればonLocationChangedがコールされるので、ここでlocationを使ってごにょごにょします。 val manager: LocationManager = getSystemService()!! manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1, 1f, object: LocationListener { override fun onLocationChanged(location: Location) { // locationの利用 } }) 最後のリスナーはラムダにすることもできます。(AndroidStudioではサジェストが出ますね) val manager: LocationManager = getSystemService()!! manager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 1, 1f) { } はい、Android Studioの補完機能を使ってなんの問題も無く実装できますし、なんのエラーも出ません。 では実行しましょう。 E/AndroidRuntime: FATAL EXCEPTION: main Process: com.android.myapplication, PID: 3391 java.lang.AbstractMethodError: abstract method "void android.location.LocationListener.onStatusChanged(java.lang.String, int, android.os.Bundle)" at android.location.LocationManager$ListenerTransport._handleMessage(LocationManager.java:304) at android.location.LocationManager$ListenerTransport.-wrap0(LocationManager.java) at android.location.LocationManager$ListenerTransport$1.handleMessage(LocationManager.java:242) at android.os.Handler.dispatchMessage(Handler.java:102) at android.os.Looper.loop(Looper.java:154) at android.app.ActivityThread.main(ActivityThread.java:6077) at java.lang.reflect.Method.invoke(Native Method) at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866) at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756) oh... どういうことか? LocationListnerの実装を見てみましょう。(コメントなどは省略) LocationListener.java public interface LocationListener { void onLocationChanged(@NonNull Location location); @Deprecated default void onStatusChanged(String provider, int status, Bundle extras) {} default void onProviderEnabled(@NonNull String provider) {} default void onProviderDisabled(@NonNull String provider) {} } ラムダ化できるのでメソッド一つの関数型インターフェースのように見えますが、4つのメソッドがあり、onLocationChanged以外はdefaultで空実装されています。 しかし、defaultが使えるのはJava8以降ですね。ちょっとコードを遡ってみましょう。 (実は前述のエラーを発生させたのはAndroid7の環境です) Android 10と11の変化 といってもそれほど遡る必要は無かったですね。Android10の時点のソースコードを見てみます。以下のようにdefaultはついていません。 LocationListener.java public interface LocationListener { void onLocationChanged(Location location); @Deprecated void onStatusChanged(String provider, int status, Bundle extras); void onProviderEnabled(String provider); void onProviderDisabled(String provider); } Android11でdefaultが追加されていますね。 interfaceにdefaultがあるので、onLocationChanged以外の実装を行わなくてもコンパイルが通りました。しかし、LocationListenerは実行環境が提供するクラスです。つまり、Andorid10以下では、実行環境側のクラスにdefaultの実装が存在しないため、AbstructMethodのままになっていて、そのメソッドがコールされてしまったために、AbstractMethodErrorが発生したというわけですね。 特にonStatusChangedはAndroid10からdeprecatedになっていて、default実装が提供されているので、もともと実装されていても、警告の修正などの際につい削除してしまいかねないメソッドですね。 まとめ 実行環境提供のインターフェースに途中からdefault実装が追加されてしまったために、メソッドをすべて実装しなくともコンパイルが通るようになってしまったが、下位バージョンでの実行時はdefault実装が存在しないために問題が発生してしまっていました。 通常利用しないメソッドをdefaultで空実装しておくのは良いことかもしれませんが、実行環境のクラスでそれをやられると、Androidの用に複数バージョンでの動作をさせないといけない場合、問題が起こってしまいますね。 正直この変更は失敗で、API/interfaceを新設するなりJetpack等の外部ライブラリで対応すべき修正だったと思います。 Androidのシステムクラスを使う場合にdefault実装を見かけたら注意しましょう。ぐらいしか言いようがありませんが、このような変更が他にないことを祈るばかりです。 以上です。
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【初心者向け】ディープリンク技術の概要と注意点

チーム内で「ディープリンク」「ダイナミックリンク」「ユニバーサルリンク」等の用語の認識が合っているのか怪しかったので、整理してみました。 基本iOS目線で書いてますが、実装寄りではなく概念寄りの内容なので、Android開発者でも読めると思います。 私の認識違いなどがありましたら、コメント欄にてご教示いただけますと幸いです。 ※この記事ではURL/URIの区別を厳密に意識せず、より馴染み深い"URL"に統一して記述しています。 ディープリンク (Deep Link) とは 任意のアプリの任意の画面に遷移させるURL、またはそれを扱う仕組みの総称であり、概念を表す用語です。 スマホアプリで代表的なユースケースは以下のとおりです。 スマホのブラウザでWebコンテンツ内のリンクをtapした際に、AアプリのX画面に遷移させる。 Aアプリのボタンをtapした際に、BアプリのY画面に遷移させる。 ディープリンクを実現する仕組み カスタムURLスキーム URLスキームとは、URLの“://”より前の部分で、リソースにアクセスするための手段を示します。 http/https, ftp, mailto などなどが既定のURLスキームです。 アプリ独自で決めたURLスキームをカスタムURLスキームと呼びます。myapp://hoge/fuga のようなURLになります。 アプリでの設定/実装によって、カスタムURLスキームによるディープリンクを実現できます。 iOS/Androidでの実装方法については素晴らしい記事が他にたくさんあるので、他に譲ります。 カスタムURLスキームの弱点と注意点 「アプリがインストールされていない場合に○○する」という要件を満たすことが困難です。 誰かが管理しているわけではないので、他のアプリとスキームが競合する可能性があります。 端末内に同じカスタムURLスキームを持つアプリが複数インストールされている場合、 Androidの場合、どのアプリを起動するかを選択するためのダイアログが表示されます。 iOSの場合、バージョンによって違いがあるようですが先/後にインストールしたアプリが勝手に起動されます。 したがって「ユーザーが気づかないうちに意図に反した挙動をしてしまう可能性がある」という弱点があり、それを狙った攻撃を「URLスキーム・ハイジャック」と呼びます。 URLスキーム・ハイジャックの事例解説記事: iOS ユニバーサルリンク (Universal Links) 見た目は"https"で始まる単なるWebページのようなURLです。 iOS端末でユニバーサルリンクにアクセスすると、ターゲットのアプリがインストールされている場合は起動しますし、インストールされていない場合は任意のWebページを表示してくれます。 この機能を使う前提作業として、関連づけファイル (apple-app-site-association/AASA) をWebサーバーに配置します。 「このドメイン・パスのURLリンクはこのアプリと関連づきますよ」という定義をするためのファイルです。 したがってAASAは、独自ドメインの、自前のWebサーバーに配置する必要があり、AASAにはアプリを一意にするBundle IDなどを設定します。 その他にアプリ側の対応(プロビジョニング・プロファイルとXcodeの設定、ハンドリングの実装)も必要です。 以上の設定と実装によって、iOS端末と、AASAを配置したWebサーバーとの間で内部的に連携して、URLリンクにアクセスした際に連携するべきアプリが決定されます。 公式ドキュメント: 参考リンク: Android アプリリンク (App Links) ユニバーサルリンクと同様の概念・機能ですので詳細は割愛します。 公式ドキュメント: Firebase Dynamic Links ユニバーサルリンク/アプリリンクは、カスタムURLスキームの弱点を解消した強力な仕組みではありますが、「自前のドメインとWebサーバーを用意し、OSごとに異なる"仕込み"を行う必要がある」というデメリットがあります。 Firebase DynamicLinksはその手間を解消することを主目的とし、 関連づけファイルを配置するWebサーバーをホスティングしてくれる Dynamic Links既定の https://{任意のサブドメイン}.page.link というドメインで使える、または独自ドメインも指定可能 関連づけファイルの設定をコンソール上から行える というサービスです。 さらに「どこから流入してアプリをインストールしたか」といった解析サービスも付いています。 公式ドキュメント:
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Android】画像を選択して、ImageViewに表示 in Kotlin

➊ はじめに 「画像を選択して、ImageViewに表示する方法」として「startActivityForResult と onActivityResult」で実装していたのですが、Androidは日進月歩しているらしく、この方法で実装するのは非推奨になってしまいました。 基盤となる startActivityForResult() API と onActivityResult() API はあらゆる API レベルの Activity クラスで使用できますが、AndroidX Activity と Fragment で導入された Activity Result API を使用することを強くおすすめします。 とのことなので、推奨方法をここ?「アクティビティの結果を取得する」で調べつつ、実装してみたいと思います。 ➋ どんな感じ? 百聞は一見にしかずということで、こんな感じ~になります。 「SELECT IMAGE」ボタンを押下すると、カメラで撮った画像を選択するアクティビティに切り替わります。画像を選択すると、選択した画像がアプリのImageViewに表示されます。 ➌ お勉強ポイント ギャラリーアクティビティの起動 アクティビティ結果の受け取り ※推奨されている「AndroidX Activity と Fragment で導入された Activity Result API」を使用。 ➍ アクティビティの起動と結果取得 (1) アクティビティの結果に対するコールバックの登録 アクティビティ結果のコールバックを登録するため、registerForActivityResult()を用います。 StartActivityForResultコントラクトは、任意のIntentを入力として受け取ってActivityResultを返す汎用コントラクトです。このコントラクトを使用すると、コールバックの一部としてresultCodeとIntentを抽出できます。 snippet // アクティビティの結果に対するコールバックの登録 private val launcher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> Log.d("registerForActivityResult(result)", result.toString()) if (result.resultCode != RESULT_OK) { // アクティビティ結果NG } else { // アクティビティ結果OK // ・・・ } } ※アクティビティの結果関連の詳細ドキュメントは、こちら?「アクティビティの結果を取得する」をご参照ください。 (2) アクティビティの起動 launch()でアクティビティ(画像選択画面)を起動します。 snippet val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "image/*" } launcher.launch(intent) ※インテント関連の詳細ドキュメントは、こちら?「ファイル ストレージ」をご参照ください。 ➎ 実装 仕組みが分かったところで、上記をまとめ、諸々実装していきます。 言語は、「Kotlin」で実装します。 (1) activity_main.xml app/src/main/res/layout/activity_main.xml UIはこんな感じになります。 コードはこちら。 activity_main.xml <?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout 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:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <ImageView android:id="@+id/imageView" android:layout_width="409dp" android:layout_height="260dp" android:contentDescription="image view" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.161" /> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Select Image" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintHorizontal_bias="0.498" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintVertical_bias="0.814" /> </androidx.constraintlayout.widget.ConstraintLayout> (2) MainActivity.kt app/src/main/java/com/poodlemaster/app4/MainActivity.kt MainActivity.kt package com.poodlemaster.app4 import android.content.Intent import android.graphics.BitmapFactory import androidx.appcompat.app.AppCompatActivity import android.net.Uri import android.os.Bundle import android.util.Log import android.view.View import android.widget.Toast import android.widget.Button import android.widget.ImageView import androidx.activity.result.contract.ActivityResultContracts class MainActivity : AppCompatActivity(), View.OnClickListener { //------------------------------------------------------------------------- override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) Log.d("status", "onCreate") setContentView(R.layout.activity_main) val button: Button = findViewById(R.id.button) button.setOnClickListener(this) } //------------------------------------------------------------------------- override fun onClick(view: View){ when (view.id) { R.id.button -> { selectPhoto() } } } //------------------------------------------------------------------------- private val launcher = registerForActivityResult( ActivityResultContracts.StartActivityForResult() ) { result -> Log.d("registerForActivityResult(result)", result.toString()) if (result.resultCode != RESULT_OK) { return@registerForActivityResult } else { try { result.data?.data?.also { uri : Uri -> val inputStream = contentResolver?.openInputStream(uri) val image = BitmapFactory.decodeStream(inputStream) val imageView: ImageView = findViewById(R.id.imageView) imageView.setImageBitmap(image) } } catch (e: Exception) { Toast.makeText(this, "エラーが発生しました", Toast.LENGTH_LONG).show() } } } //------------------------------------------------------------------------- private fun selectPhoto() { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type = "image/*" } launcher.launch(intent) } } ➏ 以上 最近、Androidアプリの基礎を勉強してみようと、重い腰を上げようやく「Android Studio」をインストールしました。kotlinという言語も初めて使いました。JAVAも触ったことなかったので、まずは見様見真似と自己流改造で色々と学べたらと思っています。 お疲れ様でした?
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む