20210113のAndroidに関する記事は12件です。

【Android】入力方法を指定する方法

プログラミング勉強日記

2021年1月13日
EditTextには数字用、パスワード用といった複数の入力タイプなど様々な入力方法の指定があるのでその方法を簡単にまとめる。

キーボードのタイプを指定する方法

 キーボードのタイプを指定するためには、EditTextタグにandroid:inputType属性を使う。

指定する値 説明 
text 通常テキストを入力する
textUrl URLを入力する(通常のキーボードに/が追加される)
number 数値を入力する
phone 電話番号を入力する(携帯電話型のキーボードになる)
<!-- Eメールアドレスを入力させたい場合 -->
<EditText
    android:id="@+id/editText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:hint="@string/hint"
    android:inputType="textEmailAddress" />

キーボード入力時を指定する

 android:inputTypeでは、初めてキーボードで入力された文字はすべて大文字にする、文字を全てドットで隠すように表示するなど指定することができる。

指定する値 説明 
textCapSentences 先頭の文字のみを大文字にする
textCapWords 単語の先頭文字のみを大文字にする
textAutoCorrect 文字のスペルミスを自動で修正する
textMultiLine 複数行を入力する
<!-- 入力された文字をすべてドット表示にする -->
<EditText
    android:id="@+id/editText"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:hint="@string/password"
    android:inputType="textPassword"/>

参考文献

入力方法のタイプの指定

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

【Android入門】BottomNavigationを作る【初心者】

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

Nreal Light でキャラクター表示

はじめに

2020年12月1日に発売されたスマートグラス、NrealLight(エンリアルライト)
今回はそこにUnityを使用してキャラクターを表示するまでのことを書いていきます。
↓NrealLight越しに見たままキャプチャーする方法が分からないので、ちょっと見え方が違う…

NrealLightとは?


詳しくはこちらを見て頂けると…
https://news.kddi.com/kddi/corporate/newsrelease/2020/11/10/4778.html

大雑把に言うと、スマートフォンに接続してXR技術を楽しめるスマートグラスです。
ミラーリングモードを使えばYoutubeなどを空間上に映し出して見ることも可能なので電車などで周りを気にせず動画を楽しむことも出来ます。
ケーブルがあるのでちょっとあやしい感じに見えますが…(笑)
※ 対応しているスマートフォンには注意が必要です。

開発環境

・Windows10
・Unity2019.4.16f1
・Android SDK 8.0(APIレベル26)
・NRSDKForUnity_1.4.8
・NrealLight(Developer Kitではないものです)
・Xperia 5 II SOG02(Nreal Lightを使用するには、ビルド番号58.0.C.11.142以降が必須)

流れ

NrealLight DeveloperKitとUnityでの開発手順は公式サイトに書いてありますので
それを参考にしながら進めていきます。
https://developer.nreal.ai/develop/unity/android-quickstart

基本的な下準備

Android開発は初めてという人向けの簡単な説明になります。(Unityはある程度使える前提)
Unityのインストール、Android SDKのインストール、UnityのAndroidSDKパス設定、Android Debug Bridge(adb)コマンドのパスを設定、Androidの開発モード設定 に触れています。
そんなの必要ないという人も多いと思うので別記事での説明になります。
https://qiita.com/yambowcto/private/918f6eefd103d09f5e1b

Unityプロジェクト設定

・テンプレートを3Dにしてプロジェクトを新規作成します。
・メニューのFile > Build Settings で Build Settings ウィンドウを開きます。
・Platform で Android を選択し Switch Platform で変更します。
・同ウィンドウ内の Player Settings ボタンを押して Project Settings ウィンドウを開きます。
・Project Settings ウィンドウ内の Player 設定を変更します。

 ※ Target API Level は level 26~29 で設定する必要があり、Android 11は現時点で非対応。
   また、Android 10の場合はAndroidManifest.xmlを変更する必要があると書かれてます。
   キャプチャー機能等使わなければ変更しなくても大丈夫かな?

・Project Settings ウィンドウ内の Quality 設定を変更します。

 ※ Skin Weights はデータ次第ですが、大抵は4 Bonesにしてあげないと破綻します。

NRSDKForUnityパッケージのインポート

公式サイトからNRSDKForUnity_1.4.8をダウンロードして、ダブルクリックするなりしてインポートします。
https://developer.nreal.ai/download

ユニティーちゃんのインポート

Unity公式サイトからユニティーちゃんのモデルデータをダウンロードしてインポートします。
https://unity-chan.com/contents/guideline

もし、error CS0234: The type or namespace name 'Policy' does not exist in the namespace 'System.Security' (are you missing an assembly reference?)のようなエラーが出ていたら AutoBlink.csを開いて using System.Security.Policy;を削除するか(使用してないので)、Project Settings > Player > Other Settings > Api Compatibility Level* を .Net 4.xに変更すれば大丈夫です。

シーンの作成

メニューのFile > New Scene で新規シーンを作ります。
ProjectウィンドウのAssets > NRSDK > Prefabs にある NRCameraRigNRInput を Hierarchyに追加します。
Assets > UnityChan > Prefabs にある unitychan も Hierarchyに追加します。
Hierarchyにある Main Camera は必要ないので削除しましょう。
unitychanの座標は Position Y:-0.94 Z:2.56 Rotation Y:180としておきます。
名前を付けてシーンを保存しておきます。

ビルド準備

さて、実際に作ったアプリをスマートフォンにインストールする簡単な方法としては、スマートフォンをUSBケーブルでPCに繋いで Build And Run を実行することですが、今回はNreal Lightをスマートフォンに接続しないと動かないアプリなので作業のたびにケーブルの付け替えを行う必要が出てしまいます。なので、Wi-Fi経由でインストールできる準備をしましょう。

下準備のadbコマンドのためのパス設定をしていることを前提に進めます。
まずはandroidスマートフォンをUSBケーブルでPCに繋ぎます。
※ 開発者向けオプションのUSBデバッグをONにするのを忘れずに!
  また、スマートフォンに対して何かしようとすると許可の確認が結構でるので
  移行の作業で出た場合には内容を確認しつつ基本的にOKします。

コマンドプロンプトを起動し、adb tcpip 5555 を打ち込みEnterを押します。
スマートフォンのIPアドレスを確認(自分のだと、設定 > デバイス情報で見れます)してから
adb connect IPアドレスを打ち込みEnterを押します。
自分の場合Enter押した後に失敗と出ますが、スマートフォン側に許可の確認が出てるので許可をすると大丈夫です。
最後にスマートフォンとPCの接続を外して adb devicesと打ち、認識できていればOK。
2021-01-13_11h28_32.png

スマートフォンにNebulaをインストール

NrealLightアプリを動かすためにはNebulaのインストールが必要です。(違ったらゴメンナサイ)
なので、Google PlayからNebulaをインストールします。(↓のやつです、同名のアプリに注意)

nebula nreal で検索すると上の方にくると思います。
スマートフォンのソフトウェアのバージョンが条件を満たしていないとNreal Lightを接続したときのNebulaの挙動がおかしくなってしまうので注意してください。(自分はそこで詰まりました…)

