タグ「Android」の 記事 58 件中 1 ~ 58 件を表示しています。

私は就寝が早いのだが、最近寝た後に通知が届いてその音で起きてしまうというのがよくある。 なので今更だがおやすみ時間モードを調べて導入した。 以前の OPPO Reno A の場合は確かサウンドか通知の設定項目にそういったものがあったのだが、今使っている OPPO Reno5 A にはそれが見当たらない。 まさか機能としてオミットされているのか? と考えて検索してみたところ Android 9 から Digital Wellbeing というアプリの使用状況を統計・グラフ表示する機能が実装され、その中の機能として用意されたらしいことが分かった。 ということで OPPO Reno5 A と TECLAST T40 Plus (タブレット) 共におやすみモードを設定したので、設定時間帯はサイレントモードになる。 これで安心だ。

まさか IntelliJ IDEA の正式な日本語化パックプラグインが提供される日が来るとは思わなかった。 最近になって Pleiades が適用できなくなっていたので仕方なく英語のまま使っていたのだが JetBrains チームからお使いの IDE が日本語に対応しましたというメールが来たのでそれを知った。 PhpStorm のような IntelliJ IDEA の派生 IDE であればプラグインから「日本語」で検索して日本語化パックをインストールすることができるようになっていた。 これは楽だ。 Android Studio に関してはこれができなかったが、これに関しても Android Studioを日本語化する(トラブルシューティング付き) という題でやり方をまとめてくださっている方がいたので、その通りにやったらできた。 感謝。

昔このあたりは一回検討したことがあったのだが、どうにも使い勝手が良くないなあと感じてそのまま放置していた。 Android 用の SSH クライアントアプリはいくつかあるが ConnectBot というアプリは後述する日本語入力の切り替え (Ctrl + Space など) がうまく動かなかった。 JuiceSSH が画面のデザインやフォントの種類 (JetBrains Mono も設定できる) など細かく設定できるので良さそうだ。 普通にサーバに SSH 接続できたのだが、日本語入力が何故かできない。 というより SSH クライアント側が日本語入力に対応していないような気がする。 どうしたものかと思ってググってみたら Android 端末から SSH 経由で日本語入力したいがヒットした。 その記事の通りやったらうまくいったのでとても参考になった。 クライアントサイドで日本語 IME を起動するのではなく、サーバサイドで日本語 IME を起動するという解決法で快適とは言い難いが、とりあえず使えるのは助かる。

私は以前は MacBook Pro をメイン PC として使用していたのでそれがあればこのような考察は不要だった。 だが今のように在宅勤務メインになってしまうとほとんどが自宅の自室での使用となりノート PC として使いたい機会がほぼなくなってしまったのでデスクトップ PC に買い替えてしまった。 タブレットでも USB-C のアダプタさえ持っていけば普通の USB キーボードを接続して作業可能なのは助かる。 外出先でガッツリ開発作業を行うことはなく、この Blog を書いたりサーバを少し弄れればいいので調べてみたわけだ。

Pixel 4 が発表されたようだ。 モバイル Suica が 50 円の鉄道利用で 1 円ぶんのポイントが付くようになったので FeliCa の重要性が増している。 その点 SIM フリーとして販売されていて FeliCa が搭載されている Pixel シリーズは貴重な存在だ。 勿論今回も FeliCa 対応のようだ。 私が使用している Pixel 3 XL はまだまだ使えるので Pixel 5 か 6 あたりまで保たせたいが、今見ると Pixel 3 XL は画面が大きい割にバッテリー容量が少ないのが少し不満だ。 やけに電池の減りが早い気がする。 私のタブレットメインの利用パターンだと廉価版の Pixel 3a でも良かったところだ。 Pixel 3a でも防水対応されているので、バイクのナビとして使用する際にも安心なのがポイント。

MediaPad M5 8.4" LTE モデルに通話 SIM を挿してスマートフォンとして使用し始めて 3 週間が経過した。 工夫次第という条件付きではあるが、特に問題なく使用できている。 チェスなどのゲームアプリがいつでも快適に使用できるのはやはり良い。 スマートフォンだと Pixel 3 XL のような比較的大きい画面のモデルでも画面が気持ち小さくて少し我慢させられているような感じだった。

私のいう工夫というのは以下のようなものだ:

  • MediaPad M5 が入るサイズのレッグポーチを身につける習慣をつける
  • HUAWEI WATCH GT などの着信や LINE 通知などが受け取れるスマートウォッチを併用する

最初会社に行くときはショルダーバッグを持ち歩くから大丈夫だろうと思っていたが、それだと昼食や休憩時に困った。 一応ズボンの後ろポケットに入れることはできるが、あまり快適ではない。 そこでショルダーバッグの代わりに DEVICE のレッグポーチを身につけていくことにより解決できた。 入りきらない荷物を持ち歩かなければならない場合のみショルダーバッグを併用すればよい。 しかしレッグポーチと併用すると足のあたりで干渉しがちなので本当はショルダーバッグではなくボディバッグやリュックの方が良いのかもしれない。

HUAWEI WATCH GT の着信や通知を受け取る機能は本当に便利で、これがなかったらさすがに MediaPad M5 をスマートフォン代わりに使用しようとは思わなかったかもしれない。 HUAWEI WATCH GT 2 がもう出るような感じではあるが、もう少しモデルチェンジが進んでからまた買いたいところだ。

それにしても、MediaPad M6 のグローバル版も未だに出ないし Google 系のサービスがインストールされない事がほぼ確定してしまったのは本当に残念だ。 かといってファーウェイ以外のメーカーの 8 インチタブレットは期待できないし、最悪将来的に iPad mini に移ることも考えなければならない。

最終的に何をやっているのかよく分からない行動になってしまっているが、一度手放した MediaPad M5 8.4 インチ LTE モデルを再度購入した。 MediaPad M6 は本当に素晴らしいタブレットだと思うが、ファーウェイに関してどうにも不穏なニュースばかりで Mate 30 に Google Play がプリインストールされないという話だし MediaPad M6 もプリインストールされない可能性が高い。 予想だが Expansys などでグローバル版は手に入るようになるかもしれないが Google Play はインストールされず、日本版は販売されないのではないかと思う。

MediaPad M5 8.4 LTE の今のところの実売価格は 4 万強とあまり落ちてはいないのだが、今回の楽天お買い物マラソンで実質 3 万円程度で購入できた (SPU + 10 店舗買いまわり 9 倍 + ショップ 9 倍)。もしも事態がいい方向に進んで日本版が Google Play 入りで販売されることになったとしても普通に買い換えれば良いと考えた。 ヤフオクやラクマで処分すれば 2 万 5 千円では売れるだろうから金額的なロスは少ない。 ただ単に売るのが面倒くさいだけだ。

スマホはちょっと使うぶんにはいいのだが、自宅でゆったりした姿勢で長時間使うと画面が小さくてどうにも目が疲れる。 MacBook Pro でここをカバーできるかと思っていたのだが、動画を見だすとバッテリーがみるみるうちに減っていき 2 時間も保たない。 やはりこのあたりの用途はタブレットが最善だと思う。

Kirin 980 は強い

MediaPad M6 8.4" が欲しいと思うようになってきた。 以前 MediaPad M5 を使ってもう 8 インチタブレットを使うことはないだろうと過去に書いており、自分でも何という心変わりだろうと笑ってしまう。 しかし 8.4 インチタブレットに対する私の考え方が変わってきたのがその心変わりの理由だ。 以前 MediaPad M5 を使っていた時はメインとなるスマートフォンを使いながら LTE モデルでデータ SIM を契約して使っていた。 今回はそうではなく MediaPad M6 をスマートフォンとして (要するに音声 SIM を指して) 使い今使っているスマートフォンは売却するということを考えている。 私はバイクのナビとしてスマートフォンを使用するのでどうしてもスマートフォンを外すことはできないのだが、最近 Pixel3 XL をメインスマートフォンとして使い P20 をバイクのナビ用として Bluetooth テザリングで使うのに慣れてしまいメイン回線をタブレットにするのもアリなのではと考えるようになってきた。

MediaPad M6 はスペックだけ見るとものすごく魅力的だ。 何しろ P30 などに使われている最新 SoC である Kirin 980 が積まれている。 恐らく値段を抑えるためだろうが、今までファーウェイのハイエンドタブレット (MediaPad M3, MediaPad M5) には型落ちの SoC を積んできていた。 それを我慢して使っていたのに、今回はどうしたことかといい意味でびっくりさせられた。 これなら最新スマートフォンに引け目を感じずに十二分に快適に使えるだろう。 肝心の Google Play ストアに関してもネット上の情報を眺めているとどうやら問題なく使えるようになりそうで安心している (非公式ではあるが中国国内版でも Google Play ストアが使えるようにできるようになったらしい)。

廉価版とは思えない完成度

仕事で触る機会を得られたのでレビューを書き記しておく。 MediaPad M5 lite 8" は MediaPad M5 の廉価版であり、MediaPad M3 lite (8 インチ) の後継モデルである。 だから私はそういう気持ちでこのモデルに対峙したのだが、いい意味で期待を裏切られた。 ハッキリ言って MediaPad M3 lite とはぜんぜん違う。

指紋認証なしはデメリットではない

MediaPad M5 や MediaPad M3 lite についていた前面下部の指紋認証が取り除かれている。 これにより認証が面倒になったのかもしれないが、私としてはそんなに気にならなかった。 それどころか指紋認証のボタンがないので、ゲーム中に間違って触れてしまうようなことがなくなるだろう。 そして、指紋認証がないので上下のベゼル幅が同じくらいになり、特に横画面にした際にデザイン的にバランスが取れるようになった。 MediaPad M3 lite と並べてみると狭ベゼルになっており一回り小さいのも好印象だ。

背面のデザインが格好良い

MediaPad M5 の背面デザインは iPhone のように白いアンテナのラインが見えてあまりクールではなかったように思うが、MediaPad M5 lite 8" は見事にグレーに統一されていて実に渋い。 MediaPad M5 より高級感があるのではないかと思うほどだ。

十分キビキビ動く

MediaPad M5 lite 8" は廉価版だが Kirin 710 という nova lite 3 と同じ SoC を積んでおり、これがまずまずのパフォーマンスを見せる。 Antutu ベンチマーク的には Kirin 950 (MediaPad M3) 相当ということで全くストレスなく使える感じがした。 ちなみに兄弟機に MediaPad M5 lite という 10 インチモデルがあるが、こちらは Kirin 659 というスペックが低い SoC を積んでいるので性能が全く違う。 MediaPad M5 lite を買うなら 8 インチモデルのほうが絶対にいい。

不満点は microUSB のみ

一点大きな不満点があるとすれば今どき microUSB というところのみだろう。 何故こうしたのか全く理解出来ないが、これのせいで他の機器が USB-C に移行できていても microUSB も併用しなければならなくなってしまう。

ただ不満点といえばそれくらいで、このタブレットが Wi-Fi 版で実売 2 万円強、LTE 版でも 2 万 5 千円強で買えてしまうのは破格だと思う。 何しろ税込価格で計算すると、Wi-Fi 版は昨日書いたニンテンドースイッチライトより安い

本日 MediaPad M6 が中国で発表された。 細かい情報に関しては phablet.jp 様の記事が詳しい。 どうせ最新の Kirin 980 ではなく型落ちの Kirin 970 (しかも MediaPad M5 の Kirin 960 と大して変わらない) を載せてくるんだろうな、と思っていたら本当に Kirin 980 (Mate 20 Pro や P30 Pro 相当!) を載せてきた。 しかも 8.4 インチに関してはバッテリー容量が減るという不穏なリークがあったのだがそれとは逆に 5100 mAh から 6100 mAh に増えているとのこと。 そして MediaPad M5 は日本発売されたモデルは 32 GB のみという悲しさだったが、今回は 64 GB からなのでその心配もない。 スペックからすると本当に神がかっている

これで例のアメリカのファーウェイ制裁がなければ 8 インチクラスとしてはライバル不在の完全無欠最強タブレットだったはずなのだが……。 OS が Android 9 というのは良いが Google Play ストア (Google Play Services: 開発者サービス) はバンドルされた状態で提供されるのだろうか。 それが気になって仕方がない。 もしそれがされるのであれば欲しすぎる一品ということになりそうだ。

8 インチタブレットに代替機が無いのが非常に厳しい

Googleがファーウェイに対してAndroidのサポートを中止へにある通り、ファーウェイが今後発売するスマートフォンやタブレットでは Play ストアを含めて Google 系のサービスが一切使用できなくなるというニュースが出た。 私は Android アプリのプログラマの為このニュースを注意深く見ていたのだが、昨日になって ARM が取引を中止したりパナソニックも取引を中止したというニュースを見てこれは非常にまずい事態になったと思った。

例えばスマートフォンであれば Galaxy や Pixel に乗り換えることができるが、7 インチから 8 インチにかけてのタブレットに関して言うとファーウェイと同等、もしくはちょっと劣る程度であったとしても代替機が全くない。 仕事で取引先に業務用タブレットの購入を促す際に 7, 8 インチタブレットの推奨端末を聞かれた時、いくら既存ファーウェイ端末が変わらず使えるとはいってもこのニュースの中「MediaPad M5 Lite 8, 若しくは MediaPad M3 Lite 8 を推奨します」とは言い辛い。

「LTE モデルを有しているそれなりのスペック、コストパフォーマンスの 7, 8 インチタブレット」という条件でギリギリ使えそうなのが Lenovo TAB4 8 Plus だがこれも中国メーカーの上やはりファーウェイの製品に比べると見劣りする。 あとは ASUS の ZenPad 8 くらいだろうがスペックが低すぎる。 サムスンあたりが作ってくれればまた少しは違うのではないかと思うのだが、フォルダブルデバイスの方に注力するためもう 8 インチタブレットは作らないと思われる。

ファブレットで我慢していくしかないか

プライベートで 8 インチタブレットに通話 SIM を刺してそれ 1 台をスマートフォンのように持ち歩き、バイクのナビの時のみ SIM を抜いた小さめのスマートフォンに Bluetooth テザリングでナビをさせる、という運用方法をいつか実現しようと思い描いていた。 しかし今回の騒動でそれがかなり難しくなってしまった。 やるなら現行 MediaPad M5 を購入してそのままずっとそれでいくしかないが、このニュースの中ではそんな気になれない。 かといって iPad mini にはしたくない (そもそも iPad mini では通話 SIM を刺していても電話ができない)。 仕方なく Pixel 3 XL や Galaxy Note 10 のようなファブレットを使用していくしかないだろう。

本日注文していた HUAWEI WATCH GT が楽天から届いた。 早速軽く使ってみたのでファーストインプレッションを書く。

バンドの装着感は良好だが写真と色味が違う

新しく出た 46mm モデルのダークグリーンのバンドのものを購入したが、バンドの素材が Apple Watch スポーツモデルでも使用されているフルオロエストラマーというものらしくしなやかなシリコン風の素材でとてもつけ心地が良い。 若干重さは感じるが許容範囲内だと思う。 ただ、写真よりバンドの色味がくすんで見える。 写真だと深緑といった感じの色だが実際見てみるとほぼ黒寄りの緑といった感じだ。

設定はとても楽

スマートフォンの方にファーウェイのヘルスケアアプリをインストールしてあれば、そのアプリ上から Bluetooth のペアリングとファームウェアのアップデートまでやや時間はかかったが簡単にできた。 Bluetooth ペアリング後はヘルスケアアプリを見れば歩数、睡眠、消費カロリー、心拍数、活動記録などグラフで見れるのが便利だ。 いちいちスマートウォッチの小さい画面で見なければならないのだと億劫だからだ。

睡眠機能に驚いた

「心拍数」もなかなかすごいが、一番驚いたのは睡眠機能だ。 装着して寝るだけで自分がいつ寝ていつ起きたか、深い睡眠と浅い睡眠はいつ合計何時間とったか、夜中いつ覚醒したかなどすべて表示される。 腕時計をしながら寝るというのにはちょっと慣れないが、この機能があるならば腕時計をしながら寝ることを習慣づけたいと思った。

バッテリーが持つのが一番のウリらしいが

この時計はバッテリーが 1 週間単位で持つほど電池持ちがいいのがウリらしいが、まだ使い始めたばかりなのでよく分からない。 昨晩 94 % まで充電してから結構操作していて、朝に少しワークアウト実施 (GPS 起動) して今 12 時間経ったが 85 % になった。 この使い方だと 1 週間は持たない気がする。 これが Apple Watch だとこの使い方で既に 50 % 以下になっているのだろうか。 それはかなり厳しい……。 ともかくバッテリーに関してはもう少し様子見が必要だ。

通知は確かに使える

スマートウォッチに送りたい通知 (例えば LINE や Gmail) をあらかじめヘルスケアアプリで設定しておけば、通知が届いた時にスマートウォッチが振動して通知内容が表示される。 確かにこれは便利だ。 特にバッグにスマートフォンを入れている女性には嬉しい機能だと思われるし、私も将来的にスマートフォンでなく 8.4 インチタブレットをスマートフォン代わりに使おうという事になった時に使える。

総括

2 万 5 千円弱で購入できるスマートウォッチとしては費用対効果がかなり高いのではないかと思う。

割と普通に使えた

HUAWEI WATCH GT というスマートウォッチのニュースが流れていたのでその情報を見ていた。 Apple Watch が出た頃は 1 日 1 回バッテリー充電しなければならないような有様で全く不便なものだな、と思っていたのだがこのスマートウォッチに関しては 1 週間、下手すると 2 週間は持つらしい。 今はまだスマートフォンを使用しているので不要だが、将来的に 8.4 インチタブレットをスマホ代わりに使うことになった時に結構使えそうな感じがする。

そこで気になったのが、このスマートウォッチがスマートフォンの Huawei Health アプリを使用しているらしいことだ。 このアプリはファーウェイのスマートフォンにプリインストールされているが、タブレットにはプリインストールされていなかった。 気になって検索してみたが、タブレットでも LTE モデルならば普通に Google Play からダウンロードしてインストールできるようだった。 そして、別にファーウェイ製のスマートフォンでなくても Google Play からダウンロードして普通に使うことができた。 これを知らなかったので Pixel 3 XL でずっと歩数計なしでやっていたが、早速今日から使うことにした。

電池の最適化に引っかかって計測できていなかった

次の日の朝早速歩いてみたが全く計測されていなかった。 どうも昨今のスマートフォンの「電池消費の最適化」に引っかかっておりサービス停止されてしまっているようだった。 アプリ内に警告も何も表示されないので分かりにくい。 そして言うまでもないがファーウェイ製のスマートフォンを購入した場合プリインストールされているのでこの問題は起きない。 こちらのブログの情報にあるように、スマートフォンの「設定 -> アプリと通知 -> 詳細設定 -> 特別なアプリアクセス -> 電池の最適化」から「ヘルスケア」と「Huawei Mobile Services」アプリの最適化をしないように設定する。

アプリ自体は使いづらいが楽天との連携が魅力

