20200529のAndroidに関する記事は8件です。

AndroidStudio 3.6.3 インストール

はじめに

AndroidStudio 3.6.3をインストールするところまで確認したいと思います。

環境と前提条件

  • windows10(64bit)
  • メモリ:8GB

当初はメモリ4GBでAndroidStudioをいじっていましたが、エミュレーター起動時に厳しさを感じたのでメモリを増設して8GBにしました。故に、メモリは8GB以上がよろしいかと思います。

AndroidStudioをダウンロードする

こちらのページを参考に進めていきます。

上記リンクを開いたら、赤枠のリンクを押下します。
2020-05-29 (10).png

赤枠に対象のバージョンと対象OSが記載されています。今回はこちらで問題ありません。
そのまま、DOWNLOAD ANDROID STUDIOを押下します。
2020-05-29 (11).png

同意にチェックして、ダウンロードを押下します。
2020-05-29 (12).png

AndroidStudioをインストールする

ダウンロードが完了したらandroid-studio-ide...を起動します。
2020-05-29 (22).PNG

SetUpウィンドウが開かれました。Nextを押下して進めていきます。
2020-05-29 (4).png

AVDをインストールするか否かを決めます。エミュレータを利用する際に必要になるのでチェックしておきます。
2020-05-29 (5).png

インストールする場所を決めます。デフォルトのまま進めます。
2020-05-29 (6).png

スタートメニューにショートカットを作るか否かと、その名前を決めます。デフォルトのまま進めます。
Installを押下してインストールを進めます。
2020-05-29 (7).png

インストールが完了したらFinishを押下します。
また、Start Android Studioにはチェックを入れておきます。
2020-05-29 (8).png

初期設定

続けて初期設定を行います。

AndroidStudioを起動すると、下記ウィンドウが開かれます。
Nextを押下して設定を進めます。
2020-05-29 (9).png

インストールタイプを選びます。今回はStandardを選びます。
2020-05-29 (15).png

UIを選べるようです。黒にしました。
2020-05-29 (16).png

設定情報を確認します。熟読する必要はないと思いますが、念のため情報はメモしておきます。
Finishを押下すると必要なファイルのダウンロードが始まります。
2020-05-29 (17).png

ダウンロードされるまでしばし待ちます。
2020-05-29 (18).png

ダウンロードが終わりました。
Finishを押下します。
2020-05-29 (19).png

改めて、AndroidStudioを起動します。
このようなウィンドウが開けばインストール成功です。
2020-05-29 (21).png

さいごに

AndroidStudio 3.6.3をインストールできたことを確認しました。
このインストール内容で一先ずHelloWorldまでは確認できました。→こちら

参考にしたサイト

https://developer.android.com/studio/install?hl=ja

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

Layout Validation を使いこなせ

Layout Validation とは

Android Studio 4.0 から使えるようになった、様々な画面サイズやデバイスの構成におけるレイアウトを同時にプレビューするためのツールです。

画面サイズだけでなく、Dark Theme や色覚多様性、多言語、テキストサイズの変更までも確認することができます。

使い方

Android Studio 4.0 以降でレイアウトのファイルを開くと IDE に Layout Validation のボタンが表示されます。

layout-validation-tab.png

様々な画面サイズの確認

左上のドロップダウンより Pixel Devices を選択することで様々な Pixel 端末の画面サイズでのレイアウトを確認することができます。

Pixel Devices screen

色覚多様性の見え方の確認

左上のドロップダウンより Color Blind を選択することで色覚多様性の見え方を確認することができます。

Color Blind screen

フォントサイズの変更の確認

左上のドロップダウンより Font Sizes を選択することでテキストサイズが変更された場合の見え方を確認できます。

Font Sizes screen

Dark Theme の確認

左上のドロップダウンより Custom を選択するとドロップダウンの右に端末のアイコンが表示されます。
その端末のアイコンから表示されるポップアップで設定した端末の状態を追加することで様々なレイアウトの確認をすることができます。

Dark Theme の場合は

  • Theme に .DayNight
  • Night Mode に Night time

を設定することで Dark Theme のレイアウトの確認ができます。

Night Mode setting screen

そのほかにも横向きや多言語での確認も設定することができます。

これまで端末で設定を変更して確認していた部分が改善されて、レイアウトの確認が捗りますね?

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

KotlinでFragmentを使う(失敗)

KotlinでFragmentを使う

KotlinでFragmentを使ってみます。失敗っていうのは結果だけで途中の段階は重要な部分もあります。

前準備

Gsonを使いますのでbuild.gradle(app)に

build.gradle(app)
dependencies {
    省略
    implementation 'com.google.code.gson:gson:2.8.6'
}

gsonを追加します。 2.8.6の部分はバージョン番号です。

strings.xml
<?xml version="1.0" encoding="utf-8" ?>
<resources>
    <string name="app_name">FragmentSample</string>
    <string name="btSave">保存</string>
    <string name="btLoad">読込</string>
    <string name="tvStr">String型</string>
    <string name="tvInt">Int型</string>
    <string name="tvDouble">Double型</string>
</resources>

空のFragmmentを作ります。
プロジェクトツリーから右クリック→新規→フラグメント→フラグメント(空白)と選んでいきます。
フラグメントの名称を決める画面ではデフォルトのBlankFragmentとしました。
作ったフラグメントのデザインは

