20200329のAndroidに関する記事は5件です。

消せないAndroidアプリをadbを使って強制アンインストール

前提

adbコマンドを実行できることが前提です。

また、開発者モードをOnにして、USBデバッグモードでPCに接続してください。

スマホに接続する

adb shellを実行して、スマホに接続します。

削除したいアプリのpackage名を調べる

am monitorを実行する。

am monitorは、起動するアプリをリアルタイムで把握するコマンドです。

HWPOT-Hはスマホの機種によって変わると思います。

HWPOT-H:/ $ am monitor
Monitoring activity manager...  available commands:
(q)uit: finish monitoring

am monitor を実行した後、アンインストールしたいアプリを起動することで、下のようになります。

HWPOT-H:/ $ am monitor
Monitoring activity manager...  available commands:
(q)uit: finish monitoring
** Activity starting: com.android.calculator2

今回は、最初から入っている計算アプリを消そうとしています。

この、com.android.calculator2というpackage名をどこかに記録しておいてください。

そして、qを押してからエンターをおすことで、am monitorを終了し、次のコマンドを入力できるようになります。

アプリをアンインストールする

pm uninstall com.android.calculator2を実行します。(com.android.calculator2は削除したいアプリのpackage名に置き換えてください)

そうすると、そのアプリをアンインストールすることができます。

備考

DELETE_FAILED_INTERNAL_ERROR1が発生する場合

pm uninstall -k --user 0 package名pm uninstall package名の代わりに使用する。

その他

hatenaでも投稿しています。
ぜひ、そちらも見てください

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Flutter】バグ解決: Bad state: Stream has already been listened to.

今回の件のGitHubはこちら

https://github.com/Tetsukick/flutter_BLoc/commit/76748e912be5896f8e998a5a50244fcb77d659b6

現象

StreamControllerを使ってBLoCパターンでstatement管理をしていたが、TabBarViewを追加して、
Tabを切り替えて、再度同じタブに復帰した際に画面の表示が崩れるようになりました。

その際のerrorがこちら。

Bad state: Stream has already been listened to.

解決策

StreamControllerではなく、BehaviorSubjectを使用することで解消

元のソースコード

エラー解消前
import 'dart:async';

class CounterBloc {
  // input
  final _actionController = StreamController<bool>();
  Sink<void> get changeCountAction => _actionController.sink;

  //output
  final _countController = StreamController<int>();
  Stream<int> get count => _countController.stream;

  int _count = 0;

  CounterBloc() {
    _actionController.stream.listen((isPlus) {
      if (isPlus) {
        _count++;
      } else {
        _count--;
      }
      _countController.sink.add(_count);
    });
  }

  void dispose() {
    _actionController.close();
    _countController.close();
  }
}
エラー解消後
import 'dart:async';
import 'package:rxdart/rxdart.dart';

class CounterBloc {
  // input
  final _actionController = BehaviorSubject<bool>();
  Sink<void> get changeCountAction => _actionController.sink;

  //output
  final _countController = BehaviorSubject<int>();
  Stream<int> get count => _countController.stream;

  int _count = 0;

  CounterBloc() {
    _actionController.stream.listen((isPlus) {
      if (isPlus) {
        _count++;
      } else {
        _count--;
      }
      _countController.sink.add(_count);
    });
  }

  void dispose() {
    _actionController.close();
    _countController.close();
  }
}

その他

勉強不足でStreamControllerBehaviorSubjectの違いをまだ理解できておりません。
ご存知の方いましたら、教えていただけますと幸甚です。

参考記事

https://stackoverflow.com/questions/51396769/flutter-bad-state-stream-has-already-been-listened-to

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

DataBindingを使っているChipをChipGroupに動的に追加/削除する

要件

  • アプリ全体ではDataBindingを使っているものとする
  • Chipの個数は可変であり、動的に増減させることが出来る
  • Chipには✕ボタンを表示し、押したら消えるようにする

実装

ライブラリの追加

Chipを使うためのライブラリを追加する。

app/build.gradle
dependencies {
    (中略)
    implementation 'com.google.android.material:material:1.1.0'
    (中略)
}

