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

Huaweiから学ぶ、AndroidのOS開発やライセンス

はじめに

Androidはオープンソース」ということは、多くの皆様がご存知だと思います。
ですが、Androidの開発元がどこで、どのようなライセンスで、どのように皆様のスマートフォンに搭載されているか、説明できる方は少ないと思います。

また、少し前に、Huaweiがアメリカに経済制裁を受けた影響で、「Huaweiのスマートフォンは今後、Androidが販売できない」のような『噂』が流れました。
ですが、そのような噂には間違いがあります。

今回は、Androidの成り立ちや開発、ライセンスについて、少し深堀りしていきたいと思います。

※指摘や修正依頼、大歓迎です!

AndroidのOS開発の流れ

Androidは以下の画像のような流れで開発され、ソースコードが公開されています。

次期バージョンをGoogleがクローズドソースとして開発

Androidの次期バージョンは、オープンソースとなる前に、まずGoogleによって、クローズドソースとして開発されます。

AOSPに公開

その後Googleによって、Android Open Source Project (AOSP、Android オープンソースプロジェクト)に、ソースコードが公開されます。
こちらの記事は、2019/9/3に、Android10のソースコードがAOSPに公開されたニュースになります。
ソースコードやリリースノートはこちら

また、この時点でのAndroidは時折、「素のAndroid」と呼ばれます。
素のAndroidには、必要最低限の機能とアプリのみ搭載されています。

この「素のAndroid」に搭載されているアプリは、主に以下のようなものがあります。

  • アルバム
  • 電卓
  • カレンダー
  • カメラ
  • 時計
  • 電話帳
  • 電話
  • Eメール
  • ホーム
  • IM
  • メディアプレーヤー
  • フォトアルバム
  • SMS/MMS

そして、この「素のAndroid」のOSSライセンスは、「Apache 2.0」となっています。
参考:https://source.android.com/setup/start/licenses
Apache 2.0は非コピーレフトのライセンスなので、素のAndroidをカスタマイズしたとしても、カスタマイズ後のソースコードを公開する義務はありません。

Googleのアプリ追加

この「素のAndroid」について、気付いた方は気付いたかもしれませんが、この時点では、「Google Chrome」や「Google Map」「Google Play」など、Googleのアプリが追加されていません。
素のAndroidには、Googleのアプリが含まれないことになります。

では、Googleのアプリはどのように追加されるかというと、それはGoogle Mobile Services (GMS)という、Googleが提供するサービスによって追加されます。
これらのサービスは、Googleのライセンスによって提供されます。そしてGoogleの提供するアプリケーションは、オープンソースではありません。
※ちなみに、ここでいう「Google」は、Androidの開発版を開発していたGoogleとは分けて考えたほうが良さそうです。

GMSで含まれるアプリは、以下のようなものです。

  • Google検索
  • Google Chrome
  • YouTube
  • Google Play ストア
  • Google Drive
  • Gmail
  • Google Map
  • Google Photos

Googleは、「Google モバイルサービス(GMS)」を無料で、端末のメーカーにライセンス供与しています。
しかし、これは一定の基準が満たされた場合に限られます。
だいたいの端末は、これらのGMSによって、Googleのアプリケーションが追加されていますが、中にはそうではないスマートフォンもあります。
各メーカーが申請し、Googleが許可した場合のみ、Googleのアプリケーションが追加されるわけです。

自社の端末用にカスタマイズ

そして、「素のAndroid」をベースに、各スマートフォン開発会社は、自社の端末(スマートフォン)用に、Androidのカスタマイズやコードの追加を行います。

例えば私が愛用する、SamsungのGalaxy Note8は、「Sペン」という、付属ペンを使用してスマートフォンに文字や絵を描ける、という機能があります。
この機能は当然、他のメーカーのスマートフォンには搭載されていません。これはSamsungが、素のAndroidをカスタマイズし、機能追加を行ったということになります。
同じバージョンのAndroidだったとしても、メーカーやスマートフォンによって機能に差が生じるのはこのためです。

また、素のAndroidのライセンスはApache 2.0なので、このカスタマイズ内容をオープンソースとして公開する義務はありません。
(素のAndroidのライセンスを明示する必要はありますが)

各メーカーはこれらの作業を、行うことによって、Androidのスマートフォンのプリインストールを設定していきます。
みなさんが「Android」と思っていたものは、実は

  • Android AOSP
  • Google Mobile Services
  • 各メーカーが独自に追加した機能

の3つの組み合わせだったんですね。

流れをまとめると、こんな画像になるでしょうか。
Android2.png

(※GMS→独自機能と書いてますが、正確にはこの順番じゃないかもしれません。平行かもしれない。イメージとして考えてください)

Huaweiの新端末

2019/09/19、Huaweiは「Mate 30」という新スマホを発表しました。
前述の問題から、「HuaweiからAndroidの新スマホは出ないのではないか」とも言われてましたが、決してそんな事はありませんでしたね。
https://japanese.engadget.com/2019/09/20/mate-30-google-play/

ですが、ここで面白い事実があります。
それは、「このMate 30には、Android OSを搭載するスマートフォンにかかわらず、Google Play Storeを含む各種Googleサービスが非搭載」ということです。

これまでの話が無かったら、「なぜ???」と思う方もいるかもしれません。
ですが、ここまで話を読んでくださった方なら、きっとわかると思います。それはつまり、

  • Android AOSPはオープンソースだから、事情があってもHuaweiの新スマホに搭載できた
  • でもGoogleのアプリケーションはGoogleへの申請が必要だから、経済制裁によって許可が下りなかった
  • なので、Android搭載だけどGoogleのアプリケーションは非搭載になった ということなのです。

ちなみに時々、「今はHuaweiもAndroidを搭載できるけど、将来的にはGoogleが制約を掛けて、Androidの搭載も出来なくなるかもしれない」という記事を見ますが、それは99.99999%ないと考えます。
なぜなら、オープンソースの定義で「個人やグループに対する差別の禁止」というものがあるからです。
仮にそんな事はあったら、それはオープンソースの原理原則を捻じ曲げることになります。
なので絶対無いです。きっと。

ですが、「Google Playストアが利用できない」という状況は、今後も継続するかもしれません。
Google Playストアには本来、素晴らしいAndroidアプリがたくさんありますので、「それらのアプリが利用できないスマートフォン」というのは、Huaweiにとって非常に痛手であることには変わりません。
そのため、Huaweiは現在、独自のストアを用意する、ということは噂されていますね。どうなることやら。

まとめ

以上、Androidとオープンソース、Googleの関係、ならびにHuaweiについて語りました。
普段Androidをお使いの方は、「これはどのフェーズで作られたものなんだ!?」と妄想すると楽しいかもしれません。
また、オープンソースを勉強するにあたって、良い題材になるとも思います!

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

親方空から女の子がをするための準備をする

あらすじ

IoTLTで下記の発表をした。1万人に1人ぐらいは同じ事したいひとは居ると思うので、デモで使ったものを作る手順を公開したい。

結果

IoTLTで使ったスライド(スライド埋め込みが聞いてない場合はリンクから開いてください)

親方空から女の子が

スライド埋め込みの方法分かんない。埋め込み方法教えていただきました。ありがとうございます!

全体構成

