tag:crieit.net,2005:https://crieit.net/tags/Android/feed 「Android」の記事 - Crieit Crieitでタグ「Android」に投稿された最近の記事 2023-08-29T08:59:27+09:00 https://crieit.net/tags/Android/feed tag:crieit.net,2005:PublicArticle/18551 2023-08-19T01:00:36+09:00 2023-08-29T08:59:27+09:00 https://crieit.net/posts/Keepass2Android-Offline Keepass2Android は Offline 版がオススメ <p>パスワード管理ソフトの中でも非常に有名なものの一つが KeePass シリーズだ。</p> <p>複数の端末での同期や、多要素認証などのセキュリティ向上設定には少し知識が必要なものの、 LastPass や 1Password などのクラウド型のソリューションとは異なり、有料プランの課金やサービス提供企業側のヘマによるセキュリティ侵害などのリスクが低いのが特徴だ。</p> <p>Andoroid 向けの古参 KeePass 互換のパスワード管理ソフトとして、 <a target="_blank" rel="nofollow noopener" href="https://github.com/PhilippC/keepass2android">Keepass2Android</a> が存在する。</p> <p>この Keepass2Android には、同じ作者によって提供される以下の2つのバージョンがある。</p> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://play.google.com/store/apps/details?id=keepass2android.keepass2android">Keepass2Android Password Safe</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://play.google.com/store/apps/details?id=keepass2android.keepass2android_nonet">Keepass2Android Offline</a></li> </ul> <p>一見すると、インストール数の多い Keepass2Android Password Safe (以下、 非オフライン版) の方が良さそうに見えるかもしれないが、実際には <strong>Keepass2Android Offline をオススメ</strong>する。</p> <h2 id="Offline版 と 非オフライン版 の違い"><a href="#Offline%E7%89%88+%E3%81%A8+%E9%9D%9E%E3%82%AA%E3%83%95%E3%83%A9%E3%82%A4%E3%83%B3%E7%89%88+%E3%81%AE%E9%81%95%E3%81%84">Offline版 と 非オフライン版 の違い</a></h2> <p>そもそも、 Offline版 と 非オフライン版 の違いは、アプリが<strong>直接</strong>インターネットにアクセスしてデータベースファイルを取得する機能の有無だ。</p> <p>しかし、 Offline版 ではデータベースファイルのクラウド同期ができない…かというと<strong>そんなことはない</strong>。<br /> Android には、 "Storage Access Framework" (SAF) という機能が備わっており、アプリがクラウドストレージと直接通信する必要なく、システムのファイルピッカーを介して、 Google Drive や OneDrive のアプリを開き、クラウドストレージとの同期ができる。</p> <p><a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/08/recommend-offline-ver-for-keepass2android-01.jpg"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/08/recommend-offline-ver-for-keepass2android-01-135x300.jpg" alt="" /></a> <a target="_blank" rel="nofollow noopener" href="https://aquasoftware.net/blog/wp-content/uploads/2023/08/recommend-offline-ver-for-keepass2android-02.jpg"><img src="https://aquasoftware.net/blog/wp-content/uploads/2023/08/recommend-offline-ver-for-keepass2android-02-135x300.jpg" alt="" /></a></p> <p>このため、 FTP や WebDAV などの一部の特殊な場所を除いて、ほとんどのクラウドストレージを Offline版 でも問題なく利用できる。<br /> 開くだけでなく、データベースを編集して保存すれば、もちろんクラウド側にも変更が反映される。</p> <p>すなわち、ほとんどの場合は Offline版 で十分なのだ。</p> <p>非オフライン が、アプリ内部から直接 Google ドライブや OneDrive, Dropbox 等を開けるのは、 SAF が一般的ではなかった過去の名残という面が大きい。</p> <h2 id="なぜ Offline版 が良いのか"><a href="#%E3%81%AA%E3%81%9C+Offline%E7%89%88+%E3%81%8C%E8%89%AF%E3%81%84%E3%81%AE%E3%81%8B">なぜ Offline版 が良いのか</a></h2> <p>上記のような前提を踏まえ、そのなかで何故 Offline版 が良いのかというと、アプリが <strong>「ネットワークへのフルアクセス」の権限を持っていない</strong> からだ。</p> <p>パスワード管理ソフトのリスクのひとつに、開発者の PC がクラッキングされて、ソフト側にキーロガーや他の脆弱性が仕込まれる可能性がある。<br /> (以前、 <a target="_blank" rel="nofollow noopener" href="https://gigazine.net/news/20230301-lastpass-incident/">LastPass でやられた</a>のがまさにそれ)</p> <p>しかし、万が一にもそのような脆弱性を仕込まれても、そもそもネットワークアクセス権限がなければ、奪われた情報をどこかに送信されることもない。<br /> すなわち、より安全になる…というワケだ。</p> <h2 id="そのほか"><a href="#%E3%81%9D%E3%81%AE%E3%81%BB%E3%81%8B">そのほか</a></h2> <p>Keepass2Android 以外の Android 向け KeePass 互換ソフトで最近人気が出てきているのが <a target="_blank" rel="nofollow noopener" href="https://www.keepassdx.com/">KeePassDX</a> だ。<br /> 元々は <a target="_blank" rel="nofollow noopener" href="https://www.keepassdroid.com/">KeePassDroid</a> のフォークだったものだが、 UI はだいぶモダナイズされている。<br /> 権限回りもだいぶ尖っていて、「バイブレーションの制御」しか存在しない。</p> <p>個人的には、ロック解除後の UI は KeePassDX の方が好みだが、生体認証を用いたクイック解除まわりの挙動は Keepass2Android の方が使いやすいので、もっぱら後者を使っている。</p> <p>利用者の多さだけでなく、 GitHub 上の OSS 開発者コミュニティの活発さからもアプリの安全性が推察できるので、ぜひ利便性と安全性のバランスが取れた、お好みの KeePass クライアントを探してみると良いだろう。</p> advanceboy tag:crieit.net,2005:PublicArticle/18324 2022-11-30T08:26:08+09:00 2022-11-30T08:26:08+09:00 https://crieit.net/posts/Google-Play-REQUEST-INSTALL-PACKAGE Google PlayからREQUEST_INSTALL_PACKAGEの許可を外せと言われてアプリを却下された時 <p>Google Playでいつも通りアプリのアップデートを申請していたらリジェクトされました。その内容が、REQUEST_INSTALL_PACKAGEは使って無さそうだからpermissionを外せ、とのことでした。</p> <p>調べてみましたが特に使っているところはなく。恐らく使用しているパッケージの中に使っているものがあるのだろうと思って調査。</p> <p><a href="https://crieit.now.sh/upload_images/b8a4463b900416895a215b155a56d9bc6386940e8c7b8.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b8a4463b900416895a215b155a56d9bc6386940e8c7b8.png?mw=700" alt="persmission_before.png" /></a></p> <p>あー…、ありました。Flutterのbuildフォルダ内を検索してみると見つかりました。調べてみるとどうも open_file というパッケージデ使われているようでした。</p> <p>外部パッケージなので対処しようがないのかな…と更に調べてみると、REQUEST_INSTALL_PACKAGEを外しただけの open_file_safe というパッケージがあるらしく、そちらに置き換えることで対処できました。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a638694e1cea87.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a638694e1cea87.png?mw=700" alt="image.png" /></a></p> <p>このように問題なくなりました。(ビルドのゴミがあるのでこれは手動で削除してよさそうです)</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/18265 2022-07-29T07:33:22+09:00 2022-07-29T07:33:22+09:00 https://crieit.net/posts/15538b281af3d9bb53ada98193886b4e 旅先でサーバ上のデータベースをメンテナンスする <p>以前に投稿した記事「旅先でクラウド上のWindowsサーバにRDP接続」は、Androidセルフォンのアプリケーションで、SSHのポートフォワードを介して<br /> WindowsサーバにRDP接続するというものでした。</p> <p><a href="https://crieit.net/posts/Windows-RDP">旅先でクラウド上のWindowsサーバにRDP接続</a></p> <p>この記事で紹介した、僕を助けてくれるアプリケーションのうちの1つがこれなんです。</p> <p><a href="https://crieit.now.sh/upload_images/0f1620b003abae44c6e2dc0b9a1cb92a62e30b62d0a6f.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0f1620b003abae44c6e2dc0b9a1cb92a62e30b62d0a6f.png?mw=700" alt="image" /></a></p> <p>これのポートフォワード設定を追加してあげると、セルフォンのアプリケーションから、サーバのデータベースのデータを参照したり更新したりすることができます。<br /> 皆さんもご存知のとおり、あたりまえのことなんですけどね。<br /> <a href="https://crieit.now.sh/upload_images/029983100bf745182d0640da3a29124262e304560a1a0.jpeg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/029983100bf745182d0640da3a29124262e304560a1a0.jpeg?mw=700" alt="image" /></a></p> <h2 id="ポートフォワード"><a href="#%E3%83%9D%E3%83%BC%E3%83%88%E3%83%95%E3%82%A9%E3%83%AF%E3%83%BC%E3%83%89">ポートフォワード</a></h2> <p>ConnectBotのSSHに下図にようなポートフォワードを追加してあげます。この例は、SSHサーバのローカルで稼働するMySQLに接続する設定です。<br /> <a href="https://crieit.now.sh/upload_images/47822d9b8e2a2f058338bdd5f7b2763562e30bf7b571b.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/47822d9b8e2a2f058338bdd5f7b2763562e30bf7b571b.png?mw=700" alt="image" /></a><br /> RDPは以前に設定したものです。</p> <h2 id="データベースのデータを操作するアプリケーション"><a href="#%E3%83%87%E3%83%BC%E3%82%BF%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AE%E3%83%87%E3%83%BC%E3%82%BF%E3%82%92%E6%93%8D%E4%BD%9C%E3%81%99%E3%82%8B%E3%82%A2%E3%83%97%E3%83%AA%E3%82%B1%E3%83%BC%E3%82%B7%E3%83%A7%E3%83%B3">データベースのデータを操作するアプリケーション</a></h2> <p>データベースのデータを操作するのに便利なアプリケーションがあります。僕の好みです。皆さんの評価など受け付けません。<br /> <a href="https://crieit.now.sh/upload_images/f6942cff736d92fe0736b22d959d417062e30d0b1153c.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/f6942cff736d92fe0736b22d959d417062e30d0b1153c.png?mw=700" alt="image" /></a><br /> MySQLへの接続はこんなふうにしてあげます。<br /> <a href="https://crieit.now.sh/upload_images/8822652800e925e419f3ed4441f1dff062e30d7d6ad50.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/8822652800e925e419f3ed4441f1dff062e30d7d6ad50.png?mw=700" alt="image" /></a><br /> デフォルトでSQLiteへの接続が見えますが、ここにSQLiteがいれば、おそらくつながるのでしょう。<br /> <a href="https://crieit.now.sh/upload_images/b02515b7e752989101e54ece9e6f6e0462e30dc24b9c7.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/b02515b7e752989101e54ece9e6f6e0462e30dc24b9c7.png?mw=700" alt="image" /></a><br /> MySQLをクリックすると、こんなふうにオブジェクトが見えるようになります。<br /> <a href="https://crieit.now.sh/upload_images/740d71e54addfcf9377a6b27426a139a62e30e1268e41.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/740d71e54addfcf9377a6b27426a139a62e30e1268e41.png?mw=700" alt="image" /></a></p> COOL MAGIC PRODUCTS tag:crieit.net,2005:PublicArticle/18260 2022-07-25T21:47:47+09:00 2022-07-25T23:01:21+09:00 https://crieit.net/posts/Windows-RDP 旅先でクラウド上のWindowsサーバにRDP接続 <p>この記事には、目新しい技術は書かれていません。既成のアプリケーションの使い方のひとつが紹介されているだけです。</p> <p>旅先でクラウド上のWindowsサーバにRDP接続したいな、という機会は、僕にはたくさんあります。PCを使える状況であればPCを使えば良いのですが、例えば電車の中などでPCを使えない、あるいはPCは使えるけどそれも面倒という場面も僕にはたくさんあります。<br /> こんなときのために、僕にはセルフォンがあるのです。</p> <h2 id="外部からの接続"><a href="#%E5%A4%96%E9%83%A8%E3%81%8B%E3%82%89%E3%81%AE%E6%8E%A5%E7%B6%9A">外部からの接続</a></h2> <p>外部から目的のWindowsサーバにRDPで接続するには、間にSSHが配置され、このSSHがポートフォワードしてくれる、というのが前提です。当然ながら、SSH接続には鍵が必要というのも前提です。<br /> つまり、何の変哲もない一般的な構成です。</p> <h2 id="僕を助けてくれるAndroidアプリ"><a href="#%E5%83%95%E3%82%92%E5%8A%A9%E3%81%91%E3%81%A6%E3%81%8F%E3%82%8C%E3%82%8BAndroid%E3%82%A2%E3%83%97%E3%83%AA">僕を助けてくれるAndroidアプリ</a></h2> <p>こういった場面で僕を助けてくれるAndroidアプリは以下の2つです。どちらか1つというわけではなく、2つがタックで助けてくれます。</p> <ul> <li>ConnectBot</li> <li>Remote Desktop</li> </ul> <p>これらはよく配慮された機能をもち、かつ余分な機能を持たないという、セルフォンのアプリケーションとして優れた特徴を持ったアプリケーションです。<br /> <a href="https://crieit.now.sh/upload_images/d56076624b7e1efe9cc376dfadad312b62de9ea0cb881.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/d56076624b7e1efe9cc376dfadad312b62de9ea0cb881.png?mw=700" alt="image" /></a><br /> <a href="https://crieit.now.sh/upload_images/a15fd3a22e58e5d358e77d9e1f0b9d9862de9ec12704a.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/a15fd3a22e58e5d358e77d9e1f0b9d9862de9ec12704a.png?mw=700" alt="image" /></a></p> <h2 id="使い方"><a href="#%E4%BD%BF%E3%81%84%E6%96%B9">使い方</a></h2> <p>ConnectBotの設定を事前に行っておくべきです。</p> <h3 id="ConnectBot"><a href="#ConnectBot">ConnectBot</a></h3> <p>以下はConnectBotの設定です。</p> <p>"Manage Pubkeys"を使って、サーバへのSSH接続を行うための鍵を登録します。<br /> <a href="https://crieit.now.sh/upload_images/bd584093bd0999427a3c12f2f66be6e462de95445d595.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/bd584093bd0999427a3c12f2f66be6e462de95445d595.png?mw=700" alt="image" /></a><br /> 鍵を登録した状態です。<br /> <a href="https://crieit.now.sh/upload_images/7f2e4dfbf1cef4fe59851276b2816fed62de9ee62bb97.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/7f2e4dfbf1cef4fe59851276b2816fed62de9ee62bb97.png?mw=700" alt="image" /></a><br /> "Hosts"にSSHサーバを登録し接続します。鍵はここで使用します。SSHサーバへの登録と接続については、この記事では解説しません。難しくはないでしょう。</p> <p>SSHでSSHサーバに接続したら、メニューから[Port Forward]を選択します。<br /> <a href="https://crieit.now.sh/upload_images/372c4d79e6285840903eb025c3048e7562de94b11ae96.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/372c4d79e6285840903eb025c3048e7562de94b11ae96.png?mw=700" alt="image" /></a><br /> WindowsサーバのRDPへのポート・フォワードを設定します。<br /> <a href="https://crieit.now.sh/upload_images/be66b92cc798d4d94f8bf1b1f82c933262de9f17184cb.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/be66b92cc798d4d94f8bf1b1f82c933262de9f17184cb.png?mw=700" alt="image" /></a></p> <h3 id="Remote Desktop"><a href="#Remote+Desktop">Remote Desktop</a></h3> <p>僕は、マイクロソフト社純正のアプリケーションを使用しています。<br /> ConnectBotによるSSH(= ポートフォワード)セッションを活かした状態で、RDPに接続します。<br /> <a href="https://crieit.now.sh/upload_images/698517cdfeff566370d6a2f2c9016cb562de9f2ebd751.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/698517cdfeff566370d6a2f2c9016cb562de9f2ebd751.png?mw=700" alt="image" /></a></p> COOL MAGIC PRODUCTS tag:crieit.net,2005:PublicArticle/18200 2022-05-29T10:37:53+09:00 2022-05-29T10:37:53+09:00 https://crieit.net/posts/Targeting-S-version-31-and-above-requires-that-one-of-FLAG-IMMUTABLE-or-FLAG-MUTABLE-be-specified-when-creating-a-PendingIntent Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntentエラー <p>FlutterでtargetSdkVersionを31にしたアプリを実機で試してみたらいきなり落ちてしまった。ログを見てみると下記のようなエラー。</p> <pre><code class="plain">Targeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent </code></pre> <p>調べてみると色々記事が見つかり、どうもFirebaseやAdMobのライブラリが関係していてみんな困っていた。とはいえそれらの情報は2021年の情報だったので、2022/5現在はさすがに改善しているのではないかと思いそのあたりのライブラリを最新にしてみたら起動するようになった。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/18137 2022-03-07T14:42:49+09:00 2022-03-07T14:42:49+09:00 https://crieit.net/posts/Android-API31 Android 虎の巻 API31対応するための修正 <p><a href="https://crieit.now.sh/upload_images/e3b6c3ee08fdf4b7856b8e60c3a0a2a16225985e9cfea.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/e3b6c3ee08fdf4b7856b8e60c3a0a2a16225985e9cfea.gif?mw=700" alt="image" /></a><br /> 上のようなアニメがつくれるフリーソフト<a target="_blank" rel="nofollow noopener" href="https://9vae.blogspot.com/p/9vae-download.html">9VAeきゅうべえ</a> を開発してます。<br /> 現在、Googleアプリストアに登録するには、Android API30 以上が必要ですが、次のバージョンに対応するために、AndroidStudio の build.gradle(Module:) を、31 に変更すると、ビルドエラーがでます。</p> <pre><code>android { compileSdkVersion 31 <--30 から 31に変更 defaultConfig { minSdkVersion 24 targetSdkVersion 31 <--30 から 31に変更 versionCode xxx versionName "x.x" } } </code></pre> <p>AndroidManifest.xml に「android:exported="true"」を追加するとビルドできるようになるみたいです。</p> <pre><code><application <activity android:name="xxxx" android:exported="true" <--これを追加するとエラーがなくなった ... > </activity> </application> </code></pre> Danjiro Daiwa tag:crieit.net,2005:PublicArticle/18022 2022-02-10T15:38:06+09:00 2022-02-10T15:38:06+09:00 https://crieit.net/posts/Amazon Amazonストアアプリが64ビット未対応と判定されたときの対応 <p><a href="https://crieit.now.sh/upload_images/102987c114f9b1e508737466a3de78196204a7b41772d.gif" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/102987c114f9b1e508737466a3de78196204a7b41772d.gif?mw=700" alt="image" /></a><br /> Amazon Fire用アプリ「9VAeきゅうべえ」に、MP4動画出力機能をつけたとろ、「64ビット対応していない」との判定がでるようになり、新しい Amazon Fire が対象外になってしまった。最新の Amazon Fire は32ビットアプリが動かないみたいだ。</p> <p>しかし、同時に作成した、Googleストアアプリは、ちゃんと64ビット対応している。</p> <p>Googleストアには、AABで登録し、Amazonは、APKで登録する。AndroidStudio は、AABもAPKも出力できる。その APK をみてみると、ちゃんと、ARM64ビット、Intel 64ビットがはいっていた。</p> <p>よりくわしく見てみると、32ビット版ライブラリの数が多かった。64ビット版に同じライブラリがないため、未対応と判定するみたい。</p> <p>違っているライブラリには、NEONという名前がついていた。NEONは、SIMD演算で動画を高速処理するためにつかわれる。32ビット版CPUには、SIMD演算できないものがあるため、ライブラリが追加されているみたいだ。そのため動画出力をいれると、64ビット未対応と判定されるようになった。</p> <p>それで結局、つぎのような3本のAPKを作成すると、全機種サポートできるようになった。</p> <ol> <li>動画出力を除いたAPK >> 32ビット・64ビット 兼用APKとして認識される</li> <li>動画出力つきAPK >>32ビット用APKとして認識される</li> <li>64ビットのみの動画出力つきAPK>> 64ビット用APKとして認識される</li> </ol> <p><a target="_blank" rel="nofollow noopener" href="https://dnjiro.hatenablog.com/entry/2019/12/11/000255">そこらへんの対応記事をこちらに書きました</a></p> Danjiro Daiwa tag:crieit.net,2005:PublicArticle/17924 2022-01-07T16:19:06+09:00 2022-01-11T17:18:59+09:00 https://crieit.net/posts/Flutter-61d7e96a4daa2 Flutterベースのモバイル向けタイムラインの作成 <h1><a target="_blank" rel="nofollow noopener" href="https://quire.io">Quire</a>タイムライン(モバイル向け)の構造を初公開</h1> <p>2018年に初めてFlutterベースのアプリを作ったときは、とても楽しく興奮しました。それから3年経ち、<a target="_blank" rel="nofollow noopener" href="https://quire.io">Quireアプリ</a>もかなり充実して、従来のモバイル向けプロジェクト管理アプリの域を超えるまでになりました。Quireモバイルアプリの現行バージョンは、階層表示、ボード表示だけでなく、タイムライン表示にも対応しています。</p> <p>モバイルアプリ向けのタイムライン表示の作成を決めたときは、簡単にできるとは思いませんでした。当時は類似の既成コンポーネントもなかったためですが、驚いたのは、インターネットでタイムライン表示の構造についての情報も見当たらなかったことです。そこで、いちかばちか、自分たちで作ってみることにしました。</p> <h5><a target="_blank" rel="nofollow noopener" href="https://quire.io">Quire</a>モバイルアプリ用のタイムラインでは、以下を計画していました。</h5> <ol> <li>横方向への無限日付スクロール</li> <li>レンダリングオンデマンド(ROD)。ビューポートにあるときのみ実行されるWidgetのState</li> <li>任意の位置に素早く配置</li> <li>操作がかんたんで使いやすいインターフェースと、スムーズなユーザーエクスペリエンス</li> </ol> <p>数週間で初期開発が完了し、以下のような構造になりました。</p> <p><img src="https://storage.googleapis.com/zenn-user-upload/52c3e5779da7-20220105.png" alt="" /></p> <ol> <li>タイムラインペインのコアベース(週、週末のセクションなど)</li> <li>タスクリスト(階層構造のタスクリスト)</li> <li>タイムラインペインのビューポートベース双方向リスト</li> <li>1ペインのみのとき、2ペインにまたがるときの両方に対応した期間の横棒</li> <li>期間の横棒上の固定ラベル</li> </ol> <p>上図のように、タスクごとにタイムラインペインが割り当てられ、すべてのタイムラインのスクロール位置は互いに同期されます。</p> <h2 id="インデックスベースのスクロールビュー"><a href="#%E3%82%A4%E3%83%B3%E3%83%87%E3%83%83%E3%82%AF%E3%82%B9%E3%83%99%E3%83%BC%E3%82%B9%E3%81%AE%E3%82%B9%E3%82%AF%E3%83%AD%E3%83%BC%E3%83%AB%E3%83%93%E3%83%A5%E3%83%BC">インデックスベースのスクロールビュー</a></h2> <p>Google Flutter Widgetに似たインデックスベースのスクロールビューを作るために、Centerに引数のあるカスタムスクロールビューを使用します。実装すると、任意の位置まで素早くスクロールできるようになります。スクロール中のどの時点でも、各位置とインデックスを表示できます。</p> <p>イメージ的には、少しスクロールした時点で、新しいCenterの引数でタイムラインをリロードしてビューポートの外に移し、またスクロールするとビューポート内に配置される、という感じです。</p> <h2 id="タイムラインペイン"><a href="#%E3%82%BF%E3%82%A4%E3%83%A0%E3%83%A9%E3%82%A4%E3%83%B3%E3%83%9A%E3%82%A4%E3%83%B3">タイムラインペイン</a></h2> <p>タイムラインをスムーズに使えるように、インデックスベースのスクロール表示と似た発想で、横方向にスクロールできるカスタム「無限双方向スクロールビュー」を実現しました。実装すると、タイムラインをなめらかにスクロールできます。</p> <p>無限双方向スクロールビューには、Flutterの強力なViewportの考え方を活用しました。そして、Backwardリストのインデックスを-1から始まる負の数に変更しました。Index 0に当たる日付が分かるようにフラグも設定して、任意の日付まで素早くスクロールできるようにしました。</p> <pre><code class="js">Widget forwardList = SliverList( delegate: SliverChildBuilderDelegate((BuildContext context, int index) { return cellBuilder(context, _getIndex(forward: true, index: index)); }) ); Widget backwardList = SliverList( delegate: SliverChildBuilderDelegate((BuildContext context, int index) { return cellBuilder(context, _getIndex(forward: false, index: index)); }), ); Scrollable( viewportBuilder: (BuildContext context, ViewportOffset offset) { return Viewport( offset: offset, center: forwardListKey, slivers: [ backwardList, forwardList, ] ); }, ) </code></pre> <h2 id="2ペインにまたがるときの問題と解決方法"><a href="#2%E3%83%9A%E3%82%A4%E3%83%B3%E3%81%AB%E3%81%BE%E3%81%9F%E3%81%8C%E3%82%8B%E3%81%A8%E3%81%8D%E3%81%AE%E5%95%8F%E9%A1%8C%E3%81%A8%E8%A7%A3%E6%B1%BA%E6%96%B9%E6%B3%95">2ペインにまたがるときの問題と解決方法</a></h2> <p>ビューポートには無限のリスト2つがスクロールされるため、期間の横棒が2つのリストにまたがることもあります。そこで、どちらのリストにも完全に同じ期間の横棒を作成し、ぴったり重ねて、リストがビューポートの外に移動してもリスト内のアンカーが壊れないようにしました。</p> <p><img src="https://storage.googleapis.com/zenn-user-upload/a56ed0583195-20220105.gif" alt="" /></p> <h2 id="固定ラベルで解決"><a href="#%E5%9B%BA%E5%AE%9A%E3%83%A9%E3%83%99%E3%83%AB%E3%81%A7%E8%A7%A3%E6%B1%BA">固定ラベルで解決</a></h2> <p>モバイル機器の小さい画面ではプロジェクトのどこを見ているかが分かりにくく、使っていると混乱してきます。この問題は、できるだけ多くの情報を提供することで軽減できます。そこで便利なのが固定ラベルです。</p> <p>最初はとにかくシンプルにするため、スクロールビューのスクロール通知に従って、位置を取得してから配置されたラベルに設定していました。固定ラベルを各タイムラインペインの開始位置に表示するには、期間の横棒の現在位置の計算がベースとなります。</p> <p>しかし、新しく配置されたラベルは次のフレームまでしか更新されず、スクロールビューと同じ時間枠で同期されないため、ずれて見えてしまいました。</p> <p>幸いFlutterコミュニティーが、レンダリングレイヤー固定ヘッダーというすばらしい解決方法を教えてくれました。つまりレイアウトのタイミングによる方法です。レンダリングレイヤーにすべてのWidgetをサイズとともに入れるだけでなく、そのピクセルすべてを計算する必要があります。最後にlocalToGlobal関数を、スクロール位置、および2ペインにまたがるときのペイン切り替えに基づいた演算操作と置き換えて、パフォーマンスを向上させました。</p> <h2 id="始まりはこれから"><a href="#%E5%A7%8B%E3%81%BE%E3%82%8A%E3%81%AF%E3%81%93%E3%82%8C%E3%81%8B%E3%82%89">始まりはこれから</a></h2> <p>今は大変な時代ですが、だからこそテクノロジーの分野で貢献したいと考えています。タイムライン表示の作成でまず考えたのは、どうやってFlutterの強力なフレームワークを活用して、ビューコンポーネントを一から作り直すことなく、軽く安定したゴージャスなUIを実現するか、ということでした。</p> <p>各日付単位はインデックスとして、FlutterのSliverに組み込まれています。ほとんどのものはWidgetレイヤーの高レベルの開発概念に留まり、固定ビューのときのみレンダリングレベルに移動します。</p> <p><a target="_blank" rel="nofollow noopener" href="https://quire.io">Quireアプリ</a>をインストールして、Flutterベースのモバイルアプリを使ってみませんか。Quireタイムラインについて気になることは、コメントを投稿するか、<a target="_blank" rel="nofollow noopener" href="https://twitter.com/quire_io">@quire_io</a>でツイートしてお知らせください!</p> <p>※転載許可済み: https://zenn.dev/quireteam/articles/2b2c44c3e49fac</p> uniyeh tag:crieit.net,2005:PublicArticle/17747 2021-11-11T08:12:52+09:00 2021-11-11T08:13:25+09:00 https://crieit.net/posts/flutter-secure-storage-Cipher-functions-OPENSSL-internal-BAD-DECRYPT flutter_secure_storageでCipher functions:OPENSSL_internal:BAD_DECRYPTエラー <p>Flutterで実機デバッグしていたらいきなりflutter_secure_storageのエラーが出るようになっていた。下記のようなエラー。(2021/11現在)</p> <p>PlatformException(Exception encountered, readAll, javax.crypto.BadPaddingException: error:1e000065:Cipher functions:OPENSSL_internal:BAD_DECRYPT</p> <p>なんだかよくわからないが、エラーが出はじめたタイミングでtargetSdkVersionを30にしていたのでもしかしたら高いバージョンのAndroid環境(Android11以降?)だと発生するのかもしれない。(とはいってもbuild.gradleを戻してもなおらない。なぜかコミットをもとに戻さないと発生しないので謎)</p> <p>issueを調べてみたら現在報告されているところで改善もしていないっぽい。なのでこれからの修正を待つしかなさそう。</p> <h2 id="一時的な対処方法"><a href="#%E4%B8%80%E6%99%82%E7%9A%84%E3%81%AA%E5%AF%BE%E5%87%A6%E6%96%B9%E6%B3%95">一時的な対処方法</a></h2> <p>まずreadAllは使わない。これを使うと絶対エラーになる。でもそれ以外はなんとなく動いているっぽい気がする。</p> <p>ということでreadを使う。しかし、多分データがなにもないとかキーが無いとかでもエラーになる。そのため、readもtry catchで囲んで、エラーが出たらデフォルト値を返すようにして利用する必要がある。</p> <p>とりあえずそれで動いた。</p> <p>issueはなんか同じようなのがいっぱいあるがとりあえずこれ<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/mogol/flutter_secure_storage/issues/248">https://github.com/mogol/flutter_secure_storage/issues/248</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/17602 2021-08-16T02:31:05+09:00 2021-08-22T07:44:40+09:00 https://crieit.net/posts/unity-ios-android-secret-manager 📔Unity で iOS/Android アプリの設定値をセキュアに扱う方法 <h1 id="はじめに"><a href="#%E3%81%AF%E3%81%98%E3%82%81%E3%81%AB">はじめに</a></h1> <p>iOS/Android でユーザーの情報をセキュアに扱う必要があったので、調査したところ Android には <a target="_blank" rel="nofollow noopener" href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">EncryptedSharedPreferences</a> が存在することを知りました。iOS には <a target="_blank" rel="nofollow noopener" href="https://developer.apple.com/documentation/security/keychain_services">Keychain Services</a> が存在します。</p> <p>今回は Unity の iOS/Android プラットフォーム上で設定値を保存するための実装を行う必要があったので、Unity から扱えるようネイティブプラグインを作成しました。今後もこういった要望はありそうでしたので、記事として手順や内容を書き記しておくことにしました。</p> <p>本記事内で紹介しているコードは下記にアップ済みです。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/nikaera/Unity-iOS-Android-SecretManager-Sample">https://github.com/nikaera/Unity-iOS-Android-SecretManager-Sample</a></p> <h1 id="動作環境"><a href="#%E5%8B%95%E4%BD%9C%E7%92%B0%E5%A2%83">動作環境</a></h1> <ul> <li>MacBook Air (M1, 2020)</li> <li>Unity 2020.3.15f2</li> <li>Android 6.0 以上 <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">EncryptedSharedPreferences</a> が使用可能なバージョン</li> </ul></li> </ul> <h1 id="Android のネイティブプラグインを作成する"><a href="#Android+%E3%81%AE%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">Android のネイティブプラグインを作成する</a></h1> <p>Android 環境ではまず <a target="_blank" rel="nofollow noopener" href="https://github.com/googlesamples/unity-jar-resolver">External Dependency Manager for Unity</a> を利用して、Unity の Android ネイティブプラグインで <code>EncryptedSharedPreferences</code> 利用可能にします。</p> <h2 id="(追記) Gradle を利用したライブラリのインストール方法"><a href="#%28%E8%BF%BD%E8%A8%98%29+Gradle+%E3%82%92%E5%88%A9%E7%94%A8%E3%81%97%E3%81%9F%E3%83%A9%E3%82%A4%E3%83%96%E3%83%A9%E3%83%AA%E3%81%AE%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E6%96%B9%E6%B3%95">(追記) Gradle を利用したライブラリのインストール方法</a></h2> <p><a target="_blank" rel="nofollow noopener" href="https://twitter.com/shiena">shiena</a> さんにご教授いただいたのですが、<a target="_blank" rel="nofollow noopener" href="https://zenn.dev/shiena/articles/unity-sqlcipher#gradle%E3%82%92%E5%88%A9%E7%94%A8">こちらの記事</a>のように Gradle を利用することでも簡易にライブラリの取り込みが可能なようでした。</p> <p>手順は上記の記事をご参照いただくとして、Gradle を利用する方法で外部ライブラリを取り込む際の <code>Assets/Plugins/Android/mainTemplate.gradle</code> および <code>Assets/Plugins/Android/gradleTemplate.properties</code> は下記になります。</p> <p>```diff gradle:Plugins/Android/mainTemplate.gradle<br /> dependencies {<br /> implementation fileTree(dir: 'libs', include: ['*.jar'])<br /> + implementation 'androidx.security:security-crypto:1.1.0-alpha03'<br /> <strong>DEPS</strong>}</p> <p>android {</p> <pre><code><br />```diff properties:Assets/Plugins/Android/gradleTemplate.properties org.gradle.jvmargs=-Xmx**JVM_HEAP_SIZE**M org.gradle.parallel=true android.enableR8=**MINIFY_WITH_R_EIGHT** + android.useAndroidX=true unityStreamingAssets=.unity3d**STREAMING_ASSETS** **ADDITIONAL_PROPERTIES** </code></pre> <p><strong>Gradle を利用した方法でライブラリを利用される際は、次の <code>External Dependency Manager for Unity で必要なパッケージをインストールする</code> の手順はスキップ可能です。<code>EncryptedSharedPreferences を利用するためのネイティブコードを追加する</code> のステップから進めてください。</strong></p> <p><code>External Dependency Manager for Unity</code> を利用する方法だと、取り込み先プロジェクト内でライブラリの競合が発生する恐れがあります。Gradle を利用する方法であれば回避が可能です。<sup id="fnref:1"><a href="#fn:1" class="footnote-ref" role="doc-noteref">1</a></sup></p> <h2 id="External Dependency Manager for Unity で必要なパッケージをインストールする"><a href="#External+Dependency+Manager+for+Unity+%E3%81%A7%E5%BF%85%E8%A6%81%E3%81%AA%E3%83%91%E3%83%83%E3%82%B1%E3%83%BC%E3%82%B8%E3%82%92%E3%82%A4%E3%83%B3%E3%82%B9%E3%83%88%E3%83%BC%E3%83%AB%E3%81%99%E3%82%8B">External Dependency Manager for Unity で必要なパッケージをインストールする</a></h2> <p><code>External Dependency Manager for Unity</code> をインポートするため <a target="_blank" rel="nofollow noopener" href="https://github.com/googlesamples/unity-jar-resolver/blob/master/external-dependency-manager-latest.unitypackage">unitypackage</a> をダウンロードして、<strong><code>EncryptedSharedPreferences</code> を導入したい Unity プロジェクトを開いてから <code>unitypackage</code> をクリックすることで、<code>External Dependency Manager for Unity</code> を Unity プロジェクトにインポートします。</strong></p> <p><img src="https://i.gyazo.com/1af7cdf4d7d5749e59e151eef1ca5493.png" alt="ダウンロードした <code>unitypackage</code> をクリックして Unity プロジェクトに External Dependency Manager for Unity をインポートする" /></p> <p>Unity プロジェクトの <code>Build Settings</code> からプラットフォームは Android に切り替えておきます。<code>Enable Android Auto-resolution?</code> というダイアログの選択肢はどちらを選んでも構いません。<sup id="fnref:2"><a href="#fn:2" class="footnote-ref" role="doc-noteref">2</a></sup></p> <p>External Dependency Manager for Unity で各種パッケージを管理する方法は <a target="_blank" rel="nofollow noopener" href="https://github.com/googlesamples/unity-jar-resolver#android-resolver-usage">README</a> に記載がある通り、<strong><code>*Dependencies.xml</code> というファイルを <code>Editor</code> フォルダに配置することで可能になります。</strong></p> <p>今回は <code>EncryptedSharedPreferences</code> を導入するため、下記の xml ファイルを <code>Editor</code> フォルダ内に配置します。</p> <pre><code class="xml"><!-- Assets/Editor/AndroidPluginDependencies.xml --> <?xml version="1.0" encoding="utf-8"?> <dependencies> <androidPackages> <!-- 本記事ではバージョン 1.1.0-alpha03 を利用している --> <androidPackage spec="androidx.security:security-crypto:1.1.0-alpha03"> <androidSdkPackageIds> <!-- Google の Maven リポジトリからインストールするため、 extra-google-m2repository を指定する --> <androidSdkPackageId>extra-google-m2repository</androidSdkPackageId> </androidSdkPackageIds> </androidPackage> </androidPackages> </dependencies> </code></pre> <p>その後、<strong>Unity メニューから <code>Assets -> External Dependency Manager -> Android Resolver -> Force Resolve</code> を選択して、<code>Assets/Editor/AndroidPluginDependencies.xml</code> の内容を元に <code>EncryptedSharedPreferences</code> を利用するのに必要なパッケージを自動で <code>Assets/Plugins/Android</code> フォルダにダウンロードします。</strong></p> <p><img src="https://i.gyazo.com/df394e15149e54dae3e9a81848512ee9.png" alt="1. Unity メニューから <code>Assets -> External Dependency Manager -> Android Resolver -> Force Resolve</code> を選択する" /><br /> <strong>1. Unity メニューから <code>Assets -> External Dependency Manager -> Android Resolver -> Force Resolve</code> を選択する</strong></p> <p><img src="https://i.gyazo.com/f6d2ec95ef9c2afdc857fecef2b165e5.png" alt="2. 実行に成功すると EncryptedSharedPreferences を利用するのに必要なライブラリ群が <code>Assets/Plugins/Android</code> フォルダに配置される" /><br /> <strong>2. 実行に成功すると EncryptedSharedPreferences を利用するのに必要なライブラリ群が <code>Assets/Plugins/Android</code> フォルダに配置される</strong></p> <p>ここまで来ればあとは Android ネイティブコードを <code>Assets/Plugins/Android</code> フォルダ内に配置して Unity 側から叩けるようにするだけです。</p> <h2 id="EncryptedSharedPreferences を利用するためのネイティブコードを追加する"><a href="#EncryptedSharedPreferences+%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B">EncryptedSharedPreferences を利用するためのネイティブコードを追加する</a></h2> <p>早速下記の Android ネイティブコードを <code>Assets/Plugins/Android~~<del>/SecretManager.java</code> に配置します。</p> <pre><code class="java">// Assets/Plugins/Android/SecretManager.java package com.nikaera; import com.unity3d.player.UnityPlayerActivity; import java.lang.Exception; // External Dependency Manager for Unity によって、 // 必要な jar が含まれているため EncryptedSharedPreferences の利用が可能になる import androidx.security.crypto.EncryptedSharedPreferences; import androidx.security.crypto.MasterKey; import android.content.Context; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.os.Bundle; import android.util.Log; public class SecretManager { private SharedPreferences sharedPreferences; public SecretManager(Context context) { try { // EncryptedSharedPreferences で設定値を保存する際に用いる、 // 暗号鍵を扱うためのラッパークラスをデフォルト設定で作成する MasterKey masterKey = new MasterKey.Builder(context) .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) .build(); // EncryptedSharedPreferences のインスタンスを生成する // コンストラクタで作成した masterKey を指定している this.sharedPreferences = EncryptedSharedPreferences.create( context, context.getPackageName(), masterKey, EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM ); } catch (Exception e) { e.printStackTrace(); } } /** * 指定したキーで値を保存する関数 * @param key 値を保存する際に用いるキー * @param value 保存したい値 * @return boolean 値の保存に成功したかどうか */ public boolean put(String key, String value) { SharedPreferences.Editor editor = sharedPreferences.edit(); editor.putString(key, value); return editor.commit(); } /** * 指定したキーで保存した値を取得する関数 * `put` 関数で保存した値を取得するのに利用する * @param key 取得したい値のキー * @return string キーに紐づく値、存在しなければ空文字が返却される */ public String get(String key) { return sharedPreferences.getString(key, null); } /** * 指定したキーで値を削除する関数 * @param key 削除したい値のキー * @return boolean 値の削除に成功したかどうか */ public boolean delete(String key) { SharedPreferences.Editor editor = sharedPreferences.edit(); editor.remove(key); return editor.commit(); } } </code></pre> <p>その後、上記を Unity スクリプトから実行可能にするための C# クラスを作成します。本記事ではファイルを <code>Assets/Scripts/EncryptedSharedPreferences.cs</code> に配置します。</p> <pre><code class="csharp">// Assets/Scripts/EncryptedSharedPreferences.cs</del><del> using UnityEngine; /// <summary> /// 利用するネイティブコードは <c>Assets/Plugins/Android/SecretManager.java</c> に記載 /// </summary> /// <remarks> /// <a href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">EncryptedSharedPreferences</a> /// </remarks> class EncryptedSharedPreferences { private readonly AndroidJavaObject _secretManager; public EncryptedSharedPreferences() { // コンストラクタで com.nikaera.SecretManager のインスタンス生成を行う var activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer") .GetStatic<AndroidJavaObject>("currentActivity"); var context = activity.Call<AndroidJavaObject>("getApplicationContext"); _secretManager = new AndroidJavaObject("com.nikaera.SecretManager", context); } public bool Put(string key, string value) { return _secretManager.Call<bool>("put", key, value); } public string Get(string key) { return _secretManager.Call<string>("get", key); } public bool Delete(string key) { return _secretManager.Call<bool>("delete", key); } } </code></pre> <p>あとは用途に応じて下記のようなコードで設定値の保存や取得などを行えます。</p> <pre><code class="csharp">// ... var _sharedPreferences = new EncryptedSharedPreferences(); // name をキーとして値を nikaera で保存する _sharedPreferences.Put("name", "nikaera"); // name をキーとして値を取得する var name = _sharedPreferences.Get("name"); // "nikaera" が出力される Debug.Log(name); // name をキーとして値を削除する _sharedPreferences.Delete("name"); // ... </code></pre> <h1 id="iOS のネイティブプラグインを作成する"><a href="#iOS+%E3%81%AE%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%E3%82%92%E4%BD%9C%E6%88%90%E3%81%99%E3%82%8B">iOS のネイティブプラグインを作成する</a></h1> <p>iOS の場合は外部ライブラリを利用しないため、<code>External Dependency Manager for Unity</code> は利用しません。<strong>本来であれば Swift で信頼できる外部フレームワークを取り込み利用できると良さそうですが、今回は Objective-C でネイティブプラグインを書いていきます。</strong><sup id="fnref:3"><a href="#fn:3" class="footnote-ref" role="doc-noteref">3</a></sup></p> <h2 id="Keychain Services を利用するためのネイティブコードを追加する"><a href="#Keychain+Services+%E3%82%92%E5%88%A9%E7%94%A8%E3%81%99%E3%82%8B%E3%81%9F%E3%82%81%E3%81%AE%E3%83%8D%E3%82%A4%E3%83%86%E3%82%A3%E3%83%96%E3%82%B3%E3%83%BC%E3%83%89%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B">Keychain Services を利用するためのネイティブコードを追加する</a></h2> <p>早速下記の iOS ネイティブコードを <code>Assets/Plugins/iOS/KeychainService.mm</code> に配置します。</p> <pre><code class="objc">// Assets/Plugins/iOS/KeychainService.mm</del>~~ // Keychain Services を利用するために Security フレームワークを利用する #import <Security/Security.h> extern "C" { // 指定したキーで値を保存する関数 // - param // - dataType: 値を保存する際に用いるキー // - value: 保存したい値 // - return // - 保存時のステータスコードを返却する (0 以外は失敗) int addItem(const char *dataType, const char *value) { NSMutableDictionary* attributes = nil; NSMutableDictionary* query = [NSMutableDictionary dictionary]; NSData* sata = [[NSString stringWithCString:value encoding:NSUTF8StringEncoding] dataUsingEncoding:NSUTF8StringEncoding]; [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass]; [query setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount]; OSStatus err = SecItemCopyMatching((CFDictionaryRef)query, NULL); if (err == noErr) { attributes = [NSMutableDictionary dictionary]; [attributes setObject:sata forKey:(id)kSecValueData]; [attributes setObject:[NSDate date] forKey:(id)kSecAttrModificationDate]; err = SecItemUpdate((CFDictionaryRef)query, (CFDictionaryRef)attributes); return (int)err; } else if (err == errSecItemNotFound) { attributes = [NSMutableDictionary dictionary]; [attributes setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass]; [attributes setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount]; [attributes setObject:sata forKey:(id)kSecValueData]; [attributes setObject:[NSDate date] forKey:(id)kSecAttrCreationDate]; [attributes setObject:[NSDate date] forKey:(id)kSecAttrModificationDate]; err = SecItemAdd((CFDictionaryRef)attributes, NULL); return (int)err; } else { return (int)err; } } // 指定したキーで値を取得する関数 // - param // - dataType: 値を取得する際に用いるキー // - return // - キーに紐づく値、存在しなければ空文字が返却される char* getItem(const char *dataType) { NSMutableDictionary* query = [NSMutableDictionary dictionary]; [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass]; [query setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount]; [query setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData]; CFDataRef cfresult = NULL; OSStatus err = SecItemCopyMatching((CFDictionaryRef)query, (CFTypeRef*)&cfresult); if (err == noErr) { NSData* passwordData = (__bridge_transfer NSData *)cfresult; const char* value = [[[NSString alloc] initWithData:passwordData encoding:NSUTF8StringEncoding] UTF8String]; char *str = strdup(value); return str; } else { return NULL; } } // 指定したキーで値を削除する関数 // - param // - dataType: 値を削除する際に用いるキー // - return // - 保存時のステータスコードを返却する (0 以外は失敗) int deleteItem(const char *dataType) { NSMutableDictionary* query = [NSMutableDictionary dictionary]; [query setObject:(id)kSecClassGenericPassword forKey:(id)kSecClass]; [query setObject:(id)[NSString stringWithCString:dataType encoding:NSUTF8StringEncoding] forKey:(id)kSecAttrAccount]; OSStatus err = SecItemDelete((CFDictionaryRef)query); if (err == noErr) { return 0; } else { return (int)err; } } } </code></pre> <p><code>Keychain Services</code> は <code>Security</code> フレームワークを利用するため、<strong><code>KeychainService.mm</code> に対して <code>Security</code> フレームワークの依存関係を設定する必要があります。</strong></p> <p><img src="https://i.gyazo.com/ba82aaced24b83b37bf8c63e1ee7142f.png" alt="<code>KeychainService.mm</code> で <code>Security</code> フレームワークの利用を可能にする" /><br /> <strong><code>KeychainService.mm</code> で <code>Security</code> フレームワークの利用を可能にする</strong></p> <p>その後、上記を Unity スクリプトから実行可能にするための C# クラスを作成します。本記事ではファイルを <code>Assets/Scripts/KeychainService.cs</code> に配置します。</p> <pre><code class="csharp">// Assets/Scripts/KeychainService.cs using System.Runtime.InteropServices; /// <summary> /// 実装は <c>Assets/Plugins/iOS/KeychainService.mm</c> に記載 /// </summary> /// <remarks> /// <a href="https://developer.apple.com/documentation/security/keychain_services">Keychain Services</a> /// </remarks> class KeychainService { #if UNITY_IOS [DllImport("__Internal")] private static extern int addItem(string dataType, string value); [DllImport("__Internal")] private static extern string getItem(string dataType); [DllImport("__Internal")] private static extern int deleteItem(string dataType); #endif public bool Put(string key, string value) { #if UNITY_IOS // 返却されるステータスが 0 なら成功 return addItem(key, value) == 0; #endif } public string Get(string key) { #if UNITY_IOS return getItem(key); #else return null; #endif } public bool Delete(string key) { #if UNITY_IOS // 返却されるステータスが 0 なら成功 return deleteItem(key) == 0; #endif } } </code></pre> <p>あとは用途に応じて下記のようなコードで設定値の保存や取得などを行えます。</p> <pre><code class="csharp">// ... var _keychainService = new KeychainService(); // name をキーとして値を nikaera で保存する _keychainService.Put("name", "nikaera"); // name をキーとして値を取得する var name = _keychainService.Get("name"); // "nikaera" が出力される Debug.Log(name); // name をキーとして値を削除する _keychainService.Delete("name"); // ... </code></pre> <h1 id="(余談) インターフェースで iOS/Android のふるまいを共通化する"><a href="#%28%E4%BD%99%E8%AB%87%29+%E3%82%A4%E3%83%B3%E3%82%BF%E3%83%BC%E3%83%95%E3%82%A7%E3%83%BC%E3%82%B9%E3%81%A7+iOS%2FAndroid+%E3%81%AE%E3%81%B5%E3%82%8B%E3%81%BE%E3%81%84%E3%82%92%E5%85%B1%E9%80%9A%E5%8C%96%E3%81%99%E3%82%8B">(余談) インターフェースで iOS/Android のふるまいを共通化する</a></h1> <p>このままだとプラットフォームを切り替える毎にコードを書き直さないとならないので、インターフェースを利用して共通化を行います。</p> <pre><code class="csharp">public interface ISecretManager { /// <summary> /// 指定したキーで値を保存する関数 /// </summary> /// <param name="key">キー</param> /// <param name="value">値</param> /// <returns>保存に成功したかどうか</returns> bool Put(string key, string value); /// <summary> /// 指定したキーの値を取得する関数 /// </summary> /// <param name="key">キー</param> /// <returns>指定したキーで設定された値、無ければ null</returns> string Get(string key); /// <summary> /// 指定したキーの値を削除する関数 /// </summary> /// <param name="key">キー</param> /// <returns>削除に成功したかどうか</returns> bool Delete(string key); } </code></pre> <p>その後、<code>Assets/Scripts/EncryptedSharedPreferences.cs</code> および <code>Assets/Scripts/KeychainService.cs</code> を下記の通り <code>ISecretManager</code> の実装に紐付けます。</p> <pre><code class="csharp">// Assets/Scripts/EncryptedSharedPreferences.cs using UnityEngine; /// <summary> /// 利用するネイティブコードは <c>Assets/Plugins/Android/SecretManager.java</c> に記載 /// </summary> /// <remarks> /// <a href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences">EncryptedSharedPreferences</a> /// </remarks> class EncryptedSharedPreferences: ISecretManager { private readonly AndroidJavaObject _secretManager; public EncryptedSharedPreferences() { var activity = new AndroidJavaClass("com.unity3d.player.UnityPlayer") .GetStatic<AndroidJavaObject>("currentActivity"); var context = activity.Call<AndroidJavaObject>("getApplicationContext"); _secretManager = new AndroidJavaObject("com.nikaera.SecretManager", context); } #region ISecretManager public bool Put(string key, string value) { return _secretManager.Call<bool>("put", key, value); } public string Get(string key) { return _secretManager.Call<string>("get", key); } public bool Delete(string key) { return _secretManager.Call<bool>("delete", key); } #endregion } </code></pre> <pre><code class="csharp">// Assets/Scripts/KeychainService.cs using System.Runtime.InteropServices; /// <summary> /// 実装は <c>Assets/Plugins/iOS/KeychainService.mm</c> に記載 /// </summary> /// <remarks> /// <a href="https://developer.apple.com/documentation/security/keychain_services">Keychain Services</a> /// </remarks> class KeychainService: ISecretManager { #if UNITY_IOS [DllImport("__Internal")] private static extern int addItem(string dataType, string value); [DllImport("__Internal")] private static extern string getItem(string dataType); [DllImport("__Internal")] private static extern int deleteItem(string dataType); #endif // KeychainService.mm に定義した関数を呼び出す #region ISecretManager public bool Put(string key, string value) { #if UNITY_IOS return addItem(key, value) == 0; #else return false; #endif } public string Get(string key) { #if UNITY_IOS return getItem(key); #else return null; #endif } public bool Delete(string key) { #if UNITY_IOS return deleteItem(key) == 0; #else return false; #endif } #endregion } </code></pre> <p>あとは上記をよしなに利用可能な <code>SecretManager</code> クラスを作成します。</p> <pre><code class="csharp">// Assets/Scripts/SecretManager.cs using UnityEngine; /// <summary> /// <em>Editor 利用時のみ PlayerPrefs を利用する</em> /// </summary> /// <remarks><see cref="KeychainService" />, <see cref="EncryptedSharedPreferences" /></remarks> public static class SecretManager { #if UNITY_EDITOR #elif UNITY_ANDROID private static ISecretManager _instance = new EncryptedSharedPreferences(); #elif UNITY_IOS private static ISecretManager _instance = new KeychainService(); #endif public static bool Put(string key, string value) { #if UNITY_EDITOR PlayerPrefs.SetString(key, value); PlayerPrefs.Save(); return true; #elif UNITY_IOS || UNITY_ANDROID return _instance.Put(key, value); #else Debug.Log("Not Implemented."); return false; #endif } public static string Get(string key) { #if UNITY_EDITOR return PlayerPrefs.GetString(key); #elif UNITY_IOS || UNITY_ANDROID return _instance.Get(key); #else Debug.Log("Not Implemented."); return null; #endif } public static bool Delete(string key) { #if UNITY_EDITOR PlayerPrefs.DeleteKey(key); PlayerPrefs.Save(); return true; #elif UNITY_IOS || UNITY_ANDROID return _instance.Delete(key); #else Debug.Log("Not Implemented."); return false; #endif } } </code></pre> <p>これでプラットフォーム間の実装差異を気にすることなく、下記のような記述で設定値の保存や取得などを行えます。<strong>iOS/Android 以外のプラットフォームで追加実装したい場合は<a target="_blank" rel="nofollow noopener" href="https://docs.unity3d.com/ja/2021.1/Manual/PlatformDependentCompilation.html">プラットフォーム依存コンパイル</a>と <code>ISecretManager</code> の実装クラスを新たに作成することで簡単に追加できます。</strong></p> <pre><code class="csharp">// ... // name をキーとして値を nikaera で保存する SecretManager.Put("name", "nikaera"); // name をキーとして値を取得する var name = SecretManager.Get("name"); // "nikaera" が出力される Debug.Log(name); // name をキーとして値を削除する SecretManager.Delete("name"); // ... </code></pre> <h1 id="おわりに"><a href="#%E3%81%8A%E3%82%8F%E3%82%8A%E3%81%AB">おわりに</a></h1> <p>今回は iOS/Android で設定値をセキュアに扱うための方法についてまとめてみました。実際は <code>Keychain Services</code> 周りは実装が大変なので、<code>External Dependency Manager for Unity</code> とか使って <a target="_blank" rel="nofollow noopener" href="https://github.com/kishikawakatsumi/KeychainAccess">KeychainAccess</a> のような外部ライブラリを利用する構成のほうが良いと思われます。</p> <p>本記事の内容に誤りがあったり、実際にはセキュアな実装ができていない等々あれば是非コメントでご指摘いただけますと幸いです。</p> <h1 id="参考リンク"><a href="#%E5%8F%82%E8%80%83%E3%83%AA%E3%83%B3%E3%82%AF">参考リンク</a></h1> <ul> <li><a target="_blank" rel="nofollow noopener" href="https://developer.android.com/topic/security/data?hl=ja">Android デベロッパー  |  Android Developers</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.android.com/reference/androidx/security/crypto/EncryptedSharedPreferences?hl=ja">EncryptedSharedPreferences  |  Android デベロッパー  |  Android Developers</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://developer.apple.com/documentation/security/keychain_services">Keychain Services | Apple Developer Documentation</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/masaki_shoji/items/6c512c7ebb30a13cda1d">SharedPreferences を自前で難読化するのはもう古い?これからは EncryptedSharedPrefenreces を使おう - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/sachiko-kame/items/261d42c57207e4b7002a">iOS のキーチェーンについて - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://qiita.com/nyhk-oi/items/189236d0627d43e7d658">Unity で IOS にセキュアに値を保存するには KeyChain を使おう - Qiita</a></li> <li><a target="_blank" rel="nofollow noopener" href="https://github.com/googlesamples/unity-jar-resolver">googlesamples/unity-jar-resolver: Unity plugin which resolves Android & iOS dependencies and performs version management</a></li> </ul> <div class="footnotes" role="doc-endnotes"> <hr /> <ol> <li id="fn:1" role="doc-endnote"> <p>逆に <code>External Dependency Manager for Unity</code> を利用する方法のメリットは、UnityPackage などでライブラリとして配布する際に、ライブラリを動作させるのに必要な外部パッケージも同梱した状態で配布が可能になるなどがあります。(当然ライセンスには気を付ける必要がありますが...) <a href="#fnref:1" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> <li id="fn:2" role="doc-endnote"> <p>パッケージの依存関係を自動で解決するかどうかという選択肢になります。本記事では明示的に Resolve を実行するため <code>Disable</code> でも <code>Enable</code> でも進行上の問題はありません。 <a href="#fnref:2" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> <li id="fn:3" role="doc-endnote"> <p><a target="_blank" rel="nofollow noopener" href="https://cocoapods.org/">CocoaPods</a> もサポートされているようなので、iOS でも Android 同様、外部ライブラリを取り込むのは簡単にできそうでした。例えば <a target="_blank" rel="nofollow noopener" href="https://github.com/kishikawakatsumi/KeychainAccess">KeychainAccess</a> とか使いたい。 <a href="#fnref:3" class="footnote-backref" role="doc-backlink">↩︎</a></p> </li> </ol> </div> nikaera tag:crieit.net,2005:PublicArticle/17062 2021-05-05T18:32:34+09:00 2021-05-05T18:32:34+09:00 https://crieit.net/posts/Big-iPhone-8 楽天Bigを買いました。iPhone 8 と半年かけて比べていきます <p>楽天Bigを購入した。別にさほど欲しかったわけではないのだけれど、楽天のUnlimitedプランの一年間無料が4/7までであったことから、駆け込みで申し込み、ついでに購入してみたという流れだ。中身はZTEだし、49,800円はちょっと高いなというのが実感だが、まぁ2万ポイント返ってくるので良しとする。今どきの楽天ポイントはRakuten Payで使えるし、良いだろう。</p> <p>これから半年ほどかけて、iPhone 8とどちらをメインにするか、自分の生活に合わせて検証していくつもりだ。</p> <h2 id="携帯料金値下げの流れ"><a href="#%E6%90%BA%E5%B8%AF%E6%96%99%E9%87%91%E5%80%A4%E4%B8%8B%E3%81%92%E3%81%AE%E6%B5%81%E3%82%8C">携帯料金値下げの流れ</a></h2> <p>携帯料金の値下げは菅総理肝いりということもあって、各社一斉に相当の値下げがなされた。MNOではDoCoMoのahamoが皮切りになったし、MVNOではIIJが口火を切って、さらにnuroが追いかけたという感じ。</p> <p>この流れは楽天にとっては逆風と思われ、実際ahamoにはかなり焦らされたのではなかろうか(まぁahamoに度肝を抜かれたのは業界全体そうだろうが…)。これ、ITリテラシーの低いそうのサポート代金が視覚化された側面もある(「<a target="_blank" rel="nofollow noopener" href="https://japanese.engadget.com/ntt-docomo-ahamo-211258897.html">20GBで2980円のahamoが安いわけ 実はドコモの本音も見え隠れ(佐野正弘) – Engadget 日本版</a>」)。</p> <p>まぁ言ってしまえばIT介護費用だったんだよね。それをなくせば安くなる、と。これは菅総理の思惑とは違ったかもしれないし、そもそも総理の介入は正直どうかと思うんだけれど、サポート費用に対価をつける流れができたのはいいことだと思う(「<a target="_blank" rel="nofollow noopener" href="https://japanese.engadget.com/docomo-ahamo-044542447.html">ドコモ、ahamoでも店頭サポート提供 ただし有償 – Engadget 日本版</a>」)。</p> <p>日本の労働生産性が非常に低い原因の一つが、学ぶ気のない、やってもらって当たり前気分の阿呆に合わせすぎていることだと思うんだ。対価を取ればいいんだけれど、無料でサポートするから、一部のテイカーのためにみんなが疲弊しているのだと思う。</p> <p>そういったサポートを必要としない人たちは多く、いわばMVNOの格安SIMなどは彼ら向けのサービスだったわけだが、楽天Unlimitedもそういうまともなリテラシーを持った人向けに訴求していた面があった。</p> <p>なので、ahamoの登場は楽天に逆風……と言えるが、そこで楽天は1GBまで無料、という恐るべきプランを打ち出し、このインパクトは非常に強いものとなった(「<a target="_blank" rel="nofollow noopener" href="https://japanese.engadget.com/rakuten-mobile-5g-055446721.html">楽天モバイル、20GBまで月1980円の段階制プラン 「1GBまで無料」 – Engadget 日本版</a>」)。</p> <p>いや実際、このプランに一番驚いたのは、IIJやnuroを始めとするMVNO勢力だよなぁ。格安SIMの愛好者が全部取られてもおかしくなさそうなエゲツないプラン……。</p> <p>なので、IIJやnuroは、1GB以上2,3GBまでならこっちのほうがお得、という微妙な間隙をつくような形になっている。</p> <p>ちなみに僕はBiglobe。なぜかというと、4月から半年間は480円/月のキャンペーンをやっているからだ。それが終われば1,280円/月になる。もとより加入時のキャッシュバックが大きかったから入ったに過ぎないのだし、半年後にはおさらばよ(MVNO利用者はこういう人が多いだろう)。</p> <h2 id="楽天Unlimitedを使うべく"><a href="#%E6%A5%BD%E5%A4%A9Unlimited%E3%82%92%E4%BD%BF%E3%81%86%E3%81%B9%E3%81%8F">楽天Unlimitedを使うべく</a></h2> <p>さて、問題は半年後、Biglobe後どこにするかだ。筆頭候補はnuroモバイルだったのだが、ギリギリ楽天Unlimitedも一年間無料キャンペーンの申し込みに間に合いそうだったので、入ってみた。</p> <p>僕が加入したのは本当にギリギリの4/7。この時点では、まだ楽天UnlimitedはiPhoneに正式対応していなかったし、ましてiPhone 8となればかなり特殊な対応をする必要があった。正直SIMのためにそこまでやるのはバカバカしかったので、iPhoneで無理にUnlimitedを使う気もなかった。まぁ楽天回線の実力もわからないので、メイン回線にするのは少し怖かったし。</p> <p>ということで、せっかくだから楽天Bigでも買ってみるかと思ったのだ。楽天Bigという名前はどう考えてもダサいが、まぁそれが楽天らしいのかもしれない。中身はZTEだと言われている(「<a target="_blank" rel="nofollow noopener" href="https://japanese.engadget.com/axion-20-5g-002023936.html">Rakuten BIGのベース? 中国で品切れ続く『世界初スマホ』ZTE AXON 20 5G(山根博士) – Engadget 日本版</a>」)。型番のZR01はZTE、Rakutenだったりするんだろうか。</p> <p>Rakuten Handという選択肢もないではなかったが、iPhone 8を持っている自分が、それを使う姿を想像できなかった。持ちやすさという点では、今やスモールデバイスに分類されるだろうiPhone 8で十分だからだ。その点、楽天Bigは6.9インチという巨大なディスプレイを持っている。したがって、当然持ちづらいことは予想できたが、その分ネットは見やすいはず。iPhone 8とは明らかに違う特性を持っているので、使う姿がまだ想像できた。</p> <p>ということで、楽天Bigを購入したわけだ。</p> <h2 id="楽天Bigの使い勝手"><a href="#%E6%A5%BD%E5%A4%A9Big%E3%81%AE%E4%BD%BF%E3%81%84%E5%8B%9D%E6%89%8B">楽天Bigの使い勝手</a></h2> <p>さて、楽天Bigを購入して一ヶ月が経過しようとしている。目論見どおり、<strong>iPhone 8とはまったく違う特徴を備えていることから、メインは相変わらずiPhone 8ではあるものの、サブスマホとしてけっこう活躍している</strong>。</p> <p>以下に所感を書く。</p> <h3 id="デカイ"><a href="#%E3%83%87%E3%82%AB%E3%82%A4">デカイ</a></h3> <p>とにかくデカイ。デカすぎる。しかも重い。ずっしりとした質量を感じる。コイツを持って読書でもしたら、ちょっとした筋トレになるかもしれない。</p> <p>が、よくよく考えてみるまでもなく本のほうが重い。つまり、<strong>楽天Bigは見た目以上の重量感がある</strong>、ということだ。あくまで重量感であるので、冷静に考えればそれほどの重さというわけではない……はずなんだが……重い。</p> <p>そして持ちづらい。ポケットにも入らない。いや、入らないことはないが、あまり入れたくはない。普段はカバンに入れている。これは男性に多いと思うが、ポケット族にとってはマイナスかもしれない。</p> <h3 id="画面上部に手が届かないが、考えられたアプリもある"><a href="#%E7%94%BB%E9%9D%A2%E4%B8%8A%E9%83%A8%E3%81%AB%E6%89%8B%E3%81%8C%E5%B1%8A%E3%81%8B%E3%81%AA%E3%81%84%E3%81%8C%E3%80%81%E8%80%83%E3%81%88%E3%82%89%E3%82%8C%E3%81%9F%E3%82%A2%E3%83%97%E3%83%AA%E3%82%82%E3%81%82%E3%82%8B">画面上部に手が届かないが、考えられたアプリもある</a></h3> <p>そして手が届かない。Twitterでツイートする時に指がつる。</p> <p>が、iPhoneやGalaxyも長細い形態を採用しているからか(というよりZTEはそれの真似だろうが)、案外そのへんについて考えられているアプリもある。たとえばブラウザアプリの<strong>Firefoxなどは、画面下部で検索からタブの選択など、多くの操作できるようになっていた</strong>。素晴らしい。</p> <p>逆に、古臭いアプリなどは画面下部に真っ黒な余白を残してくれていたりもする。悲しい。</p> <h3 id="ネットはさすがに快適"><a href="#%E3%83%8D%E3%83%83%E3%83%88%E3%81%AF%E3%81%95%E3%81%99%E3%81%8C%E3%81%AB%E5%BF%AB%E9%81%A9">ネットはさすがに快適</a></h3> <p>前述の「重い」ということを除けば、<strong>ネットはiPhone 8 よりも随分と快適</strong>。iPhone 8だと、冗談抜きで画面の半分くらいは広告やタブバーなどに占領されてしまうのがつらかったが、楽天Bigなら全然平気。特にその長細さ故に、広告は横軸で大きさを規定され、肝心の縦軸の侵食具合が減ったのは嬉しい。</p> <h3 id="バッテリーの持ちはテザリングで半日置いといても半分くらいだった"><a href="#%E3%83%90%E3%83%83%E3%83%86%E3%83%AA%E3%83%BC%E3%81%AE%E6%8C%81%E3%81%A1%E3%81%AF%E3%83%86%E3%82%B6%E3%83%AA%E3%83%B3%E3%82%B0%E3%81%A7%E5%8D%8A%E6%97%A5%E7%BD%AE%E3%81%84%E3%81%A8%E3%81%84%E3%81%A6%E3%82%82%E5%8D%8A%E5%88%86%E3%81%8F%E3%82%89%E3%81%84%E3%81%A0%E3%81%A3%E3%81%9F">バッテリーの持ちはテザリングで半日置いといても半分くらいだった</a></h3> <p>バッテリーについてだが、今の所困っていない……ものの、メインスマホとして使っていないために、まだまだ評価できない。</p> <p>ただ、<strong>テザリングで半日ほどiPhoneに繋げていても、50%にもなっていなかったことから、ヘタなモバイルルータを使うよりもよほど良いものである</strong>ことが示唆される。</p> <h3 id="指紋認証は素晴らしい!"><a href="#%E6%8C%87%E7%B4%8B%E8%AA%8D%E8%A8%BC%E3%81%AF%E7%B4%A0%E6%99%B4%E3%82%89%E3%81%97%E3%81%84%EF%BC%81">指紋認証は素晴らしい!</a></h3> <p><strong>全面ディスプレイだが、指紋認証が使える</strong>。しかも、背面のなんか押しづらいところにあるのではなく、画面下部の、iPhone 8以下でいうところのホームボタンに位置するところにあって、これがめちゃくちゃ使いやすい。正直一番感動したのはこれかもしれない。</p> <h3 id="スペックでは圧倒しているのに!Androidの悲哀、ゲーム"><a href="#%E3%82%B9%E3%83%9A%E3%83%83%E3%82%AF%E3%81%A7%E3%81%AF%E5%9C%A7%E5%80%92%E3%81%97%E3%81%A6%E3%81%84%E3%82%8B%E3%81%AE%E3%81%AB%EF%BC%81Android%E3%81%AE%E6%82%B2%E5%93%80%E3%80%81%E3%82%B2%E3%83%BC%E3%83%A0">スペックでは圧倒しているのに!Androidの悲哀、ゲーム</a></h3> <p>スマホゲーをよくしている人にとっては、やはりAndroidであることの悲哀を感じずにはいられないかもしれない。<strong>Antutuのベンチなどを見ると、楽天Bigが圧倒しているはずなのだが、実際に動かしてみるとiPhone 8のほうがヌルヌル動く</strong>。2021年になっても変わらないのだなぁ。いや、楽天Bigはたしか2018年のデバイスだけれど。まぁでも、今も変わらないんだろうな。</p> <p>まぁ、僕は大したゲームをしないので、まぁ良いんだが。ただ、その長細さが災いして、プレイしづらいゲームはけっこうあるかもしれない。</p> <h3 id="クラウドのおかげで移行がかんたん"><a href="#%E3%82%AF%E3%83%A9%E3%82%A6%E3%83%89%E3%81%AE%E3%81%8A%E3%81%8B%E3%81%92%E3%81%A7%E7%A7%BB%E8%A1%8C%E3%81%8C%E3%81%8B%E3%82%93%E3%81%9F%E3%82%93">クラウドのおかげで移行がかんたん</a></h3> <p>これは別に楽天Bigの特徴ではないが、嬉しかったのは多くのサービスで移行がすんなりいったことだ。SpotifyやAmazon Kindle, Audibleなど、サービスの移行が非常にかんたんであった。いや、共有といったほうがいいかな。iOS、Android間の移行は極めて楽になっている。</p> <p>特に感動したのはQRコード決済である。本当に楽。スペックではSuicaが圧倒しているかもしれないが、トータルのユーザーエクスペリエンスはQRコード決済が圧倒的である。交通決済でもQRコードの試験が実施されているし、やがてSuicaはQRコード決済に敗北するのではなかろうか。スペックはたしかに良いが、運用がたいへん過ぎる。導入コストも大きい。Suicaは良くも悪くも日本的技術革新の極みという感じがする。</p> <h2 id="半年かけて見定めるよ"><a href="#%E5%8D%8A%E5%B9%B4%E3%81%8B%E3%81%91%E3%81%A6%E8%A6%8B%E5%AE%9A%E3%82%81%E3%82%8B%E3%82%88">半年かけて見定めるよ</a></h2> <p>ということで、与太話も含めて随分と長くなってしまったが、少なくともサブスマホとしては思っていたより活躍してくれているし、楽天Bigだけ持って出歩くこともけっこうある。</p> <p>今後、半年ほどかけて、iPhone 8と比較し、自分に合うのはどちらなのか、見極めていくつもりだ。</p> tama tag:crieit.net,2005:PublicArticle/16800 2021-04-01T22:34:12+09:00 2021-04-01T22:34:12+09:00 https://crieit.net/posts/Flutter2-Android-localhost Flutter2だとAndroidのlocalhostに接続する方法が違う? <p>Androidでlocalhost(ただしくは10.0.2.2)に接続するため、元々 network_security_configbak.xml に許可設定を書いていたプロジェクトがあった。しかし、Flutter2にあげると接続できなくなってしまった。</p> <p>色々試してみていたところ、もうそのxmlファイルは不要なので削除してAndroidManifest.xmlからも参照を削除し、debugの方の <code>android\app\src\debug\AndroidManifest.xml</code> のmanifestタグの中、uses-permissionの隣に下記を追記したら接続できるようになった。</p> <pre><code class="xml"> <application android:usesCleartextTraffic="true" /> </code></pre> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/16697 2021-02-21T23:12:18+09:00 2021-02-22T11:36:07+09:00 https://crieit.net/posts/Flutter-Firebase-Analytics FlutterのAndroidで最新のFirebase Analytics入れるとエラーになる件について <p>FlutterにてAndroidで開発をしていた時に、最新のFirebase Analyticsであるfirebase_analytics 7.0.1 を利用するとなんかビルド時にこんなエラーが出る。全くよくわからない。</p> <pre><code class="text">FAILURE: Build failed with an exception. * What went wrong: Could not determine the dependencies of task ':app:compileDebugJavaWithJavac'. > Could not resolve all dependencies for configuration ':app:debugRuntimeClasspath'. > Problems reading data from Binary store in C:\Users\user\AppData\Local\Temp\gradle1639466432759139497.bin offset 194769 exists? true </code></pre> <p>このとおりにしたら直った。<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/gradle/gradle/issues/8489#issuecomment-774436851">https://github.com/gradle/gradle/issues/8489#issuecomment-774436851</a></p> <p>具体的にはbuild.gradleで指定しているパッケージのバージョンを上げる。</p> <p>app配下の build.gradle</p> <pre><code class="text">com.google.firebase:firebase-analytics:18.0.2 </code></pre> <p>プロジェクト配下の build.gradle</p> <pre><code class="text">com.google.gms:google-services:4.3.5 </code></pre> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/16607 2021-01-15T22:31:37+09:00 2021-01-15T22:32:00+09:00 https://crieit.net/posts/flutter-ffmpeg-proguard flutter_ffmpegもproguardの設定が必要 <p>flutter_ffmpegもproguardの設定が必要だった。なしだとリリースビルドの場合だけ落ちる。</p> <pre><code class="text">java.lang.RuntimeException: An error occurred while executing doInBackground() </code></pre> <p>とか</p> <pre><code class="text">java.lang.UnsatisfiedLinkError: Bad JNI version returned from JNI_OnLoad </code></pre> <p>とかのエラーが出てる。</p> <p>android/app/proguard-rules.pro として下記のようなファイルを作っておけばちゃんとビルドされるっぽい。</p> <pre><code class="text">-keep class com.arthenica.mobileffmpeg.Config { native <methods>; void log(long, int, byte[]); void statistics(long, int, float, float, long , int, double, double); } -keep class com.arthenica.mobileffmpeg.AbiDetect { native <methods>; } </code></pre> <p>ほんと心臓に悪いproguard</p> <p>参考)<br /> <a target="_blank" rel="nofollow noopener" href="https://github.com/tanersener/flutter-ffmpeg/issues/237">https://github.com/tanersener/flutter-ffmpeg/issues/237</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/16413 2020-12-23T22:35:18+09:00 2020-12-23T22:35:18+09:00 https://crieit.net/posts/Android-offline Androidのエミュレータがofflineとなってデバッグできない時 <p>FlutterでAndroidエミュレータを起動して開発しようとするとなぜか突然デバッグが全くできなくなった。adb devicesしてみるとエミュレータが offline と表示される。</p> <p>詳しくはよくわからないが、AVDマネージャの該当のエミュレータで Wipe Data を実行してデータを初期化? したところちゃんと起動するようになった。</p> <p><a href="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5fe3474f296b7.png" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/6e2af66c610d88bc766649f72032893a5fe3474f296b7.png?mw=700" alt="" /></a></p> <p>理由などは謎。</p> <p>参考<br /> <a target="_blank" rel="nofollow noopener" href="https://stackoverflow.com/questions/3152681/android-emulator-5554-offline">https://stackoverflow.com/questions/3152681/android-emulator-5554-offline</a></p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/16173 2020-10-25T22:42:20+09:00 2020-10-25T22:48:42+09:00 https://crieit.net/posts/bundletool-build-apks bundletool build-apksでたくさんエラーが出る時 <p>いつものようにFlutterで作成したaabファイルを実機で試そうと思いapksに変換するため <code>bundletool build-apks</code> を実行しようとしたところ大量にエラーが出た。Windowsにて。</p> <p>bundletool自体のバージョンが低かった(0.13くらい)ため最新の 1.3.0 にバージョンアップした。しかしまだエラーが出る。ただ、よく見るとエラーの内容が変わっていた。こんな感じ。最初は関係ないかと思ったが、どうもOutOfMemoryErrorが怪しいっぽい。</p> <pre><code class="text">[BT:1.3.0] Error: java.io.IOException: Failed to obtain compression information for entry java.io.UncheckedIOException: java.io.IOException: Failed to obtain compression information for entry at com.android.tools.build.bundletool.io.ApkzlibApkSerializerHelper.writeToZipFile(ApkzlibApkSerializerHelper.java:103) at com.android.tools.build.bundletool.io.StandaloneApkSerializer.writeToDiskInternal(StandaloneApkSerializer.java:76) at com.android.tools.build.bundletool.io.StandaloneApkSerializer.writeToDisk(StandaloneApkSerializer.java:43) at com.android.tools.build.bundletool.io.ApkSetBuilderFactory$ApkSetArchiveBuilder.addStandaloneApk(ApkSetBuilderFactory.java:130) at com.android.tools.build.bundletool.io.ApkSerializerManager.lambda$null$3(ApkSerializerManager.java:221) at com.google.common.util.concurrent.TrustedListenableFutureTask$TrustedFutureInterruptibleTask.runInterruptibly(TrustedListenableFutureTask.java:117) at com.google.common.util.concurrent.InterruptibleTask.run(InterruptibleTask.java:38) at com.google.common.util.concurrent.TrustedListenableFutureTask.run(TrustedListenableFutureTask.java:77) at java.util.concurrent.ThreadPoolExecutor.runWorker(Unknown Source) at java.util.concurrent.ThreadPoolExecutor$Worker.run(Unknown Source) at java.lang.Thread.run(Unknown Source) Caused by: java.io.IOException: Failed to obtain compression information for entry at com.android.tools.build.apkzlib.zip.ZFile.processAllReadyEntries(ZFile.java:1890) at com.android.tools.build.apkzlib.zip.ZFile.add(ZFile.java:1765) at com.android.tools.build.apkzlib.zip.ZFile.add(ZFile.java:1747) at com.android.tools.build.apkzlib.zip.ZFile.add(ZFile.java:1711) at com.android.tools.build.bundletool.io.ApkzlibApkSerializerHelper.addFile(ApkzlibApkSerializerHelper.java:247) at com.android.tools.build.bundletool.io.ApkzlibApkSerializerHelper.addNonAapt2Files(ApkzlibApkSerializerHelper.java:239) at com.android.tools.build.bundletool.io.ApkzlibApkSerializerHelper.writeToZipFile(ApkzlibApkSerializerHelper.java:144) at com.android.tools.build.bundletool.io.ApkzlibApkSerializerHelper.writeToZipFile(ApkzlibApkSerializerHelper.java:101) ... 11 more Caused by: java.util.concurrent.ExecutionException: java.lang.OutOfMemoryError: Java heap space at com.google.common.util.concurrent.AbstractFuture.getDoneValue(AbstractFuture.java:502) at com.google.common.util.concurrent.AbstractFuture.get(AbstractFuture.java:461) at com.google.common.util.concurrent.AbstractFuture$TrustedFuture.get(AbstractFuture.java:83) at com.android.tools.build.apkzlib.zip.ZFile.processAllReadyEntries(ZFile.java:1883) ... 18 more Caused by: java.lang.OutOfMemoryError: Java heap space </code></pre> <h2 id="解決法"><a href="#%E8%A7%A3%E6%B1%BA%E6%B3%95">解決法</a></h2> <p>調べてみたところ、やはりメモリエラーなのでは……ということで、メモリ上限の設定を入れてみた。Windowsのため、他のOSと同様のコマンドで使えるよう、下記のようなbundletool.batファイルを作成して使っていた。</p> <pre><code class="text">java -jar C:\NVPACK\jdk1.8.0_77\bin\bundletool-all-1.3.0.jar %* </code></pre> <p>これに下記のようなパラメータを付けてみた。5Gとか大きすぎるとそんなパラメータは使えないというエラーが出たので、下げていって最終的に1Gになった。環境によって違うのかもしれない。</p> <pre><code class="text">java -Xmx1G -jar C:\NVPACK\jdk1.8.0_77\bin\bundletool-all-1.3.0.jar %* </code></pre> <h2 id="原因"><a href="#%E5%8E%9F%E5%9B%A0">原因</a></h2> <p>原因は全然わからないが、最近Javaのアップデートが実行された気がするのでそのあたりなのかも?</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/16136 2020-10-16T21:59:22+09:00 2020-10-16T22:00:52+09:00 https://crieit.net/posts/Godot-Android-AdMob GodotのAndroid用AdMobプラグインがちゃんと動かない場合 <p>Godot3.2.2ではAndroid用のモジュールをプラグイン形式で簡単に追加できるようになっている。そこで色々使っているのだが、AdMobだけどうしてもうまく動かないので古いモジュールを使っていた。</p> <p>しかし、それはそれで内部の処理の呼ばれ方がおかしかったりするようで(何度か初期化が呼ばれたりしているようだった)、広告を非表示にできなかったりしておかしいため、再度プラグイン形式にチャレンジした。</p> <p>しかし、やはりプラグインを有効にすると画面が真っ暗になって何も表示されなくなってしまうという問題が発生した。下記のようなエラーが出ているっぽかった。</p> <pre><code class="text">invalid call. nonexistent function 'initwithcontentrating' in base 'JNISingleton' </code></pre> <h2 id="解決法"><a href="#%E8%A7%A3%E6%B1%BA%E6%B3%95">解決法</a></h2> <p>いろいろ調べているうちに、下記のissueにたどり着いた。</p> <p><a target="_blank" rel="nofollow noopener" href="https://github.com/Shin-NiL/Godot-Android-Admob-Plugin/issues/84"><strong>ERROR</strong>: In Object of type 'JNISingleton' · Issue #84 · Shin-NiL/Godot-Android-Admob-Plugin</a></p> <p>最新のバージョン4.1.1のプラグインを使え、とのこと。しかし、僕はGodot3.2.2.を使っていたので、最新の3.2.3でビルドされたプラグインを利用してはダメなのかと思い最新ではない4.0.0を使っていた。半信半疑で4.1.1を使ってみると、なんと動いた……!</p> <p>ということでよくわからないが解決。謎。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/16135 2020-10-15T09:57:38+09:00 2020-10-15T09:57:38+09:00 https://crieit.net/posts/Godot-Error-listening-on-port-6007 Godotで Error listening on port 6007 と出る場合 <p>GodotのアプリケーションをAndroidの実機でリモートデバッグしている場合に、 Error listening on port 6007 と出てデバッグ情報を取得できなくなる場合がある。(Godot3.2.2現在)</p> <p>これは何かというと恐らくGodotの不具合だと思うが、正常にリモートデバッグが終了できずにプロセスが残ってしまい、そのポートを専有し続けてしまう。こうなってしまうとPCを再起動しないと回復しない。直接タスクを削除できないかと思ったが今のところ成功していない。</p> <h2 id="発生した場合の改善方法"><a href="#%E7%99%BA%E7%94%9F%E3%81%97%E3%81%9F%E5%A0%B4%E5%90%88%E3%81%AE%E6%94%B9%E5%96%84%E6%96%B9%E6%B3%95">発生した場合の改善方法</a></h2> <p>リモートデバッグの設定を一旦オフにして、エディタ設定のNetwork→Debug→Remote Portの設定を適当に変えれば良い。Godotは他にも近くのポートを使っているので、6010以上にしておけば問題ない。そしてまたリモートデバッグの設定をオンにして試せば正常に動き出す。</p> <h2 id="発生させない方法"><a href="#%E7%99%BA%E7%94%9F%E3%81%95%E3%81%9B%E3%81%AA%E3%81%84%E6%96%B9%E6%B3%95">発生させない方法</a></h2> <p>このあたりはちょっと分かっていないが、前述の改善方法を試すとあとは再現しにくくなったような気もする。が、確実なのはAndroid側のアプリを、タスク一覧から強制終了させること。こうすることできっちりGodotのリモートデバッグが終了し、発生しなくなる(が、もしかしたらGodot起動後の最初のデバッグは発生してしまうかもしれない)</p> <p>という感じでなんとも曖昧な情報で申し訳ないが、とにかくこのようなやりかたで発生する回数を減らすことや、いちいちPCを再起動しなくてもすむため、だいぶ楽になる。</p> だら@Crieit開発者 tag:crieit.net,2005:PublicArticle/15995 2020-07-03T14:57:19+09:00 2020-07-03T14:57:19+09:00 https://crieit.net/posts/Flutter-IdeaShuffleMemo 新作Flutterアプリ「IdeaShuffleMemo」を広めるために考えている戦略 <p><a href="https://crieit.now.sh/upload_images/0c5772e78aa0f279ed29c9e177857c365efec751c86bc.jpg" target="_blank" rel="nofollow noopener"><img src="https://crieit.now.sh/upload_images/0c5772e78aa0f279ed29c9e177857c365efec751c86bc.jpg?mw=700" alt="image" /></a><br /> こんにちは、YuKiOです。</p> <p>7月にFlutterアプリ開発を始めて2作目のアプリ、アイデアを発想するためのメモアプリ「IdeaShuffleMemo」をリリースしました。</p> <p>■AppStore<br /> <a target="_blank" rel="nofollow noopener" href="https://apps.apple.com/jp/app/id1517535550">https://apps.apple.com/jp/app/id1517535550</a></p> <p>■Google Play<br /> <a target="_blank" rel="nofollow noopener" href="https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja">https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja</a></p> <p>1作目はとりあえずリリースすることを目的に作成しましたが、今回は「自分が欲しいもの」というテーマで作成しました。</p> <p>さらに今後のアプリ開発を含めて、機能やアプリの露出方法についても挑戦的に戦略を考えています。</p> <p>このアプリのトライアンドエラーの経験を、次回につなげていければと思っています。</p> <p>機能については、Qiitaに書いています。<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/YuKiO-OO/items/283f44da64d304a6228e">https://qiita.com/YuKiO-OO/items/283f44da64d304a6228e</a></p> <p>crieitでは、どうやって露出させていくか?考えていることを、書いていきたいと思います。</p> <h3 id="全体の戦略"><a href="#%E5%85%A8%E4%BD%93%E3%81%AE%E6%88%A6%E7%95%A5">全体の戦略</a></h3> <h4 id="アプリの収益化"><a href="#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E5%8F%8E%E7%9B%8A%E5%8C%96">アプリの収益化</a></h4> <p>せっかくアプリ開発をするのだから、お金を稼げるようになりたいです。なので、収益を得る方法として、「広告」と「サブスクリプション」の2つを用意しました。</p> <p>サブスクリプションも採用した理由は、コロナの影響もあり「広告」落ち込んでいたこと、広告収入という方法が今後変化する可能性が高く、安定したものではないからです。</p> <p>単発の課金のほうが難易度が探るかもしれませんが、長期的にみて、開発の継続、安定した収益を目指す上ではサブスクリプションがいいと判断しました。</p> <p>最終的に「お金を払ってでも使ってもらえるものを作る」というのが最終的な目標もあり、かなり苦労しましたがサブスクリプション機能を追加しました。</p> <h4 id="アプリの多言語化"><a href="#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E5%A4%9A%E8%A8%80%E8%AA%9E%E5%8C%96">アプリの多言語化</a></h4> <p>日本のアプリ市場は世界的に見ても大きいほうだとは思います。<br /> ただ、1国で月1万円稼げるレベルのアプリだったとして、日本だけにリリースすれば、1万円しか稼げません。もし全世界に配信することが出来れば、市場も広がるので、稼げるチャンスは増えるはず。(そこまで単純なものではないと思いますが)</p> <p>なのでアプリは、最初から多言語化を意識して作りました。<br /> 初めから7カ国ご対応は辛いので、英語と日本語の2つに対応をさせました。</p> <p>それに合わせて、アプリのキャプチャーや紹介文も英語で用意も。</p> <p>英語圏でも反応が少しでれば、クラウドワークスなどを使って、ネイティブにPRできるような文章を作り込んでいきながら、対応できる言語を増やしていくつもりです。</p> <p>まずは英語と日本語圏で収益を出せるかで、継続の判断をしていきたいと思います。</p> <h4 id="アプリのターゲット"><a href="#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E3%82%BF%E3%83%BC%E3%82%B2%E3%83%83%E3%83%88">アプリのターゲット</a></h4> <p>アプリのターゲットユーザーは、主にアプリやサービス開発者、ビジネスマン、経営者、起業を考えている人です。<br /> まずは、自分と近い範囲のアプリやサービスの開発者に広めたいなと思っています。</p> <h3 id="アプリの具体的な露出戦略"><a href="#%E3%82%A2%E3%83%97%E3%83%AA%E3%81%AE%E5%85%B7%E4%BD%93%E7%9A%84%E3%81%AA%E9%9C%B2%E5%87%BA%E6%88%A6%E7%95%A5">アプリの具体的な露出戦略</a></h3> <h4 id="■国内での露出"><a href="#%E2%96%A0%E5%9B%BD%E5%86%85%E3%81%A7%E3%81%AE%E9%9C%B2%E5%87%BA">■国内での露出</a></h4> <h5 id="・ツイッター"><a href="#%E3%83%BB%E3%83%84%E3%82%A4%E3%83%83%E3%82%BF%E3%83%BC">・ツイッター</a></h5> <p>まずは自身のツイッターアカウントで告知します。ただこれは、Webサービスの時と同様あまり期待はしていません。フォロワー数が少ない、インフルエンサーでもないので、よほどインパクトがあるサービスでなければ、「リリースしました」のような広告っぽい投稿は敬遠されて反応がでません。</p> <p>ただ少しでも反応を出すために、審査の間でPR動画を作ってみました。(Appleぽさを意識しながら、最後はメタルギアを意識している裏エピソードがあります。)</p> <p>カメラなど映像も趣味なので、別の趣味がいかせて良かったです。</p> <blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Webサービスやアプリ!斬新なアイデアをひらめきたい人に!アイデアを発想するためのメモアプリ✍️IdeaShuffleMemoをリリースしました!■AppStore<a target="_blank" rel="nofollow noopener" href="https://t.co/gEPzEEJ7mt">https://t.co/gEPzEEJ7mt</a>■Google Play<a target="_blank" rel="nofollow noopener" href="https://t.co/w0vTiOanGE">https://t.co/w0vTiOanGE</a>使ってみてください😁<a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E9%A7%86%E3%81%91%E5%87%BA%E3%81%97%E3%82%A8%E3%83%B3%E3%82%B8%E3%83%8B%E3%82%A2%E3%81%A8%E7%B9%8B%E3%81%8C%E3%82%8A%E3%81%9F%E3%81%84?src=hash&ref_src=twsrc%5Etfw">#駆け出しエンジニアと繋がりたい</a> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/hashtag/%E3%83%97%E3%83%AD%E3%82%B0%E3%83%A9%E3%83%9F%E3%83%B3%E3%82%B0?src=hash&ref_src=twsrc%5Etfw">#プログラミング</a> <a target="_blank" rel="nofollow noopener" href="https://t.co/gQ0dOMNgGB">pic.twitter.com/gQ0dOMNgGB</a></p>— YuKiO | 個人開発&Flutter (@oo_forward) <a target="_blank" rel="nofollow noopener" href="https://twitter.com/oo_forward/status/1278611399749431296?ref_src=twsrc%5Etfw">July 2, 2020</a></blockquote> <p>「#駆けだしエンジニアと繋がりたい」「#プログラミング初心者」のタグは、タグ検索をしているユーザーが多いのでつけています。</p> <p>ただ、特に内容を見ずにいいねを押しているユーザーもいるので、いいねの割りには反応は薄かったりします。</p> <p>まあ、固定ツイートなどの時の見た目として、いいねとリツイートが多い方がいいので、この方法を採用しました。</p> <h5 id="・Qiitaに投稿"><a href="#%E3%83%BBQiita%E3%81%AB%E6%8A%95%E7%A8%BF">・Qiitaに投稿</a></h5> <p>これはアプリの露出と、アプリ制作でお世話になった数多くの方々への恩返し、自分自身への備忘録&技術の精査の意味も含まれていますが、Qiitaに技術記事がメインで、説明の一貫としてアプリの紹介をするのは問題ないかなと思ってます。</p> <p>第1弾は、アプリ開発で使ったパッケージを紹介、それ以降はパスワードロック、データのバックアップ、サブスクリプションなどの、Flutter開発で情報が少なくて困った内容を書いていこうと考えています。</p> <p>■第1段「おすすめのパッケージ」<br /> <a target="_blank" rel="nofollow noopener" href="https://qiita.com/YuKiO-OO/items/283f44da64d304a6228e">https://qiita.com/YuKiO-OO/items/283f44da64d304a6228e</a></p> <p>おかげ様で、現時点で1100viewにはなっているので、それなりに露出としては成功しているかなと思います。</p> <h5 id="・Crieit"><a href="#%E3%83%BBCrieit">・Crieit</a></h5> <p>もちろん開発者のコミュニティであるこちらにも投稿。他が技術的な内容になっているので、ここではこのようにアプリを露出させる方法として、内容ががぶらないようにしています。</p> <h5 id="・自分のブログサイト"><a href="#%E3%83%BB%E8%87%AA%E5%88%86%E3%81%AE%E3%83%96%E3%83%AD%E3%82%B0%E3%82%B5%E3%82%A4%E3%83%88">・自分のブログサイト</a></h5> <p>自分のブログ記事は、アプリの紹介ページのような位置付けで書いています。<br /> 今後はアプリのQandAなどを追加して、情報を充実させていきたいと思います。</p> <p>予算とアプリの反響によっては、こちらも多言語対応できれば理想ですね。</p> <h5 id="・プレスリリース"><a href="#%E3%83%BB%E3%83%97%E3%83%AC%E3%82%B9%E3%83%AA%E3%83%AA%E3%83%BC%E3%82%B9">・プレスリリース</a></h5> <p>Webサービスではやったことがないのですが、プレスリリースにも挑戦しています。プレスリリースの難しいところは、個人を受け付けてなかったり、有料で、さらに1回に数万円と高額な場合があったりと、個人開発レベルでは手が出しづらい所です。</p> <p>本業ではプレスリリースを使っているのですが、無料の場合はやはり無料レベルに留まることがおおく、あまり期待はできません。</p> <p>ただ、今回は1アカウントお試して無料でできる、value pressさんを使いました。<br /> <a target="_blank" rel="nofollow noopener" href="https://www.value-press.com/">https://www.value-press.com/</a></p> <p>無料では配信できるメディアなど限られますが、有名なメディアや、ターゲットが好みそうなメディアも多いのが選んだ理由です。</p> <p>ただ、記者は開発者ではないので、アピールする部分を少しヅラすことを意識しました。</p> <p>ターゲットを、コロナ禍で厳しい環境にたっていて、何か変えたいと必死で考えている人向けとすることにして、コロナとは言葉に出していませんが、プレスリリースタイトルは、それを強くい意識した内容になっています。</p> <p>ここで少しでも反応がでれば、ある程度お金をかけてもいいかなと思っています。</p> <p>個人での注意点ですが、電話やメールアドレスが公開されます。勧誘メールが届くようになったりと、色々大変です。</p> <p>公開しても良いメールアドレスや、格安のIP電話を契約して、それを公開するようにしましょう。</p> <h5 id="・ツイッター・フェイスブック広告"><a href="#%E3%83%BB%E3%83%84%E3%82%A4%E3%83%83%E3%82%BF%E3%83%BC%E3%83%BB%E3%83%95%E3%82%A7%E3%82%A4%E3%82%B9%E3%83%96%E3%83%83%E3%82%AF%E5%BA%83%E5%91%8A">・ツイッター・フェイスブック広告</a></h5> <p>これはちょっと迷ってます。今回は実験ですが、それなりに広告費用をかけてみようと思います。<br /> これまでもツイッター広告をつかっているのですが、あまりパフォーマンスがよくありません。</p> <p>もしやるとしたらフェイスブック広告を出してみようかなと思っています。</p> <p>ブログ記事であったり、いくつかの反応を見てから、実行する予定です。</p> <h4 id="■海外戦略"><a href="#%E2%96%A0%E6%B5%B7%E5%A4%96%E6%88%A6%E7%95%A5">■海外戦略</a></h4> <h5 id="・Div"><a href="#%E3%83%BBDiv">・Div</a></h5> <p>海外のQiitaのようなサイトを探してみたのですが、stackoverflowが紹介されていましたが、なんとなく質問コーナーという感じで、技術ブログのようなものではありませんでした。</p> <p>Divはそこそこライトな感じの投稿が多そうだったので、こちらに投稿してみました。<br /> 英語は得意ではありませんが、Google翻訳を活用しながら、自分で書いたQiitaの記事を抜き出し、翻訳しました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://dev.to/oo_forward/flutter-recommended-package-used-in-the-developed-memo-application-15dl">https://dev.to/oo_forward/flutter-recommended-package-used-in-the-developed-memo-application-15dl</a></p> <p>Flutterに記事や、iOSの記事についてそこまでいいねがされていたりする記事がなかったり、どれぐらい動いているのか分かりませんが、2時間まえくらいに投稿した時点で、2件くらい「いいね!」がもらえているので、動いているのは動いているっぽいです。</p> <h5 id="・producthunt"><a href="#%E3%83%BBproducthunt">・producthunt</a></h5> <p><a href="https://crieit.net/posts/7">https://crieit.net/posts/7</a><br /> この記事で紹介さていたのでしりました。</p> <p><a target="_blank" rel="nofollow noopener" href="https://www.producthunt.com/founder-club/benefits">https://www.producthunt.com/founder-club/benefits</a></p> <p>仕組みがよくわかってないのですが、プロダクトを紹介するというサイトのようです。<br /> 紹介にあたってYoutubeの紹介動画が必要ぽいので、せっかくなら海外用のPV動画も制作して乗っけようと思っています。</p> <p>正直あまりよくわかってない感じですが.</p> <h5 id="・Hacker News"><a href="#%E3%83%BBHacker+News">・Hacker News</a></h5> <p>上記の記事でproducthuntとの連携で紹介されていました。<br /> まだ深くは理解できてないですが、このあたりも挑戦していきたいなと思います。</p> <h5 id="・Medium"><a href="#%E3%83%BBMedium">・Medium</a></h5> <p>Flutter初めてからみるようになったのですが、技術系のブログのようですね。<br /> かなり膨大な投稿量らしいので、Qiitaのよう書いたからといってすぐに反応がでなさそうですが、起点として考えた方がいいなと思います。</p> <p>どんな内容で買えば良いか、ちょっと考えています。</p> <p>せっかく英語で書くなら、日本人として特色も入れられないかなと考えています。</p> <h4 id="■ASO"><a href="#%E2%96%A0ASO">■ASO</a></h4> <p>サイト検索順位ならぬ、アプリ検索順位の最適化ASOについて。<br /> 正直、軽くしか調べてられてませんが、リリースした直後は、評価も何もないので、ダウンロードしてくれそうなユーザーが検索しそうなキーワードを入れるくらいしか、できないのではないかと考えています。</p> <p>SEOと同じ観点でユーザー主義でいくのであれば、ストア内の内容だけは順位が関係しないと勝手に思ってます。</p> <p>例えば、</p> <ul> <li>アプリの有料化率</li> <li>外部リンクからのアクセス数</li> <li>多言語への対応</li> <li>アプリの定着率など。</li> </ul> <p>AppleやGoogleどちらも、アプリのサブスクリプションが長く続く方がいいわけなので、結局SEO同様にアプリをよくしていくこと以外に方法はないと思っています。</p> <h3 id="まとめ"><a href="#%E3%81%BE%E3%81%A8%E3%82%81">まとめ</a></h3> <p>今回人生で2回目のアプリをリリースしました。アプリ開発って、アプリのコードを打ち込んで組むだけだと思いがちですが、本気でPRをやろうと思うとやることいっぱいだなと実感しています。個人開発の大変さも実感しています。</p> <p>この方法がうまくいくとも分かりませんし、アプリ自体に需要がなければ、どんなにPRしても難しいと思います。</p> <p>人事を尽くして天命を待つ</p> <p>これも一つの実験で、うまくいかなければ別の方法を試していきたいと思います。</p> <p>ツイッターもやってますので、ぜひフォローください。<br /> <a target="_blank" rel="nofollow noopener" href="https://twitter.com/oo_forward">https://twitter.com/oo_forward</a></p> <p>■AppStore<br /> <a target="_blank" rel="nofollow noopener" href="https://apps.apple.com/jp/app/id1517535550">https://apps.apple.com/jp/app/id1517535550</a></p> <p>■Google Play<br /> <a target="_blank" rel="nofollow noopener" href="https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja">https://play.google.com/store/apps/details?id=com.IdeaShuffleMemoApp&hl=ja</a></p> YuKiO | 個人開発&Flutter学習中 tag:crieit.net,2005:PublicArticle/15893 2020-05-09T14:43:35+09:00 2020-05-09T17:35:53+09:00 https://crieit.net/posts/Flutter-Android Flutterのネイティブで重い処理をさせる方法(Android) <p>Flutterを使ってAndroidのネイティブで重い処理をさせているとおかしくなることがあった。具体的にはFutureBuilderを使っている時に気づいたのだが、いつまでたってもFutureBuilderが完了状態にならない。</p> <p>そういえば以前から下記のようなログが出ていることを思い出した。</p> <pre><code class="text">Skipped 229 frames! The application may be doing too much work on its main thread </code></pre> <p>これはメインスレッドでの処理が重すぎるから途中でやめちゃったよ、というAndroid側のメッセージ。ということで、この処理を別スレッドで実行してあげる必要がある。例えば元々下記のような処理を行っているとする。</p> <pre><code class="kotlin">if (call.method == "heavy") { myInstance.heavyAction() result.success(true) } </code></pre> <p>上記を別スレッドにする必要がある。あまりここに色々詰め込みたくなかったのでメソッド内に記述した。</p> <pre><code class="kotlin">fun heavyAction(callback: () -> Unit) { thread { heavy() callback() } } </code></pre> <p>呼び出し側</p> <pre><code class="kotlin">if (call.method == "heavy") { myInstance.heavyAction() { result.success(true) } } </code></pre> <p>これで良さそうに思うのだが、今度は下記のエラーが出る。</p> <pre><code class="text">Methods marked with @UiThread must be executed on the main thread </code></pre> <p>完了処理を別スレッドで行わず、メインUIスレッドで行なえ、とのこと。</p> <p>ということで最終的に呼び出し元は下記のようになった。</p> <pre><code class="kotlin">if (call.method == "heavy") { myInstance.heavyAction() { runOnUiThread { result.success(true) } } } </code></pre> だら@Crieit開発者