Nebulaを起動してセットアップを行います。
言われるがまま許可をしながら進めていきます。すると最後にグラスを付けた時のチュートリアルに進みます。

ビルド

いよいよビルドです。
メニューのFile > BuildSettings でビルド設定を開きます。
Wi-Fi接続が上手くいっていれば、Run DeviceにIP名が入ったデバイスがあるはずです。なければRefreshで情報を更新します。
あとはBuild And Runボタンを押すとビルドが開始され、続いてインストールされます。
※ Nebulaが起動してなければPlease connect your Nreal Light Glasses.と出ます。
インストールしたアプリはNebulaのメニューから起動することができます。
これでキャラクターを表示することが出来ます。
操作方法はAPPボタン長押しでポインターリセット、ホームボタン長押しで終了になります。
以上、お疲れさまでした。

おまけ

ここからはシーンにもうちょっとだけ機能を加えてみたいと思います。
まず、HierarchyにCreateEmptyでGameObjectを作成し、その子供にunitychanを移動。
この時、GameObjectのTransformの値はリセットしておきます。
DemoにあるTargetModelDisplayCtrlを改良した以下のスクリプトをAddComponentでGameObjectに追加します。

TargetModelDisplayCtrlEx.cs
using NRKernal;
using UnityEngine;

namespace Test
{
    public class TargetModelDisplayCtrlEx : MonoBehaviour
    {
        public Transform modelTarget;

        private Vector3 m_AroundLocalAxis = Vector3.down;
        private float m_TouchScrollSpeed = 10000f;
        private float correctZ = 0.01f;
        private Vector2 m_PreviousPos;

        void Start()
        {
            ResetModel();
        }

        private void Update()
        {
            if (NRInput.GetButtonDown(ControllerButton.TRIGGER))
            {
                m_PreviousPos = NRInput.GetTouch();
            }
            else if (NRInput.GetButton(ControllerButton.TRIGGER))
            {
                UpdateScroll();
            }
            else if (NRInput.GetButtonUp(ControllerButton.TRIGGER))
            {
                m_PreviousPos = Vector2.zero;
            }
        }

        private void UpdateScroll()
        {
            if (m_PreviousPos == Vector2.zero)
                return;
            Vector2 deltaMove = NRInput.GetTouch() - m_PreviousPos;
            m_PreviousPos = NRInput.GetTouch();
            float x = Mathf.Abs(deltaMove.x);
            float y = Mathf.Abs(deltaMove.y);
            if (x > y)
                modelTarget.Rotate(m_AroundLocalAxis, deltaMove.x * m_TouchScrollSpeed * Time.deltaTime, Space.Self);
            else
            {
                Vector3 v = modelTarget.localPosition;
                v.z += deltaMove.y * m_TouchScrollSpeed * Time.deltaTime * correctZ;
                modelTarget.localPosition = v;
            }
        }

        public void ResetModel()
        {
            modelTarget.localRotation = Quaternion.Euler(0.0f, 180.0f, 0.0f);
        }
    }
}

InspectorでそのコンポーネントのModel Targetにunitychanを設定します。
これにより、アプリ中の操作で上下にスライドするとキャラクターが前後に移動、左右にスライドするとY軸回転を行うようになります。

おわりに

説明があまり上手くないかもしれませんが、いかがでしたか?
物凄く大雑把に言うと、動作環境を満たすNebulaを入れる適切なプロジェクト設定NRSDKForUnityを使う というところだけ注意すればOKだと思います。

さて、2021年に入って新たなスマートグラスの情報なども出てきています。
値段がもっとお手軽になって、街中でスマートグラスをしてても違和感がない時代が来るといいですね。

ユニティちゃんライセンスについて

ユニティちゃんライセンス
この作品はユニティちゃんライセンス条項の元に提供されています
ユニティちゃんを使用する際は、上記ライセンスをよく読んで使用しましょう。

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

Nreal Lightでキャラクター表示(準備編)

はじめに

ここではNrealLightでキャラクターを表示する下準備として、Unityを使ったAndroid開発のためのセットアップについて説明を行います。Unityに関してはある程度は使える前提で進めます。
本編はこちらです。
https://qiita.com/yambowcto/private/a973252e25cb9e5d44d7

Unityのインストール

Nreal Developer の説明にあるように Unity2019.4 LTS をインストールします。
ここでは2019.4.16f1をインストールしました。
また、インストール時には Android Build Support にチェックを入れて下さい。
Android SDK & NDK Tools と OpenJDK にもチェックが入っていると次に説明するAndroid SDK をインストールしなくても自動的にインストールされパスも設定してくれます。
ただし、指定したいTarget API LevelのSDKバージョンが入っていない場合は別途必要になるので、次に説明するAndroid Studioを入れた方が自分的には分かりやすいと思ってます。

Android SDKをインストール

まずは公式サイトからAndroid Studioをダウンロードします。
https://developer.android.com/studio?hl=ja
基本的にはデフォルトのままで進めていけばOKですが、Android SDKの場所がちょっと深いので分かりやすい場所にした方がいいかもしれません。(ここではデフォルトのままでの説明となります)

Android Studioのインストールが終わったら起動して、右下辺りにある Configure > SDK Manager を選択します。
Android SDK の必要なバージョンにチェックを入れてインストールします。
※ NrealLightの開発にはAndroid SDK 8.0(APIレベル26)以上が必要です。

UnityのAndroidSDKパス設定

メニューのEditからPreferencesを選択、ExternalToolsのAndroid SDK Tools Installed with Unityのパスを変更します。

Android Debug Bridge(adb)コマンドのパスを設定

adbコマンドを使用するためのパスの設定を行います。
これは必須というわけではないですが、NrealLightをスマートフォンに接続したままUnityで作成したapkファイルをWi-fiでインストール出来るので便利です。

まず、コントロールパネルのシステムを開きます。(ショートカット:Windowsキー + Pause)
次にシステムの詳細設定を開いて環境変数ウィンドウを表示します。

Pathを選択し、編集、新規でパスを追加します。
追加するパスはAndroidSDKのパスにplatform-toolsを付け加えたものになります。

コマンドプロンプトからadbと入力して決定、エラーが出なければOKです。
コマンドプロンプトはWindows10の場合は検索ボックスでcmdと打つと出てきます。あるいは、スタートメニュー > Windowsシステムツール からでも見つけられます。よく使うのでタスクバーにピン止めしておきましょう。

Android端末の設定

Android端末の開発向けオプションを有効にします。
この設定を表示するには、端末やOSのバージョン等によって違いがあると思うので分からない場合は検索してみて下さい。
Xperia 5ⅡSOG02 (Android 10)の場合だと、設定 > デバイス情報 > ビルド番号を7回タップすると表示されます。
表示される場所は、設定 > システム > 詳細設定 > 開発者向けオプション となりますので、その設定内のUSBデバッグを有効にしてください。許可するか表示されるのでOKを押します。
※ USBデバッグを有効にしたままにすると動作しないアプリなどありますので、必要ない場合は無効にしましょう。