ざっくりと作ったものを説明すると、顔を検知したら光って音鳴らす機械

  • MAiXBit
    • 顔の検知
      • 顔を検知したらGPIO経由でラズパイに通知
  • RaspberryPi
    • 音・光をアクチュエータ制御基板経由で動かす
      • パトライト: 12[v]
      • 音: 3.3[v]
      • 光: 3.3[v]
  • AndroidApp
    • 音声を受け取りラズパイに起動・状態遷移を通知
    • アプリ側は3秒に一回ラズパイから状態を取得
  • 電力回り
    • 12[v] バッテリー
    • ブレーカー + 10[A]ヒューズ
    • 電力供給基盤
      • 三端子レギュレータで5[v],3[v]を作成
      • 12[v]はそのまま通過
    • アクチュエータ制御基板
      • フォトカプラ + NPNトランジスタで電力供給

IchigoJamを使おうとしてあきらめる

その昔mruby用に使ってしまったため、Basicが使えない状態になっている。
初期化もかねてFirmをアップデートする。

公式を参考に最新版のファームを入れる。

まずはダウンロード | 子供パソコン IchigoJamから最新版ファームをダウンロードする。とりあえず1.3.1をダウンロードした。

次にHOME of IJUtilitiesから書き込み用ツールをダウンロードする。

IJUtilities.exeを起動するとフォントがどうとかで怒られるの

image.png

一緒にエクスプローラが開くので、Font\for IJ1.2\IchigoJam-for-Display-1.2.ttfを選択してインストールする。

image.png

フォントインストール完了後にcontinueを済ませると下記の様な画面が開く

image.png

IchigoJamをUSBケーブルで接続し、
10分ほど使い方を悩んだのち、公式に書いてある通りにする。

IJUtilities を起動した後、
ターミナルセンター ウインドウの
ツールバー オプション - Firm書換え  (IchigoJam, PanCake...)
を選択して下さい。lpc21isp ウインドウが表示されます。

image.png

何も出てきてくれない。

とりあえずココのUSBドライバ入れてみる。

image.png

状況変わらず

どうにもならなそうなのでIchigoJamは封印することにしてラズパイでやる。

MAiX BiTで人を認識してGPIO出力する

MAiX BiTでダックを検出する予定だったけど無理だったを元に顔の検出用ファームを焼きこむ。

demo_find_face.pyをコピペしてみると

Traceback (most recent call last):
  File "<stdin>", line 10
SyntaxError: invalid syntax

何コレ?もしかしてMAiXBiTも動かないのか?
1行ずつ叩いてみると

>>> task = kpu.load(0x300000)
v=3, flag=1, arch=0, layer len=24, mem=45000, out cnt=1
model_size=388776
E (236951065342) SYSCALL: Out of memory

なぜだかはよく分からないが、uPyLoader経由でboot.py上書きするとうまくいった。

とりあえず6番ピンで顔認識を出力するように変更する。

import sensor
import image
import lcd
import KPU as kpu
from Maix import GPIO
from board import board_info
from fpioa_manager import fm

fm.register(6, fm.fpioa.GPIO0)
facedetect_signal = GPIO(GPIO.GPIO0, GPIO.OUT)

lcd.init()
sensor.reset()
sensor.set_pixformat(sensor.RGB565)
sensor.set_framesize(sensor.QVGA)
sensor.run(1)
task = kpu.load(0x300000)
anchor = (1.889, 2.5245, 2.9465, 3.94056, 3.99987, 5.3658, 5.155437, 6.92275, 6.718375, 9.01025)
a = kpu.init_yolo2(task, 0.5, 0.3, 5, anchor)
facedetect_signal.value(0)
while(True):
    img = sensor.snapshot()
    code = kpu.run_yolo2(task, img)

    if code:
        facedetect_signal.value(1)
        for i in code:
            print(i)
            a = img.draw_rectangle(i.rect())
    else:
      facedetect_signal.value(0)
    a = lcd.display(img)
a = kpu.deinit(task)
fm.unregister(6, fm.fpioa.GPIO0)

ラズパイをVUIとGPIOに対応させる

取り合えず欲しい機能は下記

  • アクチュエータ制御
    • 起動したらハロウィン起動
    • 顔認識モードになったらしたらパトランプ起動
    • 顔認識したらサウンド起動
  • AndroidアプリとのIF
    • 起動の合図を受け付ける
      • post: state
    • 状態確認
      • get: state

状態としては下記を考える。1->2->3->4->2の順で遷移する。例外は認めない。

no name description
1 init 起動直後
2 wait_voice_detect 音声入力待ち
3 wait_face_detect 顔認識待ち
4 face_detecting 顔認識中

とりあえず必要なパッケージ入れる。

pip3 install RPi.GPIO
pip3 install flask

で、書く。

main.py
import RPi.GPIO as GPIO
import time
import threading
from flask import Flask, render_template, request, redirect, url_for, jsonify

app = Flask(__name__)

state = 0
STATUS_NONE = 0 
STATUS_INIT = 1
STATUS_WAIT_VOICE_DETECT = 2
STATUS_WAIT_FACE_DETECT = 3
STATUS_FACE_DETECT = 4
facedetect_pinno = 5
actuator01_pinno = 10
actuator02_pinno = 17
actuator03_pinno = 26

