2021/06/20

[js]PouchDB

前回の RxDB は、PouchDB のアダプタが使えますよ、といっていた。
今回はその Pouch DB だ。

PouchDB, the JavaScript Database that Syncs!
https://pouchdb.com/

簡単に使えるならなんでもいいんだけどね。


find() は別になっているのでインストールがいるのだった。

npm i pouchdb pouchdb-find

01: const PouchDB = require('pouchdb');
02: PouchDB.plugin(require('pouchdb-find'));
03: 
04: const db = new PouchDB('kittens');
05: 
06: const fn = async () => {
07:     db.bulkDocs([
08:         {
09:             name: "yoshida",
10:             color: "green",
11:             healthpoints: 30,
12:         },
13:         {
14:             name: "yoshio",
15:             color: "blown",
16:             healthpoints: 33,
17:         },
18:         {
19:             name: "momo",
20:             color: "pink",
21:             healthpoints: 9,
22:         },
23:     ]);
24: 
25:     await db.createIndex({
26:         index: {fields: ['healthpoints']}
27:     });
28:     const resHealth = await db.find({
29:         selector: {healthpoints: {$exists: true}},
30:         sort: ['healthpoints'],
31:     });
32:     for (let lp = 0; lp < resHealth.docs.length; lp++) {
33:         console.log(resHealth.docs[lp].name);
34:     }
35:     console.log();
36: 
37:     await db.createIndex({
38:         index: {fields: ['color']}
39:     });
40:     const resColor = await db.find({
41:         selector: {color: {$exists: true}},
42:         sort: ['color'],
43:     });
44:     for (let lp = 0; lp < resColor.docs.length; lp++) {
45:         console.log(resColor.docs[lp].name);
46:     }
47: 
48:     await db.destroy();
49: };
50: fn();

createIndex()が、fields を列挙したらその分作られるのかと思ったのだが、それぞれ作ることになるようだ。戻り値で"created" か "exists" が取ってこれるが、存在するなら作らないと書いてあるので、そんなに気にしなくてよいのか。

また、createIndex() した後に put() したら再度 createIndex() がいるのかと思ったが、そうでもないようだ。今のコレクションに対してインデックスを作るという意味ではないのか。

 

TypeScriptの場合はこんな書き方で良いのかな?
find() したドキュメントがうまいことプロパティにアクセスできなかったので as を使ってキャストしてしまった。よいのかどうかわからん。

01: import * as PouchDB from 'pouchdb';
02: 
03: PouchDB.plugin(require('pouchdb-find'));
04: 
05: const db = new PouchDB.default('kittens');
06: 
07: type dbType = {
08:     _id: string;
09:     _rev: string;
10:     name: string;
11:     color: string;
12:     healthpoints: number;
13: };
14: const fn = async () => {
15:     db.bulkDocs([
16:         {
17:             name: "yoshida",
18:             color: "green",
19:             healthpoints: 30,
20:         },
21:         {
22:             name: "yoshio",
23:             color: "blown",
24:             healthpoints: 33,
25:         },
26:         {
27:             name: "momo",
28:             color: "pink",
29:             healthpoints: 9,
30:         },
31:     ]);
32: 
33:     let idxRes = await db.createIndex({
34:         index: { fields: ['healthpoints'] }
35:     });
36:     const resHealth = await db.find({
37:         selector: { healthpoints: { $exists: true } },
38:         sort: ['healthpoints'],
39:     });
40:     for (let lp = 0; lp < resHealth.docs.length; lp++) {
41:         const r = resHealth.docs[lp] as dbType;
42:         console.log(r.name);
43:     }
44:     console.log();
45: 
46:     idxRes = await db.createIndex({
47:         index: { fields: ['color'] }
48:     });
49:     const resColor = await db.find({
50:         selector: { color: { $exists: true } },
51:         sort: ['color'],
52:     });
53:     for (let lp = 0; lp < resColor.docs.length; lp++) {
54:         const r = resColor.docs[lp] as dbType;
55:         console.log(r.name);
56:     }
57: 
58:     await db.destroy();
59: };
60: fn();

[js]RxDB

JavaScript でデータベースを使う必要性が出てきてしまった。
練習してみよう。


Introduction · RxDB - Documentation
https://rxdb.info/

NoSQLデータベースだそうな。SQLite か LMDB かでいえば、LMDB 側ということになろう。
私はそんなにデータベースを使ったことがないので、比較できるのはその2つくらいなのだ。

npm i でインストールすれば使えるのだろうと思ったが、RxDB 以外にもインストールしないと使えないらしい。
adapter というものもインストールして設定しないと使えないっぽい。
今回は Node.js なので、adapter のページにあった leveldown というものを使ってみよう。 Level DBらしい。

npm i rxdb rxjs leveldown pouchdb-adapter-leveldb

 

メニューの順番からすると、 RxDatabase, RxSchema, RxCollection, RxDocument, RxQuery の順にやっていくのだろう。

SQL ではないからか、テーブルではなくスキーマと呼ぶのか? そこはまだよいのだが、コレクションとかドキュメントとかあるのだが、何なのかが分からん。。。

開発者が知っておくべき、ドキュメント・データベースの基礎:特集:MongoDBで理解する「ドキュメント・データベース」の世界(前編)(1/3 ページ) - @IT
https://www.atmarkit.co.jp/ait/articles/1211/09/news056.html

