kivyLauncher + zxing でバーコードスキャン、で「持ってるもの管理」

kivyLauncher + zxing でバーコードスキャン、事始」を完遂。

kivyLauncher + zxing でバーコードスキャン、事始」で書いた動機の通りで、店頭で「あれれこれ持ってたっけ持ってなかったっけ」のための。特にアレだ、「初回限定版」なんぞを偶然中古ショップで見つけてしまった日によく困るわけだ。

いきなり完成品コードをば:

android.txt
1 title=Have you ever been purchased this?
2 author=hhsprings
3 orientation=all

ean_query.py: インチキ EAN lookup

local_store.py
 1 import os
 2 import sqlite3
 3 import ean_query
 4 
 5 _DBFILE = "./local_store.dat"
 6 _created = os.path.exists(_DBFILE)
 7 
 8 class LocalStore(object):
 9     def __init__(self):
10         self._conn = sqlite3.connect(_DBFILE)
11 
12         if not _created:
13             cur = self._conn.cursor()
14             cur.executescript('''\
15         CREATE TABLE t_local_store (
16             ean TEXT NOT NULL,
17             url TEXT,
18             title TEXT);
19         ''')
20             self._conn.commit()
21             cur.close()
22 
23     def insert_new(self, ean):
24         if not self.query(ean):
25             qr = ean_query.query(ean)
26             self._insert_new(ean, qr)
27         return self.query(ean)
28 
29     def _insert_new(self, ean, qr):
30         cur = self._conn.cursor()
31         for r in qr:
32             cur.execute(
33                 """INSERT INTO t_local_store(ean, url, title)
34                    VALUES (?, ?, ?)""", (ean, r['href'], r['data']))
35         self._conn.commit()
36         cur.close()
37 
38     def query(self, ean):
39         result = []
40 
41         cur = self._conn.cursor()
42         cur.execute("""SELECT url, title FROM t_local_store
43                        WHERE ean = ?""", (ean,))
44         for row in cur:
45             url, title = row
46             result.append({'url': url, 'title': title})
47         cur.close()
48 
49         return result
50 
51     def delete_ean(self, ean):
52         cur = self._conn.cursor()
53         cur.execute("""DELETE FROM t_local_store
54                        WHERE ean = ?""", (ean,))
55         cur.close()
56 
57 if __name__ == '__main__':
58     ean = "4988031146163"  #"4988013655645"
59     store = LocalStore()
60     qr = list(store.query(ean))
61     if not qr:
62         qr = store.insert_new(ean)
63     print(qr)
main.py
  1 # -*- coding: utf-8 -*-
  2 from kivy.lang import Builder
  3 from kivy.app import App
  4 from jnius import autoclass, cast
  5 from kivy.clock import mainthread
  6 from android import activity
  7 from kivy.logger import Logger
  8 from kivy.properties import StringProperty
  9 from local_store import LocalStore
 10 
 11 kv = u'''
 12 BoxLayout:
 13     orientation: 'vertical'
 14 
 15     Button:
 16         id: button_1
 17         text: 'これ持ってたっけ?'
 18         on_release: app.on_btn_query_purchased()
 19         size_hint: (1, None)
 20         height: self.texture_size[1] * 3
 21 
 22     RstDocument:
 23         id: info
 24         text: app.info
 25         size_hint: (1, None)
 26         height: root.height - button_1.height - button_2.height * 2
 27 
 28     BoxLayout:
 29         id: bottom
 30         orientation: 'vertical'
 31 
 32         BoxLayout:
 33             orientation: 'horizontal'
 34             Label:
 35                 text: app.last_ean
 36                 size_hint: (1, None)
 37                 height: button_2.height
 38             Button:
 39                 id: button_3
 40                 text: 'クリア'
 41                 size_hint: (1, None)
 42                 height: self.texture_size[1] * 3
 43                 on_release: app.last_ean = ''
 44         BoxLayout:
 45             orientation: 'horizontal'
 46             Button:
 47                 id: button_2
 48                 text: '持ってるものとして記録'
 49                 on_release: app.on_btn_record_purchased()
 50                 size_hint: (1, None)
 51                 height: self.texture_size[1] * 3
 52             Button:
 53                 id: button_3
 54                 text: '記録から削除'
 55                 on_release: app.on_btn_delete_purchased()
 56                 size_hint: (1, None)
 57                 height: self.texture_size[1] * 3
 58 
 59 '''
 60 from kivy.core.text import LabelBase, DEFAULT_FONT
 61 # please download font from:
 62 #     http://www.google.com/get/noto/
 63 # and locate it to proper directory.
 64 LabelBase.register(DEFAULT_FONT, '/sdcard/site-local-fonts/NotoSansCJKjp-Regular.otf')
 65 
 66 
 67 Intent = autoclass('android.content.Intent')
 68 PythonActivity = autoclass('org.renpy.android.PythonActivity')
 69 
 70 class DoYouHaveThisApp(App):
 71     _store = LocalStore()
 72     info = StringProperty()
 73     last_ean = StringProperty()
 74 
 75     def build(self):
 76         activity.bind(on_activity_result=self.on_activity_result)
 77         #
 78         return Builder.load_string(kv)
 79 
 80     def on_btn_query_purchased(self):
 81         intent = Intent("com.google.zxing.client.android.SCAN")
 82         intent.setPackage("com.google.zxing.client.android")
 83         intent.putExtra("SCAN_MODE", "PRODUCT_MODE")
 84         PythonActivity.mActivity.startActivityForResult(intent, 0)
 85 
 86     def on_btn_record_purchased(self):
 87         if not self.last_ean:
 88             intent = Intent("com.google.zxing.client.android.SCAN")
 89             intent.setPackage("com.google.zxing.client.android")
 90             intent.putExtra("SCAN_MODE", "PRODUCT_MODE")
 91             PythonActivity.mActivity.startActivityForResult(intent, 1)
 92         else:
 93             self.on_activity_result_record_purchased(self.last_ean)
 94 
 95     def on_btn_delete_purchased(self):
 96         if self.last_ean:
 97             self._store.delete_ean(self.last_ean)
 98             self.info = u"""
 99 記録から削除しました
100 ====================
101 {}
102 """.format(self.last_ean)
103 
104     @mainthread
105     def on_activity_result_query_purchased(self, ean):
106         self.last_ean = ean
107         qr = self._store.query(ean)
108         if qr:
109             s = u"""
110 既に持ってるものとして記録済み
111 ==============================
112 
113 """
114             s += u"\n".join([u"* " + _['title'] for _ in qr])
115             self.info = s
116         else:
117             self.info = "あなたこれ持ってない: {}".format(ean)
118 
119     @mainthread
120     def on_activity_result_record_purchased(self, ean):
121         qr = self._store.insert_new(ean)
122         if qr:
123             s = u"""
124 持ってるものとして記録
125 ======================
126 
127 """
128             s += u"\n".join([u"* " + _['title'] for _ in qr])
129             self.info = s
130 
131     def on_activity_result(self, requestCode, resultCode, intent):
132         Logger.info('requestCode={}, resultCode={}'.format(
133                 requestCode, resultCode))
134         if requestCode not in (0, 1):
135             return
136         contents = intent.getStringExtra("SCAN_RESULT")
137         format = intent.getStringExtra("SCAN_RESULT_FORMAT")
138         Logger.info('contents={}, format={}'.format(contents, format))
139 
140         if requestCode == 0:
141             self.on_activity_result_query_purchased(contents)
142         else:
143             self.on_activity_result_record_purchased(contents)
144 
145     def on_pause(self):
146         return True
147 
148 
149 if __name__ == '__main__':
150     DoYouHaveThisApp().run()