楽天市場の SPU が今月から改悪され、先月までは楽天ブックス 2,000 円以上の購入で +1 倍だったものが楽天ブックス 1,000 円以上の購入で +0.5 倍となってしまった。 その代わり楽天 Kobo で 1,000 円以上購入で +0.5 倍という条件が追加されたが、今まで私は基本的に Kindle で購入しており Kobo は使ったことがない。 今回は洗濯機を購入するため 1,000 円購入してもそれ以上のポイントがバックされる予定なので、試しに Kobo アプリを使用してみた。

小説を読んでみたが、10 インチタブレット (MediaPad M5 Pro) だと何故か設定アイコンの表示が崩れてうまく設定できない。 スマートフォンや 8 インチタブレットならば大丈夫だったので 10 インチタブレットでの使用は想定されていないのかもしれない。 あと小説のページめくりを行った際に Kindle や Google Play ブックスは指に吸い付いてくるようなスライドアニメーションをするが Kobo は指で完全にスワイプした後でアニメーションが始まりページ送りが進むといった感じで正直劣る (文章だと書くのが難しいので実際にアプリで試してみるのがよい)。 Kindle や Play ブックスだと行間幅が設定できるが Kobo は設定できないのもつらい。 ただ Kobo の日本語フォント (Kobo 筑紫明朝) はかなり読みやすいので、その点だけはよい。

ただ今回 Kobo が SPU の条件に追加されたというのもあるし、楽天ポイントが使用できポイントも貯まるのが魅力ではある。 アプリの使い勝手が劣るのはちょっと困るが、それに目を瞑ってでも使う価値はあると思った。

「罪と罰」と「決断力 + 大局観」を購入

1,000 円以上買う必要があったのだが、初回 500 円オフクーポンがあったので合計 1,500 円以上にする必要があるということで罪と罰 (完全版) 決断力 + 大局観 (羽生善治)を購入してみた。 罪と罰は普通は 2, 3 冊に分かれている長編小説だが、前述の本は 1 冊にすべて入って 800 円となかなかお得だった。

罪と罰を読むのは高校生の時以来ではあるがなかなか面白い。 余談だが私のハンドルネーム「コジオン」は罪と罰の主人公の名前からとっている。 「決断力」はちょっと読んだところ既視感があったので、実は以前読んだことがある (20 代の頃?) のかもしれなかった。 いい本であるのは間違いないが、読んだことがあるのであればちょっと失敗したかもしれない。

Android Studio で公開用の APK を作成する場合に Build -> Generate Signed APK から署名された APK を作成するが、署名ファイルを後から変更した場合に古い情報がキャッシュされてしまいメニュー上からいくら署名ファイルを新しいものに変更しても古い方で署名されてしまう不具合が起きる場合がある。 私はこの問題に 2 回遭遇したが解決に時間がかかってしまったのでメモ。

Generate Signed APK のキャッシュはモジュール直下の build/intermediates/signing_config/release/out/signing-config.json に存在する。 この中身の JSON を直接書き換えることで新しい署名を使用することができる。

スマートフォンを買う時は毎回 Spigen のスマートフォンケースを買うようにしている。 ゴツくて頑丈なので男性向けかもしれない。 今回 Pixel 3 XL 向けに購入したのはネオ・ハイブリッドというシリーズのバーガンディで、これが写真だとあまり良くなさそうに見えるが実際届いたものをみてみると Pixel 3 XL のノットピンクの筐体にもよく合っているし、レビューにも書いてあったが「ファミコンカラー」のような感じがして親近感を覚える。

早速 Pixel 3 XL のおサイフケータイ機能を使用してみた。 とりあえずモバイル Suica で通勤してみた。 全く違和感なく使えた。 ただ、これが便利かどうかは「人による」としか言えない。 Google Pay を使うことによってビューカードでなくても年会費を払わずにクレジットカードチャージができるようになったので、ビューカードを持っていない人は結構便利と言えそうだ。 モバイル Suica にするとオートチャージもできる (例えば 5,000 円を切ったら 3,000 円チャージするなどの設定ができる)。 このオートチャージは Suica 圏内でなくてもちゃんとチャージしてくれるので便利だ。 ただ、オートチャージはビューカードも機能として持っているので、モバイル Suica が特段優れているというわけではない。 ビューカードを持ち歩かなくていいというのが最大の長所ということかもしれない。 私に関して言うとビューカードと銀行キャッシュカードが一体になったものを使用しているので、モバイル Suica を使おうが使うまいが結局持ち歩かなくてはならないところが微妙ではあった……。 ただ、スマホをかざして改札を通過するのが何か懐かしくて楽しいというのはあった (昔キャリア経由のスマホを使っていた時はよくモバイル Suica で乗っていたものだった)。

他には Edy を考えてみたが Suica も Edy も今まで使っていたそれぞれのカードからの残高移行ができない。 ということは既存カードを使い切ってからでないと何となく使いにくい。 とはいえ、せっかく FeliCa 付きの端末を使っているのだから何とか Edy を使い切って移行しようと思った。

2/10 に注文した Pixel 3 XL がようやく Play ストアから発送され家に届いた。 早速弄り倒しているので、ファーストインプレッションを書いておく。 尚、はじめに結論を書いておくと非常に好印象で買ってよかったと思った。 ネット上では「値段とスペックが合ってない (高すぎる)」という意見も散見されるが、これは触ってみてはじめて良さがわかる端末のような気がした。

Not Pink は確かにピンクではない

Pixel 3 及び Pixel 3 XL のカラーバリエーションは白・黒・ピンクの 3 色だが、バレンタインセールの 25,000 円引きの対象がピンク (Not Pink) のみだった。 どうせカバーをつけて使うわけだし色など大して気にならないだろうとそのままポチったが、これは正解だった。 Not Pink の名前通り全然ピンクじゃない。 白にうっすらと淡くピンクがかっている感じで、ピンクと言われないとピンクと視認できない。 ベージュと言ったほうが近いだろうか。 ただ、筐体横の電源ボタンは確かにピンクだ。 これがアクセントになっているわけだが、裸で使う人は気になるのかもしれない。

横幅が広い有機 EL がとても美しい

Pixel 3 XL は最近の縦長液晶の流行りの中ではまずまず横幅が確保できている端末であることは以前散々調べた通りだが、更に ppi 値が 500 以上と非常に高い。 しかも有機 EL で発色がとても良く、至近距離で見ても見惚れるくらい画面が綺麗だ。

尚、私はノッチが大嫌いで Pixel 3 XL はデフォルトで大きいノッチが配置されているのだが、開発者オプションから隠すことができるのは事前に知っていたので早速隠してみた。 結果、全く違和感なく普通の大きいスマホといった雰囲気になった。 とてもいい感じだ。

スクロールや画面のトランジションなど異常に滑らか

これは前の端末である P20 (といってもまだ出て 1 年経っていないので十分現役スペックの端末である) と比べると顕著だったのだが Twitter やまとめサイトや Google Play ストアなどでのスクロールが異常に滑らかだ。 アプリ同士の画面の切り替え (トランジション) も美しく iPhone を触っているかのような感じがする。 Android 9 (Pie) の効果かもしれないが、とても驚いた。

余計なアプリが入っていないのはやはり良い

これは Nexus 時代からの利点だったが Google 謹製ということで最初は本当に最低限のアプリしか入っていないということで好印象だ。 最近だとデフォルトのアプリが比較的少ないファーウェイも Facebook アプリなどを同梱してくるので少し気になっていたのは事実。 本当に素の状態の端末が欲しいのであれば最重要候補になるだろう。 まぁ 1 点だけ文句をつけると「おサイフケータイ」アプリが異彩を放っているので FeliCa を使わない方は気になるかもしれない。

端末をノックしているかのようなバイブレーション

多くの端末では指紋認証してスクリーンロックを解除したり戻るボタンを押したときなどにバイブレーションを発生させることができる。 P20 もそうだ。 私はこれが好きなので必ず ON にしているのだが Pixel 3 XL のこのバイブレーションが他の端末とちょっと違う。 何か端末を誰かがノックしたかのような軽やかで短いバイブレーションになっている。 これが何か新しくて楽しい。

バイクのナビ用の端末は別に用意したい

Google 謹製スマホは 3 年前の Nexus 5X 以来なので期待して待っている。 今回の端末は長く使うために今使っている P20 を売らずに取っておこうと思う。 P20 を売却してしまうとバイクのナビ用にも Pixel 3 XL を使うことになるが、バイクのナビとして使うと端末が汚れやすくなるのが気になる。 それに万一転倒などで落下させてしまい定価 10 万超えの端末を破損させてしまったら精神的ダメージがかなり大きいし、ロードサービスなどに電話しようとしても電話できないという羽目になりかねない。

バイクのナビ用の端末としては以下の要件を満たしているのが望ましい:

  • GPS が搭載されていること
  • 防水であること (雨対策)

P20 はちょうどこれを満たしている。 そして P20 が駄目になってしまったら何を使えばいいのかというところだが、これはヤフオクで iPhone 7 (防水) が安く手に入るのでそれが良さそうだ。 とはいっても P20 から買い換える時はもっと世代が進んでいそうだが。 P20 を売却して iPhone 7 を調達するという手もあるが、昨今のファーウェイ騒動のせいか P20 の買取価格がかなり安くなってしまっている。

Pixel や iPhone のバッテリー交換はやりやすい

Pixel シリーズや iPhone, iPad のバッテリー交換に関しては iCracked という店が使用できる。

iCracked(アイクラックト)は、2010年に米カリフォルニア州・シリコンバレーで創業した世界最大級のスマートフォン・タブレット修理事業者です。

とあるが、サービスとして明記されているのは前述の機種なのでスマートフォンなら何でも、というわけではなさそうだ。 バッテリー交換したくなったらこの iCracked に持ち込めば即日対応してもらえる。 東京に近ければ間違いない (埼玉県は何故か嵐山町のみなのに驚き)。 気軽にバッテリー交換できるのであれば綺麗に長く使っていこうという気にもなるというものだ。 Pixel シリーズであれば最新の Android OS が 3 年は提供されるので、開発用としても長く使っていけそうだ。

本当に大事なのは横サイズ

今日のスマートフォンは軒並み縦長液晶でノッチがついているというデザインになっている。 以前は例外を除けばほぼ 16:9 で統一されていたが、今売られているものは大体がノッチ込みで 18.5:9 やそれ以上に縦が長い。 これがとても使いにくいと思っている。 縦が長いからというよりも、横が短すぎる。 CHESS HEROZ などのゲームアプリを起動すると縦が長いぶんのスペースは単純に死に領域となるので意味がない。 それよりも横が短すぎるのでゲーム画面が小さくなり、チェスの一マスが小さくなってタップし辛い。

以前 16:9 の Mate 9 から 18.5:9 の P20 に機種変更した。 画面のインチサイズ的にはそれほど変わらないのだが、横のサイズが全く違う。 昔最近の自分のガジェット事情という記事で各タブレットの液晶の縦横実寸サイズを掲載したのだが、今回は私が今回購入を検討したスマートフォンを並べてみようと思う。 計算には以前と同様に液晶ディスプレイの表示部実寸法を調べるサイトを使用する。

端末名画面サイズ (inch)解像度縦 (mm)横 (mm)ppi (参考)
Pixel 35.52160x1080125.2862.64439.082
P205.82244x1080132.39663.72429.374
Mate 95.91920x1080130.5673.44373.374
Mate 10 Pro6.02160x1080136.0868.04402.492
Pixel 3 XL6.32960x1440145.0470.56522.49
Mate 20 Pro6.43120x1440146.6467.68536.918
Find X6.42340x1080147.4268.04402.689
Mate 20 X7.22244x1080163.81278.84345.885

上記をみる限り、やはり Mate 9 は名機だったと思う。 そして P20 の横サイズは Mate 9 よりも 1 cm 近く短い。 この 1 cm がとても大きい。 スマートフォンというよりタブレットな Mate 20 X は別格として置いておくとしても、今回私が購入した Pixel 3 XL は Mate 9 には敵わないものの、まずまずの横幅を確保できている。 これならチェスも快適にプレイできそうだ。

チェスのようなゲームを快適にプレイしたいだけなのであれば、環境が許せば上記表の Mate 20 X もしくは MediaPad M5 のような 8.4 インチタブレットを購入するのが最善だ。 しかしタブレットを常に持ち歩くのは私のように相当なタブレット好きであってもなかなか難しい。 例えば仕事で昼食に出かけたり、バイクでツーリングに行ったり、ちょっと買い物に出かけたりといった時にさすがにタブレットも持っていったりはしない。 そういう時でもちょっとポケットから取り出してタクティクスを解きたいというのがチェスを趣味とする者の人情というものだ。

iOS はどうか

ついでなので iOS も調べてみた:

端末名画面サイズ (inch)解像度縦 (mm)横 (mm)ppi (参考)
iPhone 7/8 Plus5.51920x1080120.9668.04400.529
iPhone X5.82436x1125133.9861.875462.626
iPhone XR6.11792x828139.77664.584323.614
iPhone XS Max6.52688x1242150.52869.552455.549

ということで画面の横幅だけで判断するならば iPhone 7 Plus, 8 Plus もしくは iPhone XS Max が良さそうだ。 iPhone 7 Plus であればヤフオクで割と安く手に入れることができるので、お金をかけたくない人には有効な選択肢だと思う。

久々の Google 謹製のスマホ

今はファーウェイの P20 というスマートフォンを使用しているのだが、このスマホは画面サイズの表記だけ見ると 5.8 インチということで十分なサイズのように思えるが、今どきのノッチ込みの縦長液晶なので実際の液晶サイズはそれほど大きくなく、特に横幅がないのでチェスをプレイする際にマス目が小さくて操作し辛かったりする。 それに私も歳なのか小さい画面の端末をずっと見ているとどうにも目が疲れるというのもあり、もう一回り大きい端末に買い替えたいという欲求はあった。 同じくファーウェイの Mate 20 X というファブレットが 7.2 インチということでなかなか魅力的に見えたのだが、ここまで大きいサイズだとバイクのナビとしてマウントするには抵抗があるので、バイクに乗る時だけスマホ 2 台持ちのような感じになってしまいそうなのが少し気がかりだった。

Google 謹製の Pixel 3 XL にも心惹かれたのだが、何しろ値段が高すぎる。 iPhone 並に高い。 Expansys で買えばそれなりに安いのだが、こちらはグローバル版だ。 グローバル版には日本版についている Felica が無い。 別に Felica など使わなくてもいいのだが、せっかくなので使ってみたいし売却する時に差がつくかもしれないと考えた。 しかし今日何の気なしに Google Play ストアを見ていたらバレンタインセールということでなんと 25,000 円引きになっていた (Pixel 3 の方は 20,000 円引き) ! 色がピンクのみというのが少し気になったが、どうせカバーをつけて使うのでどの色にしようが変わらない。 ということで少し考えた上で購入した。 楽天でいつもの Spigen のスマホケースも注文した。

ダサいノッチは隠せることを確認済

Pixel 3 XL のノッチ (画面上の切り欠き) はとてつもなくダサいと思っていて、これが隠せないようであれば絶対に買いたくなかった。 幸いノッチを隠す方法は検索するとすぐに出てきた。 ただ、ファーウェイなどの端末のノッチを隠す内容とはちょっと違い、開発者オプションで設定する上にノッチを隠すとノッチ部分が黒くなり表示領域が少し下がるといった感じになるようだ。 ファーウェイの端末の場合はノッチ部分が黒くなりノッチ部分に通知が表示される (つまり表示領域は同じままでノッチが目立たなくなる) のでこちらの方がいいのではないかと思ったが、最近の縦長スマホの液晶の縦横比バランスがイマイチだと思っていたので別に Pixel 3 XL 方式でも問題ないのかもしれない。

ボタン連打されてしまい不正な動作をするのはよくある話

Android アプリ開発においてボタン、というより View に対しタップイベントを仕込む際に View.OnClickListener() を使用するというのは基本だと思うが、何も考えずに実装すると連打された時にイベントが 2 回、3 回発行されてしまう。 これが問題になってくるのが例えば登録処理などの部分で、データが 2 重に登録されてしまったり既に登録されている為不正なレスポンスが返却されてきてしまったりする。

対応策に関しては使い古された話題なので検索すればいくらでも出てくるが、例えばお手軽にAndroidのButtonの連打を防止してみたの記事のようにボタン押下直後にボタンの押下を無効にし Handler を使って 1 秒後に再度有効にする、といった処理を挟む。

ただ、毎回このようなお決まりの処理を書くのは辛いので Kotlin の Extension を使って共通化してみた、という記事である。

実装例

このような Extension を定義する:

/**
 * 短時間での連打による複数回実行を防ぐ setOnClickListener 実装.
  *
  * @param listener setOnClickListener
  */
 fun View.setOnSingleClickListener(listener: () -> Unit) {
     val delayMillis = 1000 // 二度押しを防止する時間
     var pushedAt = 0L
     setOnClickListener {
         if (System.currentTimeMillis() - pushedAt < delayMillis) return@setOnClickListener
         pushedAt = System.currentTimeMillis()
         listener()
     }
 }

私は Handler ではなく pushedAt という押下時の時間を持たせて押下時に現在時刻と比較することにより 1 秒以内の連打を防止する実装を行った。 勿論多くの記事がやっているように Handler でも良いが個人的には Handler インスタンスをその都度 new するより生成コストの低い Long を持たせる方が好きだ。

これを以下のように使用できる:

val register = view?.findViewById<AppCompatButton>(R.id.register)
register.setOnSingleClickListener {
    // 連打してはいけない登録処理...
}

以下 12.9 インチ iPad Pro のような大きすぎて持つのも辛いようなもの以外のサイズのタブレットにも通用する内容と思っている。

スタンド付きのケースでなくシンプルなカバーが良い

MediaPad M5 や MediaPad M5 Pro には最初からタブレットケースが付属しているのだが、それがこんな感じのいわゆる iPad のような風呂蓋のようなケースだ。 これの利点は本体を傷や衝撃から守るだけでなくそれ自体タブレットスタンドとして使用できるというところなのだが、私はこれがあまり好きではない。 タブレットスタンドとして立てて使う時以外、普通に縦持ちや横持ちで持っている時に蓋の部分がブラブラしたりちょっと束ねて持ったりすることになりあまり快適ではないからだ。

今回私が購入したものがこの普通のシリコン製の包み込むだけのカバーなのだが、これだと普通に縦持ちや横持ちで持って使用した時にとても快適だ。 私の場合は電車の中やベッドに入ったまま使うといった場合に快適ということになる。 欠点はスタンド機能がないのでそれだけでは立てて使えないということだが、別売のタブレットスタンドを使えば問題ない。 「タブレット スタンド」で検索するといろいろな種類のものが出てくるので自分の好みのものを選択できるし、何より多くのものは安価だ。

Bluetooth テザリングを使えば Wi-Fi 版でも問題ない