ドキュメントはデータベースに突っ込むデータで、コレクションはその集合・・・?

  • createRxDatabase()でデータベースを作る
  • データベースに突っ込みたいデータの構造を決めて addCollections() で追加する
  • insert() でデータを突っ込む
  • find() でデータを取り出す
01: import {
02:   createRxDatabase,
03:   addRxPlugin,
04: } from 'rxdb';
05: 
06: addRxPlugin(require('pouchdb-adapter-leveldb'));
07: const leveldown = require('leveldown');
08: 
09: const fn = async () => {
10:   const db = await createRxDatabase({
11:     name: 'heroesdb',
12:     adapter: leveldown,
13:   });
14: 
15:   const myHeroSchema = {
16:     "title": "hero schema",
17:     "version": 0,
18:     "description": "describes a simple hero",
19:     "type": "object",
20:     "properties": {
21:       "name": {
22:         "type": "string",
23:         "primary": true
24:       },
25:       "color": {
26:         "type": "string"
27:       },
28:       "healthpoints": {
29:         "type": "number",
30:         "minimum": 0,
31:         "maximum": 100
32:       },
33:     },
34:     "required": ["color"],
35:     "attachments": {
36:       "encrypted": false
37:     },
38:     indexes: ['healthpoints', 'color']
39:   }
40:   await db.addCollections({
41:     heroes: {
42:       schema: myHeroSchema
43:     }
44:   });
45: 
46:   await db.heroes.bulkInsert([
47:     {
48:       name: "yoshida",
49:       color: "green",
50:       healthpoints: 30,
51:     },
52:     {
53:       name: "yoshio",
54:       color: "blown",
55:       healthpoints: 33,
56:     },
57:     {
58:       name: "momo",
59:       color: "pink",
60:       healthpoints: 9,
61:     },
62:   ]);
63: 
64:   const query = await db.heroes.find();
65:   const resHealth = await query.sort('healthpoints').exec();
66:   for (let lp = 0; lp < resHealth.length; lp++) {
67:     console.dir(resHealth[lp].get('name'));
68:   }
69:   console.log();
70:   const resColor = await query.sort('color').exec();
71:   for (let lp = 0; lp < resColor.length; lp++) {
72:     console.dir(resColor[lp].get('name'));
73:   }
74: 
75:   await query.remove();
76: }
77: fn();


難しすぎでは!?と思ったが、SQL を使うタイプだと SQL文というテキストになっているだけであんまり変わらんか。

addCollections() の戻り値はコレクションではあるのだが、コレクションの集合のようなものが返ってくるようだ。
これに find() したかったら、今回だと .heroes.find() のように名前を挟まないといかん。
そうなると db.heroes.find() と変わらん気がする。

 

それとは別に気になったのは、RxDB はその下で動いているデータベースを隠蔽するような形で動いていると思うのだが、トランザクションがあるデータベースだとパフォーマンスが悪くならないだろうか。 bulkInsert() で全部放り込めるなら良いけれども、取得しては保存、取得しては保存、みたいな使い方をしたい場合もあるだろうし。

2021/06/19

[js]非同期とコールバックとPromise

JavaScript で検索するとたくさん出てくる「コールバック Promise」。
この記事もその1つになるのだろう。自分の考えを整理するのに、どうしても文章に書かざるを得ない人たちの一人なので仕方がない。
ちなみに、ブラウザでの動作は考えておらず、Node.js から使うものとして書いている。


JavaScript はシングルスレッドだと言われている。仕様ではここになるのかな?

8.6 Agents
https://262.ecma-international.org/11.0/#sec-agents

An agent comprises a set of ECMAScript execution contexts,
an execution context stack,
a running execution context,
an Agent Record,
and an executing thread.
Except for the executing thread, the constituents of an agent belong exclusively to that agent.

【DeepL翻訳】
エージェントは、ECMAScript の実行コンテキストのセット、実行コンテキストスタック、実行中の実行コンテキスト、エージェントレコード、および実行スレッドから構成されます。実行スレッドを除き、エージェントの構成要素はそのエージェントにのみ属します。

実行しているコンテキストではスタックとかスレッドとかで構成されるけど、全部 a や an が付いているので1つしかないということなんじゃなかろうか。
「single thread」という単語は見つけられなかったのだよ。

解釈は間違ったかもしれんが、とりあえずシングルスレッドであることは信用しよう。


シングルスレッドなのに非同期ってどういうことじゃ、と思ったが、ここは割込みができるシステムと考えればよいと思う。

 

組み込み開発ではよくあることだが、CPU は実行状態として「通常モード」と「特権モード」のようなモードというか状態というかを持つことがある。電源を入れて起動直後は特権モードで、その状態じゃないとできない設定をいろいろやってから通常モードになって動き始める、みたいな感じだ。

通常モードで動いているけれども、例えば外部のセンサからINPUTピンに対して信号が来た場合には割り込んで良いようにする、という設定にしておくと、信号が来たら現在の状態をスタックに保存してから特権モードに切り替わり、特権モードで処理をして終わったらまた通常モードに戻る、というようなことができる。

こういう特権モードになるときは CPUとしてまるまる切り替わるので、スレッドとかそういうのはない。そして特権モードでの動作は通常モードとは異なるのでコンテキストが異なる。すなわち非同期だ。

 

私の場合は JavaScript の非同期をそんな世界観で眺めている。


非同期なところまではよいのだが、非同期で処理を何かしたいことがほとんどだろう。 setTimeout() みたいな遅延動作なんかはわかりやすい例で、何か動作を遅延させてから実行したいのだ。