AppThemeの変更

Chipを使う際にAppComponentだとクラッシュするので暫定対応。
本記事ではTheme.MaterialComponents.Light.DarkActionBarを利用している。

styles.xml
<resources>

    <!-- Base application theme. -->
<!--    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">--> 
    <style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
        <!-- Customize your theme here. -->
        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
    </style>

</resources>

タグの定義

Tag.kt
data class Tag(
    val id: String,
    val name: String,
)

Chipのレイアウトを用意

view_tag_chip.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="tag"
            type="com.masaibar.chipsample.Tag" />
    </data>

    <com.google.android.material.chip.Chip
        style="@style/Widget.MaterialComponents.Chip.Entry"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:checkable="false"
        android:text="@{tag.name}"
        android:textAppearance="@style/TextAppearance.AppCompat.Small"
        tools:text="Tag" />

</layout>

Chipを追加する画面レイアウトを用意

activity_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <FrameLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="16dp"
        tools:context=".view.main.MainActivity">

        <com.google.android.material.chip.ChipGroup
            android:id="@+id/chip_group_tags"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            app:singleSelection="false" />
    </FrameLayout>
</layout>

動的に追加

ChipにもDataBindingを使いたいのでinflateしてタグ情報をバインドしている。
✕ボタンが押された際のイベントはsetOnCloseIconClickListenerで設定できる。
Chipが生成できたらChipGroupTagsのaddView()に渡してやると、ChipGroupの子要素に追加される。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = DataBindingUtil.setContentView(
                this,
                R.layout.activity_main
        )

        for (i in 0 until 20) {
            DataBindingUtil.inflate<ViewTagChipBinding>(
                    LayoutInflater.from(this),
                    R.layout.view_tag_chip,
                    null,
                    false
            ).apply {
                tag = Tag(
                        id = i,
                        name = UUID.randomUUID().toString().substring(0, Random.nextInt(10))
                )
                (root as? Chip)?.setOnCloseIconClickListener {
                    binding.chipGroupTags.removeView(root)
                }
            }.let {
                binding.chipGroupTags.addView(it.root)
            }
        }
    }
}

結果

ChipGroupのおかげでChipの大きさに応じて良しなにレイアウト、収まらない場合は改行してくれているのがわかる。
image.png

✕ボタンを押すとChipが消え、消えたところは良しなにレイアウトを詰めてくれる。
image.png

参考

https://stackoverflow.com/questions/40917521/android-data-binding-programmatically-instantiated-view
https://stackoverflow.com/questions/50494502/how-can-i-add-the-new-android-chips-dynamically-in-android/50823177

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Google Play ConsoleでのANRの調査

Google Play Consoleでは、アプリのクラッシュやANRの発生ログを確認することができます。
ANRは、クラッシュに比べると発生原因や再現手順を比較的追いにくい場合が多いです。

そこで、基本的なANRの調査方法についてまとめました。

ANRの基本的な知識

念のため、はじめにANRの基本についても書いておきました。
基本的な内容なので、読み飛ばしていただいでも結構です。

ANRとは

ANRは、Application Not Respondingの略です。
その名の通り、アプリが応答していないことをユーザーに伝えるための仕組みです。

ANRが発生すると、「アプリケーション応答なし」のダイアログが表示されます。
「ボタンをタップしたらフリーズした…:fearful:」といったときに、以下のように表示されるダイアログがANRのダイアログです。
無題.png

ANRの発生条件

ANRの発生条件は以下の2つです。

  • 入力イベント(キーの押下や画面タッチなどのイベント)に対する応答が 5 秒以内にない
  • BroadcastReceiverの実行が 10 秒以内に終了しない

ANRの発生要因

ANRは、UIスレッドで実行時間の長い処理を行って処理をブロックしてしまうことが要因で起こります。

UIスレッドは、画面の描画などGUI関連の操作に使用されるスレッドですが、タッチイベントなどユーザーの操作に対してフィードバックを与えるようなイベント処理もUIスレッドで行われます。

