Site cover image

ふつうのITエンジニアの独り言

本業はAndroidとiPhoneのアプリ開発のエンジニアです。将来はフリーランスで海の近くで妻とのんびり暮らすことを夢見て、幅広くIT技術に触れていきたいと思います。このブログはその備忘録と私のポートフォリオとして活動記録を記すものです。

Macで動作するAndroidのDevice ExplorerのようなアプリをPythonで自作する(その2)

目次


はじめに


 前回の記事で、PythonでADB File Controllerなるアプリを作りました。Macに接続したAndroidデバイスからADBコマンドを利用してファイル情報を取得して表示したり、ダウンロードしたりできるアプリです。

 それなりに動くものは作れたものの、まだまだ改善したいことがあったので、今回は気になる箇所を修正して最終的にはアプリ化まで実施したので、変更点とアプリ化の方法を紹介します。

変更後のアプリの全体像

変更点①:アイコンを表示


TKinterのTreeViewにアイコン表示

 ディレクトリ構造ツリーのアイテム名の前にアイコンを表示するようにしました。TkinterのTreeViewでアイコンを表示するには、PhotoImageでアイコンを取り込んで、treeに渡すことで簡単に表示することができます。

 まず、表示したいアイコンを用意してプロジェクト内に配置します。私はAssetsというフォルダを用意して、その中にpngファイルをすべて置きました。

アイコンを追加したところ
from tkinter import ttk, PhotoImage

folder_icon = PhotoImage(file=resource_path("Assets/folder_16.png"))
tree.insert(item_id, 'end', text=item_name, values=item_infos, image=folder_icon) 
treeの要素の一つにアイコンを設定する例

 アイテムの種類は、フォルダ(閉じる/開く) + シンボリックリンク + ファイルの計4種類を用意し、adb ls -lコマンドで得られるtypeから表示するアイコンを切り替える処理を行います。フォルダのアイコンについては、ツリーの開閉に合わせて動的にアイコンを切り替えようにしていますが、TreeViewでは動的な変更にも対応してくれます。具体的には、変更したいツリーアイテムのIDと変更後の画像をPhotoImageをtree.itemの引数に渡すことで切り替えられるので、開閉のイベント発生時に動的に変更するようにしました。

tree.item(item_id, image=self.folder_icon)
treeの任意のアイテムのアイコンを動的に変更する例

 今回利用したアイコンは16 x 16サイズを利用したので問題にならなかったのですが、大きなサイズの画像をアイコンにしようとすると、そのままでは表示が見切れてしまいます。なので、目的のサイズに変更してから利用すれば良いと思います(たぶん)。TKinterで画像のサイズを変更する方法は、こちらの記事が参考になりそうです。

右クリックメニューにアイコン表示

 つづいて、右クリックで表示されるメニューにもアイコンを表示しました。メニューにアイコンを表示するには、メニューのadd_commandの引数でPhotoImageを渡すことで実現できます。ここで、compound="left" の指定がないとラベルなしのアイコンだけの表示になります。

メニューにアイコン追加
from tkinter import Menu, PhotoImage

context_menu = Menu(self, tearoff=0)
context_menu.add_command(label='Android -> PC', command=self.on_select_download, image=self.download_icon, compound="left")

アイコン画像の入手

 アイコンはこちらのサイトで公開されているものを利用させていただきました。

変更点②:フォルダの新規作成


 右クリックのメニューに[make folder]を選択できるようにして、任意のフォルダ内に新しいフォルダを作成できるようにしました。フォルダの作成はmkdirコマンドで実施します。

make folderで”new_folder”フォルダを生成する。
adb -s <デバイスID> shell mkdir <dstパス>
mkdirのフォーマット

変更点③:シンボリックリンクの表示改善


リンク先のディレクトリ判定

 ls -lコマンドを実行すると、パーミッションが”lrw-r—r—”と表示されるアイテムがあります。この最初に表示されている文字がタイプを表しており、’l’はシンボリックリンクを意味します。Windowsでいうところのショートカットです。このシンボリックリンクを表示するにあたり、リンク先がディレクトリかファイルかを判定する必要がありました。ディレクトリの場合はツリーを展開できるようにする必要があり、逆にファイルの場合は展開ができないようにしたいためです。シンボリックリンクはそれだけではリンク先を判断することができないので、ls -ldコマンドを利用して判定を行います。