def main():
  global state
  global facedetect_pinno

  change_state(STATUS_INIT)

  GPIO.setmode(GPIO.BCM)
  GPIO.setup(facedetect_pinno, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
  GPIO.setup(actuator01_pinno, GPIO.OUT)
  GPIO.setup(actuator02_pinno, GPIO.OUT)
  GPIO.setup(actuator03_pinno, GPIO.OUT)
  try:
    GPIO.output(actuator01_pinno, 0)
    GPIO.output(actuator02_pinno, 0)
    GPIO.output(actuator03_pinno, 0)

    change_state(STATUS_WAIT_VOICE_DETECT)
    while True:
      time.sleep(1)
      if state == STATUS_WAIT_VOICE_DETECT:
        continue
      if state == STATUS_WAIT_FACE_DETECT:
        if GPIO.input(facedetect_pinno) == GPIO.HIGH:
          change_state(STATUS_FACE_DETECT)
        continue
      if state == STATUS_FACE_DETECT:
        time.sleep(30)
        change_state(STATUS_WAIT_VOICE_DETECT)
        continue
  finally:
    # TODO expect KeyboardInterrupt
    GPIO.output(actuator01_pinno, 0)
    GPIO.output(actuator02_pinno, 0)
    GPIO.output(actuator03_pinno, 0)
    GPIO.cleanup()

@app.route('/state', methods=['GET'])
def get_state():
  global state
  output = {
    "state":state
  }
  return jsonify(output)

@app.route('/state', methods=['POST'])
def post_state():
  global state
  next_state = request.json['state']
  change_state(next_state) 
  output = {
    "state":state
  }
  return jsonify(output)

def change_state(next_state):
  global state
  if state == STATUS_NONE and next_state == STATUS_INIT:
    print("change state: INIT")
    state = next_state
    return

  if state == STATUS_INIT and next_state == STATUS_WAIT_VOICE_DETECT:
    print("change state: WAIT_VOICE_DETECT")
    GPIO.output(actuator01_pinno, 1)
    state = next_state
    return

  if state == STATUS_WAIT_VOICE_DETECT and next_state == STATUS_WAIT_FACE_DETECT:
    print("change state: WAIT_FACE_DETECT")
    GPIO.output(actuator02_pinno, 1)
    state = next_state
    return

  if state == STATUS_WAIT_FACE_DETECT and next_state == STATUS_FACE_DETECT:
    print("change state: FACE_DETECT")
    GPIO.output(actuator03_pinno, 1)
    state = next_state
    return

  if state == STATUS_FACE_DETECT and next_state == STATUS_WAIT_VOICE_DETECT:
    print("change state: WAIT_VOICE_DETECT")
    GPIO.output(actuator02_pinno, 0)
    GPIO.output(actuator03_pinno, 0)
    state = next_state
    return

  print("change state error {0} to {1}".format(state, next_state))

if __name__ == "__main__":
  mainThread = threading.Thread(target=main)
  mainThread.start()
  app.debug = True
  app.run(host='0.0.0.0', port='3000')

Android側のアプリを作る

Android側で行うのは下記点

  • RaspberryPiから現在の状態を取得して画面に表示
  • システム起動を受け取りRaspberryPiへ送信

上記点のみこなせれば後はどうでもいい。

package com.example.thegirlfalldown

import android.app.Notification
import android.content.Intent
import android.net.Uri
import android.os.*
import androidx.appcompat.app.AppCompatActivity
import android.speech.RecognitionListener
import android.speech.RecognizerIntent
import android.speech.SpeechRecognizer
import android.util.Log
import android.view.View
import android.widget.EditText
import android.widget.TextView
import android.widget.Toast
import androidx.core.os.HandlerCompat.postDelayed
import kotlinx.android.synthetic.main.activity_main.*

import org.json.JSONArray
import org.json.JSONObject
import java.io.*
import java.net.HttpURLConnection
import java.net.URL


class MainActivity : AppCompatActivity() {
    val TAG = MainActivity::class.java.simpleName
    lateinit var handler: Handler
    lateinit var lblStatus: TextView
    lateinit var txtRaspiIPAddr: EditText
    var MSG_DETECT_STARTUP_SYSTEM = 1001
    var MSG_STATUS_REFRESH = 1002

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        lblStatus = findViewById(R.id.lblStatus)
        txtRaspiIPAddr = findViewById(R.id.txtRaspiIPAddr)
        handler = Handler(object: Handler.Callback {
            override fun handleMessage(msg: Message): Boolean {
                if (msg.what == MSG_DETECT_STARTUP_SYSTEM){
                    val task = object: AsyncTask<String, String, String>() {
                        override fun doInBackground(vararg param: String?): String {

                            var result:String = ""

                            val url = URL(param[0])
                            val httpClient = url.openConnection() as HttpURLConnection
                            httpClient.setReadTimeout(3000)
                            httpClient.setConnectTimeout(3000)
                            httpClient.requestMethod = param[1]

                            httpClient.instanceFollowRedirects = false
                            httpClient.doOutput = true
                            httpClient.doInput = true
                            httpClient.useCaches = false
                            httpClient.setRequestProperty("Content-Type", "application/json; charset=utf-8")
                            try {
                                httpClient.connect()
                                val os = httpClient.getOutputStream()
                                val writer = BufferedWriter(OutputStreamWriter(os, "UTF-8"))
                                writer.write(param[1])
                                writer.flush()
                                writer.close()
                                os.close()

                                if (httpClient.responseCode == HttpURLConnection.HTTP_OK) {
                                    val stream = BufferedInputStream(httpClient.inputStream)
                                    val data: String = readStream(inputStream = stream)
                                    return data
                                } else {
                                    println("ERROR ${httpClient.responseCode}")
                                }
                            } catch (e: Exception) {
                                e.printStackTrace()
                            } finally {
                                httpClient.disconnect()
                            }

                            return ""
                        }

                        fun readStream(inputStream: BufferedInputStream): String {
                            val bufferedReader = BufferedReader(InputStreamReader(inputStream))
                            val stringBuilder = StringBuilder()
                            bufferedReader.forEachLine { stringBuilder.append(it) }
                            return stringBuilder.toString()
                        }

                        override fun onPostExecute(result: String?) {
                            var status = ""
                            if ( result != ""){
                                val toast = Toast.makeText(applicationContext, result, Toast.LENGTH_LONG)
                                toast.show()
                            }
                        }
                    }
                    if ("${txtRaspiIPAddr.text}" != "") {
                        val addr = "http://${txtRaspiIPAddr.text}:3000/state"
                        val poststring =  "{\"status\": 3}"
                        task.execute(addr, poststring)
                    }else {
                        val toast = Toast.makeText(applicationContext, "アドレス入れろ", Toast.LENGTH_LONG)
                        toast.show()
                    }
                    return true
                }
                if (msg.what == MSG_STATUS_REFRESH) {
                    val task = object: AsyncTask<String, String, Long>() {
                        override fun doInBackground(vararg param: String?): Long {

                            var result:Long = 0L

                            val url = URL(param[0])
                            val httpClient = url.openConnection() as HttpURLConnection
                            httpClient.setReadTimeout(3000)
                            httpClient.setConnectTimeout(3000)
                            httpClient.requestMethod = "GET"

                            try {
                                if (httpClient.responseCode == HttpURLConnection.HTTP_OK) {
                                    val stream = BufferedInputStream(httpClient.inputStream)
                                    val data: String = readStream(inputStream = stream)
                                    val returnData = JSONObject(data)
                                    if (returnData.has("status")) {
                                        result = returnData.getLong("status")
                                    }
                                } else {
                                    println("ERROR ${httpClient.responseCode}")
                                }
                            } catch (e: Exception) {
                                e.printStackTrace()
                            } finally {
                                httpClient.disconnect()
                            }

                            return result
                        }

                        fun readStream(inputStream: BufferedInputStream): String {
                            val bufferedReader = BufferedReader(InputStreamReader(inputStream))
                            val stringBuilder = StringBuilder()
                            bufferedReader.forEachLine { stringBuilder.append(it) }
                            return stringBuilder.toString()
                        }
                        override fun onPostExecute(result: Long?) {
                            var status = ""
                            if ( result == 0L){
                                status = "Status: Unknown"
                            }
                            if (result == 1L ){
                                status = "Status: Init"
                            }
                            if (result == 2L) {
                                status = "Status: Wait Voice Detect"
                            }
                            if (result == 3L ) {
                                status = "Status: Wait Face Detect"
                            }
                            if (result == 4L ){
                                status = "Status: Face Detecting"
                                tweetGirlFallDown()
                            }
                            lblStatus.text = status
                        }
                    }
                    if ("${txtRaspiIPAddr.text}" != "") {
                        val addr = "http://${txtRaspiIPAddr.text}:3000/state"
                        task.execute(addr)
                    }
                }
                return false
            }
        })

        val timerHandler = Handler()
        val r = object : Runnable {
            internal var count = 0
            override fun run() {
                val msg = Message.obtain()
                msg.what = MSG_STATUS_REFRESH
                handler.sendMessage(msg)
                timerHandler.postDelayed(this, 3000)
            }
        }
        timerHandler.post(r)
    }

    fun tweetGirlFallDown() {
        val intent = Intent (Intent.ACTION_VIEW);
        val messsage = Uri.encode("親方空から女の子が! #親方空から女の子がシステム");
        intent.setData(Uri.parse("twitter://post?message=" + messsage));
        startActivity(intent);
    }

    fun onClickBtnVoiceDetect(v: View) {
        val intent = Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH)
        intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, RecognizerIntent.LANGUAGE_MODEL_FREE_FORM)
        intent.putExtra(RecognizerIntent.EXTRA_PREFER_OFFLINE, true)
        intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, packageName)
        val recognizer = SpeechRecognizer.createSpeechRecognizer(applicationContext)
        recognizer.setRecognitionListener(object : RecognitionListener {
            override fun onReadyForSpeech(p0: Bundle?) {}
            override fun onRmsChanged(p0: Float) {}
            override fun onBufferReceived(p0: ByteArray?) {}
            override fun onPartialResults(p0: Bundle?) {}
            override fun onEvent(p0: Int, p1: Bundle?) {}
            override fun onBeginningOfSpeech() {}
            override fun onEndOfSpeech() {}
            override fun onError(p0: Int) {}
            override fun onResults(results: Bundle?) {
                if (results == null) {
                    return
                }
                val texts = results!!.getStringArrayList(android.speech.SpeechRecognizer.RESULTS_RECOGNITION)
                var detectKeyword = false
                texts?.let {
                    for (text in texts) {
                        Log.w(TAG, "recognize: $text")
                        if (text == "起動開始") {
                            detectKeyword = true
                            break
                        }
                    }
                    if (!detectKeyword) {
                        val msg = Message.obtain()
                        msg.what = MSG_DETECT_STARTUP_SYSTEM
                        handler.sendMessage(msg)
                    }
                }
            }
        })
        recognizer.startListening(intent)
    }
}