おわりに

一部手かなり抜きな説明ですが、以上が基本的なUnityでAndroidアプリを開発するための下準備になります。

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

Android ViewBindingで、radioButtonを使う方法

前書き

 今日は2021年1月13日、久しぶりにAndroidプログラミングを再開した。思い起こせば3ヶ月前、Androidプログラミングに悩み、いっそ初心に戻ろうと思い、デザインパターンやKotlinプログラミン(ラムダ式、関数型プログラミングなど)を勉強してみた。あれから3ヶ月、これらを習得したと言うよりは飽きたので、久しぶりにAndroidプログラミングに戻ってきた。
 久しぶりにAndroidStudioを立ち上げたら、アップデートの嵐。時代に取り残されまいと?勢いづいて、バンバンとupdateを繰り返した。「おお、時代はAndroidStudio“4.1”にアップデートしたか!」
 これが運の分かれ道、世の中「日進月歩」と言いますが、今やITは「光陰矢の如し、月日が過ぎれば技術は飛躍し、既に私は取り残されている」状態であり、"findViewById()"メソッドが使えないじゃないですか!
 ちょっと調べると、Android デベロッパードキュメントガイドビュー バインディングにその説明がありました。
 また、次のような記事もあります。
  ・Kotlin Android Extensions の未来
  ・Android Studioを4.1にしたら「kotlin-android-extensions」が消えてしまった件
 変化の速い(進歩の速い?)言語には、まったくを以って付いて行けません。

 さて、本レポートでは、ViewBindingで、radioButtonを使い方を説明します。想定する画面のイメージは次図の通りです。
画面.png

1. 画面イメージに従って、レイアウトファイルを作成

画面イメージに従って、レイアウトファイルを作成します。もちろんpaletteペインを使って、viewをドラッグアンドドロップをしても結構ですし、コードを打ち込んでもいいです。

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">

    <TextView
        android:id="@+id/textView"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="168dp"
        android:text="TextView"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.498"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <RadioGroup
        android:id="@+id/radioGroup1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="32dp"
        android:layout_marginBottom="32dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView">

        <RadioButton
            android:id="@+id/radioButton1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="ラジオボタン1" />

        <RadioButton
            android:id="@+id/radioButton2"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="ラジオボタン2" />

    </RadioGroup>
</androidx.constraintlayout.widget.ConstraintLayout>

2. ViewBindingを使うための前処理

 ViewBindingを使うために、先ずbuild.gradle(Module:...)ファイルを修正(追記)します。
buildGradleModule50%.png

(1)プロジェクトのAndroidビューの中のbuild.gradel(Module:...)ファイルを開く。
(2)android{・・・}の中に次に示したbuildFeature{・・・]のブロックを追記。

build.gradle#Module
    buildFeatures{   // このブロックを追記
        viewBinding = true
    }

(3)「Sync Now]をクリックし、viewBindingを有効にする。

3. ViewBindingを使うためにActivityXxxBindingクラスを呼び出しそのインスタンスを定義する

 自動で生成されたアクティビティークラス(MainActivity.kt)に、ViewBingdingを定義しているActivityMainBindingクラス呼び出しそのインスタンスを定義するコードを次のように追記する。なお、ビューのレイアウトファイルがactivity_main.xmlならば、クラス名はActivityMainBindingとなる。(これば決まり事です。もしレイアウトファイルがactivity_xxx.xmlならば、キャメル表記にし、Bindingが付加されたものActivityXxxBindingがクラス名になります。)

MainActivity.kt
package com.example.testradiobutton

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.example.testradiobutton.databinding.ActivityMainBinding  // Alt+Enterでコード補完によりインポートされる

class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding   // 追記 
                                                        //(追記時に赤くなったActivityMainBindingの文字の上に
                                                        // カーソルを置きAlt+Enterを押すと、コード補完により
                                                        // ActivityMainBindingクラスのインポート文が追記される)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)   // 追記
        // setContentView(R.layout.activity_main)   // アクティビティ生成時に自動で生成されるコード(削除)
        setContentView(binding.root)    // 追記
    }
}

4.ラジオボタンを操作した時の動作を記述

MainActivityクラスのonCreate()メソッドにラジオボタンを選択したときコードを追記します。このコードのロジックは、選択したラジオボタンのテキストプロパティをtextViewに表示します。

MainActivity.kt
    // 省略
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding   
     override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)   
        setContentView(binding.root) 

        binding.radioGroup1.setOnCheckedChangeListener { group, checkedId ->    // 追記
            if (group == binding.radioGroup1) {                                 // 追記  ラジオボタングループがいくつかあることを想定(一つだけなら不要)
                binding.textView.text = when (checkedId) {                      // 追記  ラジオボタングループの中のどれが選択されているかを判定
                    R.id.radioButton1 -> binding.radioButton1.text              // 追記  ラジオボタンのtextプロパティーの内容をテキストビューのtextプロパティーに与える
                    R.id.radioButton2 -> binding.radioButton2.text              // 追記  同上
                    else -> "何したの?"                                         // 追記  想定がの選択がされた時
                }                                                               // 追記
            }                                                                   // 追記
        }                                                                       // 追記

    }
}

5.ActivityMainBindingクラスのソースファイルのある場所

 ActivityMainBindingクラスのソースファイルのある場所は、次のとおり。
ActivityMainBindingJava_Path.png

 またそのソースコードは、次のとおり。

ActivityMainBinding.java
// Generated by view binder compiler. Do not edit!
package com.example.testradiobutton.databinding;

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.RadioButton;
import android.widget.RadioGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.viewbinding.ViewBinding;
import com.example.testradiobutton.R;
import java.lang.NullPointerException;
import java.lang.Override;
import java.lang.String;

public final class ActivityMainBinding implements ViewBinding {
  @NonNull
  private final ConstraintLayout rootView;

  @NonNull
  public final RadioButton radioButton1;

  @NonNull
  public final RadioButton radioButton2;

  @NonNull
  public final RadioGroup radioGroup1;

  @NonNull
  public final TextView textView;

  private ActivityMainBinding(@NonNull ConstraintLayout rootView, @NonNull RadioButton radioButton1,
      @NonNull RadioButton radioButton2, @NonNull RadioGroup radioGroup1,
      @NonNull TextView textView) {
    this.rootView = rootView;
    this.radioButton1 = radioButton1;
    this.radioButton2 = radioButton2;
    this.radioGroup1 = radioGroup1;
    this.textView = textView;
  }

  @Override
  @NonNull
  public ConstraintLayout getRoot() {
    return rootView;
  }