こんな画面:

「これ持ってたっけ?」ボタンを押すとバーコードスキャナ起動。これでバーコードを読み取って戻ってくると、持ってたか持ってなかったかが真ん中の RstDocument に。

キャプチャで 「4988031146163」になってる部分が読み取れた製品コードで、この状態で「持ってるものとして記録」ボタンで Sqlite に記録。また「これ持ってたっけ?」ボタン経由しないか、クリアボタンでこのラベルを空にした状態で「持ってるものとして記録」ボタンだと、同じくバーコードスキャナが起動して、この場合はそのまま Sqlite に記録。「記録から削除」ボタンは文字通り。

みての通りで、あきらかにいらないデータがヒットしているけれど、これは「インチキ EAN ルックアップ」の宿命。まぁ「あんたナニモノ?」が EAN コードだけでおよそわかればいい、ってノリなんで、まぁいいか、て感じ。







作るのに困った点がいくつか。

一つ目。まずは on_pause の件。「kivyLauncher + zxing でバーコードスキャン、事始」ではログとしてはちゃんと on_activity_result に戻ってきてたのでいらないと思ってたが、ないとダメだった。

二つ目。sqlite の制限について。これ:

 1 Traceback (most recent call last):
 2   File "jnius/jnius_proxy.pxi", line 47, in jnius.jnius.PythonJavaClass.invoke (jnius/jnius.c:29223)
 3   File "jnius/jnius_proxy.pxi", line 73, in jnius.jnius.PythonJavaClass._invoke (jnius/jnius.c:29924)
 4   File "/home/tito/code/python-for-android-upstream/build/python-install/lib/python2.7/site-packages/android/activity.py", line 32, in onActivityResult
 5   File "main.py", line 63, in on_activity_result
 6     Logger.info(self._store.insert_new(contents))
 7   File "/storage/emulated/0/kivy/zxing_barcode_0/local_store.py", line 25, in insert_new
 8     self._insert_new(ean, qr)
 9   File "/storage/emulated/0/kivy/zxing_barcode_0/local_store.py", line 29, in _insert_new