fragment_blank.xml
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
    android:id="@+id/frame"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".BlankFragment">

    <!-- TODO: Update blank fragment layout -->

    <LinearLayout
        android:id="@+id/llv"
        android:layout_width="204dp"
        android:layout_height="301dp"
        android:orientation="vertical">

        <LinearLayout
            android:id="@+id/llh"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="right"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tvStr"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/tvStr" />

            <EditText
                android:id="@+id/etStr"
                android:layout_width="144dp"
                android:layout_height="wrap_content"
                android:ems="10"
                android:inputType="text" />

        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="right"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tvInt"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/tvInt" />

            <EditText
                android:id="@+id/etInt"
                android:layout_width="144dp"
                android:layout_height="wrap_content"
                android:ems="10"
                android:inputType="number" />

        </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="right"
            android:orientation="horizontal">

            <TextView
                android:id="@+id/tvDouble"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/tvDouble" />

            <EditText
                android:id="@+id/etDouble"
                android:layout_width="144dp"
                android:layout_height="wrap_content"
                android:ems="10"
                android:inputType="numberDecimal" />
        </LinearLayout>

    </LinearLayout>

</FrameLayout>

としました。
今度はMainActivityの方を作っていきます。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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"
    android:id="@+id/clMain"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:id="@+id/clTool"
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="buttonClick"
            android:text="Button"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent" />

    </androidx.constraintlayout.widget.ConstraintLayout>

    <LinearLayout
        android:id="@+id/llView"
        android:layout_width="wrap_content"
        android:layout_height="378dp"
        android:orientation="horizontal"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.231"
        app:layout_constraintStart_toStartOf="@+id/clTool"
        app:layout_constraintTop_toBottomOf="@+id/clTool">

        <fragment
            android:id="@+id/fragmentView"
            android:name="com.example.qiitafragmentsample.BlankFragment"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1" />

        <LinearLayout
            android:id="@+id/llEdit"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:orientation="vertical">

        </LinearLayout>

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

先ほど作ったFragmentをすでに設計段階で置くことが出来ます。
設計時に置いたFragmentに初期値を
そしてボタンを押したらFragmentを作るようにしてみます。

MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        val dStr = findViewById<EditText>(R.id.etStr)
        val dInt = findViewById<EditText>(R.id.etInt)
        val dDouble = findViewById<EditText>(R.id.etDouble)
        dStr.setText("Test")
        dInt.setText("1234")
        dDouble.setText("98.76")
    }

    fun buttonClick(view : View){
        val dStr = findViewById<EditText>(R.id.etStr)
        val dInt = findViewById<EditText>(R.id.etInt)
        val dDouble = findViewById<EditText>(R.id.etDouble)
        val bundle = Bundle()
        val data = gsonFile()
        data.dStr = dStr.text.toString()
        data.dInt = dInt.text.toString().toInt()
        data.dDouble = dDouble.text.toString().toDouble()
        data.backColor = Color.rgb(0xff, 0, 55)
        bundle.putString("gson", data.gsonString())

        val editFragment = BlankFragment()
        editFragment.setArguments(bundle)
        val fragmentManager = supportFragmentManager
        val fragmentTransaction = fragmentManager.beginTransaction()
        fragmentTransaction.replace(R.id.llEdit, editFragment)
        fragmentTransaction.commit()
    }
}

Fragmentに値を渡す

bundle.kt
        val bundle = Bundle()
        val data = gsonFile()
        data.dStr = dStr.text.toString()
        data.dInt = dInt.text.toString().toInt()
        data.dDouble = dDouble.text.toString().toDouble()
        data.backColor = Color.rgb(0xff, 0, 55)
        bundle.putString("gson", data.gsonString())

Fragmentには直接値を渡せないのでBundle()という仕組みを使います。
BundleにputString(キー,値) などして値を入れておくと受け取った側のFragmentでgetString(キー)とすれば値を受け取ることが出来ます。
色々な値があるFragmentにいちいち get putしてられないので独自にGsonを使って処理を簡単にしています。
今回作ったFragmentには 文字列型のdStr、数値型の dInt、浮動小数点型の dDouble あと確認のためbackColor という数値(色を管理させている)も用意しています。

replace.kt
fragmentTransaction.replace(R.id.llEdit, editFragment)

LinearLayoutに付けたID llEdit を今回作ったFragmentに置き換えています。他にaddとかありますが使わないと思います。

今度はFragment側で受け取ってみます。

BlankFragment.kt
class BlankFragment : Fragment() {
    private val data = gsonFile()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val str : String = arguments?.getString("gson") ?:return
        Log.d("Sample","Fragment str=$str")
        data.fromData(str)
        val dStr : EditText? = activity?.findViewById<EditText>(R.id.etStr)
        val dInt : EditText? = activity?.findViewById<EditText>(R.id.etInt)
        val dDouble : EditText? = activity?.findViewById<EditText>(R.id.etDouble)
        Log.d("Sample","dStr = ${data.dStr} dInt = ${data.dInt} dDouble = ${data.dDouble}")

        dStr?.setText(data.dStr)
        dInt?.setText(data.dInt.toString())
        dDouble?.setText(data.dDouble.toString())
        dStr?.setTextColor(data.backColor)
    }

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.fragment_blank, container, false)
    }
}

Fragmentを作ったときに出来た宣言にいくつか追加します。

Gsonを管理するgsonFileクラスは後で説明します。

Fragment側で値を受け取る

gson.kt
val str : String = arguments?.getString("gson") ?:return

Fragmentには直接値を渡すことが出来ず arguments経由で渡されるのでそこからgson形式の文字列を取り出します。設計時には取り出すことが出来ずstrがnullになるのでその場合は処理をしないようにしています。
null許容型にしていても if(str == null)とかで判断出来ないので注意です。

クラスにデータを管理させる

独自のgsonDataクラスを宣言してデータを管理しています。
管理したいデータが他にもあれば宣言を追加して、初期化して、代入命令 assignに代入文を追加します。

gsonData.kt
open class gsonData(){
    var dStr : String = ""
    var dInt : Int = 0
    var dDouble : Double = 0.0
    var backColor : Int = android.R.color.background_light