  @NonNull
  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater) {
    return inflate(inflater, null, false);
  }

  @NonNull
  public static ActivityMainBinding inflate(@NonNull LayoutInflater inflater,
      @Nullable ViewGroup parent, boolean attachToParent) {
    View root = inflater.inflate(R.layout.activity_main, parent, false);
    if (attachToParent) {
      parent.addView(root);
    }
    return bind(root);
  }

  @NonNull
  public static ActivityMainBinding bind(@NonNull View rootView) {
    // The body of this method is generated in a way you would not otherwise write.
    // This is done to optimize the compiled bytecode for size and performance.
    int id;
    missingId: {
      id = R.id.radioButton1;
      RadioButton radioButton1 = rootView.findViewById(id);
      if (radioButton1 == null) {
        break missingId;
      }

      id = R.id.radioButton2;
      RadioButton radioButton2 = rootView.findViewById(id);
      if (radioButton2 == null) {
        break missingId;
      }

      id = R.id.radioGroup1;
      RadioGroup radioGroup1 = rootView.findViewById(id);
      if (radioGroup1 == null) {
        break missingId;
      }

      id = R.id.textView;
      TextView textView = rootView.findViewById(id);
      if (textView == null) {
        break missingId;
      }

      return new ActivityMainBinding((ConstraintLayout) rootView, radioButton1, radioButton2,
          radioGroup1, textView);
    }
    String missingId = rootView.getResources().getResourceName(id);
    throw new NullPointerException("Missing required view with ID: ".concat(missingId));
  }
}

後書き

 ViewBindingで、radioButtonを使う方法を探してみましたが、日本語のものが見当たらなかったので、まとめていました。

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

AndroidのDangerousパーミッションについての基本の基

AndroidのDangerousパーミッションについての基本の基

Androidのパーミッションはいつもなんとなくで利用していましたので、改めて、基本をおさらいしたくここにまとめさせていただくことといたしました。

そもそもAndroidの権限について

一言で言うと、アプリがなんでも自由にアクセスし悪さなどができないようにそれぞれのデータや機能に対してアクセス・利用などについての権限制度とその許可を必要とする機能です。

公式ページ - Android での権限
※以下、公式ページより抜粋

権限の目的は、Android ユーザーのプライバシーを保護することです。Android アプリは、ユーザーの機密データ(連絡先や SMS)と特定のシステム機能(カメラやインターネット)にアクセスする権限をリクエストする必要があります。機能に応じて、システムは権限を自動的に付与するか、またはユーザーにリクエストの承認を求めます。

Android セキュリティ アーキテクチャの設計においては、他のアプリ、オペレーティング システム、またはユーザーに悪影響を及ぼしかねないオペレーションの実行権限を、いかなるアプリにもデフォルトで付与しないよう徹底されています。そうしたオペレーションとしては、ユーザーの個人情報(連絡先やメールアドレス)の読み取りまたは書き込み、他のアプリのファイルの読み取りまたは書き込み、ネットワークへのアクセス、デバイスのスリープモードへの移行の抑止などがあります。

権限の保護レベルにも強弱がある

保護レベルはNormal・Signature・Dangerousの3つです。

以下、公式ページより抜粋

Normal

標準の権限に該当するのは、アプリがアプリのサンドボックス外のデータやリソースにアクセスする必要があるが、ユーザーのプライバシーまたは他のアプリのオペレーションに影響する危険がほとんどないケースです。

たとえば、タイムゾーンを設定する権限は、標準の権限です。

Signature

このタイプの権限については、権限の使用を要求するアプリが権限を定義しているアプリと同じ証明書で署名されている場合に限り、システムはインストール時に権限をアプリに付与します。

Dangerous

危険な権限に該当するのは、アプリがユーザーの個人情報を含むデータやリソースを要求するケースや、ユーザーが保存したデータや他のアプリのオペレーションに影響を及ぼす可能性があるケースです。

たとえば、ユーザーの連絡先を読み取る機能には、危険な権限が必要です。危険な権限が必要であるとアプリが宣言した場合、ユーザーは明示的にアプリに権限を付与することを求められます。ユーザーが権限を承認しない限り、アプリはその権限に依存する機能を提供できません。

アプリが危険な権限を使用するには、実行時に権限の付与をユーザーにリクエストする必要があります。ユーザーにプロンプトを表示する方法の詳細については、危険な権限をリクエストするプロンプトをご覧ください。

NormalとDangerousの許可方法の違い

前述の通り、Androidでは保護レベルによって許可方法が異なります。

Normal

AndroidManifest.xml に許可を記載するだけで利用できます!

Dangerous

AndroidManifest.xml への記述 + アプリ起動時にダイアログ表示をしてユーザーの許可を取得する必要があります!

ユーザーとして利用しているとAndroidでもiOSでも、最初の方に「アプリに〇〇を許可しますか?」と言うようなポップアップでのアイアログが表示される場面をよく見ると思います。
最近は通知を許可するかどうかで見る機会が多いかと思いますが、このようにユーザーに同意いただいて許可のボタンを押していただく必要があります。

Dangerousのパーミッションの取得コード例

今回は「CALL_PHONE:電話発信の許可」を例にコード例を記載していきます

STEP1: AndroidManifest.xml でパーミッションを設定する

AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="・・・">
   <uses-permission android:name="android.permission.CALL_PHONE" />
</manifest>

STEP2: Dangerous パーミッションの許可を確認

現在、該当のDangerousパーミッションが許可されているか確認します

MainActivity
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

     //許可状況の確認
     int permissionCheck = ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE);
     if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
       // すでにユーザーがパーミッションを許可している場合の処理
     } else {
       // ユーザーはパーミッションを許可していない場合の処理
     }
}

STEP3: Dangerousパーミッションの許可されていない場合、ユーザーに許可を求める画面を表示

Dangerousパーミッションの許可されていない場合、ユーザーに許可を求める画面を表示します

MainActivity
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

     //許可状況の確認
     int permissionCheck = ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE);
     if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
       // すでにユーザーがパーミッションを許可している場合の処理
     } else {
       // ユーザーはパーミッションを許可していない場合の処理

       //ユーザーに許可を求める画面を表示
       //ここから
       ActivityCompat.requestPermissions(MainActivity.this,new String[] { Manifest.permission.CALL_PHONE },0);
       //ここまで

     }
}

※requestPermissionsのリクエストコードについて、具体的な利用方法などご存知の方がいたら教えていただけますと幸いです。

STEP5: Dangerous パーミッションのリクエスト結果の処理

リクエスト結果は、リクエストを行ったActivityインスタンスのonRequestPermissionsResultメソッドの中で、リクエストが許可されたか確認できます。
リクエスト結果に合わせてそれぞれに合った処理を記載して完了です。

MainActivity
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

     //許可状況の確認
     int permissionCheck = ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE);
     if (permissionCheck == PackageManager.PERMISSION_GRANTED) {
       // すでにユーザーがパーミッションを許可している場合の処理
     } else {
       // ユーザーはパーミッションを許可していない場合の処理

       //ユーザーに許可を求める画面を表示
       ActivityCompat.requestPermissions(MainActivity.this,new String[] { Manifest.permission.CALL_PHONE },0);

     }

    //ここから
    @Override
    public void onRequestPermissionsResult(int requestCode, String permissions[], int[] grantResults) {
        if (requestCode == 0) {
            if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Dangerous パーミッションのリクエストが許可!!
            } else {
                // Dangerous パーミッションのリクエストが拒否。
            }
         }
    }
    //ここまで
}

まとめ

