種子島島内の交通事情 (2025年6月版)
TL;DR 種子島内の移動では車はほぼ必須。公共交通機関を使う場合は最低限の移動しかできないと思ったほうがいい。
今回H-IIA F50の打ち上げを見に行くために移動手段を色々検討していたのでblog記事としてまとめてみました。
島内の交通手段
6/29時点での情報なので実際に参考にする場合は時刻表や運休の情報を確認してください。
- GLOBAL THINKさんの路線バス1
- 種子島空港に接続する乗合タクシーをハシゴする
南種子町内の交通手段
3/31まではコミュニティバスがあったようなのですが4/1からはのるーとによる運行に切り替わっています。乗り降りする時間や場所を事前に予約する乗合タクシーのような運行になっているようです。 電話、のるーとアプリ、LINEから事前予約が可能です。ただし、平日のみの運行で土日祝は休みです。
まとめ
旅行日程に土日祝が挟まるとただでさえ少ない交通手段がより少なくなりますので車やバイクで移動できない場合は慎重に旅程を組むべきだと思いました。
島に辿り着いても宿への移動手段がない場合もあり得ますので宿の場所も重要になります。
- 旧大和バス↩
Play Billing Library 5を試してみる
この記事はAndroid Advent Calendar 2022の18日目の記事です。
今回はPlay Billing Library 5 (PBL5)について色々試したので書いてみようと思います。特にOfferを設定した場合にどんな SubscriptionOfferDetails が得られるのか気になっていたので調べました。
ひょっとしたら実行環境などによって挙動が変わる可能性がありますのでご了承くださいmm
雑なおさらい
Google I/O 2022で定期購入機能の変更が発表されました。
今までプランの名前、期間、金額、割引条件など1つにまとまっていた設定がSubscription, Base Plan, Offerというツリー構造に変わりました。