    fun assign(a : gsonData){
        dStr = a.dStr
        dInt = a.dInt
        dDouble = a.dDouble
        backColor = a.backColor

    }
    fun gsonString() : String{
        return Gson().toJson(this)
    }
}

先ほど宣言したデータを管理するクラスにデータ→Gson、Gson→データなど命令を追加します。
自分自身にデータを渡せないのでデータ部分の宣言と処理部分で分けています。
gsonFileクラス内にgsonDataを定義しても良いのですがそれだと gsonFild.data.dIntと長くなるので継承にしました。他に良い方法あればご教授願います。

gsonFile.kt
class gsonFile() : gsonData(){
    fun fromData(str : String){
        if (str == "") return
        val data =  Gson().fromJson<gsonData>(str, gsonData::class.java) as gsonData
        super.assign(data)
    }
    fun fileSave(filename : String,str : String){
        File(SingletonContext.applicationContext().filesDir, filename).writer().use {
            it.write(str)
        }

    }
    fun fileLoad(filename : String) : String?{
        val readFile = File(SingletonContext.applicationContext().filesDir, filename)

        if(!readFile.exists()){
            Log.d("debug","No file exists")
            return null
        }
        else{
            return readFile.bufferedReader().use(BufferedReader::readText)
        }
    }
}

Contextを使い回す。

ファイルの読み書きにContextがいるのですが単なるデータクラスにあるわけもなく、このクラスから参照しています。

private class SingletonContext : Application() {
    init { instance = this }
    companion object {
        private var instance: SingletonContext? = null
        fun applicationContext() : Context {return instance!!.applicationContext}
    }
}

結果

最初に表示されている設計時Fragmentに入力した値はボタンを押すと新しいFragmentが作られそちらに渡されます・・・が、そうはなりません。
値は渡るもののval dStr : EditText? = activity?.findViewById(R.id.etStr)
で得られる R.id.etStr は設計時のとボタンクリックで作ったものが同じなのでなんと設計時の方が参照されます。

Fragmentの目的は
1.表示させる項目を動的に変化させる
2.画面サイズにより表示、非表示を切り替える
であり、同じFragmentを同時に画面に表示させる物では無いと思われます。

最後に

練習でFragmentを使って見ましたが、こうなりました。
あと重要なことですが、作ったFragmentへ値は渡せましたが値を返すことは出来ません。

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

AndroidStudio 3.6.3を日本語化する

はじめに

Android Studioを日本語化するところまで確認したいと思います。

環境と前提条件

  • windows10
  • Android Studio 3.6.3(64bit)

Android Studioがインストールされていることが前提です。
インストール方法はこちらを参考にしてみてください。

プラグインをダウンロードする

こちらからプラグインをダウンロードします。

リンクを開いたら、 Pleiades プラグイン・ダウンロードのwindowsボタンを押下します。
あと、7-zipで解凍云々と書かれていますね。公式に従い、インストールしていない場合は7-zipもインストールします。後ほど使います。
2020-05-28 (34).png

続いて、リンクを押下します。ダウンロードされるまでしばし待ちましょう。。。
2020-05-28 (36).png

ダウンロードが完了したら7-zipで解凍します。
(展開されたファイルはひとつのフォルダにまとめました。)
2020-05-28 (10).png

日本語化する

続いて、setup.exeを起動します。
2020-05-28 (35).png

日本語化するアプリケーションを選択します。
デフォルトであればC:\Program Files\Android\Android Studio\bin\studio64.exeです。
2020-05-28 (12).png

日本語化します。これはすぐに終わります。
2020-05-28 (14).png

次にAndroid Studioが日本語化されているか、確認します。
2020-05-29.png
できました!

さいごに

Android Studio 3.6.3を日本語化できることを確認しました。

参考にしたサイト

https://neet-rookie.hatenablog.com/entry/2019/04/27/104248

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

AndroidStudioで様々なボタンの動作を確認

AndroidStudioでボタンの動作を確認します

ボタンのクリックイベント、ボタンのチェック状態を確認するサンプルを作ってみます。

strings.xml
<?xml version="1.0" encoding="utf-8" ?>
<resources>
    <string name="app_name">ButtonSample</string>
    <string name="radio_Udon">うどん</string>
    <string name="radio_Soba">そば</string>
    <string name="check_Age">油揚げ</string>
    <string name="check_Tempura">天ぷら</string>
    <string name="btn_Pay">精算</string>
    <string name="tv_Pay">お支払い</string>
</resources>

うどん、そば屋さんで精算するアプリです。

activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 
    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"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <RadioGroup
        android:id="@+id/radioGroup"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content">

        <RadioButton
            android:id="@+id/radioUdon"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/radio_Udon" />

        <RadioButton
            android:id="@+id/radioSoba"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/radio_Soba" />

        <CheckBox
            android:id="@+id/checkAge"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:checked="false"
            android:text="@string/check_Age" />

        <CheckBox
            android:id="@+id/checkTempura"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/check_Tempura" />

        <Button
            android:id="@+id/btPay"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="8dp"
            android:layout_marginTop="8dp"
            android:onClick="onClick"
            android:text="@string/btn_Pay" />

        <TextView
            android:id="@+id/tvPay"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@string/tv_Pay" />
    </RadioGroup>

</androidx.constraintlayout.widget.ConstraintLayout>

RadioGroupにボタンを入れると勝手に整列するので本来の使い方とは違うと思いますが使いました。

MainActivity.kt
class MainActivity : AppCompatActivity() {

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

    fun onClick(view : View){
        val radioUdon = findViewById<RadioButton>(R.id.radioUdon)
        val radioSoba = findViewById<RadioButton>(R.id.radioSoba)
        val checkAge = findViewById<CheckBox>(R.id.checkAge)
        val checkTempura = findViewById<CheckBox>(R.id.checkTempura)
        val tvPay = findViewById<TextView>(R.id.tvPay)

        var sum : Int=0
        if (radioUdon.isChecked){sum += 350}
        if (radioSoba.isChecked){sum += 370}
        if (checkAge.isChecked){sum += 150}
        if (checkTempura.isChecked){sum +=200}
        tvPay.setText("合計:$sum 円です")
    }
}