しかし、実行し始めたコンテキストとは別のコンテキストで動作するので、素直に続きを行うわけにはいかない。コンソールにログを出す、みたいな影響を与えないものであれば気にしなくてよいのだが、変数に値を代入したりファイルに書き込んだりしたいことが多かろう。

グローバル変数みたいにスコープが広い変数であれば代入できると思うが、ローカル変数に戻したいという場合もあろう。そういうときは実行したい単位を関数にしてしまい、Promise<戻り値>を戻すようにして、asyncを付けておくと、呼び出し元はその関数を await つけて呼び出せば

 

例えば、このコードをそのまま実行すると、setTimeout()したログは一番最後に出力される。

01: console.log('hello');
02: setTimeout(() => { console.log('meme'); }, 1000);
03: console.log('world');

hello
world
meme

 

setTimeout()の後にworldを出したいだけなら、こんな感じか。
ちなみに TypeScript で書いていて、 tsconfig.json の "target" は "es2015" にしている。 tsc --init で作られたそのままだと怒られてダメだったのだよね。
vscode で実行していたのでデバッグ設定をしていたのだが、"Launch Program" で実行しても console.log の出力がどこにも出てこなくて悩んだ。実行するとタブが "TERMINAL" になるので、急いで "DEBUG CONSOLE"にすると出てきた。

01: async function setTimeoutPromise(msec: number): Promise<void> {
02:     return new Promise<void>((resolve: any, reject: any) => {
03:         setTimeout(() => { resolve();}, msec);
04:     });
05: }
06: 
07: const fn = async () => {
08:     console.log('hello');
09:     await setTimeoutPromise(1000);
10:     console.log('world');
11: };
12: fn();

なんかめんどくさいね。
await を使いたいがために async の関数が2つ必要になってしまった。

 

それはともかく、setTimeout() の第1引数は関数を受け取るタイプで、指定した時間後にこの関数を呼んでくれる。つまりコールバックだ。
これを呼び出し元で同期に見せかけたいので、Promise を使ってコールバックで resolve() を呼び出すようにしている。

 

自分で作った関数だけで非同期が発生することはなくて、そこからsetTimeout() なり npm でインストールしたライブラリなり、なんらかの外部に依存する場合しか非同期になることはないだろう。そして JavaScript で提供された関数を呼ばないで済むことはまずないので(たぶん変数の参照や代入だけか?)、前提条件はあまり考えなくて良いだろう。

 

非同期の基本的な処理完了通知はコールバックのはず。ただ、組み込み CPUでの割り込み処理で特権モードになったままになるのではなく、Node.js のコンテキストに戻されるはずだから、pthread の join で待って、戻ってきたらコールバック関数を呼び出す、みたいなことをやっているのか。

Promiseはその先の話で、いろいろ書き方はあるのだろうが、

  1. Promise を new するときにコールバックで結果を返す関数を最後に呼び出す関数を登録する
    1. 登録する関数の引数は (resolve, reject) にするのが一般的。
    2. コールバックが呼ばれるときに resolve() を呼び出す(失敗だったら reject() を呼び出す)。
    3. コールバックで結果を得てそれを次も使いたい場合は resolve() の引数にする。
  2. promise.then() を呼び出して、引数は resolve() の中身を実装するようなイメージの関数にする。
01: function setTimeoutPromise(message: string, msec: number): Promise<string> {
02:     return new Promise<string>((resolve: any, reject: any) => {
03:         setTimeout(() => { resolve(message);}, msec);
04:     });
05: }
06: 
07: console.log('hello');
08: setTimeoutPromise('world', 1000).then((msg: string) => {
09:     console.log(msg);
10: });

setTimeoutPromise()の引数をコールバック関数内の resolve()に渡せているのが面白いところだ。全然コンテキストが異なるならアクセスできないはずなので、うまいことやってるのだろう。深く探る必要もあるまい。

そして setTimeoutPromise() に async をつけて、呼び出すときに await をつけると、resolve() の引数がそのまま戻り値として使えるようになる。つまり同期になる。

 

なので、もうあまり難しいことは覚えずに async - await だけ覚えておけばいいかな、と思っていたのだ。


しかし、パフォーマンスを出さないといけないようになると、全部同期で済ませるのはもったいないこともある。非同期になる理由が「忙しいから」であれば待っていても良いのだが、「待ち時間が発生するため」の場合には、その間に他のことをやってしまいたいからだ。

 

まず、Promise.all()というものがある。

Promise.all() - JavaScript | MDN
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/all

Promiseのインスタンスを配列で受け取って、then でも配列で結果を返している。面白い。
これを async - await で書くとこんな感じか。

01: const promise1 = Promise.resolve(3);
02: const promise2 = 42;
03: const promise3 = new Promise((resolve, reject) => {
04:     setTimeout(resolve, 100, 'foo');
05: });
06: 
07: const fn = async () => {
08:     const result = await Promise.all([promise1, promise2, promise3]);
09:     console.log(result);
10: }
11: 
12: fn();
13: 

all()以外にもいろいろある

順番にやっていかないといけないものは async - await でつなげれば良いし、特に順番はどうでもよいなら Promise.all() でまとめてしまえばよい。本当にどうでもよいならコールバック関数が呼ばれておしまい、でよいのだが、そうもいかんことが多かろう。

 

これでなんとかなるかと思っているのだが、どうなんだろうね。苦労してみないと分からんのだろう。

2021/06/13

[android] HTTPでJSONを取りたい