・必要なパーミッションを確認しましょう
・まずはマニュフェストにパーミッションを記載しましょう
・現在のパーミッションの状況を確認しましょう
・未許可の場合、許可を求める画面を表示しましょう
・ユーザーの許可結果を取得し、結果に合わせてアプリ側で必要な処理を実装しましょう

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

AndroidでSIMの電話番号を取得する方法

AndroidでSIMの電話番号を取得する方法

作成しているAndroidアプリで、SIMの電話番号を取得する必要性があり調べましたので、ここに備忘録としてまとめたいと思います。

利用するクラス:TelephonyManagerクラス

TelephonyManagerクラスを用いることで、Androidの電話番号やSIMのシリアルナンバーなどの情報を取得することができます。

利用するメソッド

TelephonyManagerクラスでは主に以下のクラスで各情報を取得することができます

メソッド 取得できる情報
getLine1Number 電話番号
getDeviceId デバイスID
getSimSerialNumber SIMのシリアル番号
getSimState SIMの状態(通信可能か、PINロックされているかなど)

※他にもあるようですので、必要に応じて調べてみてください。

STEP1: パーミッション

端末の情報を取得するにはマニフェストで「READ_PHONE_STATE」のパーミッションが必要です。

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

STEP2: TelephonyManagerクラスをインスタンス化

MainActivity
private void checkPhoneState{
  //STEP1
  TelephonyManager mTelephonyManager = (TelephonyManager)getSystemService(TELEPHONY_SERVICE);
}

STEP3: 取得したい情報のメソッドを利用

MainActivity
private void checkPhoneState{
  //STEP1
  TelephonyManager mTelephonyManager = (TelephonyManager)getSystemService(TELEPHONY_SERVICE);

 //STEP2
 System.out.println("電話番号:" + mTelephonyManager.getLine1Number());

}

まとめ

・取得したい情報とそのメソッドを見つけましょう
・「READ_PHONE_STATE」をパーミッションしましょう
・TelephonyManagerクラスをインスタンス化しましょう
・TelephonyManagerのメソッドを利用して、取得したい情報を取得しましょう

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

[Android] Google Play ベストオブ 2020 エンターテイメント部門大賞アプリの裏側

私が勤めている Amazia という会社で開発している Palfe(パルフェ) という Android アプリがこの度 Google Play ベストオブ 2020 日本版でエンターテイメント部門の大賞をいただきました?

今回この Palfe というアプリの裏側について、簡単にご紹介したいと思います。

MAD score

Palfe の MAD score です。

summary.png

kotlin.png

jetpack.png

開発環境

minSdkVersion

23 です。

プロジェクト / Issue 管理

プロジェクトも Issue の管理も GitHub を使用しています。

Issue の管理には追加で ZenHub というサービスを使っており、GitHub の Issue 管理をよりやりやすくしてくれています。

ドキュメント管理

ドキュメント管理には Qiita Team を使っています。

Git-flow

開発には Git-flow を採用しています。

App Bundle

App Bundle 対応済みです。

CI/CD

CI には Bitrise を使用しています。 develop ブランチへのプルリクエストをトリガーにビルド、Unit Test、静的解析(ktlintdetekt、Android Lint)を実行しています。

静的解析などで問題があった場合は、Danger でプルリクエストにコメントされるようになっています。

リリース前のテストはステージング環境で行っていますが、ステージング環境用の APK は Firebase App Distribution にアップロードしています。

最近別のプロジェクトで Bitrise から GitHub Actions へ移行したので、Palfe もそのうち GitHub Actions へ移行するかもしれません。

デザイン管理

Figma を使っています。

必要な画像素材は Figma からダウンロードしてプロジェクトに組み込んでいます。

画像素材は基本的にはベクタ画像としてプロジェクトに組み込みますが、複雑な画像の場合には WebP 形式に変換して組み込みます。

使用ライブラリ

  • Kotlin Coroutines

    • 非同期処理で使っています
    • Flow や Channel も使っています
  • OkHttpRetrofit

    • 通信処理のために使っています
  • Moshi

    • JSON のパースに使っています
  • Dagger

    • そのうち Hilt に移行したいと考えています
  • AssistedInject

    • Dagger で公式にサポートされるようなので、そのうち削除する予定です
  • Glide

  • Material Components

    • モーションによるアニメーションやダークテーマにも対応しています
  • Groupie

    • RecyclerView 内に異なる種類のレイアウトを表示するのに便利なので使用しています
  • flow-preferences

    • SharedPreferences で保存しているデータに変更があった場合、Coroutine Flow で変更通知を受け取れます
    • DataStore が安定版になれば移行を検討しようかと考えています
  • LiveEvent

    • LiveData でイベント通知を行うために使用しています
    • SharedFlow がリリースされたため、今後はこちらを使用することを検討しています
  • PhotoView

    • 画像を拡大表示したりするために使用しています
  • Timber

    • ログ出力のために使用しています
    • LogCat への出力以外にも、Timber.DebugTree を継承したクラスを作成して Log.e() で Crashlytics にログが出力されるようにする設定などを行っています
  • Flipper

    • デバッグのために使っています
  • Google Play Billing Library

    • 課金処理のために使っています

Jetpack 系

  • Activity

    • ViewModel を取得するための拡張関数 viewModels()OnBackPressedDispatcher などを使っています
  • Fragment

    • ViewModel を取得するための拡張関する viewModels()activityViewModels()FragmentContainerViewFragmentFactory などを使っています
  • Room

  • ConstraintLayout

  • Lifecycle

    • LiveData、ViewModel、SavedState などを使っています
  • ViewPager2

    • 漫画を縦スクロールでも読めるように、と思って採用しました(まだ縦スクロールできるように実装していませんが。。。)
    • ViewPager2 の中で縦方向にスクロールする RecyclerView を配置した場合、リストを縦方向にスクロールしようとしても ViewPager2 の横スワイプの効きがよくてうまく操作できないことがあったので、漫画を表示する以外の箇所は普通の ViewPager を使っています
  • RecyclerView

  • Paging

    • 直接 Paging を使っているわけではなく、Firebase-UI(後述) というライブラリを使っており、そのライブラリが Paging を使っています
  • WorkManager

    • 以前使用していましたが、現状では下位互換のために残しているだけなので削除予定です

テスト系

  • JUnit4

  • Truth

    • アサーションライブラリです
  • MockK

    • モック用のライブラリです
  • Robolectric

    • JVM 上で Android 依存のコードを実行することができるテストフレームワークです
  • AndroidX Test

    • Runner と JUnit を導入しています

Firebase 系

  • Crashlytics

    • クラッシュ解析に使っています
  • Analytics

    • ユーザー行動の分析に使っています
  • Firestore

    • ユーザーが読んだ漫画の閲覧履歴やお気に入りのデータの保存に使っています
  • AdMob

    • 広告表示のために使っています
  • Auth

    • ユーザー認証のために使っています
  • Firebase-UI

    • Firestore で保存された漫画の閲覧履歴などを RecyclerView で表示するために FirestorePagingAdapter というクラスを使っています