ハードを組む

今回動かすもの整理

no name voltage[v] description
1 MAiX BiT 5
2 Raspberry Pi 3B 5
3 ハロウィンライト 3 ラズパイから制御
4 警報機 3 ラズパイから制御
5 パトランプ 12 ラズパイから制御。リレー経由で動かす
6 車用パトランプ 12 パトランプと同一タイミングで動かしたかったけど入手間に合わず。

電圧変換基盤

  • 12[v]はバッテリーからとる
    • バッテリーで電源切り離せるようにする
    • 10[A]のヒューズ入れる
  • 12[v]から3[v],5[v]を三端子レギュレータで作成する。
  • 入出力
    • 入力は12[v]
    • 3[v]出力の口が3つ
    • 5[v]出力の口が2つ
    • 12[v]出力の口が2つ
  • 回路図は気が向いたら乗せる。

こんな感じ

image.png

変換は12[v]->3[v]。12[v]->5[v]->3[v]の方が良いらしいけどやってない。

制御基板

  • ハロウィンライト、警報機はフォトカプラ + NPNトランジスタを利用
  • パトランプに関してはフォトカプラ + NPNトランジスタ + リレー
  • 回路図は気が向いたら乗せる。

こんな感じ

image.png

万が一用に手動でも動かせるようにしておいた。

ふりかえり

シータ役の方に落ちてきた位置を調整してもらいつつ、落ちてきた女の子(実際は横移動の男の子)を認識できた。
やる事やれて嬉しい。

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

PermissionsDispatcherで気軽にPermissionを付与する

背景

プロジェクトにPermissionsDispatcherが導入されていたので色々触ってみる

使い方

順番に使い方を記載してく。

1. Activity / FragmentにRuntimePermissionsアノテーションをつける

以下のような形。