10     cur = self._conn.cursor()
11 ProgrammingError: SQLite objects created in a thread can only be used in that same thread.The object was created in thread id -517703376 and this is thread id -150914252

同一スレッドに閉じないとダメ。なので main.py に「@mainthread」入れてる。

三つ目。zxing 以外が反応しうる、という点。なんだか QPython が反応しちゃって、「どっち使う?」と聞かれるようになってた。ので:

1     def on_btn_query_purchased(self):
2         intent = Intent("com.google.zxing.client.android.SCAN")
3         intent.setPackage("com.google.zxing.client.android")
4         intent.putExtra("SCAN_MODE", "PRODUCT_MODE")
5         PythonActivity.mActivity.startActivityForResult(intent, 0)

四つ目。日本語フォント問題。これは気持ち悪い対処だった…。

1 from kivy.core.text import LabelBase, DEFAULT_FONT
2 # please download font from:
3 #     http://www.google.com/get/noto/
4 # and locate it to proper directory.
5 LabelBase.register(DEFAULT_FONT, '/sdcard/site-local-fonts/NotoSansCJKjp-Regular.otf')

としてる部分なんだけれども、これ、Google Noto Fontsからわざわざダウンロードして置いてる。けどなんで? アタシのスマホは android 6.0 で、 /system/fonts/NotoSansJP-Regular.otf なんてのは元から入ってたのね。なんだけどこれ、日本語部分以外が全部化ける。んなアホな…。(日本語が化けないならまだわかるんだけど、日本語もところどころ化ける。化ける、というのは、四角にバッテン。) うーん、日本製(SHARP Aquos)なのにもとから標準で使えるものってないもんなの? ほかのアプリってフォントも自分でまかなってんのかしらん?

以上4点だけ苦労した…、けどまぁほぼ一日で出来た。まぁチョロいちゃぁチョロい。







「EAN」言うておるけれど、実際 ISBN を扱えるようにするのも別に難しくないし、今は「バーコード」だけ扱ってるけど、zxing は普通に QR コードも扱えるので…、まぁもっと拡張できますよん。もし欲しかったら自分で追加してみたら?