Android用のライブラリAARが提供されていて、それを起動するとHTTPで内部的に通信をして取得したり指示したりするようになっているものがあるとする。まあ、今の私の状況なんだけどね。

permissionの設定を乗り越えて起動できるようになったのは良かったのだが、HTTPで内部でやりとりをする方法が分からん。

HTTP クライアントを選択する
https://developer.android.com/training/basics/network-ops/connecting#choose-http-client

ネットで検索すると HttpURLConnection がよく出てくるのだが、Retrofit というものを使う例になっているようだ。 HttpURLConnection の方が基本的な雰囲気をかもし出しているが、Retrofit だと JSON なんかもうまく扱ってくれそうなことが書いてあるので、黙って使うことにする。
ただ、概要の方には「HTTPライブラリVolley」と書いてあるので、実はそっちの方が良いのかも。。。
まあよい。必要ならやるだけだ。

Retrofit #Download
https://square.github.io/retrofit/#download

Gradleの場合は implementation を追加すれば良いそうだ。 Android Studio の Project Structure から 検索してもよさそうだ。"retrofit" で検索するとたくさん出てきたので、com.squareup.retrofit2 で始まっているものを選択した。

implementation 'com.squareup.retrofit2:retrofit:2.9.0'

Retrofit のサンプルでは GsonConverterFactory というものを使っていたので、そちらも書くとよいだろう。

implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

他にもコンバータがあるので、必要に応じて使い分けるのだろう。


Introduction に従えば何か動いてくれるんじゃなかろうか。

Introduction
https://square.github.io/retrofit/#introduction

baseUrl と GitHubService の定義からそれっぽい URL にアクセスしてみよう。

https://api.github.com/users/hirokuma

いろいろ返ってきた。

image

あちこちサイトを見たが、結局こちらのと同じような実装になった。

Retrofitの使い方・HTTPリクエスト・サーバー通信【Android・Kotlin】
https://101010.fun/programming/android-try-retrofit.html

私が作ったのはこちら。

https://github.com/hirokuma/AndroidHttpSample1

味も素っ気もないね。Android Studioでプロジェクトを新規作成したところからcommitすればよかったな。
最終的には同じようになってしまったが、一応 Android Developer での書き方と、 Retrofit のサンプルを参考にしていたのだ。サンプルは Android ではないので

 

Android Developerのように suspend fun にするとうまく動かせなかった。コルーチンにしなさい、みたいなエラーになるのだが、理解できていないのだ。

Android での Kotlin コルーチン  |  Android デベロッパー  |  Android Developers
https://developer.android.com/kotlin/coroutines?hl=ja

go言語にもあったが、軽量スレッドみたいなものらしい。昔はスレッドを軽量プロセスと呼んでたこともあったと思うが、スレッドですら生ぬるい時代になったのか。

 

suspend fun というのも何だかわかってないし。Java にはそういうのなかったような気がするが、最近できたんだろうか。
いろいろわかってなくてつらいな。

2021/06/06

[kotlin] はてな はnullable

少し前に、JavaScript の ?? や ?. の意味を調べた。

hiro99ma blog: [js]はてな、はてなはてな、はてなドット
https://blog.hirokuma.work/2021/05/js.html

C言語だと ! は否定、 ? は三項演算子の1番目、という意味しかなかったのだが、JavaScript はいろいろ意味があるんだなぁ、くらいに思っていた。

 

が、Kotlin にも ? がけっこう出てくることに気付いた。
最近の言語の流行りなんだろうか。

 

以前も載せた気がするのだが、もう私の記憶に残っていないからよしとしよう。

Keywords and operators | Kotlin
https://kotlinlang.org/docs/keyword-reference.html#operators-and-special-symbols

演算子なので operator だ。

? だけだと「これは nullableですよ」、 ?. は、nullではない場合だけ呼び出すという意味だそうな。

 


Kotlin にも === と !== があった。
referential equality という名前で、 a === b は a と b が同じオブジェクトを指している場合に trueになる。

 

JavaScript はどうだったかというと、 strict equality という名前で、日本語だと「厳密等価」と呼ぶそうだ。アルゴリズムに従って判定するとのこと。 x === y の場合、

  1. Type(x) と Type(y) が異なるならば false
  2. Type(x) が undefined なら true
  3. Type(x) が null なら true
  4. Type(x) が 数値の場合、
    1. x が NaN なら false
    2. y が NaN なら false
    3. x と y の数値が同じ値なら true
    4. x が +0、 y が -0 なら true
    5. x が -0、 y が +0 なら true
  5. Type(x) が 文字列の場合、 x と y が同じ文字列(同じ文字長かつ各位置の文字が一致)なら true、それ以外は false
  6. Type(x) が 真偽値(boolean)の場合、どちらも true か どちらも falseの場合は true、それ以外は false
  7. x と y が同じオブジェクトを指しているなら true、それ以外は false

 

7番目が kotlin の定義と同じなのだけど、じゃあ Kotlin でオブジェクト以外だと === が使えないということなのか? いやそもそも Kotlin は JavaScript を意識した言語なのだろうか?

そんなことを考えながらページを見ていったのだが 「JS」 というのがあることに気付いた。

image

意識しているとかなんとかではなく、

Kotlin for JavaScript | Kotlin
https://kotlinlang.org/docs/js-overview.html

Kotlin for JS という JavaScript からトランスパイルするものだそうな。
トランスパイルって何だろうと検索すると、TypeScript を JavaScript にトランスパイルする、とか、トランスパイラーとか、そんなのが出てきた。中間言語に置き換える、というのともちょっと違うのかもしれんが、昔の cfront みたいなものと思ってもよいのかもしれん。