MainActivity.kt
@RuntimePermissions
class MainActivity : AppCompatActivity() {
MainFragment.kt
@RuntimePermissions
class MainFragment : DaggerFragment() {

2. 欲しいパーミッションの取得に成功している or 失敗した後に実行される関数を実装

MainActivity.kt
    fun readDeviceData() {
       getIMEI()
    }

3. 2.で作成した関数にNeedPermissionアノテーションをつける

NeedPermissionの中には欲しい権限情報を記載する

MainActivity.kt
    @NeedsPermission(Manifest.permission.READ_PHONE_STATE)
    fun readDeviceData() {
       getIMEI()
    }

4. ビルドする

自動的にコードが生成される

5. 関数名 + WithPermissionCheck()という名で拡張関数ができているので呼ぶ。

MainActivity.kt
   override fun onCreate(savedInstanceState: Bundle?) {
       readDeviceDataWithPermissionCheck()
   }

6. onRequestPermissionsResultをoverrideし、生成された拡張関数onRequestPermissionsResultを呼ぶ

MainActivity.kt
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        onRequestPermissionsResult(requestCode, grantResults)
    }

以上。

自動生成されたコード

こんな感じ。ほぼほぼルーチン作業だったパーミッションコードが自動生成されている。
すでにパーミッションが設定されている場合でも呼び出してくれる形なので便利。

MainActivityPermissionsDispatcher.kt
private const val REQUEST_READDEVICEDATA: Int = 2

private val PERMISSION_READDEVICEDATA: Array<String> =
    arrayOf("android.permission.READ_PHONE_STATE")

fun MainActivity.readDeviceDataWithPermissionCheck() {
  if (PermissionUtils.hasSelfPermissions(this, *PERMISSION_READDEVICEDATA)) {
    readDeviceData()
  } else {
    ActivityCompat.requestPermissions(this, PERMISSION_READDEVICEDATA,
        REQUEST_READDEVICEDATA)
  }
}

fun MainActivity.onRequestPermissionsResult(requestCode: Int, grantResults: IntArray) {
  when (requestCode) {
    REQUEST_STARTLOCATIONSAMPLING ->
     {
      if (PermissionUtils.verifyPermissions(*grantResults)) {
        readDeviceData()
      }
    }
  }
}

はまったところ。

二つパーミッションを設定したくて別々の関数を作って連続で呼び出したが、後者が呼び出されないことがあった。
連続してパーミッションを取得したい場合は@NeedPermissionに二つ設定した方が良さそう。

@NeedsPermission(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_PHONE_STATE)
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

PermissionsDispatcherで楽にRuntime Permissionを付与する

背景

プロジェクトにPermissionsDispatcherが導入されていたので色々触ってみる

PermissionsDispatcherとは

Android 6.0(M) 以降から導入されたruntime permissionの実装を楽にしてくれるライブラリ。
https://github.com/permissions-dispatcher/PermissionsDispatcher

runtime permissionについて
https://developer.android.com/training/permissions/requesting

導入方法

build.gradleに以下を記載。

apply plugin: 'kotlin-kapt'

dependencies {
  implementation "org.permissionsdispatcher:permissionsdispatcher:${latest.version}"
  kapt "org.permissionsdispatcher:permissionsdispatcher-processor:${latest.version}"
}

自動生成なのでkotlin-kaptプラグインを忘れないように。

使い方

順番に使い方を記載してく。

1. Activity / FragmentにRuntimePermissionsアノテーションをつける

以下のような形。

MainActivity.kt
@RuntimePermissions
class MainActivity : AppCompatActivity() {
MainFragment.kt
@RuntimePermissions
class MainFragment : DaggerFragment() {

2. 欲しいパーミッションの取得に成功している or 失敗した後に実行される関数を実装

MainActivity.kt
    fun readDeviceData() {
       getIMEI()
    }

3. 2.で作成した関数にNeedPermissionアノテーションをつける

NeedPermissionの中には欲しい権限情報を記載する

MainActivity.kt
    @NeedsPermission(Manifest.permission.READ_PHONE_STATE)
    fun readDeviceData() {
       getIMEI()
    }

4. ビルドする

自動的にコードが生成される

5. 関数名 + WithPermissionCheck()という名で拡張関数ができているので呼ぶ。

MainActivity.kt
   override fun onCreate(savedInstanceState: Bundle?) {
       readDeviceDataWithPermissionCheck()
   }

6. onRequestPermissionsResultをoverrideし、生成された拡張関数onRequestPermissionsResultを呼ぶ

MainActivity.kt
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        onRequestPermissionsResult(requestCode, grantResults)
    }

以上。

自動生成されたコード

こんな感じ。ほぼほぼルーチン作業だったパーミッションコードが自動生成されている。
すでにパーミッションが設定されている場合でも呼び出してくれる形なので便利。

MainActivityPermissionsDispatcher.kt
private const val REQUEST_READDEVICEDATA: Int = 2

private val PERMISSION_READDEVICEDATA: Array<String> =
    arrayOf("android.permission.READ_PHONE_STATE")

fun MainActivity.readDeviceDataWithPermissionCheck() {
  if (PermissionUtils.hasSelfPermissions(this, *PERMISSION_READDEVICEDATA)) {
    readDeviceData()
  } else {
    ActivityCompat.requestPermissions(this, PERMISSION_READDEVICEDATA,
        REQUEST_READDEVICEDATA)
  }
}

fun MainActivity.onRequestPermissionsResult(requestCode: Int, grantResults: IntArray) {
  when (requestCode) {
    REQUEST_STARTLOCATIONSAMPLING ->
     {
      if (PermissionUtils.verifyPermissions(*grantResults)) {
        readDeviceData()
      }
    }
  }
}

二つ同時にパーミッションを取得したい場合

連続してパーミッションを取得したい場合は@NeedPermissionに二つ設定する。

@NeedsPermission(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_PHONE_STATE)

まとめ

他にもユーザーがアクセス許可を付与しない場合に呼び出されるメソッドやなぜ許可が必要なのかを表示するためのメソッドなども指定できるので、ちょっとだけ楽になる。

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

PermissionsDispatcherでRuntime Permissionをちょっとだけ楽にする

背景

プロジェクトにPermissionsDispatcherが導入されていたので色々触ってみる

PermissionsDispatcherとは

Android 6.0(M) 以降から導入されたruntime permissionの実装を楽にしてくれるライブラリ。
https://github.com/permissions-dispatcher/PermissionsDispatcher

runtime permissionについて
https://developer.android.com/training/permissions/requesting

導入方法

build.gradleに以下を記載。

apply plugin: 'kotlin-kapt'

dependencies {
  implementation "org.permissionsdispatcher:permissionsdispatcher:${latest.version}"
  kapt "org.permissionsdispatcher:permissionsdispatcher-processor:${latest.version}"
}

自動生成なのでkotlin-kaptプラグインを忘れないように。

使い方

順番に使い方を記載してく。

1. Activity / FragmentにRuntimePermissionsアノテーションをつける

以下のような形。

MainActivity.kt
@RuntimePermissions
class MainActivity : AppCompatActivity() {
MainFragment.kt
@RuntimePermissions
class MainFragment : DaggerFragment() {

2. 欲しいパーミッションの取得に成功している or 失敗した後に実行される関数を実装

MainActivity.kt
    fun readDeviceData() {
       getIMEI()
    }

3. 2.で作成した関数にNeedPermissionアノテーションをつける

NeedPermissionの中には欲しい権限情報を記載する

MainActivity.kt
    @NeedsPermission(Manifest.permission.READ_PHONE_STATE)
    fun readDeviceData() {
       getIMEI()
    }

4. ビルドする

自動的にコードが生成される

5. 関数名 + WithPermissionCheck()という名で拡張関数ができているので呼ぶ。

MainActivity.kt
   override fun onCreate(savedInstanceState: Bundle?) {
       readDeviceDataWithPermissionCheck()
   }

6. onRequestPermissionsResultをoverrideし、生成された拡張関数onRequestPermissionsResultを呼ぶ

MainActivity.kt
    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        onRequestPermissionsResult(requestCode, grantResults)
    }

以上。

自動生成されたコード

こんな感じ。ほぼほぼルーチン作業だったパーミッションコードが自動生成されている。
すでにパーミッションが設定されている場合でも呼び出してくれる形なので便利。

MainActivityPermissionsDispatcher.kt
private const val REQUEST_READDEVICEDATA: Int = 2

private val PERMISSION_READDEVICEDATA: Array<String> =
    arrayOf("android.permission.READ_PHONE_STATE")

fun MainActivity.readDeviceDataWithPermissionCheck() {
  if (PermissionUtils.hasSelfPermissions(this, *PERMISSION_READDEVICEDATA)) {
    readDeviceData()
  } else {
    ActivityCompat.requestPermissions(this, PERMISSION_READDEVICEDATA,
        REQUEST_READDEVICEDATA)
  }
}

fun MainActivity.onRequestPermissionsResult(requestCode: Int, grantResults: IntArray) {
  when (requestCode) {
    REQUEST_STARTLOCATIONSAMPLING ->
     {
      if (PermissionUtils.verifyPermissions(*grantResults)) {
        readDeviceData()
      }
    }
  }
}

二つ同時にパーミッションを取得したい場合

連続してパーミッションを取得したい場合は@NeedPermissionに二つ設定する。

@NeedsPermission(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.READ_PHONE_STATE)

まとめ

他にもユーザーがアクセス許可を付与しない場合に呼び出されるメソッドやなぜ許可が必要なのかを表示するためのメソッドなども指定できるので、ちょっとだけ楽になる。

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

Retrofit2とOkhttp3を使ったAPI開発

RetrofitとOkHttpを使ってWebAPIを呼び出します。

今回利用するAPIはQiitを使います
https://qiita.com/api/v2/items?page=1&per_page=20

極力コメントアウトして、何が書かれているか残しときました。

RetrofitとOkhttpとは

RetrofitはOkHttpラッパーでありHTTPクライアントを実装する際によく利用されます。
最近のサービスで通信が必要な処理を実装する時はほとんど使われているそうです。

HTTP通信を実装する際にコールバックの処理を書きやすくします。

手順

1.gradleとManifest
2.interface
3.Model
4.クライアント
5.呼び出し

1.gradleとManifest

Gradle Scriptsのbuild.gradle(Module.app)の所にライブラリ導入

build.gradle
dependencies{
    implementation 'com.squareup.retrofit2:retrofit:2.6.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.6.0'

   //書き方として以下もある
   //def retrofitVersion = '2.6.0'
   //implementation 'com.squareup.retrofit2:retrofit:$retrofitVersion'
   //implementation 'com.squareup.retrofit2:converter-gson:$retrofitVersion'
}

今回ネットワークを利用した通信なのでAndroidManifestに以下を記入

app/src/main/AndroidManifest.xml
<manifest xmlns:android=
~省略
//以下を記入
<uses-permission android:name="android.permission.INTERNET"/>

    <application
//~省略

2.interface

今回は HTTP の GET メソッドを使うため@GET をつけました。

ApiService.kt
interface ApiService {