値段は適当です。正常に動くか確認してみましょう。
radioButton、checkButtonともにisCheckedを調べるとボタンのチェック状態がわかります。このプロパティに代入すると表示も変化します。

1つのイベントを複数のボタンから利用する

毎回精算ボタンを押すのは面倒なのでradioButton、checkButtonがクリックされても計算するようにしましょう。
radioButton、checkButtonを全て選択した状態でonClickの値を設定すれば全てに反映されます。
もう精算ボタンは不要なので消してしまいましょう。

イベント発生元を調べる

複数のボタンから同じイベントを呼ぶ場合、呼ばれた側がどのボタンから呼ばれたのか知りたい場合があります。

その場合はイベントの引数として渡されるView型の viewを利用するようです。

view.kt
    fun onClick(view : View){
        val radioUdon = findViewById<RadioButton>(R.id.radioUdon)
        val radioSoba = findViewById<RadioButton>(R.id.radioSoba)
        val checkAge = findViewById<CheckBox>(R.id.checkAge)
        val checkTempura = findViewById<CheckBox>(R.id.checkTempura)
        val tvPay = findViewById<TextView>(R.id.tvPay)

        var sum : Int=0
        if (radioUdon.isChecked){sum += 350}
        if (radioSoba.isChecked){sum += 370}
        if (checkAge.isChecked){sum += 150}
        if (checkTempura.isChecked){sum +=200}
        tvPay.setText("合計:$sum 円です")
        when(view.id) {
            radioUdon.id    -> { Log.d("Sample","うどんが押された") }
            radioSoba.id    -> { Log.d("Sample","そばが押された")   }
            checkAge.id     -> { Log.d("Sample","油揚げが押された") }
            checkTempura.id -> { Log.d("Sample","天ぷらが押された") }
        }
    }

このように viewのidとレイアウト上のidをfindViewByIdで変換した idを比較することで、どこから来たのか判明します。

最後に

今回使用したonClick(view : View)のイベント受け取り方法は引数が view : View のものに限定されるようです。
それ以外はリスナーを使うのですが全てのリスナーを説明するための情報がまだ不十分なので全部揃ったら説明というかメモします。

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

Android Studioへの画像リソースの追加が劇的に楽になってたので紹介します

これまでのやり方

これ僕だけなのかもしれないのですが、今までは下記のようなサイズ別の画像を
Screen Shot 2020-05-29 at 9.54.00.png
下記のリソースフォルダへ一つ一つコピペしておりました。
Screen Shot 2020-05-29 at 9.54.55.png
これが地味に面倒で、コピー対象にはldpiがなかったり、v24などがあったり、ちゃんと間違いのないコピペができたかを毎度チェックしていました。今回のように一つのリソースならまだしも、大きめの改修でリソースの数が多いと、この作業だけで結構気力を奪われます。

これからのやり方

Android Studio 3.4からは Resource Manager という機能が追加されました。下記画面の項目から開くことができます。
Screen_Shot_2020-05-29_at_10_05_32.png

Resource ManagerDrawableColorLayoutMip MapStringなどのリソースをGUIで表示することができます。画像以外のDrawableリソースもプレビューされるのでわざわざファイルを開いて確認する手間が省けてこれだけでも便利です。

さて、それでは本題の画像の追加方法についてですが、まず下記のようにDrawableタブを選択し、+ をクリックします。

Screen_Shot_2020-05-29_at_10_17_59.png

Import Drawables を選びます。

Screen_Shot_2020-05-29_at_10_20_10.png

別ウィンドウが開きファイルの選択を求められるので、フォルダごと選択して Openを押すと、

Screen_Shot_2020-05-29_at_11_48_36.png

なんということでしょう!インポートする画像がそれぞれのフォルダへ自動で選別されプレビューされました!

Screen_Shot_2020-05-29_at_11_52_13.png

あとは Next -> Import -> Addと選択するだけで簡単画像リソースを追加することができます!

あとがき

2012年からAndroid 開発に携わってきましたが、最初はEclipseでした。それがAndroid Studioに変わり今でも進化を続けてどんどん便利になっています。開発もどんどん楽になるので嬉しいですね!

参考

https://stackoverflow.com/questions/28503229/fast-ways-to-import-drawables-in-android-studio/28503506

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

THETAプラグインでTensorFlow Liteのセグメンテーションをかける

はじめに

リコーのYuuki_Sです。
弊社ではRICOH THETAという全周囲360度撮れるカメラを出しています。
RICOH THETA VやTHETA Z1は、OSにAndroidを採用しており、Androidアプリを作る感覚でTHETAをカスタマイズすることもでき、そのカスタマイズ機能を「プラグイン」と呼んでいます。(詳細は本記事の末尾を参照)。


上述の通りTHETAは、カメラでありながらAndroid端末でもあるため、単体で撮影し機械学習の処理をかけて出力することが可能です。

以前、@KA-2さんがTHETAプラグインで連続フレームにTensorFlow Liteの物体認識をかける記事を掲載しましたが、今回はセマンティックセグメンテーションを実施する方法を記載しようと思います。

本記事を参考にすることで、セグメンテーション結果をライブプレビューに反映したり、
下図の様に人物と背景で異なる画像処理をおこなうことが可能になります。
Gif_Result.gif

セマンティックセグメンテーションとは

画像中のそれぞれのピクセルがどの様な属性(クラス)の物体に属しているか分類することを指します。
例えば、下図のように様々な物体が写った画像から、この領域のピクセルは人間、この領域は車といった具合に領域分割することです。
image.png
画像引用元:"Panoptic Segmentation" Alexander Kirillov et al.,CVPR, 2019