ともかく、Kotlin for JS は transpile to JavaScript なので、Kotlin で書いたものを JavaScript に変換して、あとは JavaScript でやってもらうのだろう。立ち位置としては TypeScript と同じようなところにいるのか。まあ、Android などの場合も JVM に transpile するものだし、 Kotlin/Native は iOSも対象に入っているようなので、Kotlin が各ネイティブ言語へのラッパー言語のような戦略をとっているということか。 Java や .NET がやっていることよりも階層が上なので、各言語に対応する大変さはあるものの、その言語が使える環境であれば仮想的なものを用意せずに動かせるという利点がある。今のように「Android は Java」「iOS は Swift」のようにプラットフォームと言語がかなり固定化している時代に向いていると思う。 Kotlin で書いておけば「Android は Java やめます」ってなったとしても、 Kotlin から新しい環境へのトランスパイラさえ作ってあれば済むしね。大変だろうけど。

今回の === も、対応する概念がない言語の場合はトランスパイラがそういうコードを吐いてくれればよいだけなので、言語的な共通さを探すのはあまり意味が無いのかもしれんな。

[android] JavaからKotlinに変換したい

Androidアプリをやるからには、興味がなくても画面が多少いじれないとねー、と思って、手軽そう(な気がした)React Nativeを使ってみようとした。

Setting up the development environment · React Native
https://reactnative.dev/docs/environment-setup

Android Studioはある。Node もインストールしている。Windows 環境変数も設定している。
よし。

image

ちょっと見栄を張って(?)、TypeScript で作ってみる。

npx react-native init RNSample1 --template react-native-template-typescript

書いてあるとおりにやると、エミュレータが立ち上がってサンプルアプリが起動した。
ちなみに React Native は v0.64.2 のようだ。あまり npm のしくみをわかってないのだが、登録されている最新版を使ってくれているのかな?
(注:書いたときはv0.64.2だと思い込んでいたが、package.json を見ると v0.64.1 だった。何か見間違えていたらしい。 npm i react-native@0.64.2 まで指定するとちゃんと更新された。)


このテンプレートは Android の部分が Javaでできているので、どうせなら Kotlinにしておきたい。

テストアプリを作っていて気付いたのだが、Java のコードをクリップボードでペースとすると自動的に Kotlin変換して貼り付けるか聞いてくれるのだ。
すばらしい!

というわけで、プロジェクトを丸々 Kotlin に変換してくれないだろうか。

既存のプロジェクトに Kotlin を追加するにはどうすればよいですか?
https://developer.android.com/kotlin/faq#convert

Java ファイルを開いて、メニューから Convert を選んでやるとよいそうだ。
確認ダイアログが出たあと、やり方を聞いてくる。

image

これで OKを押すと変換され・・・あれ? 初回だからかもしれないけど build.gradle(ルートとappの2ファイル)しか更新されないぞ??

もう一度同じことをやると、今度は変換された。ファイルの拡張子も .kt にしてくれている。
まあよしとしよう(偉そうに...)。


しかし、ファイルを開いてからというのがちょっと面倒だ。
ディレクトリ以下まるまるやってくれないだろうか?

まず、1回目はツリーのトップにある app を選択して実行する。
同じダイアログが出てくるので、OK する。

image

 

2回目はここで実行。
よしよし、複数ファイル行ってくれた。

image

 

と、あっさりうまくいったように書いたが、この手順に落ち着くまでいろいろ失敗した。
ツリー表示の種類のせいか、選択しているディレクトリのせいかわからないけど、 build.gradle の plugins がどうのこうのというエラーが出ていたのだ。

まあ、うまくいったからよいのだ。

変換されたのは、 Java→Kotlin 以外だと build.gradle が 2ファイルだけだ。
何がどう変わったのかは、経験不足で分からん。 ext.kotlin_version とか kotrin-android とか、そんな感じ。

2021/06/05

[android] Googleドライブアプリにintentは投げられないのか

Android で ACTION_SEND で intent を投げるときに chooserでくるんでアプリ選択をユーザにさせるようにしたけれども、Googleドライブにしかファイルを保存させたくないという場合だったら直接Googleドライブアプリに intentを投げればいいんじゃなかろうか?

 

そう思って明示的インテントを作ろうとしたのだが、はて Googleドライブアプリのパッケージが分からない。

端末にインストールされているアプリ情報を取得する - すいはんぶろぐ.io
https://suihan74.github.io/posts/2021/01_09_00_get_installed_applications_info/

こちらを見て、getInstalledApplications() と getApplicationLabel() を組み合わせて一覧を作ってみたのだが、Android アプリ一覧で「ドライブ」に相当するラベル名がない。
パッケージ名も "drive" とか "drv" のようなものがない。まあこっちは略称が違うだけかもしれんが。

Android Studio が getInstalledApplications() で リンク付きの warningを出していたので見てみた。

Android 11 でのパッケージへのアクセス  |  Android デベロッパー  |  Android Developers
https://developer.android.com/about/versions/11/privacy/package-visibility

なんか難解な日本語だが、Android 11 からは Android 11 以降をターゲットとしているアプリは manifest に <queries> を書いておかないと一覧から外れてしまうことがあると言っている?

たぶん Googleドライブアプリのような AOSP にはない Googleアプリは Android 11 以降をターゲットにしていると思うが、メニューには出てくるしなぁ。でもメニューだから出てくると言うこともありそうだし。。。

 