このフィードバックが遅くなると、ユーザーから「なぜか反応しない。固まった。フリーズしたああああ:rage::rage:」など異常事態に感じられてしまいます。

なので、素早く処理をしてあげたいところですが、UIスレッドはスレッドなので、同時には1つの処理しかできません。

つまり、このUIスレッドでユーザーのタッチ反応とは別で、長い処理(ネットワーク通信やデータベース関連の処理など)を行ってしまうと、ユーザが画面をタッチしても、タッチのイベント処理がこの長い時間のかかる処理が終わるのを待ってから処理されてしまう...ことになります。

そのために、UIスレッドで実行されるメソッドでは、実行する処理をできる限り少なくする必要があります。

ANRの対策方法

以下の方法がANRの対策方法として一般的です。

  • 主なライフサイクルメソッド(onCreate()onResume()など)で設定する内容はできる限り少なくする
  • ネットワークやデータベースの処理や、CPUへの負荷が高い計算(ビットマップのサイズ変更など)は、ワーカースレッドで行う

※データベース処理の場合はワーカースレッドもしくは非同期リクエストを使用するでもいいと思います。

:point_up:UIスレッドで行う処理はなるべく短くして、時間のかかる処理は別スレッドで行う:point_up:
これがアプリのUXを向上させる&ANRを防ぐ鉄則だと思います。

Google Play consoleでのANRの調査方法

基本的な見方

Google Play console上に表示される、ANRのログの基本的な見方は以下の通りです。

  • 下から順番(降順)に追って見る
  • 黒字で書いてあるものは自分たちのコード(グレーは内部的な処理)
  • 最新のエラー発生versionを見て、最初に該当のANRが発生したversionCodeを確認する(既存なのか新規発生なのかがわかる)

例として、以下のようなログが表示された場合。
image.png

まず、黒字で書いてあるものは自分たちのコード(グレーは内部的な処理)なので、
この場合だと、InstalledApponCreate()からStartNotificationWorker.ktというクラスを呼んでいる所が問題の該当箇所になっている可能性が高いとわかります。

ANRを調査するときに意識すること

問題になっていそうな該箇所がわかったら、次にその箇所のコードを見て、以下のような観点で調査をしていきます。

  • 「黒字」で書かれた自分たちのコードがどういうタイミングで走るのか?
  • UIスレッドで行われているか?
  • 通信周りやDB操作は「非同期」で処理していない所があるか?
  • ログにたくさん出ていないか?(ログにたくさん出ているところは、処理がネストしていることが原因になっていることが多い)

自分が書いたコードの場所でANRが発生している例

例として、以下のような自分が書いたコード部分でANRが発生していそうなログだった場合を考えます。

at jp.co.hoge.view.ProgressAnimationLayout.<init> 
at jp.co.hoge.ui.base.BaseFragment.onCreateView (BaseFragment.java:126) 

まず、上記のログから、BaseFragmentonCreateView()でANRが発生していることがわかります。
onCreateView()が走っている場合は、Fragmentが生成されたタイミングになるので、UIスレッドの処理の最中だとわかります。

ここで、onCreateView()にそんなに時間がかかっていなければログには表示されないので、onCreateView()の中の処理に絞ることができます。

そして、ProgressAnimationLayout.<init>initとなっているのがポイントで、これは view の初期化タイミングを意味します。

そうなると、

FragmentonCreateView()の中で ProgressAnimationLayoutの初期化の中で10秒以上たった = 「Fragment の view の初期化処理が重い」

といった感じで仮説を考えることができます。

内部的な処理の場所でANRが発生している例

ANRを日々調査していると、以下のような黒字が全くなく、内部的なコードの中で発生しているANRもよくあります。
image.png

こういう場合も下から順に追って、怪しそうなやつを探します。

今回の場合だと、

at androidx.work.WorkManager.initialize<init> 

この辺りが怪しそうだなと思います。
以下のような流れで、WorkManegerの初期化に時間がかかってANRが発生してしまったかななど考えられます。

  1. アプリが死んでいる状態で、PlayService に登録していた WorkManager が立ち上がる
  2. Application 関係の初期化
  3. Worker の実行中に時間がかかってしまったパターン