    //パラメータの前までをここに書く
    @GET("api/v2/items")
    //呼び出す際に必要なリクエストURLのパラメータの設定
    fun apiDemo(
        @Query("page") page: Int,
        @Query("par_page") perPage: Int
    ): Call<List<QiitResponse>>
}

3.Model

APIから取り出したいデータを格納するデータクラス

QiitResponse.kt
//後に欲しいデータを取得する際に必要となる箱
data class QiitResponse(
    val url: String?,
    val title: String?,
    val user: User?
)

//階層が分かれている時は分ける
data class User(
    val id: String?
)

実際に表示する際に必要となるクラス

Model.kt
class Model {
    var title: String? = null
    var id: String? = null
    var url: String? = null
}

4.クライアント

Client.kt
    //Clientを作成
    val httpBuilder: OkHttpClient.Builder get() {
        //httpClinetのBuilderを作る
        val httpClient = OkHttpClient.Builder()
        //create http client headerの追加
            httpClient.addInterceptor(Interceptor { chain ->
                val original = chain.request()
                val request = original.newBuilder()
                    .header("Accept", "application/json")
                    .method(original.method(), original.body())
                    .build()
                //proceedメソッドは再びパーミッション許可ダイアログを表示してその結果を返します
                var response = chain.proceed(request)

                return@Interceptor response
            })
            .readTimeout(30, TimeUnit.SECONDS)

        //log
        val loggingInterceptor = HttpLoggingInterceptor()
        loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY
        httpClient.addInterceptor(loggingInterceptor)

        return httpClient
    }

     //繋ぎこみ
     fun createService(): ApiService {
          var client = httpBuilder.build()
          var retrofit = Retrofit.Builder()
              .baseUrl("https://qiita.com/")//基本のurl設定
              .addConverterFactory(GsonConverterFactory.create())//Gsonの使用
              .client(client)//カスタマイズしたokhttpのクライアントの設定
              .build()
          //Interfaceから実装を取得
          var API = retrofit.create(ApiService::class.java)

          return API
      }

5.呼び出し

a.kt
//データリストに保存し、そのデータの取得
    fun fetchAllUserData(): List<Model> {

        val dataList = mutableListOf<Model>()
        //リクエストURl作成してデータとる パラメータの引数の設定も行う
        Retrofit.createService().apiDemo(page = 1, perPage = 20).enqueue(object : Callback<List<QiitResponse>> {

            //非同期処理
            override fun onResponse(call: Call<List<QiitResponse>>, response: Response<List<QiitResponse>>) {
                Log.d("TAGres","onResponse")

                //ステータスコードが200:OKなので、ここではちゃんと通信できたよ
                if (response.isSuccessful) {
                    response.body()?.let {
                        for (item in it) {
                            val data: Model = Model().also {
                                //取得したいものをAPIから手元のリスト(Model)に
                                it.title = item.title
                                it.url = item.url
                                it.id = item.user!!.id
                            }
                            //取得したデータをModelに追加
                            dataList.add(data)
                        }
                        //今回recyclerViewを利用しているが、これを書かないと先に画面の処理が終えてしまうので表示されなくなります。
                        recyclerView.adapter?.notifyDataSetChanged()
                    }
                } else {
                }
            }
            override fun onFailure(call: Call<List<QiitResponse>>, t: Throwable) {
                Log.d("TAGres","onFailure")
            }
        })
        return dataList
    }

Okhttpを利用するとonResponseとonFailureでコールバックを用いた非同期通信が利用できる。

終わりに

初めてのAPIの周りなのでもっとうまく書ける方法はあると思いますが、自分がわかるようにまとめました。
1つの参考資料になってくれたら嬉しいです。
また、何か違う部分ありましたら、コメントいただけると嬉しいです。

次はMoshi、RxJavaを取り入れてみたいです。

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

App Crawlerによる自動UIテストにスクリプト制御を加える

  • そもそもApp Crawlerとはなんぞや?という方はこちらの記事がわかりやすいと思います。簡単に言うと、Androidアプリをテストコード無しに自動UIテストしてくれるもの。

App Crawlerに関して調べていて、こちらのスライドの「スクリプトを用意することで制御をいれることも可能(ログインとか)」の一文を見つけ、App Crawlerによる自動UItestにスクリプト制御を入れてみることにした。

が、App Crawler用の公式ドキュメントが用意されておらず、Firebase Test Lab のRoboテストのドキュメントやApp Crawlerのソースコードを見ながら苦労したので共有。

手順

  1. Roboスクリプトを作ります。これはもともとFirebase Test LabのRoboテストにスクリプト制御を入れるためのスクリプトで、Android Studioで作れます。(作り方の1~4までを行う。)
  2. 1で作ったスクリプトをApp Crawler実行時にオプション--robo-script-fileを付けて渡してやる。
    例 :
java -jar ./app-crawler/crawl_launcher.jar --android-sdk $ANDROID_HOME --app-package-name com.kkkkan.hogehoge --robo-script-file MainActivity_robo_script.json

以上。

ハマったポイント

恐らくこれはRoboスクリプトのバグだと思うのだが、画面に表示されているViewに表示されている文字列が日本語だとRoboスクリプトのtextフィールドが文字化けする。(ボタンのandroid:textが「同意する」の時など。)
スクリプト制御でタッチしてほしいViewのtextフィールドが文字化けしているとうまく動かない。(ほかの部分は化けてても大丈夫っぽい。)