私はスマートフォンの方は楽天のポイントの関係で楽天モバイルを契約しているのだが、これがお世辞も良いとは言えない SIM で特に通勤電車や昼時に低速通信になりブチブチ切れたりしていた。 そのため以前のタブレットである MediaPad M5 の時は高速な LINE モバイルを使いからと LINE をタブレットでも見たいからという理由で LTE 版を使用していた。 これはいつでも好きなタイミングでネットに接続できてとても便利なのだが、格安 SIM を 1 回線契約するとどうしてもその分の費用が増えてしまう。 私の場合は基本 1 G のプランで足りないときだけ 3 G に変更していたのだがこの使い方でも年間 1 万円強といったところだ。

MediaPad M5 Pro は日本国内版だと Wi-Fi モデルしか無いのでそれにしたが、懸念点はテザリングがあまり快適ではないのでは、ということだった。 しかし今回 MediaPad M5 Pro に買い換える前にしばらく Bluetooth テザリングで運用してみて全く問題ないことを確認したので躊躇なく買い換えることができた。 ネット接続を共有するテザリングには 3 種類の方式 (USB, Wi-Fi, Bluetooth) があるのだが、PC と接続すること前提の USB は置いておくが Wi-Fi テザリングはずっとつけておくとスマートフォン側の電力を消費してしまうし常時 ON にしておくのが難しい。 そこをいくと Bluetooth テザリングに関してはスマートフォン側を一旦 ON にすればスマートフォンの電源を落とすまで有効の上消費電力も少ない。 通信速度に関しては仕様上 1 Mbps が上限というのが少し残念だが、私の使用用途に関していえば全く問題なかった。

問題は Bluetooth テザリングだとスマートフォンとタブレットの距離が離れてしまったり、タブレットを長時間スリープにしていたりするとテザリングが切断されてしまうことだが、これに関しては Bluetooth Auto Connect というアプリで「タブレットの画面が ON (スリープ解除) になったら自動的にスマートフォンに Bluetooth 接続」という設定をすることで解決できた。

8.4 インチタブレットである MediaPad M5 から 10.8 インチの MediaPad M5 Pro に買い換えてみた。 ちょっと調べればわかるがこの 2 つは SoC (CPU, RAM サイズ) が同じなので画面サイズが違うところ以外は大体同じような感じで使えることが予想できた。

8 インチか 10 インチかは人による

人によるのかもしれないが 8.4 インチタブレットを 5 ヶ月使ってみて思ったのは、軽くて持ち運びが便利なのだが縦持ちする場合でも片手でホールドするような場面は結局ほとんどなく小さい利点をあまり生かせなかった。 私は以前からずっと 10 インチ級のタブレットを使用していたので 10.8 インチの MediaPad M5 Pro が重いとは特に感じない。 むしろ昔の 10 インチタブレットは大抵 600 g 以上あったのを考えると MediaPad M5 Pro の 500 g は割と軽いように感じる。

私はゲームはほとんどしないし、プレイするゲーム (チェスやバックギャモンなどのテーブルゲームが主) が別に横向きでホールドしながらプレイする必要のないゲームばかりなので 10 インチタブレットでも快適にプレイできた。 あと Amazon プライムや YouTube などの動画はやはり大きい画面のほうが快適だ。 結局のところ使う人のスタイルによって最適な画面サイズは変わってくるのだろう。

早速電車でも使ってみたが、さすがに電車の中の取り回しは 8 インチタブレットに軍配が上がるのは間違いない。 縦向きならばそんなに気にならないが立ちながら横向きで持つのは結構疲れる。 このタブレットは 10.8 インチということで従来の 10 インチタブレットよりも更に少し大きいのが効いてくるようだ。 電車の中メインで使用する方は迷わず 8 インチタブレットがいいだろう。

32 GB と 64 GB の差はかなり大きい

MediaPad M5 の方は日本で販売されているモデルは残念ながらすべて 32 GB のものとなっている。 この 32 GB というのがキツく、OS やプリインストールされているアプリが含まれているので実質的には初期状態でも 20 GB 強程度の空き容量しかない。 これだと写真や音楽、動画を溜め込まない人であったとしても心もとない容量だ。

そこをいくとこの MediaPad M5 Pro は 64 GB ということで初期状態で 50 GB 強の空き容量がある。 空き容量的には MediaPad M5 の 2.5 倍ということになっており、普通に使うぶんには十分な容量だ。 更に写真や音楽、動画をいっぱい保存したい場合は microSD カードを挿せば問題ない。

スタイラスペンは一般人には不要か

MediaPad M5 Pro にはスタイラスペンが付属してくる。 これが Apple Pencil のように USB-C 端子で充電できるし筆圧感知して線を書き分けられる……らしいが個人的にそういう機能は使わないし、別に手書きをしたいとも思わなかった。 マウス代わりに使えるかと思ったが Web 上ではタップしているのと同じ挙動となるようだ。 というわけで個人的には不要だった。

デスクトップモードは割と面白い

MediaPad M5 にない機能として「デスクトップモード」があり、これを起動すると Windows のような画面になりアプリをウインドウで開いて複数起動することができる。 これがなかなか便利そうだ。 Bluetooth キーボードとマウスをつなげればちょっとしたノート PC 気分だ。

プライバシーポリシー

広告 ID の使用

広告を表示する際に固有の ID が利用されています。 アプリから送信されるデータという点で、利用することをここに明記しています。 この情報から個人が特定されることはありません。

インターネットアクセス

アプリ内への広告表示に利用されています。

バイブレーション

タイマー機能での通知で使用しています。

作った理由

最近はプライバシーポリシーを明記しないとストアからの掲載が削除されてしまうケースが多くなった。 今回私の作成している時報アプリであるコジ時計も広告を使用しているという理由で掲載から外されてしまった。 そのため急遽プライバシーポリシーのページを作成して対処することにした。

プライバシーポリシーは HTML リンクを Google Play の対象ページ及びアプリ内から張る必要があるが、プライバシーポリシーのみのサイトを新たに作成するのも手間なので Blog の 1 記事として載せてお茶を濁しておく。

この対応に関しては Cocoamix.jp 様の記事が大変参考になった。

Android アプリがどの SDK バージョンをターゲットにしているかの指定に targetSdkVersion があるが、この値は常に最新の SDK バージョンを指定することが推奨される。 今現在だと 28 (Android P) となっている。 厄介なのが targetSdkVersion をインクリメントすると既存のコードでコンパイルができなくなったり @Deprecated アノテーションが付与された API が増えていたりと修正を余儀なくされるということだ。 古いアプリに関してはこれが面倒なので無理に上げなくていいのでは、と構えていたがデベロッパに対しては既に何度かアナウンスされていた通り 2018 年 11 月以降既存アプリのアップデートでも targetSdkVersion >= 26 が必須となる。 26 というのは結構高い値で、これは困る。 ちょっとした修正をしてお茶を濁そうとした時でも targetSdkVersion を上げてコンパイルが通るように調整且つ既存のコードが正しく動作するかを確認しなければならないからだ。 「作ったアプリをずっとメンテナンスし続ける」というのは結構辛い。

よくあるクラッシュレポートツール

Firebase Crashlytics に概要の説明が書いてある:

Firebase Crashlytics は軽量なリアルタイムのクラッシュ レポート ツールで、アプリの品質を低下させる安定性の問題を追跡し、優先順位を付け、修正するのに役立ちます。Crashlytics を使用すると、クラッシュをインテリジェントにグループ化し、クラッシュにつながった状況をあぶり出すことによって、トラブルシューティングの時間を節約できます。

この手のツールというのは昔から何かしらあって Google Play Console にも同様にクラッシュレポートを表示する機能があるのだが、これを入れておくと開発中のアプリを誰かに試してもらっている時にクラッシュした場合にそのバグレポートを Web 上ですぐに確認できるので便利だ。 ちなみに昔は Firebase Crash Reporting という機能があったのだがこれは非推奨となり Firebase Crashlytics への移行が求められる。 というわけで私としても既存アプリの Crashlytics への移行を余儀なくされたわけだが、その過程で微妙にハマったのでメモ。

手順を正しく踏まないと収集開始されない

導入方法は 公式の Get started に詳しく書かれているのでこの通りにやれば簡単に導入できる。 はずだったのだがこの通り build.gradle に書いてアプリを実行、適当にクラッシュさせても全く反映されない。 具体的には Firebase Console から対象アプリの Crashlytics を開いても以下の未設定の画面のままとなる。

Crashlytics 未設定

どうも以下の手順を踏まないと駄目なようなので書いておく。

1. 「Crashlytics を設定」を押しておく

前述の Crashlytics の未設定を示す画面で「Crashlytics を設定」を押下し画面遷移、このアプリでの Crashlytics の利用は初めてですか?の質問にはいと答える。 そして次の SDK をインストールしますの項目で 「Crashlytics ドキュメントに移動」ボタンを押下する。 すると以下の「Crashlytics 設定待ち」の画面になる。 この画面を立ち上げたままにしておく。

Crashlytics 設定待ち

2. アプリを起動して一旦クラッシュさせる

前述の状態を満たした上でアプリをビルドし、何かしらのわざとクラッシュするコードを仕掛けてクラッシュさせる。 公式に書いてある通り Crashlytics.getInstance().crash() がいいだろう。 そして、公式に書いてある通りアプリを起動した瞬間にクラッシュするのではなく、何かしらのボタンを押下したりなどの際にクラッシュさせるのがいいと思われる。 何故ならどうも Crashlytics のレポートは再度アプリを起動した時に送信されるようだからだ。

3. アプリを再度起動する

Crashlytics のレポートを送信させるために再度アプリを起動する。

4. Crashlytics 設定待ちの画面を見ながら少し待つ

  1. で設定した設定待ち画面を見ながら少し待っていると、設定が完了したことを示す画面に遷移する。
あい
2018-08-11 15:19:37
クラッシュさせてもさせてもCrashlyticsの画面に遷移しないので困り果てていました。こちらの情報でやっと解決させることができました。ありがとうございました!
コジオン
2018-08-11 15:25:09
おお、こんな情報が役に立つとは思っていませんでした。
お役に立てたようで嬉しいです。

サムスンの microSDXC カードである EVO Plus の 128 GB を先日購入し、今日届いたのでまた試しに A1 SD Bench で計測してみた。 このカードは公称では読み込み最大 100 MB/s に書き込み最大 90 MB/s なのだが、計測してみたところ 読み込み 57.80 MB/s に書き込み 49.77 MB/s という結果だった。 何度か計測して同じような値が出たので間違いないと思われる。 私が前使っていた microSD カードに対して書き込み速度は大幅に向上し MediaPad M5 の内部ストレージの速度とも遜色ない状態となったが、読み込み速度が思ったより振るわなかった。 まぁ折角買ったわけだし、まずまず満足な結果なのでこのまま大切に使おうと思う。

Android の環境設定で「ストレージの設定 -> デフォルトの保存場所」を SD カードにしても即座にすべてアプリのデータが移るわけではないようだ。 それどころか内部ストレージ側に本来不要なデータが残り続けてしまうような気がする。 端末を一旦リセットして最初から SD カード保存でやった方が良さそうだ。 今ちょうど土日で時間があるので一旦綺麗にしようと思った。

日本では出ないかと思っていた

ファーウェイのスマートフォンである P20 シリーズにはそれぞれハイエンド、ミドルレンジ、ローエンドとして P20 Pro, P20, P20 lite (Lite でなく lite なのは伝統のようだ) が用意されており P20 Pro に関してはドコモ専売、P20 lite は au から出るということで日本での SIM フリー端末として P20 が発売されないのでは、と思っていたが先日 11 日に発表があった通り 6/15 に発売するようだ。

私が愛用しているスマートフォン (というよりファブレット) である Mate 9 は素晴らしい端末であり、特にライカ製のカメラで撮影した写真はこの Blog で何度も掲載している通りとても美しい。 ただ、私はバイクのナビで使用しているという関係もあって防水モデルの方が望ましい。 また MediaPad M5 などのタブレットと併用する為、端末のサイズはできるだけコンパクトな方がいい。 ファーウェイはものすごくコンパクトなモデルというのは販売していないが、この P20 ないし前モデルの P10 などは昨今のスマートフォンにしては比較的サイズがコンパクトで控えめながらも防水 (防雨程度) 端末であり、且つしっかりとライカ製のデュアルカメラを搭載している。 現時点で私の理想に近いと言っていいスマートフォンとなっている。

P20 を見て思うこと

前情報で示されていた通り、この P20 (Pro と lite も) には iPhone X のようなノッチ (液晶カケ) が存在する。 私がこれがとても格好悪いと思っているのだが、ファーウェイはちゃんとノッチが隠せるようなオプションも用意している。 ノッチを隠す設定にするとノッチの部分は黒塗りになり Android のステータスバーとして機能するようだ。 この設定が無かったら P20 は見送っていただろう。

また P20 の価格がミドルレンジモデルにしては高価で 69,800 円 (税抜) となっている。 ただこれに関しては Mate 9 が当時にしてはとても挑戦的な価格だった (税抜 60,800 円) だけで、最近の価格設定を通常に戻してきただけのように見える。 SoC は Mate 10 Pro や P20 Pro と同じ Kirin 970 だし内部ストレージ容量は 128 GB もある。 正直 128 GB もあっても使い切れないくらいだ。 日本発売の MediaPad M5 の方にこのくらいの容量が欲しかった。

一つ気になるのが AppGallery というファーウェイ独自のアプリストアがプリインストールされているということだ。 こういう独自のアプリストアというのは何度も見てきたがどれもいい印象がないし正直邪魔だった。 MediaPad M5 などと同じく幾つかのプリインストールされているアプリは消せないだろうし、変な動きをしないか気になるところだ。 追記: 日本国内で販売されているものにはインストールされていない。 結局何だったのか。 Google Play が使えない中国国内用だろうか。

多分狙っているのだろうが、また都合よく楽天スーパーセールが始まるので買うのに丁度いい。

ppi とは

ディスプレイの緻密さを表す単位として ppi がよく用いられる。 Wikipedia の記載を引用する:

ppi(ピーピーアイ)とは、pixel per inchの略で、ディスプレイやビットマップ画像における解像度を示す単位である。別名画素密度 (pixel density) とも呼ばれる。 解像度とは、すなわち、画像を表現する格子の細かさであり、一般に1インチあたりのピクセルの数を表す(1平方インチあたりではない)。ppiで表したピクセル密度のことを単にppiと呼ぶことがある。

つまり、例えば 220 ppi だと 1 インチ (2.54 cm) の直線の中に 220 点のドットが含まれているということになる。

久々に MediaPad M3 Lite 10 を使ってみた

MediaPad M5 (8.4 インチ) をしばらく使って慣れてきたので、久々に MediaPad M3 Lite 10 を引っ張り出してきて試してみた。 やはり自分にとっては画面が大きいほうがいいのかが気になったからだ。 これに関しては個人差も大きいだろうが、私にとっては 8.4 でも 10.1 でもどちらでもいい、という結論になった。 どちらのスクリーンサイズも利点・欠点あり甲乙つけがたい。 8.4 インチに最適なゲームをプレイすることがメインであれば 8.4 インチがいいと思うが、10.1 インチの大画面も魅力的だ。 そして 8.4 インチが持ち運びに優れているように見えるが、結局のところズボンのポケットに入らずバッグに入れる必要があるので感覚的に 10.1 インチとそんなに変わらない。

私が「もう MediaPad M3 Lite 10 には戻れないな」と思ったのは画面サイズではなく前述の ppi だ。 MediaPad M3 Lite 10 は 10.1 インチで 1920x1200 (224 ppi) とタブレットとしては標準的な ppi なのだが MediaPad M5 の 8.4 インチ 2560x1600 (359 ppi) と見比べてしまうととても荒く見えてしまって辛い。 iPad や MacBook Pro で初めて Retina ディスプレイを見た時に近いような感覚がある。

そういえば今販売されている iPad ないし iPad Pro の ppi が 300 ppi に届いていない (264 ppi) のにも少しびっくりしたが iPad のディスプレイは十分美しいように感じた。 MediaPad M5 Pro の 10.8 インチ 2560x1600 (280 ppi) でも iPad より高精細なので十分なのだろう。

今日ようやく MediaPad M5 (8.4 インチ LTE) を受け取ることができた。 早速一通り設定し、軽くだがゲームもプレイしてみたところでファーストインプレッションを書き留めておくことにする。 尚、私の以前のタブレットは MediaPad M3 Lite 10 なのでそちらと比較する内容が含まれることに留意されたい。

プリインストールアプリが増えてしまっている

この MediaPad M5 は残念ながら以前のファーウェイの端末 (MediaPad M3 Lite 10 や Mate 9) に比べてプリインストールアプリが増えてしまっている。 具体的には Abema TV やウイルスバスターに Facebook, Instagram や「キッズモード」なるアプリも入っていた。 大体のアプリはアンインストールできるのだが Facebook App Manager というアプリが無効にできるだけでアンインストールできない。 正直気持ち悪いのでアンインストールさせてほしい。

ファーウェイ端末のいいところはプリインストールアプリが少なくファーウェイ製のアプリが Google の純正アプリかと思わせるほどのセンスのいいものが揃っていることだと思っていたのだが、少しそのアドバンテージが削られてしまった気がする。 1 回端末を初期化すれば綺麗になるのだろうか。

ゲームモードを実現する Game Suite

MediaPad M5 にはプリインストールアプリとして Game Suite が入っているのだが、このアプリを使うとゲームアプリと思われるものを選択してゲームモード (電池消費が激しいがパフォーマンスが向上する) で起動することができる。 私はあまり負荷の高いゲームはやらないので正直効果のほどはよく分からなかったが、ゲーム好きの方には刺さる機能だろう。 実際 MJ モバイルは MediaPad M3 Lite 10 だとモタツキ気味だったが MediaPad M5 だととても快適に動作する。

この Game Suite でいいなと思ったのはゲームモードに加えて以下の設定ができることだ:

ゲーム時の鳴動制限

有効にすると、通話、アラーム、電池残量低下の警告を除き、画面上のすべての通知が非表示になります。 また、通話やアラームに対しても鳴動しません。

スマホでリアルタイムに進行するゲーム (MJ モバイルなど) をやっていて突然電話がかかってきて中断させられて悔しい思いをしたことが何度かあった。 更に LINE の通知が重要な部分に被さってしまって見えないなどの不都合もあった。 この機能があればその心配はなくなるだろう。

ナビゲーションキーのロック

画面外ナビゲーションキーをロックすることで、ゲーム時の誤動作を防止できます。 操作を実行するには、キーを 3 回タップしてください。

ゲームに熱中する余りナビゲーションキーを誤って押してしまったことは確かに何度かあった。 私がプレイしているゲームは簡単に復帰できるのでそこまで必要性を感じないが、アプリが隠れてしまっては困る方にはとてもいい機能だと思った。

高解像度の恩恵はあまり感じない

MediaPad M3 Lite 10 が 10.1 インチで 1920x1200 だったのに対しこの端末は 8.4 インチで 2560x1600 という高解像度だが、使っていてそんなに違いを感じなかった。 Web ブラウジングや Kindle (漫画でなく活字) であれば多少は違って見えるが、ゲームや動画などは全く違いを感じない。 高解像度のせいで電池消費やパフォーマンスに影響が出てしまっているのだとしたら、このサイズの端末は 1920x1200 の方がいいのではないか。