この様な分類処理は、昔から画像処理の分野でおこなわれていましたが、Deep Learningを用いることで飛躍的に性能が向上しました。
今では、iPhoneのポートレートモードや自動車の自動運転などでも利用されています。

TensorFlow Liteのセグメンテーション

TensorFlow Liteのページで紹介されているモデルの1つに、このセグメンテーション処理があります。
image.png

詳しくはTensorFlow Lite Segmentationのページを参照してください。
TensorFlow Liteでは、下記の21種類のクラス分けが可能です。
(background/aeroplane/bicycle/bird/boat/bottle/bus/car/cat/chair/cow/diningtable/dog/horse/motorbike/person/pottedplant/sheep/sofa/train/tv)
今回試した環境では、人間とTVのクラス分けが出来ることを確認しました。

THETA Plug-in SDKで動作環境構築

それでは、本題であるTHETA上でのセグメンテーション動作環境構築に入っていきます。
作業のベースとなるプロジェクトファイル一式は、@KA-2さんの以下記事で解説したものです。

THETAプラグインでライブプリビューを扱いやすくする

下準備

今回使用するセグメンテーションのサンプルコードは、AndroidXライブラリを用いてKotlinで書かれています。これらに対応させつつ、サンプルコードを導入しMainActivity.javaからセグメンテーション処理を利用できるようにします。

※KotlinもAndroidXも使用経験がなく、試行錯誤で対応した結果を記載しているため、冗長な部分があるかもしれません。

build.gradle(Module:app)の設定

*「apply」に2行追加。
*「testInstrumentationRunner」を変更。
*「aaptOptions」で、モデルファイルを圧縮しないように指定。
*「implementation」の定義を書き換え。
*「targetSdkVersion」等を変更。

build.gradle
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 29
    buildToolsVersion "29.0.2"
    省略
    defaultConfig {
          省略
        targetSdkVersion 29
    省略
        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }
    省略
    aaptOptions {
        noCompress "tflite"
    }
}

dependencies {
    implementation fileTree(include: ['*.jar'], dir: 'libs')
    implementation 'androidx.appcompat:appcompat:1.0.0'
    implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'androidx.test.ext:junit:1.1.1'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.0'
    implementation 'com.theta360:pluginlibrary:2.1.0'
    implementation 'org.nanohttpd:nanohttpd-webserver:2.3.1'

    implementation('org.tensorflow:tensorflow-lite:0.0.0-nightly') { changing = true }
    implementation('org.tensorflow:tensorflow-lite-gpu:0.0.0-nightly') { changing = true }
    implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.3.71'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.0'
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.0'
    implementation 'androidx.core:core-ktx:1.2.0'
    implementation "androidx.exifinterface:exifinterface:1.1.0"
}

build.gradle(Project:~)の設定

TensorFlow Lite側のコードがkotlinのため、以下を追加しました。

build.gradle
buildscript {
   省略
    dependencies {   
         省略
        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.0"
        省略
    }
}

学習済みモデルの配置

TensorFlow LiteのセグメンテーションのページにあるDownload stater modelのボタンを押して、学習済みモデルをダウンロードします。
記事執筆時点のファイル名は「deeplabv3_257_mv_gpu.tflite」です。

このファイルをassetsフォルダーに配置してください。
image.png

セグメンテーション処理クラスの追加

Android用のサンプルコードから、以下の3ファイルを取得します。

ファイル名 説明
ImageUtils.kt 画像の読み込みと操作をおこなうユーティリティ
ModelExecutionResult.kt セグメンテーション結果の構造を記載したクラス
ImageSegmentationModelExecutor.kt セグメンテーションの実処理が記述されているファイル

取得したファイルは、配置の仕方にあわせpackageの定義を書き換えてください。

以下は、ベースとしたプロジェクトの「~\app\src\main\java\com\theta360\extendedpreview」に3つのファイルを配置した場合の例です。この場合、packageの定義は3ファイル共に以下となります。また、元のpackage定義はコメントアウトしてください。

package com.theta360.extendedpreview
//package org.tensorflow.lite.examples.imagesegmentation

AndroidXへのリファクタリング

ここまで終えたら、プロジェクト全体をAndroidXへリファクタリングします。
ツールバーのRefactorからMingrate to AndroidXを選択します。
image.png

選択後、現状のプロジェクトの状態をZip形式でバックアップするか尋ねられますので、必要な場合はバックアップします。
その後、リファクタリングが必要な箇所の検索が始まり、終わるとリストが表示されますので、Do Refactorボタンをクリックします。
image.png

最後に、レイアウトファイルを編集します。
image.png
activity_main.xmlを開き、

<androidx.constraintlayout.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"

に書き換えます。

以上で、MainActivity.javaからセグメンテーション処理を利用できる様になります。

MainActivityからセグメンテーション処理を利用する

まずは、プレビュー画面にセグメンテーション結果を重ねて描画してみます。
処理の流れは下記になります。
プレビュー画像の取得→中央部分を正方形で切り抜き→セグメンテーション処理に流し込む→結果を受け取り重ねて描画

これをおこなうコードは次の通りです。

MainActivity.java
private byte[]      latestFrame_Result;
private ImageSegmentationModelExecutor imageSegmentationModel;