Gradle Plugin 系

  • ktlint

    • Gradle で Kotlin のコンパイルの実行前に、ktlint によるフォーマットのタスクを実行するように設定したりしています
  • OSS Licenses Gradle Plugin

    • オープンソースライセンスの表示のために使っています
  • Gradle Play Publisher

  • Releases Hub Gradle Plugin

    • 使用しているライブラリにバージョンアップがあった場合にプルリクエストを出してくれます
    • GitHub Actions でクーロンを設定して定期的にバージョンアップがあるかどうかをチェックしています
  • Gradle Swagger Generator Plugin

    • Palfe の Web API は Swagger でスキーマ定義されています。この Gradle Plugin で Swagger の定義ファイルからコードを生成するために使用しています
    • 生成されるコードは Web API のレスポンスとなるクラス、エラーレスポンスのクラス、Retrofit で使うインターフェースです

アーキテクチャ

マルチモジュール + MVVM です。

マルチモジュールについて

Google が Gradle のマルチモジュールを推奨しているため、Palfe でもマルチモジュールを採用しています。

以下のような感じでモジュールを分割しています。

.
├── app : アプリケーションモジュール
├── base : Android Framework に依存せず、各モジュールで使用される処理を定義するためのライブラリモジュール
├── base-android : Android 関連の各モジュールで使用される処理を定義するためのライブラリモジュール
├── data
│   ├── db : Room 関連の処理を定義したライブラリモジュール
│   ├── firestore : Firestore 関連の処理を定義したライブラリモジュール
│   ├── network : ネットワーク関連の処理を定義したライブラリモジュール
│   └── preferences : SharedPreferences 関連の処理を定義したライブラリモジュール
├── feature : 特定の機能に関する処理のためのライブラリモジュール。例えば Analytics や課金処理など。
│   ├── analytics
│   ├── billing
│   └── ...
└── ui : UI 関連の処理を定義したライブラリモジュール。画面ごとに個々にモジュールを作成している
    ├── common
    └── ...

MVVM について

Google が推奨しているアーキテクチャはこちらに記載されているような構成かと思いますが、Palfe では Repository がなく ViewModel で直接 API でネットワークからデータを取得したり、DB からデータを取得したりしています。

基本的にはだいたいどの画面も以下のような処理の流れになります。

  • ViewModel で API や DB からデータを取得する
  • 取得したデータを Activity や Fragment で必要な形式に整形し、LiveData で通知する
  • Activity や Fragment では ViewModel の LiveData を監視し、値が通知されたら表示処理を行う(Data Binding も使用しています)

このような構成にしたのは、構成をシンプルにしたいという考えからで、主に以下のような理由からです。

  • 別のプロジェクトで Clean Architecture のような構成を採用したが、単に API からデータを取ってきて一覧表示するだけの画面を実装するのにいくつものクラスを作成する必要があり、かなり冗長となってしまった

  • 新しいメンバーが参画したときに、できる限りアーキテクチャの把握を容易にしたいため

課題 / やりたいこと

  • メンバーの拡充

    • Android の開発メンバーは私と業務委託でもう一人の二名体制です。他のプロジェクトも掛け持ちしているため、Palfe の開発にはあまり時間を割けていない状況です
    • 興味のある方はぜひ!
  • テストコード

    • 現状ではテストはあまり書けていないです
  • リファクタリング

    • 基本 ViewModel にロジックを書いていく方針なので ViewModel が肥大化している箇所があったりします。こういった箇所はもう少し適切に役割分担してコードの見通しがよくなるようにしたいと考えています
    • マルチモジュールで UI に関しては画面ごとにモジュールを作成していますが、分割の粒度が細かすぎた気がしてます。どの粒度で分割するか正直悩ましいですが、もう少し分割の粒度が大きい方がコードの見通しがよい気もしています
  • In-app review、In-app updates の導入

    • 別のプロジェクトでアプリ内レビューやアプリ内アップデートの機能を実装したので、Palfe にも導入したいと考えています
  • ライブラリの導入、更新

    • 複雑な設定が不要になるので、Hilt への移行は行いたいと考えています
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidアプリで電話をかける処理の基本の基

Androidアプリで電話をかける処理の基本の基

Androidアプリで電話をかける処理をなんとなく実装していたため、改めてここに記載をし、自身の復讐をできればと思います。

※主に自身の毎日の復習・学習の機会創出、アウトプットによる知識の定着を目的としております。
暖かい目で見ていただけますと幸いです。

Androidアプリから電話をかける方法

IntentのACTION_CALLを利用する方法

IntentのACTION_CALLを利用することで、アプリ側で入力された番号を持って、電話をかけることができます。

※マニフェストで許可が必要
マニフェストでCALL_PHONEの許可が必要です。

IntentのACTION_DIALを利用する方法

IntentのACTION_DIAL利用することで、アプリ側で入力された番号を持って、ダイヤラーと電話アプリを開くことができます

※ACTION_CALLとACTION_DIALの違い
ACTION_CALL:電話がそのまますぐかかる
ACTION_DIAL:電話アプリ・ダイヤラーを開くだけ。そこから改めて電話アプリ・ダイヤラー側で発信の操作(タップ)が必要

Dangerous パーミッションの許可取得

Android 6.0らパーミッションの概念が変わり、ユーザーに許可を必要とするパーミッションと必要としないパーミッションの2種類にわかれました。

※おさらい
・Nomalパーミッション:アプリインストール時、AndroidManifest.xmlの記載を元に権限が得られる
・Dangerousパーミッション:ユーザーの許可により権限を得られる。ユーザーによって後から権限の取り消しも可能。

電話に関連するDangerousパーミッション(パーミッショングループPHONEに属するパーミッション)もありますので、用途に合わせて必要な権限を取得するよう設定しましょう。

基本的なコード

   Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:012345678"));
   startActivity(intent);

※マニフェストも許可が必要

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

エラー時の処理

ダイヤログ・電話アプリへの接続権限がないなどで、繋がらない可能性があります。
この基本的なコードのままでは例外時は電話が繋がらずかつ画面上でもなんの通知もない状態となってしまい、ユーザーからはどうなっているのか全くわからない状態になってしまいます。

ですので、例外処理を追加し、例外時はユーザーに例外内容を通知するように設定しましょう。

※または、startActivityの前に権限をチェックする処理を記載する方法でも対応可(発信ボタンを押したら、まずは権限チェックして、あればstartActivityとか)

   Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse("tel:012345678"));

   //ここから例外処理
   try{
     startActivity(intent);
   }catch(e: SecurityException){
     Toast.makeText(context, "エラーです・・・・", Toast.LENGTH_SHORT).show()
   }

まとめ

・各権限を事前に取得しましょう。
・Intentを利用して電話をかけましょう。
・エラー時に備え、例外処理も実装しておきましょう。
・権限がないのに、アプリ上で発信操作をしようとするユーザーに対する対策も構築しておきましょう。

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

AWS Device FarmのホストマシンにAndroid SDKをインストールする

AWS Device FarmのホストマシンでAndroid SDKを利用する方法をメモする。