うん、よくわからんが、そこまでして Googleドライブアプリに投げたいわけでもないので、よしとしよう。

[android] intent

前回は、テキストファイルを Googleドライブに対して共有することができた。
正確にいえば、ファイルを受け付けるパッケージに対して共有できた、になるのかな?

気にしているのは intentを投げたときに logcatに出てくる例外。出てくるだけで動作としては問題ないので困っている。 grantUriPermission()で READ_URI_PERMISSION を与えると出なくなるので、さらに困っている。動作しないなら対策しようとするし、ログが消せないならあきらめるのだが、どちらもできてしまうからだ。

何から調べるとよいかわからないので、まずは intentからいってみよう。


Android にいくつ基本要素があるか知らないが、 intent はその中でもかなり上位に位置するだろう。

インテントとインテント フィルタ  |  Android デベロッパー  |  Android Developers
https://developer.android.google.cn/guide/components/intents-filters?hl=ja

前回は startActivity() を呼んだので、アクティビティの開始になる。特定のパッケージを指定しなくても、暗黙的インテントであればマッチするパッケージの候補を出してくれるのは助かるね。
それより前には startService() とか bindService() とかしたので、サービスの開始もやったことがあることになる。ブロードキャストはまだだな。

 

intent への設定項目は多数ある。

気になっていたのは setData() と putExtra()。どっちも intent先に渡すデータを設定しているように見えるのだが、違いが分からん。
読むと、データはまあデータで、エクストラは key-value のペアとのこと。前回の intentでは URIしか送るものはなかったのだが、それはデータではなくエクストラだった。

どこでその使い分けをするかというと、アクションくらいしかなさそうだ。。。と思ったのだが、下の例を見ていくと「明示的インテントの例」では送り先のパッケージを指定してデータを設定して startService()しているだけで、アクション指定はない。そういうやり方もあるのか。

ACTION_SEND を見ると、エクストラの記述がいろいろある。 ACTION_PICK は getData()と書いてあるので、送り元としては setData()してあげればよいということかな。公式というか共通というか、そういうアクションについてはそのページの Input のところを見てやればよいのだろう。

 

改めて ACTION_SEND を見よう。とにかく書いてあることが多い。

  • Activity Action
    • データを誰かに送信する
    • データの配信先は特定されていない
    • データの送信先をユーザに尋ねるのは、アクションの受信者次第
    • SEND intentを立ち上げるとき chooserでくるんであげると、ユーザがデータの送信方法を選択するプロンプトが出てくる(通常はそうする)
  • Input
    • MIMEタイプ
    • EXTRA_TEXT か EXTRA_STREAM + 送信データ
      • EXTRA_TEXTの場合、MIMEは "text/plain"
        • その場合、EXTRA_HTML_TEXT でクライアントがHTMLフォーマットでテキストを取得するようにもできる
      • それ以外なら EXTRA_STREAM
    • JELLY_BEAN(Android 4.1)以降であれば、データを setClipData()経由で送ることもできる
      • コンテンツを共有するときに FLAG_GRANT_READ_URI_PERMISSIONを使用できる
      • このアプローチを使用する場合、互換性のために EXTRA_TEXT か EXTRA_STREAM で同じデータを提供する必要がある。ClipDataを設定しない場合、 setActivity()を呼んだときにコピーされる。
    • Oreo(Android 8)から、CATEGORY_TYPED_OPENABLEが渡された場合、EXTRA_STREAM または setClipData()を介して渡された URIsは openTypedAssetFileDescriptor()を使用するあっセットタイプファイルとしてのみ開くことができる
    • 受信側が解釈できるオプションの標準エクストラは以下
      • EXTRA_EMAIL, EXTRA_CC, EXTRA_BCC, EXTRA_SUBJECT
  • Output
    • 無し
  • Constant Value
    • "android.intent.action.SEND"

機械的に訳した。

 

EXTRA_TEXT や EXTRA_STREAM で渡す方法だけでなく、ClipData経由でも渡すことができるのが grantの例外と関わっているのだろう。

まず ClipDataだが、これはクリップボードのデータ、というわけではないようだ。というのも間違いなのかな。クリップボードに保存したデータは ClipData扱いになるから、結果としては同じで良いのかな?

intentに ClipDataを紐付ける関数として setClipData()があるが、ClipDataに含まれる URIに対して FLAG_GRANT_READ_URI_PERMISSION や FLAG_GRANT_WRITE_URI_PERMISSION が機能するそうだ。

 

つまり、今回のように ClipData を使うつもりがない場合には気にしなくてよいはずだ!という結論に至った。私の解釈が間違ってなければね。

01:     fun sendIntent(view: View?) {
02:         val filename = "myfile.txt"
03:         val pathUri: Uri = FileProvider.getUriForFile(
04:             this,
05:             BuildConfig.APPLICATION_ID + ".fileprovider",
06:             File(filesDir, filename)
07:         )
08:         val shareIntent: Intent = Intent().apply {
09:             action = Intent.ACTION_SEND
10:             putExtra(Intent.EXTRA_STREAM, pathUri)
11:             type = "plain/text"
12:         }
13:         val chooseIntent: Intent = Intent.createChooser(shareIntent, null)
14:         // ClipDataを使う場合
15:         // grantUriPermission("android", pathUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
16:         startActivity(chooseIntent)
17:     }

AndroidStudio というか IntelliJ というかはショートカットキーでコメントアウトすると行頭になるのがちょっときになるね。思わず手動でやってしまった。