public void drawTFThread() {;
        new Thread(new Runnable() {
            @Override
            public void run() {
                Bitmap beforeBmp = null;
                android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
                imageSegmentationModel = new ImageSegmentationModelExecutor((Context)MainActivity.this, true);
                while (mFinished == false) {
                    byte[] jpegFrame = latestLvFrame;
                    if ( jpegFrame != null ) {
                        //JPEG -> Bitmap
                        BitmapFactory.Options options = new  BitmapFactory.Options();
                        options.inMutable = true;
                        Bitmap bitmap = BitmapFactory.decodeByteArray(jpegFrame, 0, jpegFrame.length);
                        Bitmap output = Bitmap.createBitmap(bitmap.getWidth(),bitmap.getHeight(), Bitmap.Config.ARGB_8888);
                        //crop
                        Bitmap cropBitmap = Bitmap.createBitmap(bitmap, (bitmap.getWidth() - bitmap.getHeight())/2, 0, bitmap.getHeight(), bitmap.getHeight(), null, true);
                        //TF処理
                        ModelExecutionResult result = imageSegmentationModel.execute(cropBitmap);
                        //結果の描画
                        Bitmap result_bmp = result.getBitmapResult();
                        Bitmap afterResizeBitmap = Bitmap.createScaledBitmap(result_bmp,cropBitmap.getWidth(),cropBitmap.getHeight(),true);
                        Canvas resultCanvas = new Canvas(output);
                        resultCanvas.drawBitmap(bitmap,0,0,null);
                        resultCanvas.drawBitmap(afterResizeBitmap,(bitmap.getWidth() - bitmap.getHeight())/2, 0,null);
                        //ライブビューへの反映
                        ByteArrayOutputStream baos = new ByteArrayOutputStream();
                        output.compress(Bitmap.CompressFormat.JPEG, 100, baos);
                        latestFrame_Result = baos.toByteArray();
                    } else {
                        try {
                            Thread.sleep(33);
                        } catch (InterruptedException e) {e.printStackTrace();}
                    }}}}).start();
}

事前処理、事後処理で少し長くなっていますが、セグメンテーション処理をおこなうのは下記の1行です。
cropBitmapという名前の切り抜いたBitmapを渡しています。

ModelExecutionResult result = imageSegmentationModel.execute(cropBitmap);

そして、セグメンテーション結果は以下でBitmapとして受け取ります。

Bitmap result_bmp = result.getBitmapResult();

この例では、セグメンテーション結果と元画像が重なった画像を取得していますが、セグメンテーション結果のマスクのみの取得も可能です。
結果の種類は、ModelExecutionResult.ktで定義されており、以下があります。

変数名 説明
bitmapResult 元画像とセグメンテーション結果が重なった画像
bitmapOriginal 元画像
bitmapMaskOnly セグメンテーション結果のマスクのみ画像
executionLog ログ情報
itemsFound セグメンテーションした物体の属性番号リスト

さて、処理するコードを書いたので、これを呼び出す記述とプレビューに反映する記述をおこないます。

MainActivity.java
   //処理を呼び出す記述
   protected void onResume() {
        省略
        drawTFThread();
    }   
   //プレビューに反映する処理(returnするBitmapを変更)
   private WebServer.Callback mWebServerCallback = new WebServer.Callback() {
   省略
   public byte[] getLatestFrame() {
            //return latestLvFrame;
            return latestFrame_Result;
        }};

以上を変更し実行すると下図のようにプレビューにセグメンテーション結果が描画されます。
この画像中では人間しか分類していない為、1種類しか色分けされていませんが、複数検出されれば、複数の色で描画されます。
Gif_Preview.gif
なお、現状ではフレームレートが1fps程度しか出ません。認識状態の確認には使えますが、コマ送りです。
(スマホでプラグイン動作のプレビューが見えるのは、やはり便利)

撮影画像に対するセグメンテーション&画像処理

せっかく人領域と背景領域がセグメンテーションにより分離できるようになったので、
これを撮影画像に対して反映し、簡単な処理をした画像を保存する仕組みを作ってみます。

具体的には撮影ボタンが押されたことをトリガとして、撮影画像のファイルパスを取得し、その画像に前述と同じセグメンテーション処理をおこないます。
そして、セグメンテーション結果から人物と背景領域に異なる画像処理をかけて保存します。