パフォーマンスの差

MediaPad M3 Lite 10 との比較であるが、ゲーム (特に MJ モバイル) は動作が機敏になったし、タスクを切り替える時の処理が速い。 とはいっても感動するほどの差ではない。 よく言われていることだが YouTube 動画や Kindle を読む程度なら MediaPad M3 Lite で十分だと思う。

急速充電は便利

このタブレットは急速充電に対応しているのが便利だ。 Tronsmart のモバイルバッテリーを使うと Mate 9 でも急速充電できるのだが、試しに MediaPad M5 を繋いでみたら急速充電になった。 但し Mate 9 の純正 AC アダプタを繋いでも急速充電にはならない (Mate 9 は超急速充電になる)。

8.4 インチと 10.1 インチのサイズ感

今回 10.1 インチの端末から 8.4 インチの端末に変更して「ちょっと小さい」と思った。 これが面白いものでネット上の噂を見ると 8 インチでも大きすぎるから 7 インチのタブレットが欲しいという声も少なからずあるのを知っている。 このあたりは完全に好みの問題だろう。 勿論ゲームをプレイするにはこのサイズがベストだと思う。 10.1 インチだと指を動かす距離が長くて辛い時がある。 本当は 8.4 インチと 10.8 インチの MediaPad M5 を両方持って使い分ければ最高なのだが、そうするとどちらか使わなくなりそうなので致し方ないところだ。

決定的な対処法が今のところ存在しない様子

どうも数日前から一部のファーウェイ製スマートフォン・タブレットにおいて Uber のような Google Map を使用しているアプリで本来地図が表示される領域に「Google Play services are updating」という文言が表示されて地図が描画されないという不具合が出ているようだ。 一部のと書いたが、正常に動いているファーウェイ端末においても後述する「Google Play 開発者サービスのデータの削除」を行うと現象が再現する。

この問題は Google Play Help ForumIssue Tracker に上がっている。 今後問題が修正されることを期待したい。

尚、私が試した以下のような手法では現状ではどの方法をとっても端末の再起動を行うと元に戻ってしまう:

  • Google Play 開発者サービスのデータを削除
  • Google Play 開発者サービスをアンインストールし工場出荷状態に戻した上で更新
  • APKMirror を使用し過去のバージョンの Google Play 開発者サービスをインストールする (放っておくと結局 Google Play によって最新にされてしまいそのまま再起動すると同現象となる)

Google Play 開発者サービスのデータを削除

Google Play 開発者サービスのデータを削除

上記に紹介した対応方法の中で「Google Play 開発者サービスのデータを削除」が一番簡単なので、とりあえずバグが修正されるまではこの方法を使用して端末を再起動せずに使う形でお茶を濁すしかなさそうだ。 これは以下の方法で行うことができる:

  1. 端末の「設定」「アプリ」を開く
  2. 表示されているリストの中から Google Play 開発者サービスを選択 (表示されない場合はプルダウンメニューで「全てのアプリ: 有効」を選択)
  3. 「ストレージ」を押下するとメモリ使用量が表示されるので「容量を管理」を押下 (「キャッシュを消去」だと変化がないので注意)
  4. 「データをすべて消去」を押下しデータを削除する
  5. Uber のような Google Map を使用するアプリを開き Google Map が正しく表示されるのを確認する (再起動すると戻ってしまうので注意)

Google Play 開発者サービスが修正された (2018/05/19 追記)

この問題は 2018/05/19 に公開された Google Play 開発者サービス (バージョン 12.6.85) で修正された模様。 ただ私の環境だと再起動した直後は依然として Google Play services are updating が表示されるが、再起動してから 2, 3 分待ってから対象のアプリを開くと Google Map が正しく描画されるようになった。

Git for Windows で公開鍵必須になった?

今日新しい PC で Android Studio をセットアップしリモートの Git リポジトリに SSH 接続で git clone しようとしたら以下のエラーが表示された:

Could not read from remote repository.

これだけでは何だか分からない。 Android Studio 的には PATH に通っている git コマンドを発行しているだけなので Git for Windows 側が同様に失敗しているわけだ。 Windows PowerShell 上で同様に git clone してもエラーが表示されるが、今度はもう少し具体的なエラーが出た。

Permission denied (publickey)

これは何なのだろうか、ということで検索してみたら、どうも公開鍵が無いので接続できないということらしい。

公開鍵を生成しサーバに送信

Windows のユーザホームディレクトリは /Users/(ユーザ名) であり、その下の .ssh フォルダに移動する。 そこで公開鍵を生成する:

ssh-keygen -t rsa

出てくる質問は全部そのまま Enter でよい。 秘密鍵 (id_rsa) と公開鍵 (id_rsa.pub) が生成されるので、公開鍵の方を SSH 接続したいサーバに転送する:

scp id_rsa.pub (サーバ URL):~

最後にサーバ側 .ssh/authorized_keys に追記する形で公開鍵の登録完了する:

ssh (サーバ URL)
cat id_rsa.pub >> .ssh/authorized_keys

試しに ssh コマンドで接続し、パスワード認証無しで接続できることを確認する。

全 Kotlin 化されたコジごみカレンダー v3.0 公開に書いた通り 1 年半前にコジごみカレンダーを Java から Kotlin に全面刷新したのだが、コジ時計の方はずっと Android Studio でなく Eclipse ベースのコードのままで少し変更しようにもかなりの労力を傾けて修正作業をしなければならず、どうしても手が進まない状況になってしまっていた。 そこで今回コジ時計の方も少しずつ古いコードを見ながら Kotlin で移植するという作業を進めており、今日やっとアプリ内に表示している広告の承認が下りたので Google Play に公開することができた。

1 年半前と違って Kotlin を業務で使用するのにも慣れている状態になっているので、古い Java コードを移植すると機能によっては半分以下のステップ数になった。 Kotlin の記述力はとにかく強力で、もう Java に戻りたいとは全く思わないほどだ。

また、デザインのトレンドもマテリアルデザイン (フラットデザイン) に完全に移っているので、古いグラデーションを多用するようなデザインは排除し、マテリアルデザインを意識した「単色基調でのっぺりとしたデザイン」に調整した。

フェードインやフェードアウト

点滅の前に一番よくあるアニメーションであるフェードインやフェードアウトについて言及しておく。 Android 2.x 系の時は Animation クラスを使用した古いやり方をしていたと思うが Android 4.x 以上では ObjectAnimator を使う:

// ビューの alpha 値を 1 から 0 に変化させるアニメーション
ObjectAnimator.ofFloat(view, "alpha", 1f, 0f).apply {
    addListener(object : Animator.AnimatorListener {
        override fun onAnimationEnd(animation: Animator?) {
            view.visibility = View.GONE  // アニメーション終了時にビュー自体を非表示にする
        }
        override fun onAnimationCancel(animation: Animator?) = Unit
        override fun onAnimationStart(animation: Animator?) = Unit
        override fun onAnimationRepeat(animation: Animator?) = Unit
    })
    duration = 1000  // 1,000 ミリ秒で実行
    start()
}

この ObjectAnimator.ofFloat() を使うと任意の時間内に任意のプロパティを滑らかに変更させることができるので、ほとんどの場面はこれでいけるはずだ。 この例ではフェードアウトさせるアニメーションなのだが、フェードアウトさせた後にビュー自体の存在判定を消さないと当たり判定として残り続けてしまう (alpha = 0 では透明度が 0 になっているだけでタップした際のイベントは走ってしまう)。 その為アニメーション終了時に visibility に対し GONE をセットしている。 これ自体は Android 2.x 時代からの使い古された手法だ。

点滅

ビューを点滅 (blink) させるというのは marquee と共に古いホームページの技法のように思えるが、今回実装する機会があったのでメモ。 数百ミリ秒ごとに visibility を切り替えてもいいのだが、それだとチカチカとした感じの古いタイプの点滅になってしまう。 フェードイン・フェードアウトを繰り返すタイプの点滅にしたい。

ObjectAnimator に逆方向にリピートする為の設定があったので、これを使えば短いコード量で実現できた:

ObjectAnimator.ofFloat(view, "alpha", 1.0f, 0.0f).apply {
    repeatCount = ObjectAnimator.INFINITE  // 無限に繰り返す
    repeatMode = ObjectAnimator.REVERSE  // 逆方向に繰り返す
    duration = 500
    start()
}

repeatModeObjectAnimator.RESTART という定数も入れることができるが、こちらだとただ単に 1.0 から 0.0 へのアニメーションを繰り返すような動きになる。 ObjectAnimator.REVERSE にすると alpha に対し 1.0 から 0.0 へのアニメーションが終わった後 0.0 から 1.0 へ再度アニメーション、その後そのパターンが永遠に繰り返されることになる。

再度設定

今年 9 月に「かえうち」が届いた時に月配列 K を見据えて一通りの設定を行い Android タブレットで便利に利用していたわけだが、少し改良をしようと思い今日会社から持ち帰ってきて設定を行うことにした。 改めてかえうちの公式サイトを見てみると何やらいろいろとソフトが変わっている。 以前はかえうちの設定ファイルを Web アプリ上で行い、ファイルを吐き出してクライアントサイドのアプリ (かえうちライター) で書き込むという流れで、いろいろと罠があってよく調べないとハマるような感じだった。 しかし今日見てみると「かえうちカスタマイズ ソフトウェア版」というのができており、これを使うと設定のカスタマイズを行ってそのまま「書き込む」ボタンを押すと「カスタマイズモード」にしなくてもアプリが自動的にかえうちをカスタマイズモードにして書き込んでくれるという便利仕様になっていた。 これなら「設定→書き込み→試し打ち」という一連の流れがとてもスムーズにできる。 素晴らしい改良だと思った。

配列面切り替えと同時のマクロを IME 切り替えではなく入力モード切り替えにする

私は PC で月配列 K を使う時にキーボードのスペースキーの両隣にある変換キーと無変換キーをそれぞれ「入力モードかな」「入力モード英数」キーとして設定している。 Mac の JIS キーボードではスペースキーの隣に英数キーとかなキーがあるのだが、これを押下するとそれぞれ入力モードを「英数」「かな」に切り替えられるという挙動を踏襲している。 この使い心地を Android 端末でも実現できれば最高なのだが、残念ながら Android の場合入力モードを「英数」「かな」にするような独立したキーはない。 ただトグルであれば行う方法はある。 トグルだと間違えて 2 回押してしまった場合はかえうちの認識とシステムの IME 状態がズレてしまうのだが、それはもう仕方がない。

以前は IME の切り替え (例えば Google 日本語入力と Gboard のトグル) でこれを実現していた。 Android 端末の場合 Win + Space でこれを行うことができる。 ただ、これは想定通り動作はするのだが IME の切り替えに 1, 2 秒かかりその間に入力を始めてしまうと意図しない文字が入力されてしまい快適ではない。 あとから気づいたのだが 半角/全角 で Google 日本語入力のままで入力モードを英語と日本語に切り替えることができる。 なので変換キーと無変換キーを押下した際のマクロに 半角/全角 を含めることで対応できた。