2021/06/04

[android] テキストファイルをGoogleドライブに置きたい (3)

はい、というわけでファイルをGoogleドライブに置きたいだけの記事が、これでもう3回目です。
がんばっていきましょう。

前回は、エミュレータで実行して Googleドライブに置けそうな画面になったものの、Googleアカウントの設定をしていなかったので確認できなかったところまでだ。
エミュレータにメインアカウントを登録するのって、気が引けるのは私だけだろうか。


手元にあった、ASUS の P008 というタブレットで動かしてみる。
Nougat の Android 7.0 の方だ。 Android 7って、 7.0系と 7.1系があるけど、けっこう違うのかな?

これがボタンを押して intent が飛んだとき。

image

マイドライブを選択すると、マイドライブの直下に myfile.txt が保存されていた。
中身は「Hello world!」。

期待通りである。
めでたしめでたし。


・・・とはいかない。

昨日の記事には書いたが、エミュレータで実行したときは共有先の候補が出るところまでなのに logcat にはずらずらと exceptionが出力されていたのだ。

それが P008 では出ていない。何も出ていない。
ということは Android OSが新しくなってセキュリティ的に厳しくなったことで出るようになったと考えるのが妥当か。

 

では、 Android 11 がインストールされている Pixel3aXL で動かしてみる。
動作はほぼそのままなのだが、logcatに出てきた。
最初に intentを飛ばすところで出ているようだ。それ以降は出ていない。

 

2021-06-03 22:03:24.606 28296-28316/com.example.sample3 E/DatabaseUtils: Writing exception to parcel
     java.lang.SecurityException: Permission Denial: reading androidx.core.content.FileProvider uri content://com.example.sample3.fileprovider/myfiles/myfile.txt from pid=28371, uid=1000 requires the provider be exported, or grantUriPermission()
        at android.content.ContentProvider.enforceReadPermissionInner(ContentProvider.java:820)
        at android.content.ContentProvider$Transport.enforceReadPermission(ContentProvider.java:684)
         at android.content.ContentProvider$Transport.query(ContentProvider.java:239)
        at android.content.ContentProviderNative.onTransact(ContentProviderNative.java:106)
        at android.os.Binder.execTransactInternal(Binder.java:1154)
        at android.os.Binder.execTransact(Binder.java:1123)

改行が入って見づらいのだが、探しやすそうなのはこの辺か。

E/DatabaseUtils: Writing exception to parcel

Permission Denial: reading androidx.core.content.FileProvider uri ..(略).. requires the provider be exported, or grantUriPermission()

FireProvider で grantUriPermission() ということで検索すると、こういうのが出てきた。

Granting Temporary Permissions to a URI
https://developer.android.com/reference/androidx/core/content/FileProvider#Permissions

一時的に、作成したコンテントURI に対してアクセス許可を与える話らしい。
Permission Denial なので何か権限がいるのは分かるが、URI に与えるのか。ファイルに Read 権限を与えないと読むことができないのと同じだろうか。

いや、Writing exception なので書込みに関しての例外が起きたのだろう。でも、URI って読めればよいのではないのか?

Stackoverflow で解決案を出している人は、

android - Permission Denial while sharing file with FileProvider - Stack Overflow
https://stackoverflow.com/questions/57689792/permission-denial-while-sharing-file-with-fileprovider

まずは試そう。

01:     fun sendIntent(view: View?) {
02:         val filename = "myfile.txt"
03:         val pathUri: Uri = FileProvider.getUriForFile(
04:             this,
05:             BuildConfig.APPLICATION_ID + ".fileprovider",
06:             File(getFilesDir(), filename)
07:         )
08:         val shareIntent: Intent = Intent().apply {
09:             action = Intent.ACTION_SEND
10:             putExtra(Intent.EXTRA_STREAM, pathUri)
11:             type = "plain/text"
12:         }
13:         val chooseIntent: Intent = Intent.createChooser(shareIntent, null)
14:         val resInfoList =
15:             this.packageManager.queryIntentActivities(chooseIntent, PackageManager.MATCH_DEFAULT_ONLY)
16:         for (resolveInfo in resInfoList) {
17:             val packageName = resolveInfo.activityInfo.packageName
18:             grantUriPermission(
19:                 packageName,
20:                 pathUri,
21:                 Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION
22:             )
23:         }
24:         startActivity(chooseIntent)
25:     }

雰囲気としては、intent を送ることができそうなところを MATCH_DEFAULT_ONLY という条件で検索して、それらのパッケージに対して個別に grantUriPermission() で READ | WRITE の一時的な権限を割り当てる、というところか。

URI に与えるのではなく、 URIにアクセスする方に与えているのかな?
例外のメッセージも 1つだけだったので、実はどれか特定のアプリだけが WRITE も必要なのかもしれん。

 

いや、そうだとしたら、この方法は WRITE がいらないアプリに対しても許可を与えているということになるんじゃないか?
というわけで、for文の中で packageName をログ出力したのだが・・・「android」 1つしか出てこなかった。

結果として、android に READ だけ付けてあげれば exceptionは出なくなった。