見分け方としては、他のRunnableになっているスレッドをいくつか開いて調査します。

  • Runnableになっている Thread(Signal Catcher)をみるとAndroidのBootプロセス系が走っているところだった
  • Runnable になっている Thread(Queue)を見ると RemoteConfigManager.<init>が走っていて、held mutexes = mutator lock(shared held)と書いあった

したがって、

Remote Configの初期化に時間のかかった & mutax がロックされているのでどこかで共有プロセスがロックされて動けない状態が続いたことで10秒経過してANRが発生

といった感じで、内部的な処理の場合でも仮設を考えられます。

まとめ

ANRを調査するときは、以下のような事を意識して調査すると、問題の箇所が見つけやすい
というのが自分なりの見解です。

  • 「黒字」で書かれた自分たちのコードがどういうタイミングで走るのか?
  • UIスレッドで行われているか?
  • 通信周りやDB操作は「非同期」で処理していない所があるか?
  • ログにたくさん出ていないか?(ログにたくさん出ているところは、処理がネストしていることが原因になっていることが多い)

また、ANRが発生しないようにコードを書くことがそもそも大切なので、上記の内容はコードを書く際も気をつけるポイントになってくるかなとも思います。

あくまで今回は、Google Play Console上のログから、原因の調査をするところまでなので、
この後は、CPU Profilerなどを使用して、自分の仮説が正しいかさらに詳細な調査をしていく流れになっていくと思います。詳細はこちらの公式ページにわかりやすくまとまっています。
CPU Profiler を使用して CPU アクティビティを検査する

自分の経験からできあがった見解なので、間違っている所もあるかもしれません。
もしあればご指摘いただけると嬉しいです。
最後まで読んでいいただき、ありがとうございました。:bow:

参考文献

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Android Vulkan で画面回転の取り扱いのメモ

Vulkan では, いろいろ自前で処理コードをかかないといけません.

Android で, 画面が回転したり, 非アクティブになったりだと SwapChain の作り直しになります.

Android 側で回転させる

https://developer.android.com/ndk/guides/graphics/design-notes#apply

Android のコンポジターのほうで画面回転させて表示させる方法があります(筆者は試したことありません)

GPU 負荷がかかるとありますので, できれば自前で処理したほうがよさそうです.

回転を固定する

AndroidManifest.xml に screenOrientation 記述でとりあえず固定して, 回転を考えるのを止める手があります.

https://developer.android.com/guide/topics/manifest/activity-element
(orientation にもいろいろパターンあってめんどいですね..)

ただ, 条件によっては回転してしまったり, OS 側の機能かなにか無理やり回転させることはできるっぽいので, Google Play できちんとしたアプリ公開するなら回転対応は必須そうです.

How to force landscape or portrait mode in apps like Instagram and others (Android)
https://www.phonearena.com/news/How-to-force-landscape-or-portrait-mode-in-apps-like-Instagram-and-others-Android_id78439

how to force landscape mode with NDK using pure c++ codes
https://stackoverflow.com/questions/12702868/how-to-force-landscape-mode-with-ndk-using-pure-c-codes

注意点

<activity> のタグに android:screenOrientation を記載しないといけません!

筆者は上位の <application> タグに記載してしまい, 設定が反映されず 1/2 営業日を浪費してしまいました :cry:

回転の処理をいい感じに扱う

Handling Device Orientation Efficiently in Vulkan With Pre-Rotation
https://android-developers.googleblog.com/2020/02/handling-device-orientation-efficiently.html

めんどくさいですね...

画面が回転したかどうかの判定は, Android 10 だと vkQueuePresentKHR で取得できて楽です.
Android 9 or earlier だとめんどいです.

まとめ

画面がリサイズとかしたらいずれにせよ SwapChain 作り直しになりますので, SwapChain になるべく依存しないデータ構造にしておくのがベターですね.
(UniformBufferObject とか, swapChain の枚数ぶん Descriptor 作成が必要になったりする. 画面リサイズとかで swapChain の枚数が変わるというのは基本ないですが...)

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む