ちなみに Android 端末の方に繋いでいる物理キーボードが英語配列であると認識されていると正しく 半角/全角が発行されず、英語キーボードの「`」が発行されてしまう。 Android 端末側の物理キーボード配列の定義はかえうちの方の設定ではなく「設定→詳細設定→言語と文字入力」の「物理キーボード」で Google 日本語入力をインストールしている場合は「Google 日本語入力」を押下するとキーボードレイアウトを選択することができる。 ここで英語配列のものではなく「日本語 109A 」などの日本語配列を選択する。

Android で月配列 K でイータイピングができるか

表題の通り Android 端末でイータイピングをすることができるかという話なのだが、実はローマ字や英語では普通にイータイピングをプレイすることができる。 これは Bluetooth キーボードなどを使用してもいいし、とても打ちにくいがイータイピングで表示されるキーボード上で打ってもいい。 うまくマッピングすれば月配列 K を使えるのではないか……と思い試してみたが駄目だった。 かえうちを使わずに普通に JIS かなでイータイピングのかなをプレイしてもうまくいかないのだから出来るわけがない。 濁点、半濁点や「む」などのキーを押下した時に発行されるキーコードが異なるのだろう。 このあたりがよく分からない。

昔対応した問題だったのだが、最近再度同じようなアプリを作成した時に忘れていてハマったので備忘を残しておく。

Google Maps Android API v2 で長時間同じ地図画面で待ち受けるようなアプリを作成する場合、地図を長時間触っていると StackOverflowError で落ちる場合がある。 これが地図を拡大したりちょっとスクロールした際に突然落ちるのでプロダクションでこれが起きてしまうと「このアプリは地図をちょっと触っているだけで落ちる」という散々な評価をいただくことになるし、長時間触っていないと再現しないという関係上開発時に気づかないことが多い。 例えば以下のようなスタックトレースが吐かれる:

java.lang.StackOverflowError: at com.google.maps.api.android.lib6.gmm6.util.e.b(:com.google.android.gms.DynamiteModulesB@11746036:22)
at com.google.maps.api.android.lib6.gmm6.store.cache.s.a(:com.google.android.gms.DynamiteModulesB@11746036:8)
at com.google.maps.api.android.lib6.gmm6.store.n.a(:com.google.android.gms.DynamiteModulesB@11746036:49)
at com.google.maps.api.android.lib6.gmm6.indoor.o.a(:com.google.android.gms.DynamiteModulesB@11746036:80)
at com.google.maps.api.android.lib6.gmm6.indoor.o.a(:com.google.android.gms.DynamiteModulesB@11746036:70)
...

これは Google Maps Android API v2 の「インドア表示」で起こる。 ほとんどの要件ではインドア表示など不要だと思われるので、以下のように設定で OFF にすれば起きなくなるので地図を使用する画面では機械的に以下を入れてしまっていいのではないかと思う:

mapFragment.getMapAsync { it.isIndoorEnabled = false }  // インドア表示 OFF
mapFragment.getMapAsync { it.uiSettings.isTiltGesturesEnabled = false }  // チルト操作 (傾けて地図を 3D 表示) OFF

2 番目の「チルト操作 OFF」も可能であれば設定した方がいい。 確かこれも ON にしていると OutOfMemoryError で落ちることがあった気がした (確証とれず)。

ちなみに OutOfMemoryError で落ちる件に関しては Stack Overflow に書かれている通り AndroidManifest.xmllargeHeap = true を宣言するなり適切なタイミングで System.gc()GoogleMap.clear() を呼ぶ必要が出てくるだろう。

先日書いた通り 10 インチタブレットである MediaPad M3 Lite 10 のカバーを風呂蓋型のものから普通のシリコン型に変えた。 やはりこちらの方が電車内での取り扱い時に身軽で便利だ。 ただ、風呂蓋型のカバーは蓋を三角に折りたたんでスタンドとして使えるという利点があった。 そこでシリコン型のカバーであったとしても外付けのタブレット用のスタンドを使用して立てて使用することにした。

Anker コンパクトマルチアングルスタンドを購入。 このスタンドは何段階かの角度調整をすることができる。 シンプルな作りながら機能的に必要十分で全く問題ない。 むしろ風呂蓋型のカバーに対して明らかに勝っていると思った部分は横だけでなく縦向きでスタンドに立てて使うことができることだ。 これは将棋ウォーズやドラクエなどの縦画面固定のアプリをスタンドに立てて遊びたい時に重宝しそうだ。

自宅と会社で持ち運んで使ってもいいのだが、価格も安いので 2 台持ちしてそれぞれに常備してもいいだろう。 次の楽天でもう 1 台買い足すつもりだ。

妻が Amazon お急ぎ便で HUAWEI のスマートフォンである P10 lite を注文し、昨日の夕方に届いた。 妻は Android は使ったことがあるとはいえ長らく iOS だったということもあり「初期設定だけして欲しい」ということでその手伝いをしていた。 システムのフォントサイズを調整したり、ランチャーをドロワー型にしたり、スリープ時間を調整したり、Google 日本語入力をインストールして使いやすく設定したり、といった自分がよく行っていることを同様に行った。

P10 lite を触ってみた感想としては「事前の想像とほぼ違わなかった」といったところだ。 ミドルレンジモデルに関しては会社で何度か触っているのもあって同様なのだろうなと思っていたが、やはり想像通り所々もたつく場面はあるがまあ普通に使えるなといった印象だった。 大抵の人は今のハイエンドモデル (P10 や Mate 9) を触らずにこれだけ使っていればそう不満は感じないだろう。 実際 Antutu ベンチマークのスコアから言っても 2, 3 年前のハイエンドモデルと遜色なく、そう考えるとコストパフォーマンスは異常に良い。

一点だけ想定外だったのは端子が USB Type-C でなく未だに microUSB だったことだ。 自分としては USB Type-C の方が前後区別なく挿せるし端子の統一といった意味でも有利なので気になった。

風呂蓋型カバーが故障

今私は MediaPad M3 Lite 10 という 10 インチタブレットを便利に使用している。 そのタブレットでは AVIDET 製 (中国?) の iPad でよくある風呂蓋型のカバーを使用していたのだが、つい先日蓋を留める磁石を固定しているプラスチックの部分が割れて磁石が飛び出してしまい、しかも磁石をそのまま紛失してしまった。 2 カ月くらいしか保たなかったことになる。 1,000 円強しかしないカバーだったのでそこまで悲観はしていないのだが、これを機に風呂蓋型でなく普通のシリコン製の透明で覆うだけのタイプのものに変更しようと考えた。 10 インチタブレットは結構重量もあるのだが、それでも机の上に置いて使うような場面はそう多くない。 それに電車で手に持って使っていると、風呂蓋型だとどうしてもフタの部分が後ろに垂れ下がっている状態であまりスマートではない。

シリコン製のカバーだと更に安く 1,000 円以下で余裕で購入できてしまう。 メーカーはまた AVIDET 製とした。 よく分からないメーカーだが安いのはありがたい。

タブレットスタンド

机の上で使うシーンは多くはないとはいえ、それでもちょっと立てかけて使えると便利だ。 今はいろいろなメーカーからタブレット用のスタンドが出ているのでその中からチョイス。 安心の Anker 製のものを注文。 100 g 程度なので常にカバンに忍ばせておくのもアリだろうか。

届いたらまずは試しに使ってみて、使い勝手がよかったらもう 1 台買って会社に据え置くと便利そうだ。

タブレットは 10.1 インチか 8.4 インチか

今私は HUAWEI のタブレットである MediaPad M3 Lite 10 という 10 インチタブレットを使っているのだが、概ね満足であり感想は以前レビューで書いた通りなのだが最近もう少し不満点が出てきた。 私は同様に HUAWEI のハイエンド大型スマートフォンである Mate 9 を使用しているのだが、それより明らかに Wi-Fi の掴みが悪いのである。 私の自室は Wi-Fi ルータから結構距離が離れているのだが、そこだと Mate 9 はほぼ確実に電波を掴めるのに対し M3 Lite 10 の方は一旦スリープに入れて復帰した時など掴めないことが多く Wi-Fi の ON/OFF を繰り返しても結局接続できないことがあり諦めて Mate 9 のテザリングを入れる (Mate 9 側の LTE の残量が結構あるので) などしてしまっている。 多分これは 8.4 インチのタブレットである MediaPad M3 の方を使用すればこちらは廉価版という位置付けではないので Mate 9 と同様に Wi-Fi 電波を掴むのだろう (未検証)。 仕方がないのかもしれないが、こういう風に見えないが必須の部分で差を付けてくるところが少し残念に思った。 HUAWEI が 10.1 インチの方の MediaPad M3 にあたる上位版を出してくれればいいのだが、需要の関係上望みは薄いだろう。

私は以前 Nexus 7 を使用していたことがあり職場にもあるので今でも触ることがあるのだが、私の感覚で恐縮であるが明らかにタブレットとして常用するには苦痛を感じるほど画面が小さい。 今時のスクリーンが大型化したファブレットと比べると「一回り大型化したファブレット」といった印象しか受けず、これならスマートフォン 1 台のみでいいのではと思ってしまう。 そして、私は以前 Nexus 9 という 8.9 インチのタブレットを使用していたが、これは世の中の人からはいろいろと酷評されたようだが自分としてはサイズを含めて割と満足に使用していた。 何より 4:3 の画面比率が Kindle や PDF を読むのに都合が良かった。 ただ PDF を読むのには画面は大きければ大きいほど良い。 10.1 インチタブレットでも「少し文字が小さいかな」と思ってしまう場面があるほどなので 8.4 インチタブレットだと横画面にしてスクロールしないと厳しいだろう。

常々「大は小を兼ねる」と思っているので迷わず 10.1 インチにしたのだが、今後 HUAWEI が M3 と同様に 8.4 インチでのみハイエンド版を出すのであれば次 (M4) はそちらにしようと思った。 よくよく考えるとファブレットである Mate 9 で明らかに守備範囲外なのが PDF 閲覧、次点 Kindle であと動画鑑賞とゲーム、ブラウジングが続くぐらいだが別に 8.4 インチであったとしても少し快適でなくなるのが PDF 閲覧くらいで Kindle 以降は問題ないと思われる。

そもそもスクリーンサイズが一回り小さいと思っていた Nexus 9 (8.9 インチ) より 0.5 インチ下がるのがどうなのか、Nexus 7 より「1.4 インチしか大きくなっていない」のがどうなのかと思っているのだが、ここはスクリーンサイズを調べてみることにする。 こういうのはディスプレイの実寸法を計算するサイトがあるのでそこを利用する。

端末名 画面サイズ (inch) 解像度 横 (mm) 縦 (mm) ppi (参考)
Mate 9 5.9 1920x1080 130.56 73.44 373.374
Nexus 7 (2013) 7.0 1920x1200 151.68 94.8 323.451
MediaPad M3 8.4 2560x1600 181.76 113.6 359.39
Nexus 9 8.9 2048x1536 180.224 135.168 287.64
iPad 2017 9.7 2048x1536 196.608 147.456 263.918
MediaPad M3 Lite 10 10.1 1920x1200 216.96 135.6 224.174

こう見てみるとやはり Nexus 9 の画面比率 4:3 が効いていて縦 (短辺) の長さが 10 インチタブレットと同じくらいの長さだが横 (長辺) の長さが 8.4 インチタブレットと同じくらいという結果になった。 MediaPad M3 は Nexus 9 よりも縦が 2.15 cm くらい短い。 これがちょっと気になるところな気がするのだがどうなのだろうか……。 ただ Nexus 7 よりは 1.88 cm ほど長い。 Nexus 7 は MediaPad M3 Lite 10 の中間くらいでちょっと Nexus 7 寄り。 ……表にすれば分かりやすいかと思ったが却って迷う結果になってしまった。 やはり使ってみないと分からない。

MediaPad M4 がどうなるのか分からないが、少しサイズが大きくなってくれればありがたいと思うばかりだ。 逆にサイズが小さくなってしまった場合は更に困ってしまうことになるが……。

ファブレットやタブレット 1 台持ちよりスマートフォン + タブレット

以前私は「タブレットとスマートフォンを別々に持つよりもファブレットを 1 台だけ持って使ったほうが便利なのでは」と思って Mate 9 のみを使用していた時があった。 人によってはそれが最適解なのだろうし、大きい端末で通話することが気にならないのであれば MediaPad M3 にも通話アプリが付いているのでタブレットのみの運用というのも可能ではある。 だが、私はバイクのナビとしても使用しているのでタブレットのみという選択肢はあり得ない。 タブレットをバイクにマウントする製品も無いことはないが、やはり脱落の危険性が大幅に増えるので常用するには厳しい。 ナビに使用するのにはそんなに大きいスクリーンサイズは必要としないというのもある。 4.5 インチ程度でも十分だ。

ファブレットでも明らかに使う気になれないのは前述の通り PDF と Kindle だ。 ここが必要ない方はスマートフォンのみでも良いのだろう。 ゲームに関してはファブレットでも十分快適であり、私が今使っている MediaPad M3 Lite 10 だと逆に大きすぎて指の可動範囲が広いのがそんなに快適でないように感じる。 そう考えるとやはり 8.4 インチはなかなか考えられたサイズなのかもしれない。 難しい。

ともかく、そういうわけで次買い換えるとしたら出るであろう MediaPad M4 と Mate 9 より少し小さいスマートフォン、できれば防水端末が良い。 ナビとして使う場合に防水が付いていると安心感がまるで違う。

イヤホンジャックが消える日が来るのだろうか

昨今の iPhone では極端な薄型化の為かイヤホンジャックさえも廃止されている。 これは賛否両論あるところだが、もし HUAWEI の端末もこの時流に乗ってきたとしたら私もまた Bluetooth イヤホンに戻すことを考えなくてはならなくなる。 Bluetooth イヤホンは確かにコードが絡まりにくくなるのは長所なのだが、それよりも電池の心配をしなければならないのとペアリングされた複数のデバイスを切り替える時に失敗する場合があるのが気になるところではある。 イヤホンを使うシーンというのは要するに音を漏らすと迷惑である場合がほとんどだと思われるので、例えば電車内などで切り替えたと思ったつもりだったのが切り替わってなくて音を出してしまった……という失敗も何度かあった。 あと「まだ大丈夫だろう」と思って使っていたら突然電池が切れて同様に音を漏らしてしまったケースも何度もあった。 Apple の AirPods は使ったことがないが、値段が高すぎるのでなかなか厳しいところがある。 そもそも iPhone ではないので連携部分の優位性がないので微妙だろうか。

面倒なのでできれば相手をしたくないが

さて、前回Service のプロセスがアプリと同一の場合、つまり通常の場合に Binder を用いて双方向でやり取りする方法に関して考察したが、今回は止むを得ない事情でプロセスを分けなければならなくなった場合に Messenger を用いてプロセス間通信を行う為の実装例に関して記載する。

Service 側の実装に関してコード例を示す:

companion object {

    /** クライアント側からの replyTo 受信. */
    val MESSAGE_RECEIVING_REPLY_TO = 1

    /** クライアントへのメッセージ送信. */
    val MESSAGE_SEND = 2
}

/** サービス側の処理を行うハンドラ. */
private class MyHandler : Handler() {

    /** クライアントの Messenger. */
    var clientMessenger: Messenger? = null

    override fun handleMessage(msg: Message?) {
        when (msg?.what) {
            MESSAGE_RECEIVING_REPLY_TO -> clientMessenger = msg.replyTo
            else -> super.handleMessage(msg)
        }
    }
}

/** ハンドラ. */
private val mHandler = MyHandler()

/** サービス側のメッセンジャ. */
private val mMessenger = Messenger(mHandler)

override fun onBind(intent: Intent?): IBinder = mMessenger.binder

override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
    // サービス実行から 5 秒後にコールバック実行
    Handler().postDelayed({
        val bundle = Bundle().apply { putString("message", "ほげふが") }
        mHandler.clientMessenger?.send(Message.obtain(null, MESSAGE_SEND, bundle))
    }, 5000)
    return Service.START_STICKY_COMPATIBILITY
}

こちらでもプロセスが同一の場合と同様に onBind()IBinder を返却しているが、こちらは Messenger に対応する Binder である (後で Messenger に復元可能な Binder) ところが異なる。 mMessenger というフィールドを持っているが、これは Handler インスタンスを Messenger クラスでラップしたものである。 Handler クラスの方を拡張することによって相手から投げられたメッセージをどう処理するかを実装する。

上記の実装例では msg.replyTo をフィールドに持っているが、これは Service からみてどのクライアントの Messenger でメッセージを送信すれば良いのかを判別するためのものである。 つまり Service 側の任意のタイミングでクライアント側にメッセージを送信したい場合は以下の手順を踏む必要がある:

  1. Service の接続確立時にクライアント側から replyTo を教えるためのメッセージを Service 側の Messenger を使って送信
  2. Service 側の送信したいタイミングでフィールドに持っている replyTo を使用してクライアントに送信

上記コードでは onStartCommand() 内で 5 秒後に実際にクライアントに対してメッセージを送信している。

Activity / Fragment 側のコード

/** クライアント側の処理を行うハンドラ. */
private class MyHandler(val activity: MainActivity) : Handler() {
    override fun handleMessage(msg: Message?) {
        when (msg?.what) {
            MyService.MESSAGE_SEND -> activity.button.text = (msg.obj as Bundle).getString("message")
            else -> Log.w(javaClass.simpleName, "Unknown what: ${msg?.what}")
        }
    }
}

/** サービスとの bind 時の接続を行う. */
private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mServiceMessenger = Messenger(binder)

        // サービス側に replyTo を介してクライアント側の Messenger を教える
        val message = Message.obtain(null, MyService.MESSAGE_RECEIVING_REPLY_TO)
        message.replyTo = mClientMessenger
        mServiceMessenger?.send(message)
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        mServiceMessenger = null
    }
}

/** サービス側の Messenger. */
private var mServiceMessenger: Messenger? = null

/** クライアント側の Messenger. */
private val mClientMessenger = Messenger(MyHandler(this))

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // サービスを bind する
    val intent = Intent(applicationContext, MyService::class.java)
    startService(intent)
    bindService(intent, mConnection, Context.BIND_AUTO_CREATE)
}

override fun onDestroy() {
    unbindService(mConnection)
    val intent = Intent(applicationContext, MyService::class.java)
    stopService(intent)
    super.onDestroy()
}

Service との接続が確立した時に渡ってきた IBinder を逆に Messenger のコンストラクタに渡すことで Service 側の Messenger を復元している。 それを使用することで Service 側にメッセージを送信することができる。 先ほど記載したように、メッセージの replyToActivity, Fragment 側の Messenger を詰めて送信しておく。

後は Service 側と同様にこちら側でも Handler クラスを拡張して Service から渡ってきたメッセージを処理する為のコードを書く。

まとめ

  1. ServicebindService() する
  2. Service とのコネクション確立時に ServiceMessenger を使用して Service に対しクライアント側の MessengerreplyTo に詰めて送信することで教える
  3. Service 側で Handler の拡張クラスの実装によって渡ってきた replyTo を格納しておく
  4. replyTo を使用して任意のタイミングでクライアントに対しメッセージ送信
  5. クライアント側の Handler の拡張クラスでの実装で UI の表示処理などを行う

手順をしっかり理解しないと間違えそうな内容なので、やはり手軽ではないと思う。

プロセスが同一の場合の例

大抵の場合アプリケーション内に Service を置く場合は必要に迫られない限りアプリケーションと同一プロセスとすると思われる。 この場合 Fragment 側から Service を操作したい場合は bindService を使用し Binder 経由で操作を行うのが一般的だ。

あまり意味のない例で恐縮であるが、例えば以下のような「現在時刻のタイムスタンプを返すサービス」を実装したものとする:

/**
 * 現在時刻を返すサービス.
 */
class MyService : Service() {

    inner class MyBinder : Binder() {
        fun getTime() = Date().time
    }

    /** Binder インスタンス. */
    private val mBinder = MyBinder()

    override fun onBind(intent: Intent?): IBinder {
        Log.d(javaClass.simpleName, "onBind start.")
        return mBinder
    }

    override fun onUnbind(intent: Intent?): Boolean {
        Log.d(javaClass.simpleName, "onUnbind start.")
        return super.onUnbind(intent)
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d(javaClass.simpleName, "onStartCommand start.")

        // TODO 本当は何か UI に関係ない部分で待機するような処理が入るはず

        return super.onStartCommand(intent, flags, startId)
    }

    override fun onCreate() {
        super.onCreate()
        Log.d(javaClass.simpleName, "onCreate start.")
    }

    override fun onDestroy() {
        Log.d(javaClass.simpleName, "onDestroy start.")
        super.onDestroy()
    }
}

上記の場合 onBind() でサービス内で定義されている MyBinder を返却しているが、クライアント (Fragment) 側からこのインスタンスを使用してサービスの機能を利用する。 よって、例えば「ボタンを押したらサービスからデータを取ってきて画面に表示する」といった実装を行う場合は以下のようなコードになる:

/** サービスとの bind 時の接続を行う. */
private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mBinder = binder as MyService.MyBinder
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        mBinder = null
    }
}

/** Binder. */
private var mBinder: MyService.MyBinder? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // サービスを bind する
    val intent = Intent(applicationContext, MyService::class.java)
    bindService(intent, mConnection, Context.BIND_AUTO_CREATE)

    // ボタン押下でサービスからデータ取得し表示する
    button.setOnClickListener { text.text = mBinder?.getTime()?.toString() }
}

override fun onDestroy() {
    unbindService(mConnection)
    super.onDestroy()
}

この例だと bindService() しかしていないので Service 側としては onCreate()onBind() は呼ばれるが onStartCommand() はコールされないことに注意する。 onStartCommand() はあくまで startService() した時に呼ばれる処理となっている。

ServiceConnection.onServiceConnected() 内で Service 側で定義したクラスである MyService.MyBinder にキャストしているが、これが可能なためにこの MyService.MyBinder に対し自由にメソッドやフィールドを追加することにより Service に対して好きに処理を行うことができるようになる。

Service から任意のタイミングでクライアントに処理を行わせたい場合

上記の Binder の例はあくまでクライアント側から任意のタイミングで Service に対し処理を行わせたい場合に使用できる。 逆方向である Service からクライアントに対し処理を行わせる場合であるが、以下の 3 通りが考えられる:

  • Binder を介してクライアント側からコールバックを渡す
  • EventBusRxJava などのライブラリを使用して任意のイベントを飛ばす
  • BroadcastReceiver をクライアント側に実装して Service からの sendBroadcast() を受け取る

今は EventBus などのライブラリを使用すると任意の位置からイベントを伝搬できるのでとても便利だ。 ここでは 1 番目の Binder を用いて実現する方法に関して書く。

例えばサービスが起動された 5 秒後にクライアント側で実装されたコールバックを実行する例を示す。 サービス側の実装として、まずクライアント側で実装すべきコールバックのインターフェースを開示する:

class MyService : Service() {

    /** サービスで 5 秒後に実行されるコールバック. */
    interface Callback {
        fun doIt()
    }

    inner class MyBinder : Binder() {
        fun getTime() = Date().time

        // クライアント側からコールバックを渡す為のメソッドを追加
        fun set(callback: Callback?) {
            mCallback = callback
        }
    }

    /** Binder インスタンス. */
    private val mBinder = MyBinder()

    /** クライアント側で実装されたコールバック */
    private var mCallback: Callback? = null

    override fun onBind(intent: Intent?): IBinder = mBinder

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        // サービス実行から 5 秒後にコールバック実行
        Handler().postDelayed({
            mCallback?.doIt()
        }, 5000)
        return Service.START_STICKY_COMPATIBILITY
    }
}

後は以下のようにクライアント側でコールバックの実装を行い、サービスのコネクションが張れたタイミングでコールバックを Binder を通してサービス側に渡してやれば良い:

private val mCallback = object : Callback {
    override fun doIt() {
        button.text = "サービス側から呼ばれた"
    }
}

/** サービスとの bind 時の接続を行う. */
private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mBinder = binder as MyService.MyBinder
        mBinder?.set(mCallback)  // サービス側に定義したコールバックを渡す
    }

    override fun onServiceDisconnected(name: ComponentName?) {
        mBinder?.set(null)
        mBinder = null
    }
}

/** Binder. */
private var mBinder: MyService.MyBinder? = null

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // サービスを bind する
    val intent = Intent(applicationContext, MyService::class.java)
    startService(intent)
    bindService(intent, mConnection, Context.BIND_AUTO_CREATE)

    // ボタン押下でサービスからデータ取得し表示する
    button.setOnClickListener { text.text = mBinder?.getTime()?.toString() }
}

override fun onDestroy() {
    unbindService(mConnection)
    val intent = Intent(applicationContext, MyService::class.java)
    stopService(intent)
    super.onDestroy()
}

これで 5 秒後にボタンに「サービス側から呼ばれた」という文字列が表示される。

プロセスを分けるとどうなるか

さて Android の場合同一アプリ内であっても別のプロセスを使用した Service を定義することができる。 これは AndroidManitest.xml に以下のように定義を加えることにより実現できる:

<service
    android:name=".MyService"
    android:process=":remote"/>

android:process=":remote" の部分であるが、:remote の箇所は別に :hoge などの他の名前でも良い。 このようにして先ほどのコードを実行してみると ServiceConnection.onServiceConnected()MyService.MyBinder にキャストしている箇所で ClassCastException がスローされるようになる:

private val mConnection = object : ServiceConnection {
    override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
        mBinder = binder as MyService.MyBinder  // 駄目. binder は BinderProxy が渡ってくる!!
    }
}

デバッグ実行で見てみると分かるが Service 側で渡した Binder の参照がそのまま渡ってくるのではなく BinderProxy というプロキシクラスのインスタンスが渡ってくるようになる。 その為このようにキャスト前提の実装にしてしまうと失敗する。 また EventBus などのライブラリもプロセス間通信には使用することができない。

この場合は公式にある通り Messenger を使用するわけだが、長くなったのでそのコード例は次回とする。

さて、本日で MediaPad M3 Lite 10 を購入して 2 週間ほど経過した。 やはり継続して使ってくると使い始め当初とはまた違った視点で見えてきたこともあった。 大体のことはファーストインプレッションの方に書いてしまったのでもし MediaPad M3 Lite 10 の情報を探しておられるのであればそちらも参照いただきたいのだが、ここには追加で感じたことを書く。

指紋認証はやはり便利

Nexus 9 まではロック解除時に毎回パターンを入力していたのだが、この端末は指紋認証を使うことができるし認証精度もとても良いので便利だ。 尚、スマートフォンは持つスタイルが大体決まっているので 1 つの指 (多くの方は右手人差し指) のみを登録すればいいのだが、タブレットの場合いろいろな持ち方をして使うので直感的に指を伸ばすとその指が指紋認証登録されていなかったりしてもどかしさを感じる。 多少面倒でも最初に複数の指 (左手と右手の親指・人差し指・中指) を登録してしまったほうが良さそうだ。

解像度 1920x1200 は別に問題ない

この端末の解像度は 1920x1200 であり、最近の 7 ~ 8 インチタブレットはおろか高解像度スマートフォンよりも解像度が低いのだが、確かにスマートフォンを持つ距離で見るとドットがはっきり見えるのだが正直実運用ではこの解像度で十分である。 解像度が低いことで消費電力が少なくなりバッテリーの持ちが良くなっていると思うので悪いことばかりではない。

ただ、この端末の下位モデルである MediaPad T3 10 の 1280x800 だと明らかに画質が荒い。 個人的には不満なく Web ブラウジング、Kindle や写真、動画を楽しみたいならば 1920x1200 が最低ラインだと思う。

電池持ちは異常にいいが残量表示は若干眉唾

この端末の電池持ちは異常に良く、体感として同じく電池持ちが良い Mate 9 と比べても遥かに良いと感じる。 ちなみに私が使用しているのは LTE モデルなので条件的には同じはずである。

ただこの端末の電池残量表示に関してはどうも信用出来ないところがあり 30 分ほど使用しても残量が 100% のままだったりすることがある。 先ほど電車内で 1 時間ほど使っても残量 98% だったが流石にそれもおかしい気がする。 車のガソリン残量表示みたいに、残量が少なくなってきたら表示上の減りが速くなるという感じだろうか。

イヤホンジャックの位置が最悪

これがこの端末で一番辛いと思った点だった。 この端末は多くはカバーを付けて立てて使うであろう大型のタブレットなのにも関わらず、イヤホンジャックが長い方の側面に付いている。 これだと風呂フタ型のカバーを付けてテーブルの上に乗せて浅く横方向で使おうとした時にイヤホンジャックが下に来てしまい、そのままイヤホンを刺そうとするととても不格好な事になってしまう。 また、縦で持って使おうとした時もイヤホンジャックがちょうど手に持ちたい位置にある為イヤホンの端子が邪魔で快適に持つことが出来ない。

ちなみに iPad はどうか、と思い調べてみたが、やはり短い方の側面に付いていた。 兄弟機の MediaPad M3 の方の位置も調べてみたが、こちらも短い方の側面に付いていた。 絶対にこちらの方が良い。 MediaPad M3 Lite 10 の位置は設計ミスレベルだと思うし、何故 MediaPad M3 で出来ていたことが出来ないのか……。

充電端子も今時 Micro USB

MacBook から始まった USB Type-C であるが Mate 9 などの最近のスマートフォンやタブレットでは旧来の Micro USB ではなく USB Type-C が増えてきている。 しかしながらこの端末は Micro USB である。 これだと Mate 9 と一緒に持ち歩く場合 Micro USB と USB Type-C のケーブルを両方持っていかなければならない。 更に USB Type-C の方が給電速度が速いので Micro USB 且つバッテリー容量が多いこの端末の場合満充電まで時間がかかる。 ただ、前述の通りこの端末はそうそう電池が減らないのでその点ではあまり困らない。

カバーは 1,000 円近辺のもので十分

これは本体のレビューとは違うのだが、今はタブレットのカバーは Amazon などで 1,000 円近辺でいくつも出ている。 その中の 1 つを購入して使っているが品質上全く問題ない。 風呂ブタを閉じればちゃんとスリープ状態になるし便利。 1,000 円ほどの投資で綺麗に使えて処分するときも高く買い取ってもらえるのだから積極的に活用するのがいいだろう。

議事録作成

私はよく会議で議事録を作成しているのだが、ノート PC を持ってくるにしても結構重量があって負担があるので Android タブレットに外部キーボードを繋いで同じように使えないかと考えた。 Happy Hacking Keyboard Professional JP は会社に置いてあるので micro USB - USB 変換アダプタさえあればいけそうだ。 変換アダプタは Amazon で格安で手に入れることができた。

少々使い勝手は慣れが必要だが悪くない

メモ帳としては普段使用している Google Keep を使用した。 ローマ字打ちしか選択できないが最近はなるべく打つようにしているのでそんなに詰まらずに打てる。 各種ショートカット (F7 ~ F10 や Ctrl + A, V など) も Windows と共通になっているようで同じ感覚でいけそうだ。

1 つ、入力メソッドの切り替えに関しては半角 / かなキーを押すか Win + スペースでないといけなかった。 私は入力メソッドの切り替えを多用する打ち方をしているので慣れが必要である。 今手元に英語キーボードが無いので試せないのだが英語キーボードの場合は Win + スペースのみだろうか。

会社で打つ場合はこれでもいけそうだが、旅行・ツーリング先で打つ場合にわざわざ比較的コンパクトとはいえそれなりに重量もある HHKB を持っていくのはどう見てもスマートではない。 また、アダプタで接続できるとはいえキーボードの線が伸びているのも不格好だ。 今後手元に届くであろう「かえうち」が使えないという難点はあるが、それと比較してもコンパクトで安価な Bluetooth キーボードがあれば最高な気がする。 ちょっと検討してみようかと思う。

早速購入した HUAWEI MediaPad M3 Lite 10 をデバッグで使用してみた。 やはり画面が広い端末だと机に置いた状態でとても見やすくていい感じだ。 ただ最初の状態では Mate 9 と同様に Logcat にアプリ側で出力しているデバッグログ (例えば Log.d("HOGE", "FUGA")) が出力されない。 幸いにも Mate 9 と同様の対処方法で出力することができた。 Ascend Mate 7 も同様の方法のようであるし、多くの HUAWEI 端末は全てこの方法でいけるのではないかと思われる。

念の為ここにその方法を書き残しておく:

  1. 端末の電話アプリを起動
  2. *#*#2846579#*#* とダイヤルする
  3. 何やら黒い画面が開くので「1. Background Settings」を選択
  4. 「3. LOG Settings」を押下
  5. 「AP Log」「Charge Log」「Sleep Log」にチェックを入れる (AP Log だけでもよい?)

何と 10 インチタブレットである MediaPad M3 Lite 10 には電話アプリが入っているのである。 ということは恐らく音声通話 SIM を挿せばちゃんと携帯電話として使用できるということなのだろう。

ちなみに上記の方法で設定しても再起動をした時など設定が解除されている時がある。 その時は面倒だがまた同様の手順でデバッグログを ON にする。 何度もやっていると*#*#2846579#*#* も記憶してしまうので今になってみるとそれほど不都合はない。 開発者でない多くの人に対してはわずかとはいえ電力消費をするであろうデバッグログは無用の長物であるので、初期状態で出力が OFF になっているという HUAWEI 端末の配慮は悪くないと思う。

今日注文していた MediaPad M3 Lite 10 が届いた。 ショップの発送がとても早かった。 本来は数日使ってからインプレを書くわけであるが Nexus 9 LTE でも書いた通り私はタブレット端末に関してはこれまで何台も使ってきているので数時間触れば何となく長所・短所が見えてきたのでファーストインプレッションとして書き留めておくことにする。

10 インチ Android タブレットにしては軽い

最近ではこの程度は当たり前なのかもしれないが 10 インチで 460 g と iPad 2017 年モデル並である。 最初に持った時「軽い」と感じた。以前使っていた 10 インチタブレットは 600 g 超えだったのでそれに比べると随分軽く感じる。 勿論人によっては「やっぱりちょっと重い」と思うかもしれないが、正直技術の進歩に驚かされた。

スペックの低さはあまり気にならない

この端末は発売時期が最近なのに対し CPU が非力だと言われているが、確かに Mate 9 のようなハイエンドモデルと比べるとモタツキを感じるものの不快に思うほどの遅さは感じず必要十分だと思った。 MJ モバイルなどプレイしてみたが特に問題は感じられなかった。 勿論 YouTube 視聴や Kindle など全く問題ない。 少なくとも Nexus 9 よりはキビキビと動作する。

唯一みんゴルは元々処理が重いせいか少しコマ落ちのような形になりプレイし辛さは感じた。 ゲームを主目的にする場合は注意が必要だろう。

プリインストールアプリが少ないのがいい

HUAWEI の端末はどれもプリインストールされているアプリが少ない上簡単にアンインストールすることができるのが好印象なのだが、この端末でもしっかりそれが踏襲されていた。 また HUAWEI 独自のランチャーである EMUI は相変わらず Android デフォルトのようなシンプルな見た目で良い。

物理キーをナビゲーションボタンにすることができる

この端末は指紋認証用のセンサーが前面下部に付いているのだが、そのセンサー部分を使用して Android の「戻る」「ホーム」「履歴」のナビゲーションボタンの機能をもたせることができる。 シングルタップで「戻る」、長押しで「ホーム」、左右スワイプで「履歴」といった感じだ。 これが意外と使いやすい。 Android デフォルトだと画面下の領域にこのナビゲーションボタンが配置されてしまうのだが、それが無くなるので表示領域も少し広くなる。

勿論 Android デフォルトのボタン配置にすることもできるので使いやすい方を選べば良い。

音は良いことは良いが過度の期待をしてはいけない

この端末はスピーカーが上下 4 箇所についているのでさまざまなレビューサイトで「音がいい」と紹介されている。 確かにそれは間違いではなかったのだがあくまで「タブレットにしては音がいい」というだけであるので過度の期待は禁物である。 納得できる部分だったのでそこまで不満はない。

値段に対する満足度がそこそこ高い

最後に、この端末は Wi-Fi モデルで 3 万円を切り、LTE モデルでも 3 万 5 千円を切るというミドルレンジモデルの価格帯である。 だが私が今まで使ってきたタブレットは 5 万円若しくはそれ以上するものでも動作に突っかかりを感じたり重量があったりして少なからず不満があったのも確かだ。 このタブレットはこの価格で以前感じたような不満をほとんど感じない。

ただ iOS が許容できるのであれば Wi-Fi モデルであればもう少し出せば iPad 2017 年モデルが買えるのでそちらがいいだろう。 このタブレットはどうしても Android の 10 インチ級の端末が使いたい人向けだと思った。 ゲームなどの処理が重い用途でなければ恐らく十分満足できるだろう。

後日改めてレビューした

2 週間使った感想として後日 MediaPad M3 Lite 10 改めてレビューを書いたのでそちらも参照頂ければ幸いである。

普段であれば楽天スーパーセールまで待つところなのだが、楽天は少し高いのと、早めに買っておけば月末に行く予定の旅行でも使えそうなので結局 LTE モデルを kakaku.com の最安でポチった。 合わせて amazon の方で 1,000 円くらいのケースを購入。 もっと良さそうな 10 インチのモデルが出たら買い換えるつもりなのでケースに入れて綺麗に使っておく (ヤフオクの買取価格が上がる)。 まぁこの手のガジェットは常に処分するときの事を考えて綺麗に使うところではある。

ちなみにデータ SIM であるが LINE モバイルではなくイオンモバイルにした。 月 518 円 (税込) と 22 円安いというのは誤差程度としても、キャンペーンで入会金が 1 円だったのが大きいとみた。 また、イオンモバイルは最低契約期間がなくいつ解約しても違約金が発生しないらしいのもお手軽でいい。

SIM は一週間くらいかかるようなので旅行には間に合わないかもしれないが、別に SIM が無くても Wi-Fi モデルだと思って使えばどうということはない。 この MediaPad M3 Lite 10 は Wi-Fi モデルと LTE モデルの価格差がそれほど無いのが魅力だ。 iPad (2017) だとこうはいかない。

10 インチタブレットという誘惑

私は昔から 10 インチ若しくは 9 インチ級のタブレットを度々変えては使用してきた。 このあたりの系譜は Nexus 9 LTE に書いた通りだ。 ただ、実は Nexus 9 に関しては Mate 9 を購入した際に手放してしまい、しばらくタブレット無しの生活を送っていた。 Mate 9 は 5.9 インチとなかなかの画面サイズなのでタブレット無しでもいけるだろう、という打算もあった。

今年になって HP Spectre x360 という 2-in-1 ノート PC を購入したのだが、購入当初これが「タブレット PC のように (Android や iOS タブレットの代わりとして) 使用できるだろう」と思っていた。 2-in-1 なのでキーボード部分を 360 度折りたたんでタブレットのようにして使用することが出来るからだ。 ただ、この使用方法は 1 回だけ試してみて「これは無いな」と思ってしまった。 本体が 1.3 kg もあるのでタブレットスタイルで持つと全く快適ではないし Windows 10 自体 Android と比べるとタブレットモードがあるとはいえまだまだタッチ操作の最適化が進んでいるとは言い難いところはあった。 あと、ノート PC は綺麗な液晶で見たいというのに慣れてしまっていて、指で触ってしまうと指紋が残ってしまうのが気になるところでもあった。

HUAWEI の 10 インチタブレットミドルレンジモデル

Mate 9 と合わせるわけではないが今回気になったのも HUAWEI の 10.1 インチタブレット MediaPad M3 Lite 10 である。 Lite という名前が付いているが MediaPad M3 10 Plus などという製品はない。 ただ、これの下位に位置するモデルとしては MediaPad T3 10 がある。 だがこの下位モデルの方はスペックも相当低く、液晶も 1280x800 と粗いもので全く購入対象とするには至らなかった。

この MediaPad M3 Lite 10 は Wi-Fi モデルは 3 万円を切るし LTE モデルであったとしても 3 万 5 千円弱という手頃な価格が魅力と言える。 ただその代わりスペックが低めで各種ベンチマークも振るわない結果となっており、数値上は Nexus 9 よりちょっと劣る程度しか出ない端末らしい。 流石にそれはないんじゃないか、と思い 2ch のスレッドなども漁ったが、そもそも Nexus 9 があのベンチマーク結果とは思えないくらい処理が緩慢な端末だったらしい。 私は Nexus 9 を使っていてそんなに不満には思わなかったので、恐らくこの MediaPad M3 Lite 10 でもいけるだろうと踏んだわけだ。 個人的には 5 万円くらい出してももう少し高スペックの端末が良かったのだが、現状そういうものが存在しないので仕方がない。 10 インチ級のタブレットは作ってもあまり売れないのだろう。

ちなみに MediaPad M3 というモデルは 8.4 インチのもっと高スペックのタブレットなのであるが、8.4 インチと少し小さいのが気になったので泣く泣く選択肢から外した。 Mate 9 と釣り合いをとるには 10 インチくらいないと気になるところだ。

使用用途としては Mate 9 で唯一画面が小さすぎて使う気にならなかった Kindle や PDF (技術書) 閲覧、そして Amazon ビデオや d アニメなどの動画、MJ モバイルといったところだ。 このタブレットを購入した場合 Mate 9 には引き続きバイクのナビとして活躍してもらうつもりだ。

Nexus 9 を使用していた時に挿していたデータ SIM は解約してしまったのだが、もし使うとしたら LINE モバイルあたりの 1G プランにするだろう。 月額 540 円ととても安い。 良い時代になったものだ。

素の AsyncTask は冗長過ぎる

例えば「重い処理をワーカースレッドで行い戻ってきた文字列を Toast で表示する」といった処理を AsyncTask で共通化せずに無名クラスで実装すると以下のようになる:

object : AsyncTask<Unit, Unit, String>() {
    override fun doInBackground(vararg p0: Unit?): String {
        Thread.sleep(3000)
        return "めっせーじ"
    }

    override fun onPostExecute(result: String?) {
        Toast.makeText(context, result, Toast.LENGTH_LONG).show()
    }
}.execute()

Kotlin かつちょっとした処理を書こうとしているだけなのにえらく冗長なコードになっている。 この問題点は AsyncTask が 3 つも raw type (ジェネリクスの型パラメーター) を要求する抽象クラスになっていることに加え、Kotlin の場合無名クラスを定義するのに object : (継承元クラス) といった割と仰々しい構文を使わなければならないことだ。

このような無名クラスを使うのではなく、Kotlin のシンプルなラムダ式を渡せばいいようにしたい。

SimpleAsyncTask

以下、内部的には AsyncTask を使用するが各種メソッドの override が必要な時のみラムダ式を渡せばいいように実装してみた:

class SimpleAsyncTask<R>(private val background: () -> R,
        private val pre: () -> Unit = {}, private val post: (R) -> Unit = {},
        private val cancelled: () -> Unit = {}) : AsyncTask<Unit, Unit, R>() {
    override fun onPreExecute() = pre()
    override fun doInBackground(vararg p0: Unit?): R = background()
    override fun onPostExecute(result: R) = post(result)
    override fun onCancelled() = cancelled()
}

AsyncTask が持つ経過表示を行うための機能 onProgressUpdate() はほとんど使用しないのでバッサリ切った。 また、パラメータを AsyncTask().execute(param) のように渡せる機能も共通化しないのであれば必要ないので、今回即時実行するケースのみにターゲットを絞り削った。 これで以下のように使える:

// 一番最初の AsyncTask の例と等価のコード
SimpleAsyncTask(background = {
    Thread.sleep(3000)
    "めっせーじ"
}, post = { Toast.makeText(context, it, Toast.LENGTH_LONG).show() }).execute()

あとすこし改善

かなりシンプルにはなったが AsyncTaskexecute() をコールし忘れて発火されないという凡ミスがよくある。 どうせパラメータを渡さないのだから execute() を書く必要はない。 そこで、以下の関数をトップレベルに配置する:

/**
 * SimpleAsyncTask を使用し非同期で実行する.
 *
 * @param R 非同期実行スレッドの戻り型
 * @param background 非同期実行処理
 * @param pre 非同期実行前処理 (未セットならば処理を行わない)
 * @param post 非同期実行後処理 (未セットならば処理を行わない)
 * @param cancelled 非同期実行キャンセル時処理 (未セットならば処理を行わない)
 */
fun <R> doAsync(background: () -> R, pre: () -> Unit = {}, post: (R) -> Unit = {}, cancelled: () -> Unit = {})
    = SimpleAsyncTask<R>(background, pre, post, cancelled).execute()

これで以下のように書ける:

doAsync(background = {
    Thread.sleep(3000)
    "めっせーじ"
}, post = { Toast.makeText(context, it, Toast.LENGTH_LONG).show() })

Kotlin の型推論が強力なおかげで <R> を明示する必要はない。 とてもシンプルで素晴らしい。

onSaveInstanceState 後に表示しようとするとクラッシュする DialogFragment

DialogFragment は例えば以下のように表示できる:

// MainActivity 内のコード
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    MyDialogFragment().show(supportFragmentManager, null)  // 表示処理
}

class MyDialogFragment : AppCompatDialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog
            = AlertDialog.Builder(activity).setMessage("Hello World").create()
}

通常はこれで問題ないのだが、例えば以下のようにして画面を開いた直後に「スリープ状態にする」「画面を閉じる」など行うとクラッシュする:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // 5 秒待ってから表示. この間にスリープやバックグラウンドに回すとクラッシュ!!
    Handler().postDelayed({ MyDialogFragment().show(supportFragmentManager, null) }, 5000)
}

class MyDialogFragment : AppCompatDialogFragment() {
    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog
            = AlertDialog.Builder(activity).setMessage("Hello World").create()
}

上記のように数秒待ってからダイアログ表示することなどそうそうないと思われるかもしれないが、昨今は通信を伴ったアプリの開発が当たり前であり通信の結果やエラー表示をダイアログで行っていたりするとよくこのエラーを目にすることとなる。

クラッシュの内容は java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState といった感じで恐らく Fragment を使っている人であればよく見ているのではないかと思う。 要するに Activity や Fragment の onPause() から onResume() 間には Fragment (DialogFragment) を操作できないというわけであるが、これの回避が思ったより難しい。

onPause() から onResume() 間に操作しようとしている DialogFragment の show()dismiss()ouResume() の後に pending するという方法でうまく回避できたのでここに記しておく。

前提

今回は前提として私が Qiita に以前書いた DialogFragment の共通化処理を入れているものとする。 例えば以下のような MyDialogFragment が用意されているものとする:

class MyDialogFragment : AppCompatDialogFragment() {

    /** コールバック */
    private val mCallback: Callback by lazy {
        if (targetFragment is Callback) {
            targetFragment as Callback
        } else if (activity is Callback) {
            activity as Callback
        } else {
            throw UnsupportedOperationException()
        }
    }

    /** リクエストコード */
    private val mRequestCode: Int
        get() = if (arguments.containsKey("request_code")) arguments.getInt("request_code") else targetRequestCode

    /**
     * MyDialog で何か処理が起こった場合にコールバックされるリスナ.
     */
    interface Callback {

        /**
         * MyDialog で positiveButton, NegativeButton, リスト選択など行われた際に呼ばれる.
         *
         * @param requestCode MyDialogFragment 実行時 mRequestCode
         * @param resultCode DialogInterface.BUTTON_(POSI|NEGA)TIVE 若しくはリストの position
         * @param params MyDialogFragment に受渡した引数
         */
        fun onMyDialogSucceeded(requestCode: Int, resultCode: Int, params: Bundle) = Unit

        /**
         * MyDialog がキャンセルされた時に呼ばれる.
         *
         * @param requestCode MyDialogFragment 実行時 mRequestCode
         * @param params MyDialogFragment に受渡した引数
         */
        fun onMyDialogCancelled(requestCode: Int, params: Bundle) = Unit

        /**
         * MyDialog で multiChoice した際に呼ばれる.
         *
         * @param requestCode MyDialogFragment 実行時 mRequestCode
         * @param position チェック位置
         * @param checked チェックされたか否か
         */
        fun onMyDialogMultiChoiceClicked(requestCode: Int, position: Int, checked: Boolean) = Unit

        /**
         * MyDialog で dateSet した際に呼ばれる.
         *
         * @param requestCode MyDialogFragment 実行時 mRequestCode
         * @param year 年
         * @param month 月
         * @param date 日
         */
        fun onMyDialogDateSet(requestCode: Int, year: Int, month: Int, date: Int) = Unit
    }

    override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
        val listener = DialogInterface.OnClickListener { _, which ->
            dismiss()
            mCallback.onMyDialogSucceeded(mRequestCode, which, arguments.getBundle("params"))
        }
        val isProgress = arguments.getBoolean("is_progress")
        val title = arguments.getString("title")
        val message = arguments.getString("message")
        val positive = arguments.getString("positive")
        val negative = arguments.getString("negative")
        val items = arguments.getStringArray("items")
        val multiChoiceItems = arguments.getStringArray("multi_choice_items")
        val multiChoiceItemValues = arguments.getBooleanArray("multi_choice_item_values")
        val date = arguments.getSerializable("date") as Date?
        isCancelable = arguments.getBoolean("cancelable")

        if (isProgress) {
            return ProgressDialog(activity).apply { message.let { setMessage(it) } }
        } else if (date != null) {
            val cal = Calendar.getInstance(Locale.getDefault())
            cal.time = date
            val picker = DatePickerDialog(activity, { _, year, month, date ->
                mCallback.onMyDialogDateSet(mRequestCode, year, month, date)
            }, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DATE))
            title?.let { picker.setTitle(it) }
            message?.let { picker.setMessage(it) }
            return picker
        } else {
            val builder = AlertDialog.Builder(activity)
            title?.let { builder.setTitle(it) }
            message?.let { builder.setMessage(it) }
            positive?.let { builder.setPositiveButton(positive, listener) }
            negative?.let { builder.setNegativeButton(negative, listener) }
            items?.let { builder.setItems(items, listener) }
            multiChoiceItems?.let {
                builder.setMultiChoiceItems(multiChoiceItems, multiChoiceItemValues) {
                    _, position, checked ->
                    mCallback.onMyDialogMultiChoiceClicked(mRequestCode, position, checked)
                }
            }
            return builder.create()
        }
    }

    override fun onCancel(dialog: DialogInterface?) {
        mCallback.onMyDialogCancelled(mRequestCode, arguments.getBundle("params") ?: Bundle())
    }
}

通常のタイトルやメッセージを持ったダイアログ表示に加えて、複数のアイテムから 1 つを選択するものや複数チェックできるもの、あと DatePickerDialog での日付選択や ProgressDialog での経過表示も対応している。 これを呼ぶ場合用にもショートカット用のメソッドを以下のように用意する:

/**
 * MyDialogFragment を表示する.
 *
 * @param requestCode リスクエストコード
 * @param params Bundle パラメータ
 * @param title タイトル
 * @param message メッセージ
 * @param positive positiveButton ラベル
 * @param negative negativeButton ラベル
 * @param items シングルタップ選択用の items
 * @param multiChoiceItems 複数選択用の items
 * @param multiChoiceItemValues 複数選択用の items (値)
 * @param date DatePicker 用の初期値
 * @param tag Tag
 * @param cancelable キャンセル出来るか否か
 */
fun showDialog(requestCode: Int = -1, params: Bundle = Bundle(), title: Any? = null,
        message: Any? = null, positive: Any? = null, negative: Any? = null,
        items: Any? = null, multiChoiceItems: Any? = null, multiChoiceItemValues: BooleanArray? = null,
        date: Date? = null, tag: String = "default", cancelable: Boolean = true) {
    fun getStringArray(x: Any): Array<String> = when (x) {
        is Int -> resources.getStringArray(x)
        is ArrayList<*> -> x.toArray(arrayOfNulls(0))
        is Array<*> -> x.map(Any?::toString).toTypedArray()
        else -> throw IllegalArgumentException()
    }
    if (this !is MyDialogFragment.Callback) {
        throw UnsupportedOperationException("Using MyDialogFragment must implement Callback to Fragment")
    }
    val args = Bundle()
    title?.let { args.putString("title", if (it is Int) getString(it) else it.toString()) }
    message?.let { args.putString("message", if (it is Int) getString(it) else it.toString()) }
    items?.let { args.putStringArray("items", getStringArray(it)) }
    multiChoiceItems?.let { args.putStringArray("multi_choice_items", getStringArray(it)) }
    multiChoiceItemValues?.let { args.putBooleanArray("multi_choice_item_values", it) }
    date?.let { args.putSerializable("date", it) }
    positive?.let { args.putString("positive", if (it is Int) getString(it) else it.toString()) }
    negative?.let { args.putString("negative", if (it is Int) getString(it) else it.toString()) }
    args.putBoolean("cancelable", cancelable)
    args.putBundle("params", params)

    MyDialogFragment().apply { arguments = args }.show(supportFragmentManager, tag)
}

ちょっと複雑そうに見えるのは、各種引数をリソース ID と文字列の双方で渡せるように考慮を入れているからである (その為多くの型を Any? にしている)。 このメソッドを使用して前述の MyDialogFragment を show() するには以下のように書く:

// 5 秒待ってから表示
Handler().postDelayed({ showDialog(message = "Hello World") }, 5000)

この時点では onPause() 後の pending の対応が入っていないため先ほどと同じようにスリープにしたり画面を閉じたりすると IllegalStateException がスローされる。

対処

この対処を行う上で残念なのが結局 Fragment を拡張するようなパターンが必要 (BaseFragment のようなもの) になってしまうということだ。 どうしても onResume() と密に結びついてしまうので仕方がない。 これを避けようとすると作成した Activity や Fragment の onResume() の前に毎回お決まりのコードを入れなければならないので若干辛いものがある。

abstract class MyFragment : Fragment(), MyDialogFragment.Callback {

    /** 復帰のために貯めておく PTDialogFragment の表示データ */
    private val mDialogData: MutableList<Bundle> = mutableListOf()

    /** 復帰のために貯めておく dismissDialog のデータ */
    private val mDismissingTags: MutableList<String> = mutableListOf()

    /**
     * ProgressDialog を dismiss する.
     *
     * @param tag Tag
     */
    fun dismissDialog(tag: String = "progress") {
        if (isResumed) {
            fragmentManager.findFragmentByTag(tag)?.let {
                if (it is AppCompatDialogFragment) {
                    it.dismiss()
                } else {
                    throw UnsupportedOperationException()  // DialogFragment ではない
                }
            }
        } else {
            mDismissingTags += tag  // resume でない場合は pending する
        }
    }

    /**
     * ProgressDialog を表示する.
     *
     * @param message メッセージ
     * @param tag Tag
     * @param cancelable キャンセルできるか否か
     */
    fun showProgressDialog(message: Any? = null, tag: String = "progress", cancelable: Boolean = true) {
        val args = Bundle()
        args.putBoolean("is_progress", true)
        message?.let { args.putString("message", if (it is Int) getString(it) else it.toString()) }
        args.putBoolean("cancelable", cancelable)

        // resume でない場合は pending する
        if (isResumed) {
            with(MyDialogFragment()) {
                arguments = args
                setTargetFragment(this@MyFragment, 0)
                show(this@MyFragment.fragmentManager, tag)
            }
        } else {
            // 復帰させるために requestCode, tag を格納する
            args.putString("tag", tag)
            mDialogData += args
        }
    }

    /**
     * PTDialogFragment を表示する.
     *
     * @param requestCode リスクエストコード
     * @param params Bundle パラメータ
     * @param title タイトル
     * @param message メッセージ
     * @param positive positiveButton ラベル
     * @param negative negativeButton ラベル
     * @param items シングルタップ選択用の items
     * @param multiChoiceItems 複数選択用の items
     * @param multiChoiceItemValues 複数選択用の items (値)
     * @param date DatePicker 用の初期値
     * @param tag Tag
     * @param cancelable キャンセル出来るか否か
     */
    fun showDialog(requestCode: Int = -1, params: Bundle = Bundle(), title: Any? = null,
            message: Any? = null, positive: Any? = null, negative: Any? = null,
            items: Any? = null, multiChoiceItems: Any? = null, multiChoiceItemValues: BooleanArray? = null,
            date: Date? = null, tag: String = "default", cancelable: Boolean = true) {
        fun getStringArray(x: Any): Array<String> = when (x) {
            is Int -> context.resources.getStringArray(x)
            is ArrayList<*> -> x.toArray(arrayOfNulls(0))
            is Array<*> -> x.map(Any?::toString).toTypedArray()
            else -> throw IllegalArgumentException()
        }
        if (this !is MyDialogFragment.Callback) {
            throw UnsupportedOperationException("Using PTDialogFragment must implement Callback to Fragment")
        }

        // 既に detach されている場合は何もしない
        if (isDetached) return

        val args = Bundle()
        title?.let { args.putString("title", if (it is Int) getString(it) else it.toString()) }
        message?.let { args.putString("message", if (it is Int) getString(it) else it.toString()) }
        items?.let { args.putStringArray("items", getStringArray(it)) }
        multiChoiceItems?.let { args.putStringArray("multi_choice_items", getStringArray(it)) }
        multiChoiceItemValues?.let { args.putBooleanArray("multi_choice_item_values", it) }
        date?.let { args.putSerializable("date", it) }
        positive?.let { args.putString("positive", if (it is Int) getString(it) else it.toString()) }
        negative?.let { args.putString("negative", if (it is Int) getString(it) else it.toString()) }
        args.putBoolean("cancelable", cancelable)
        args.putBundle("params", params)

        // resume でない場合は pending する
        if (isResumed) {
            with(MyDialogFragment()) {
                arguments = args
                setTargetFragment(this@MyFragment, requestCode)
                show(this@MyFragment.fragmentManager, tag)
            }
        } else {
            // 復帰させるために requestCode, tag を格納する
            args.putInt("request_code", requestCode)
            args.putString("tag", tag)
            mDialogData += args
        }
    }

    override fun onResume() {
        super.onResume()
        mDialogData.forEach {
            with(MyDialogFragment()) {
                arguments = it
                setTargetFragment(this@MyFragment, it.getInt("request_code"))
                show(this@MyFragment.fragmentManager, it.getString("tag"))
            }
        }
        mDialogData.clear()
        mDismissingTags.forEach { dismissDialog(it) }
        mDismissingTags.clear()
    }
}

こんな MyFragment を用意する。 そうして以下のように MyFragment を継承した Fragment 上で MyDialogFragment を表示してみる:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    savedInstanceState ?: supportFragmentManager.beginTransaction()
            .add(android.R.id.content, AFragment())
            .commit()
}

class AFragment : MyFragment() {
    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)
        
        // 5 秒後表示. 今度はスリープしてもクラッシュせずに復帰時にちゃんと Dialog が表示される!!
        Handler().postDelayed({ showDialog(message = "Hello World") }, 5000)
    }
}

今度はクラッシュしないし、画面復帰時に正しく表示しようとしていた Dialog が表示される。 肝は Dialog を表示しようとする時に isResumed でなかったら tag で表示しようとしている Dialog の情報を保存し onResume() 後に改めて表示するというところだ。 これを共通ダイアログに対して行っておくとこのようなボイラープレートコードをあちこちに仕込まなくて済むようになるので有用である。

日本固有の悩みだろうか

Android で Google Map を配置したアプリを作成する場合は Google Maps Android API を使用することになると思うが、現状 Google Map 内に表示される文字サイズを変更するような API は用意されていないし Android 端末の設定の文字サイズを変更してもアプリ内の文字サイズは変わる (但しアプリ側が行儀よく sp 単位でフォントサイズ指定されている場合) のだが、Google Map 内の文字サイズは残念ながら連動しない。

老眼、つまり高齢化は先進国における世界的な流れだと思うのだが、何故 Google Maps Android API にそのような考慮がなされていないのか。 恐らく欧米はラテン文字を使用しているためフォントサイズが比較的小さくても視認性が高くそういった問題は余り出てこないのではないだろうか。 その点漢字は不利である。

高齢者向けのアプリを開発する場合どうしても「文字サイズを大きくする」というところを考慮しなければならない。 API が用意されていないので、逆に地図自体を大きくするのはどうかと考えた。 少しボヤケた感じにはなるが、意外と悪くないアプローチなのでここに書き残しておく。

まず地図自体を大きくする

SupportMapFragment のインスタンスを取得しそれに紐づく View に対し setScaleXsetScaleY を呼べば簡単に変更できる。 例えば 2 倍にする場合は以下でよい:

mMapFragment.view?.scaleX = 2.0f
mMapFragment.view?.scaleY = 2.0f

だがこれを愚直に行うと以下の問題が発生する:

  • Google ロゴや現在位置ボタン、ズームイン・アウトボタンが画面の領域外に見切れてしまう
  • ボタン類や配置したオブジェクト (ピン、吹き出しなど) も 2 倍になってしまう

ロゴやボタンが見切れないようにする

幸い GoogleMap#setPadding というメソッドで Google Map に対しパディングを設定できる。 パディングは端末の縦横ピクセルを取得し適切な値を設定しなければならない。 Android アプリ開発ではよくあるパターンではあるが View のサイズ取得はビューツリー構築後でないと計算されておらず 0 になってしまうところに注意する。 このように ViewTreeObserver#addOnGlobalLayoutListener を使うのがイディオムだ:

mMapFragment.view?.viewTreeObserver?.addOnGlobalLayoutListener {
    view?.let { view ->
        mMapFragment.getMapAsync { map ->
            // 2 倍だけでなく 1.5 倍などもサポートする場合もう少し工夫する必要あり
            val (width, height) = view.width to view.height
            val paddingLeft = (width / 4).toInt()
            val paddingTop = (height / 4).toInt()
            map.setPadding(paddingLeft, paddingTop, paddingLeft, paddingTop)
        }
    }
}

2 倍になってしまうボタンを使わずに自作する

2 倍になってしまう Google ロゴは仕方ないとして、ボタン類が 2 倍になっているのはどうにも不格好だ。 まずデフォルトのボタン類を以下で非表示にする:

// map は GoogleMap インスタンス (getMapAsync() での取得後)
map.uiSettings.isZoomControlsEnabled = false
map.uiSettings.isMyLocationButtonEnabled = false

続いて自作のアイコンを配置するわけだが、アイコン画像は Google Material Icons に自由に使えるアイコンが配布されているので、これを引っ張ってくれば良い。 そして GoogleMap#getMapAsync()GoogleMap インスタンスを取得できたタイミングで以下のような感じでイベントをセットすればよい:

myLocation.setOnClickListener {
    // TODO 本当は myLocation は deprecated なので FusedLocationApi を使用したものに置き換えるのがよい
    val latLng = LatLng(map.myLocation.latitude, map.myLocation.longitude)
    map.animateCamera(CameraUpdateFactory.newLatLng(latLng))
}
zoomIn.setOnClickListener { map.animateCamera(CameraUpdateFactory.zoomIn()) }
zoomOut.setOnClickListener { map.animateCamera(CameraUpdateFactory.zoomOut()) }

2 ヶ月半ほど使用

HUAWEI のフラッグシップモデル、5.9 インチ Android スマートフォンの Mate 9 を去年購入した。 最初品薄でなかなか手に入らなかったのだが、運良く年が変わる前に手に入れることができ、今まで 2 ヶ月半ほど使用してきた。 感想をここにちょっとしたレビューとして書き残しておく。

ちなみに私は Android アプリケーションプログラマではあるが、特にガジェットマニアというわけではなく、スマートフォンはこれ 1 台しか持っていないしタブレットも今は所有していない。 なのでコアなレビューは出来ない。 そういったものをお望みの方はネット上で多くの方がレビューしているのでそちらを参照頂きたい。

比較対象は LG の Nexus 5X である。

画面はデカイがベゼルが狭いので使い勝手が良い

5.9 インチの画面サイズというともうファブレットのサイズである為普段大きすぎるのではないか、と思われるかもしれないがベゼル (枠) が狭いので意外と大きさは感じない。 私はズボンの前ポケットに入れることが良くあるが、5.2 インチの Nexus 5X と比べてもそこまで圧迫感を感じない。 むしろ Nexus 5X でも前ポケットは厳しい時があったので、昨今の大画面のスマートフォンでは別途バッグに入れるなどして対処した方がいいかもしれない。

この画面サイズだとゲームが非常に気持ちよくプレイできるので、ゲーム重視の方はお勧めである。 私は MJ モバイル (麻雀) や将棋ウォーズをよくプレイしているが、駒や牌がとても掴みやすくなった。

電池持ちが非常に良い

私はバイクに取り付けてナビとして使用しているが、バイクから USB 給電などしていないので Nexus 5X だとどんどん電池が減ってしまいモバイルバッテリー必須の上充電速度も遅く困っていたのだが、Mate 9 にしたら電池の減るスピードが半分近くに減った。 これは自分にとって非常に重要なポイントで気に入っている。

Mate 9 のウリである「超急速充電」が出来るのは純正のアダプタだけらしいので、普通のモバイルバッテリーだと充電速度が遅いのだが全然気にならない。

スピーカーの最大音量は結構大きく出来る

バイクに乗っている時に音量を最大にしても Nexus 5X だとちょっと音が小さかったのだが、Mate 9 はもっと大きく出来るのでナビの音声が聞き取りやすい。

プリインストールされている HUAWEI のアプリがどれもシンプル

DoCoMo や au で購入したスマートフォンの場合、大抵プリインストールされているアプリがクールでない上に動作が緩慢だったりして辟易するものだが、HUAWEI がプリインストールしているアプリはどれも「そのまま使いたくなる」ものばかりで好感が持てる。 例えば「カレンダー」「ミラー」「時計」「電卓」「連絡先」といった感じなのだが、どれも日本メーカー特有の「芋臭さ」が全くない。 前はプリインストールアプリがうるさくない Android 端末を使いたければ Pixel (Nexus) を買わなければいけないのかと思っていたがそんなことはなかったわけだ。

カメラ性能は素晴らしい

私は自撮りも滅多にしないしカメラに拘る方ではないのだが、それでもこの機種のカメラは素晴らしいと分かる。 撮影した画像はこの Blog に幾つか載せてあるので参照頂きたいが、ウォーターマーク (HUAWEI Mate 9 LEICA DUAL CAMERA といった文字列) を入れることができ、これもなかなか格好いい。 勿論ウォーターマーク無しでも撮影できる。

カラーバリエーションは最近ブラックが用意された

発売当時は白と金しか無く、消去法で白を選択したのだが、本当はベゼルが黒い方が好きなので黒があればそちらが良かった。 今は販売されているようなのでちょっと悔しい。

高級感漂う

この端末は HUAWEI のフラッグシップモデルらしいが、破格の 6 万弱で手に入った。 フラッグシップモデルの名に恥じぬ造形だし、チープさは微塵も感じない。

総括

自分としては弱点らしき弱点は特に見つからない、かなりの良端末といえる。 今まで持ったスマートフォンの中でナンバーワンといえる。

実は HUAWEI の端末は初めてではなく P9lite という安い端末を使っていたことがあるのだが、それも値段に見合わず質感が良く好感が持てる端末だった。 但し動作は値段なりに緩慢な感じだったのは仕方がないところか。 Mate 9 の方は勿論サクサク動作するので心配はいらない。

今更なのだが Nexus 5X のレビューを書いてみる。 私はこの前身である Nexus 5 を使用していたこともあるのだが Nexus 5 は当時としてはコンパクトな 5 インチスマートフォンであり Nexus ブランドならではのプリインアプリ一切無しの上に挙動も早く重宝していたので 5X も購入したのだが、正直期待外れだった部分もあった。

最近使っていて一番イライラするのは MJ モバイルやちょっとしたゲームなどの挙動が明らかにもたつくことだ。 Nexus 9 に比べて処理がワンテンポ遅い。 普通画面が大きくて古い Nexus 9 の方が負けるものだと思うのだが……。

いい部分も書くと、この端末では背面に指紋認証センサーが付いており、スリープ状態でここに人差し指を当てることで即座にロック解除できるのだが便利。 これは今後日本で販売されるであろう Pixel (XL) にも踏襲されている。

電池持ちは普通で良くも悪くもない印象。 ただ急速充電できるのは便利。

最近は価格も下がってきているので手軽にプリインアプリ無しの SIM フリースマホを持ちたい方はいいかもしれない。 ただ今だとコスパで選ぶなら Huawei P9lite がいい気がする。

ウィジェット部分は旧アプリのものをほぼそのまま移植したコジごみカレンダーだったが、何故かウィジェットの配置するセル数が想定しているサイズより大きくなってしまうという問題が起きていた。 コジごみカレンダーは「セル縦 1 × 横 2」サイズのものを想定していたが、何故か Nexus 5X で配備しようとすると「2 × 3」になってしまう。

私は業務の Android 開発ではウィジェットを使用するという事がほぼ皆無だったので昨今のウィジェット開発事情に関して全く無知であった。 これに関しては株式会社ノーティス様の記事が非常に参考になった。

targetSdkVersion = 14 (Android 4.0) 以上の場合ウィジェットの 1 セルあたりの計算式が従来と異なるというのが原因で、旧コジごみカレンダーは Android 2.2 向けということで targetSdkVersion = 8 であったが今回の新アプリに関しては Android Nougat (Android N) に設定してあった。 まぁ今時 targetSdkVersion に過去のものを指定するというのは良くないのでウィジェットの計算式を新方式にして解決した。

株式会社ノーティス様の記事にも記載があるが、新旧仕様での n セルの dp 量は以下で計算出来る:

(targetSdkVersion 14 未満) 74 n - 2
(targetSdkVersion 14 以上)
70
n - 30

例えば 1 × 2 で配置しようとすると旧仕様だと 72 × 146 (dp) だが新仕様だと 40 × 110 (dp) となる。全然違う。

かなり手間をかけた全面刷新

コジごみカレンダー自体は 5 年近く前に初版をリリースした歴史ある Android アプリなのだが、アプリの構造が Android 2.2 時代のもののままなので更新するにも何かと制約がありモチベーションが保てなくなっていた。 なので現代の技術で全面リプレースするという構想は実は 2, 3 年前から持っていたのだが、既にかなり複雑なアプリに仕上がってしまっていたので移植しようとすると全ての機能を網羅しなければならず、なかなか最後まで続かない結果になっていた。 作っては中断、作ってはまた中断を繰り返しており、開発は頓挫状態となっていた。

Kotlin に感謝

では何故今回モチベーションが保てたのかというと間違いなく Kotlin のお陰だ。 既存の冗長な Java コードを Kotlin で書き換えるのが非常に気持ちよく、地味な移植作業を続けるのがそれ程苦ではなくなっていた。

今回 In-app Billing の既存のコード以外全て Kotlin で書き直している。 業務で Kotlin を使うのは流石に憚られたが、自分のアプリなら自由に使えるのでなかなか楽しい。

ただ 「Android 開発経験がそれほどない技術者は素直に Java で書いたほうがいい」と思った。 所々 Kotlin の文法ミスなのか Android SDK の API の使用方法が間違っているのか判断がつかない場面があったので、恐らく経験が浅いと解決できないのではないかと思われたからだ。

これが特に良かったという点

実際にコジごみカレンダーのコードを一部引用し述べる。

Fragment で Activity のインスタンスを使用する場合コンストラクタ等で行おうとするとまだ Activity に attach() されてないので getActivity()null になる。 これを避ける為には Fragment#onAttach() で Activity のインスタンスを参照するか Fragment#onActivityCreated()getActivity() するのが普通なのだが、そうすると以下のように退屈な代入を書かなければならない:

private ContentResolver mResolver;  // ここで代入できないから final にできない!!
...
@Override
public void onActivityCreated(final Bundle savedInstanceState) {
    super.onActivityCreated(savedInstanceState);
    mResolver = getActivity().getContentResolver();
}

Kotlin なら by lazy { ... } を使えば実際に使用する箇所でコードブロックが走る (遅延実行) ので大丈夫だ:

// mResolver を onActivityCreated() 以降でしか参照しないならこれで問題ない
private val mResolver: ContentResolver by lazy { activity.contentResolver }

多数の分岐を持った変数代入を Java でやると以下だ:

// 読みやすいけど退屈
final int backgroundColor;
if (year == currentYear && month == currentMonth && date == currentDate) {
    backgroundColor = getResources().getColor(R.color.calendar_today);
} else if (targetMonth != month) {
    backgroundColor = getResources().getColor(R.color.super_light_gray);
} else if (j == 0 || dayType.isHoliday(cal) || dayType.isExtra(cal)) {
    backgroundColor = getResources().getColor(R.color.calendar_sunday);
} else if (j == 6) {
    backgroundColor = getResources().getColor(R.color.calendar_saturday);
} else {
    backgroundColor = Color.WHITE;
}
columnLayout.setBackgroundColor(backgroundColor);

Kotlin だと when 文があるのでかなり簡潔になる:

columnLayout.setBackgroundColor(when {
    year == currentYear && month == currentMonth && date == currentDate -> resources.getColor(R.color.calendar_today)
    targetMonth != month -> resources.getColor(R.color.calendar_not_target_month)
    j == 0 || Utils.isHoliday(cal) || Utils.isExtra(cal) -> resources.getColor(R.color.calendar_sunday)
    j == 6 -> resources.getColor(R.color.calendar_saturday)
    else -> Color.WHITE
})

後は拡張メソッド (エクステンション) やトップレベル関数、インライン関数やラムダ式なども勿論便利だった。 もうプライベートで行うコーディングは Java で行う気がしない。

ちょっと前までは AppCompatActivity を使用した場合に PreferenceFragment が Support Library の Fragment を継承していないのでそのまま使えず歯がゆい思いをしていたが Android Support Library の Fragment を継承した PreferenceFragmentCompat が登場しその問題が解決された。 しかし古いアプリを移植しようとして RingtonePreference (通知音選択) がそのまま使えない事に気付いた。 SwitchPreferece には SwitchPreferenceCompat が用意されているのに RingtonePreference にはそういったものがない。

仕方が無いので普通の Preference で自分で実装することにした。

呼び出し側

暗黙的インテントを投げることで同機能を実装できる:

    mNotificationSound.setOnPreferenceClickListener {
        val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER)
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_NOTIFICATION)
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_DEFAULT, true)
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_SHOW_SILENT, true)
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_DEFAULT_URI, Settings.System.DEFAULT_NOTIFICATION_URI)

        val existingValue = pref().getString("setting.notificationSound", null)

        // Uri として null を指定すると通知音「なし」扱いになることに注意
        val uri: Uri? = when {
            existingValue == null -> Settings.System.DEFAULT_NOTIFICATION_URI
            existingValue.length > 0 -> Uri.parse(existingValue)
            else -> null
        }
        intent.putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, uri)
        startActivityForResult(intent, 0)
        true
    }

「Preference の設定がまだされていない場合」「通知音がなし (サイレント) の場合」「通知音が設定されている場合」の 3 通りあることに注意。 その場合分けは上のコードの when 文に示されている。

onActivityResult() 側

暗黙的インテントの戻り値を受け取る為に onActivityResult() も実装する:

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    if (requestCode === 0 && data != null) {
        val ringtone = data.getParcelableExtra<Parcelable?>(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
        pref().edit().putString("setting.notificationSound", ringtone?.let { it.toString() } ?: "").apply()
    } else {
        super.onActivityResult(requestCode, resultCode, data)
    }
}

こちらは通知音がなしの場合は null で渡ってくるので Preference に空文字で設定しておく。 空文字で無くても null でさえなければ何でも構わない。 null だと未設定との区別がつかなくなる。

ちなみに上記 val ringtone には content://media/internal/audio/media/22 といった感じの CONTENT_URI 表現が入ってくる。 実際に Notification を鳴らす際にこのまま使うことができる。

Qiita で Bundle を少しだけ簡単に書くという記事を書いた。今回はそれの Kotlin バージョンとなる。

ソースコード

/**
 * Bundle オブジェクトを作成する utility method.
 *
 * @param args key と value の pair
 * @return Bundle
 */
fun bundle(vararg args: Pair<String, Any?>): Bundle = Bundle().put(*args)

/**
 * すでにある Bundle オブジェクトに key と value を複数追加する utility method.
 *
 * @receiver Bundle
 * @param args key と value の pair
 * @return Bundle
 */
fun Bundle.put(vararg args: Pair<String, Any?>): Bundle = args.fold(this, { bundle, (key, value) ->
    when (value) {
        null -> bundle.putString(key, null)
        is Boolean -> bundle.putBoolean(key, value)
        is BooleanArray -> bundle.putBooleanArray(key, value)
        is Bundle -> bundle.putBundle(key, value)
        is Byte -> bundle.putByte(key, value)
        is ByteArray -> bundle.putByteArray(key, value)
        is String -> bundle.putString(key, value)
        is Char -> bundle.putChar(key, value)
        is CharArray -> bundle.putCharArray(key, value)
        is CharSequence -> bundle.putCharSequence(key, value)
        is Double -> bundle.putDouble(key, value)
        is DoubleArray -> bundle.putDoubleArray(key, value)
        is Float -> bundle.putFloat(key, value)
        is FloatArray -> bundle.putFloatArray(key, value)
        is Short -> bundle.putShort(key, value)
        is ShortArray -> bundle.putShortArray(key, value)
        is Int -> bundle.putInt(key, value)
        is IntArray -> bundle.putIntArray(key, value)
        is Long -> bundle.putLong(key, value)
        is LongArray -> bundle.putLongArray(key, value)
        is Parcelable -> bundle.putParcelable(key, value)
        is Array<*> -> {
            if (value.size > 0 && value.all { it is String }) {
                bundle.putStringArray(key, value.map { it as String }.toTypedArray())
            } else if (value.size > 0 && value.all {it is CharSequence}) {
                bundle.putCharSequenceArray(key, value.map { it as CharSequence }.toTypedArray())
            } else if (value.size > 0 && value.all {it is Parcelable }) {
                bundle.putParcelableArray(key, value.map { it as Parcelable }.toTypedArray())
            } else {
                throw IllegalArgumentException("$key の配列における型パラメータが Bundle で扱えない.")
            }
        }
        is ArrayList<*> -> {
            if (value.size > 0 && value.all { it is String }) {
                bundle.putStringArrayList(key, ArrayList(value.map { it as String }))
            } else if (value.size > 0 && value.all {it is CharSequence}) {
                bundle.putCharSequenceArrayList(key, ArrayList(value.map { it as CharSequence }))
            } else if (value.size > 0 && value.all {it is Int}) {
                bundle.putIntegerArrayList(key, ArrayList(value.map { it as Int }))
            } else if (value.size > 0 && value.all {it is Parcelable }) {
                bundle.putParcelableArrayList(key, ArrayList(value.map { it as Parcelable }))
            } else {
                throw IllegalArgumentException("$key の ArrayList における型パラメータが Bundle で扱えない.")
            }
        }
        else -> throw IllegalArgumentException(key + "に対応する値が解釈できない.")
    }
    bundle
})

使用例

以下の様な感じで使える:

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    // Bundle オブジェクト生成 {hoge: "fuga", hage: [1, 2, 3]}
    val b = bundle("hoge" to "fuga", "hage" to arrayOf(1, 2, 3))

    // Bundle にあたかも put メソッドがあるかのように {hehe: "ouie"} 追加
    b.put("hehe" to "ouie")
    println(b)  // Bundle[{hage=[Ljava.lang.Integer;@a0250eb, hehe=fufu, hoge=fuga, hoho=ouie}]
}

拡張関数

Kotlin だと何が有利かというところで 1 つめ。 拡張関数という仕組みがあり、既存クラスにあたかもそういうメソッドを持っているかのように関数を追加できる。 上記例の fun Bundle.put(vararg args: Pair<String, Any?>): Bundle がそれである。 この仕組のお陰で別途ユーティリティクラスを作成してそれを呼ぶようなことをしなくても直接オブジェクトから呼べて自然な実装となる。

第一級関数

Java だと必ずクラスを作ってその中にメソッドを書かなければならないが Kotlin は一番外側に関数を記述可能。 上記例の fun bundle(vararg args: Pair<String, Any?>): Bundle がそれである。 しかもこの外側の関数をそのまま import できる (Java だとちょっと違うが static インポートに当たるか) のでソースコードがスッキリする。

to は custom operator

"hoge" to "fuga"to って何だろう。言語仕様だろうか。などと思ったかもしれないが、これは custom operator でビルトインのライブラリに以下のように定義されている:

public infix fun <A, B> A.to(that: B): Pair<A, B> = Pair(this, that)

この infix というのを付けると自由な演算子が追加できるらしい。 この例だと任意のクラス A, B に対し to 演算子を適用すると Pair<A, B> インスタンスを生成して返す、というところか。

以前 Kotlin にはタプルがあったらしいが、これがあるので廃止されたらしい。

Hello World が短い

関係ないが Kotlin の Hello World が短い。以下で良い:

fun main(args: Array<String>) = println("Hello Kotlin!")

Java は以下:

public class Java {
    public static void main(String...args) {
        System.out.println("Hello World");
    }
}

写真を効率的にバックアップするのはどうすればよいのか

私は写真を撮るのがそんなに頻繁ではないのだが、写真を撮った後 PC に送ったりする時にどうするのがベストなのかいつも悩んでいた。 この話をすると何故か多くの人が SD カード を使用する事を選択しているように思う。 SD カードで移す話を聞くと、昔 CF (コンパクトフラッシュ) カードに記録するタイプのデジカメを使用していて、それのデータを PC に移す時の事を思い出す。 物理デバイスを差し替えるというのは 1 回 1 回は大した手間ではないのだが、毎回それを行うのはやはり手間に感じるものだ。

iPhone は Lightning ケーブルを Mac に繋いで iTunes で自動なのは知っているが、以下 Android での話。

世の中で Android が使われるようになって Android 端末を microUSB ケーブルで PC と繋いでファイルをやり取りするというのは定着したと思う。 OS X だと Android File Transfer だろう。 Android の良い所は Linux ディレクトリ構造がちゃんと表示されるのでパーミッションが許される場所ならば置きたいところに好きなファイルを置けることだ。 これで写真や音楽やら簡単にやり取りすることができる。が、やはりベストではない。

ケーブルを繋ぐのも面倒なので、いつもカメラで撮影後 Google ドライブにアップロードするという方法を取っていた。 しかし、手動でアップロードするのも面倒だ。カメラアプリで写真を撮った瞬間に勝手にクラウドストレージにバックアップして欲しい。 Dropbox だと昔からこのサービス (写真の自動アップロード) があって凄く便利だった。が、筆者は Dropbox を使っておらず Google ドライブを愛用している。

Google フォトのバックアップ

いつだったか、とはいえ最近だと思うが Android の Google フォトアプリが更新されバックアップの機能が追加された。 Android で Google フォトアプリを開き「設定」->「バックアップと同期」を押下。 するとバックアップの設定が出てくるのでこれを ON にする。 そうすると Android のカメラアプリで撮影すると自分の Google ドライブの「Google フォト」フォルダに自動的にバックアップしてくれるようになる。

一つ、「アップロードサイズ」の設定があり、「高画質」の方だと写真が無制限でバックアップできるというのがある。 これは写真を撮りまくる人には非常に便利なのではないか。 もう一つの設定「元のサイズ (一切圧縮されない)」だともともとの Google ドライブの容量 (2016/01 現在無料枠で 15G) が適用される。 私の場合は写真をあまり撮らないのでこれで十分かもしれない。

しかも Android だけではなく iOS 版もあるし、なんと Windows 版と OS X 版もある。 PC 側で収集した画像も自動で Google ドライブの同一フォルダにバックアップしてくれるわけだ。