【Android】他のアプリの上に重ねて表示まとめ
Androidの世界では基本的に1アプリのみが前面に出る。
しかし android.permission.SYSTEM_ALERT_WINDOW
を宣言することで他のアプリが前面の場合でも Viewを表示できる
Android 6.0未満
制限はなく、 android.permission.SYSTEM_ALERT_WINDOW
を宣言するだけで利用可能
Android 6.0以上
攻撃者でもこの権限を悪用し、ランサムウェアなどで常に画面表示することができてしまうため、ユーザの明示的な許可が必要になった。 ただしAndroid 6.0.1以上でGoogle Playアプリ6.0.5以上が入っている場合、Google Playからインストールしたアプリは「他のアプリの上に重ねて表示」の権限がデフォルトオンになる。恐らくGoogle的にはGoogle Playにあるアプリは安全という前提がある。 https://stackoverflow.com/questions/36016369/system-alert-window-how-to-get-this-permission-automatically-on-android-6-0-an/36019034
この権限はRuntime Perissionではなく設定の奥にあるのでIntentを飛ばし、 onActivityResult()
でハンドリングする。
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:$packageName")) startActivityForResult(intent, REQUEST_CODE_OVERLAY_PERMIISSION)
設定が有効かどうかは Settings.canDrawOverlays()
を使う。
Android 8.0以上
targetSdkVersion 26以上にてViewを表示するときの指定でTYPE_APPLICATION_OVERLAY
を使わなければいけなくなった。これによりシステムUIなどの重要なViewの上に描画できなくなり、アプリインストール時に権限の上にViewを表示して隠すなどの攻撃が不可能になった。
https://developer.android.com/about/versions/oreo/android-8.0-changes
Android 8.0では権限取得にバグがあり、変更がすぐに反映されないので監視する。 https://stackoverflow.com/questions/46173460/why-in-android-o-method-settings-candrawoverlays-returns-false-when-user-has
Android 8.1では修正済み。https://issuetracker.google.com/issues/66072795
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) { val appOpsManager = getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager appOpsManager.startWatchingMode(AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW, packageName, object : AppOpsManager.OnOpChangedListener { override fun onOpChanged(op: String?, packageName: String?) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.O) { appOpsManager.stopWatchingMode(this) onOverlayPermissionResult(Settings.canDrawOverlays()) } } }) }
【Android】Google AnalyticsからFirebaseへの乗り換え
Firebaseを使ってみたかったので置き換えてみた。
プロジェクトのbuild.gradle
dependencies { ... classpath 'com.google.gms:google-services:4.0.0' }
アプリのbuild.gradle
dependencies { ... implementation "com.google.firebase:firebase-core:16.0.1" } apply plugin: 'com.google.gms.google-services'
イベントを送るクラスの置き換え
companion object DefaultTracker { - fun setUp(context: Context): Tracker { - val analytics = GoogleAnalytics.getInstance(context) - return analytics.newTracker(R.xml.global_tracker) + fun setUp(context: Context): FirebaseAnalytics { + return FirebaseAnalytics.getInstance(context)
必要ないコードの削除
スクリーンは screen_view
というイベントで勝手に送ってくれるので削除
- tracker.setScreenName(screenName) - tracker.send(HitBuilders.ScreenViewBuilder().build())
クリックイベントの置き換え
Firebase側である程度のイベントを用意してくれている。FirebaseAnalytics.Param.CONTENT_TYPE
や FirebaseAnalytics.Param.VALUE
で色々指定できそうなのでヘルパーメソッドを作った。
fun sendButtonEvent(itemId: String) { sendEvent(itemId, "button", FirebaseAnalytics.Event.SELECT_CONTENT) } fun sendSettingEvent(setting: String, value: String) { sendEvent(setting, "setting", FirebaseAnalytics.Event.SELECT_CONTENT, value) } private fun sendEvent(itemId: String, contentType: String, event: String, value: String = "") { if (BuildConfig.DEBUG) return val params = Bundle().apply { putString(FirebaseAnalytics.Param.ITEM_ID, itemId) putString(FirebaseAnalytics.Param.CONTENT_TYPE, contentType) putString(FirebaseAnalytics.Param.VALUE, value) } tracker.logEvent(event, params) }
デバッグ
コンソールでDebugViewを開く。あとは端末を繋いで adb shell setprop debug.firebase.analytics.app <package_name>
を実行すればデバッグができる。
ErgoDoxのキースイッチを交換する
最近どうもdが押しにくく、ファームウェアをアップデートしても直らなかったため、キースイッチを交換することにしました。
を参考にやりました。
必要なもの
キースイッチ($10+送料$15=$25=2732円) *1ドル約109円として計算
Cherry MX Black Keyswitch - Linear - 5 pack by Cherry
- キースイッチを引き抜く器具 ($6.99+送料$15=$21.99=2403円) *1ドル約109円として計算
MX Switch Top Removal Tool (Set of 2) by Mechanical Keyboards Inc
当初↑の記事を読んで100均のクリップを改造すればOKと書いてあったので、キースイッチを引き抜く器具は買っていなかったが、私には難しすぎたので結局注文。送料がもったいない・・・ 海外発送なので届くまでに約1ヶ月ほどかかるので注意。
手順
1.キーキャップを取る
2 キースイッチを取る
3 ばねとばねについているパーツを取る
4 新しいキースイッチを分解する
5 新しいキースイッチのばねとパーツをつけ、カバーをする
おわりに
100均のクリップを改造したものでは1時間かかってもキースイッチが取れなかったが、器具を買ってやったら1分でできたので素直に器具を買うことをおすすめします・・・
targetSdkVersion 26でAndroid 2.3をサポートすることのコストの高さ
今年の11月以降、Google PlayにはtargetSdkVersion26
(Android 8.0)以上のアプリしかアップロードできなくなる(新規アプリは8月)。
それでも状況によってはAndroid 2.3をサポートしなければならない場合がある・・・
しかし、古いAndroidをサポートするためのサポートライブラリは基本的には targetSdkVersion
のバージョンと合わせなければならず、 26.0.0からminSdkVersion
が14(Android 4.0)になってしまった・・・
それでもAndroid 2.3 をサポートする方法
- サポートライブラリを使わない
- サポートライブラリ 25.4.0を使う
- Android 2.3向けとAndroid 4.0以上向けでビルド分け、2.3向けではサポートライブラリ 25.4.0, 4.0以上向けでは最新のサポートライブラリを使う
の3つが考えられるがどれも非常にコストがかかる。
まず1つ目のサポートライブラリを使わない方法。これはGoogleがサポートライブラリで吸収してくれるはずのAPI差分やバグ修正などの恩恵を全て受けられないことになる。OSバージョンをカバーするテスト工数がかさむし、予期せぬバグが発生することが多々起きることが想像できる・・・(そしてバグ修正もつらそう・・・)
2つ目はサポートライブラリ 25.4.0を使う。これがコスト的には一番ましかもしれない。ただ Googleが推奨するように targetSdkVersion
のバージョンと合わないため、こちらでも予期せぬバグが起きうる。個人的には、1つ目よりはまだましなのではないかと信じたい。
3つ目が1番安全で、Android 2.3向けとAndroid 4.0以上向けでビルド分け、サポートライブラリのバージョンを分ける方法。これはバグが1番起きにくいし、全バージョンをサポートライブラリで網羅できる。ただし、サポートライブラリ 26.0.0以上から入るAPI(例えば Android 8.0からの通知チャンネル用コンストラクタ NotificationCompat.Builder(Context context. String title)
など) を使うとコードを分けなければならない。終わりゆくAndroid 2.3をサポートするためにそのようなコードを書かなければならないのはかなりつらいし、ビルドが2つになって管理・テストもつらい。
NotificationCompat.Builder | Android Developers
もうAndroid 2.3をサポートするのはやめよう
もはやAndroid 2.3をサポートする時代ではないと個人的には思う。
Android 2.3は0.3%しかユーザがおらず、そのユーザをサポートして得られる利益が上記のような対策をとるコストを超える場合はないのではと思う(BtoBtoCで包括的な契約だと話は変わってきそう)。
Googleの各種ライブラリはAndroid 4.0, 4.1以上が当たり前だし、クックパッドは minSdkVersionを21(Android 5.0)にしている。
minSdkVersion 21で開発していきたい・・・!
おわりに
スマートニュースは Android 2.2をサポートしているらしいがどうやってやっているのだろう・・・
【Android】リリース署名の設定をプロジェクト外から上書きする
GitHubでソースを公開していてリリース署名の設定をしたい場合、公開用にはダミーの署名を置き、手元ではそれを上書きしたい。かつ git status
でnot stagedのところに上書き内容がいちいち出ないようにプロジェクト外から上書きしたい。
gradle.properties
は プロジェクト直下/gradle.properties
が先に読まれ、次に ~/.gradle/gradle.properties
が読み込まれるためプロジェクト外から値を上書きできる。
app/build.gradle
変数を囲むのはダブルクオーテーションじゃないと動かない。
android { .... signingConfigs { release { keyAlias "$RELEASE_KEY_ALIAS" storeFile file("$RELEASE_STORE_FILE_PATH") keyPassword "$RELEASE_KEY_PASSWORD" storePassword "$RELEASE_STORE_PASSWORD" } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt' signingConfig signingConfigs.release } } ... }
プロジェクト直下のgradle.properties
dummy-release-keyはapp以下に置く
RELEASE_KEY_ALIAS=key RELEASE_STORE_FILE_PATH=dummy-release-key RELEASE_KEY_PASSWORD=testtest RELEASE_STORE_PASSWORD=testtest
そして ~/.gradle/gradle.properties
に本当のリリース署名の設定を書く。
【Android】Toolbarの矢印をタップしたら画面を閉じる
ポイントはonOptionsItemSelected()
でandroid.R.id.home
をハンドリングしないと反応してくれない点
class MainActivity: AppCompatActivity { ... override fun onCreateOptionsMenu(menu: Menu): Boolean { // Inflate menu resource file. menuInflater.inflate(R.menu.main, menu) return true } override fun onOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { // For arrow button on toolbar android.R.id.home -> finish() } return super.onOptionsItemSelected(item) } override fun initToolbar() { val toolbar = findViewById(R.id.toolbar) as Toolbar setSupportActionBar(toolbar) val actionBar = supportActionBar if (actionBar != null) { // Show back arrow icon actionBar.setDisplayHomeAsUpEnabled(true) actionBar.setDisplayShowHomeEnabled(true) actionBar.setTitle(R.string.app_name) } } }
RxJavaのflatMap(mapper, combine)でリストデータをそれぞれ別スレッドで非同期処理する
例えばRSSのリストがあり、それを別々のスレッドで処理して全部の処理が終わったらUIを更新したいとする。
RxJavaのflatMap(mapper, combine)
を使えばリストのデータを1つ1つのObservable/Flowable
に変換して処理できる。
まず第一引数のmapper
部分でそれぞれのデータを受け取ってObservable/Flowable
を作る。
このときsubscribeOn(Schedulers.io())
などでスレッドを分けておかないと全て同じワーカースレッドで実行されてしまうので注意。
ArrayList<RSS> rssList = ... Flowable.fromIterable(rssList) .subscribeOn(Schedulers.io()) .flatMap(new Function<RSS, Publisher<? extends RSS>>() { @Override public Publisher<? extends RSS> apply(RSS rss) throws Exception { return Flowable.just(rss).subscribeOn(Schedulers.io()); } },
次に第二引数のcombine
部分で更新処理を行う
Flowable.fromIterable(rssList) .subscribeOn(Schedulers.io()) .flatMap(new Function<RSS, Publisher<? extends RSS>>() { ... }, new BiFunction<RSS, RSS, RSS>() { @Override public RSS apply(RSS rss, RSS rss2) throws Exception { Log.d("test", "BiFunction, Thread:" + Thread.currentThread().getName() + ", rss:" + rss2.title()); // RSSの更新処理 ... return rss2; } })
あとはUIスレッドで結果を受け取って処理をすればよい。
ArrayList<RSS> rssList = ... Flowable.fromIterable(rssList) .subscribeOn(Schedulers.io()) .flatMap(new Function<RSS, Publisher<? extends RSS>>() { ... }, new BiFunction<RSS, RSS, RSS>() { ... }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<RSS>() { @Override public void onSubscribe(Subscription s) { s.request(rssList.size()); } @Override public void onNext(RSS rss) { // 進捗更新 } @Override public void onError(Throwable t) { } @Override public void onComplete() { // 終了処理 } });
全体のコード
Java(ラムダなし)
ArrayList<RSS> rssList = ... Flowable.fromIterable(rssList) .subscribeOn(Schedulers.io()) .filter(new Predicate<RSS>() { @Override public boolean test(RSS rss) throws Exception { return rss.id() > 0; } }) .flatMap(new Function<RSS, Publisher<? extends RSS>>() { @Override public Publisher<? extends RSS> apply(RSS rss) throws Exception { return Flowable.just(rss).subscribeOn(Schedulers.io()); } }, new BiFunction<RSS, RSS, RSS>() { @Override public RSS apply(RSS rss, RSS rss2) throws Exception { Log.d("test", "BiFunction, Thread:" + Thread.currentThread().getName() + ", rss:" + rss2.title()); // RSSの更新処理 ... return rss2; } }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<RSS>() { @Override public void onSubscribe(Subscription s) { s.request(rssList.size()); } @Override public void onNext(RSS rss) { // 進捗更新 } @Override public void onError(Throwable t) { } @Override public void onComplete() { // 終了処理 } });
Java(ラムダあり)
ArrayList<RSS> rssList = ... Flowable.fromIterable(rssList) .subscribeOn(Schedulers.io()) .flatMap(rss -> Flowable.just(rss).subscribeOn(Schedulers.io()), (rss, rss2) -> { Log.d("test", "BiFunction, Thread:" + Thread.currentThread().getName() + ", rss:" + rss2.title()); // RSSの更新処理 ... return rss2; }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(new Subscriber<RSS>() { @Override public void onSubscribe(Subscription s) { s.request(rssList.size()); } @Override public void onNext(RSS rss) { // 進捗更新 } @Override public void onError(Throwable t) { } @Override public void onComplete() { // 終了処理 } });
Kotlin
Flowable.fromIterable<RSS>(rsss) .subscribeOn(Schedulers.io()) .filter { rss -> rss.id > 0 } .flatMap({ data -> Flowable.just(data).subscribeOn(Schedulers.io()) }) { _, rss2 -> Log.d("test", "BiFunction, Thread:" + Thread.currentThread().name + ", rss:" + rss2.title) // RSSの更新処理 ... rss2 } .observeOn(AndroidSchedulers.mainThread()) .subscribe(object: Subscriber<RSS> { override fun onSubscribe(s: Subscription?) { s!!.request(rssList.size.toLong()) } override fun onNext(t: RSS?) { // 進捗更新 } override fun onError(t: Throwable?) { } override fun onComplete() { // 終了処理 } })