これらの処理はTakePictureTask.Callback内のonTakePictureに記述します。
(THETAの撮影画像利用に関しては右記の記事が参考になります。→画像処理を含むTHETAプラグインの実装方法【THETAプラグイン開発】

コードは以下の通りです。

MainActivity.java
   public static final String DCIM = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).getPath();

 public void onTakePicture(String fileUrl) {
            //撮影済み画像のファイルパス取得
            Matcher matcher = Pattern.compile("/\\d{3}RICOH.*").matcher(fileUrl);
            if (matcher.find()) {
                fileUrl = DCIM + matcher.group();
                Log.d(TAG,matcher.group());
            }
            //JPEG読み込み、Bitmap化
            BitmapFactory.Options options = new  BitmapFactory.Options();
            options.inMutable = true;
            File file = new File(fileUrl);
            Bitmap bitmap_input = null;
            try(InputStream inputStream = new FileInputStream(file)) {
                bitmap_input = BitmapFactory.decodeStream(inputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
            //crop
            Bitmap cropBitmap = Bitmap.createBitmap(bitmap_input, (bitmap_input.getWidth() - bitmap_input.getHeight())/2, 0, bitmap_input.getHeight(), bitmap_input.getHeight(), null, true);
            //TF処理
            ModelExecutionResult result = imageSegmentationModel.execute(cropBitmap);
            //結果の処理(マスク画像を抽出し、それをリサイズ)
            Bitmap result_bmp_mask = result.getBitmapMaskOnly();
            Bitmap afterResizeBitmap = Bitmap.createScaledBitmap(result_bmp_mask,cropBitmap.getWidth(),cropBitmap.getHeight(),true);
            //人物領域をマスク画像に基づいて抽出
            Bitmap output_Front = Bitmap.createBitmap(bitmap_input.getWidth(),bitmap_input.getHeight(), Bitmap.Config.ARGB_8888);
            Canvas resultCanvas_Front = new Canvas(output_Front);
            Paint paint = new Paint();
            resultCanvas_Front.drawBitmap(cropBitmap,(bitmap_input.getWidth() - bitmap_input.getHeight())/2, 0,paint);
            paint.setFilterBitmap(false);
            paint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN));
            resultCanvas_Front.drawBitmap(afterResizeBitmap,(bitmap_input.getWidth() - bitmap_input.getHeight())/2, 0,paint);
            paint.setXfermode(null);
            if (cropBitmap != null) {
                cropBitmap.recycle();
                cropBitmap = null;
            }
            if (afterResizeBitmap != null) {
                afterResizeBitmap.recycle();
                afterResizeBitmap = null;
            }
            if (result_bmp_mask != null) {
                result_bmp_mask.recycle();
                result_bmp_mask = null;
            }
            //背景領域を彩度0にする
            Bitmap  output_back = Bitmap.createBitmap(bitmap_input.getWidth(),bitmap_input.getHeight(), Bitmap.Config.RGB_565);
            Canvas resultCanvas_Back = new Canvas(output_back);
            Paint paint_ColorFilter = new Paint();
            ColorMatrix cm = new ColorMatrix();
            cm.setSaturation(0);
            ColorMatrixColorFilter f = new ColorMatrixColorFilter(cm);
            paint_ColorFilter.setColorFilter(f);
            resultCanvas_Back.drawBitmap(bitmap_input,0,0,paint_ColorFilter);
            //人物領域と背景領域を重ね合わせ
            Bitmap  output = Bitmap.createBitmap(bitmap_input.getWidth(),bitmap_input.getHeight(), Bitmap.Config.RGB_565);
            Canvas resultCanvas = new Canvas(output);
            resultCanvas.drawBitmap(output_back,0,0,null);
            resultCanvas.drawBitmap(output_Front,0,0,null);

            //画像保存処理開始
            try {
                FileOutputStream fos = null;
                String targetFileName = fileUrl.substring(fileUrl.length() - 11);
                targetFileName = targetFileName.substring(0,7);
                int addNum = Integer.parseInt(targetFileName) + 1;
                String outputFileName = fileUrl.substring(0,fileUrl.length() - 11) + String.format("%07d", addNum)+".JPG";
                fos = new FileOutputStream(outputFileName);
                output.compress(Bitmap.CompressFormat.JPEG, 100, fos);
                fos.close();
                String[] FolderUrls = new String[]{"DCIM/100RICOH"};
                notificationDatabaseUpdate(FolderUrls);
            } catch (Exception e) {
                Log.e("Error", "" + e.toString());
            }
            startPreview(mGetLiveViewTaskCallback, previewFormatNo);
        }

また、画像を保存するためにマニフェストファイルに外部ストレージアクセスを追記します。

AndroidManifest.xml
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

ポイントとしてはマスク画像を利用した合成にPorterDuffの機能を利用しています。
PorterDuffは、キャンバス上でBitmapの合成を可能にする機能です。
これによって、マスク画像と撮影画像に比較演算をおこない、撮影画像から人物領域を抜き出しています。

結果

撮影ボタンを押すと、下図のような画像が保存されます。
人物の領域だけ色が残っているのが分かるかと思います。
R0010228_resize.JPG

まとめ

今回はTensorFlow Liteのセグメンテーション処理をTHETA上でおこなう方法を紹介しました。
機械学習で撮影対象を認識し、対象ごとに異なる処理をかけることは最近のスマホアプリでもトレンドだと思いますが、カメラ単体で完結するのは珍しいと思いますので、ぜひ興味があればTHETAプラグイン、触ってみてください。

RICOH THETAプラグインパートナープログラムについて

THETAプラグインをご存じない方はこちらをご覧ください。
パートナープログラムへの登録方法はこちらにもまとめてあります。
QiitaのRICOH THETAプラグイン開発者コミュニティ TOPページ「About」に便利な記事リンク集もあります。
興味を持たれた方はTwitterのフォローとTHETAプラグイン開発コミュニティ(Slack)への参加もよろしくおねがいします。

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

楽天モバイルで自社回線かパートナー(KDDI)回線のどちらに接続しているか判定する

前置き

筆者はデジモノガジェットが好きなので、楽天モバイルのRakuten UN-LIMITを契約しています。
ご存知の方も多いと思いますが、楽天モバイルは後発のキャリアですので自社の回線のエリアが狭く、パートナー(KDDI)回線でエリアを補強しています。
自社回線は容量無制限でパートナー(KDDI)回線は5GBという制限があるので、リアルタイムでどちらの回線に接続しているか把握できれば便利だと思い調査しました。

構成

名前 バージョン
macOS Catalina 10.15.4
AndroidStudio 3.6.3
Kotlin 1.3.72
AVD(API) 使用しません
実機 SH-RM11

判定ロジック

世間では電波の周波数(バンド)を取得して判断していることが多いと思います。

バンド 事業者
楽天モバイル自社回線
18 パートナー(KDDI)回線

この記事では接続中の基地局の情報を取得して確認してみます。

テスト方法

楽天モバイル公式対応の端末(SH-RM11)で自社回線エリアとパートナー(KDDI)回線エリアの境界付近で検証します。
自宅が境界付近で自社回線に接続されたりパートナー(KDDI)回線に接続されたりしていまして、実験に適しています。

ソースコードの説明

MainActivity.kt
package com.devnokiyo.bandscope

import android.Manifest
import android.content.pm.PackageManager
import android.os.Bundle
import android.telephony.*
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat

class MainActivity : AppCompatActivity() {
    private lateinit var telephonyManager: TelephonyManager

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

        // Manifest.permission.ACCESS_COARSE_LOCATIONのパーミッションを取得して良いか確認します。
        // 〜〜 省略 〜〜