binはディレクトリ、acpiはファイルへのシンボリックリンク
adb -s <デバイスID> shell ls -ld <dstパス>/
指定パスのディレクトリ情報を表示する

 ここで工夫するのが、パスの最後に必ず”/”をつけることです。”/”をつけると、そのパスはディレクトリであることを明示的に指示することになります。これによって指定パスがディレクトリの場合には情報が表示され、逆にファイルの場合はエラーとなります。

% ls -ld bin/
drwxr-x--x 4 root shell 8192 2009-01-01 09:00 bin/

% ls -ld link/
ls: link/: No such file or directory
ls -ldをディレクトリとファイルに実行したときの出力例

大量ファイルの一括判定

 シンボリックリンクの判定処理を行うにあたって、フォルダ内に大量にシンボリックリンクが含まれるケースで躓くことになりました。というのも、フォルダ内のファイルを一個ずつls -ldコマンドで確認すると秒オーダーの時間が掛かってしまうという問題が見つかったためです。リンク数にもよりますが10秒を超えるケースもありました。これを解決する手段として考えたのが、ls -ldコマンドの引数に複数のパスを渡すというものです。lsコマンドには複数のパスを渡すことができ、これらを一括して処理してくれます。この工夫によって10秒→1秒未満にまで短縮することができました。

adb -s <デバイスID> shell ls -ld <dstパス1>/ <dstパス2>/ <dstパス3>/
ls -ldに複数のパスを渡して一括処理させる

 このコマンドで得られる出力は、指定したパスの順番とは関係なく出力されます。そのため、出力結果からディレクトリ名/フォルダ名を抽出する必要があります。出力から名前を抽出するには、正規表現を使うと簡単です。

pattern = r'(.)(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2})\s+(.+)/'
folder_info = re.findall(pattern, "drwxr-x--x 4 root shell 8192 2009-01-01 09:00 bin/")

print(folder_info[0][8]) # ディレクトリ名を表示
出力からディレクトリ名を正規表現で抽出するPythonコード
pattern = r'ls: (.+)/:.*'
file_info = re.findall(pattern, "ls: link/: No such file or directory")

print(file_info[0]) # ファイル名を表示
出力からファイル名を正規表現で抽出するPythonコード

Pyinstallerでアプリ化する


Pyinstallerコマンド

 作成したアプリを、MacのDockに入れていつでも実行できるようにするためにappにしてみました。app化するにあたり、利用したのはPyinstallerです。Pyinstallerの導入方法についての説明は省略しますが、以下のコマンドでapp化することができました。

pyinstaller --add-data "Assets:Assets" --add-binary "resources/adb:resources" --noconsole --icon=icon.icns ADBFileController.py
pyinstallerでADBFileControllerをアプリ化するコマンド
  • —add-dataオプションでアイコン画像が保存されているAssetsフォルダをアプリに取り込みます。
  • —add-binaryオプションでadb実行ファイルをアプリに取り込みます。
  • —noconsoleオプションで実行ファイルをバンドルします。
  • —iconオプションでアプリアイコンを指定します。

コマンドで生成されたADBFileController.appは、Macのアプリケーションフォルダに入れることで、Launchpad上で普通のアプリと同じ用に利用できるようになります。

左下のアプリがADBFileController

adb実行ファイルの取り込み

 今回のapp化では--add-binaryオプションでadbコマンドの実行ファイルをアプリに直接埋め込みました。理由は、アプリ化するとadbコマンドのパスが見つからず、実行できない状況となったためです。この解決方法として、adbコマンドへの絶対パスを指定するという方法もありますが、将来的にコードをGithubで配信するとなった場合に、私の環境に依存する情報は埋め込みたくありませんでした。

 そこで、adbの実行ファイルをwhichコマンドで探し、プロジェクト内のresourcesフォルダにコピーしました。また、adbのパスは開発環境とapp実行時で変化するため、パスは動的に取得するように変更しました。

