カテゴリー

Androidアプリ

【Android】AppWidget の ListView で、setOnClickFillInIntent で指定した Intent が反映されないときは

2021/12/28・Androidアプリ

問題発生 Androidアプリ開発中。AppWidgetにListViewを実装しようと思い、クリック時に開くページをsetOnClickFillInIntentで設定しようとしたが、何故か RemoteViews$setPendingIntentTemplate で設定したPendingIntentのテンプレートが setOnClickFillInIntent の Intent で上書きされない。 setPendingIntentTemplate(R.id.listView, PendingIntent.getActivity(context, UUID.randomUUID().hashCode(), Intent(context, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)) setOnClickFillInIntent(R.id.listItemLayout, Intent() .putExtra(MainActivity.EXTRA, list[position]["id"] as Int)) Result intent.extras -> Bundle[{}] 解決方法 Android 12以降をターゲットにする際、PendingIntentをImmutableにするかMutableにするかの指定が必須になった。何も考えず全部Immutableにするとこういうことになる。 setOnClickFillInIntentは実質的にPendingIntentの書き換えをしているため、PendingIntent.FLAG_IMMUTABLEが指定されていると追加のIntentが完全に無視される。そのため、Android 12以降ではPendingIntent.FLAG_MUTABLEの指定、Android 11以前では無指定にする必要がある。地味にハマった。 val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT else PendingIntent.FLAG_UPDATE_CURRENT setPendingIntentTemplate(R.id.listView, PendingIntent.getActivity(context, UUID.randomUUID().hashCode(), Intent(context, MainActivity::class.java), flags))

【Flutter】【Firebase Auth】Twitterログイン失敗でハマった & Twitterログインの裏で何が行われているのか。

2021/12/05・Androidアプリ

ハマった。 FlutterのFirebase AuthenticationでTwitterログインを実装中、何度やっても下のようなエラーが出てログインできなかった。 The supplied auth credential is malformed or has expired. [ Failed to fetch resource from https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true, http status: 401, http response: {"errors":[{"code":32,"message":"Could not authenticate you."}]} ] これを解決するため、利用しているOAuthライブラリのバグかと思い、OAuthの仕組みから全部勉強してゼロから実装してみました が、直らず。 さらにAPIキーとシークレットの再生成も試したが、ダメ。 初歩的な問題だった。 初めてエラーが出て 2週間後、ふと思いつきました。 「あれ、Firebase Consoleで設定したAPIキー、更新してなくね」 ここでOAuthの仕組みの勉強が役に立ったわけですが、サインイン時にアクセストークンとシークレットを渡してsignInWithCredentialを呼ぶと、Twitterのverify_token APIが呼ばれている様子です。 なんとここには、アクセストークン・アクセストークンシークレットだけでなく、APIキー・APIシークレット もAPIを叩くのに必要でした。 つまり、TwitterLoginに渡すAPIキー・APIシークレットと同じものをFirebase Consoleに設定しないと、この段階でAPIキー・APIシークレットが間違っているためログインに失敗します。 つまり、クライアントとFBサーバーの2箇所で持たせる必要があります。 誰かのお役に立てば幸いです。 おまけ ここからはOAuthの勉強で知った、Twitterログインの裏で何が行われているのかを説明してみます。 1. リクエストを作成する まずはOAuthのリクエストトークンを取得するリクエスト内容を作成します。具体的には、HTTPメソッド、APIのエンドポイント(簡単に言えばクエリーパラメータを除いたURL)、クエリーパラメータを全部くっつけて1つの文字列にしたものです。 APIエンドポイントは「https://api.twitter.com/oauth/request_token」です。 必要なのは6つ。 ①コールバックURI。サインイン後、どこに戻ってくるかのURL。 ②APIキー ③ナンス(ランダムな文字列。生成方法は任意) ④署名方法「HMAC-SHA1」 ⑤タイムスタンプ (現在時刻) ⑥OAuthバージョン「1.0」 クエリーパラメータはそれぞれの値をパーセントエンコードし、以下の例のように結合します。 oauth_callback=submon%3A%2F%2F&oauth_consumer_key={API_KEY}&oauth_nonce=abcdefg123456&oauth_signature_method=HMAC-SHA1&oauth_timestamp=1638708010&oauth_version=1.0 ※{API_KEY}の部分はAPIキー 次に、APIのエンドポイントとクエリーパラメータをそれぞれURLエンコード(パーセントエンコード)します。そして、それらを「&」を挟んでくっつけます。以下が例になります。これを①とします。 POST&https%3A%2F%2Fapi.twitter.com%2Foauth%2Frequest_token&oauth_callback%3Dsubmon%253A%252F%252F%26oauth_consumer_key%3D%7BCONSUMER_KEY%7D%26oauth_nonce%3Dabcdefg123456%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3D1638708010%26oauth_version%3D1.0 次に、APIシークレットとアクセストークンシークレットを「&」で結合した文字列を作成します。この段階でアクセストークンシークレットは存在しないため、空にします。以下が例になります。これを②とします。 {API_SECRET}& ※{API_SECRET}の部分はAPIシークレット。 2. 署名を作成する ②を鍵として、①をHMAC-SHA1でハッシュ化し、これをBase64エンコードします。これを③とします。 これはリクエスト内容が途中で改竄されていないかチェックする役割を果たしています。ハッシュ化されたものは基本的に復元できませんので、推測ですがTwitter側のAPIで同じ方法でハッシュを作り、一致するかチェックしていると思われます。 3. APIを叩いてリクエストトークンを取得 ようやくHTTPリクエストを作成します。先ほどのクエリーパラメータに oauth_signature=③ を追加してAPIを叩きます。 すると、ログイン画面用のトークン(④)とシークレットが返ってきます。 4. ユーザーをログイン画面に移動させる 次に、ユーザーを以下のURLにアクセスさせます。 https://api.twitter.com/oauth/authorize?oauth_token=④ すると、先程のトークンとベリファイア(?)(⑤)が返ってきます。 5. アクセストークンを取得する 最後に、アクセストークンを取得します。https://api.twitter.com/oauth/access_tokenに向かってoauth_consumer_key(APIキー)とoauth_token(④)とoauth_verifier(⑤)をぶつければアクセストークンとシークレットが返ってきます。これを使ってTwitterのユーザー情報にようやくアクセスできます。長かった。 以上となります。

【Firebase Dynamic Links + Android】SHA256フィンガープリントを登録しているのに、リンクを踏んでもアプリを直接開いてくれない時は

2021/11/22・Androidアプリ

ハマった。 Firebase ConsoleにてSHA256を登録しても、Dynamic LinkがAndroidアプリ(Flutter)で認証済みリンクとして認識されない現象が発生。Android 12では手動で追加すれば開いてくれますが、普通のユーザーはそんなことしません。 対処法 単純にintent-filterにautoVerify属性を追加する必要があったらしい。これがなんと、firebase_dynamic_linksのREADME.mdには書いてない(記事作成時点)。なんでや <intent-filter android:autoVerify="true"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:scheme="https" android:host="example.page.link" /> </intent-filter> 解決に導いたページ https://developer.android.com/training/app-links/verify-site-associations

macOS 12 Monterey Beta6において、IntelliJ IDEAやAndroid Studioが起動しない問題への対処法

2021/09/09・Androidアプリ

現状 手持ちのM1 MacBook ProをmacOS 12 Beta6にアップデートしたところ、Android Studioが全く起動しなくなりました。 少し調べてみたところ、どうやらIntelliJ IDEAが対応していないことが問題らしく、コンフィグをいじればまた起動するようになります。 手持ちのMacはM1ですが、Intelでも起こるようです。 対処法 .vmoptionsをいじります。 以下のファイルを開きます。 IntelliJ IDEAの場合 ~/Library/Application Support/JetBrains/IntelliJIdea2020.3/idea.vmoptions Android Studioの場合 ~/Library/Application Support/Google/AndroidStudio2020.3/studio.vmoptions このファイル内に以下の行を追加します。 -XX:+TieredCompilation -XX:TieredStopAtLevel=1 これで保存すると起動できるようになるはずです。 参考 https://youtrack.jetbrains.com/issue/JBR-3715

【Android Studio】Appleシリコン版でターミナルが開かない問題の対処法 / Workaround for Android Studio Apple Silicon Version Terminal Doesn’t Open

2021/06/02・Androidアプリ

先日、Android Studioのベータ版にApple Sillicon (M1 Mac) 対応版が公開されました。しかし、ターミナルが開かないという問題を抱えています。その問題への対処法を解説します。具体的には、以下のように表示されます。 Cannot open Local Terminal Failed to start [/bin/zsh, –login, -i] in /projectdir See your idea.log (Help | Show Log in Finder) for the details. 対処法 1. JetBrains/pty4j レポジトリをクローンする 以下のコマンドを使ってPC上の任意の場所で git clone し、native フォルダーに移動してください。 git clone https://github.com/JetBrains/pty4j && cd pty4j/native 2. ビルドし、所定の場所にぶち込む 以下のコマンドをそれぞれ実行してください。 Android Studio Preview.appへのパスは環境ごとに置き換えてください。 clang -fPIC -c *.c clang -shared -o libpty.dylib *.o cp libpty.dylib "/Applications/Android Studio Preview.app/Contents/lib/pty4j-native/darwin/" 3. Android Studio を再起動する 私は地味にはまりましたが、再起動しないと治らないっぽいです。再起動後は正常にターミナルが開くようになりました。 以上です。

Androidにおける「位置情報の許可」正しく理解してます?

2021/04/25・Androidアプリ

皆さん、こんにちは。 今回は、Androidにおいて「位置情報権限」について解説します。コイツはかなり曲者で、Android自体が誤解を招く表現をしているが故、多くの方が誤解していると思います。 今回はAndroidにおいて位置情報権限が必要なケースをお伝えします。 位置情報権限を許可するとできること まず、位置情報権限を許可すると何ができるのかを列挙します。 GPSによる位置情報の取得 Wi-Fiスキャンによる位置情報の取得 Bluetoothスキャンによる位置情報の取得 付近のWi-Fiのスキャン 付近のBluetooth端末のスキャン これらが「位置情報の許可」をされたアプリができるようになることです。一番上は当たり前として、意味不明なのが赤字の下4つではないでしょうか。 「Wi-Fiをスキャンして位置情報が分かるの?」 分かるんです。実は、BluetoothやWi-Fiをスキャンすることで、現在地が超大まかに分かってしまいます。 Wi-FiやBluetoothのアクセスポイントには現在地情報が埋め込まれていることがあり、それをスキャンすることでそのWi-Fiに接続せずとも、今何県にいるか程度は分かります。(誤差半径3kmくらい) つまり、Wi-FiやBluetoothのスキャンをすれば位置情報も分かるよね?ってことでスキャンを行うアプリはもれなく位置情報の許可が必要になります。 位置情報を使わないのに位置情報権限の許可が必要な例 ということはですよ、位置情報をアプリで利用しないにもかかわらず位置情報の許可が必要になる例があるというわけです。Wi-FiやBluetoothのスキャンだけをする場合です。 例えば、I-O DATAからリリースされている「Wi-Fiミレル」というアプリ。これはWi-Fiの情報表示アプリで、付近のWi-Fiの混雑状況をチェックする機能も付いているため、位置情報権限がないと付近のWi-Fiをスキャンできず、利用できません。 また、ヤマハのホームシアターの制御アプリ。これは付近のBluetoothをスキャンして自動的にホームシアターに接続するため、位置情報権限が必要です。 いずれのアプリにもレビューには「位置情報権限がないと起動せず怖い」などと書かれていることがありますが、その理由はこのようなことなのです。 まとめ 上のようなアプリでは、位置情報権限を要求するからと言って位置情報を使うとは限りません。しかし、この権限を許可してしまえばGPSでの位置情報の取得も可能になります。「なら位置情報を切ってしまおう」と思うかもしれませんが、これはWi-FiやBluetoothのスキャンも無効化されるため、そのアプリの機能が使えなくなります。 Wi-Fiなどのスキャンを行うアプリが位置情報を使っているかは知るすべがありません。このようなアプリを使いたい場合、位置情報の収集をしていないと信じて使うしかありません。このようなことは普通プライバシーポリシーに記載されているはずです。 ただ、Googleのような情報を商品としている企業ならともかく、上のような利用者の少ない野良アプリが位置情報を収集するメリットってほぼないと思います。(Googleは、野良アプリが可能な範疇など比ではないほどエグい位置情報収集を行っています。) 以上、Androidアプリコラム第3回でした。

【備忘録】Android 権限許可時のちゃんとした実装

2020/04/12・Androidアプリ

皆さん、権限許可してますか? 今回は、備忘録も兼ねてAndroid 6.0以降で権限許可させるときのちゃんとした実装を紹介します。 まず、今回の実装はこんな感じです。 許可ダイアログを出して許可された場合 許可ダイアログを出して許可されなかった場合 「今後表示しない」設定にされた場合 この3つで分岐させます。 ソースコード override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) if (requestCode == REQUEST_CODE) { if (grantResults.all { it == PERMISSION_GRANTED }) { // 許可された } else { if (ActivityCompat.shouldShowRequestPermissionRationale(this, 権限)) { // 許可されなかった } else { // 「今後表示しない」にチェックを入れられた } } } } shouldShowRequestPermissionRationale()メソッドは、本来「今後表示しない」をチェックするメソッドではないので、直接「今後表示しない」にチェックが入ったかを返すわけではありません。実際は以下のように値を返します。 表示回数0回→false 表示回数1回以降→true 表示回数2回以降、「今後表示しない」にチェックが入った場合→false そのため、onRequestPermissionsResult()にこのメソッドを置くことで、まだ1回も表示していない場合のfalseを回避できます。こういう仕組みで判別しています。 つまりは、権限が許可されていない場合は問答無用でrequestPermission()を呼びます。そうすると、「今後表示しない」にチェックが入っていた場合でもonRequestPermissionResultメソッドで拒否されたものとして結果が返ってきます。 2020/10/12追記:Android 11以降の仕様について Androd 11以降では、2回目に「許可しない」をタップした場合は問答無用で「今後表示しない」にチェックを入れたものとして扱われ、3回目以降は表示されなくなります。(だからといってロジックを変更させる必要は特にありません。)

Android Studioの便利キーまとめ

2019/10/05・Androidアプリ

今回は、Android Studioの起動時に表示されるTipに書いてある便利なキーをまとめてみました。ほぼ自分用ですが。 ※随時更新 キーTipをそのままGoogle翻訳で翻訳 / 簡易的な説明Ctrl + Alt + 8プログラムのデバッグ中に式の値を簡単に評価するには、エディターでそのテキストを選択し(Ctrl + Wを数回押してこの操作を効率的に実行できます)、Alt + Shift + 8を押します。Alt(LinuxではControl + Alt + Shift)プログラムのデバッグ中に式の値をすばやく評価するには、Alt(LinuxではControl + Alt + Shift)を押しながらこの式をクリックして値を確認し、計算し、メソッドを呼び出します。Ctrl + Space基本的な補完(Ctrl +スペース)を使用して、さまざまな種類のファイルのテキスト内の単語やコメントを完成させます。入力したプレフィックスで始まる現在のファイルのすべての単語がルックアップリストに表示されます。Ctrl + Shift + クリックマルチカーソルCtrl + Pカーソルを括弧内に入れてCtrl + Pで引数の再表示(地味に便利)Ctrl + Qキャレットでクラスまたはメソッドのドキュメントをすばやく表示するには、Ctrl + Q(表示|クイックドキュメント)を押します。Ctrl + BまたはCtrl + クリックコードのどこかで使用されているクラス、メソッド、または変数の宣言に移動するには、キャレットを使用箇所に置き、Ctrl + B(ナビゲート|宣言)を押します。 Ctrlキーを押しながら使用法でマウスをクリックして、宣言にジャンプすることもできます。Ctrl + F12Ctrl + F12(ナビゲート|ファイル構造)を使用して、現在編集中のファイルをすばやくナビゲートできます。現在のクラスのメンバーのリストが表示されます。ナビゲートする要素を選択し、EnterキーまたはF4キーを押します。リスト内のアイテムを簡単に見つけるには、名前の入力を開始します。Ctrl + O / Ctrl + ICtrl + O(コード|メソッドのオーバーライド)を押すと、基本クラスのメソッドを簡単にオーバーライドできます。現在のクラスが実装する(または抽象基本クラスの)インターフェイスのメソッドを実装するには、Ctrl + I(コード|メソッドの実装)を使用します。Ctrl+Shift+スペースSmartTypeコード補完は、式全体の予想される型を分析することにより、現在のコンテキストに適したメソッドと変数を見つけるのに非常に役立ちます。そうすると、Android Studioは最も適切な上位5つの結果を特定し、青色の背景でそれらを強調表示します。Tabコード補完を使用する場合、Tabキーを使用してポップアップリストで現在強調表示されている選択を受け入れることができます。 Enterキーで受け入れるのとは異なり、選択した名前はキャレットの右側にある残りの名前を上書きします。これは、あるメソッドまたは変数名を別のメソッドまたは変数名で置き換える場合に特に役立ちます。Ctrl + ECtrl + E(表示|最近のファイル)は、最近アクセスしたファイルのポップアップリストを表示します。目的のファイルを選択し、Enterキーを押して開きます。Ctrl+Qカーソルが乗っているメソッドなどのドキュメンテーションの表示 ※キーマップ発見しました。

【Android】外部SDカードの権限取得で「今後表示しない」を入れられてしまったら

2019/10/13・Androidアプリ

皆さん、こんにちは。 今回は、外部のSDカードへのアクセス権取得画面で、「今後表示しない」にチェックを入れられてしまったとき、どうすればいいか解説します。 先に断っておきますが、**この方法は一部の端末でしかできません。**Galaxy S9の最新版ではできることを確認していますが、できない端末が大半かもしれません。その時は、データを削除させるしかないと思います。 それでは、解説します。 ある日、設定画面を漁ってたらこんな物を見つけてしまいました。 タップするとアプリが並んでいて、適当にタップすると中にはなんと こんなのがありました。今まではデータを削除させるしか、「今後表示しない」にチェックを入れられてしまったときはなかったので、これで勝ったと思いました。 このActivityはどうやって表示させるのか、QuickShortcutMakerで調べてみると、どうやら以下のクラスで起動できるらしい。 com.android.settings.Settings$DirectoryAccessSettingsActivity なので、Intentを作り、startActivityします。 Intent().apply { setClassName("com.android.settings", "com.android.settings.Settings\$DirectoryAccessSettingsActivity") } ただ、これだと起動できない端末が大半なので、クラスが存在するかチェックする処理をはさみます。存在しない場合はおとなしくデータ削除させます。 val isLaunchable = try { packageManager.getActivityInfo(ComponentName("com.android.settings", "com.android.settings.Settings\$DirectoryAccessSettingsActivity"), 0) true } catch (e: PackageManager.NameNotFoundException) { false } val intent = if (isLaunchable) { Intent().apply { setClassName("com.android.settings", "com.android.settings.Settings\$DirectoryAccessSettingsActivity") } } else { Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply { val uri = Uri.fromParts("package", packageName, null) this.data = uri } } startActivity(intent) あとはその旨をユーザーに知らせればOK! それでは、よいAndroidライフを。

Flutterでハマった点/Kotlinからの変換メモ①

2020/10/04・Androidアプリ

AppBar (Androidで言うToolBar) ・ScaffoldにAppBar引数があるため、そこにAppBarをインスタンス化して追加 BottomNavigaton ・ScaffoldにbottomNavigation引数があるため、そこにBottomNavigationをインスタンス化して追加 アイコン ・Iconsクラスに大量に有用なアイコンが並んでいるので、Icon(Icons.xxx)のように使う。 ページの作成 import 'package:flutter/cupertino.dart'; class PageName extends StatefulWidget { @override State<StatefulWidget> createState() => _PageNameState(); } class _PageNameState extends State<PageName> { @override Widget build(BuildContext context) { return Center( child: Text("Hello World!"), ); } } PageNameの部分にページ名。 ・「stful」と入力すると、一発で作成できる。 ページ遷移 Navigator.push(context, MaterialPageRoute(builder: (context) => PageName())); PageNameの部分に遷移先ページ名。 タップ音(操作音)フィードバック Feedback.forTap(context); DartからKotlinコード呼び出し Qiitaにあった情報はちょっと古かったので、公式ドキュメントのお世話に。 Kotlin側 class MainActivity: FlutterActivity() { companion object { const val BATTERY_CHANNEL = "sample.channel/battery" // 適当な文字列。普通は「パッケージ名/チャンネル名(任意の名称)」 } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, BATTERY_CHANNEL).setMethodCallHandler { call, result -> when(call.method) { "getBatteryLevel" -> { val level = batteryLevel when { Build.VERSION.SDK_INT < Build.VERSION_CODES.P && level == 0 -> result.error(null, "Battery Level returned illegal value.", null) Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && level == Int.MIN_VALUE -> result.error(null, "Battery Level returned illegal value.", null) else -> result.success(level) } } else -> result.notImplemented() } } } val batteryLevel: Int get() { return batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY) } } ※Android 5.0以降を対象にした実装です。 Dart側 MethodChannel("sample.channel/battery").invokeMethod("getBatteryLevel").then((result) { print(result); }); ※「Open for Editing in Android Studio」をクリックして編集する。 ※反映にはAndroid側のプロジェクトでビルドする必要がある。 onDestroy() @override void dispose() { // process super.dispose(); } ファイル転送関連 UDP bind UDP.bind(Endpoint.broadcast(port: Port(port))); 自身のIPアドレスの取得 NetworkInterface.list(type: InternetAddressType.IPv4).then((result) { String address; result.forEach((element) { if (address != null) return; try { address = element.addresses.firstWhere((element) => element.address.startsWith("192.168.0.")).address; } catch (e) {} }); address // アドレス }); 自身のモデル名等を取得 ・pubspec.yamlの依存関係に以下を追記 device_info: ">=0.4.2+8 <2.0.0" ・DeviceInfoPluginをインスタンス化→取得 var devInfo = DeviceInfoPlugin(); // Android if (Platform.isAndroid) var modelName = (await devInfo.androidInfo).model; // iOS if (Platform.isIOS) var modelName = (await devInfo.iosInfo).utsname.machine;