        // TelephonyManagerを取得します。
        telephonyManager = getSystemService(TELEPHONY_SERVICE) as TelephonyManager
    }

    public override fun onPause() {
        super.onPause()
        // リスナーを解除します。
        telephonyManager.listen(phoneStateListener, PhoneStateListener.LISTEN_NONE)
    }

    public override fun onResume() {
        super.onResume()
        // セルの情報が変わったり、電波状態が変更になったときのリスナーを登録します。
        telephonyManager.listen(
            phoneStateListener,
            PhoneStateListener.LISTEN_CELL_INFO or PhoneStateListener.LISTEN_SIGNAL_STRENGTHS
        )
    }

    public override fun onDestroy() {
        super.onDestroy()
    }

    /**
     * PhoneStateListenerを定義します。
     */
    private val phoneStateListener = object : PhoneStateListener() {
        /**
         * 接続中の基地局の情報が変更された時のイベントです。
         */
        override fun onCellInfoChanged(cellInfo: MutableList<CellInfo>?) {
            super.onCellInfoChanged(cellInfo)

            // SIMカードを抜いたときはnullになっていました。
            cellInfo ?: return
            showCellInfo(cellInfo)
        }

        /**
         * 電波強度が変化した時のイベントです。
         */
        override fun onSignalStrengthsChanged(signalStrength: SignalStrength?) {
            super.onSignalStrengthsChanged(signalStrength)
            // Manifest.permission.ACCESS_COARSE_LOCATIONのパーミッションを取得しているか判定します。
            // 〜〜 省略 〜〜

            showCellInfo(telephonyManager.allCellInfo)
        }

        private fun showCellInfo(cellInfos: List<CellInfo>) {
            // LTE(4G)の基地局のうち信号があるものを抽出します。
            val cellInfoLtes = cellInfos.filter { q -> q is CellInfoLte && q.isRegistered }
                .map { q -> q as CellInfoLte }

            // LTE(4G)の基地局情報をログに出力します。
            cellInfoLtes.forEach { q ->
                Log.d("TestApp", "Cell ID                 is ${q.cellIdentity.ci}")
                Log.d(
                    "TestApp",
                    "Mobile Network Operator is ${q.cellIdentity.mobileNetworkOperator}"
                )
                Log.d("TestApp", "Earfcn                  is ${q.cellIdentity.earfcn}")
                Log.d("TestApp", "Operator Alpha Short    is ${q.cellIdentity.operatorAlphaShort}")
            }
        }
    }
}

ログの出力内容

実機をUSBデバックしてサンプルアプリを実行してみます。
Cell IDは位置が割り出せると聞きますので、一部マスクさせて頂きますが、自社回線に接続されたりパートナー(KDDI)回線に接続されたりしていることが確認できました。

2020-05-09 00:30:20.823 10205-10205/com.devnokiyo.bandscope D/TestApp: Cell ID                 is 37*****1
2020-05-09 00:30:20.823 10205-10205/com.devnokiyo.bandscope D/TestApp: Mobile Network Operator is 44050
2020-05-09 00:30:20.823 10205-10205/com.devnokiyo.bandscope D/TestApp: Earfcn                  is 5900
2020-05-09 00:30:20.823 10205-10205/com.devnokiyo.bandscope D/TestApp: Operator Alpha Short    is KDDI

2020-05-09 00:32:17.033 10205-10205/com.devnokiyo.bandscope D/TestApp: Cell ID                 is 36*****3
2020-05-09 00:32:17.033 10205-10205/com.devnokiyo.bandscope D/TestApp: Mobile Network Operator is 44050
2020-05-09 00:32:17.033 10205-10205/com.devnokiyo.bandscope D/TestApp: Earfcn                  is 5900
2020-05-09 00:32:17.034 10205-10205/com.devnokiyo.bandscope D/TestApp: Operator Alpha Short    is KDDI

2020-05-09 00:32:44.043 10205-10205/com.devnokiyo.bandscope D/TestApp: Cell ID                 is 34*****6
2020-05-09 00:32:44.043 10205-10205/com.devnokiyo.bandscope D/TestApp: Mobile Network Operator is 44050
2020-05-09 00:32:44.043 10205-10205/com.devnokiyo.bandscope D/TestApp: Earfcn                  is 5900
2020-05-09 00:32:44.043 10205-10205/com.devnokiyo.bandscope D/TestApp: Operator Alpha Short    is KDDI


2020-05-09 00:35:43.779 12740-12740/com.devnokiyo.bandscope D/TestApp: Cell ID                 is 88*****4
2020-05-09 00:35:43.779 12740-12740/com.devnokiyo.bandscope D/TestApp: Mobile Network Operator is 44011
2020-05-09 00:35:43.779 12740-12740/com.devnokiyo.bandscope D/TestApp: Earfcn                  is 1500
2020-05-09 00:35:43.779 12740-12740/com.devnokiyo.bandscope D/TestApp: Operator Alpha Short    is Rakuten

2020-05-09 00:40:27.566 14476-14476/com.devnokiyo.bandscope D/TestApp: Cell ID                 is 88*****6
2020-05-09 00:40:27.567 14476-14476/com.devnokiyo.bandscope D/TestApp: Mobile Network Operator is 44011
2020-05-09 00:40:27.567 14476-14476/com.devnokiyo.bandscope D/TestApp: Earfcn                  is 1500
2020-05-09 00:40:27.567 14476-14476/com.devnokiyo.bandscope D/TestApp: Operator Alpha Short    is Rakuten

終わりに

本当はServiceで通知バーにリアルタイム表示するアプリを作成する予定でしたが、新しく買ったOPPOのスマホは通知バーにうまく表示できないようです。
先日ツイートをしてしまったくらい、すっかり開発意欲が萎えてしまいました:sob::sob::sob:

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