なお通常Device Farmのホストマシンでは、ビルド済みのapkを端末へインストールする程度のことしか行わないので、基本的にAndroid SDKは必要ない。( adb などの platform-tools はプリインストールされているので)

とはいえ、あると便利な場合もまれにある(ホストマシン上でapkのビルドをしたいなど)ので、ここで方法をまとめる。

はじめに

AWS Device Farmのテスト実行環境は、

  • モバイル端末(クライアント)
  • そのクライアントと接続されたLinuxサーバー(ホスト)

から構成されている。

今回は、上記のホストマシンにAndroid SDKをインストールする方法をまとめる。

方法

Device Farmのカスタムテスト環境の機能を用いる。
適当なスタンダードテスト環境に対して、カスタマイズした test_spec.yml をアップロードすれば良い。

test_spec.yml
phases:
  install:
    commands:
      - mkdir ~/android_sdk
      - cd ~/android_sdk
      # download and install commandline tools
      - curl https://dl.google.com/android/repository/commandlinetools-linux-6858069_latest.zip -o sdk.zip
      - unzip sdk.zip
      # set environment variables for android sdk
      - export ANDROID_HOME_OLD=$ANDROID_HOME
      - export ANDROID_HOME=$HOME/android_sdk
      - export ANDROID_SDK_ROOT=$HOME/android_sdk
      # install android sdk
      - yes | $ANDROID_HOME/cmdline-tools/bin/sdkmanager "build-tools;28.0.3" "platforms;android-29" --sdk_root=$ANDROID_HOME
      # use existing platform-tools (which is a specific build for AWS device farm)
      - rm -rf $ANDROID_HOME/platform-tools
      - ln -s $ANDROID_HOME_OLD/platform-tools $ANDROID_HOME
      - export PATH="$PATH":"$ANDROID_HOME/platfom-tools"
      # return to the previous directory
      - cd -

ポイントは下記3点:

1. commandline toolsを利用した最小限のインストール

Android Studioをインストールすると必要なものが全部入る認識だが、サイズが大きく不要なものも多いので、今回は利用しない。
sdkmanager を利用すると、必要なものだけをインストールできる。参照

また、Command line toolsのURLはこちらから入手できる。

2. platform-tools は既存の物を使う

新規にインストールされるplatform-toolsを利用すると、端末との通信ができなくなるなどの不具合があった。
もともとインストールされているplatform-toolsは、AWS Device Farm用にカスタマイズされたものらしい。
このため、新しい$ANDROID_HOME下にシンボリックリンクを張って、既存のplatform-toolsが参照されるようにする。

3. test_spec.yml のアップロード方法

文法はこちらを参照: Working with Custom Test Environments in AWS Device Farm

アップロードする際は、既存のスタンダードテスト環境をベースに、カスタムされたtest_spec.ymlをアップロードする形になる。

Appium等を用いる場合は、素直に対応する環境を選べば良い。
一方で、Flutter driveを用いる場合は、そもそも対応するスタンダード環境が存在しない。

この場合は、適当な環境(e.g. APPIUM_PYTHON)を選んだ上で、テストパッケージもアップロードし、その上で test_spec.yml をアップロードする。
次のStackoverflowに方法がまとめられている: Running Flutter Integration Tests in AWS Device Farm, Saucelabs, Firebase Test Lab etc

以上です。

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

Retrofit + LiveData のテストコードを実装してみた

以前、Android Jetpack をフル活用して、Qiita の LGTM 数ゼロの記事だけ検索できるアプリを作ったという記事を執筆しました。
その際、下記のようなArticleListViewModelを実装しました。

ArticleListViewModel.kt
/**
 * 記事表示用ViewModel
 */
class ArticleListViewModel @ViewModelInject constructor(
    private val searchRepository: SearchRepository
) : ViewModel() {

    // 記事一覧(読み書き用)
    // MutableLiveDataだと受け取った側でも値を操作できてしまうので、読み取り用のLiveDataも用意しておく
    private val _articleList = MutableLiveData<Result<List<Article>>>()
    val articleList: LiveData<Result<List<Article>>> = _articleList

    /**
     * 検索処理
     * @param page ページ番号 (1から100まで)
     * @param perPage 1ページあたりに含まれる要素数 (1から100まで)
     * @param query 検索クエリ
     */
    fun search(page: Int, perPage: Int, query: String) = viewModelScope.launch {
        try {
            Timber.d("search page=$page, perPage=$perPage, query=$query")
            val response = searchRepository.search(page.toString(), perPage.toString(), query)

            // Responseに失敗しても何かしら返す
            val result = if (response.isSuccessful) {
                response.body() ?: mutableListOf()
            } else {
                mutableListOf()
            }

            // LGTM数0の記事だけに絞る
            val filteredResult = result.filter {
                it.likes_count == 0
            }

            // viewModelScopeはメインスレッドなので、setValueで値をセットする
            _articleList.value = Result.success(filteredResult)
        } catch (e: CancellationException) {
            // キャンセルの場合は何もしない
        } catch (e: Throwable) {
            _articleList.value = Result.failure(e)
        }
    }
}

ArticleListViewModelでは LiveData を使用しています。
また、SearchRepository(実質SearchServiceのラッパークラス)では Retrofit を使用しています。
そのため、本記事では Retrofit + LiveData のテストコードを実装してみます。

テスト用ライブラリの準備

app/build.gradleに下記を追記してください。

app/build.gradle
dependencies {
    testImplementation "com.google.truth:truth:1.1"
    testImplementation "androidx.arch.core:core-testing:2.1.0"
    ...
    // Retrofit
    def retrofit_version = "2.9.0"
    testImplementation "com.squareup.retrofit2:retrofit-mock:$retrofit_version"
    ...
    // coroutines
    def coroutines_version = "1.4.2"
    testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$coroutines_version"
}

Retrofit のモッククラスを実装する

SearchServiceのモックとして下記のクラスを実装します。

MockSearchService.kt
/**
 * SearchServiceのモック
 */
class MockSearchService(
    private val delegate: BehaviorDelegate<SearchService>,
) : SearchService {
    var response: List<Article>? = null

    override suspend fun search(
        page: String,
        perPage: String,
        query: String
    ): Response<List<Article>> {
        return delegate.returningResponse(response).search(page, perPage, query)
    }
}

BehaviorDelegate#returningResponseで引数に設定されたresponseを返却するようにしています。

テストコードを実装する

LiveData をテストするための準備を行う

LiveData をそのままテストしようとすると、下記のエラーが出力されます。

Exception in thread "pool-1-thread-1 @coroutine#1" java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.

以下のルールを設定します。

ArticleListViewModelTest.kt
// LiveDataをテストするために必要
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()

ViewModel のインスタンスを生成するための準備を行う

下記のように ViewModel のインスタンスを生成するための準備をします。

ArticleListViewModelTest.kt
private val retrofit = Retrofit.Builder()
    .baseUrl("https://qiita.com")
    .addConverterFactory(MoshiConverterFactory.create())
    .build()
private val behavior = NetworkBehavior.create()
private val delegate = MockRetrofit.Builder(retrofit).networkBehavior(behavior).build()
    .create(SearchService::class.java)