01:     fun sendIntent(view: View?) {
02:         val filename = "myfile.txt"
03:         val pathUri: Uri = FileProvider.getUriForFile(
04:             this,
05:             BuildConfig.APPLICATION_ID + ".fileprovider",
06:             File(filesDir, filename)
07:         )
08:         val shareIntent: Intent = Intent().apply {
09:             action = Intent.ACTION_SEND
10:             putExtra(Intent.EXTRA_STREAM, pathUri)
11:             type = "plain/text"
12:         }
13:         val chooseIntent: Intent = Intent.createChooser(shareIntent, null)
14:         grantUriPermission("android", pathUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
15:         startActivity(chooseIntent)
16:     }

ただ・・・あまりすっきりはしない。
だって、grantUriPermission() しなくても Googleドライブにファイルは置けるのだから。
logcat に例外を見たくないというだけで権限を追加するのもなぁ。

とはいえ、READ くらい付けてやらないといかんのじゃないか、という気がしなくもない。
うーむ。。。

2021/06/03

[android] テキストファイルをGoogleドライブに置きたい (2)

前回はファイル共有の設定までだった。
今度は置く方だ。

他のアプリへの単純なデータの送信  |  Android デベロッパー  |  Android Developers
https://developer.android.com/training/sharing/send#send-binary-content

ファイルを置くということであれこれ悩んだのだが、ファイル共有することのできるURIデータを送信する、という考え方でよいのではなかろうか。
ここに載っていた例はテキストコンテンツやバイナリコンテンツで別々に書かれているものの、中を見れば putExtra()で送信したいデータを決め、setType()でデータの種類を教えているだけだと思うのだ。

テキストコンテンツの場合は文字列をそのまま渡しているが、バイナリコンテンツの場合はURIを渡している。だからバイナリコンテンツの送信をまねすれば良かろう。良いはずだ。

 

あれこれ調べながらやっているうちに寝る時間になってしまったじゃないか!
とりあえず、Activityの onCreate()でファイルを作って、ボタンを押したら共有のダイアログ?が出てくるところまで確認した。

01:     fun sendIntent(view: View?) {
02:         val filename = "myfile.txt"
03:         val pathUri: Uri = FileProvider.getUriForFile(
04:             this,
05:             BuildConfig.APPLICATION_ID + ".fileprovider",
06:             File(getFilesDir(), filename)
07:         )
08:         val shareIntent: Intent = Intent().apply {
09:             action = Intent.ACTION_SEND
10:             putExtra(Intent.EXTRA_STREAM, pathUri)
11:             type = "plain/text"
12:         }
13:         startActivity(Intent.createChooser(shareIntent, null))
14:     }

なんで Googleドライブに保存するところまで確認しなかったかというと、エミュレータを使っていて Googleアカウントの設定がされていなかったためだ。

次回は実機でやろう。
それに、これを実行しても logcatには例外が出ていたので、そこも気になる。動いてるけど出ているから、よろしくはないのだろう。

2021/06/02

[android] テキストファイルをGoogleドライブに置きたい (1)

そう、ただそれだけのことだ。Android といえば Googleだから、Googleドライブに置くだけだったら簡単に違いない。

それがどういう処理なのか考えていなかったのだが、ファイル共有に相当するようだった。

ファイル共有の設定  |  Android デベロッパー  |  Android Developers
https://developer.android.google.cn/training/secure-file-sharing/setup-sharing?hl=ja

ローカルファイルのパスを渡しておしまい、とはいかないようだ。


いや、その前にこれを見つけた。

他のアプリへの単純なデータの送信  |  Android デベロッパー  |  Android Developers
https://developer.android.google.cn/training/sharing/send?hl=ja

単純こそ求めるものなのだよ。

テキストコンテンツとバイナリコンテンツがある。
テキストファイルをファイルのままで渡したい場合にはどうなるのだろうか?
ここの例に出てくるのは「コンテンツ」としてのデータなのだと思うが、私はあくまでファイルとして渡したいのだ。

 

そうなんだよ。タイトルも「データの送信」って書いてあるじゃないか。
となると、やはりファイルの共有を読むことになるだろう。


いろいろ設定がいる。

  • AndroidManifest.xml に FileProvider を追加
  • res/xml にファイルパス設定をする XMLファイルを追加
    • このファイルは AndroidManifest.xml で指定することになる
  • 実装する

 

AndroidManifest.xml は、まあなんとなくいいとしよう。
設定は必要だ。

<paths> の設定をする XMLファイル。
これは、共有ファイルを格納するディレクトリを指定するもののようだ。最初からディレクトリを指定しておくというのはわかる。
よくわからないのが、パスセグメント、というやつだ。

<files-path path="images/" name="myimages" />

attributeの pathに指定するのがアプリのデータディレクトリ/files の下に配置するディレクトリ名で、この中に入っているファイルが共有されるのだろう。そして name がパスセグメントというものなのだろう。 コンテンツURIに追加すると言われても、このページの中ではそれ以上の説明がないのだ。

content://com.example.myapp.fileprovider/myimages/default_image.jpg

この例からすると、 path の方が物理デバイスのディレクトリ名で、 name の方が URIのコンテンツ名のディレクトリ名ということなのかね。


ともかく、これで準備だけはできたようだ。
あとはファイルを置いて共有するだけなのだが・・・

ファイルの共有  |  Android デベロッパー  |  Android Developers
https://developer.android.google.cn/training/secure-file-sharing/share-file?hl=ja

ファイルの共有って、共有アイコンをタップしたときに下からするっと選択画面が出てくるあれではないのだろうか?
さっきの「データの送信」の方はそういう画面例が出ていたのだが、ああいうのがほしいのだ。

共有ファイルのリクエスト  |  Android デベロッパー  |  Android Developers
https://developer.android.google.cn/training/secure-file-sharing/request-file?hl=ja

よし、続きは後日だ。