ツリー構造になることで1つのSubscriptionの下に複数のBase Plan(例: 月額プラン、年間プランなど)を設定できたり、1つのBase Planの下に複数のOfferを設定できるようになりました。
syarihuさんのDroidKaigiのセッションで詳しく解説されているので一度ご覧になることをお勧めします。
Play Billing Library 5
上の変更に対応したのがPBL5です。5.1.0が最新版です。
定期購入のアイテム情報を取得するにはqueryProductDetailsAsync) 関数またはktxライブラリの queryProductDetails 関数を使います。1
query すると ProductDetails やその中に含まれる SubscriptionOfferDetails や PricingPhases が得られます。
以下のサンプルコードのようにqueryした結果 SubscriptionOfferDetails を探してきてその中の offerToken を購入のパラメーターとして設定します。
class BillingSample { private val billingClient: BillingClient = TODO() fun launchPurchase(productId: String) { val products = listOf( QueryProductDetailsParams.Product.newBuilder() .setProductId(productId) .setProductType(BillingClient.ProductType.SUBS) .build() ) val params = QueryProductDetailsParams.newBuilder() .setProductList(products) .build() billingClient.queryProductDetailsAsync( params, this::handleQueryProductDetailsResult ) } /** * queryProductDetailsAsyncのコールバック関数 */ private fun handleQueryProductDetailsResult( result: BillingResult, productDetailsList: List<ProductDetails> ) { if (result.responseCode != BillingResponseCode.OK) { return } val productDetails = productDetailsList.firstOrNull() ?: return val subscriptionOfferDetails = productDetails.subscriptionOfferDetails ?: return val purchaseOfferToken = findSuitableOfferToken(subscriptionOfferDetails) ?: return // 購入フローに渡すパラメーターの構築 val productDetailsParamsList = listOf( BillingFlowParams.ProductDetailsParams.newBuilder() .setProductDetails(productDetails) .setOfferToken(purchaseOfferToken) .build() ) val billingFlowParams = BillingFlowParams.newBuilder() .setProductDetailsParamsList(productDetailsParamsList) .build() TODO("購入フローに渡す") } private fun findSuitableOfferToken( subscriptionOfferDetailsList: List<SubscriptionOfferDetails> ): String? { TODO("subscriptionOfferDetailsListから適したofferTokenを探して返す") // For example, please refer retrieveEligibleOffers and leastPricedOfferToken function in // https://github.com/android/play-billing-samples/blob/main/PlayBillingCodelab/finished/src/main/java/com/sample/subscriptionscodelab/ui/MainViewModel.kt } }
PBL5を試してみる
PBL5を試したい場合、Codelabを使うのが一番お手軽です。2
releaseビルドを作ってGoogle Play Consoleから内部テスト(internal-testing)にアップロードすれば一般公開しなくてもテストできます。
今回はこのアプリを使ってOfferの設定を変えながらどのようなsubscriptionOfferDetailsが得られるのか実験したいと思います。
このアプリではbasicとpremiumの2つのsubscriptionの下にそれぞれmonthly, yearly, prepaidの3つのbase planを設定しています。
また、subscriptionOfferDetailsの内容をlogcatで確認するために MainViewModel の buy 関数にログを仕込みました。
試した結果
Offer無しの場合
Offerが設定されていない場合、 subscriptionOfferDetails はBase Plan 1つにつき1つしかありません。
subscriptionOfferDetails = [
{
basePlanId=monthlybasic,
offerId=null,
offerTags=[monthlybasic],
offerToken=(deleted),
pricingPhases=[...]
},
{
basePlanId=yearlybasic,
offerId=null,
offerTags=[yearlybasic],
offerToken=(deleted),
pricingPhases=[...]
},
{
basePlanId=prepaidbasic,
offerId=null,
offerTags=[prepaidbasic],
offerToken=(deleted),
pricingPhases=[...]
}
]
初回購入限定のOfferを設定した場合
monthlybasicに初回購入限定でお試し期間1ヶ月のOffer(下の画像のmonthlybasic-freetrial)を設定してみます。

初回購入かどうかはGoogle Play側でチェックしているのでサービス側では特にチェックをする必要はないです。
初回購入時はbasePlanIdがmonthlybasicのsubscriptionOfferDetails は2つ受け取れます。
片方は通常の購入、もう片方はお試し期間のOffer付きの購入です。offerIdやofferTagsを見ると区別がつくと思います。
subscriptionOfferDetails = [
↓ここからmonthlybasic
{
basePlanId=monthlybasic,
offerId=monthlybasic-freetrial,
offerTags=[freetrial, monthlybasic],
offerToken=(deleted),
pricingPhases=[
{
formattedPrice = Free,
priceCurrencyCode = JPY,
billingPeriod = P1M,
billingCycleCount = 1,
recurrenceMode = 2
},
{
formattedPrice = ¥100,
priceCurrencyCode = JPY,
billingPeriod = P1M,
billingCycleCount = 0,
recurrenceMode = 1
}
]
},
{
basePlanId=monthlybasic,
offerId=null,
offerTags=[monthlybasic],
offerToken=(deleted),
pricingPhases=[
{
formattedPrice = ¥100,
priceCurrencyCode = JPY,
billingPeriod = P1M,
billingCycleCount = 0,
recurrenceMode = 1
}
]
},
↑ここまで
{basePlanId=yearlybasic, offerId=null, offerTags=[yearlybasic], offerToken=(deleted), pricingPhases=[{formattedPrice = ¥1,000, priceCurrencyCode = JPY, billingPeriod = P1Y, billingCycleCount = 0, recurrenceMode = 1}]},
{basePlanId=prepaidbasic, offerId=null, offerTags=[prepaidbasic], offerToken=(deleted), pricingPhases=[{formattedPrice = ¥120, priceCurrencyCode = JPY, billingPeriod = P1M, billingCycleCount = 0, recurrenceMode = 3}]}
]
二回目の購入時は初回購入限定のOfferは渡されません
subscriptionOfferDetails = [
↓ここからmonthlybasic
{
basePlanId=monthlybasic,
offerId=null,
offerTags=[monthlybasic],
offerToken=(deleted),
pricingPhases=[
{
formattedPrice = ¥100,
priceCurrencyCode = JPY,
billingPeriod = P1M,
billingCycleCount = 0,
recurrenceMode = 1
}
]
},
↑ここまで
{basePlanId=yearlybasic, offerId=null, offerTags=[yearlybasic], offerToken=(deleted), pricingPhases=[{formattedPrice = ¥1,000, priceCurrencyCode = JPY, billingPeriod = P1Y, billingCycleCount = 0, recurrenceMode = 1}]},
{basePlanId=prepaidbasic, offerId=null, offerTags=[prepaidbasic], offerToken=(deleted), pricingPhases=[{formattedPrice = ¥120, priceCurrencyCode = JPY, billingPeriod = P1M, billingCycleCount = 0, recurrenceMode = 3}]}
]
デベロッパー指定のOfferを設定した場合
今度はmonthlypremiumにデベロッパー指定のOffer(下の画像のmonthlypremium-custom)を設定してみます。

デベロッパー指定の場合、サービス側でユーザーがこのOfferを適用できるかどうかチェックする必要があります。
初回購入時はmonthlypremiumのsubscriptionOfferDetails は2つあります。
片方は通常の購入、もう片方はお試し期間のOffer付きの購入です。
subscriptionOfferDetails = [
↓ここからmonthlypremium
{basePlanId=monthlypremium, offerId=monthlypremium-custom, offerTags=[custom, monthlypremium], offerToken=(deleted), pricingPhases=[{formattedPrice = Free, priceCurrencyCode = JPY, billingPeriod = P1M, billingCycleCount = 1, recurrenceMode = 2}, {formattedPrice = ¥500, priceCurrencyCode = JPY, billingPeriod = P1M, billingCycleCount = 0, recurrenceMode = 1}]},
{basePlanId=monthlypremium, offerId=null, offerTags=[monthlypremium], offerToken=(deleted), pricingPhases=[{formattedPrice = ¥500, priceCurrencyCode = JPY, billingPeriod = P1M, billingCycleCount = 0, recurrenceMode = 1}]},
↑ここまで
{basePlanId=yearlypremium, offerId=null, offerTags=[yearlypremium], offerToken=(deleted), pricingPhases=[{formattedPrice = ¥5,000, priceCurrencyCode = JPY, billingPeriod = P1Y, billingCycleCount = 0, recurrenceMode = 1}]},
{basePlanId=prepaidpremium, offerId=null, offerTags=[prepaidpremium], offerToken=(deleted), pricingPhases=[{formattedPrice = ¥550, priceCurrencyCode = JPY, billingPeriod = P1M, billingCycleCount = 0, recurrenceMode = 3}]}
]
二回目の購入時も初回と同様にmonthlypremiumのsubscriptionOfferDetails は2つあります。
subscriptionOfferDetails = [
↓ここからmonthlypremium
{basePlanId=monthlypremium, offerId=monthlypremium-custom, offerTags=[custom, monthlypremium], offerToken=(deleted), pricingPhases=[{formattedPrice = Free, priceCurrencyCode = JPY, billingPeriod = P1M, billingCycleCount = 1, recurrenceMode = 2}, {formattedPrice = ¥500, priceCurrencyCode = JPY, billingPeriod = P1M, billingCycleCount = 0, recurrenceMode = 1}]},
{basePlanId=monthlypremium, offerId=null, offerTags=[monthlypremium], offerToken=(deleted), pricingPhases=[{formattedPrice = ¥500, priceCurrencyCode = JPY, billingPeriod = P1M, billingCycleCount = 0, recurrenceMode = 1}]},
↑ここまで
{basePlanId=yearlypremium, offerId=null, offerTags=[yearlypremium], offerToken=(deleted), pricingPhases=[{formattedPrice = ¥5,000, priceCurrencyCode = JPY, billingPeriod = P1Y, billingCycleCount = 0, recurrenceMode = 1}]},
{basePlanId=prepaidpremium, offerId=null, offerTags=[prepaidpremium], offerToken=(deleted), pricingPhases=[{formattedPrice = ¥550, priceCurrencyCode = JPY, billingPeriod = P1M, billingCycleCount = 0, recurrenceMode = 3}]}
]
まとめ
個人的には Offer (特にデベロッパー指定 Offer)を使うことでサービス側で柔軟に割引やお試しをユーザーに提供できるのに魅力を感じています。
一方で ProductDetailsの SubscriptionOfferDetails のリストの中から意図したもの(購入フローに渡したいもの)を選ぶロジックがどうしても複雑になりそう3なのがネックになっています。
個人的にはアプリ側ではなくサーバー側でロジックを実装してアプリ側はサーバー側から指定されたofferTokenをそのまま使うパターンがシンプルでいいかなと感じています。
余談
定期購入アイテムを登録するときにこれまで inappproducts APIを使っていた場合は注意が必要です。
Google Play Consoleから編集しようとしてもread only状態になっていると思います。編集可能にするボタンがありますがクリックして編集可能にすると inappproducts APIでは編集できなくなり、新しいSubscriptions Publishing APIを使わなければならない状態になります。
Googleの公式ドキュメントでも下のように記載されています。
Google Play Console で定期購入の編集を行うと、それ以降は inappproducts API を定期購入に使用できなくなります。 2022 年 5 月より前に Publishing API を使用している場合、問題を避けるために、既存の定期購入は Google Play Console ですべて読み取り専用として表示されます。変更しようとすると、この制限に関する警告が表示される場合があります。Google Play Console で定期購入を編集する場合は、新しい Subscription Publishing エンドポイントを使用できるよう、バックエンドの統合を事前に更新してください。
https://developer.android.com/google/play/billing/compatibility?hl=ja#managing-subscriptions
余談2
追記(2024-08-16): Google の中の人が見てくれたのか分かりませんが Play Billing Lab という便利なテストアプリが最近リリースされました。これを使えば下のめんどくささは解消できると思います!
初回限定Offerの挙動のテストは地味に面倒くさく、一度購入するたびに新しいProduct + Base Plan + Offerを作り直すか新しいGoogleアカウントを作る必要がありました。
この辺のテストが簡単になると大変ありがたいです...4
参考資料
ISUCON10に参加しました
ISUCONにチーム名「締切駆動開発」で初参加しました。 今までISUCON出たいなぁと思ってたもののチーム作るのが難しくて申し込みすらできなかったんですが今回は1人参加OKということでえいやっと申し込みました。
結果
言語はPHPで参加して最終スコアは589で惨敗でした。N+1問題の解決に時間がかかってDBのロードバランシングまで頭が回りませんでしたorz.. リポジトリは https://github.com/tsuyosh/isucon10q/ です

タイムライン
10時開始だったのが12時に延期になったので朝食を食べて洗濯しながら準備を進めていました。
12:20 競技が開始されたもののポータルページがBad Gatewayで死んでしまったので復旧を待ちながら事前に決めていたタスクを進めました。
余談ですが今回の真のお題はポータルページだったのでは、、と思うほど運営の方が大変そうでした。お疲れさまです。。 初参加なので以前のポータルページは知りませんがリーダーボードが時系列グラフ付きで見やすくなっていて、リクエストしたベンチマークもリアルタイムで状況が更新されて終わったらNotificationでプッシュ通知してくれるなど非常に便利でした。
sshの導通確認。当日マニュアルの内容を読んでssh configを変更。無事ssh繋がりました。
Host isucon-bastion HostName (踏み台サーバーのIP) Port 20340 User isucon TCPKeepAlive yes Host isucon-server1 ProxyJump isucon-bastion User isucon HostName 10.162.93.101 TCPKeepAlive yes LocalForward localhost:10080 localhost:80 Host isucon-server2 ProxyJump isucon-bastion User isucon HostName 10.162.93.102 TCPKeepAlive yes LocalForward localhost:20080 localhost:80 Host isucon-server3 ProxyJump isucon-bastion User isucon HostName 10.162.93.103 TCPKeepAlive yes LocalForward localhost:30080 localhost:80
各サーバーの設定をPHP実装に切り替え
当日マニュアルの確認をしながらAPIの実装をななめ読みする。
踏み台経由でブラウザからアクセスできるのでISUUMOを実際に使いながらAPIを確認。
ソースコードや設定ファイルをgithubにpush https://github.com/tsuyosh/isucon10q/commit/fc77bd929285c4af2370ec0b7650325d383f5391 https://github.com/tsuyosh/isucon10q/commit/e1f30519715bf205ca61bbf715ba61847ccd99f0 https://github.com/tsuyosh/isucon10q/commit/9652fcc398e23e94956e7f8e232b9a435d2ba269 https://github.com/tsuyosh/isucon10q/commit/edf9b7226973070e73c88044510f82ea0b173b1e
ISUUMOを使いながら問題になりそうな部分を付箋に書いていって簡易タスク管理をしました。(この辺は1人チームの楽なところ)
- なぞって検索のN+1問題
- 絞り込み項目が多い(price, width, height, depth, kindなど)
- features検索がLIKEを使った部分一致検索
- おすすめ物件の検索条件の改良
- 椅子を買うときのSELECT ~ FOR UPDATE部分の削除
- お買い得リスト(low_priced)APIの結果のキャッシュ
- 負荷分散
ポータルページが復旧したので早速初期ベンチを実行。初期値は330。他のチームは510前後だったのでいきなり差がついていた。。
13:57 計測できるようにMySQLのslow queryを出力 https://github.com/tsuyosh/isucon10q/commit/eab0200d33c4a91c3d8c232999edb122c8c2576e
ただ、SQLの条件文が絞り込み項目の数によって動的に変わるのでmysqldumpslowでボトルネックになりそうな部分を絞り込めませんでした。。
13:59 alpで解析できるようにnginxの設定を変更 https://github.com/tsuyosh/isucon10q/commit/53e0650f98174d4b2db8e6cecd0e9c526e65dd61
これもalpの使い方をあまり知らなかったため集計をいい感じにグループ化できなくてCOUNT=1の結果がズラーと並んでしまい活躍の機会がありませんでした。 終わった後でパターンマッチングでグループ化できるのを知りました。
16:00 とりあえずORDER BYでINDEXが使えるように複合INDEXを追加。Scoreが446に https://github.com/tsuyosh/isucon10q/commit/5ea37a3e36aa6d3d1c681e2885ea54f57d4de593
次になぞって検索のN+1問題に取り掛かる。 MySQLはGEOMETRY型にSpatial INDEXが使えるはずなのでカラムを追加してSQL文を変えれば解決するはずと思ってDB schemaの変更とINSERT, SELECT文の修正を行う。 https://github.com/tsuyosh/isucon10q/commit/6ec6ee4f418e665864907d8bbfa247fbf9e302c9 https://github.com/tsuyosh/isucon10q/commit/49673e754b8541ff98eaeb2c6445fb90b2b0e2a6
ただ、ここでベンチマークを実行してもFailになってしまい調べても原因がわからないので一旦revert。この時点で18:11なので2時間位格闘してたことになる。。 https://github.com/tsuyosh/isucon10q/commit/60feecf5381cd6c478ecb4029a5f20172a870887
18:12 椅子の購入時にstockをチェックするために SELECT ~ FOR UPDATE をやってるのが無駄だったので UPDATE ~ WHETE stock > 0 のような感じでUPDATE文1つで済むように変更。ただ、思ったより効果はなくてスコアは437。
https://github.com/tsuyosh/isucon10q/commit/8acb3527d697703f5e39ca5e7f086b870f8a7935
18:41 price, width, height, depth, kindの絞り込み検索はいいアイディアが浮かばなかったのでやけくそで全部INDEXを貼ったものの案の定スコアは上がらず。 https://github.com/tsuyosh/isucon10q/commit/142b0014a45d44a759c3890009c9b5ca7cac5321
再度N+1問題に挑戦。今度はestateの初期データ部分(2_DummyChairData.sql)は弄らず、ALTER TABLE文を追加で実行するSQL( 3_AddLocationToEstate.sql )を追加。
https://github.com/tsuyosh/isucon10q/commit/d0b693c66a32c9f0fcab4695ca46f1cc27fd4555
最初はベンチマークFailに悩まされたもののログを追っていくと init.sh 以外でも initialize APIでも実行するSQLファイルを追加する必要があるのがわかって速攻で追加。これでベンチが通ってスコアが589に伸びた。(最初に初期データのSQLを弄ったのが悔やまれる...)
https://github.com/tsuyosh/isucon10q/commit/4b6f6ba4374160d6900ee8d369f3d802d8148cf1
最後にBOT対策をやろうかと作業していたのだが時間が差し迫ってたので無理をせずこのままfinish
予選が終わっての感想
事前に色々準備をして1人でも回るようにしたかったのですがチーム名の如く準備は進まず結局ほぼすべての作業を手作業でやってました。
mysqlに接続するとかログを確認するとかgithubからサーバーにdeployする作業が地味にめんどくさかった。
あとssh接続が何も操作していないとハングって再接続しないといけなかったのが地味に辛かったです。ssh configで TCPKeepAlive yes にしていたのですが。。
予選が終わった直後からDiscordのrandomチャネルで振り返りのコメントを見ていましたが知見に溢れていてずっと感心していました。DBを分割したらスコアが上がったという話を聞いてやっぱIOがボトルネックになってたんだなぁと思いました。あとfeatures検索の対処方法(bitsetとか全文検索とか)もなるほどなぁと思いました。
あとなぞって検索は皆さん苦労されてたようです。(ネタ元の)SUUMOでもすごく便利な機能なのでN+1問題は絶対解決したかったw 解決できてよかったww
今回は1人で参加しましたがよほど自信がない限りはチームを組んで役割分担したほうが良さそう。来年はチーム参加したいです。(本業がAndroidエンジニアなのでチームビルディングは大変ですが...)
参加者やスタッフの皆さんお疲れ様でした。
MacでNASのファイルを開くと変なファイルができるので直してみた
MacでNASのファイルを開くと._(開いたファイル名)という名前のファイルができます。
ぐぐるとリソースフォークAppleDouble Header fileというmacOSのファイルシステム固有の仕様のようです。
しばらくは放っておいたのですが勝手にファイルを作られるとディレクトリのタイムスタンプが更新されてディレクトリをソートする際に大変不便だったので色々調べてみました。
macOSで無効化できないか?
結論を先にいうとできませんでした。.DS_Storeは下のコマンドで無効化できたんですけどAppleDouble Header fileの無効化はできませんでした。
defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool TRUE
NAS側でなんとかする
macOSでできないとなると後はNAS(Samba)側でなんとかするしかありません。 色々探してみるとmacOS向けの設定オプションがありました。
動作環境
前提条件として動作環境を載せときます。
リソースフォークを消す
最初に既にあるリソースフォークを消します。macOSにはdot_cleanという便利なコマンドがあるのでそれで消せます。
dot_clean /Volumes/(マウントしているNASの名前)
Sambaの設定変更
/etc/samba/smb.confに# For interoperability with macOS以下の項目を追加しました。
[shared]
path = /shared
browseable = Yes
read only = No
# For interoperability with macOS
vfs objects = catia fruit streams_xattr
fruit:encoding = native
streams_xattr:prefix = user.
streams_xattr:store_stream_type = no
smb.confを編集したらsambaをリロードします
$ sudo systemctl reload smbd $ sudo systemctl reload nmbd
動作確認
試しにLinuxのコンソールからhogehoge.txtを作りました。 拡張属性はgetfattrコマンドで確認できますが作った直後は何もありません。
tsuyosh@enterprise:/shared$ touch hogehoge.txt tsuyosh@enterprise:/shared$ getfattr -d hogehoge.txt
Mac上から開くと何も変更を加えなくても下のように更新時間?が保存されます。
tsuyosh@enterprise:/shared$ getfattr -d hogehoge.txt # file: hogehoge.txt user.com.apple.lastuseddate#PS=0sDLILXQAAAACR38suAAAAAAA=
GW中に読んだ英語を記録していたので晒してみる
会社のEnglish Technical Writingのレッスンでゴールデンウィーク中に読んだり聞いたりした英語の文書を記録しようという課題があったのでどうせなのでblogで晒してみようと思います。 今回はbookmarkに積ん読していたblog記事が沢山あったので面白そうなのから読んでみました。JakeのAndroidのJava対応やD8/R8に関するシリーズは結構面白かったです(まだ途中だけど)。
読書時間は試験じゃないので読み飛ばすとかはしないけど思ったより長めだなぁという感想です。(Mediumに表示される読書時間よりはるかに長い) あと基本的な単語を使ってても意味がよくわからないphraseとかもあってもうちょっと語彙力改善したいなぁと感じました。
| URL | 読書時間(時間:分) | よくわからなかった単語やフレーズ |
|---|---|---|
| https://android-developers.googleblog.com/2019/04/android-q-scoped-storage-best-practices.html | 00:14 | helping shape Android, elaborate change, Being devlopers ourselves, how we, can better align, retained, limit file clutter, community engagement |
| https://source.android.com/security/app-sandbox | 00:16 | in this respect, break out of, one must compromise the security of the Linux kernel, enforce, defense-in-depth, over time, discretionary, per-physical-user boundary, confused deputy vulnerabilities, genuinely |
| https://medium.com/@elye.project/debug-without-using-temporary-log-in-android-studio-91d9394a1e94 | 00:09 | relevant, according to your TAG, cheer up, With this in places |
| https://jakewharton.com/androids-java-8-support/ | 01:01 | practically unfathomable, at the time of writing, over time, bespoke, having desugar occur, doesn’t contain any traces, a bit dense, and intimidating, reinforcing, dwindling, holding back, ahead of the game |
| https://9to5google.com/2019/05/01/android-ui-framework/ | 00:19 | far more in-depth, more of, as to |
| https://jakewharton.com/androids-java-9-10-11-and-12-support/ | 00:51 | table stakes , uptake, fares, As such, innocuous, over time, predates, As with |
| https://jakewharton.com/avoiding-vendor-and-version-specific-vm-bugs/ | 00:39 | tip you off, lies with, happened close to home, exempt, dormant, admittedly, contrived, distilled into, conditionals, uncovered |
| https://medium.com/androiddevelopers/coroutines-on-android-part-i-getting-the-background-3e0e54d20bb | 00:33 | Together, keep track, Give it a read |
| https://medium.com/androiddevelopers/coroutines-on-android-part-ii-getting-started-3bff117176dd | 00:57 | tedious, error prone, linger, helps us out, reason about, obscure |
| https://medium.com/@elizarov/null-is-your-friend-not-a-mistake-b63ff1751dd5 | 00:30 | witnessed, dreaded, plague, way before, off-guard, heinous, pollute, flaw, akin, whatsoever |
| https://medium.com/netflix-techblog/android-rx-onerror-guidelines-e68e8dc7383f | 00:22 | As with, intuitive, divergent, outright, intermittent, forcibly, deterministic, moving off |
| https://developer.squareup.com/blog/developing-on-ios-and-android/ | 00:10 | convergence, spartan, emerged, endeavor, surrogate |
drawable属性にcolorリソースが指定できるのがモヤッとしたので調べてみた
TL;DR
Context#getDrawable()の引数にColorStateList以外のcolor resource id(e.g. R.color.hogehoge)を渡すとColorDrawableが返ってきます。
本文
なんかタイトルがモヤっとしててわかりにくいと思いますがきっかけは下のようなコードが動いたからです。
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@color/colorPrimaryDark" android:state_enabled="false"/>
<item android:drawable="@color/colorAccent" android:state_pressed="true"/>
<item android:drawable="@color/colorPrimary"/>
</selector>
一見平凡なStateListDrawableのdrawable resourceですがandroid:drawableにcolorリソースを指定してもColorDrawableとして認識されます。
Drawable Resourceのリファレンスを確認してもReference to a drawable resourceとしか書いてなくて一見するとdrawable resourceしか受け付けないように見えます。
ただ、Context.getDrawableのリファレンス)を見るとidの定義がThe desired resource identifier, as generated by the aapt tool.と書いていて若干曖昧な表現になってました。
というわけで以下のようなコードがどういう処理をしているかAOSPの実装を掘り下げていきました。
val drawable = getDrawable(R.drawable.state_list_demo)
読み進んで行くとResources#getDrawableForDensity(id:Int, density:Int, theme)に行き着きます。
public Drawable getDrawableForDensity(@DrawableRes int id, int density, @Nullable Theme theme) {
final TypedValue value = obtainTempTypedValue();
try {
final ResourcesImpl impl = mResourcesImpl;
impl.getValueForDensity(id, density, value, true);
return impl.loadDrawable(this, value, id, density, theme);
} finally {
releaseTempTypedValue(value);
}
}
ResourcesImpl#getValueForDensityも気になりますが*1、Drawableを返しているのはloadDrawableなのでこっちを読み進んでいきます。
コードが長いので端折りますが途中でこんなコードが見つかります。
final boolean isColorDrawable;
final DrawableCache caches;
final long key;
if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT
&& value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
isColorDrawable = true;
caches = mColorDrawableCache;
key = value.data;
} else {
isColorDrawable = false;
caches = mDrawableCache;
key = (((long) value.assetCookie) << 32) | value.data;
}
どうも引数で渡されたvalue(TypedValue)のtypeがTYPE_FIRST_COLOR_INTからTYPE_LAST_COLOR_INTの範囲ならisColorDrawableがtrueになることがわかります。この辺を追いかければ答えが見つかりそうです。
少し読み進めていくとあっさり答えが見つかりました。
} else if (isColorDrawable) {
dr = new ColorDrawable(value.data);
} else {
つまり引数で渡したリソースがTypedColor.typeがColor関連ならばColorDrawableを返すようです。
おまけ1
一応StateListDrawableがどのように生成されるのかも確認してみました。やはり<item>タグのandroid:drawable属性も最終的にはResourcesImpl#loadDrawable()を通るようです。
Context#getDrawable(id:Int) -> Resources#getDrawable(id) -> Resources#getDrawable(id, theme = null) -> Resources#getDrawableForDensity() -> ResourcesImpl#loadDrawable() -> ResourcesImpl#loadDrawableForCookie() -> Drawable.createFromXmlForDensity() ← xmlファイルのパース処理 -> Drawable.createFromXmlInnerForDensity() -> DrawableInflater#inflateFromXmlForDensity() ← xmlファイルの最初のタグ名に応じてDrawableインスタンスを生成 -> StateListDrawable#inflate() ← 残りのxmlのパースは生成されたDrawableに委譲 -> StateListDrawable#inflateChildElements() ← itemタグのパース -> TypedArray#getDrawable(index:Int) -> TypedArray#getDrawableForDensity(index, density = 0) -> Resources#loadDrawable() -> ResourcesImpl#loadDrawable() -> 以下続く
おまけ2
ちなみにStateListDrawableと同じタグ(<selector>)を使っているStateColorListのリソースもこんな使い方ができるようですw
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
android:textColor="@drawable/state_color_list_sample" />
<!-- res/drawable/state_color_list_sample.xml -->
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/colorPrimaryDark" android:state_enabled="false"/>
<item android:color="@color/colorAccent" android:state_pressed="true"/>
<item android:color="@color/colorPrimary"/>
</selector>
追記1
Context.getDrawable()にColorStateListのリソースIDを渡すと見事に落ちました。
StateListDrawableと勘違いしてパースに失敗するようです。
Caused by: org.xmlpull.v1.XmlPullParserException: Binary XML file line #3: <item> tag requires a 'drawable' attribute or child tag defining a drawable
at android.graphics.drawable.StateListDrawable.inflateChildElements(StateListDrawable.java:190)
at android.graphics.drawable.StateListDrawable.inflate(StateListDrawable.java:122)
at android.graphics.drawable.DrawableInflater.inflateFromXmlForDensity(DrawableInflater.java:142)
at android.graphics.drawable.Drawable.createFromXmlInnerForDensity(Drawable.java:1332)
at android.graphics.drawable.Drawable.createFromXmlForDensity(Drawable.java:1291)
at android.content.res.ResourcesImpl.loadDrawableForCookie(ResourcesImpl.java:833)
at android.content.res.ResourcesImpl.loadDrawable(ResourcesImpl.java:631)
at android.content.res.Resources.getDrawableForDensity(Resources.java:888)
at android.content.res.Resources.getDrawable(Resources.java:827)
at android.content.Context.getDrawable(Context.java:626)
at androidx.core.content.ContextCompat.getDrawable(ContextCompat.java:463)
*1:コードをざっくり見たところ、指定したリソースIDの情報を取ってきてTypedValueにセットするメソッドのようです。
2018年を雑に振り返る
2017年に引き続いて雑に振り返りたいと思います。
移住&転職
去年の12月に会社を退職して福岡に移住するのを決めました。 前々から頭の中で燻っていたのですが勢い余ってやったというのが大きいです。 大きい決断には勢いが大事だと学びました。*1
(追記) 転職についてはひよこ大佐のツイートも励みになりました。面識もなんにもないですがひよこ大佐にはお礼を言いたいです。
4月30日付けで退職して2ヶ月弱は人生初の週休7日を体験しました。 失業者になったので失業手当を受けるべくハローワークにも通って説明会を受けたりPCをポチポチ触ったりしてました。*2
実際には3月末ぐらいに応募したLINE Fukuokaで内定を貰ったので6月15日から働き始めました。
家
家賃は安いです。前の住居(@渋谷)よりも広くて新築な上に安かったです。 まぁ通勤時間が徒歩10分から自転車30分に変わりましたが許容範囲内です。 博多とか天神付近に住みたい場合は1LDKで10万弱で住めそう。
食べ物
うどんは美味しいです。賛否両論ありそうですが牧のうどんは美味しかったです。*3 もつ鍋も結構あちこちにお店があります。ラーメンはとんこつより塩が好きなんですがShin Shinは好きです。 あと会社に週二で移動販売に来てくれるKen'sカレーは結構人気があるみたいです。
お賃金
おかげさまで増えました。地方都市でもAndroidエンジニアとして食っていけそうです。
会社では何してるの?
LINE Client(メッセンジャー)の1機能を担当しているチームに入りました。 普段はバグを修正したりPull Requestのレビューをしながら現在担当しているプロジェクトに取り組んでいます。
福岡のモバイルエンジニアは、LINEや関連サービスのiOS/Android clientを各地の拠点と連携し開発しています(写真はメッセンジャー担当チーム)。自分の手でLINEを良くしたい方、大規模ユーザアプリ開発に興味ある方、Swift/Kotlin好きな方お待ちしてます! #LINEのエンジニアhttps://t.co/2NBozG7aLi pic.twitter.com/TYiEx9RmcS
— LINE Developers (@LINE_DEV) December 13, 2018
開発環境
iMac(Core i7, 32GB)とMacbook Pro(Core i5, 16GB)と液晶モニターで普段開発してます。 オフィスについては941さんのBlogが詳しいです。
その他
通勤の途中で新二又瀬という交差点があるのですが、着陸する飛行機がすぐ上を通るので市街地の中に空港があるのが実感できます。

映画
37本くらい観てました。来年もだいたいこのくらいで推移しそう。 福岡に引越しして行きつけの映画館がTOHOシネマズからUnited Cinemasに変わりました。 TOHOシネマズのマイレージは4,000マイルくらい貯まってたんですが使わずに消滅しそう(´・ω・`)
面白かったのは
- スター・ウォーズ ハン・ソロ
- キングスマン ゴールデン・サークル
- パシフィック・リム アップライジング
- バーフバリ
- GODZILLA 決戦機動増殖都市 / 星を喰う者
- ミッション:インポッシブル/フォールアウト
- オーシャンズ8
- クレイジー・リッチ!
- 2001年宇宙の旅 IMAX
- ボヘミアン・ラプソディ
- ファンタスティック・ビーストと黒い魔法使いの誕生
IMAX版の2001年宇宙の旅を観に来たけど休憩(Intermission)がある。オリジナルと同じ仕様なのかな pic.twitter.com/UO1SQOJklM
— Tsuyoshi UEHARA (@uecchi) 2018年10月27日
アニメ
INGRESS THE ANIMATIONはまだ途中までしか見れてないorz
Ingress
4月7日のFukuoka XM Festivalに参加しました。ちょうど物件を探す予定と重なったので急遽参加できることになったんですが当日は寒いわ雹が降るわで薄着で来たのが大誤算でしたw あと7月28日のCassandra Prime Sapporo Anomalyにも参加しました。初札幌で久々のアノマリー参加でした。
来年は3月23日のDarsana Prime Tokyo Anomalyに行きたいですね。
進捗確認
結局できたのは最初の目標だけでしたorz
- ✅転職&移住
- ❎体重を落とす
- ❎DroidKaigiにCfPを出したい
- ❎ISUCONに参加したい
来年の目標
- 身体のメンテナンスをちゃんとしよう
- 体重を落とす
- 英語を喋れるようになる。具体的にはIELTS 6.5以上
- DroidKaigiにCfPを出したい
- ISUCONに参加したい
積み残しを少しづつ片付けていかないと...😅
振り返りは以上です。これで心置きなく年を越せますw 皆様良いお年を🎍