private val searchService = MockSearchService(delegate)
private val viewModel = ArticleListViewModel(SearchRepository(searchService))

NetworkBehavior.create()でネットワークの振る舞いを擬似的に再現するための設定を行えるようにします。
MockRetrofit.BuilderBehaviorDelegateのインスタンスを生成します。

Dispatcher の置き換えを行う

viewModelScopeはメインスレッドですが、そのままテストを実行すると下記のエラーが出力されます。

java.lang.IllegalStateException: Module with the Main dispatcher had failed to initialize. For tests Dispatchers.setMain from kotlinx-coroutines-test module can be used

エラーの通り、Dispatchers.setMainを使って別の Dispatcher に置き換えます。

ArticleListViewModelTest.kt
@ExperimentalCoroutinesApi
class ArticleListViewModelTest {
    ...
    @Before
    fun setUp() {
        Dispatchers.setMain(Dispatchers.Unconfined)
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
    }
    ...
}

LiveData の値を取得するための準備を行う

以下のようにテストコード上で直接 LiveData の値を取得しようとすると、値の取得が間に合わず null となり失敗します。

ArticleListViewModelTest.kt
viewModel.search(1, 1, "")
assertThat(viewModel.articleList.value?.isSuccess).isTrue() // isSuccessがnullとなる

architecture-components-samplesLiveDataTestUtil.javaという LiveData を取得するためのクラスがあるのでコピーします。

LiveDataTestUtil.kt
/**
 * https://github.com/android/architecture-components-samples/blob/main/BasicSample/app/src/androidTest/java/com/example/android/persistence/LiveDataTestUtil.java
 */
object LiveDataTestUtil {

    /**
     * Get the value from a LiveData object. We're waiting for LiveData to emit, for 2 seconds.
     * Once we got a notification via onChanged, we stop observing.
     */
    @Throws(InterruptedException::class)
    fun <T> getValue(liveData: LiveData<T>): T? {
        val data = arrayOfNulls<Any>(1)
        val latch = CountDownLatch(1)
        val observer = object : Observer<T> {
            override fun onChanged(@Nullable o: T) {
                data[0] = o
                latch.countDown()
                liveData.removeObserver(this)
            }
        }
        liveData.observeForever(observer)
        latch.await(2, TimeUnit.SECONDS)
        return data[0] as T?
    }
}

これでテストコードを実装するための準備が整いました。

具体的なテストケースを実装する

例えば 0LGTM の記事が存在するケースを実装します。

ArticleListViewModelTest.kt
@Test
fun search_0LGTMの記事あり() {
    behavior.apply {
        setDelay(0, TimeUnit.MILLISECONDS) // 即座に結果が返ってくるようにする
        setVariancePercent(0)
        setFailurePercent(0)
        setErrorPercent(0)
    }
    val articleList = listOf(
        Article("", "", 0, "", User("", "", ""))
    )
    searchService.response = articleList
    viewModel.search(1, 1, "")
    val result = LiveDataTestUtil.getValue(viewModel.articleList)

    // 成功扱いか
    assertThat(result?.isSuccess).isTrue()

    // データが存在するか
    assertThat(result?.getOrNull()).isNotNull()

    // データが1件以上存在するか
    assertThat(result?.getOrNull()).isNotEmpty()
}

まずネットーワークの振る舞いを設定するため、behaviorを設定します。

ArticleListViewModelTest.kt
behavior.apply {
    setDelay(0, TimeUnit.MILLISECONDS) // 即座に結果が返ってくるようにする
    setVariancePercent(0)
    setFailurePercent(0)
    setErrorPercent(0)
}

各設定項目の内容およびデフォルト値は以下の通りです。

設定項目 設定内容 デフォルト値
setDelay 応答が受信されるまでにかかる時間 2000 ミリ秒
setVariancePercent ネットワークの遅延が遅くなる確率 ±40%
setFailurePercent ネットワーク障害(例外)が発生する確率 3%
setErrorPercent HTTP エラーが発生する確率 0%

例えば例外を意図的に発生させたい場合はsetFailurePercent(100)にし、HTTP エラーを意図的に発生させたい場合はsetErrorPercent(100)にしてください。
必ず成功させたい場合はいずれも 0 にしてください。

次に適当なダミーデータを用意してレスポンスに設定し、処理を実行します。

ArticleListViewModelTest.kt
val articleList = listOf(
    Article("", "", 0, "", User("", "", ""))
)
searchService.response = articleList
viewModel.search(1, 1, "")

最後にLiveDataTestUtilで LiveData の値を取得し、各種チェックを行います。

ArticleListViewModelTest.kt
val result = LiveDataTestUtil.getValue(viewModel.articleList)

// 成功扱いか
assertThat(result?.isSuccess).isTrue()

// データが存在するか
assertThat(result?.getOrNull()).isNotNull()

// データが1件以上存在するか
assertThat(result?.getOrNull()).isNotEmpty()

あとは同じ要領で他のテストケースを実装していくだけです。

まとめ

Retrofit + LiveData のテストコードを実装しました。
Retrofit のモックライブラリでさまざまな振る舞いができるのは便利だと感じました。
色々ハマりポイントがあったので、似たような境遇の方の助けになると嬉しいです。

参考 URL

ソースコード

hiesiea/Qiita0LgtmViewer

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

【Android】pecoを使って特定のシステムサービスのdumpsysを簡単にする

dumpsysとは?

Androidデバイス上で動作しているシステムサービスの状態をダンプしてくれるツールです

https://developer.android.com/studio/command-line/dumpsys

dumpsysの面倒なところ

adb shell dumpsysを実行すると、すべてのシステムサービスの状態がダンプされます。
しかし、通常使う場合には特定のシステムサービスの状態のみダンプしたいことがほとんどです。
例えば、バッテリーの状態をダンプしたい時にはadb shell dumpsys batteryって感じで、特定のシステムサービス名を指定します。
ただ、システムサービスの数が多いので名前を覚えるのがちょっと面倒です。

pecoを使って特定のシステムサービスのdumpsysを簡単にする

システムサービス名をなんとなく覚えるだけでいい感じに特定のシステムサービス名を指定してdumpsysできるコマンドを組んでみました。
そのコマンドが以下になります。

adb shell dumpsys -l | sed -e '1,2d' | peco | xargs adb shell dumpsys

まずadb shell dumpsys -lでシステムサービスの一覧を取得しています。
次にsedで出力される不要な行を削ります。
次にpecoに渡します。
最期にpecoで選択したやつをadb shell dumpsysに渡すことで、特定のシステムサービスのdumpsysが行えます。

Demo

こんな感じ:relaxed:
pecoを使うことでインタラクティブに絞り込みができるので、システムサービス名をなんとなく覚えるだけ大丈夫!

adb_dumpsys_peco.gif

aliasにするとさらに楽

こんな感じで✨

alias dumpsys="adb shell dumpsys -l | sed -e '1,2d' | peco | xargs adb shell dumpsys"

おわりに

快適なadbライフをお過ごしください?

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