% which adb
/Users/<user>/Library/Android/sdk/platform-tools/adb
Mac内のadbコマンドを見つけるコマンド
import sys

def get_resource_path(relative_path):
    if hasattr(sys, '_MEIPASS'):
        return os.path.join(sys._MEIPASS, relative_path)
    return os.path.join(os.path.abspath("."), relative_path)
adb_path = get_resource_path("resources/adb")
プロジェクト内に置いたadbへの絶対パスを取得するPythonコード

設定ファイルの保存先変更

 app化するにあたって、設定保存ファイルの保存先を変更しました。以前の保存先はプロジェクト内のsettingsフォルダに保存していたのですが、app化した時にフォルダへのアクセスエラーが発生しました。当然、appではプロジェクトというものが無いため、フォルダを作れないことが原因となります。実は、この問題の原因に気がつくまでに結構な時間が掛かりました。というのも、app化するとコンソールが表示されないためエラーの発生箇所がわからないためです。そこで、問題箇所を特定するために、Macのログを確認することにしました。

% log show --predicate 'eventMessage contains "ADBFileController"' --info --last 1h
デバッグのために実行したログ出力コマンド

 ログを確認することで設定ファイルの扱いが原因であることが特定できたので、設定ファイルの保存先に一般的にアプリが利用する”/Users/<username>/Library/Application Support”直下にアプリディレクトリを作成して保存することにしました。

アプリアイコン用意

 Macアプリで表示するアイコンとして1024 x 1024のpngを用意しました。Mac上で動くAndroidのファイルビューアなので、安直にドロイド君にリンゴを持たせておきました。Appleロゴをもたせたアイコンも作ったのですが、Appleのロゴ使用ガイドラインに抵触することを恐れて、普通のリンゴにしました。

icns作成

 pyinstallerでアプリアイコンを設定するにはicnsを作成する必要がありました。icon.iconsetというフォルダを用意して、その中に16x16から1024x1024までの複数種類のサイズのアイコンを格納します。1024 x 1024のicon.pngを用意したので、ターミナルでコマンドを実行することで、複数種類のアイコンが生成できます。画像が用意できたら、iconutilコマンドでicnsを生成します。

sips -z 16 16     icon.png --out icon.iconset/icon_16x16.png
sips -z 32 32     icon.png --out icon.iconset/icon_16x16@2x.png
sips -z 32 32     icon.png --out icon.iconset/icon_32x32.png
sips -z 64 64     icon.png --out icon.iconset/icon_32x32@2x.png
sips -z 128 128   icon.png --out icon.iconset/icon_128x128.png
sips -z 256 256   icon.png --out icon.iconset/icon_128x128@2x.png
sips -z 256 256   icon.png --out icon.iconset/icon_256x256.png
sips -z 512 512   icon.png --out icon.iconset/icon_256x256@2x.png
sips -z 512 512   icon.png --out icon.iconset/icon_512x512.png
sips -z 1024 1024 icon.png --out icon.iconset/icon_512x512@2x.png
画像のリサイズする
iconutil -c icns icon.iconset
icnsを生成する

作成したicnsは、pyinstallerコマンドで—iconオプションに使用しました。

まとめ


 今回はアプリの細かい表示仕様にこだわって改造を行った。特に苦労したのはシンボリックリンクの取り扱いで、ディレクトリ判定と対象のシンボリックリンクを効率よく処理する方法を解決する方法を見つけるまでに多大な時間がかかった。そのかいあってアプリの表示は個人的には大満足な仕上がりとなった。また、アプリ化に当たっても、出力した実行ファイルであれば動作するが、バンドル化した途端に動作しないという現象が発生して、原因を特定するのにも苦労した。結果として設定ファイルの保存先に問題が見つかったのだが、この過程でMacのログを確認する方法を覚えたし、一般的なアプリの設定ファイルの保存先も知ることができたので結果的には良かったと思う。

 ここまでで作成したソースコードですが、まだまだ公開できたものではないので、コードを見直して最終的にはGithubで公開したいと思います。