  • この場合、textフィールドを空文字列にするとうまく動く。(ただし、タッチしてほしいViewにアプリのソースのlayoutファイルの方でちゃんとresource idを設定しておかなきゃダメ。)
  • Roboスクリプトの構成としては、elementDescriptorsフィールドの配列の最初の要素がタッチされるViewになっているみたいだ。

例 :
(Before)

[
  {
    "eventType": "VIEW_CLICKED",
    "timestamp": 1569478244303,
    "replacementText": "���߂",
    "actionCode": -1,
    "delayTime": 0,
    "canScrollTo": false,
    "elementDescriptors": [
      {
        "className": "androidx.appcompat.widget.AppCompatButton",
        "recyclerViewChildPosition": -1,
        "adapterViewChildPosition": -1,
        "groupViewChildPosition": 0,
        "resourceId": "com.kkkkan.hogehoge:id/button",
        "contentDescription": "",
        "text": "��"
      },
      (後略)

(After)

[
  {
    "eventType": "VIEW_CLICKED",
    "timestamp": 1569478244303,
    "replacementText": "���߂",
    "actionCode": -1,
    "delayTime": 0,
    "canScrollTo": false,
    "elementDescriptors": [
      {
        "className": "androidx.appcompat.widget.AppCompatButton",
        "recyclerViewChildPosition": -1,
        "adapterViewChildPosition": -1,
        "groupViewChildPosition": 0,
        "resourceId": "com.kkkkan.hogehoge:id/button",
        "contentDescription": "",
        "text": ""
      },
      (後略)

今回知った事

現在、App Crawlerのオプションについて公式ドキュメントにも記述があるが、実はここで紹介されているよりも沢山のオプションがあるらしい。

https://dl.google.com/appcrawler/beta1/app-crawle.zip でapp-crawler.zipをダウンロードし解凍 → 更にその中のcrawl_launcher.jarをzipファイルとして解凍してみた。すると、
app-crawler\crawl_launcher\androidx\test\tools\crawler\launcher\setup\CrawlSetup.javaにApp Crawlerのオプションと思われるものがたくさん定義されていた。
以下、そのコピー

public abstract class CrawlSetup {
    public static final String DEVICE_OUTPUT = "/sdcard/app_firebase_test_lab";
    public static final String ROBO_TEMP_FILES = "/sdcard/robo_tmp_files";
    private static final String APP_PACKAGE_NAME_FLAG = "--app-package-name";
    private static final String APK_FILE_FLAG = "--apk-file";
    private static final String GUIDE_FILE_FLAG = "--guide-file";
    private static final String TEXT_GUIDE_FILE_FLAG = "--text-guide-file";
    private static final String ROBO_SCRIPT_FILE_FLAG = "--robo-script-file";
    private static final String EXPERIMENTS_FILE_FLAG = "--experiments-file";
    private static final String CRAWL_TIMEOUT_FLAG = "--timeout-sec";
    private static final String OUTPUT_DIR_FLAG = "--output-dir";
    private static final String UI_AUTOMATOR_MODE_FLAG = "--ui-automator-mode";
    private static final String INSTANT_APPS_MODE_FLAG = "--instant-apps-mode";
    private static final String TEST_ACCESSIBILITY_FLAG = "--test-accessibility";
    private static final String WAKE_DEVICE_FLAG = "--wake-device";
    private static final String PAUSE_FLAG = "--pause";
    private static final String DEVICE_NAME_REGEX_FLAG = "--device-name";
    private static final String DEVICE_SERIAL_CODE_FLAG = "--device-serial-code";
    private static final String PRINT_NON_SDK_USAGE = "--print-non-sdk-usage";

 (後略)

いつか時間があったら、他のオプションについてもどんなものなのか調べてみたい。

感想

  • 初回起動時には、自動的に立ち上がる利用規約画面で同意ボタンを押さないと次に進めないアプリをApp Crawlerでテストしようとしていて、今まで制御を入れないとどうしても同意しないボタンを押されてしまい初回起動時に関して自動テストできなくて困っていたが、これでできるようになってうれしい。
  • Roboスクリプトをオプションで渡せばいいのであろうことは雰囲気ですぐわかったが、肝心のオプション名が「恐らく存在しているであろうオプション名がまとまって定義されているクラスをcrawl_launcher.jarを解凍して探せばいいのでは…?」と気づくまで、ググってみたりあてずっぽうでそれっぽいの入れてみたりしてもうまく探し当てられなくて大変だった。
    --helpってしてcrawl_launcher.jarを実行してみても無反応だったし。
  • そもそも、公式ドキュメントにちゃんと全てのオプションの記述を入れるか、crawl_launcher.jarに--helpオプションを作ってほしい。

参考にしたもの

https://speakerdeck.com/tkmnzm/introduce-jetpack-app-crawler-tool
https://qiita.com/kafumi/items/fff5324a4bb70b7b7986

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

Android10から出たダークテーマにアプリを対応させないようにするには

はじめに

実機のAndroid10でダークテーマにしたらアプリの色が予期せぬ色になってしまったのでダークテーマを無視して
今まで通りのアプリの色を表示したい!

環境

macOS Mojave 10.14.6
AndroidStudio3.5
compileSdkVersion 28
minSdkVersion 21

アプリでダークテーマを無視する

Googleさんは非推奨かもだけど、暫定対応。

元々のstyles.xml
<style name="AppTheme" parent="Theme.AppCompat.DayNight.NoActionBar">
変更後のstyles.xml
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidの各webブラウザアプリの挙動調査

androidのwebブラウザアプリからリンクタッチで自アプリを起動するときに、各webブラウザアプリがintentにどのようなフラグを付けているか調べた

背景

自アプリから自webページをwebブラウザアプリを起動して閲覧
→ webブラウザアプリで色々やって最終的に自webページ上のリンクをタッチ
→ そのリンクを自アプリのActivityに設定したintentfilterを用いて拾ってやり、自アプリのActivityを起動
ということをするにあたってActivityのlaunchModeなどを決めるため各webブラウザアプリの挙動を知りたくなった。

調べたもの

Huawei nova3 (android 9) :

  • chrome
  • FireFox

Fujitsu F-05J (android 7.1.1) :

  • chrome
  • FireFox
  • プリインストールブラウザアプリ

SONY SO-02E(android 4.4.2) :

  • chrome
  • プリインストールブラウザアプリ

結果

chrome : 3端末とも FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_CLEAR_TOP
FireFox : 2端末ともフラグ無し
プリインストールブラウザアプリ : 2端末ともフラグ無し

  • chrome以外はフラグ無し。
  • ユーザーがchromeで開いてもそれ以外のアプリで開いても同じ挙動になるようにするにはintentfilterを付けるActivityのlaunchModeをsinglesingle taskにしたらよさそう。それでもchromeはFLAG_ACTIVITY_CLEAR_TOPも使用しているので完全に同じ動きにはならないけど…

調べ方

  • 最初はテスト用に、自アプリ→webアプリ→自アプリという動きをするアプリを作って、端末にインストールし、各webアプリごと・各launch modeごとに、webから自アプリに戻ってきた後に端末の戻るボタンを押してどういう挙動をするかということを地道に調べた。その結果をもとに推理した。(すごく時間がかかった)

  • その後、システムがLogcatにintentが飛ばされた時にどのフラグが使われているか見れるログを出しているということを聞き、再度調べてみた。

裏話

テストアプリで調べて推理した結果では実は

  • chromeはFLAG_ACTIVITY_NEW_TASKを使用している
  • それ以外のアプリはフラグを使用していない

という結論になっていたのだが、実はchromeはFLAG_ACTIVITY_CLEAR_TOPフラグも使用していることが発覚した。
Logcatで調べる方が簡単だし得られる結果が正確だった。

感想

Logcatで意外に色々情報が見れる事があるみたいなので、今後何か疑問に思った時に調査のためにテストアプリを作って時間をかけたりする前に一度Logcatをじっと眺めてほしい情報が落ちていないか確認していきたい。

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

Android 10のScoped Storage(対象範囲別ストレージ)に写真を保存する

Android 10きた

新しく追加されたナビゲーション操作は快適です。◀●■より使いやすい。
ダークモードも簡単に実装できる。Force Darkってやつかな。styles.xmlのThemeに一行書き足せばダークモードに対応できる。

<item name="android:forceDarkAllowed">true</item>

そして対象範囲別ストレージ(Scoped Storage)ですよ。

あとイースターエッグが面白いので遊んでみてね。アイコンを完成させるゲームです。

対象範囲別ストレージとは

アプリごとにフォルダが作られて、そこでファイルを保存したりする。
サンドボックスってやつ?
ちなみにアプリごとに作られたフォルダにアクセスするときは権限無しで読み書きできます。
(そのアプリ専用のフォルダなので自由に使えます。)

あとこのドキュメントが日本語版と英語版で内容が違うっぽいので英語の方も見ましょう。

getExternalStorageDirectory()は非推奨に

今までのように自由な場所にフォルダを作ったりはできなくなったっぽい?
あれこれ影響やばくない?

本題

今回は写真をバックアップしてくれるアプリを作ろうと思います。理由は消されても泣き寝入りしないように。
対象範囲別ストレージに写真を保存します。
写真は端末のギャラリーに入ってるものを利用する。

環境

端末 Pixel 3 XL
バージョン Android 10
言語 Kotlin
compileSdkVersion 29

レイアウト

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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"
    android:gravity="center"
    android:orientation="vertical"
    tools:context=".MainActivity">


    <Button
        android:id="@+id/image_picker_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="画像一覧" />

    <TextView
        android:gravity="center"
        android:id="@+id/progress_textview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="じょうたい" />

</LinearLayout>

画像を選ぶ画面を出す

よく見るやつですね。

Screenshot_1569517621.png

コードはこちら

    val imageOpenCode = 845

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

        //ギャラリー出す
        image_picker_button.setOnClickListener {
            val intent = Intent(Intent.ACTION_PICK)
            intent.type = "image/*";
            startActivityForResult(intent, imageOpenCode)
        }
    }

ボタンを押したら画像選択画面が表示されるはずです。

写真を受け取る

選んだ画像の情報を得るにはonActivityResultを用意します。
UriはPictures/(以下略とかの絶対パスではないため注意です。

       override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        //画像を選んだらここに来る
        if (requestCode == imageOpenCode && resultCode == Activity.RESULT_OK) {
            //正しいかチェックした
            val uri = data?.data
            if (uri != null) {
                //保存する
                writeScopedStorage(uri)
            }
        }
       }

対象範囲別ストレージに保存する

ようやく本題です。長かったね。

    fun writeScopedStorage(uri: Uri) {
        //ScopedStorageで作られるサンドボックスへのぱす
        val scopedStoragePath = getExternalFilesDir(null)
        //写真ファイル作成
        val file = File("${scopedStoragePath?.path}/test.jpg")

        //InputStream 元データの方?
        val inputStream = contentResolver.openInputStream(uri)
        //OutputStream さっき作ったFileに書き込む
        val outputStream = file.outputStream()

        //かきかき
        thread {
            while (true) {
                val data = inputStream?.read()
                if (data == -1) {
                    //終わったらぬける
                    break
                }
                if (data != null) {
                    outputStream.write(data)
                }
            }
            runOnUiThread {
                progress_textview.text = "コピーが完了しました。"
            }
        }
    }

画像を選んで少し待つとボタンの下のTextViewのテキストが「コピーが完了しました。」に変わるので変わったらファイルマネージャーで見てみましょう。

Screenshot_20190927-022518.png

範囲別ストレージの場所はどこ?

/sdcard/Android/data/アプリケーションID/files
です!

/sdcard/Android/data まで進んだら新しく追加された順に並び直せば探すのが楽になります。

名前を写真の名前にできないの?

このままでは画像ファイルが「test.png」っていう名前が固定されているので直しましょう。

    /**
     * 画像の名前を取得。MediaStore(写真とかの情報データベース)に問い合わせる
     * @param uri Uriです。
     * */
    fun getFileName(uri: Uri): String? {
        val cursor = contentResolver.query(
            uri,
            null,
            null,
            null,
            null
        )
        //null ちぇっく
        if (cursor != null) {
            //一番上に移動
            if (cursor.moveToFirst()) {
                return cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME))
            }
            //とじようね!
            cursor.close()
        }
        return "test.jpg"
    }

これが画像の名前を取得するまでです。なっが!

全部くっつけたコード

class MainActivity : AppCompatActivity() {

    val imageOpenCode = 845

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

        //ギャラリー出す
        image_picker_button.setOnClickListener {
            val intent = Intent(Intent.ACTION_PICK)
            intent.type = "image/*";
            startActivityForResult(intent, imageOpenCode)
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        //画像を選んだらここに来る
        if (requestCode == imageOpenCode && resultCode == Activity.RESULT_OK) {
            //正しいかチェックした
            val uri = data?.data
            if (uri != null) {
                //保存する
                writeScopedStorage(uri)
            }
        }
    }

    fun writeScopedStorage(uri: Uri) {
        //ScopedStorageで作られるサンドボックスへのぱす
        val scopedStoragePath = getExternalFilesDir(null)
        //写真ファイル作成
        val file = File("${scopedStoragePath?.path}/${getFileName(uri)}")

        //InputStream 元データの方?
        val inputStream = contentResolver.openInputStream(uri)
        //OutputStream さっき作ったFileに書き込む
        val outputStream = file.outputStream()

        //かきかき
        thread {
            while (true) {
                val data = inputStream?.read()
                if (data == -1) {
                    //終わったらぬける
                    break
                }
                if (data != null) {
                    outputStream.write(data)
                }
            }
            runOnUiThread {
                progress_textview.text = "コピーが完了しました。"
            }
        }
    }

    /**
     * 画像の名前を取得。MediaStore(写真とかの情報データベース)に問い合わせる
     * @param uri Uriです。
     * */
    fun getFileName(uri: Uri): String? {
        val cursor = contentResolver.query(
            uri,
            null,
            null,
            null,
            null
        )
        //null ちぇっく
        if (cursor != null) {
            //一番上に移動
            if (cursor.moveToFirst()) {
                return cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DISPLAY_NAME))
            }
            //とじようね!
            cursor.close()
        }
        return "test.jpg"
    }

}

おわりに

ファイルの書き込みの部分が遅いんだけど速くなりませんかね・・?

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

Fragmentの初期配置ってどうすのがいいんだっけ

調べたこと

FragmentをActivityにaddする処理の書き方について

背景

コードレビューにてこんなコードを見つけました。

savedInstanceState ?: run {
    val fragment = HogeFragment.newInstance()
    supportFragmentManager
            .beginTransaction()
            .add(R.id.container, fragment)
            .commit()
}

savedInstanceState を確認して、
Activityの再生成時にFragmentが重複しないようにしてくれてるね。
いいじゃん、いいじゃん。

あれ...?
だけど、なんか懐かしさを感じる。
最近これを自分で書いた記憶ないぞ..!?

自分の最近のプロジェクトを覗いてみる

supportFragmentManager.findFragmentById(R.id.container) ?: run {
    val fragment = HogeFragment.newInstance()
    supportFragmentManager
            .beginTransaction()
            .add(R.id.container, fragment)
            .commit()
}

思い出しました。
指定したViewのIDにFragmentが登録されているかを確認して、
Fragmentが登録されていなければaddする、としていました。

どっちがいいの??

それぞれ、僕の理解で日本語に落とし込みます。

  • Activityが初回生成時には、Fragmentをaddする
  • ViewにFragmentが登録されていなければ、Fragmentをaddする

どうでしょう?
個人的には、後者の方が考えやすいです。
当然、画面回転などのActivity再生成はどちらでもハンドリングできます。
なので、最近僕は savedInstanceState ?: run {} を書いていなかったんです。

最後に

ちなみに、背景で書いたコードレビューではコメントを付けずにスルーしました。
なぜなら、これは好みの範疇だと思ったからです。

ただ、世間ではjetpackのnavigationを使う流れがきています。
自分でFragmentをaddするなんて時代遅れなのかもしれません。

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

[Android] adbコマンドが覚えられない。。。

adbコマンド便利なんだけど、機能多すぎて覚えられない。。。
そんな折、AOSPでbashの補完スクリプトを用意してくれていることに気づいて感動したので、備忘メモ。

source等で読み込むと、adbのサブコマンドに対してTAB補完が効くようになる(そのまま)。

$ source sdk/bash_completion/adb.bash

asciicast

上記動画のように、dumpsysの引数まで補完可能。
他にも、ターゲット側のファイルパスやパッケージ名など、かなり幅広く補完してくれる。
bashを使っている場合はとりあえず有効にしておくことをオススメします。

確認環境は、Android 8.1、PCはUbuntu18.04です。

参考

adb.bash
adb command reference - Android Developer
記事にAsciinemaを埋め込むことができるようになりました

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