- 投稿日:2019-09-27T21:01:34+09:00
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つの組み合わせだったんですね。
(※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をお使いの方は、「これはどのフェーズで作られたものなんだ!?」と妄想すると楽しいかもしれません。
また、オープンソースを勉強するにあたって、良い題材になるとも思います!
- 投稿日:2019-09-27T20:12:57+09:00
親方空から女の子がをするための準備をする
あらすじ
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を起動するとフォントがどうとかで怒られるの
一緒にエクスプローラが開くので、Font\for IJ1.2\IchigoJam-for-Display-1.2.ttfを選択してインストールする。
フォントインストール完了後にcontinueを済ませると下記の様な画面が開く
IchigoJamをUSBケーブルで接続し、
10分ほど使い方を悩んだのち、公式に書いてある通りにする。IJUtilities を起動した後、 ターミナルセンター ウインドウの ツールバー オプション - Firm書換え (IchigoJam, PanCake...) を選択して下さい。lpc21isp ウインドウが表示されます。何も出てきてくれない。
とりあえずココのUSBドライバ入れてみる。
状況変わらず
どうにもならなそうなので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.pyimport 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つ
- 回路図は気が向いたら乗せる。
こんな感じ
変換は12[v]->3[v]。12[v]->5[v]->3[v]の方が良いらしいけどやってない。
制御基板
- ハロウィンライト、警報機はフォトカプラ + NPNトランジスタを利用
- パトランプに関してはフォトカプラ + NPNトランジスタ + リレー
- 回路図は気が向いたら乗せる。
こんな感じ
万が一用に手動でも動かせるようにしておいた。
ふりかえり
シータ役の方に落ちてきた位置を調整してもらいつつ、落ちてきた女の子(実際は横移動の男の子)を認識できた。
やる事やれて嬉しい。親方空から女の子が! #親方空から女の子が
— みやた@代打中 (@miyata080825) September 27, 2019
- 投稿日:2019-09-27T18:51:16+09:00
PermissionsDispatcherで気軽にPermissionを付与する
背景
プロジェクトにPermissionsDispatcherが導入されていたので色々触ってみる
使い方
順番に使い方を記載してく。
1. Activity / FragmentにRuntimePermissionsアノテーションをつける
以下のような形。
MainActivity.kt@RuntimePermissions class MainActivity : AppCompatActivity() {MainFragment.kt@RuntimePermissions class MainFragment : DaggerFragment() {2. 欲しいパーミッションの取得に成功している or 失敗した後に実行される関数を実装
MainActivity.ktfun readDeviceData() { getIMEI() }3. 2.で作成した関数にNeedPermissionアノテーションをつける
NeedPermissionの中には欲しい権限情報を記載する
MainActivity.kt@NeedsPermission(Manifest.permission.READ_PHONE_STATE) fun readDeviceData() { getIMEI() }4. ビルドする
自動的にコードが生成される
5. 関数名 + WithPermissionCheck()という名で拡張関数ができているので呼ぶ。
MainActivity.ktoverride fun onCreate(savedInstanceState: Bundle?) { readDeviceDataWithPermissionCheck() }6. onRequestPermissionsResultをoverrideし、生成された拡張関数onRequestPermissionsResultを呼ぶ
MainActivity.ktoverride fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) onRequestPermissionsResult(requestCode, grantResults) }以上。
自動生成されたコード
こんな感じ。ほぼほぼルーチン作業だったパーミッションコードが自動生成されている。
すでにパーミッションが設定されている場合でも呼び出してくれる形なので便利。MainActivityPermissionsDispatcher.ktprivate 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)
- 投稿日:2019-09-27T18:51:16+09:00
PermissionsDispatcherで楽にRuntime Permissionを付与する
背景
プロジェクトにPermissionsDispatcherが導入されていたので色々触ってみる
PermissionsDispatcherとは
Android 6.0(M) 以降から導入されたruntime permissionの実装を楽にしてくれるライブラリ。
https://github.com/permissions-dispatcher/PermissionsDispatcherruntime 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.ktfun readDeviceData() { getIMEI() }3. 2.で作成した関数にNeedPermissionアノテーションをつける
NeedPermissionの中には欲しい権限情報を記載する
MainActivity.kt@NeedsPermission(Manifest.permission.READ_PHONE_STATE) fun readDeviceData() { getIMEI() }4. ビルドする
自動的にコードが生成される
5. 関数名 + WithPermissionCheck()という名で拡張関数ができているので呼ぶ。
MainActivity.ktoverride fun onCreate(savedInstanceState: Bundle?) { readDeviceDataWithPermissionCheck() }6. onRequestPermissionsResultをoverrideし、生成された拡張関数onRequestPermissionsResultを呼ぶ
MainActivity.ktoverride fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) onRequestPermissionsResult(requestCode, grantResults) }以上。
自動生成されたコード
こんな感じ。ほぼほぼルーチン作業だったパーミッションコードが自動生成されている。
すでにパーミッションが設定されている場合でも呼び出してくれる形なので便利。MainActivityPermissionsDispatcher.ktprivate 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)まとめ
他にもユーザーがアクセス許可を付与しない場合に呼び出されるメソッドやなぜ許可が必要なのかを表示するためのメソッドなども指定できるので、ちょっとだけ楽になる。
- 投稿日:2019-09-27T18:51:16+09:00
PermissionsDispatcherでRuntime Permissionをちょっとだけ楽にする
背景
プロジェクトにPermissionsDispatcherが導入されていたので色々触ってみる
PermissionsDispatcherとは
Android 6.0(M) 以降から導入されたruntime permissionの実装を楽にしてくれるライブラリ。
https://github.com/permissions-dispatcher/PermissionsDispatcherruntime 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.ktfun readDeviceData() { getIMEI() }3. 2.で作成した関数にNeedPermissionアノテーションをつける
NeedPermissionの中には欲しい権限情報を記載する
MainActivity.kt@NeedsPermission(Manifest.permission.READ_PHONE_STATE) fun readDeviceData() { getIMEI() }4. ビルドする
自動的にコードが生成される
5. 関数名 + WithPermissionCheck()という名で拡張関数ができているので呼ぶ。
MainActivity.ktoverride fun onCreate(savedInstanceState: Bundle?) { readDeviceDataWithPermissionCheck() }6. onRequestPermissionsResultをoverrideし、生成された拡張関数onRequestPermissionsResultを呼ぶ
MainActivity.ktoverride fun onRequestPermissionsResult( requestCode: Int, permissions: Array<out String>, grantResults: IntArray ) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) onRequestPermissionsResult(requestCode, grantResults) }以上。
自動生成されたコード
こんな感じ。ほぼほぼルーチン作業だったパーミッションコードが自動生成されている。
すでにパーミッションが設定されている場合でも呼び出してくれる形なので便利。MainActivityPermissionsDispatcher.ktprivate 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)まとめ
他にもユーザーがアクセス許可を付与しない場合に呼び出されるメソッドやなぜ許可が必要なのかを表示するためのメソッドなども指定できるので、ちょっとだけ楽になる。
- 投稿日:2019-09-27T18:28:38+09:00
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.gradledependencies{ 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.ktinterface 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.ktclass 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を取り入れてみたいです。
- 投稿日:2019-09-27T15:24:14+09:00
App Crawlerによる自動UIテストにスクリプト制御を加える
- そもそもApp Crawlerとはなんぞや?という方はこちらの記事がわかりやすいと思います。簡単に言うと、Androidアプリをテストコード無しに自動UIテストしてくれるもの。
App Crawlerに関して調べていて、こちらのスライドの「スクリプトを用意することで制御をいれることも可能(ログインとか)」の一文を見つけ、App Crawlerによる自動UItestにスクリプト制御を入れてみることにした。
が、App Crawler用の公式ドキュメントが用意されておらず、Firebase Test Lab のRoboテストのドキュメントやApp Crawlerのソースコードを見ながら苦労したので共有。手順
- Roboスクリプトを作ります。これはもともとFirebase Test LabのRoboテストにスクリプト制御を入れるためのスクリプトで、Android Studioで作れます。(作り方の1~4までを行う。)
- 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
- 投稿日:2019-09-27T15:19:45+09:00
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">
- 投稿日:2019-09-27T09:12:42+09:00
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をじっと眺めてほしい情報が落ちていないか確認していきたい。
- 投稿日:2019-09-27T02:57:55+09:00
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>画像を選ぶ画面を出す
よく見るやつですね。
コードはこちら
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のテキストが「コピーが完了しました。」に変わるので変わったらファイルマネージャーで見てみましょう。
範囲別ストレージの場所はどこ?
/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" } }おわりに
ファイルの書き込みの部分が遅いんだけど速くなりませんかね・・?
- 投稿日:2019-09-27T01:02:21+09:00
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するなんて時代遅れなのかもしれません。
- 投稿日:2019-09-27T00:40:24+09:00
[Android] adbコマンドが覚えられない。。。
adb
コマンド便利なんだけど、機能多すぎて覚えられない。。。
そんな折、AOSPでbashの補完スクリプトを用意してくれていることに気づいて感動したので、備忘メモ。
source
等で読み込むと、adbのサブコマンドに対してTAB補完が効くようになる(そのまま)。$ source sdk/bash_completion/adb.bash上記動画のように、
dumpsys
の引数まで補完可能。
他にも、ターゲット側のファイルパスやパッケージ名など、かなり幅広く補完してくれる。
bashを使っている場合はとりあえず有効にしておくことをオススメします。確認環境は、Android 8.1、PCはUbuntu18.04です。
参考
adb.bash
adb command reference - Android Developer
記事にAsciinemaを埋め込むことができるようになりました